forked from jmug/stoatchat
feat: Send push notifications for dm call start/end
This commit is contained in:
162
crates/daemons/pushd/src/consumers/inbound/dm_call.rs
Normal file
162
crates/daemons/pushd/src/consumers/inbound/dm_call.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::consumers::inbound::internal::*;
|
||||
use amqprs::{
|
||||
channel::{BasicPublishArguments, Channel},
|
||||
connection::Connection,
|
||||
consumer::AsyncConsumer,
|
||||
BasicProperties, Deliver,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use log::debug;
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
|
||||
pub struct DmCallConsumer {
|
||||
#[allow(dead_code)]
|
||||
db: Database,
|
||||
authifier_db: authifier::Database,
|
||||
conn: Option<Connection>,
|
||||
channel: Option<Channel>,
|
||||
}
|
||||
|
||||
impl Channeled for DmCallConsumer {
|
||||
fn get_connection(&self) -> Option<&Connection> {
|
||||
if self.conn.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(self.conn.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_channel(&self) -> Option<&Channel> {
|
||||
if self.channel.is_none() {
|
||||
None
|
||||
} else {
|
||||
Some(self.channel.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn set_connection(&mut self, conn: Connection) {
|
||||
self.conn = Some(conn);
|
||||
}
|
||||
|
||||
fn set_channel(&mut self, channel: Channel) {
|
||||
self.channel = Some(channel)
|
||||
}
|
||||
}
|
||||
|
||||
impl DmCallConsumer {
|
||||
pub fn new(db: Database, authifier_db: authifier::Database) -> DmCallConsumer {
|
||||
DmCallConsumer {
|
||||
db,
|
||||
authifier_db,
|
||||
conn: None,
|
||||
channel: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn consume_event(
|
||||
&mut self,
|
||||
_channel: &Channel,
|
||||
_deliver: Deliver,
|
||||
_basic_properties: BasicProperties,
|
||||
content: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let content = String::from_utf8(content)?;
|
||||
let _p: InternalDmCallPayload = serde_json::from_str(content.as_str())?;
|
||||
let payload = _p.payload;
|
||||
|
||||
debug!("Received dm call start/stop event");
|
||||
|
||||
let call_recipients: Vec<String> = if let Some(recipients) = _p.recipients {
|
||||
recipients
|
||||
} else {
|
||||
match self.db.fetch_channel(&payload.channel_id).await? {
|
||||
revolt_database::Channel::DirectMessage { recipients, .. }
|
||||
| revolt_database::Channel::Group { recipients, .. } => recipients
|
||||
.into_iter()
|
||||
.filter(|r| *r != payload.initiator_id)
|
||||
.collect(),
|
||||
_ => {
|
||||
warn!(
|
||||
"Discarding dm call start/stop event for non-dm/group channel {}",
|
||||
payload.channel_id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
for user_id in call_recipients {
|
||||
if let Ok(sessions) = self.authifier_db.find_sessions(&user_id).await {
|
||||
for session in sessions {
|
||||
if let Some(sub) = session.subscription {
|
||||
let mut sendable = PayloadToService {
|
||||
notification: PayloadKind::DmCallStartEnd(payload.clone()),
|
||||
token: sub.auth,
|
||||
user_id: session.user_id,
|
||||
session_id: session.id,
|
||||
extras: HashMap::new(),
|
||||
};
|
||||
|
||||
let args: BasicPublishArguments;
|
||||
|
||||
if sub.endpoint == "apn" {
|
||||
args = BasicPublishArguments::new(
|
||||
config.pushd.exchange.as_str(),
|
||||
config.pushd.apn.queue.as_str(),
|
||||
)
|
||||
.finish();
|
||||
} else if sub.endpoint == "fcm" {
|
||||
args = BasicPublishArguments::new(
|
||||
config.pushd.exchange.as_str(),
|
||||
config.pushd.fcm.queue.as_str(),
|
||||
)
|
||||
.finish();
|
||||
} else {
|
||||
// web push (vapid)
|
||||
args = BasicPublishArguments::new(
|
||||
config.pushd.exchange.as_str(),
|
||||
config.pushd.vapid.queue.as_str(),
|
||||
)
|
||||
.finish();
|
||||
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
|
||||
sendable
|
||||
.extras
|
||||
.insert("endpoint".to_string(), sub.endpoint.clone());
|
||||
}
|
||||
|
||||
let payload = serde_json::to_string(&sendable)?;
|
||||
|
||||
publish_message(self, payload.into(), args).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[async_trait]
|
||||
impl AsyncConsumer for DmCallConsumer {
|
||||
/// This consumer handles delegating messages into their respective platform queues.
|
||||
async fn consume(
|
||||
&mut self,
|
||||
channel: &Channel,
|
||||
deliver: Deliver,
|
||||
basic_properties: BasicProperties,
|
||||
content: Vec<u8>,
|
||||
) {
|
||||
if let Err(err) = self
|
||||
.consume_event(channel, deliver, basic_properties, content)
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_anyhow(&err);
|
||||
warn!("Failed to process dm call start/stop event: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod ack;
|
||||
pub mod dm_call;
|
||||
pub mod fr_accepted;
|
||||
pub mod fr_received;
|
||||
pub mod generic;
|
||||
|
||||
@@ -47,6 +47,32 @@ impl<'a> PayloadLike for MessagePayload<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct CallStartStopPayload<'a> {
|
||||
aps: APS<'a>,
|
||||
#[serde(skip_serializing)]
|
||||
options: NotificationOptions<'a>,
|
||||
#[serde(skip_serializing)]
|
||||
device_token: &'a str,
|
||||
|
||||
initiator_id: &'a str,
|
||||
#[serde(rename = "camelCase")]
|
||||
channel_id: &'a str,
|
||||
#[serde(rename = "camelCase")]
|
||||
started_at: &'a str,
|
||||
#[serde(rename = "camelCase")]
|
||||
ended: bool,
|
||||
}
|
||||
|
||||
impl<'a> PayloadLike for CallStartStopPayload<'a> {
|
||||
fn get_device_token(&self) -> &'a str {
|
||||
self.device_token
|
||||
}
|
||||
fn get_options(&self) -> &NotificationOptions {
|
||||
&self.options
|
||||
}
|
||||
}
|
||||
|
||||
// region: consumer
|
||||
|
||||
pub struct ApnsOutboundConsumer {
|
||||
@@ -310,6 +336,7 @@ impl ApnsOutboundConsumer {
|
||||
);
|
||||
resp = self.client.send(apn_payload).await;
|
||||
}
|
||||
|
||||
PayloadKind::BadgeUpdate(badge) => {
|
||||
let apn_payload = Payload {
|
||||
aps: APS {
|
||||
@@ -324,6 +351,35 @@ impl ApnsOutboundConsumer {
|
||||
debug!("Sending badge update for user: {:}", &payload.user_id);
|
||||
resp = self.client.send(apn_payload).await;
|
||||
}
|
||||
|
||||
PayloadKind::DmCallStartEnd(alert) => {
|
||||
let started_at = alert.started_at.map_or(String::new(), |f| f.clone());
|
||||
|
||||
let apn_payload = CallStartStopPayload {
|
||||
aps: APS {
|
||||
alert: None,
|
||||
badge: self.get_badge_count(&payload.user_id).await,
|
||||
sound: None,
|
||||
thread_id: None,
|
||||
content_available: None,
|
||||
category: None,
|
||||
mutable_content: Some(1),
|
||||
url_args: None,
|
||||
},
|
||||
device_token: &payload.token,
|
||||
options: payload_options.clone(),
|
||||
initiator_id: &alert.initiator_id,
|
||||
channel_id: &alert.channel_id,
|
||||
started_at: &started_at,
|
||||
ended: alert.ended,
|
||||
};
|
||||
|
||||
debug!(
|
||||
"Sending call start/stop notification for user: {:}",
|
||||
&payload.user_id
|
||||
);
|
||||
resp = self.client.send(apn_payload).await;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = resp {
|
||||
|
||||
@@ -5,11 +5,12 @@ use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProp
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use fcm_v1::{
|
||||
android::AndroidConfig,
|
||||
android::{AndroidConfig, AndroidMessagePriority},
|
||||
auth::{Authenticator, ServiceAccountKey},
|
||||
message::{Message, Notification},
|
||||
Client, Error as FcmError,
|
||||
};
|
||||
use revolt_config::config;
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_models::v0::{Channel, PushNotification};
|
||||
use serde_json::Value;
|
||||
@@ -170,6 +171,37 @@ impl FcmOutboundConsumer {
|
||||
resp = self.client.send(&msg).await;
|
||||
}
|
||||
|
||||
PayloadKind::DmCallStartEnd(alert) => {
|
||||
let mut data: HashMap<String, Value> = HashMap::new();
|
||||
data.insert(
|
||||
"initiator_id".to_string(),
|
||||
Value::String(alert.initiator_id),
|
||||
);
|
||||
data.insert("channel_id".to_string(), Value::String(alert.channel_id));
|
||||
data.insert(
|
||||
"started_at".to_string(),
|
||||
Value::String(alert.started_at.unwrap_or_else(|| "".to_string())),
|
||||
);
|
||||
data.insert("ended".to_string(), Value::Bool(alert.ended));
|
||||
|
||||
let msg = Message {
|
||||
token: Some(payload.token),
|
||||
notification: None,
|
||||
data: Some(data),
|
||||
android: Some(AndroidConfig {
|
||||
priority: Some(AndroidMessagePriority::High),
|
||||
ttl: Some(format!(
|
||||
"{}s",
|
||||
config().await.api.livekit.call_ring_duration
|
||||
)),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
resp = self.client.send(&msg).await;
|
||||
}
|
||||
|
||||
PayloadKind::BadgeUpdate(_) => {
|
||||
bail!("FCM cannot handle badge updates and they should not be sent here.");
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use base64::{
|
||||
engine::{self},
|
||||
Engine as _,
|
||||
};
|
||||
use revolt_database::{events::rabbit::*, Database};
|
||||
use revolt_database::{events::rabbit::*, util::format_display_name, Database};
|
||||
use web_push::{
|
||||
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
|
||||
WebPushClient, WebPushError, WebPushMessageBuilder,
|
||||
@@ -107,6 +107,33 @@ impl VapidOutboundConsumer {
|
||||
PayloadKind::MessageNotification(alert) => {
|
||||
payload_body = serde_json::to_string(&alert)?;
|
||||
}
|
||||
PayloadKind::DmCallStartEnd(alert) => {
|
||||
let initiator_name = if let Some(server_id) =
|
||||
self.db.fetch_channel(&alert.channel_id).await?.server()
|
||||
{
|
||||
format_display_name(&self.db, &alert.initiator_id, Some(server_id)).await
|
||||
} else {
|
||||
format_display_name(&self.db, &alert.initiator_id, None).await
|
||||
}?;
|
||||
|
||||
let channel = self.db.fetch_channel(&alert.channel_id).await?;
|
||||
let mut body = HashMap::new();
|
||||
|
||||
match channel {
|
||||
revolt_database::Channel::DirectMessage { .. } => {
|
||||
body.insert("body", format!("{} is calling you", initiator_name));
|
||||
}
|
||||
revolt_database::Channel::Group { name, .. } => {
|
||||
body.insert(
|
||||
"body",
|
||||
format!("{} is calling your group, {}", initiator_name, name),
|
||||
);
|
||||
}
|
||||
_ => bail!("Invalid DmCallStart/End channel type"),
|
||||
}
|
||||
|
||||
payload_body = serde_json::to_string(&body)?;
|
||||
}
|
||||
PayloadKind::BadgeUpdate(_) => {
|
||||
bail!("Vapid cannot handle badge updates and they should not be sent here.");
|
||||
}
|
||||
|
||||
@@ -16,8 +16,9 @@ use tokio::sync::Notify;
|
||||
mod consumers;
|
||||
use consumers::{
|
||||
inbound::{
|
||||
ack::AckConsumer, fr_accepted::FRAcceptedConsumer, fr_received::FRReceivedConsumer,
|
||||
generic::GenericConsumer, mass_mention::MassMessageConsumer, message::MessageConsumer,
|
||||
ack::AckConsumer, dm_call::DmCallConsumer, fr_accepted::FRAcceptedConsumer,
|
||||
fr_received::FRReceivedConsumer, generic::GenericConsumer,
|
||||
mass_mention::MassMessageConsumer, message::MessageConsumer,
|
||||
},
|
||||
outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer},
|
||||
};
|
||||
@@ -102,6 +103,7 @@ async fn main() {
|
||||
.await,
|
||||
);
|
||||
|
||||
// inbound: Mass Mentions
|
||||
connections.push(
|
||||
make_queue_and_consume(
|
||||
&config,
|
||||
@@ -113,6 +115,18 @@ async fn main() {
|
||||
.await,
|
||||
);
|
||||
|
||||
// inbound: Dm Calls
|
||||
connections.push(
|
||||
make_queue_and_consume(
|
||||
&config,
|
||||
&config.pushd.dm_call_queue,
|
||||
config.pushd.get_dm_call_routing_key().as_str(),
|
||||
None,
|
||||
DmCallConsumer::new(db.clone(), authifier.clone()),
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
if !config.pushd.apn.pkcs8.is_empty() {
|
||||
connections.push(
|
||||
make_queue_and_consume(
|
||||
|
||||
@@ -59,9 +59,7 @@ pub async fn ingress(
|
||||
let channel_id = channel_id.to_internal_error()?;
|
||||
let user_id = user_id.to_internal_error()?;
|
||||
|
||||
let channel = Reference::from_unchecked(channel_id)
|
||||
.as_channel(db)
|
||||
.await?;
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
let voice_state = create_voice_state(channel_id, channel.server(), user_id).await?;
|
||||
|
||||
@@ -86,9 +84,7 @@ pub async fn ingress(
|
||||
|
||||
// First user who joined - send call started system message.
|
||||
if event.room.as_ref().unwrap().num_participants == 1 {
|
||||
let user = Reference::from_unchecked(user_id)
|
||||
.as_user(db)
|
||||
.await?;
|
||||
let user = Reference::from_unchecked(user_id).as_user(db).await?;
|
||||
|
||||
let mut call_started_message = SystemMessage::CallStarted {
|
||||
by: user_id.to_string(),
|
||||
@@ -120,9 +116,7 @@ pub async fn ingress(
|
||||
let channel_id = channel_id.to_internal_error()?;
|
||||
let user_id = user_id.to_internal_error()?;
|
||||
|
||||
let channel = Reference::from_unchecked(channel_id)
|
||||
.as_channel(db)
|
||||
.await?;
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
delete_voice_state(channel_id, channel.server(), user_id).await?;
|
||||
|
||||
@@ -143,6 +137,14 @@ pub async fn ingress(
|
||||
let members = get_voice_channel_members(channel_id).await?;
|
||||
|
||||
if members.is_none_or(|m| m.is_empty()) {
|
||||
// The channel is empty so send out an "end" message for ringing
|
||||
if let Err(e) = amqp
|
||||
.dm_call_updated(&user_id, &channel_id, None, true, None)
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_internal_error!(&e);
|
||||
}
|
||||
|
||||
if let Some(system_message_id) =
|
||||
take_channel_call_started_system_message(channel_id).await?
|
||||
{
|
||||
@@ -179,13 +181,9 @@ pub async fn ingress(
|
||||
let user_id = user_id.to_internal_error()?;
|
||||
let track = event.track.as_ref().to_internal_error()?;
|
||||
|
||||
let channel = Reference::from_unchecked(channel_id)
|
||||
.as_channel(db)
|
||||
.await?;
|
||||
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
|
||||
|
||||
let user = Reference::from_unchecked(user_id)
|
||||
.as_user(db)
|
||||
.await?;
|
||||
let user = Reference::from_unchecked(user_id).as_user(db).await?;
|
||||
|
||||
let user_limits = user.limits().await;
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
use std::env;
|
||||
|
||||
use amqprs::{
|
||||
channel::ExchangeDeclareArguments,
|
||||
connection::{Connection, OpenConnectionArguments},
|
||||
};
|
||||
use revolt_config::config;
|
||||
use revolt_database::DatabaseInfo;
|
||||
use revolt_database::{voice::VoiceClient, AMQP};
|
||||
use revolt_result::Result;
|
||||
@@ -18,36 +13,11 @@ mod guard;
|
||||
async fn main() -> Result<(), rocket::Error> {
|
||||
revolt_config::configure!(voice_ingress);
|
||||
|
||||
let config = config().await;
|
||||
let amqp = AMQP::new_auto().await;
|
||||
|
||||
let database = DatabaseInfo::Auto.connect().await.unwrap();
|
||||
let voice_client = VoiceClient::from_revolt_config().await;
|
||||
|
||||
let connection = Connection::open(&OpenConnectionArguments::new(
|
||||
&config.rabbit.host,
|
||||
config.rabbit.port,
|
||||
&config.rabbit.username,
|
||||
&config.rabbit.password,
|
||||
))
|
||||
.await
|
||||
.expect("Failed to connect to RabbitMQ");
|
||||
|
||||
let channel = connection
|
||||
.open_channel(None)
|
||||
.await
|
||||
.expect("Failed to open RabbitMQ channel");
|
||||
|
||||
channel
|
||||
.exchange_declare(
|
||||
ExchangeDeclareArguments::new(&config.pushd.exchange, "direct")
|
||||
.durable(true)
|
||||
.finish(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to declare exchange");
|
||||
|
||||
let amqp = AMQP::new(connection, channel);
|
||||
|
||||
let _rocket = build()
|
||||
.manage(database)
|
||||
.manage(voice_client)
|
||||
|
||||
Reference in New Issue
Block a user