Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ axum-tracing-opentelemetry = "0.32.1"
color-eyre = "0.6.5"
config = "0.15.18"
futures = "0.3.31"
firebae-cm = { version = "0.4.2", git = "https://github.com/famedly/firebae-cm.git", branch = "thomast/fix-apns-payload" }
firebae-cm = { version = "0.4.2", git = "https://github.com/famedly/firebae-cm.git", branch = "thomast/deserializable-enums" }
gcp_auth = "0.12.4"
opentelemetry = { version = "0.29.0", features = ["metrics"] }
opentelemetry_sdk = { version = "0.29.0", features = ["metrics", "rt-tokio"] }
Expand All @@ -26,6 +26,7 @@ prometheus = "0.14"
rust-telemetry = {version = "1.2.0", features = ["axum"]}
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
time = { version = "0.3.44", features = ["serde"] }
tokio = { version = "1.48.0", features = ["full"] }
tower = "0.4.13"
tower-http = { version = "0.6.6", features = [ "catch-panic", "normalize-path" ] }
Expand Down
52 changes: 45 additions & 7 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,54 @@ hedwig:
app_id: "org.matrix.awesome_client"
# specifies how many attempts at pushing a notification to a device should be made before giving up and reporting the push key as dead
push_max_retries: 5
# common fields for notifications sent to FCM and APNS
# notification_title and notification_body support the <count> placeholder, which is replaced with the number of unread messages
notification_click_action: "FLUTTER_NOTIFICATION_CLICK"
notification_title: "<count> unread rooms"
notification_body: "Open app to read the messages"
notification_body: "Open app to read the messages, <count> unread messages"
notification_sound: "default"
notification_icon: "notifications_icon"
notification_tag: "org.matrix.default_notification"
fcm_notification_android_channel_id: "org.matrix.app.message"
# https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns#Know-when-to-use-push-types
apns_push_type: background
apns_topic: app.bundle.id
# fields specific for notifications sent to android devices: https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#androidnotification
notification_android:
icon: "notifications_icon"
tag: "org.matrix.default_notification"
channel_id: "org.matrix.app.message"
color: null
body_loc_key: null
body_loc_args: null
title_loc_key: null
title_loc_args: null
ticker: null
sticky: false
event_time: null
local_only: false
default_sound: null
notification_priority: null
default_vibrate_timings: true
default_light_settings: true
vibrate_timings: null
visibility: null
light_settings: null
image: null

# headers specific for notifications sent to iOS devices: https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns#Send-a-POST-request-to-APNs
# those will be used both through FCM and direct APNS
apns_headers:
# https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns#Know-when-to-use-push-types
apns_push_type: background
apns_topic: app.bundle.id
apns_id: ""
apns_priority: null
pub apns_expiration: null
pub apns_topic: null
pub apns_collapse_id: null

# payload specific for notifications sent to iOS devices: https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification#Payload-key-reference
# those will be used both through FCM and direct APNS
apns_payload:
category: null
mutable_content: 1
content_available: 1

# leave blank if you don't want to use APNS directly
apns_key_file_path: "path/to/apns_key.p8"
fcm_credentials_file_path: "/path/to/fcm_credentials.json"
Expand Down
48 changes: 42 additions & 6 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ use firebae_cm::{FirebaseMap, IntoFirebaseMap};
use opentelemetry::metrics::{Counter, Histogram, Meter};
use serde::{Deserialize, Serialize};

use crate::error::{ErrCode, HedwigError};
use crate::{
error::{ErrCode, HedwigError},
settings::DeserializablePushType,
};

/// The notification priority
#[derive(Deserialize, Serialize, Debug, Clone)]
Expand Down Expand Up @@ -277,19 +280,52 @@ impl IntoFirebaseMap for NotificationData {
}

/// APNS headers
#[derive(Debug)]
#[derive(Debug, Deserialize, Clone)]
pub struct ApnsHeaders {
/// APNS ID
pub apns_id: Option<String>,
/// Priority
pub apns_priority: String,
pub apns_priority: Option<String>,
/// Push type
pub apns_push_type: String,
pub apns_push_type: DeserializablePushType,
/// Expiration in seconds
pub apns_expiration: Option<u64>,
/// Topic
pub apns_topic: Option<String>,
/// Collapse ID
pub apns_collapse_id: Option<String>,
}

/// APNS payload
#[derive(Debug, Deserialize, Clone)]
pub struct ApnsPayload {
/// Category
pub category: Option<String>,
/// mutable_content
pub mutable_content: u8,
/// content_available
pub content_available: u8,
}

impl IntoFirebaseMap for ApnsHeaders {
fn as_map(&self) -> FirebaseMap {
let mut map = FirebaseMap::new();
map.insert("apns-priority", &self.apns_priority);
map.insert("apns-push-type", &self.apns_push_type);
map.insert("apns-push-type", &self.apns_push_type.to_string());
if let Some(ref v) = self.apns_priority {
map.insert("apns-priority", v);
}
if let Some(ref v) = self.apns_id {
map.insert("apns-id", v);
}
if let Some(ref v) = self.apns_expiration {
map.insert("apns-expiration", v);
}
if let Some(ref v) = self.apns_topic {
map.insert("apns-topic", v);
}
if let Some(ref v) = self.apns_collapse_id {
map.insert("apns-collapse-id", v);
}
map
}
}
Expand Down
152 changes: 130 additions & 22 deletions src/pusher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ use crate::{
apns::APNSSender,
error::{ErrCode, HedwigError},
fcm::FcmSender,
models::{ApnsHeaders, DataMessageType, Device, Notification},
models::{DataMessageType, Device, Notification},
settings::Settings,
};

/// Pushes the FCM notification to the given device
#[allow(clippy::unused_async)]
#[allow(clippy::too_many_lines)]
pub async fn push_notification_fcm(
notification: &Notification,
device: &Device,
Expand All @@ -53,7 +54,9 @@ pub async fn push_notification_fcm(

let fcm_notification = firebae_cm::Notification {
title: Some(settings.hedwig.notification_title.replace("<count>", &count.to_string())),
body: Some(settings.hedwig.notification_body.clone()),
body: Some(
settings.hedwig.notification_body.clone().replace("<count>", &count.to_string()),
),
image: None,
};

Expand Down Expand Up @@ -85,24 +88,120 @@ pub async fn push_notification_fcm(

let mut android_notification = AndroidNotification::new();
android_notification
.channel_id(settings.hedwig.fcm_notification_android_channel_id.clone());
android_notification.icon(settings.hedwig.notification_icon.clone());
.channel_id(settings.hedwig.notification_android.channel_id.clone());
android_notification.icon(settings.hedwig.notification_android.icon.clone());
android_notification.sound(settings.hedwig.notification_sound.clone());
android_notification.tag(settings.hedwig.notification_tag.clone());
android_notification.tag(settings.hedwig.notification_android.tag.clone());
android_notification.click_action(settings.hedwig.notification_click_action.clone());

// set the values that are not None
settings
.hedwig
.notification_android
.color
.as_ref()
.map(|v| android_notification.color(v.clone()));
settings
.hedwig
.notification_android
.body_loc_key
.as_ref()
.map(|v| android_notification.body_loc_key(v.clone()));
settings
.hedwig
.notification_android
.body_loc_args
.as_ref()
.map(|v| android_notification.body_loc_args(v.clone()));
settings
.hedwig
.notification_android
.title_loc_key
.as_ref()
.map(|v| android_notification.title_loc_key(v.clone()));
settings
.hedwig
.notification_android
.title_loc_args
.as_ref()
.map(|v| android_notification.title_loc_args(v.clone()));
settings
.hedwig
.notification_android
.ticker
.as_ref()
.map(|v| android_notification.ticker(v.clone()));
settings
.hedwig
.notification_android
.event_time
.as_ref()
.map(|v| android_notification.event_time(*v));
settings
.hedwig
.notification_android
.default_sound
.as_ref()
.map(|v| android_notification.default_sound(*v));
settings
.hedwig
.notification_android
.vibrate_timings
.as_ref()
.map(|v| android_notification.vibrate_timings(v.clone()));
settings
.hedwig
.notification_android
.image
.as_ref()
.map(|v| android_notification.image(v.clone()));
settings.hedwig.notification_android.sticky.map(|v| android_notification.sticky(v));
settings
.hedwig
.notification_android
.local_only
.map(|v| android_notification.local_only(v));
settings
.hedwig
.notification_android
.default_vibrate_timings
.map(|v| android_notification.default_vibrate_timings(v));
settings
.hedwig
.notification_android
.default_light_settings
.map(|v| android_notification.default_light_settings(v));
settings
.hedwig
.notification_android
.notification_priority
.as_ref()
.map(|v| android_notification.notification_priority(v.clone()));
settings
.hedwig
.notification_android
.visibility
.as_ref()
.map(|v| android_notification.visibility(v.clone()));
settings
.hedwig
.notification_android
.light_settings
.as_ref()
.map(|v| android_notification.light_settings(v.clone()));

let mut android_config = AndroidConfig::new();
android_config.notification(android_notification);
android_config.direct_boot_ok(false);
android_config.priority(AndroidMessagePriority::High);

let mut ios_config = ApnsConfig::new();
ios_config.headers(ApnsHeaders {
apns_priority: "10".to_owned(),
apns_push_type: settings.hedwig.apns_push_type.0.to_string(),
})?;
ios_config.headers(settings.hedwig.apns_headers.clone())?;
ios_config.payload(json!({
"aps": {
"mutable-content": settings.hedwig.apns_payload.mutable_content,
"content-available": settings.hedwig.apns_payload.content_available,
"category": settings.hedwig.apns_payload.category,
"badge": count,
"sound": settings.hedwig.notification_sound
}
Expand All @@ -127,17 +226,15 @@ pub async fn push_notification_fcm(
let mut ios_config = ApnsConfig::new();
ios_config.payload(json!({
"aps": {
"mutable-content": 1,
"mutable-content": settings.hedwig.apns_payload.mutable_content,
"content-available": settings.hedwig.apns_payload.content_available,
"category": settings.hedwig.apns_payload.category,
"badge": count,
"sound": settings.hedwig.notification_sound
}
}))?;

// Priority needs to be 5 for the service extension to be used
ios_config.headers(ApnsHeaders {
apns_priority: "5".to_owned(),
apns_push_type: settings.hedwig.apns_push_type.0.to_string(),
})?;
ios_config.headers(settings.hedwig.apns_headers.clone())?;

body.apns(ios_config);
}
Expand All @@ -161,16 +258,27 @@ pub async fn push_notification_apns(

let count = notification.counts.as_ref().and_then(|c| c.unread).unwrap_or_default();

let builder = DefaultNotificationBuilder::new()
.set_body(settings.hedwig.notification_body.clone())
let mut builder = DefaultNotificationBuilder::new()
.set_body(settings.hedwig.notification_body.clone().replace("<count>", &count.to_string()))
.set_sound(settings.hedwig.notification_sound.clone())
.set_title(settings.hedwig.notification_title.clone())
.set_badge(u32::from(count))
.set_mutable_content();
.set_title(
settings.hedwig.notification_title.clone().replace("<count>", &count.to_string()),
)
.set_badge(u32::from(count));

if settings.hedwig.apns_payload.mutable_content == 1 {
builder = builder.set_mutable_content();
}
if let Some(category) = settings.hedwig.apns_payload.category.clone() {
builder = builder.set_category(category);
}
if settings.hedwig.apns_payload.content_available == 1 {
builder = builder.set_content_available();
}

let options = NotificationOptions {
apns_topic: Some(settings.hedwig.apns_topic.clone()),
apns_push_type: Some(settings.hedwig.apns_push_type.0),
apns_topic: settings.hedwig.apns_headers.apns_topic.clone(),
apns_push_type: Some(settings.hedwig.apns_headers.apns_push_type.0),
..Default::default()
};

Expand Down
Loading
Loading