Compare commits

...

12 Commits

Author SHA1 Message Date
B. Petersen
df4466cff9 implement deeplink api 2024-11-19 17:43:51 +01:00
B. Petersen
d847669309 draft ffi for deeplinks 2024-11-19 17:43:51 +01:00
B. Petersen
1a8cc1094d notify webxdc info messages as needed 2024-11-19 17:43:51 +01:00
B. Petersen
f753dbd823 test notify api 2024-11-19 17:43:51 +01:00
B. Petersen
c1252ee814 accept notify addresses 2024-11-19 17:43:51 +01:00
B. Petersen
79993f3011 simpler naming, matching better the density of the other items - and avoids snake_case and camelCase confusion :) 2024-11-19 17:43:51 +01:00
B. Petersen
1bc6b1e17e add option to notifiy info-messages 2024-11-19 17:43:51 +01:00
B. Petersen
64bd7f66a5 replace DC_EVENT_INCOMING_WEBXDC_INFO by a normal DC_EVENT_INCOMING_MSG 2024-11-19 17:43:51 +01:00
B. Petersen
8ba3e88848 DC_EVENT_INCOMING_WEBXDC_INFO: contact is in info message already 2024-11-19 17:43:51 +01:00
B. Petersen
de95c6e27e DC_EVENT_INCOMING_WEBXDC_INFO: text is in message already 2024-11-19 17:43:51 +01:00
B. Petersen
331e3dc29a DC_EVENT_INCOMING_WEBXDC_INFO gets info_msg_id, not webxdc_instance 2024-11-19 17:43:51 +01:00
B. Petersen
d8ae5ebec8 draft ffi for webxdc-notifications 2024-11-19 17:28:56 +01:00
6 changed files with 306 additions and 25 deletions

View File

@@ -1154,7 +1154,13 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
* @memberof dc_context_t
* @param context The context object.
* @param msg_id The ID of the message with the webxdc instance.
* @param json program-readable data, the actual payload
* @param json program-readable data, this is create in JS land as:
* - `payload`: any JS object or primitive.
* - `info`: optional informational message. will be shown in chat an may be added as system notification.
* - `notify`: optional array of user `addr` that should be notified eg. by a sound.
* note that still all users get the update payload and the `info` message shown in a chat.
* - `document`: optional document name. shown eg. in title bar.
* - `summary`: optional summary. shown beside app icon.
* @param descr The user-visible description of JSON data,
* in case of a chess game, e.g. the move.
* @return 1=success, 0=error
@@ -4511,6 +4517,26 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
/**
* Get deeplink attached to an info message.
* The info message need to be of type DC_INFO_WEBXDC_INFO_MESSAGE.
*
* Typically, this is used to start the corresponding webxdc directly or indirectly
* and passing the deeplink to `window.webxdc.deeplink` in JS land.
*
* @memberof dc_context_t
* @param context The context object.
* @param info_msg_id The info message object.
* Not: the webxdc instance.
* @return The deeplink that can be passed to `window.webxdc.deeplink` in JS land.
* The content of the deeplink is a full status update and its semantic is defined by the app.
* Returns NULL if there is no deeplink attached to the info message and on errors.
*
*/
char* dc_msg_get_webxdc_deeplink (const dc_msg_t* msg);
/**
* Check if a message is still in creation. A message is in creation between
* the calls to dc_prepare_msg() and dc_send_msg().
@@ -6085,6 +6111,10 @@ void dc_event_unref(dc_event_t* event);
*
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
*
* If the message is an webxdc info message,
* dc_msg_get_parent() returns the webxdc instance the notification belongs to.
* Use dc_msg_get_webxdc_deeplink() to get the deeplink to pass to `window.webxdc.deeplink` in JS land.
*
* @param data1 (int) chat_id
* @param data2 (int) msg_id
*/

View File

@@ -3681,6 +3681,31 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int
ffi_msg.message.get_info_type() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_get_webxdc_deeplink(msg: *mut dc_msg_t) -> *mut libc::c_char {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_webxdc_deeplink()");
return "".strdup();
}
let ffi_msg = &*msg;
let ctx = &*ffi_msg.context;
let res = block_on(async move {
ffi_msg
.message
.get_webxdc_deeplink(ctx)
.await
.context("failed to get deeplink")
.log_err(ctx)
.unwrap_or(None)
});
match res {
Some(str) => str.strdup(),
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int {
if msg.is_null() {

View File

@@ -4508,9 +4508,34 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
/// Adds an informational message to chat.
///
/// For example, it can be a message showing that a member was added to a group.
/// Doesn't fail if the chat doesn't exist.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add_info_msg_with_cmd(
context: &Context,
chat_id: ChatId,
text: &str,
cmd: SystemMessage,
timestamp_sort: i64,
timestamp_sent_rcvd: Option<i64>,
parent: Option<&Message>,
from_id: Option<ContactId>,
) -> Result<MsgId> {
add_info_msg_with_importance(
context,
chat_id,
text,
cmd,
timestamp_sort,
timestamp_sent_rcvd,
parent,
from_id,
false,
)
.await
}
/// Adds an informational message to chat, optionally showing a notification for important messages.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add_info_msg_with_importance(
context: &Context,
chat_id: ChatId,
text: &str,
@@ -4520,6 +4545,7 @@ pub(crate) async fn add_info_msg_with_cmd(
timestamp_sent_rcvd: Option<i64>,
parent: Option<&Message>,
from_id: Option<ContactId>,
important: bool,
) -> Result<MsgId> {
let rfc724_mid = create_outgoing_rfc724_mid();
let ephemeral_timer = chat_id.get_ephemeral_timer(context).await?;
@@ -4553,7 +4579,7 @@ pub(crate) async fn add_info_msg_with_cmd(
context.new_msgs_notify.notify_one();
let msg_id = MsgId::new(row_id.try_into()?);
context.emit_msgs_changed(chat_id, msg_id);
chat_id.emit_msg_event(context, msg_id, important);
Ok(msg_id)
}

View File

@@ -63,6 +63,7 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver<DebugEventLo
summary: None,
document: None,
uid: None,
notify: None,
},
time,
)

View File

@@ -177,6 +177,10 @@ pub struct StatusUpdateItem {
/// If there is no ID, message is always considered to be unique.
#[serde(skip_serializing_if = "Option::is_none")]
pub uid: Option<String>,
/// Array of Addr that should be notified about this update.
#[serde(skip_serializing_if = "Option::is_none")]
pub notify: Option<Vec<String>>,
}
/// Update items as passed to the UIs.
@@ -311,29 +315,53 @@ impl Context {
if can_info_msg {
if let Some(ref info) = status_update_item.info {
if let Some(info_msg_id) =
self.get_overwritable_info_msg_id(instance, from_id).await?
{
chat::update_msg_text_and_timestamp(
self,
instance.chat_id,
info_msg_id,
info.as_str(),
timestamp,
)
.await?;
let mut info_msg_id = self.get_overwritable_info_msg_id(instance, from_id).await?;
let notify_list = status_update_item.notify;
if notify_list.is_none() && info_msg_id.is_some() {
if let Some(info_msg_id) = info_msg_id {
chat::update_msg_text_and_timestamp(
self,
instance.chat_id,
info_msg_id,
info.as_str(),
timestamp,
)
.await?;
}
} else {
chat::add_info_msg_with_cmd(
self,
instance.chat_id,
info.as_str(),
SystemMessage::WebxdcInfoMessage,
timestamp,
None,
Some(instance),
Some(from_id),
)
.await?;
let notify = if let Some(notify_list) = notify_list {
if let Ok(self_addr) = self.get_primary_self_addr().await {
notify_list.contains(&self_addr)
} else {
false
}
} else {
false
};
info_msg_id = Some(
chat::add_info_msg_with_importance(
self,
instance.chat_id,
info.as_str(),
SystemMessage::WebxdcInfoMessage,
timestamp,
None,
Some(instance),
Some(from_id),
notify,
)
.await?,
);
}
if let Some(info_msg_id) = info_msg_id {
let mut info_msg = Message::load_from_db(self, info_msg_id).await?;
info_msg
.param
.set_int(Param::Arg, status_update_serial.to_u32() as i32);
info_msg.update_param(self).await?;
}
}
}
@@ -906,6 +934,37 @@ impl Message {
internet_access,
})
}
/// Get deeplink attached to an info message.
///
/// The info message need to be of type SystemMessage::WebxdcInfoMessage.
/// Typically, this is used to start the corresponding webxdc directly or indirectly
// and passing the deeplink to `window.webxdc.deeplink` in JS land.
pub async fn get_webxdc_deeplink(&self, context: &Context) -> Result<Option<String>> {
let Some(serial) = self.param.get_int(Param::Arg) else {
return Ok(None);
};
let serial = StatusUpdateSerial::new(serial as u32);
let Some(instance) = self.parent(&context).await? else {
return Ok(None);
};
let update_item_str: String = context
.sql
.query_get_value(
"SELECT update_item FROM msgs_status_updates WHERE msg_id=? AND id=?",
(instance.id, serial),
)
.await?
.context("cannot read update item")?;
let update_item = StatusUpdateItem {
uid: None, // Erase UIDs, apps, bots and tests don't need to know them.
..serde_json::from_str(&update_item_str)?
};
let json = serde_json::to_string(&update_item)?;
Ok(Some(json))
}
}
#[cfg(test)]
@@ -1422,6 +1481,7 @@ mod tests {
document: None,
summary: None,
uid: Some("iecie2Ze".to_string()),
notify: None,
},
1640178619,
true,
@@ -1446,6 +1506,7 @@ mod tests {
document: None,
summary: None,
uid: Some("iecie2Ze".to_string()),
notify: None,
},
1640178619,
true,
@@ -1479,6 +1540,7 @@ mod tests {
document: None,
summary: None,
uid: None,
notify: None,
},
1640178619,
true,
@@ -1498,6 +1560,7 @@ mod tests {
document: None,
summary: None,
uid: None,
notify: None,
},
1640178619,
true,
@@ -2871,4 +2934,139 @@ sth_for_the = "future""#
Ok(())
}
async fn has_incoming_msg_event(t: &TestContext, msg: Message) -> bool {
t.evtracker
.get_matching_opt(t, |evt| {
if let EventType::IncomingMsg { msg_id, .. } = evt {
*msg_id == msg.id
} else {
false
}
})
.await
.is_some()
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_notify_one() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona])
.await;
let instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
alice
.send_webxdc_status_update(
instance.id,
r#"{"payload":7,"info": "your move!","notify":["bob@example.net"]}"#,
"d",
)
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let info_msg = alice.get_last_msg().await;
assert!(info_msg.is_info());
assert!(!has_incoming_msg_event(&alice, info_msg).await);
bob.recv_msg(&sent1).await;
bob.recv_msg_trash(&sent2).await;
let info_msg = bob.get_last_msg().await;
assert!(info_msg.is_info());
assert!(has_incoming_msg_event(&bob, info_msg).await);
fiona.recv_msg(&sent1).await;
fiona.recv_msg_trash(&sent2).await;
let info_msg = fiona.get_last_msg().await;
assert!(info_msg.is_info());
assert!(!has_incoming_msg_event(&fiona, info_msg).await);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_notify_multiple() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let fiona = tcm.fiona().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona])
.await;
let instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
alice
.send_webxdc_status_update(
instance.id,
r#"{"payload":7,"info": "my move!","notify":["bob@example.net","fiona@example.net"]}"#,
"d",
)
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let info_msg = alice.get_last_msg().await;
assert!(info_msg.is_info());
assert!(!has_incoming_msg_event(&alice, info_msg).await);
bob.recv_msg(&sent1).await;
bob.recv_msg_trash(&sent2).await;
let info_msg = bob.get_last_msg().await;
assert!(info_msg.is_info());
assert!(has_incoming_msg_event(&bob, info_msg).await);
fiona.recv_msg(&sent1).await;
fiona.recv_msg_trash(&sent2).await;
let info_msg = fiona.get_last_msg().await;
assert!(info_msg.is_info());
assert!(has_incoming_msg_event(&fiona, info_msg).await);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_deeplink() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = tcm.alice().await;
let bob = tcm.bob().await;
let grp_id = alice
.create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob])
.await;
let instance = send_webxdc_instance(&alice, grp_id).await?;
let sent1 = alice.pop_sent_msg().await;
alice
.send_webxdc_status_update(
instance.id,
r#"{"payload": "my deeplink data", "info": "my move!"}"#,
"d",
)
.await?;
alice.flush_status_updates().await?;
let sent2 = alice.pop_sent_msg().await;
let info_msg = alice.get_last_msg().await;
assert!(info_msg.is_info());
assert_eq!(
info_msg.get_webxdc_deeplink(&alice).await?.unwrap(),
r#"{"payload":"my deeplink data","info":"my move!"}"#
);
bob.recv_msg(&sent1).await;
bob.recv_msg_trash(&sent2).await;
let info_msg = bob.get_last_msg().await;
assert!(info_msg.is_info());
assert_eq!(
info_msg.get_webxdc_deeplink(&bob).await?.unwrap(),
r#"{"payload":"my deeplink data","info":"my move!"}"#
);
Ok(())
}
}

View File

@@ -149,6 +149,7 @@ pub(crate) async fn intercept_get_updates(
document: None,
summary: None,
uid: None,
notify: None,
},
serial: StatusUpdateSerial(location.location_id),
max_serial: StatusUpdateSerial(location.location_id),