chore: switch to lapin (#767)

* chore: begin switching to lapin fully

Signed-off-by: Zomatree <me@zomatree.live>

* chore: update rest of pushd to lapin

Signed-off-by: Zomatree <me@zomatree.live>

* chore: cleanup code

Signed-off-by: Zomatree <me@zomatree.live>

* chore: cleanup code

Signed-off-by: Zomatree <me@zomatree.live>

* fix: github webui sucks

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: Zomatree <me@zomatree.live>
Signed-off-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
Co-authored-by: Tom <iamtomahawkx@gmail.com>
Release-As: 0.13.6
This commit is contained in:
Angelo Kontaxis
2026-05-18 23:46:17 +01:00
committed by GitHub
parent 018afaf38f
commit 5b1985381a
26 changed files with 911 additions and 1323 deletions

View File

@@ -3,7 +3,7 @@ use lapin::{
options::*,
types::FieldTable,
uri::{AMQPAuthority, AMQPQueryString, AMQPUri, AMQPUserInfo},
ConnectionBuilder, ConnectionProperties,
ConnectionBuilder, ConnectionProperties, ExchangeKind,
};
use log::info;
use redis_kiss::{get_connection, AsyncCommands, Conn as RedisConnection};
@@ -46,6 +46,42 @@ pub async fn task(db: Database) -> Result<()> {
.await
.expect("Failed to create channel");
reader_channel
.exchange_declare(
config.rabbit.default_exchange.clone().into(),
ExchangeKind::Topic,
ExchangeDeclareOptions {
durable: true,
..Default::default()
},
FieldTable::default(),
)
.await
.expect("Failed to declare exchange");
reader_channel
.queue_declare(
config.rabbit.queues.acks.clone().into(),
QueueDeclareOptions {
durable: true,
..Default::default()
},
FieldTable::default(),
)
.await
.expect("Failed to bind queue");
reader_channel
.queue_bind(
config.rabbit.queues.acks.clone().into(),
config.rabbit.default_exchange.into(),
config.rabbit.queues.acks.clone().into(),
QueueBindOptions::default(),
FieldTable::default(),
)
.await
.expect("Failed to bind channel");
let mut consumer = reader_channel
.basic_consume(
config.rabbit.queues.acks.into(),

View File

@@ -15,7 +15,7 @@ revolt-parser = { workspace = true }
anyhow = { workspace = true }
amqprs = { workspace = true }
lapin = { workspace = true }
fcm_v1 = { workspace = true }
web-push = { workspace = true }
isahc = { workspace = true, features = ["json"], optional = true }

View File

@@ -1,96 +1,69 @@
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use std::sync::Arc;
use crate::utils::Consumer;
use anyhow::Result;
use async_trait::async_trait;
use lapin::{message::Delivery, Channel, Connection};
use revolt_database::{events::rabbit::*, Database};
#[derive(Clone)]
#[allow(unused)]
pub struct AckConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
connection: Arc<Connection>,
channel: Arc<Channel>,
}
impl Channeled for AckConsumer {
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 AckConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> AckConsumer {
AckConsumer {
#[async_trait]
impl Consumer for AckConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<Channel>,
) -> Self {
Self {
db,
authifier_db,
conn: None,
channel: None,
connection,
channel,
}
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for AckConsumer {
fn channel(&self) -> &Arc<Channel> {
&self.channel
}
/// This consumer processes all acks the platform receives, and sends relevant badge updates to apple platforms.
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
let content = String::from_utf8(content).unwrap();
let payload: AckPayload = serde_json::from_str(content.as_str()).unwrap();
async fn consume(&self, delivery: Delivery) -> Result<()> {
let payload: AckPayload = serde_json::from_slice(&delivery.data)?;
// Step 1: fetch unreads and don't continue if there's no unreads
#[allow(clippy::disallowed_methods)]
let unreads = self.db.fetch_unread_mentions(&payload.user_id).await;
// #[allow(clippy::disallowed_methods)]
debug!("Processing unreads for {:}", &payload.user_id);
if let Ok(u) = &unreads {
let unreads = if let Ok(u) = self.db.fetch_unread_mentions(&payload.user_id).await {
if u.is_empty() {
debug!(
"Discarding unread task (no mentions found) for {:}",
&payload.user_id
);
return;
}
return Ok(());
};
u
} else {
return;
}
return Ok(());
};
if let Ok(sessions) = self.authifier_db.find_sessions(&payload.user_id).await {
let config = revolt_config::config().await;
// Step 2: find any apple sessions, since we don't need to calculate this for anything else.
// If there's no apple sessions, we can return early
let apple_sessions: Vec<&authifier::models::Session> = sessions
.iter()
let mut apple_sessions = sessions
.into_iter()
.filter(|session| {
if let Some(sub) = &session.subscription {
sub.endpoint == "apn"
@@ -98,19 +71,19 @@ impl AsyncConsumer for AckConsumer {
false
}
})
.collect();
.peekable();
if apple_sessions.is_empty() {
if apple_sessions.peek().is_none() {
debug!(
"Discarding unread task (no apn sessions found) for {:}",
&payload.user_id
);
return;
return Ok(());
}
// Step 3: calculate the actual mention count, since we have to send it out
let mut mention_count = 0;
for u in &unreads.unwrap() {
for u in &unreads {
mention_count += u.mentions.as_ref().unwrap().len()
}
@@ -123,26 +96,22 @@ impl AsyncConsumer for AckConsumer {
token: session.subscription.as_ref().unwrap().auth.clone(),
extras: Default::default(),
};
let raw_service_payload = serde_json::to_string(&service_payload);
let payload = serde_json::to_string(&service_payload)?;
if let Ok(p) = raw_service_payload {
let args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
log::debug!(
"Publishing ack to apn session {}",
session.subscription.as_ref().unwrap().auth
);
log::debug!(
"Publishing ack to apn session {}",
session.subscription.as_ref().unwrap().auth
);
publish_message(self, p.into(), args).await;
} else {
log::warn!("Failed to serialize ack badge update payload!");
revolt_config::capture_error(&raw_service_payload.unwrap_err());
}
self.publish_message(
payload.as_bytes(),
&config.pushd.exchange,
&config.pushd.apn.queue,
)
.await?;
}
}
Ok(())
}
}

View File

@@ -1,70 +1,44 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use crate::utils::Consumer;
use anyhow::Result;
use async_trait::async_trait;
use lapin::{message::Delivery, Channel, Connection};
use log::debug;
use revolt_database::{events::rabbit::*, Database};
#[derive(Clone)]
#[allow(unused)]
pub struct DmCallConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
connection: Arc<Connection>,
channel: Arc<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 {
#[async_trait]
impl Consumer for DmCallConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<Channel>,
) -> Self {
Self {
db,
authifier_db,
conn: None,
channel: None,
connection,
channel,
}
}
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())?;
fn channel(&self) -> &Arc<Channel> {
&self.channel
}
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(&self, delivery: Delivery) -> Result<()> {
let _p: InternalDmCallPayload = serde_json::from_slice(&delivery.data)?;
let payload = _p.payload;
debug!("Received dm call start/stop event");
@@ -107,36 +81,27 @@ impl DmCallConsumer {
extras: HashMap::new(),
};
let args: BasicPublishArguments;
let routing_key = match sub.endpoint.as_str() {
"apn" => &config.pushd.apn.queue,
"fcm" => &config.pushd.fcm.queue,
endpoint => {
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), endpoint.to_string());
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("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
&config.pushd.vapid.queue
}
};
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
self.publish_message(
payload.as_bytes(),
&config.pushd.exchange,
routing_key,
)
.await?;
}
}
}
@@ -145,24 +110,3 @@ impl DmCallConsumer {
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:?}");
}
}
}

View File

@@ -1,70 +1,44 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use crate::utils::Consumer;
use anyhow::Result;
use async_trait::async_trait;
use lapin::{message::Delivery, Channel, Connection};
use log::debug;
use revolt_database::{events::rabbit::*, Database};
#[derive(Clone)]
#[allow(unused)]
pub struct FRAcceptedConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
connection: Arc<Connection>,
channel: Arc<Channel>,
}
impl Channeled for FRAcceptedConsumer {
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 FRAcceptedConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> FRAcceptedConsumer {
FRAcceptedConsumer {
#[async_trait]
impl Consumer for FRAcceptedConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<Channel>,
) -> Self {
Self {
db,
authifier_db,
conn: None,
channel: None,
connection,
channel,
}
}
async fn consume_event(
&mut self,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: FRAcceptedPayload = serde_json::from_str(content.as_str())?;
fn channel(&self) -> &Arc<Channel> {
&self.channel
}
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(&self, delivery: Delivery) -> Result<()> {
let payload: FRAcceptedPayload = serde_json::from_slice(&delivery.data)?;
debug!("Received FR accept event");
@@ -80,36 +54,23 @@ impl FRAcceptedConsumer {
extras: HashMap::new(),
};
let args: BasicPublishArguments;
let routing_key = match sub.endpoint.as_str() {
"apn" => &config.pushd.apn.queue,
"fcm" => &config.pushd.fcm.queue,
endpoint => {
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), endpoint.to_string());
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("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
&config.pushd.vapid.queue
}
};
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key)
.await?;
}
}
}
@@ -117,24 +78,3 @@ impl FRAcceptedConsumer {
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FRAcceptedConsumer {
/// 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);
eprintln!("Failed to process friend request accepted event: {err:?}");
}
}
}

View File

@@ -1,70 +1,44 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use crate::utils::Consumer;
use anyhow::Result;
use async_trait::async_trait;
use lapin::{message::Delivery, Channel, Connection};
use log::debug;
use revolt_database::{events::rabbit::*, Database};
#[derive(Clone)]
#[allow(unused)]
pub struct FRReceivedConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
connection: Arc<Connection>,
channel: Arc<Channel>,
}
impl Channeled for FRReceivedConsumer {
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 FRReceivedConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> FRReceivedConsumer {
FRReceivedConsumer {
#[async_trait]
impl Consumer for FRReceivedConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<Channel>,
) -> Self {
Self {
db,
authifier_db,
conn: None,
channel: None,
connection,
channel,
}
}
async fn consume_event(
&mut self,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: FRReceivedPayload = serde_json::from_str(content.as_str())?;
fn channel(&self) -> &Arc<Channel> {
&self.channel
}
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(&self, delivery: Delivery) -> Result<()> {
let payload: FRReceivedPayload = serde_json::from_slice(&delivery.data)?;
debug!("Received FR received event");
@@ -80,36 +54,23 @@ impl FRReceivedConsumer {
extras: HashMap::new(),
};
let args: BasicPublishArguments;
let routing_key = match sub.endpoint.as_str() {
"apn" => &config.pushd.apn.queue,
"fcm" => &config.pushd.fcm.queue,
endpoint => {
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), endpoint.to_string());
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("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
&config.pushd.vapid.queue
}
};
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key)
.await?;
}
}
}
@@ -117,24 +78,3 @@ impl FRReceivedConsumer {
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FRReceivedConsumer {
/// 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);
eprintln!("Failed to process friend request received event: {err:?}");
}
}
}

View File

@@ -1,70 +1,44 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use crate::utils::Consumer;
use anyhow::Result;
use async_trait::async_trait;
use lapin::{message::Delivery, Channel, Connection};
use log::debug;
use revolt_database::{events::rabbit::*, Database};
#[derive(Clone)]
#[allow(unused)]
pub struct GenericConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
connection: Arc<Connection>,
channel: Arc<Channel>,
}
impl Channeled for GenericConsumer {
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 GenericConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> GenericConsumer {
GenericConsumer {
#[async_trait]
impl Consumer for GenericConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<Channel>,
) -> Self {
Self {
db,
authifier_db,
conn: None,
channel: None,
connection,
channel,
}
}
async fn consume_event(
&mut self,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: MessageSentPayload = serde_json::from_str(content.as_str())?;
fn channel(&self) -> &Arc<Channel> {
&self.channel
}
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(&self, delivery: Delivery) -> Result<()> {
let payload: MessageSentPayload = serde_json::from_slice(&delivery.data)?;
debug!("Received message event on origin");
@@ -86,36 +60,23 @@ impl GenericConsumer {
extras: HashMap::new(),
};
let args: BasicPublishArguments;
let routing_key = match sub.endpoint.as_str() {
"apn" => &config.pushd.apn.queue,
"fcm" => &config.pushd.fcm.queue,
endpoint => {
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), endpoint.to_string());
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("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
&config.pushd.vapid.queue
}
};
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key)
.await?;
}
}
}
@@ -123,24 +84,3 @@ impl GenericConsumer {
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for GenericConsumer {
/// 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);
eprintln!("Failed to process generic event: {err:?}");
}
}
}

View File

@@ -1,53 +0,0 @@
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::{Connection, OpenConnectionArguments},
BasicProperties,
};
use log::{debug, warn};
pub(crate) trait Channeled {
#[allow(unused)]
fn get_connection(&self) -> Option<&Connection>;
fn get_channel(&self) -> Option<&Channel>;
fn set_connection(&mut self, conn: Connection);
fn set_channel(&mut self, channel: Channel);
}
pub(crate) async fn make_channel<T: Channeled>(consumer: &mut T) {
let config = revolt_config::config().await;
let args = OpenConnectionArguments::new(
&config.rabbit.host,
config.rabbit.port,
&config.rabbit.username,
&config.rabbit.password,
);
let conn = amqprs::connection::Connection::open(&args).await.unwrap();
let channel = conn.open_channel(None).await.unwrap();
consumer.set_connection(conn);
consumer.set_channel(channel);
}
pub(crate) async fn publish_message<T: Channeled>(
consumer: &mut T,
payload: Vec<u8>,
args: BasicPublishArguments,
) {
let routing_key = &args.routing_key.clone();
let mut channel = consumer.get_channel();
if channel.is_none() {
make_channel(consumer).await;
channel = consumer.get_channel();
}
if let Some(chnl) = channel {
chnl.basic_publish(BasicProperties::default(), payload.clone(), args.clone())
.await
.unwrap();
debug!("Sent message to queue for target {}", routing_key);
} else {
warn!("Failed to unwrap channel (including attempt to make a channel)!")
}
}

View File

@@ -1,17 +1,13 @@
use std::{
collections::{HashMap, HashSet},
hash::RandomState,
sync::Arc,
};
use crate::{consumers::inbound::internal::*, utils};
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use crate::utils::{render_notification_content, Consumer};
use anyhow::Result;
use async_trait::async_trait;
use lapin::{message::Delivery, Channel, Connection};
use revolt_database::{
events::rabbit::*, util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member,
MessageFlagsValue,
@@ -19,52 +15,18 @@ use revolt_database::{
use revolt_models::v0::{MessageFlags, PushNotification};
use revolt_result::ToRevoltError;
#[derive(Clone)]
#[allow(unused)]
pub struct MassMessageConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
}
impl Channeled for MassMessageConsumer {
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)
}
connection: Arc<Connection>,
channel: Arc<Channel>,
}
impl MassMessageConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> MassMessageConsumer {
MassMessageConsumer {
db,
authifier_db,
conn: None,
channel: None,
}
}
async fn fire_notification_for_users(
&mut self,
&self,
push: &PushNotification,
users: &[String],
) -> Result<()> {
@@ -84,56 +46,58 @@ impl MassMessageConsumer {
extras: HashMap::new(),
};
let args: BasicPublishArguments;
let routing_key = match sub.endpoint.as_str() {
"apn" => &config.pushd.apn.queue,
"fcm" => &config.pushd.fcm.queue,
endpoint => {
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), endpoint.to_string());
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("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
&config.pushd.vapid.queue
}
};
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key)
.await?;
}
}
}
Ok(())
}
}
async fn consume_event(
&mut self,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
#[async_trait]
impl Consumer for MassMessageConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<Channel>,
) -> Self {
Self {
db,
authifier_db,
connection,
channel,
}
}
fn channel(&self) -> &Arc<Channel> {
&self.channel
}
/// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications.
async fn consume(&self, delivery: Delivery) -> Result<()> {
let mut payload: MassMessageSentPayload = serde_json::from_slice(&delivery.data)?;
let config = revolt_config::config().await;
let content = String::from_utf8(content)?;
let mut payload: MassMessageSentPayload = serde_json::from_str(content.as_str())?;
for push in payload.notifications.iter_mut() {
if let Ok(body) = utils::render_notification_content(push, &self.db)
if let Ok(body) = render_notification_content(push, &self.db)
.await
.to_internal_error()
{
@@ -280,24 +244,3 @@ impl MassMessageConsumer {
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for MassMessageConsumer {
/// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications
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);
eprintln!("Failed to process mass message event: {err:?}");
}
}
}

View File

@@ -1,76 +1,46 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use crate::{consumers::inbound::internal::*, utils};
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use crate::utils::{render_notification_content, Consumer};
use anyhow::Result;
use async_trait::async_trait;
use lapin::{message::Delivery, Channel, Connection};
use log::debug;
use revolt_database::{events::rabbit::*, Database};
use revolt_result::ToRevoltError;
#[derive(Clone)]
#[allow(unused)]
pub struct MessageConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
connection: Arc<Connection>,
channel: Arc<Channel>,
}
impl Channeled for MessageConsumer {
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 MessageConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> MessageConsumer {
MessageConsumer {
#[async_trait]
impl Consumer for MessageConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<Channel>,
) -> Self {
Self {
db,
authifier_db,
conn: None,
channel: None,
connection,
channel,
}
}
async fn consume_event(
&mut self,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let content = String::from_utf8(content)?;
let mut payload: MessageSentPayload = serde_json::from_str(content.as_str())?;
fn channel(&self) -> &Arc<Channel> {
&self.channel
}
if let Ok(body) = utils::render_notification_content(&payload.notification, &self.db)
.await
.to_internal_error()
{
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(&self, delivery: Delivery) -> Result<()> {
let mut payload: MessageSentPayload = serde_json::from_slice(&delivery.data)?;
if let Ok(body) = render_notification_content(&payload.notification, &self.db).await {
payload.notification.raw_body = Some(payload.notification.body);
payload.notification.body = body;
}
@@ -95,36 +65,22 @@ impl MessageConsumer {
extras: HashMap::new(),
};
let args: BasicPublishArguments;
let routing_key = match sub.endpoint.as_str() {
"apn" => &config.pushd.apn.queue,
"fcm" => &config.pushd.fcm.queue,
endpoint => {
sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), endpoint.to_string());
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("p256dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
&config.pushd.vapid.queue
}
};
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key)
.await?;
}
}
}
@@ -132,24 +88,3 @@ impl MessageConsumer {
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for MessageConsumer {
/// 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);
eprintln!("Failed to process message event: {err:?}");
}
}
}

View File

@@ -3,6 +3,5 @@ pub mod dm_call;
pub mod fr_accepted;
pub mod fr_received;
pub mod generic;
mod internal;
pub mod mass_mention;
pub mod message;

View File

@@ -1,12 +1,13 @@
use std::{borrow::Cow, collections::BTreeMap, io::Cursor};
use std::{borrow::Cow, collections::BTreeMap, io::Cursor, sync::Arc};
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use anyhow::{anyhow, Result};
use crate::utils::Consumer;
use anyhow::Result;
use async_trait::async_trait;
use base64::{
engine::{self},
Engine as _,
};
use lapin::{message::Delivery, Channel as AMQPChannel, Connection};
use revolt_a2::{
request::{
notification::{DefaultAlert, NotificationOptions},
@@ -42,7 +43,7 @@ impl<'a> PayloadLike for MessagePayload<'a> {
fn get_device_token(&self) -> &'a str {
self.device_token
}
fn get_options(&self) -> &NotificationOptions {
fn get_options(&self) -> &NotificationOptions<'a> {
&self.options
}
}
@@ -68,16 +69,20 @@ impl<'a> PayloadLike for CallStartStopPayload<'a> {
fn get_device_token(&self) -> &'a str {
self.device_token
}
fn get_options(&self) -> &NotificationOptions {
fn get_options(&self) -> &NotificationOptions<'a> {
&self.options
}
}
// region: consumer
#[derive(Clone)]
#[allow(unused)]
pub struct ApnsOutboundConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<AMQPChannel>,
client: Client,
}
@@ -117,15 +122,21 @@ impl ApnsOutboundConsumer {
}
}
impl ApnsOutboundConsumer {
pub async fn new(db: Database) -> Result<ApnsOutboundConsumer, &'static str> {
#[async_trait]
impl Consumer for ApnsOutboundConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<AMQPChannel>,
) -> Self {
let config = revolt_config::config().await;
if config.pushd.apn.pkcs8.is_empty()
|| config.pushd.apn.key_id.is_empty()
|| config.pushd.apn.team_id.is_empty()
{
return Err("Missing APN keys.");
panic!("Missing APN keys.");
}
let endpoint = if config.pushd.apn.sandbox {
@@ -148,18 +159,21 @@ impl ApnsOutboundConsumer {
)
.expect("could not create APN client");
Ok(ApnsOutboundConsumer { db, client })
Self {
db,
authifier_db,
connection,
channel,
client,
}
}
async fn consume_event(
&mut self,
_channel: &AmqpChannel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str())?;
fn channel(&self) -> &Arc<AMQPChannel> {
&self.channel
}
async fn consume(&self, delivery: Delivery) -> Result<()> {
let payload: PayloadToService = serde_json::from_slice(&delivery.data)?;
let payload_options = NotificationOptions {
apns_id: None,
@@ -170,20 +184,15 @@ impl ApnsOutboundConsumer {
apns_collapse_id: None,
};
let resp: Result<Response, Error>;
match payload.notification {
let resp = match payload.notification {
PayloadKind::FRReceived(alert) => {
let loc_args = vec![Cow::from(
alert
.from_user
.display_name
.or(Some(format!(
alert.from_user.display_name.clone().unwrap_or_else(|| {
format!(
"{}#{}",
alert.from_user.username, alert.from_user.discriminator
)))
.clone()
.ok_or_else(|| anyhow!("missing name"))?,
)
}),
)];
let apn_payload = Payload {
@@ -216,20 +225,17 @@ impl ApnsOutboundConsumer {
"Sending friend request received for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
self.client.send(apn_payload).await
}
PayloadKind::FRAccepted(alert) => {
let loc_args = vec![Cow::from(
alert
.accepted_user
.display_name
.or(Some(format!(
alert.accepted_user.display_name.clone().unwrap_or_else(|| {
format!(
"{}#{}",
alert.accepted_user.username, alert.accepted_user.discriminator
)))
.clone()
.ok_or_else(|| anyhow!("missing name"))?,
)
}),
)];
let apn_payload = Payload {
@@ -262,7 +268,7 @@ impl ApnsOutboundConsumer {
"Sending friend request accept for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
self.client.send(apn_payload).await
}
PayloadKind::Generic(alert) => {
let apn_payload = Payload {
@@ -295,7 +301,7 @@ impl ApnsOutboundConsumer {
"Sending generic notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
self.client.send(apn_payload).await
}
PayloadKind::MessageNotification(alert) => {
@@ -334,7 +340,7 @@ impl ApnsOutboundConsumer {
"Sending message notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
self.client.send(apn_payload).await
}
PayloadKind::BadgeUpdate(badge) => {
@@ -349,7 +355,7 @@ impl ApnsOutboundConsumer {
};
debug!("Sending badge update for user: {:}", &payload.user_id);
resp = self.client.send(apn_payload).await;
self.client.send(apn_payload).await
}
PayloadKind::DmCallStartEnd(alert) => {
@@ -378,58 +384,37 @@ impl ApnsOutboundConsumer {
"Sending call start/stop notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
self.client.send(apn_payload).await
}
}
};
if let Err(err) = resp {
match err {
Error::ResponseError(Response {
error:
Some(ErrorBody {
reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered,
..
}),
..
}) => {
info!(
"Removing APNS subscription id {:} (user: {:}) due to invalid token",
&payload.session_id, &payload.user_id
);
if let Err(err) = self
.db
.remove_push_subscription_by_session_id(&payload.session_id)
.await
{
revolt_config::capture_error(&err);
}
}
err => {
match resp {
Err(Error::ResponseError(Response {
error:
Some(ErrorBody {
reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered,
..
}),
..
})) => {
info!(
"Removing APNS subscription id {:} (user: {:}) due to invalid token",
&payload.session_id, &payload.user_id
);
if let Err(err) = self
.db
.remove_push_subscription_by_session_id(&payload.session_id)
.await
{
revolt_config::capture_error(&err);
}
}
}
resp => {
resp?;
}
};
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for ApnsOutboundConsumer {
async fn consume(
&mut self,
channel: &AmqpChannel,
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);
eprintln!("Failed to process APN event: {err:?}");
}
}
}

View File

@@ -1,14 +1,14 @@
use std::{collections::HashMap, time::Duration};
use std::{collections::HashMap, sync::Arc, time::Duration};
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use anyhow::{anyhow, bail, Result};
use crate::utils::Consumer;
use anyhow::{bail, Result};
use async_trait::async_trait;
use fcm_v1::{
auth::{Authenticator, ServiceAccountKey},
message::Message,
Client, Error as FcmError,
};
use lapin::{message::Delivery, Channel as AMQPChannel, Connection};
use revolt_config::config;
use revolt_database::{events::rabbit::*, Database};
use serde_json::Value;
@@ -115,17 +115,31 @@ impl NotificationData {
}
}
#[derive(Clone)]
#[allow(unused)]
pub struct FcmOutboundConsumer {
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<AMQPChannel>,
client: Client,
}
impl FcmOutboundConsumer {
pub async fn new(db: Database) -> Result<FcmOutboundConsumer, &'static str> {
#[async_trait]
impl Consumer for FcmOutboundConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<AMQPChannel>,
) -> Self {
let config = revolt_config::config().await;
Ok(FcmOutboundConsumer {
Self {
db,
authifier_db,
connection,
channel,
client: Client::new(
Authenticator::service_account::<&str>(ServiceAccountKey {
key_type: Some(config.pushd.fcm.key_type),
@@ -145,33 +159,27 @@ impl FcmOutboundConsumer {
false,
Duration::from_secs(5),
),
})
}
}
async fn consume_event(
&mut self,
_channel: &AmqpChannel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str())?;
fn channel(&self) -> &Arc<AMQPChannel> {
&self.channel
}
async fn consume(&self, delivery: Delivery) -> Result<()> {
let payload: PayloadToService = serde_json::from_slice(&delivery.data)?;
#[allow(clippy::needless_late_init)]
let resp: Result<Message, FcmError>;
match payload.notification {
PayloadKind::FRReceived(alert) => {
let name = alert
.from_user
.display_name
.or(Some(format!(
let name = alert.from_user.display_name.clone().unwrap_or_else(|| {
format!(
"{}#{}",
alert.from_user.username, alert.from_user.discriminator
)))
.clone()
.ok_or_else(|| anyhow!("missing name"))?;
)
});
let data = NotificationData::FRReceived {
id: alert.from_user.id,
@@ -188,15 +196,12 @@ impl FcmOutboundConsumer {
}
PayloadKind::FRAccepted(alert) => {
let name = alert
.accepted_user
.display_name
.or(Some(format!(
let name = alert.accepted_user.display_name.clone().unwrap_or_else(|| {
format!(
"{}#{}",
alert.accepted_user.username, alert.accepted_user.discriminator
)))
.clone()
.ok_or_else(|| anyhow!("missing name"))?;
)
});
let data = NotificationData::FRAccepted {
id: alert.accepted_user.id,
@@ -269,43 +274,21 @@ impl FcmOutboundConsumer {
}
}
if let Err(err) = resp {
match err {
FcmError::Auth => {
if let Err(err) = self
.db
.remove_push_subscription_by_session_id(&payload.session_id)
.await
{
revolt_config::capture_error(&err);
}
}
err => {
match resp {
Err(FcmError::Auth) => {
if let Err(err) = self
.db
.remove_push_subscription_by_session_id(&payload.session_id)
.await
{
revolt_config::capture_error(&err);
}
}
}
res => {
res?;
}
};
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FcmOutboundConsumer {
async fn consume(
&mut self,
channel: &AmqpChannel,
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);
eprintln!("Failed to process FCM event: {err:?}");
}
}
}

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use crate::utils::Consumer;
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
@@ -8,46 +8,60 @@ use base64::{
engine::{self},
Engine as _,
};
use lapin::{message::Delivery, Channel as AMQPChannel, Connection};
use revolt_database::{events::rabbit::*, util::format_display_name, Database};
use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushError, WebPushMessageBuilder,
};
#[derive(Clone)]
#[allow(unused)]
pub struct VapidOutboundConsumer {
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<AMQPChannel>,
client: IsahcWebPushClient,
pkey: Vec<u8>,
pkey: Arc<Vec<u8>>,
}
impl VapidOutboundConsumer {
pub async fn new(db: Database) -> Result<VapidOutboundConsumer> {
#[async_trait]
impl Consumer for VapidOutboundConsumer {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<AMQPChannel>,
) -> Self {
let config = revolt_config::config().await;
if config.pushd.vapid.private_key.is_empty() | config.pushd.vapid.public_key.is_empty() {
bail!("no Vapid keys present");
if config.pushd.vapid.private_key.is_empty() || config.pushd.vapid.public_key.is_empty() {
panic!("no Vapid keys present");
}
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
.decode(config.pushd.vapid.private_key)
.expect("valid `VAPID_PRIVATE_KEY`");
let web_push_private_key = Arc::new(
engine::general_purpose::URL_SAFE_NO_PAD
.decode(config.pushd.vapid.private_key)
.expect("valid `VAPID_PRIVATE_KEY`"),
);
Ok(VapidOutboundConsumer {
Self {
db,
authifier_db,
connection,
channel,
client: IsahcWebPushClient::new().unwrap(),
pkey: web_push_private_key,
})
}
}
async fn consume_event(
&mut self,
_channel: &AmqpChannel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str())?;
fn channel(&self) -> &Arc<AMQPChannel> {
&self.channel
}
async fn consume(&self, delivery: Delivery) -> Result<()> {
let payload: PayloadToService = serde_json::from_slice(&delivery.data)?;
let subscription = SubscriptionInfo {
endpoint: payload
@@ -65,10 +79,7 @@ impl VapidOutboundConsumer {
},
};
#[allow(clippy::needless_late_init)]
let payload_body: String;
match payload.notification {
let payload_body = match payload.notification {
PayloadKind::FRReceived(alert) => {
let name = alert
.from_user
@@ -83,7 +94,7 @@ impl VapidOutboundConsumer {
let mut body = HashMap::new();
body.insert("body", format!("{} sent you a friend request", name));
payload_body = serde_json::to_string(&body)?;
serde_json::to_string(&body)?
}
PayloadKind::FRAccepted(alert) => {
let name = alert
@@ -99,14 +110,10 @@ impl VapidOutboundConsumer {
let mut body = HashMap::new();
body.insert("body", format!("{} accepted your friend request", name));
payload_body = serde_json::to_string(&body)?;
}
PayloadKind::Generic(alert) => {
payload_body = serde_json::to_string(&alert)?;
}
PayloadKind::MessageNotification(alert) => {
payload_body = serde_json::to_string(&alert)?;
serde_json::to_string(&body)?
}
PayloadKind::Generic(alert) => serde_json::to_string(&alert)?,
PayloadKind::MessageNotification(alert) => 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()
@@ -132,59 +139,41 @@ impl VapidOutboundConsumer {
_ => bail!("Invalid DmCallStart/End channel type"),
}
payload_body = serde_json::to_string(&body)?;
serde_json::to_string(&body)?
}
PayloadKind::BadgeUpdate(_) => {
bail!("Vapid cannot handle badge updates and they should not be sent here.");
}
}
};
match VapidSignatureBuilder::from_pem(std::io::Cursor::new(&self.pkey), &subscription) {
Ok(sig_builder) => match sig_builder.build() {
Ok(signature) => {
let mut builder = WebPushMessageBuilder::new(&subscription);
builder.set_vapid_signature(signature);
let signature = VapidSignatureBuilder::from_pem(
std::io::Cursor::new(self.pkey.as_ref()),
&subscription,
)?
.build()?;
builder.set_payload(ContentEncoding::AesGcm, payload_body.as_bytes());
let mut builder = WebPushMessageBuilder::new(&subscription);
builder.set_vapid_signature(signature);
match builder.build() {
Ok(msg) => {
if let Err(err) = self.client.send(msg).await {
if err == WebPushError::Unauthorized {
self.db
.remove_push_subscription_by_session_id(&payload.session_id)
.await?;
}
}
builder.set_payload(ContentEncoding::AesGcm, payload_body.as_bytes());
Ok(())
}
Err(err) => Err(err.into()),
}
let msg = builder.build()?;
match self.client.send(msg).await {
Err(WebPushError::Unauthorized) => {
if let Err(err) = self
.db
.remove_push_subscription_by_session_id(&payload.session_id)
.await
{
revolt_config::capture_error(&err);
}
Err(err) => Err(err.into()),
},
Err(err) => Err(err.into()),
}
}
}
}
res => {
res?;
}
};
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for VapidOutboundConsumer {
async fn consume(
&mut self,
channel: &AmqpChannel,
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);
eprintln!("Failed to process Vapid event: {err:?}");
}
Ok(())
}
}

View File

@@ -1,17 +1,16 @@
#[macro_use]
extern crate log;
use amqprs::{
channel::{
BasicConsumeArguments, Channel, ExchangeDeclareArguments, QueueBindArguments,
QueueDeclareArguments,
},
connection::{Connection, OpenConnectionArguments},
consumer::AsyncConsumer,
FieldTable,
use std::sync::Arc;
use lapin::{
options::{BasicConsumeOptions, ExchangeDeclareOptions, QueueBindOptions, QueueDeclareOptions},
types::{AMQPValue, FieldTable},
Channel, Connection, ConnectionProperties,
};
use revolt_config::{config, Settings};
use tokio::sync::Notify;
use revolt_database::Database;
use tokio::signal::ctrl_c;
mod consumers;
mod utils;
@@ -24,6 +23,8 @@ use consumers::{
outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer},
};
use crate::utils::{Consumer, Delegate};
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
// Configure logging and environment
@@ -43,7 +44,24 @@ async fn main() {
panic!("Mongo is not in use, can't connect via authifier!")
}
let mut connections: Vec<(Channel, Connection)> = Vec::new();
let config = config().await;
let connection = Arc::new(
Connection::connect(
&format!(
"amqp://{}:{}@{}:{}",
&config.rabbit.username,
&config.rabbit.password,
&config.rabbit.host,
&config.rabbit.port,
),
ConnectionProperties::default(),
)
.await
.expect("Failed to connect to RabbitMQ"),
);
let mut channels = Vec::new();
// An explainer of how this works:
// The inbound connections are on separate routing keys, such that they only receive the proper payload
@@ -54,171 +72,178 @@ async fn main() {
// This'll require some interesting shimming if we need to add more events once this is in prod (different payloads between prod and test),
// but that sounds like a problem for future us.
let config = config().await;
// inbound: generic
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<GenericConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.generic_queue,
config.pushd.get_generic_routing_key().as_str(),
&config.pushd.get_generic_routing_key(),
None,
GenericConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
// inbound: messages
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<MessageConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.message_queue,
config.pushd.get_message_routing_key().as_str(),
&config.pushd.get_message_routing_key(),
None,
MessageConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
// inbound: FR received
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<FRReceivedConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.fr_received_queue,
config.pushd.get_fr_received_routing_key().as_str(),
&config.pushd.get_fr_received_routing_key(),
None,
FRReceivedConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
// inbound: FR accepted
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<FRAcceptedConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.fr_accepted_queue,
config.pushd.get_fr_accepted_routing_key().as_str(),
&config.pushd.get_fr_accepted_routing_key(),
None,
FRAcceptedConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
// inbound: Mass Mentions
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<MassMessageConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.mass_mention_queue,
config.pushd.get_mass_mention_routing_key().as_str(),
&config.pushd.get_mass_mention_routing_key(),
None,
MassMessageConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
// inbound: Dm Calls
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<DmCallConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.dm_call_queue,
config.pushd.get_dm_call_routing_key().as_str(),
&config.pushd.get_dm_call_routing_key(),
None,
DmCallConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
if !config.pushd.apn.pkcs8.is_empty() {
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<ApnsOutboundConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.apn.queue,
&config.pushd.apn.queue,
None,
ApnsOutboundConsumer::new(db.clone()).await.unwrap(),
)
.await,
);
let mut table = FieldTable::new();
table.insert("x-message-deduplication".try_into().unwrap(), "true".into());
let mut table = FieldTable::default();
table.insert("x-message-deduplication".into(), AMQPValue::Boolean(true));
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<AckConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.ack_queue,
&config.pushd.ack_queue,
Some(table),
AckConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
}
if !config.pushd.fcm.auth_uri.is_empty() {
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<FcmOutboundConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.fcm.queue,
&config.pushd.fcm.queue,
None,
FcmOutboundConsumer::new(db.clone()).await.unwrap(),
)
.await,
)
);
}
if !config.pushd.vapid.public_key.is_empty() {
connections.push(
make_queue_and_consume(
channels.push(
make_queue_and_consume::<VapidOutboundConsumer>(
&db,
&authifier,
&connection,
&config,
&config.pushd.vapid.queue,
&config.pushd.vapid.queue,
None,
VapidOutboundConsumer::new(db.clone()).await.unwrap(),
)
.await,
)
);
}
let guard = Notify::new();
guard.notified().await;
ctrl_c().await.unwrap();
for (channel, conn) in connections {
channel.close().await.expect("Unable to close channel");
conn.close().await.expect("Unable to close connection");
for channel in channels {
let _ = channel.close(0, "close".into()).await;
}
}
async fn make_queue_and_consume<F>(
db: &Database,
authifier_db: &authifier::Database,
connection: &Arc<Connection>,
config: &Settings,
queue_name: &str,
routing_key: &str,
queue_args: Option<FieldTable>,
consumer: F,
) -> (Channel, Connection)
) -> Arc<Channel>
where
F: AsyncConsumer + Send + 'static,
F: Consumer,
{
let connection = Connection::open(&OpenConnectionArguments::new(
&config.rabbit.host,
config.rabbit.port,
&config.rabbit.username,
&config.rabbit.password,
))
.await
.unwrap();
let channel = connection.open_channel(None).await.unwrap();
let channel = Arc::new(connection.create_channel().await.unwrap());
channel
.exchange_declare(
ExchangeDeclareArguments::new(&config.pushd.exchange, "direct")
.durable(true)
.finish(),
config.pushd.exchange.clone().into(),
lapin::ExchangeKind::Direct,
ExchangeDeclareOptions {
durable: true,
..Default::default()
},
FieldTable::default(),
)
.await
.expect("Failed to declare pushd exchange");
.expect("Failed to declare exchange");
let mut queue_name = queue_name.to_string();
@@ -230,35 +255,59 @@ where
let queue_name = queue_name.as_str();
let mut args = QueueDeclareArguments::new(queue_name);
args.durable(true);
if let Some(arg) = queue_args {
args.arguments(arg);
}
let args = args.finish();
_ = channel.queue_declare(args).await.unwrap().unwrap();
let args = QueueDeclareOptions {
durable: true,
..Default::default()
};
channel
.queue_bind(QueueBindArguments::new(
queue_name,
&config.pushd.exchange,
routing_key,
))
.queue_declare(queue_name.into(), args, queue_args.unwrap_or_default())
.await
.unwrap();
channel
.queue_bind(
queue_name.into(),
config.pushd.exchange.clone().into(),
routing_key.into(),
QueueBindOptions::default(),
FieldTable::default(),
)
.await
.expect(
"This probably means the revolt.notifications exchange does not exist in rabbitmq!",
);
let args = BasicConsumeArguments::new(queue_name, "")
.manual_ack(false)
.finish();
let routing_key = channel.basic_consume(consumer, args).await.unwrap();
let consumer = channel
.basic_consume(
queue_name.into(),
"".into(),
BasicConsumeOptions {
no_ack: true,
..Default::default()
},
FieldTable::default(),
)
.await
.unwrap();
info!(
"Consuming routing key {} as queue {}, tag {}",
routing_key, queue_name, routing_key
routing_key,
queue_name,
consumer.tag()
);
(channel, connection)
let delegate = Delegate(
F::create(
db.clone(),
authifier_db.clone(),
connection.clone(),
channel.clone(),
)
.await,
);
consumer.set_delegate(delegate);
channel
}

View File

@@ -0,0 +1,91 @@
use std::{
future::{ready, Future},
pin::Pin,
sync::Arc,
};
use anyhow::Result;
use async_trait::async_trait;
use lapin::{
message::{Delivery, DeliveryResult},
options::BasicPublishOptions,
BasicProperties, Channel, Connection, ConsumerDelegate, Error as AMQPError,
};
use log::debug;
use revolt_database::Database;
#[async_trait]
pub trait Consumer: Clone + Send + Sync + 'static {
async fn create(
db: Database,
authifier_db: authifier::Database,
connection: Arc<Connection>,
channel: Arc<Channel>,
) -> Self;
fn channel(&self) -> &Arc<Channel>;
async fn consume(&self, delivery: Delivery) -> Result<()>;
async fn publish_message_with_options(
&self,
payload: &[u8],
exchange: &str,
routing_key: &str,
options: BasicPublishOptions,
properties: BasicProperties,
) -> Result<(), AMQPError> {
let channel = self.channel();
channel
.basic_publish(
exchange.into(),
routing_key.into(),
options,
payload,
properties,
)
.await?;
debug!("Sent message to queue for target {}", routing_key);
Ok(())
}
async fn publish_message(
&self,
payload: &[u8],
exchange: &str,
routing_key: &str,
) -> Result<(), AMQPError> {
self.publish_message_with_options(
payload,
exchange,
routing_key,
BasicPublishOptions::default(),
BasicProperties::default(),
)
.await
}
}
pub struct Delegate<C: Consumer>(pub C);
impl<C: Consumer> ConsumerDelegate for Delegate<C> {
fn on_new_delivery(
&self,
delivery: DeliveryResult,
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
match delivery {
Ok(Some(delivery)) => {
let consumer = self.0.clone();
Box::pin(async move {
if let Err(e) = consumer.consume(delivery).await {
revolt_config::capture_anyhow(&e);
log::error!("{e:?}");
};
})
}
Ok(None) => Box::pin(ready(())),
Err(e) => Box::pin(async move { log::error!("Received bad delivery: {e:?}") }),
}
}
}

View File

@@ -1,2 +1,5 @@
mod renderer;
mod consumer;
pub use renderer::render_notification_content;
pub use consumer::{Consumer, Delegate};

View File

@@ -44,6 +44,3 @@ revolt-permissions = { workspace = true }
livekit-api = { workspace = true }
livekit-protocol = { workspace = true }
livekit-runtime = { workspace = true, features = ["tokio"] }
# RabbitMQ
amqprs = { workspace = true }

View File

@@ -13,7 +13,7 @@ mod guard;
async fn main() -> Result<(), rocket::Error> {
revolt_config::configure!(voice_ingress);
let amqp = AMQP::new_auto().await.unwrap();
let amqp = AMQP::new_auto().await;
let database = DatabaseInfo::Auto.connect().await.unwrap();
let voice_client = VoiceClient::from_revolt_config().await;