Feat: Push notification server (#387)

* feat: create base of push daemon

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

* Add outbound senders

* Make web_push send to rabbit instead (temp stuff)

* feat: stability and friend requests

* make vapid fr stuff not suck

* swap naming of queue

* move pushd into daemons folder

* fix cargo file for move into daemons folder

* feat: probably working fcm push notifs

* comment out fcm webpush stuff since the config keys dont exist

* fix fcm, name queues according to their prod status and configure routing keys

* add pushd to docker

* mix: Remove old code, add stuff to pushd

* fix: lockfile

* feat: update rocket to 5.0.1

* fix: fix queues and ack bugs

* Move rabbit messsage processing into ack queue

* chore: update readme

* chore: optimizations for ack database hits

* pushd flowchart

* misc: update flowchart

* exit dependancy hell

* add rocket_impl flag to authifier

* make the tests file of delta actually compile

* fix: don't silence every push message

* fix: don't silence all messages

* add debug logging for sending data to rabbit from message events

* validate mentions at a server membership level

* put back that import that was actually important

* minor fix to lockfile

* update delta authifier

* feat: proper permissions for push notifications

* add unit test for mention sanitization

* remove local file dependancy on authifier

* update ports to proper defaults

* fixTM the node bindings

* Theoretically configure docker releases for pushd

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

* declare exchange in pushd and delta

* fix createbuckets script

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

* fix: reference db implementation

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

* fix: remove finally redundant code

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

* fix: changes

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

* fix: other changes

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

* fix: make channel name return result

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

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
This commit is contained in:
Tom
2024-11-28 13:34:17 -08:00
committed by GitHub
parent ed5ded5e45
commit b55765d7c7
71 changed files with 3463 additions and 1059 deletions

View File

@@ -52,20 +52,21 @@ async-std = { version = "1.8.0", features = [
lettre = "0.10.0-alpha.4"
# web
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
"json",
] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" }
rocket = { version = "0.5.1", default-features = false, features = ["json"] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" }
rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_authifier = { version = "1.0.8" }
rocket_authifier = { version = "1.0.9" }
rocket_prometheus = "0.10.0-rc.3"
# spec generation
schemars = "0.8.8"
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
revolt_rocket_okapi = { version = "0.10.0", features = ["swagger"] }
# rabbit
amqprs = { version = "1.7.0" }
# core
authifier = "1.0.8"
authifier = "1.0.9"
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database", features = [
"rocket-impl",

View File

@@ -10,12 +10,17 @@ pub mod util;
use revolt_config::config;
use revolt_database::events::client::EventV1;
use revolt_database::AMQP;
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics;
use std::net::Ipv4Addr;
use std::str::FromStr;
use amqprs::{
channel::ExchangeDeclareArguments,
connection::{Connection, OpenConnectionArguments},
};
use async_std::channel::unbounded;
use authifier::AuthifierEvent;
use rocket::data::ToByteUnit;
@@ -32,7 +37,7 @@ pub async fn web() -> Rocket<Build> {
db.migrate_database().await.unwrap();
// Setup Authifier event channel
let (sender, receiver) = unbounded();
let (_, receiver) = unbounded();
// Setup Authifier
let authifier = db.clone().to_authifier().await;
@@ -53,9 +58,6 @@ pub async fn web() -> Rocket<Build> {
}
});
// Launch background task workers
revolt_database::tasks::start_workers(db.clone(), authifier.database.clone());
// Configure CORS
let cors = CorsOptions {
allowed_origins: AllowedOrigins::All,
@@ -79,6 +81,31 @@ pub async fn web() -> Rocket<Build> {
)
.into();
// Configure Rabbit
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();
channel
.exchange_declare(
ExchangeDeclareArguments::new(&config.pushd.exchange, "direct")
.durable(true)
.finish(),
)
.await
.expect("Failed to declare exchange");
let amqp = AMQP::new(connection, channel);
// Launch background task workers
revolt_database::tasks::start_workers(db.clone(), amqp.clone());
// Configure Rocket
let rocket = rocket::build();
let prometheus = PrometheusMetrics::new();
@@ -91,6 +118,7 @@ pub async fn web() -> Rocket<Build> {
.mount("/swagger/", swagger)
.manage(authifier)
.manage(db)
.manage(amqp)
.manage(cors.clone())
.attach(util::ratelimiter::RatelimitFairing)
.attach(cors)

View File

@@ -1,6 +1,6 @@
use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::Member;
use revolt_database::{util::reference::Reference, Database, User};
use revolt_database::{Member, AMQP};
use revolt_models::v0;
use revolt_permissions::{
calculate_channel_permissions, calculate_server_permissions, ChannelPermission,
@@ -18,6 +18,7 @@ use rocket_empty::EmptyResponse;
#[post("/<target>/invite", data = "<dest>")]
pub async fn invite_bot(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
dest: Json<v0::InviteBotDestination>,
@@ -55,7 +56,7 @@ pub async fn invite_bot(
.throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
channel
.add_user_to_group(db, &bot_user, &user.id)
.add_user_to_group(db, amqp, &bot_user, &user.id)
.await
.map(|_| EmptyResponse)
}
@@ -93,9 +94,12 @@ mod test {
.client
.post(format!("/bots/{}/invite", bot.id))
.header(ContentType::JSON)
.body(json!(v0::InviteBotDestination::Group {
group: group.id().to_string()
}).to_string())
.body(
json!(v0::InviteBotDestination::Group {
group: group.id().to_string()
})
.to_string(),
)
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;

View File

@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, PartialChannel, User,
Channel, Database, PartialChannel, User, AMQP,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -15,6 +15,7 @@ use rocket_empty::EmptyResponse;
#[delete("/<target>?<options..>")]
pub async fn delete(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
options: v0::OptionsChannelDelete,
@@ -39,7 +40,13 @@ pub async fn delete(
.await
.map(|_| EmptyResponse),
Channel::Group { .. } => channel
.remove_user_from_group(db, &user, None, options.leave_silently.unwrap_or_default())
.remove_user_from_group(
db,
amqp,
&user,
None,
options.leave_silently.unwrap_or_default(),
)
.await
.map(|_| EmptyResponse),
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {

View File

@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, File, PartialChannel, SystemMessage, User,
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -15,6 +15,7 @@ use validator::Validate;
#[patch("/<target>", data = "<data>")]
pub async fn edit(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
data: Json<v0::DataEditChannel>,
@@ -73,7 +74,15 @@ pub async fn edit(
return Err(create_error!(InvalidOperation));
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), None, None, &channel, false)
.send(
db,
Some(amqp),
user.as_author_for_system(),
None,
None,
&channel,
false,
)
.await
.ok();
}
@@ -151,7 +160,15 @@ pub async fn edit(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), None, None, &channel, false)
.send(
db,
Some(amqp),
user.as_author_for_system(),
None,
None,
&channel,
false,
)
.await
.ok();
}
@@ -161,7 +178,15 @@ pub async fn edit(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), None, None, &channel, false)
.send(
db,
Some(amqp),
user.as_author_for_system(),
None,
None,
&channel,
false,
)
.await
.ok();
}
@@ -171,7 +196,15 @@ pub async fn edit(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), None, None, &channel, false)
.send(
db,
Some(amqp),
user.as_author_for_system(),
None,
None,
&channel,
false,
)
.await
.ok();
}

View File

@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User,
Channel, Database, User, AMQP,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
@@ -15,6 +15,7 @@ use rocket_empty::EmptyResponse;
#[put("/<group_id>/recipients/<member_id>")]
pub async fn add_member(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
group_id: Reference,
member_id: Reference,
@@ -38,7 +39,7 @@ pub async fn add_member(
}
channel
.add_user_to_group(db, &member, &user.id)
.add_user_to_group(db, amqp, &member, &user.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -1,4 +1,4 @@
use revolt_database::{util::reference::Reference, Channel, Database, User};
use revolt_database::{util::reference::Reference, Channel, Database, User, AMQP};
use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result};
@@ -12,6 +12,7 @@ use rocket_empty::EmptyResponse;
#[delete("/<target>/recipients/<member>")]
pub async fn remove_member(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
member: Reference,
@@ -42,7 +43,7 @@ pub async fn remove_member(
}
channel
.remove_user_from_group(db, &member, Some(&user.id), false)
.remove_user_from_group(db, amqp, &member, Some(&user.id), false)
.await
.map(|_| EmptyResponse)
}

View File

@@ -1,5 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, PartialMessage, SystemMessage, User
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, PartialMessage, SystemMessage, User, AMQP,
};
use revolt_models::v0::MessageAuthor;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -14,6 +15,7 @@ use rocket_empty::EmptyResponse;
#[post("/<target>/messages/<msg>/pin")]
pub async fn message_pin(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
msg: Reference,
@@ -28,30 +30,38 @@ pub async fn message_pin(
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
if message.pinned.unwrap_or_default() {
return Err(create_error!(AlreadyPinned))
return Err(create_error!(AlreadyPinned));
}
message.update(db, PartialMessage {
pinned: Some(true),
..Default::default()
}, vec![]).await?;
message
.update(
db,
PartialMessage {
pinned: Some(true),
..Default::default()
},
vec![],
)
.await?;
SystemMessage::MessagePinned {
id: message.id.clone(),
by: user.id.clone()
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(
db,
Some(amqp),
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref())
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
&channel,
false
).await?;
false,
)
.await?;
Ok(EmptyResponse)
}
@@ -59,7 +69,11 @@ pub async fn message_pin(
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, util::{idempotency::IdempotencyKey, reference::Reference}, Member, Message, Server};
use revolt_database::{
events::client::EventV1,
util::{idempotency::IdempotencyKey, reference::Reference},
Member, Message, Server,
};
use revolt_models::v0::{self, SystemMessage};
use rocket::http::{Header, Status};
@@ -75,24 +89,29 @@ mod test {
..Default::default()
},
&user,
true
).await.expect("Failed to create test server");
true,
)
.await
.expect("Failed to create test server");
let (member, channels) = Member::create(&harness.db, &server, &user, Some(channels)).await.expect("Failed to create member");
let (member, channels) = Member::create(&harness.db, &server, &user, Some(channels))
.await
.expect("Failed to create member");
let channel = &channels[0];
let message = Message::create_from_api(
&harness.db,
None,
channel.clone(),
v0::DataMessageSend {
content:Some("Test message".to_string()),
content: Some("Test message".to_string()),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
@@ -100,14 +119,18 @@ mod test {
user.limits().await,
IdempotencyKey::unchecked_from_string("0".to_string()),
false,
false
false,
)
.await
.expect("Failed to create message");
let response = harness
.client
.post(format!("/channels/{}/messages/{}/pin", channel.id(), &message.id))
.post(format!(
"/channels/{}/messages/{}/pin",
channel.id(),
&message.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
@@ -115,34 +138,37 @@ mod test {
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness.wait_for_event(channel.id(), |event| {
match event {
EventV1::Message(message) => {
match &message.system {
Some(SystemMessage::MessagePinned { by, .. }) => {
assert_eq!(by, &user.id);
harness
.wait_for_event(channel.id(), |event| match event {
EventV1::Message(message) => match &message.system {
Some(SystemMessage::MessagePinned { by, .. }) => {
assert_eq!(by, &user.id);
true
},
_ => false
true
}
_ => false,
},
_ => false
}
}).await;
_ => false,
})
.await;
harness.wait_for_event(channel.id(), |event| {
match event {
EventV1::MessageUpdate { id, channel: channel_id, data, .. } => {
harness
.wait_for_event(channel.id(), |event| match event {
EventV1::MessageUpdate {
id,
channel: channel_id,
data,
..
} => {
assert_eq!(id, &message.id);
assert_eq!(channel_id, channel.id());
assert_eq!(data.pinned, Some(true));
true
},
_ => false
}
}).await;
}
_ => false,
})
.await;
let updated_message = Reference::from_unchecked(message.id)
.as_message(&harness.db)

View File

@@ -3,7 +3,7 @@ use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::{
util::idempotency::IdempotencyKey, util::reference::Reference, Database, User,
};
use revolt_database::{Interactions, Message};
use revolt_database::{Interactions, Message, AMQP};
use revolt_models::v0;
use revolt_permissions::PermissionQuery;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -19,6 +19,7 @@ use validator::Validate;
#[post("/<target>/messages", data = "<data>")]
pub async fn message_send(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
data: Json<v0::DataMessageSend>,
@@ -93,6 +94,7 @@ pub async fn message_send(
Ok(Json(
Message::create_from_api(
db,
Some(amqp),
channel,
data,
v0::MessageAuthor::User(&author),
@@ -107,3 +109,218 @@ pub async fn message_send(
.into_model(Some(model_user), model_member),
))
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use crate::{rocket, util::test::TestHarness};
use revolt_database::{
util::{idempotency::IdempotencyKey, reference::Reference},
Channel, Member, Message, PartialChannel, PartialMember, Role, Server,
};
use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_permissions::{ChannelPermission, OverrideField};
#[rocket::async_test]
async fn message_mention_constraints() {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let (_, _, second_user) = harness.new_user().await;
let (server, channels) = Server::create(
&harness.db,
v0::DataCreateServer {
name: "Test Server".to_string(),
..Default::default()
},
&user,
true,
)
.await
.expect("Failed to create test server");
let server_mut: &mut Server = &mut server.clone();
let mut locked_channel = Channel::create_server_channel(
&harness.db,
server_mut,
DataCreateServerChannel {
channel_type: v0::LegacyServerChannelType::Text,
name: "Hidden Channel".to_string(),
description: None,
nsfw: Some(false),
},
true,
)
.await
.expect("Failed to make new channel");
let role = Role {
name: "Show Hidden Channel".to_string(),
permissions: OverrideField { a: 0, d: 0 },
colour: None,
hoist: false,
rank: 5,
};
let role_id = role
.create(&harness.db, &server.id)
.await
.expect("Failed to create the role");
let mut overrides = HashMap::new();
overrides.insert(
role_id.clone(),
OverrideField {
a: (ChannelPermission::ViewChannel) as i64,
d: 0,
},
);
let partial = PartialChannel {
name: None,
owner: None,
description: None,
icon: None,
nsfw: None,
active: None,
permissions: None,
role_permissions: Some(overrides),
default_permissions: Some(OverrideField {
a: 0,
d: ChannelPermission::ViewChannel as i64,
}),
last_message_id: None,
};
locked_channel
.update(&harness.db, partial, vec![])
.await
.expect("Failed to update the channel permissions for special role");
Member::create(&harness.db, &server, &user, Some(channels.clone()))
.await
.expect("Failed to create member");
let member = Reference::from_unchecked(user.id.clone())
.as_member(&harness.db, &server.id)
.await
.expect("Failed to get member");
// Second user is not part of the server
let message = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
locked_channel.clone(),
v0::DataMessageSend {
content: Some(format!("<@{}>", second_user.id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("0".to_string()),
false,
true,
)
.await
.expect("Failed to create message");
// The mention should not go through here
assert!(
message.mentions.is_none() || message.mentions.unwrap().is_empty(),
"Mention failed to be scrubbed when the user is not part of the server"
);
Member::create(&harness.db, &server, &second_user, Some(channels.clone()))
.await
.expect("Failed to create second member");
let mut second_member = Reference::from_unchecked(second_user.id.clone())
.as_member(&harness.db, &server.id)
.await
.expect("Failed to get second member");
// Second user cannot see the channel
let message = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
locked_channel.clone(),
v0::DataMessageSend {
content: Some(format!("<@{}>", second_user.id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
true,
)
.await
.expect("Failed to create message");
// The mention should not go through here
assert!(
message.mentions.is_none() || message.mentions.unwrap().is_empty(),
"Mention failed to be scrubbed when the user cannot see the channel"
);
let second_member_roles = vec![role_id.clone()];
let partial = PartialMember {
id: None,
joined_at: None,
nickname: None,
avatar: None,
timeout: None,
roles: Some(second_member_roles),
};
second_member
.update(&harness.db, partial, vec![])
.await
.expect("Failed to update the second user's roles");
// This time the mention SHOULD go through
let message = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
locked_channel.clone(),
v0::DataMessageSend {
content: Some(format!("<@{}>", second_user.id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("2".to_string()),
false,
true,
)
.await
.expect("Failed to create message");
// The mention SHOULD go through here
assert!(
message.mentions.is_some() && !message.mentions.unwrap().is_empty(),
"Mention was scrubbed when the user can see the channel"
);
}
}

View File

@@ -1,5 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, FieldsMessage, PartialMessage, SystemMessage, User
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, FieldsMessage, PartialMessage, SystemMessage, User, AMQP,
};
use revolt_models::v0::MessageAuthor;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -14,6 +15,7 @@ use rocket_empty::EmptyResponse;
#[delete("/<target>/messages/<msg>/pin")]
pub async fn message_unpin(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
msg: Reference,
@@ -28,27 +30,31 @@ pub async fn message_unpin(
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
if !message.pinned.unwrap_or_default() {
return Err(create_error!(NotPinned))
return Err(create_error!(NotPinned));
}
message.update(db, PartialMessage::default(), vec![FieldsMessage::Pinned]).await?;
message
.update(db, PartialMessage::default(), vec![FieldsMessage::Pinned])
.await?;
SystemMessage::MessageUnpinned {
id: message.id.clone(),
by: user.id.clone()
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(
db,
Some(amqp),
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref())
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
&channel,
false
).await?;
false,
)
.await?;
Ok(EmptyResponse)
}
@@ -56,7 +62,11 @@ pub async fn message_unpin(
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, util::{idempotency::IdempotencyKey, reference::Reference}, Member, Message, PartialMessage, Server};
use revolt_database::{
events::client::EventV1,
util::{idempotency::IdempotencyKey, reference::Reference},
Member, Message, PartialMessage, Server,
};
use revolt_models::v0::{self, FieldsMessage, SystemMessage};
use rocket::http::{Header, Status};
@@ -72,26 +82,34 @@ mod test {
..Default::default()
},
&user,
true
).await.expect("Failed to create test server");
true,
)
.await
.expect("Failed to create test server");
let channel = &channels[0];
Member::create(&harness.db, &server, &user, Some(channels.clone())).await.expect("Failed to create member");
let member = Reference::from_unchecked(user.id.clone()).as_member(&harness.db, &server.id).await.expect("Failed to get member");
Member::create(&harness.db, &server, &user, Some(channels.clone()))
.await
.expect("Failed to create member");
let member = Reference::from_unchecked(user.id.clone())
.as_member(&harness.db, &server.id)
.await
.expect("Failed to get member");
let message = Message::create_from_api(
&harness.db,
None,
channel.clone(),
v0::DataMessageSend {
content:Some("Test message".to_string()),
content: Some("Test message".to_string()),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
@@ -99,23 +117,31 @@ mod test {
user.limits().await,
IdempotencyKey::unchecked_from_string("0".to_string()),
false,
false
false,
)
.await
.expect("Failed to create message");
harness.db.update_message(
&message.id,
&PartialMessage {
pinned: Some(true),
..Default::default()
},
vec![]
).await.expect("Failed to update message");
harness
.db
.update_message(
&message.id,
&PartialMessage {
pinned: Some(true),
..Default::default()
},
vec![],
)
.await
.expect("Failed to update message");
let response = harness
.client
.delete(format!("/channels/{}/messages/{}/pin", channel.id(), &message.id))
.delete(format!(
"/channels/{}/messages/{}/pin",
channel.id(),
&message.id
))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
@@ -123,33 +149,31 @@ mod test {
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness.wait_for_event(channel.id(), |event| {
match event {
EventV1::Message(message) => {
match &message.system {
Some(SystemMessage::MessageUnpinned { by, .. }) => {
assert_eq!(by, &user.id);
harness
.wait_for_event(channel.id(), |event| match event {
EventV1::Message(message) => match &message.system {
Some(SystemMessage::MessageUnpinned { by, .. }) => {
assert_eq!(by, &user.id);
true
},
_ => false
true
}
_ => false,
},
_ => false
}
}).await;
_ => false,
})
.await;
harness.wait_for_event(channel.id(), |event| {
match event {
harness
.wait_for_event(channel.id(), |event| match event {
EventV1::MessageUpdate { id, clear, .. } => {
assert_eq!(&message.id, id);
assert_eq!(clear, &[FieldsMessage::Pinned]);
true
},
_ => false
}
}).await;
}
_ => false,
})
.await;
let updated_message = Reference::from_unchecked(message.id)
.as_message(&harness.db)

View File

@@ -1,4 +1,4 @@
use revolt_database::{util::reference::Reference, Channel, Database, Invite, Member, User};
use revolt_database::{util::reference::Reference, Channel, Database, Invite, Member, User, AMQP};
use revolt_models::v0::{self, InviteJoinResponse};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
@@ -10,6 +10,7 @@ use rocket::{serde::json::Json, State};
#[post("/<target>")]
pub async fn join(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
) -> Result<Json<v0::InviteJoinResponse>> {
@@ -34,7 +35,7 @@ pub async fn join(
channel, creator, ..
} => {
let mut channel = db.fetch_channel(channel).await?;
channel.add_user_to_group(db, &user, creator).await?;
channel.add_user_to_group(db, amqp, &user, creator).await?;
if let Channel::Group { recipients, .. } = &channel {
Ok(Json(InviteJoinResponse::Group {
users: User::fetch_many_ids_as_mutuals(db, &user, recipients).await?,

View File

@@ -114,7 +114,7 @@ pub async fn root() -> Result<Json<RevoltConfig>> {
},
ws: config.hosts.events,
app: config.hosts.app,
vapid: config.api.vapid.public_key,
vapid: config.pushd.vapid.public_key,
build: BuildInformation {
commit_sha: option_env!("VERGEN_GIT_SHA")
.unwrap_or_else(|| "<failed to generate>")

View File

@@ -1,5 +1,5 @@
use revolt_database::util::reference::Reference;
use revolt_database::{Database, User};
use revolt_database::{Database, User, AMQP};
use revolt_models::v0;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
@@ -12,6 +12,7 @@ use rocket::State;
#[put("/<target>/friend")]
pub async fn add(
db: &State<Database>,
amqp: &State<AMQP>,
mut user: User,
target: Reference,
) -> Result<Json<v0::User>> {
@@ -21,6 +22,6 @@ pub async fn add(
return Err(create_error!(IsBot));
}
user.add_friend(db, &mut target).await?;
user.add_friend(db, amqp, &mut target).await?;
Ok(Json(target.into(db, &user).await))
}

View File

@@ -1,5 +1,5 @@
use revolt_database::util::reference::Reference;
use revolt_database::{Database, User};
// use revolt_database::util::reference::Reference;
use revolt_database::{Database, User, AMQP};
use revolt_models::v0;
use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
@@ -12,6 +12,7 @@ use rocket::State;
#[post("/friend", data = "<data>")]
pub async fn send_friend_request(
db: &State<Database>,
amqp: &State<AMQP>,
mut user: User,
data: Json<v0::DataSendFriendRequest>,
) -> Result<Json<v0::User>> {
@@ -22,7 +23,7 @@ pub async fn send_friend_request(
return Err(create_error!(IsBot));
}
user.add_friend(db, &mut target).await?;
user.add_friend(db, amqp, &mut target).await?;
Ok(Json(target.into(db, &user).await))
} else {
Err(create_error!(InvalidProperty))

View File

@@ -1,7 +1,7 @@
use revolt_config::config;
use revolt_database::{
util::{idempotency::IdempotencyKey, reference::Reference},
Database, Message,
Database, Message, AMQP,
};
use revolt_models::v0;
use revolt_permissions::{ChannelPermission, PermissionValue};
@@ -17,6 +17,7 @@ use validator::Validate;
#[post("/<webhook_id>/<token>", data = "<data>")]
pub async fn webhook_execute(
db: &State<Database>,
amqp: &State<AMQP>,
webhook_id: Reference,
token: String,
data: Json<v0::DataMessageSend>,
@@ -56,6 +57,7 @@ pub async fn webhook_execute(
Ok(Json(
Message::create_from_api(
db,
Some(amqp),
channel,
data,
v0::MessageAuthor::Webhook(&webhook.into()),

View File

@@ -1,4 +1,4 @@
use revolt_database::{util::reference::Reference, Database, Message};
use revolt_database::{util::reference::Reference, Database, Message, AMQP};
use revolt_models::v0::{MessageAuthor, SendableEmbed, Webhook};
use revolt_result::{create_error, Error, Result};
use revolt_rocket_okapi::{
@@ -635,7 +635,10 @@ impl<'r> FromRequest<'r> for EventHeader<'r> {
async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
let headers = request.headers();
let Some(event) = headers.get_one("X-GitHub-Event") else {
return rocket::request::Outcome::Failure((Status::BadRequest, create_error!(InvalidOperation)))
return rocket::request::Outcome::Error((
Status::BadRequest,
create_error!(InvalidOperation),
));
};
rocket::request::Outcome::Success(Self(event))
@@ -747,6 +750,7 @@ fn convert_event(data: &str, event_name: &str) -> Result<Event> {
#[post("/<webhook_id>/<token>/github", data = "<data>")]
pub async fn webhook_execute_github(
db: &State<Database>,
amqp: &State<AMQP>,
webhook_id: Reference,
token: String,
event: EventHeader<'_>,
@@ -784,7 +788,9 @@ pub async fn webhook_execute_github(
r#ref,
..
}) => {
let Some(branch) = r#ref.split('/').nth(2) else { return Ok(()) };
let Some(branch) = r#ref.split('/').nth(2) else {
return Ok(());
};
if forced {
let description = format!(
@@ -1074,6 +1080,7 @@ pub async fn webhook_execute_github(
message
.send(
db,
Some(amqp),
MessageAuthor::Webhook(&webhook.into()),
None,
None,

View File

@@ -235,7 +235,7 @@ impl<'r> FromRequest<'r> for Ratelimiter {
match ratelimiter {
Ok(ratelimiter) => Outcome::Success(*ratelimiter),
Err(ratelimiter) => Outcome::Failure((Status::TooManyRequests, *ratelimiter)),
Err(ratelimiter) => Outcome::Error((Status::TooManyRequests, *ratelimiter)),
}
}
}
@@ -264,7 +264,7 @@ impl Fairing for RatelimitFairing {
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
use rocket::outcome::Outcome;
if let Outcome::Failure(_) = request.guard::<Ratelimiter>().await {
if let Outcome::Error(_) = request.guard::<Ratelimiter>().await {
info!(
"User rate-limited on route {}! (IP = {:?})",
request.uri(),
@@ -278,7 +278,7 @@ impl Fairing for RatelimitFairing {
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
let guard = request.guard::<Ratelimiter>().await;
let (Outcome::Success(ratelimiter) | Outcome::Failure((_, ratelimiter))) = guard else {
let (Outcome::Success(ratelimiter) | Outcome::Error((_, ratelimiter))) = guard else {
unreachable!()
};
let Ratelimiter {
@@ -293,7 +293,7 @@ impl Fairing for RatelimitFairing {
response.set_raw_header("X-RateLimit-Remaining", remaining.to_string());
response.set_raw_header("X-RateLimit-Reset-After", reset.to_string());
if guard.is_failure() {
if guard.is_error() {
response.set_status(Status::TooManyRequests);
}
}
@@ -313,7 +313,7 @@ impl<'r> FromRequest<'r> for RatelimitInformation {
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
let info = match request.guard::<Ratelimiter>().await {
Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter),
Outcome::Failure((_, ratelimiter)) => RatelimitInformation::Failure {
Outcome::Error((_, ratelimiter)) => RatelimitInformation::Failure {
retry_after: ratelimiter.reset,
},
_ => unreachable!(),

View File

@@ -5,7 +5,7 @@ use authifier::{
use futures::StreamExt;
use rand::Rng;
use redis_kiss::redis::aio::PubSub;
use revolt_database::{events::client::EventV1, Database, User};
use revolt_database::{events::client::EventV1, Database, User, AMQP};
use revolt_models::v0;
use rocket::local::asynchronous::Client;
@@ -13,12 +13,15 @@ pub struct TestHarness {
pub client: Client,
authifier: Authifier,
pub db: Database,
pub amqp: AMQP,
sub: PubSub,
event_buffer: Vec<(String, EventV1)>,
}
impl TestHarness {
pub async fn new() -> TestHarness {
let config = revolt_config::config().await;
let client = Client::tracked(crate::web().await)
.await
.expect("valid rocket instance");
@@ -41,10 +44,25 @@ impl TestHarness {
.expect("`Authifier`")
.clone();
let connection = amqprs::connection::Connection::open(
&amqprs::connection::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 amqp = AMQP::new(connection, channel);
TestHarness {
client,
authifier,
db,
amqp,
sub,
event_buffer: vec![],
}