refactor(pushd): add non-panic error handling to all queue consumers

This commit is contained in:
izzy
2025-05-13 10:41:03 +01:00
parent 01e0f9e558
commit 2aff76c369
14 changed files with 460 additions and 184 deletions

View File

@@ -5,7 +5,10 @@ edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies]
revolt-config = { version = "0.8.5", path = "../../core/config" }
revolt-result = { version = "0.8.5", path = "../../core/result" }
revolt-config = { version = "0.8.5", path = "../../core/config", features = [
"report-macros",
] }
revolt-database = { version = "0.8.5", path = "../../core/database" }
revolt-models = { version = "0.8.5", path = "../../core/models", features = [
"validator",
@@ -14,6 +17,8 @@ revolt-presence = { version = "0.8.5", path = "../../core/presence", features =
"redis-is-patched",
] }
anyhow = { version = "1.0.98" }
amqprs = { version = "1.7.0" }
fcm_v1 = "0.3.0"
web-push = "0.10.0"

View File

@@ -7,6 +7,7 @@ use amqprs::{
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use anyhow::Result;
use async_trait::async_trait;
use log::debug;
use revolt_database::{events::rabbit::*, Database};
@@ -54,21 +55,16 @@ impl FRAcceptedConsumer {
channel: None,
}
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FRAcceptedConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
async fn consume_event(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) {
let content = String::from_utf8(content).unwrap();
let payload: FRAcceptedPayload = serde_json::from_str(content.as_str()).unwrap();
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: FRAcceptedPayload = serde_json::from_str(content.as_str())?;
debug!("Received FR accept event");
@@ -111,11 +107,34 @@ impl AsyncConsumer for FRAcceptedConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable).unwrap();
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
}
}
}
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

@@ -7,6 +7,7 @@ use amqprs::{
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use anyhow::Result;
use async_trait::async_trait;
use log::debug;
use revolt_database::{events::rabbit::*, Database};
@@ -54,21 +55,16 @@ impl FRReceivedConsumer {
channel: None,
}
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FRReceivedConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
async fn consume_event(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) {
let content = String::from_utf8(content).unwrap();
let payload: FRReceivedPayload = serde_json::from_str(content.as_str()).unwrap();
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: FRReceivedPayload = serde_json::from_str(content.as_str())?;
debug!("Received FR received event");
@@ -111,11 +107,34 @@ impl AsyncConsumer for FRReceivedConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable).unwrap();
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
}
}
}
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

@@ -7,6 +7,7 @@ use amqprs::{
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use anyhow::Result;
use async_trait::async_trait;
use log::debug;
use revolt_database::{events::rabbit::*, Database};
@@ -54,21 +55,16 @@ impl GenericConsumer {
channel: None,
}
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for GenericConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
async fn consume_event(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) {
let content = String::from_utf8(content).unwrap();
let payload: MessageSentPayload = serde_json::from_str(content.as_str()).unwrap();
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: MessageSentPayload = serde_json::from_str(content.as_str())?;
debug!("Received message event on origin");
@@ -117,11 +113,34 @@ impl AsyncConsumer for GenericConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable).unwrap();
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
}
}
}
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

@@ -10,6 +10,7 @@ use amqprs::{
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use anyhow::Result;
use async_trait::async_trait;
use revolt_database::{
events::rabbit::*, util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member,
@@ -61,7 +62,11 @@ impl MassMessageConsumer {
}
}
async fn fire_notification_for_users(&mut self, push: &PushNotification, users: &[String]) {
async fn fire_notification_for_users(
&mut self,
push: &PushNotification,
users: &[String],
) -> Result<()> {
if let Ok(sessions) = self
.authifier_db
.find_sessions_with_subscription(users)
@@ -105,29 +110,26 @@ impl MassMessageConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable).unwrap();
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
}
}
}
}
}
#[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(
Ok(())
}
async fn consume_event(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) {
) -> Result<()> {
let config = revolt_config::config().await;
let content = String::from_utf8(content).unwrap();
let payload: MassMessageSentPayload = serde_json::from_str(content.as_str()).unwrap();
let content = String::from_utf8(content)?;
let payload: MassMessageSentPayload = serde_json::from_str(content.as_str())?;
debug!("Received mass message event");
@@ -159,8 +161,7 @@ impl AsyncConsumer for MassMessageConsumer {
let mut db_query = self
.db
.fetch_all_members_chunked(&payload.server_id)
.await
.expect("Failed to fetch members from database");
.await?;
let mut exhausted = false;
let ack_chnl = vec![push.channel.id().to_string()];
@@ -203,7 +204,8 @@ impl AsyncConsumer for MassMessageConsumer {
target_users, online_users
);
self.fire_notification_for_users(&push, &target_users).await;
self.fire_notification_for_users(&push, &target_users)
.await?;
if exhausted {
break;
@@ -211,19 +213,11 @@ impl AsyncConsumer for MassMessageConsumer {
}
} else if let Some(roles) = &push.message.role_mentions {
// role mentions
let _role_members = self
let mut role_members = self
.db
.fetch_all_members_with_roles_chunked(&payload.server_id, roles)
.await;
.await?;
debug!("role members: {:?}", _role_members);
if _role_members.is_err() {
revolt_config::capture_error(&_role_members.err().unwrap());
return;
}
let mut role_members = _role_members.unwrap();
let mut chunk = vec![];
let mut exhausted = false;
@@ -266,10 +260,33 @@ impl AsyncConsumer for MassMessageConsumer {
debug!("targets: {:?}", targets);
self.fire_notification_for_users(&push, &targets).await;
self.fire_notification_for_users(&push, &targets).await?;
}
}
}
}
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

@@ -7,6 +7,7 @@ use amqprs::{
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use anyhow::Result;
use async_trait::async_trait;
use log::debug;
use revolt_database::{events::rabbit::*, Database};
@@ -54,21 +55,16 @@ impl MessageConsumer {
channel: None,
}
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for MessageConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
async fn consume_event(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) {
let content = String::from_utf8(content).unwrap();
let payload: MessageSentPayload = serde_json::from_str(content.as_str()).unwrap();
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: MessageSentPayload = serde_json::from_str(content.as_str())?;
debug!("Received message event on origin");
@@ -117,11 +113,34 @@ impl AsyncConsumer for MessageConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable).unwrap();
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
}
}
}
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

@@ -1,6 +1,7 @@
use std::{borrow::Cow, collections::BTreeMap, io::Cursor};
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use base64::{
engine::{self},
@@ -122,20 +123,16 @@ impl ApnsOutboundConsumer {
Ok(ApnsOutboundConsumer { db, client })
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for ApnsOutboundConsumer {
async fn consume(
async fn consume_event(
&mut self,
channel: &AmqpChannel,
deliver: Deliver,
basic_properties: BasicProperties,
_channel: &AmqpChannel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) {
let content = String::from_utf8(content).unwrap();
let payload: PayloadToService = serde_json::from_str(content.as_str()).unwrap();
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str())?;
let payload_options = NotificationOptions {
apns_id: None,
@@ -159,7 +156,7 @@ impl AsyncConsumer for ApnsOutboundConsumer {
alert.from_user.username, alert.from_user.discriminator
)))
.clone()
.unwrap(),
.ok_or_else(|| anyhow!("missing name"))?,
)];
let apn_payload = Payload {
@@ -205,7 +202,7 @@ impl AsyncConsumer for ApnsOutboundConsumer {
alert.accepted_user.username, alert.accepted_user.discriminator
)))
.clone()
.unwrap(),
.ok_or_else(|| anyhow!("missing name"))?,
)];
let apn_payload = Payload {
@@ -355,5 +352,27 @@ impl AsyncConsumer for ApnsOutboundConsumer {
}
}
}
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

@@ -2,6 +2,7 @@ use std::{collections::HashMap, time::Duration};
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use fcm_v1::{
android::AndroidConfig,
@@ -64,22 +65,16 @@ impl FcmOutboundConsumer {
),
})
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FcmOutboundConsumer {
async fn consume(
async fn consume_event(
&mut self,
channel: &AmqpChannel,
deliver: Deliver,
basic_properties: BasicProperties,
_channel: &AmqpChannel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) {
let content = String::from_utf8(content).unwrap();
let payload: PayloadToService = serde_json::from_str(content.as_str()).unwrap();
let config = revolt_config::config().await;
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str())?;
#[allow(clippy::needless_late_init)]
let resp: Result<Message, FcmError>;
@@ -94,7 +89,7 @@ impl AsyncConsumer for FcmOutboundConsumer {
alert.from_user.username, alert.from_user.discriminator
)))
.clone()
.unwrap();
.ok_or_else(|| anyhow!("missing name"))?;
let mut data = HashMap::new();
data.insert(
@@ -122,7 +117,7 @@ impl AsyncConsumer for FcmOutboundConsumer {
alert.accepted_user.username, alert.accepted_user.discriminator
)))
.clone()
.unwrap();
.ok_or_else(|| anyhow!("missing name"))?;
let mut data: HashMap<String, Value> = HashMap::new();
data.insert(
@@ -175,7 +170,7 @@ impl AsyncConsumer for FcmOutboundConsumer {
}
PayloadKind::BadgeUpdate(_) => {
panic!("FCM cannot handle badge updates, and they should not be sent here.")
bail!("FCM cannot handle badge updates and they should not be sent here.");
}
}
@@ -195,5 +190,27 @@ impl AsyncConsumer for FcmOutboundConsumer {
}
}
}
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

@@ -2,13 +2,13 @@ use std::collections::HashMap;
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait;
use base64::{
engine::{self},
Engine as _,
};
use revolt_database::{events::rabbit::*, Database};
// use revolt_models::v0::{Channel, PushNotification};
use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushError, WebPushMessageBuilder,
@@ -21,11 +21,11 @@ pub struct VapidOutboundConsumer {
}
impl VapidOutboundConsumer {
pub async fn new(db: Database) -> Result<VapidOutboundConsumer, &'static str> {
pub async fn new(db: Database) -> Result<VapidOutboundConsumer> {
let config = revolt_config::config().await;
if config.pushd.vapid.private_key.is_empty() | config.pushd.vapid.public_key.is_empty() {
return Err("No Vapid keys present");
bail!("no Vapid keys present");
}
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
@@ -38,28 +38,30 @@ impl VapidOutboundConsumer {
pkey: web_push_private_key,
})
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for VapidOutboundConsumer {
async fn consume(
async fn consume_event(
&mut self,
channel: &AmqpChannel,
deliver: Deliver,
basic_properties: BasicProperties,
_channel: &AmqpChannel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) {
let content = String::from_utf8(content).unwrap();
let payload: PayloadToService = serde_json::from_str(content.as_str()).unwrap();
let config = revolt_config::config().await;
) -> Result<()> {
let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str())?;
let subscription = SubscriptionInfo {
endpoint: payload.extras.get("endpoint").unwrap().clone(),
endpoint: payload
.extras
.get("endpoint")
.ok_or_else(|| anyhow!("missing endpoint"))?
.clone(),
keys: SubscriptionKeys {
auth: payload.token,
p256dh: payload.extras.get("p256dh").unwrap().clone(),
p256dh: payload
.extras
.get("p256dh")
.ok_or_else(|| anyhow!("missing p256dh"))?
.clone(),
},
};
@@ -76,12 +78,12 @@ impl AsyncConsumer for VapidOutboundConsumer {
alert.from_user.username, alert.from_user.discriminator
)))
.clone()
.unwrap();
.ok_or_else(|| anyhow!("missing name"))?;
let mut body = HashMap::new();
body.insert("body", format!("{} sent you a friend request", name));
payload_body = serde_json::to_string(&body).unwrap();
payload_body = serde_json::to_string(&body)?;
}
PayloadKind::FRAccepted(alert) => {
let name = alert
@@ -92,21 +94,21 @@ impl AsyncConsumer for VapidOutboundConsumer {
alert.accepted_user.username, alert.accepted_user.discriminator
)))
.clone()
.unwrap();
.ok_or_else(|| anyhow!("missing name"))?;
let mut body = HashMap::new();
body.insert("body", format!("{} accepted your friend request", name));
payload_body = serde_json::to_string(&body).unwrap();
payload_body = serde_json::to_string(&body)?;
}
PayloadKind::Generic(alert) => {
payload_body = serde_json::to_string(&alert).unwrap();
payload_body = serde_json::to_string(&alert)?;
}
PayloadKind::MessageNotification(alert) => {
payload_body = serde_json::to_string(&alert).unwrap();
payload_body = serde_json::to_string(&alert)?;
}
PayloadKind::BadgeUpdate(_) => {
panic!("Vapid cannot handle badge updates, and they should not be sent here.")
bail!("Vapid cannot handle badge updates and they should not be sent here.");
}
}
@@ -122,28 +124,40 @@ impl AsyncConsumer for VapidOutboundConsumer {
Ok(msg) => {
if let Err(err) = self.client.send(msg).await {
if err == WebPushError::Unauthorized {
if let Err(err) = self
.db
self.db
.remove_push_subscription_by_session_id(&payload.session_id)
.await
{
revolt_config::capture_error(&err);
}
.await?;
}
}
Ok(())
}
Err(err) => {
revolt_config::capture_error(&err);
}
Err(err) => Err(err.into()),
}
}
Err(err) => {
revolt_config::capture_error(&err);
}
Err(err) => Err(err.into()),
},
Err(err) => {
revolt_config::capture_error(&err);
}
Err(err) => Err(err.into()),
}
}
}
#[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:?}");
}
}
}

View File

@@ -27,6 +27,9 @@ async fn main() {
let config = config().await;
pretty_env_logger::init();
// Configure logging and environment
revolt_config::configure!(pushd);
// Setup database
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
let authifier: authifier::Database;