feat(threads): gateway, messages and push thread support (#3250)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-07 15:24:40 +02:00
1 parent 4e730832c7
commit 5f4295e399
108 files changed
+15857 -176

No files matched your search

+114 -3
View File
@@ -26,10 +26,17 @@ const RING_CALLER_NAME_KEY: &str = "caller_name";
const RING_CALLER_AVATAR_KEY: &str = "caller_avatar_url";
const RING_AVATAR_KEYS: [&str; 1] = [RING_CALLER_AVATAR_KEY];
const RING_IDENTITY_KEYS: [&str; 2] = [RING_CALLER_ID_KEY, RING_CALLER_NAME_KEY];
const MINIMAL_DATA_KEYS: [&str; 9] = [
const THREAD_DATA_KEYS: [&str; 3] = ["parent_id", "parent_name", "channel_name"];
const NOTIF_TYPE_KEY: &str = "notif_type_id";
const FORUM_THREAD_CREATED_NOTIF_TYPE: u64 = 9;
const FCM_DEFAULT_CHANNEL_ID: &str = "fluxer_default_push";
const FCM_FORUM_THREAD_CREATED_CHANNEL_ID: &str = "fluxer_forum_thread_created";
const MINIMAL_DATA_KEYS: [&str; 11] = [
"type",
"action",
"channel_id",
"parent_id",
NOTIF_TYPE_KEY,
"message_id",
"guild_id",
"target_user_id",
@@ -46,7 +53,7 @@ pub fn web_push_message(job: &MessageJob, target_user_id: &str, badge_count: u32
} else {
Value::String(job.guild_id.clone())
};
let data = json!({
let mut data = json!({
"channel_id": job.channel_id,
"author_avatar_url": fields.icon,
"message_id": job.message_id,
@@ -57,6 +64,7 @@ pub fn web_push_message(job: &MessageJob, target_user_id: &str, badge_count: u32
"target_user_id": target_user_id,
"has_media": image_url.is_some(),
});
merge_thread_fields(&mut data, job);
let notification = json!({
"title": fields.title,
"body": fields.body,
@@ -81,6 +89,28 @@ pub fn web_push_message(job: &MessageJob, target_user_id: &str, badge_count: u32
envelope
}
fn merge_thread_fields(data: &mut Value, job: &MessageJob) {
let Some(object) = data.as_object_mut() else {
return;
};
let fields = [
job.parent_id.as_deref(),
job.parent_name.as_deref(),
job.channel_name.as_deref(),
];
for (key, value) in THREAD_DATA_KEYS.into_iter().zip(fields) {
if let Some(value) = value.filter(|value| !value.is_empty()) {
object.insert(key.to_owned(), value.into());
}
}
if job.is_forum_thread_created() {
object.insert(
NOTIF_TYPE_KEY.to_owned(),
FORUM_THREAD_CREATED_NOTIF_TYPE.into(),
);
}
}
pub fn web_push_clear(job: &ClearJob, badge_count: u32) -> Value {
let tag = format!("channel:{}", job.channel_id);
let data = json!({
@@ -200,7 +230,7 @@ fn fcm_notification_message(device_token: &str, envelope: &Value) -> Value {
data.insert("image_url".to_owned(), url.into());
}
let mut android_notification = json!({
"channel_id": "fluxer_default_push",
"channel_id": fcm_channel_id(envelope),
"tag": tag,
"click_action": FCM_CLICK_ACTION,
});
@@ -220,6 +250,18 @@ fn fcm_notification_message(device_token: &str, envelope: &Value) -> Value {
})
}
fn fcm_channel_id(envelope: &Value) -> &'static str {
let notif_type = envelope
.get("data")
.and_then(|data| data.get(NOTIF_TYPE_KEY))
.and_then(Value::as_u64);
if notif_type == Some(FORUM_THREAD_CREATED_NOTIF_TYPE) {
FCM_FORUM_THREAD_CREATED_CHANNEL_ID
} else {
FCM_DEFAULT_CHANNEL_ID
}
}
pub fn apns_payload(envelope: &Value) -> Value {
let data = envelope.get("data");
let mut payload = data.and_then(Value::as_object).cloned().unwrap_or_default();
@@ -616,6 +658,21 @@ mod tests {
image_url: image_url.map(str::to_owned),
},
user_ids: vec![USER_ID.to_owned()],
kind: None,
parent_id: None,
parent_name: None,
channel_name: None,
}
}
fn thread_job() -> MessageJob {
MessageJob {
guild_id: "4455".to_owned(),
kind: Some("thread".to_owned()),
parent_id: Some("7788".to_owned()),
parent_name: Some("general".to_owned()),
channel_name: Some("release plans".to_owned()),
..message_job(None)
}
}
@@ -669,6 +726,60 @@ mod tests {
);
}
#[test]
fn a_message_outside_a_thread_carries_no_thread_keys() {
let envelope = web_push_message(&message_job(None), USER_ID, 3);
for key in THREAD_DATA_KEYS {
assert!(envelope["data"].get(key).is_none(), "{key} leaked");
}
}
#[test]
fn a_thread_message_carries_its_parent_and_names() {
let envelope = web_push_message(&thread_job(), USER_ID, 3);
assert_eq!(envelope["data"]["parent_id"], json!("7788"));
assert_eq!(envelope["data"]["parent_name"], json!("general"));
assert_eq!(envelope["data"]["channel_name"], json!("release plans"));
assert_eq!(apns_payload(&envelope)["parent_id"], json!("7788"));
}
#[test]
fn only_a_forum_thread_created_message_carries_its_notification_type() {
let thread = web_push_message(&thread_job(), USER_ID, 3);
assert!(thread["data"].get(NOTIF_TYPE_KEY).is_none());
let forum = MessageJob {
kind: Some("forum_thread_created".to_owned()),
..thread_job()
};
let envelope = web_push_message(&forum, USER_ID, 3);
assert_eq!(envelope["data"][NOTIF_TYPE_KEY], json!(9));
assert_eq!(envelope["data"]["parent_id"], json!("7788"));
let message = fcm_message("device-token", &envelope);
assert_eq!(message["message"]["data"][NOTIF_TYPE_KEY], json!("9"));
assert_eq!(
message["message"]["android"]["notification"]["channel_id"],
json!(FCM_FORUM_THREAD_CREATED_CHANNEL_ID)
);
assert_eq!(
fcm_message("device-token", &thread)["message"]["android"]["notification"]["channel_id"],
json!(FCM_DEFAULT_CHANNEL_ID)
);
let budget = serialize(&minimal(&envelope, usize::MAX)).len();
let (bytes, _step) = fit(&envelope, budget);
let shrunk: Value = serde_json::from_slice(&bytes).expect("the shrunk payload is json");
assert_eq!(shrunk["data"][NOTIF_TYPE_KEY], json!(9));
}
#[test]
fn a_shrunk_thread_message_keeps_its_parent_id() {
let envelope = web_push_message(&thread_job(), USER_ID, 3);
let budget = serialize(&minimal(&envelope, usize::MAX)).len();
let (bytes, step) = fit(&envelope, budget);
assert_eq!(step, Some(PayloadShrink::Minimal));
let shrunk: Value = serde_json::from_slice(&bytes).expect("the shrunk payload is json");
assert_eq!(shrunk["data"]["parent_id"], json!("7788"));
}
#[test]
fn a_shrunk_clear_still_tells_the_client_it_is_a_clear() {
let envelope = web_push_clear(&clear_job(), 3);