Merge pull request #1735 from deltachat/invite-call-api

add APIs for videochats
This commit is contained in:
bjoern
2020-07-26 18:11:02 +02:00
committed by GitHub
14 changed files with 352 additions and 15 deletions

View File

@@ -318,11 +318,11 @@ char* dc_get_blobdir (const dc_context_t* context);
* The library uses the `media_quality` setting to use different defaults
* for recoding images sent with type DC_MSG_IMAGE.
* If needed, recoding other file types is up to the UI.
* - `basic_web_rtc_instance` = address to webrtc signaling server (https://github.com/cracker0dks/basicwebrtc)
* that should be used for opening video hangouts.
* This property is only used in the UIs not by the core itself.
* Format: https://example.com/subdir
* The other properties that are needed for a call such as the roomname will be set by the client in the anchor part of the url.
* - `webrtc_instance` = webrtc instance to use for videochats in the form
* `[basicwebrtc:]https://example.com/subdir#roomname=$ROOM`
* if the url is prefixed by `basicwebrtc`, the server is assumed to be of the type
* https://github.com/cracker0dks/basicwebrtc which some UIs have native support for.
* If no type is prefixed, the videochat is handled completely in a browser.
*
* If you want to retrieve a value, use dc_get_config().
*
@@ -839,6 +839,41 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
uint32_t dc_send_text_msg (dc_context_t* context, uint32_t chat_id, const char* text_to_send);
/**
* Send invitation to a videochat.
*
* This function reads the `webrtc_instance` config value,
* may check that the server is working in some way
* and creates a unique room for this chat, if needed doing a TOKEN roundtrip for that.
*
* After that, the function sends out a message that contains information to join the room:
*
* - To allow non-delta-clients to join the chat,
* the message contains a text-area with some descriptive text
* and a url that can be opened in a supported browser to join the videochat
*
* - delta-clients can get all information needed from
* the message object, using eg.
* dc_msg_get_videochat_url() and check dc_msg_get_viewtype() for DC_MSG_VIDEOCHAT_INVITATION
*
* dc_send_videochat_invitation() is blocking and may take a while,
* so the UIs will typically call the function from within a thread.
* Moreover, UIs will typically enter the room directly without an additional click on the message,
* for this purpose, the function returns the message-id directly.
*
* As for other messages sent, this function
* sends the event #DC_EVENT_MSGS_CHANGED on succcess, the message has a delivery state, and so on.
* The recipient will get noticed by the call as usual by DC_EVENT_INCOMING_MSG or DC_EVENT_MSGS_CHANGED,
* However, UIs might some things differently, eg. play a different sound.
*
* @param context The context object.
* @param chat_id The chat to start a videochat for.
* @return The id if the message sent out
* or 0 for errors.
*/
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
/**
* Save a draft for a chat in the database.
*
@@ -3240,6 +3275,55 @@ int dc_msg_is_setupmessage (const dc_msg_t* msg);
char* dc_msg_get_setupcodebegin (const dc_msg_t* msg);
/**
* Get url of a videochat invitation.
*
* Videochat invitations are sent out using dc_send_videochat_invitation()
* and dc_msg_get_viewtype() returns #DC_MSG_VIDEOCHAT_INVITATION for such invitations.
*
* @param msg The message object.
* @return If the message contains a videochat invitation,
* the url of the invitation is returned.
* If the message is no videochat invitation, NULL is returned.
* Must be released using dc_str_unref() when done.
*/
char* dc_msg_get_videochat_url (const dc_msg_t* msg);
/**
* Get type of videochat.
*
* Calling this functions only makes sense for messages of type #DC_MSG_VIDEOCHAT_INVITATION,
* in this case, if "basic webrtc" as of https://github.com/cracker0dks/basicwebrtc was used to initiate the videochat,
* dc_msg_get_videochat_type() returns DC_VIDEOCHATTYPE_BASICWEBRTC.
* "basic webrtc" videochat may be processed natively by the app
* whereas for other urls just the browser is opened.
*
* The videochat-url can be retrieved using dc_msg_get_videochat_url().
* To check if a message is a videochat invitation at all, check the message type for #DC_MSG_VIDEOCHAT_INVITATION.
*
* @param msg The message object.
* @return Type of the videochat as of DC_VIDEOCHATTYPE_BASICWEBRTC or DC_VIDEOCHATTYPE_UNKNOWN.
*
* Example:
* ~~~
* if (dc_msg_get_viewtype(msg) == DC_MSG_VIDEOCHAT_INVITATION) {
* if (dc_msg_get_videochat_type(msg) == DC_VIDEOCHATTYPE_BASICWEBRTC) {
* // videochat invitation that we ship a client for
* } else {
* // use browser for videochat, just open the url
* }
* } else {
* // not a videochat invitation
* }
* ~~~
*/
int dc_msg_get_videochat_type (const dc_msg_t* msg);
#define DC_VIDEOCHATTYPE_UNKNOWN 0
#define DC_VIDEOCHATTYPE_BASICWEBRTC 1
/**
* Set the text of a message object.
* This does not alter any information in the database; this may be done by dc_send_msg() later.
@@ -3775,6 +3859,18 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
*/
#define DC_MSG_FILE 60
/**
* Message indicating an incoming or outgoing videochat.
* The message was created via dc_send_videochat_invitation() on this or a remote device.
*
* Typically, such messages are rendered differently by the UIs,
* eg. contain a button to join the videochat.
* The url for joining can be retrieved using dc_msg_get_videochat_url().
*/
#define DC_MSG_VIDEOCHAT_INVITATION 70
/**
* @}
*/
@@ -4517,8 +4613,10 @@ void dc_event_unref(dc_event_t* event);
#define DC_STR_EPHEMERAL_DAY 79
#define DC_STR_EPHEMERAL_WEEK 80
#define DC_STR_EPHEMERAL_FOUR_WEEKS 81
#define DC_STR_VIDEOCHAT_INVITATION 82
#define DC_STR_VIDEOCHAT_INVITE_MSG_BODY 83
#define DC_STR_COUNT 81
#define DC_STR_COUNT 83
/*
* @}

View File

@@ -710,6 +710,25 @@ pub unsafe extern "C" fn dc_send_text_msg(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_send_videochat_invitation(
context: *mut dc_context_t,
chat_id: u32,
) -> u32 {
if context.is_null() {
eprintln!("ignoring careless call to dc_send_videochat_invitation()");
return 0;
}
let ctx = &*context;
block_on(async move {
chat::send_videochat_invitation(&ctx, ChatId::new(chat_id))
.await
.map(|msg_id| msg_id.to_u32())
.unwrap_or_log_default(&ctx, "Failed to send video chat invitation")
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_set_draft(
context: *mut dc_context_t,
@@ -2820,6 +2839,31 @@ pub unsafe extern "C" fn dc_msg_is_setupmessage(msg: *mut dc_msg_t) -> libc::c_i
ffi_msg.message.is_setupmessage().into()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_videochat_url(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_videochat_url()");
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg
.message
.get_videochat_url()
.unwrap_or_default()
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_videochat_type(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_videochat_type()");
return 0;
}
let ffi_msg = &*msg;
ffi_msg.message.get_videochat_type().unwrap_or_default() as i32
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_setupcodebegin(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {

View File

@@ -183,7 +183,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
let temp2 = dc_timestamp_to_str(msg.get_timestamp());
let msgtext = msg.get_text();
println!(
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{} [{}]",
"{}{}{}{}: {} (Contact#{}): {} {}{}{}{}{}{} [{}]",
prefix.as_ref(),
msg.get_id(),
if msg.get_showpadlock() { "🔒" } else { "" },
@@ -202,6 +202,15 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
"[FRESH]"
},
if msg.is_info() { "[INFO]" } else { "" },
if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!(
"[VIDEOCHAT-INVITATION: {}, type={}]",
msg.get_videochat_url().unwrap_or_default(),
msg.get_videochat_type().unwrap_or_default()
)
} else {
"".to_string()
},
if msg.is_forwarded() {
"[FORWARDED]"
} else {
@@ -359,6 +368,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
send-garbage\n\
sendimage <file> [<text>]\n\
sendfile <file> [<text>]\n\
videochat\n\
draft [<text>]\n\
devicemsg <text>\n\
listmedia\n\
@@ -808,6 +818,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"videochat" => {
ensure!(sel_chat.is_some(), "No chat selected.");
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
}
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");

View File

@@ -158,7 +158,7 @@ const DB_COMMANDS: [&str; 9] = [
"housekeeping",
];
const CHAT_COMMANDS: [&str; 26] = [
const CHAT_COMMANDS: [&str; 27] = [
"listchats",
"listarchived",
"chat",
@@ -178,6 +178,7 @@ const CHAT_COMMANDS: [&str; 26] = [
"send",
"sendimage",
"sendfile",
"videochat",
"draft",
"listmedia",
"archive",

View File

@@ -1374,11 +1374,12 @@ pub(crate) fn msgtype_has_file(msgtype: Viewtype) -> bool {
Viewtype::Voice => true,
Viewtype::Video => true,
Viewtype::File => true,
Viewtype::VideochatInvitation => false,
}
}
async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<(), Error> {
if msg.viewtype == Viewtype::Text {
if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation {
// the caller should check if the message text is empty
} else if msgtype_has_file(msg.viewtype) {
let blob = msg
@@ -1611,6 +1612,44 @@ pub async fn send_text_msg(
send_msg(context, chat_id, &mut msg).await
}
pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Result<MsgId, Error> {
ensure!(
!chat_id.is_special(),
"video chat invitation cannot be sent to special chat: {}",
chat_id
);
let instance = if let Some(instance) = context.get_config(Config::WebrtcInstance).await {
if !instance.is_empty() {
instance
} else {
bail!("webrtc_instance is empty");
}
} else {
bail!("webrtc_instance not set");
};
let room = dc_create_id();
let instance = if instance.contains("$ROOM") {
instance.replace("$ROOM", &room)
} else {
format!("{}{}", instance, room)
};
let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.param.set(Param::WebrtcRoom, &instance);
msg.text = Some(
context
.stock_string_repl_str(
StockMessage::VideochatInviteMsgBody,
Message::parse_webrtc_instance(&instance).1,
)
.await,
);
send_msg(context, chat_id, &mut msg).await
}
pub async fn get_chat_msgs(
context: &Context,
chat_id: ChatId,

View File

@@ -123,12 +123,8 @@ pub enum Config {
/// because we do not want to send a second warning)
NotifyAboutWrongPw,
/// address to webrtc signaling server (https://github.com/cracker0dks/basicwebrtc)
/// that should be used for opening video hangouts.
/// This property is only used in the UIs not by the core itself.
/// Format: https://example.com/subdir
/// The other properties that are needed for a call such as the roomname will be set by the client in the anchor part of the url.
BasicWebRTCInstance,
/// address to webrtc instance to use for videochats
WebrtcInstance,
}
impl Context {

View File

@@ -84,6 +84,19 @@ impl Default for KeyGenType {
}
}
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql)]
#[repr(i8)]
pub enum VideochatType {
Unknown = 0,
BasicWebrtc = 1,
}
impl Default for VideochatType {
fn default() -> Self {
VideochatType::Unknown
}
}
pub const DC_HANDSHAKE_CONTINUE_NORMAL_PROCESSING: i32 = 0x01;
pub const DC_HANDSHAKE_STOP_NORMAL_PROCESSING: i32 = 0x02;
pub const DC_HANDSHAKE_ADD_DELETE_JOB: i32 = 0x04;
@@ -296,6 +309,9 @@ pub enum Viewtype {
/// The file is set via dc_msg_set_file()
/// and retrieved via dc_msg_get_file().
File = 60,
/// Message is an invitation to a videochat.
VideochatInvitation = 70,
}
impl Default for Viewtype {

View File

@@ -35,6 +35,7 @@ pub enum HeaderDef {
ChatContent,
ChatDuration,
ChatDispositionNotificationTo,
ChatWebrtcRoom,
Autocrypt,
AutocryptSetupMessage,
SecureJoin,

View File

@@ -638,6 +638,39 @@ impl Message {
None
}
/// split a webrtc_instance as defined by the corresponding config-value into a type and a url
pub fn parse_webrtc_instance(instance: &str) -> (VideochatType, String) {
let mut split = instance.splitn(2, ':');
let type_str = split.next().unwrap_or_default().to_lowercase();
let url = split.next();
if type_str == "basicwebrtc" {
(
VideochatType::BasicWebrtc,
url.unwrap_or_default().to_string(),
)
} else {
(VideochatType::Unknown, instance.to_string())
}
}
pub fn get_videochat_url(&self) -> Option<String> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
return Some(Message::parse_webrtc_instance(instance).1);
}
}
None
}
pub fn get_videochat_type(&self) -> Option<VideochatType> {
if self.viewtype == Viewtype::VideochatInvitation {
if let Some(instance) = self.param.get(Param::WebrtcRoom) {
return Some(Message::parse_webrtc_instance(instance).0);
}
}
None
}
pub fn set_text(&mut self, text: Option<String>) {
self.text = text;
}
@@ -1241,6 +1274,13 @@ pub async fn get_summarytext_by_raw(
format!("{} {}", label, file_name)
}
}
Viewtype::VideochatInvitation => {
append_text = false;
context
.stock_str(StockMessage::VideochatInvitation)
.await
.into_owned()
}
_ => {
if param.get_cmd() != SystemMessage::LocationOnly {
"".to_string()
@@ -1799,4 +1839,19 @@ mod tests {
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
);
}
#[async_std::test]
async fn test_parse_webrtc_instance() {
let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar");
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
assert_eq!(url, "https://foo/bar");
let (webrtc_type, url) = Message::parse_webrtc_instance("bAsIcwEbrTc:url");
assert_eq!(webrtc_type, VideochatType::BasicWebrtc);
assert_eq!(url, "url");
let (webrtc_type, url) = Message::parse_webrtc_instance("https://foo/bar?key=val#key=val");
assert_eq!(webrtc_type, VideochatType::Unknown);
assert_eq!(url, "https://foo/bar?key=val#key=val");
}
}

View File

@@ -875,6 +875,19 @@ impl<'a, 'b> MimeFactory<'a, 'b> {
if self.msg.viewtype == Viewtype::Sticker {
protected_headers.push(Header::new("Chat-Content".into(), "sticker".into()));
} else if self.msg.viewtype == Viewtype::VideochatInvitation {
protected_headers.push(Header::new(
"Chat-Content".into(),
"videochat-invitation".into(),
));
protected_headers.push(Header::new(
"Chat-Webrtc-Room".into(),
self.msg
.param
.get(Param::WebrtcRoom)
.unwrap_or_default()
.into(),
));
}
if self.msg.viewtype == Viewtype::Voice

View File

@@ -242,6 +242,19 @@ impl MimeMessage {
}
}
fn parse_videochat_headers(&mut self) {
if let Some(value) = self.get(HeaderDef::ChatContent).cloned() {
if value == "videochat-invitation" {
let instance = self.get(HeaderDef::ChatWebrtcRoom).cloned();
if let Some(part) = self.parts.first_mut() {
part.typ = Viewtype::VideochatInvitation;
part.param
.set(Param::WebrtcRoom, instance.unwrap_or_default());
}
}
}
}
/// Squashes mutlipart chat messages with attachment into single-part messages.
///
/// Delta Chat sends attachments, such as images, in two-part messages, with the first message
@@ -314,6 +327,7 @@ impl MimeMessage {
fn parse_headers(&mut self, context: &Context) -> Result<()> {
self.parse_system_message_headers(context)?;
self.parse_avatar_headers();
self.parse_videochat_headers();
self.squash_attachment_parts();
if let Some(ref subject) = self.get_subject() {
@@ -1449,6 +1463,28 @@ mod tests {
assert!(mimeparser.group_avatar.unwrap().is_change());
}
#[async_std::test]
async fn test_mimeparser_with_videochat() {
let t = TestContext::new().await;
let raw = include_bytes!("../test-data/message/videochat_invitation.eml");
let mimeparser = MimeMessage::from_bytes(&t.ctx, &raw[..]).await.unwrap();
assert_eq!(mimeparser.parts.len(), 1);
assert_eq!(mimeparser.parts[0].typ, Viewtype::VideochatInvitation);
assert_eq!(
mimeparser.parts[0]
.param
.get(Param::WebrtcRoom)
.unwrap_or_default(),
"https://example.org/p2p/?roomname=6HiduoAn4xN"
);
assert!(mimeparser.parts[0]
.msg
.contains("https://example.org/p2p/?roomname=6HiduoAn4xN"));
assert_eq!(mimeparser.user_avatar, None);
assert_eq!(mimeparser.group_avatar, None);
}
#[async_std::test]
async fn test_mimeparser_message_kml() {
let context = TestContext::new().await;

View File

@@ -68,6 +68,9 @@ pub enum Param {
/// For Messages
AttachGroupImage = b'A',
/// For Messages
WebrtcRoom = b'V',
/// For Messages: space-separated list of messaged IDs of forwarded copies.
///
/// This is used when a [crate::message::Message] is in the

View File

@@ -210,6 +210,12 @@ pub enum StockMessage {
#[strum(props(fallback = "Message deletion timer is set to 4 weeks."))]
MsgEphemeralTimerFourWeeks = 81,
#[strum(props(fallback = "Video chat invitation"))]
VideochatInvitation = 82,
#[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
VideochatInviteMsgBody = 83,
}
/*

View File

@@ -0,0 +1,15 @@
Content-Type: text/plain; charset=utf-8
Subject: Message from user
Message-ID: <Mr.f1O61111evx.ikocf333353@example.org>
Date: Mon, 20 Jul 2020 14:28:30 +0000
X-Mailer: Delta Chat Core 1.40.0/CLI
Chat-Version: 1.0
Chat-Content: videochat-invitation
Chat-Webrtc-Room: https://example.org/p2p/?roomname=6HiduoAn4xN
To: <tunis3@example.org>
From: "=?utf-8?q??=" <tunis4@example.org>
You are invited to an videochat, click https://example.org/p2p/?roomname=6HiduoAn4xN to join.
--
Sent with my Delta Chat Messenger: https://delta.chat