diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 4b0e526df..15fba4ab3 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -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 /* * @} diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 8578548f9..aa0442318 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -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() { diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index 4890ada73..b6fd67a3d 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -183,7 +183,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef, 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, 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 []\n\ sendfile []\n\ + videochat\n\ draft []\n\ devicemsg \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 missing."); diff --git a/examples/repl/main.rs b/examples/repl/main.rs index e53bed1c2..7be1c9bea 100644 --- a/examples/repl/main.rs +++ b/examples/repl/main.rs @@ -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", diff --git a/src/chat.rs b/src/chat.rs index f21ce144a..74af3f678 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -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 { + 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, diff --git a/src/config.rs b/src/config.rs index 31067a27f..6434f560b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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 { diff --git a/src/constants.rs b/src/constants.rs index e6489547c..eb9e2a6f8 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -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 { diff --git a/src/headerdef.rs b/src/headerdef.rs index d87929435..5743351e6 100644 --- a/src/headerdef.rs +++ b/src/headerdef.rs @@ -35,6 +35,7 @@ pub enum HeaderDef { ChatContent, ChatDuration, ChatDispositionNotificationTo, + ChatWebrtcRoom, Autocrypt, AutocryptSetupMessage, SecureJoin, diff --git a/src/message.rs b/src/message.rs index fa87fdca3..38e90b3dc 100644 --- a/src/message.rs +++ b/src/message.rs @@ -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 { + 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 { + 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) { 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"); + } } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 55ca51ee9..5b2cdeb2a 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -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 diff --git a/src/mimeparser.rs b/src/mimeparser.rs index b8304ef6a..abb1523e2 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -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; diff --git a/src/param.rs b/src/param.rs index 77169f42c..4d87202eb 100644 --- a/src/param.rs +++ b/src/param.rs @@ -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 diff --git a/src/stock.rs b/src/stock.rs index 84ef72f09..09e0ecd9c 100644 --- a/src/stock.rs +++ b/src/stock.rs @@ -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, } /* diff --git a/test-data/message/videochat_invitation.eml b/test-data/message/videochat_invitation.eml new file mode 100644 index 000000000..4c9e9c7e5 --- /dev/null +++ b/test-data/message/videochat_invitation.eml @@ -0,0 +1,15 @@ +Content-Type: text/plain; charset=utf-8 +Subject: Message from user +Message-ID: +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: +From: "=?utf-8?q??=" + +You are invited to an videochat, click https://example.org/p2p/?roomname=6HiduoAn4xN to join. + +-- +Sent with my Delta Chat Messenger: https://delta.chat