fix apple push notifications

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
This commit is contained in:
IAmTomahawkx
2024-07-11 19:44:52 -07:00
parent 27f15f7b02
commit 8468ce19cb
6 changed files with 126 additions and 45 deletions

4
Cargo.lock generated
View File

@@ -3788,9 +3788,9 @@ dependencies = [
[[package]] [[package]]
name = "revolt_a2" name = "revolt_a2"
version = "0.10.0" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "466eb5262fcbb26e6e10b8a0acf56eb7cc095008132ff4ebddfc44d8672f8066" checksum = "edbe1f79cb41271d3cd8f932d75dddeba963c19dc93d1ee6cbe0391b495ab2f5"
dependencies = [ dependencies = [
"base64 0.21.3", "base64 0.21.3",
"erased-serde", "erased-serde",

View File

@@ -444,8 +444,8 @@ impl Message {
) -> Result<()> { ) -> Result<()> {
self.send_without_notifications( self.send_without_notifications(
db, db,
user, user.clone(),
member, member.clone(),
matches!(channel, Channel::DirectMessage { .. }), matches!(channel, Channel::DirectMessage { .. }),
generate_embeds, generate_embeds,
) )
@@ -463,7 +463,7 @@ impl Message {
} }
}, },
PushNotification::from( PushNotification::from(
self.clone().into_model(None, None), self.clone().into_model(user, member),
Some(author), Some(author),
&channel.id(), &channel.id(),
) )

View File

@@ -6,23 +6,53 @@ use base64::{
}; };
use deadqueue::limited::Queue; use deadqueue::limited::Queue;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use revolt_a2::{Client, ClientConfig, DefaultNotificationBuilder, Endpoint}; use revolt_a2::{
use revolt_a2::{Error, ErrorBody, ErrorReason, NotificationBuilder, Response}; request::{
notification::{DefaultAlert, NotificationOptions},
payload::{APSAlert, PayloadLike, APS},
},
Client, ClientConfig, DefaultNotificationBuilder, Endpoint, Error, ErrorBody, ErrorReason,
NotificationBuilder, Priority, PushType, Response,
};
use revolt_config::config; use revolt_config::config;
use revolt_models::v0::{Message, PushNotification}; use revolt_models::v0::{Message, PushNotification};
use crate::Database; use crate::Database;
/// Payload information /// Payload information, before assembly
#[derive(Debug)] #[derive(Debug)]
pub struct ApnCustomPayload { pub struct ApnPayload {
message: Message, message: Message,
serverId: String, url: String,
authorAvatar: String, authorAvatar: String,
authorDisplayName: String, authorDisplayName: String,
channelName: String, channelName: String,
} }
#[derive(Serialize, Debug)]
struct Payload<'a> {
aps: APS<'a>,
#[serde(skip_serializing)]
options: NotificationOptions<'a>,
#[serde(skip_serializing)]
device_token: &'a str,
message: Message,
url: String,
authorAvatar: String,
authorDisplayName: String,
channelName: String,
}
impl<'a> PayloadLike for Payload<'a> {
fn get_device_token(&self) -> &'a str {
self.device_token
}
fn get_options(&self) -> &NotificationOptions {
&self.options
}
}
/// Task information /// Task information
#[derive(Debug)] #[derive(Debug)]
pub struct ApnTask { pub struct ApnTask {
@@ -45,7 +75,7 @@ pub struct ApnTask {
category: String, category: String,
/// Payload used by the iOS client to modify the notification /// Payload used by the iOS client to modify the notification
payload: ApnCustomPayload, custom_payload: ApnPayload,
} }
impl ApnTask { impl ApnTask {
@@ -55,7 +85,10 @@ impl ApnTask {
// in a group, it would look like "Sendername in groupname" // in a group, it would look like "Sendername in groupname"
// in a dm it should just be "Sendername". // in a dm it should just be "Sendername".
// not sure how feasible all those are given the PushNotification object as it currently stands. // not sure how feasible all those are given the PushNotification object as it currently stands.
todo!(); format!(
"{} in {}",
notification.author, notification.message.channel
) // TODO: this absolutely needs a channel name
} }
pub fn from_notification( pub fn from_notification(
@@ -70,12 +103,12 @@ impl ApnTask {
body: notification.body.to_string(), body: notification.body.to_string(),
thread_id: notification.tag.to_string(), thread_id: notification.tag.to_string(),
category: "ALERT_MESSAGE".to_string(), category: "ALERT_MESSAGE".to_string(),
payload: ApnCustomPayload { custom_payload: ApnPayload {
message: (), message: notification.message.clone(),
serverId: (), url: notification.url.clone(),
authorAvatar: notification.icon.to_string(), authorAvatar: notification.icon.clone(),
authorDisplayName: notification.author.to_string(), authorDisplayName: notification.author.clone(),
channelName: (), channelName: "#fetchchannelnamehere".to_string(), // TODO: get actual channel name
}, },
} }
} }
@@ -110,8 +143,7 @@ pub async fn worker(db: Database) {
.decode(config.api.apn.pkcs8) .decode(config.api.apn.pkcs8)
.expect("valid `pcks8`"); .expect("valid `pcks8`");
let client_config = ClientConfig::default(); let client_config = ClientConfig::new(endpoint);
client_config.endpoint = endpoint;
let client = Client::token( let client = Client::token(
&mut Cursor::new(pkcs8), &mut Cursor::new(pkcs8),
@@ -121,21 +153,52 @@ pub async fn worker(db: Database) {
) )
.expect("could not create APN client"); .expect("could not create APN client");
let payload_options = NotificationOptions {
apns_id: None,
apns_push_type: Some(PushType::Alert),
apns_expiration: None,
apns_priority: Some(Priority::High),
apns_topic: Some("chat.revolt.app"),
apns_collapse_id: None,
};
loop { loop {
let task = Q.pop().await; let task = Q.pop().await;
let payload = DefaultNotificationBuilder::new()
.set_title(&task.title)
.set_body(&task.body)
.set_thread_id(&task.thread_id)
.set_category(&task.category)
.set_mutable_content() // allows the service extension to execute
.build(&task.device_token, Default::default());
payload.data = &task.payload; // this looks like a job for someone more rust-inclined than me. -tom let payload: Payload = Payload {
aps: APS {
alert: Some(APSAlert::Default(DefaultAlert {
title: Some(&task.title),
subtitle: None,
body: Some(&task.body),
title_loc_key: None,
title_loc_args: None,
action_loc_key: None,
loc_key: None,
loc_args: None,
launch_image: None,
})),
badge: Some(1),
sound: None,
thread_id: Some(&task.thread_id),
content_available: Some(1),
category: Some(&task.category),
mutable_content: Some(1),
url_args: None,
},
device_token: &task.device_token,
options: payload_options.clone(),
message: task.custom_payload.message,
url: task.custom_payload.url,
authorAvatar: task.custom_payload.authorAvatar,
authorDisplayName: task.custom_payload.authorDisplayName,
channelName: task.custom_payload.channelName,
};
println!("sending APNS payload: {:?}", payload.data); let resp = client.send(payload).await;
//println!("response from APNS: {:?}", resp);
if let Err(err) = client.send(payload).await { if let Err(err) = resp {
match err { match err {
Error::ResponseError(Response { Error::ResponseError(Response {
error: error:

View File

@@ -6,7 +6,6 @@ use base64::{
Engine as _, Engine as _,
}; };
use deadqueue::limited::Queue; use deadqueue::limited::Queue;
use fcm::FcmError;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use revolt_config::config; use revolt_config::config;
use revolt_models::v0::PushNotification; use revolt_models::v0::PushNotification;
@@ -82,6 +81,7 @@ pub async fn worker(db: Database) {
tag, tag,
timestamp: _, timestamp: _,
url: _, url: _,
message: _,
} = &task.payload; } = &task.payload;
let mut notification = fcm::NotificationBuilder::new(); let mut notification = fcm::NotificationBuilder::new();

View File

@@ -92,38 +92,38 @@ auto_derived!(
pub struct WebsiteMetadata { pub struct WebsiteMetadata {
/// Direct URL to web page /// Direct URL to web page
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>, pub url: Option<String>,
/// Original direct URL /// Original direct URL
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
original_url: Option<String>, pub original_url: Option<String>,
/// Remote content /// Remote content
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
special: Option<Special>, pub special: Option<Special>,
/// Title of website /// Title of website
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>, pub title: Option<String>,
/// Description of website /// Description of website
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>, pub description: Option<String>,
/// Embedded image /// Embedded image
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
image: Option<Image>, pub image: Option<Image>,
/// Embedded video /// Embedded video
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
video: Option<Video>, pub video: Option<Video>,
// #[serde(skip_serializing_if = "Option::is_none")] // #[serde(skip_serializing_if = "Option::is_none")]
// opengraph_type: Option<String>, // opengraph_type: Option<String>,
/// Site name /// Site name
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
site_name: Option<String>, pub site_name: Option<String>,
/// URL to site icon /// URL to site icon
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
icon_url: Option<String>, pub icon_url: Option<String>,
/// CSS Colour /// CSS Colour
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
colour: Option<String>, pub colour: Option<String>,
} }
/// Text Embed /// Text Embed

View File

@@ -200,6 +200,8 @@ auto_derived!(
pub timestamp: u64, pub timestamp: u64,
/// URL to open when clicking notification /// URL to open when clicking notification
pub url: String, pub url: String,
/// The message object itself, to send to clients for processing
pub message: Message,
} }
/// Representation of a text embed before it is sent. /// Representation of a text embed before it is sent.
@@ -437,15 +439,30 @@ impl PushNotification {
format!("{}/assets/logo.png", config.hosts.app) format!("{}/assets/logo.png", config.hosts.app)
}; };
let image = msg.attachments.and_then(|attachments| { let image = msg.attachments.as_ref().and_then(|attachments| {
attachments attachments
.first() .first()
.map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id)) .map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
}); });
let body = if let Some(sys) = msg.system { let body = if let Some(ref sys) = msg.system {
sys.into() sys.clone().into()
} else if let Some(text) = msg.content { } else if let Some(ref text) = msg.content {
text.clone()
} else if let Some(text) = msg.embeds.as_ref().and_then(|embeds| match embeds.first() {
Some(Embed::Image(_)) => Some("Sent an image".to_string()),
Some(Embed::Video(_)) => Some("Sent a video".to_string()),
Some(Embed::Text(e)) => e
.description
.clone()
.or(e.title.clone().or(Some("Empty Embed".to_string()))),
Some(Embed::Website(e)) => e.title.clone().or(e
.description
.clone()
.or(e.site_name.clone().or(Some("Empty Embed".to_string())))),
Some(Embed::None) => Some("Empty Message".to_string()), // ???
None => Some("Empty Message".to_string()), // ??
}) {
text text
} else { } else {
"Empty Message".to_string() "Empty Message".to_string()
@@ -466,6 +483,7 @@ impl PushNotification {
tag: channel_id.to_string(), tag: channel_id.to_string(),
timestamp, timestamp,
url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id), url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id),
message: msg,
} }
} }
} }