Compare commits

..
28 Commits
Author SHA1 Message Date
Paul Makles f903f716e3 chore: bump version to 0.7.11 2024-06-25 21:03:00 +01:00
Paul Makles c5f4b94aa5 fix(bonfire): Ready event should include user with online status true (if applicable) 2024-06-25 21:02:27 +01:00
Paul Makles 1d5dae4751 feat(core): include user and member on Message events 2024-06-25 21:01:45 +01:00
Paul Makles ac20b6bc99 chore: bump version to 0.7.10 2024-06-25 19:24:28 +01:00
Paul Makles 6ec8007e4e feat(core): separate limits for new user accounts 2024-06-25 19:20:34 +01:00
Paul Makles e5eea267cf refactor(core/config): move limits to 'global' key 2024-06-25 18:10:23 +01:00
Paul Makles de5add09d0 fix(core/database): only run outgoing friend checks if we are creating new request
closes #327
2024-06-25 18:07:16 +01:00
Paul Makles 1ec8f46c1d chore: bump version to 0.7.9 2024-06-23 19:39:32 +01:00
Paul Makles 80666848cc feat(core): add a limit to no. of outgoing pending friend requests 2024-06-23 19:39:18 +01:00
Paul Makles e8e9613169 chore: bump version to 0.7.8 2024-06-21 16:13:16 +01:00
Paul Makles eda36436a8 fix: specify configuration 2024-06-21 16:13:02 +01:00
Paul Makles 93e05e9f18 fix(bonfire): ignore all Redis errors but Canceled 2024-06-20 22:12:00 +01:00
Paul Makles 090d8cba71 chore: bump version to 0.7.7 2024-06-20 21:41:12 +01:00
Paul Makles f16e72e329 fix(bonfire): drop connections if Redis disconnects 2024-06-20 21:40:44 +01:00
Paul Makles 8248a4a5b8 fix(bonfire): clean up Redis connection after disconnect 2024-06-20 21:30:47 +01:00
Paul Makles cf0b4274c8 chore: bump version to 0.7.6 2024-06-19 18:25:11 +01:00
Paul Makles 36ecb48c7b feat(bonfire): don't fan out UserUpdate (server) by default 2024-06-19 18:24:51 +01:00
Paul Makles ba411fef43 chore: bump version to 0.7.5 2024-06-15 11:02:30 +01:00
Paul Makles 4055363cff fix(core/database): fetch users mentioned in system messages 2024-06-15 11:02:09 +01:00
Paul Makles d179d1e695 fix(delta): don't specify the other member in permission check
supersedes / closes #323
2024-06-15 11:01:03 +01:00
Paul Makles fc9ceb7540 refactor(bonfire): add additional error handling 2024-06-15 10:51:00 +01:00
Paul Makles a31a3e2e65 refactor: remove profile from v0 user, added unintentionally 2024-06-15 10:50:43 +01:00
Paul Makles b12e728514 feat(core/result): implement std::error::Error for Error 2024-06-15 10:50:22 +01:00
Paul Makles 962c7d62c7 fix: allow fetching discoverable servers as invites 2024-06-11 18:46:07 +01:00
Paul Makles d677716f93 docs: update development guide 2024-06-11 14:16:44 +01:00
Paul Makles 8099310f89 fix: wait on unwaited futures 2024-06-11 14:16:35 +01:00
Paul Makles 5c40f66010 fix: wrong match keyword causing disconnects on typing event 2024-06-11 14:01:59 +01:00
Paul Makles 8e7dd21bce docs: add note about creating GitHub release [skip ci] 2024-06-11 13:08:24 +01:00
45 changed files with 855 additions and 314 deletions

No files matched your search

Generated
+74 -27
View File
@@ -138,11 +138,23 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
dependencies = [ dependencies = [
"concurrent-queue", "concurrent-queue 1.2.2",
"event-listener", "event-listener 2.5.2",
"futures-core", "futures-core",
] ]
[[package]]
name = "async-channel"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
dependencies = [
"concurrent-queue 2.5.0",
"event-listener-strategy",
"futures-core",
"pin-project-lite 0.2.13",
]
[[package]] [[package]]
name = "async-executor" name = "async-executor"
version = "1.4.1" version = "1.4.1"
@@ -150,7 +162,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
dependencies = [ dependencies = [
"async-task", "async-task",
"concurrent-queue", "concurrent-queue 1.2.2",
"fastrand", "fastrand",
"futures-lite", "futures-lite",
"once_cell", "once_cell",
@@ -163,7 +175,7 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880" checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880"
dependencies = [ dependencies = [
"async-channel", "async-channel 1.6.1",
"async-executor", "async-executor",
"async-io", "async-io",
"async-lock", "async-lock",
@@ -180,7 +192,7 @@ version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07"
dependencies = [ dependencies = [
"concurrent-queue", "concurrent-queue 1.2.2",
"futures-lite", "futures-lite",
"libc", "libc",
"log", "log",
@@ -199,7 +211,7 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
dependencies = [ dependencies = [
"event-listener", "event-listener 2.5.2",
] ]
[[package]] [[package]]
@@ -211,7 +223,7 @@ dependencies = [
"async-io", "async-io",
"blocking", "blocking",
"cfg-if", "cfg-if",
"event-listener", "event-listener 2.5.2",
"futures-lite", "futures-lite",
"libc", "libc",
"once_cell", "once_cell",
@@ -237,7 +249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
dependencies = [ dependencies = [
"async-attributes", "async-attributes",
"async-channel", "async-channel 1.6.1",
"async-global-executor", "async-global-executor",
"async-io", "async-io",
"async-lock", "async-lock",
@@ -531,7 +543,7 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc"
dependencies = [ dependencies = [
"async-channel", "async-channel 1.6.1",
"async-task", "async-task",
"atomic-waker", "atomic-waker",
"fastrand", "fastrand",
@@ -729,6 +741,15 @@ dependencies = [
"cache-padded", "cache-padded",
] ]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "config" name = "config"
version = "0.13.3" version = "0.13.3"
@@ -833,13 +854,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.8" version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
dependencies = [
"cfg-if",
"lazy_static",
]
[[package]] [[package]]
name = "crypto-bigint" name = "crypto-bigint"
@@ -1295,6 +1312,27 @@ version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
[[package]]
name = "event-listener"
version = "5.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
dependencies = [
"concurrent-queue 2.5.0",
"parking",
"pin-project-lite 0.2.13",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
dependencies = [
"event-listener 5.3.1",
"pin-project-lite 0.2.13",
]
[[package]] [[package]]
name = "fake-simd" name = "fake-simd"
version = "0.1.2" version = "0.1.2"
@@ -1976,13 +2014,13 @@ version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
dependencies = [ dependencies = [
"async-channel", "async-channel 1.6.1",
"castaway", "castaway",
"crossbeam-utils", "crossbeam-utils",
"curl", "curl",
"curl-sys", "curl-sys",
"encoding_rs", "encoding_rs",
"event-listener", "event-listener 2.5.2",
"futures-lite", "futures-lite",
"http", "http",
"log", "log",
@@ -2266,6 +2304,12 @@ dependencies = [
"linked-hash-map", "linked-hash-map",
] ]
[[package]]
name = "lru_time_cache"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd"
[[package]] [[package]]
name = "maplit" name = "maplit"
version = "1.0.2" version = "1.0.2"
@@ -3408,8 +3452,9 @@ dependencies = [
[[package]] [[package]]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.7.4" version = "0.7.11"
dependencies = [ dependencies = [
"async-channel 2.3.1",
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
"authifier", "authifier",
@@ -3418,6 +3463,7 @@ dependencies = [
"futures", "futures",
"log", "log",
"lru 0.7.6", "lru 0.7.6",
"lru_time_cache",
"once_cell", "once_cell",
"querystring", "querystring",
"redis-kiss", "redis-kiss",
@@ -3436,7 +3482,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-config" name = "revolt-config"
version = "0.7.4" version = "0.7.11"
dependencies = [ dependencies = [
"async-std", "async-std",
"cached", "cached",
@@ -3452,7 +3498,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-database" name = "revolt-database"
version = "0.7.4" version = "0.7.11"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"async-recursion", "async-recursion",
@@ -3498,9 +3544,9 @@ dependencies = [
[[package]] [[package]]
name = "revolt-delta" name = "revolt-delta"
version = "0.7.4" version = "0.7.11"
dependencies = [ dependencies = [
"async-channel", "async-channel 1.6.1",
"async-std", "async-std",
"authifier", "authifier",
"bitfield", "bitfield",
@@ -3526,6 +3572,7 @@ dependencies = [
"revolt-database", "revolt-database",
"revolt-models", "revolt-models",
"revolt-permissions", "revolt-permissions",
"revolt-presence",
"revolt-result", "revolt-result",
"revolt_rocket_okapi", "revolt_rocket_okapi",
"rocket", "rocket",
@@ -3544,7 +3591,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-models" name = "revolt-models"
version = "0.7.4" version = "0.7.11"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"iso8601-timestamp 0.2.11", "iso8601-timestamp 0.2.11",
@@ -3561,7 +3608,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.7.4" version = "0.7.11"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3576,7 +3623,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-presence" name = "revolt-presence"
version = "0.7.4" version = "0.7.11"
dependencies = [ dependencies = [
"async-std", "async-std",
"log", "log",
@@ -3587,7 +3634,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-result" name = "revolt-result"
version = "0.7.4" version = "0.7.11"
dependencies = [ dependencies = [
"revolt_okapi", "revolt_okapi",
"revolt_rocket_okapi", "revolt_rocket_okapi",
@@ -4362,7 +4409,7 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
dependencies = [ dependencies = [
"async-channel", "async-channel 1.6.1",
"futures-core", "futures-core",
"futures-io", "futures-io",
] ]
+24
View File
@@ -48,7 +48,29 @@ cp .env.example .env
# (optionally) copy the default configuration file # (optionally) copy the default configuration file
cp crates/core/config/Revolt.toml Revolt.toml cp crates/core/config/Revolt.toml Revolt.toml
# configure as necessary... # configure as necessary...
```
You may want to copy the following configuration:
```toml
# Revolt.toml
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost"
[hosts]
app = "http://local.revolt.chat"
api = "http://local.revolt.chat:8000"
events = "ws://local.revolt.chat:9000"
autumn = "http://local.revolt.chat:3000"
january = "http://local.revolt.chat:7000"
voso_legacy = ""
voso_legacy_ws = ""
```
Then continue:
```bash
# start other necessary services # start other necessary services
docker compose up -d docker compose up -d
@@ -105,6 +127,8 @@ Tag and push a new release by running:
just release just release
``` ```
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/revoltchat/backend/releases/new) to create a changelog.
## Testing ## Testing
First, start the required services: First, start the required services:
+4 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.7.4" version = "0.7.11"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -14,6 +14,8 @@ lru = "0.7.6"
ulid = "0.5.0" ulid = "0.5.0"
once_cell = "1.9.0" once_cell = "1.9.0"
redis-kiss = "0.1.4" redis-kiss = "0.1.4"
lru_time_cache = "0.11.11"
async-channel = "2.3.1"
# parsing # parsing
querystring = "1.1.0" querystring = "1.1.0"
@@ -39,7 +41,7 @@ revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" } revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" } revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.7.4", path = "../core/permissions" } revolt-permissions = { version = "0.7.11", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis # redis
+49 -25
View File
@@ -21,11 +21,11 @@ impl Cache {
let server = self.servers.get(server); let server = self.servers.get(server);
let mut query = let mut query =
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap()) DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
.channel(&channel); .channel(channel);
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel); // let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
if let Some(member) = member { if let Some(member) = member {
query = query.member(&member); query = query.member(member);
} }
if let Some(server) = server { if let Some(server) = server {
@@ -92,6 +92,7 @@ impl State {
/// Generate a Ready packet for the current user /// Generate a Ready packet for the current user
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> { pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
let user = self.clone_user(); let user = self.clone_user();
self.cache.is_bot = user.bot.is_some();
// Find all relationships to the user. // Find all relationships to the user.
let mut user_ids: HashSet<String> = user let mut user_ids: HashSet<String> = user
@@ -179,22 +180,26 @@ impl State {
.collect(); .collect();
// Make sure we see our own user correctly. // Make sure we see our own user correctly.
users.push(user.into_self().await); users.push(user.into_self(true).await);
// Set subscription state internally. // Set subscription state internally.
self.reset_state(); self.reset_state().await;
self.insert_subscription(self.private_topic.clone()); self.insert_subscription(self.private_topic.clone()).await;
for user in &users { for user in &users {
self.insert_subscription(user.id.clone()); self.insert_subscription(user.id.clone()).await;
} }
for server in &servers { for server in &servers {
self.insert_subscription(server.id.clone()); self.insert_subscription(server.id.clone()).await;
if self.cache.is_bot {
self.insert_subscription(format!("{}u", server.id)).await;
}
} }
for channel in &channels { for channel in &channels {
self.insert_subscription(channel.id().to_string()); self.insert_subscription(channel.id().to_string()).await;
} }
Ok(EventV1::Ready { Ok(EventV1::Ready {
@@ -236,11 +241,11 @@ impl State {
let mut bulk_events = vec![]; let mut bulk_events = vec![];
for id in added_channels { for id in added_channels {
self.insert_subscription(id); self.insert_subscription(id).await;
} }
for id in removed_channels { for id in removed_channels {
self.remove_subscription(&id); self.remove_subscription(&id).await;
self.cache.channels.remove(&id); self.cache.channels.remove(&id);
bulk_events.push(EventV1::ChannelDelete { id }); bulk_events.push(EventV1::ChannelDelete { id });
@@ -263,7 +268,7 @@ impl State {
.channels .channels
.insert(channel.id().to_string(), channel.clone()); .insert(channel.id().to_string(), channel.clone());
self.insert_subscription(channel.id().to_string()); self.insert_subscription(channel.id().to_string()).await;
bulk_events.push(EventV1::ChannelCreate(channel.into())); bulk_events.push(EventV1::ChannelCreate(channel.into()));
} }
} }
@@ -336,7 +341,7 @@ impl State {
match event { match event {
EventV1::ChannelCreate(channel) => { EventV1::ChannelCreate(channel) => {
let id = channel.id().to_string(); let id = channel.id().to_string();
self.insert_subscription(id.clone()); self.insert_subscription(id.clone()).await;
self.cache.channels.insert(id, channel.clone().into()); self.cache.channels.insert(id, channel.clone().into());
} }
EventV1::ChannelUpdate { EventV1::ChannelUpdate {
@@ -376,17 +381,17 @@ impl State {
} }
} }
EventV1::ChannelDelete { id } => { EventV1::ChannelDelete { id } => {
self.remove_subscription(id); self.remove_subscription(id).await;
self.cache.channels.remove(id); self.cache.channels.remove(id);
} }
EventV1::ChannelGroupJoin { user, .. } => { EventV1::ChannelGroupJoin { user, .. } => {
self.insert_subscription(user.clone()); self.insert_subscription(user.clone()).await;
} }
EventV1::ChannelGroupLeave { id, user, .. } => { EventV1::ChannelGroupLeave { id, user, .. } => {
if user == &self.cache.user_id { if user == &self.cache.user_id {
self.remove_subscription(id); self.remove_subscription(id).await;
} else if !self.cache.can_subscribe_to_user(user) { } else if !self.cache.can_subscribe_to_user(user) {
self.remove_subscription(user); self.remove_subscription(user).await;
} }
} }
@@ -396,7 +401,12 @@ impl State {
channels, channels,
emojis: _, emojis: _,
} => { } => {
self.insert_subscription(id.clone()); self.insert_subscription(id.clone()).await;
if self.cache.is_bot {
self.insert_subscription(format!("{}u", id)).await;
}
self.cache.servers.insert(id.clone(), server.clone().into()); self.cache.servers.insert(id.clone(), server.clone().into());
let member = Member { let member = Member {
id: MemberCompositeKey { id: MemberCompositeKey {
@@ -435,11 +445,11 @@ impl State {
} }
EventV1::ServerMemberLeave { id, user } => { EventV1::ServerMemberLeave { id, user } => {
if user == &self.cache.user_id { if user == &self.cache.user_id {
self.remove_subscription(id); self.remove_subscription(id).await;
if let Some(server) = self.cache.servers.remove(id) { if let Some(server) = self.cache.servers.remove(id) {
for channel in &server.channels { for channel in &server.channels {
self.remove_subscription(channel); self.remove_subscription(channel).await;
self.cache.channels.remove(channel); self.cache.channels.remove(channel);
} }
} }
@@ -447,11 +457,11 @@ impl State {
} }
} }
EventV1::ServerDelete { id } => { EventV1::ServerDelete { id } => {
self.remove_subscription(id); self.remove_subscription(id).await;
if let Some(server) = self.cache.servers.remove(id) { if let Some(server) = self.cache.servers.remove(id) {
for channel in &server.channels { for channel in &server.channels {
self.remove_subscription(channel); self.remove_subscription(channel).await;
self.cache.channels.remove(channel); self.cache.channels.remove(channel);
} }
} }
@@ -524,9 +534,23 @@ impl State {
self.cache.users.insert(id.clone(), user.clone().into()); self.cache.users.insert(id.clone(), user.clone().into());
if self.cache.can_subscribe_to_user(id) { if self.cache.can_subscribe_to_user(id) {
self.insert_subscription(id.clone()); self.insert_subscription(id.clone()).await;
} else { } else {
self.remove_subscription(id); self.remove_subscription(id).await;
}
}
EventV1::Message(message) => {
// Since Message events are fanned out to many clients,
// we must reconstruct the relationship value at this end.
if let Some(user) = &mut message.user {
user.relationship = self
.cache
.users
.get(&self.cache.user_id)
.expect("missing self?")
.relationship_with(&message.author)
.into();
} }
} }
@@ -540,11 +564,11 @@ impl State {
// Sub / unsub accordingly. // Sub / unsub accordingly.
if let Some(id) = queue_add { if let Some(id) = queue_add {
self.insert_subscription(id); self.insert_subscription(id).await;
} }
if let Some(id) = queue_remove { if let Some(id) = queue_remove {
self.remove_subscription(&id); self.remove_subscription(&id).await;
} }
true true
+44 -1
View File
@@ -1,10 +1,12 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
sync::Arc, sync::Arc,
time::Duration,
}; };
use async_std::sync::RwLock; use async_std::sync::{Mutex, RwLock};
use lru::LruCache; use lru::LruCache;
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
use revolt_database::{Channel, Member, Server, User}; use revolt_database::{Channel, Member, Server, User};
/// Enumeration representing some change in subscriptions /// Enumeration representing some change in subscriptions
@@ -34,6 +36,7 @@ pub enum SubscriptionStateChange {
#[derive(Debug)] #[derive(Debug)]
pub struct Cache { pub struct Cache {
pub user_id: String, pub user_id: String,
pub is_bot: bool,
pub users: HashMap<String, User>, pub users: HashMap<String, User>,
pub channels: HashMap<String, Channel>, pub channels: HashMap<String, Channel>,
@@ -47,6 +50,7 @@ impl Default for Cache {
fn default() -> Self { fn default() -> Self {
Cache { Cache {
user_id: Default::default(), user_id: Default::default(),
is_bot: false,
users: Default::default(), users: Default::default(),
channels: Default::default(), channels: Default::default(),
@@ -65,7 +69,9 @@ pub struct State {
pub session_id: String, pub session_id: String,
pub private_topic: String, pub private_topic: String,
pub state: SubscriptionStateChange, pub state: SubscriptionStateChange,
pub subscribed: Arc<RwLock<HashSet<String>>>, pub subscribed: Arc<RwLock<HashSet<String>>>,
pub active_servers: Arc<Mutex<LruTimeCache<String, ()>>>,
} }
impl State { impl State {
@@ -86,6 +92,10 @@ impl State {
State { State {
cache, cache,
subscribed: Arc::new(RwLock::new(subscribed)), subscribed: Arc::new(RwLock::new(subscribed)),
active_servers: Arc::new(Mutex::new(LruTimeCache::with_expiry_duration_and_capacity(
Duration::from_secs(900),
5,
))),
session_id, session_id,
private_topic, private_topic,
state: SubscriptionStateChange::Reset, state: SubscriptionStateChange::Reset,
@@ -94,6 +104,39 @@ impl State {
/// Apply currently queued state /// Apply currently queued state
pub async fn apply_state(&mut self) -> SubscriptionStateChange { pub async fn apply_state(&mut self) -> SubscriptionStateChange {
// Check if we need to change subscriptions to member event topics
if !self.cache.is_bot {
enum Server {
Subscribe(String),
Unsubscribe(String),
}
let active_server_changes: Vec<Server> = {
let mut active_servers = self.active_servers.lock().await;
active_servers
.notify_iter()
.map(|e| match e {
TimedEntry::Valid(k, _) => Server::Subscribe(format!("{}u", k)),
TimedEntry::Expired(k, _) => Server::Unsubscribe(format!("{}u", k)),
})
.collect()
// It is bad practice to open more than one Mutex at once and could
// lead to a deadlock, so instead we choose to collect the changes.
};
for entry in active_server_changes {
match entry {
Server::Subscribe(k) => {
self.insert_subscription(k).await;
}
Server::Unsubscribe(k) => {
self.remove_subscription(&k).await;
}
}
}
}
// Flush changes to subscriptions
let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None); let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None);
let mut subscribed = self.subscribed.write().await; let mut subscribed = self.subscribed.write().await;
if let SubscriptionStateChange::Change { add, remove } = &state { if let SubscriptionStateChange::Change { add, remove } = &state {
+330 -136
View File
@@ -3,12 +3,13 @@ use std::{collections::HashSet, net::SocketAddr, sync::Arc};
use async_tungstenite::WebSocketStream; use async_tungstenite::WebSocketStream;
use authifier::AuthifierEvent; use authifier::AuthifierEvent;
use fred::{ use fred::{
error::{RedisError, RedisErrorKind},
interfaces::{ClientLike, EventInterface, PubsubInterface}, interfaces::{ClientLike, EventInterface, PubsubInterface},
types::RedisConfig, types::RedisConfig,
}; };
use futures::{ use futures::{
channel::oneshot, channel::oneshot,
pin_mut, select, join, pin_mut, select,
stream::{SplitSink, SplitStream}, stream::{SplitSink, SplitStream},
FutureExt, SinkExt, StreamExt, TryStreamExt, FutureExt, SinkExt, StreamExt, TryStreamExt,
}; };
@@ -22,8 +23,10 @@ use revolt_presence::{create_session, delete_session};
use async_std::{ use async_std::{
net::TcpStream, net::TcpStream,
sync::{Mutex, RwLock}, sync::{Mutex, RwLock},
task::spawn,
}; };
use revolt_result::create_error; use revolt_result::create_error;
use sentry::Level;
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback}; use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
use crate::events::state::{State, SubscriptionStateChange}; use crate::events::state::{State, SubscriptionStateChange};
@@ -52,6 +55,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
let Ok(mut config) = receiver.await else { let Ok(mut config) = receiver.await else {
return; return;
}; };
info!( info!(
"User {addr:?} provided protocol configuration (version = {}, format = {:?})", "User {addr:?} provided protocol configuration (version = {}, format = {:?})",
config.get_protocol_version(), config.get_protocol_version(),
@@ -63,10 +67,8 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
// If the user has not provided authentication, request information. // If the user has not provided authentication, request information.
if config.get_session_token().is_none() { if config.get_session_token().is_none() {
while let Ok(message) = read.try_next().await { while let Ok(Some(message)) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) = if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
config.decode(message.as_ref().unwrap())
{
config.set_session_token(token); config.set_session_token(token);
break; break;
} }
@@ -75,7 +77,10 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
// Try to authenticate the user. // Try to authenticate the user.
let Some(token) = config.get_session_token().as_ref() else { let Some(token) = config.get_session_token().as_ref() else {
write.send(config.encode(&create_error!(InvalidSession))).await.ok(); write
.send(config.encode(&create_error!(InvalidSession)))
.await
.ok();
return; return;
}; };
@@ -94,20 +99,24 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
let user_id = state.cache.user_id.clone(); let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated. // Notify socket we have authenticated.
if write if let Err(err) = write.send(config.encode(&EventV1::Authenticated)).await {
.send(config.encode(&EventV1::Authenticated)) error!("Failed to write: {err:?}");
.await sentry::capture_error(&err);
.is_err()
{
return; return;
} }
// Download required data to local cache and send Ready payload. // Download required data to local cache and send Ready payload.
let Ok(ready_payload) = state.generate_ready_payload(db).await else { let ready_payload = match state.generate_ready_payload(db).await {
return; Ok(ready_payload) => ready_payload,
Err(err) => {
sentry::capture_error(&err);
return;
}
}; };
if write.send(config.encode(&ready_payload)).await.is_err() { if let Err(err) = write.send(config.encode(&ready_payload)).await {
error!("Failed to write: {err:?}");
sentry::capture_error(&err);
return; return;
} }
@@ -120,22 +129,44 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
} }
{ {
// Setup channels and mutexes
let write = Mutex::new(write); let write = Mutex::new(write);
let subscribed = state.subscribed.clone(); let subscribed = state.subscribed.clone();
let active_servers = state.active_servers.clone();
let (topic_signal_s, topic_signal_r) = async_channel::unbounded();
// TODO: this needs to be rewritten
// Create channels through which the tasks can signal to each other they need to clean up
let (kill_signal_1_s, kill_signal_1_r) = async_channel::bounded(1);
let (kill_signal_2_s, kill_signal_2_r) = async_channel::bounded(1);
// Create a PubSub connection to poll on. // Create a PubSub connection to poll on.
let listener = listener(db, &mut state, addr, &config, &write).fuse(); let listener = listener_with_kill_signal(
// Read from WebSocket stream. db,
let worker = worker(addr, subscribed, user_id.clone(), &config, read, &write).fuse(); &mut state,
addr,
// Pin both tasks. &config,
pin_mut!(listener, worker); topic_signal_r,
kill_signal_1_r,
// Wait for either disconnect or for listener to die. &write,
select!( kill_signal_2_s,
() = listener => {},
() = worker => {}
); );
// Read from WebSocket stream.
let worker = worker_with_kill_signal(
addr,
subscribed,
active_servers,
user_id.clone(),
&config,
topic_signal_s,
kill_signal_2_r,
read,
&write,
kill_signal_1_s,
);
join!(listener, worker);
} }
// Clean up presence session. // Clean up presence session.
let last_session = delete_session(&user_id, session_id).await; let last_session = delete_session(&user_id, session_id).await;
@@ -146,31 +177,88 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
} }
} }
#[allow(clippy::too_many_arguments)]
async fn listener_with_kill_signal(
db: &'static Database,
state: &mut State,
addr: SocketAddr,
config: &ProtocolConfiguration,
topic_signal_r: async_channel::Receiver<()>,
kill_signal_r: async_channel::Receiver<()>,
write: &Mutex<WsWriter>,
kill_signal_s: async_channel::Sender<()>,
) {
listener(
db,
state,
addr,
config,
topic_signal_r,
kill_signal_r,
write,
)
.await;
kill_signal_s.send(()).await.ok();
}
async fn listener( async fn listener(
db: &'static Database, db: &'static Database,
state: &mut State, state: &mut State,
addr: SocketAddr, addr: SocketAddr,
config: &ProtocolConfiguration, config: &ProtocolConfiguration,
topic_signal_r: async_channel::Receiver<()>,
kill_signal_r: async_channel::Receiver<()>,
write: &Mutex<WsWriter>, write: &Mutex<WsWriter>,
) { ) {
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap(); let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
let Ok(subscriber) = fred::types::Builder::from_config(redis_config).build_subscriber_client() let subscriber = match fred::types::Builder::from_config(redis_config).build_subscriber_client()
else { {
return; Ok(subscriber) => subscriber,
Err(err) => {
error!("Failed to build a subscriber: {err:?}");
sentry::capture_error(&err);
return;
}
}; };
if subscriber.init().await.is_err() {
if let Err(err) = subscriber.init().await {
error!("Failed to init subscriber: {err:?}");
sentry::capture_error(&err);
return; return;
}; };
// Handle Redis connection dropping
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
subscriber.on_error(move |err| {
if let RedisErrorKind::Canceled = err.kind() {
let clean_up_s = clean_up_s.clone();
spawn(async move {
clean_up_s.lock().await.send(()).await.ok();
});
}
Ok(())
});
let mut message_rx = subscriber.message_rx(); let mut message_rx = subscriber.message_rx();
loop { 'out: loop {
// Check for state changes for subscriptions. // Check for state changes for subscriptions.
match state.apply_state().await { match state.apply_state().await {
SubscriptionStateChange::Reset => { SubscriptionStateChange::Reset => {
subscriber.unsubscribe_all().await.unwrap(); if let Err(err) = subscriber.unsubscribe_all().await {
error!("Unsubscribe all failed: {err:?}");
sentry::capture_error(&err);
break 'out;
}
let subscribed = state.subscribed.read().await; let subscribed = state.subscribed.read().await;
for id in subscribed.iter() { for id in subscribed.iter() {
subscriber.subscribe(id).await.unwrap(); if let Err(err) = subscriber.subscribe(id).await {
error!("Subscribe failed: {err:?}");
sentry::capture_error(&err);
break 'out;
}
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -181,149 +269,255 @@ async fn listener(
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}"); info!("{addr:?} unsubscribing from {id}");
subscriber.unsubscribe(id).await.unwrap(); if let Err(err) = subscriber.unsubscribe(id).await {
error!("Unsubscribe failed: {err:?}");
sentry::capture_error(&err);
break 'out;
}
} }
for id in add { for id in add {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}"); info!("{addr:?} subscribing to {id}");
subscriber.subscribe(id).await.unwrap(); if let Err(err) = subscriber.subscribe(id).await {
error!("Subscribe failed: {err:?}");
sentry::capture_error(&err);
break 'out;
}
} }
} }
SubscriptionStateChange::None => {} SubscriptionStateChange::None => {}
} }
// Handle incoming events. let t1 = message_rx.recv().fuse();
let Ok(message) = message_rx.recv().await.map_err(|e| { let t2 = topic_signal_r.recv().fuse();
warn!("Error while consuming pub/sub messages: {e:?}"); let t3 = kill_signal_r.recv().fuse();
sentry::capture_error(&e); let t4 = clean_up_r.recv().fuse();
}) else {
return;
};
let event = match *REDIS_PAYLOAD_TYPE { pin_mut!(t1, t2, t3, t4);
PayloadType::Json => message
.value
.as_str()
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
PayloadType::Msgpack => message
.value
.as_bytes()
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
PayloadType::Bincode => message
.value
.as_bytes()
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
};
let Some(mut event) = event else { select! {
warn!("Failed to deserialise an event for {}!", message.channel); _ = t4 => {
return; break 'out;
}; },
_ = t3 => {
break 'out;
},
_ = t2 => {},
message = t1 => {
// Handle incoming events.
let message = match message {
Ok(message) => message,
Err(e) => {
error!("Error while consuming pub/sub messages: {e:?}");
sentry::capture_error(&e);
break 'out;
}
};
if let EventV1::Auth(auth) = &event { let event = match *REDIS_PAYLOAD_TYPE {
if let AuthifierEvent::DeleteSession { session_id, .. } = auth { PayloadType::Json => message
if &state.session_id == session_id { .value
event = EventV1::Logout; .as_str()
} .and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
} else if let AuthifierEvent::DeleteAllSessions { PayloadType::Msgpack => message
exclude_session_id, .. .value
} = auth .as_bytes()
{ .and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
if let Some(excluded) = exclude_session_id { PayloadType::Bincode => message
if &state.session_id != excluded { .value
event = EventV1::Logout; .as_bytes()
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
};
let Some(mut event) = event else {
let err = format!(
"Failed to deserialise an event for {}! Introspection: `{:?}`",
message.channel,
message
.value
.as_string()
.map(|x| x.chars().take(32).collect::<String>())
);
error!("{}", err);
sentry::capture_message(&err, Level::Error);
break 'out;
};
if let EventV1::Auth(auth) = &event {
if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
if &state.session_id == session_id {
event = EventV1::Logout;
}
} else if let AuthifierEvent::DeleteAllSessions {
exclude_session_id, ..
} = auth
{
if let Some(excluded) = exclude_session_id {
if &state.session_id != excluded {
event = EventV1::Logout;
}
} else {
event = EventV1::Logout;
}
} }
} else { } else {
event = EventV1::Logout; let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send {
continue;
}
}
let result = write.lock().await.send(config.encode(&event)).await;
if let Err(e) = result {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
let err = format!("Error while sending an event to {addr:?}: {e:?}");
warn!("{}", err);
sentry::capture_message(&err, Level::Warning);
}
break 'out;
}
if let EventV1::Logout = event {
info!("User {addr:?} received log out event!");
break 'out;
} }
} }
} else {
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send {
continue;
}
} }
}
let result = write.lock().await.send(config.encode(&event)).await; if let Err(err) = subscriber.quit().await {
if let Err(e) = result { error!("{}", err);
use async_tungstenite::tungstenite::Error; sentry::capture_error(&err);
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
warn!("Error while sending an event to {addr:?}: {e:?}");
}
return;
}
if let EventV1::Logout = event {
return;
}
} }
} }
#[allow(clippy::too_many_arguments)]
async fn worker_with_kill_signal(
addr: SocketAddr,
subscribed: Arc<RwLock<HashSet<String>>>,
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
user_id: String,
config: &ProtocolConfiguration,
topic_signal_s: async_channel::Sender<()>,
kill_signal_r: async_channel::Receiver<()>,
read: WsReader,
write: &Mutex<WsWriter>,
kill_signal_s: async_channel::Sender<()>,
) {
worker(
addr,
subscribed,
active_servers,
user_id,
config,
topic_signal_s,
kill_signal_r,
read,
write,
)
.await;
kill_signal_s.send(()).await.ok();
}
#[allow(clippy::too_many_arguments)]
async fn worker( async fn worker(
addr: SocketAddr, addr: SocketAddr,
subscribed: Arc<RwLock<HashSet<String>>>, subscribed: Arc<RwLock<HashSet<String>>>,
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
user_id: String, user_id: String,
config: &ProtocolConfiguration, config: &ProtocolConfiguration,
topic_signal_s: async_channel::Sender<()>,
kill_signal_r: async_channel::Receiver<()>,
mut read: WsReader, mut read: WsReader,
write: &Mutex<WsWriter>, write: &Mutex<WsWriter>,
) { ) {
loop { loop {
let result = read.try_next().await; let t1 = read.try_next().fuse();
let msg = match result { let t2 = kill_signal_r.recv().fuse();
Ok(Some(msg)) => msg,
Ok(None) => return, pin_mut!(t1, t2);
Err(e) => {
use async_tungstenite::tungstenite::Error; select! {
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) { _ = t2 => {
warn!("Error while reading an event from {addr:?}: {e:?}");
}
return; return;
} },
}; result = t1 => {
let msg = match result {
Ok(Some(msg)) => msg,
Ok(None) => {
warn!("Received a None message!");
sentry::capture_message("Received a None message!", Level::Warning);
return;
}
Err(e) => {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
let err = format!("Error while reading an event from {addr:?}: {e:?}");
warn!("{}", err);
sentry::capture_message(&err, Level::Warning);
}
let Ok(payload) = config.decode(&msg) else { return;
continue; }
}; };
match payload { let Ok(payload) = config.decode(&msg) else {
ClientMessage::BeginTyping { channel } => { continue;
if !subscribed.read().await.contains(&channel) { };
break;
}
EventV1::ChannelStartTyping { match payload {
id: channel.clone(), ClientMessage::BeginTyping { channel } => {
user: user_id.clone(), if !subscribed.read().await.contains(&channel) {
} continue;
.p(channel.clone()) }
.await;
}
ClientMessage::EndTyping { channel } => {
if !subscribed.read().await.contains(&channel) {
break;
}
EventV1::ChannelStopTyping { EventV1::ChannelStartTyping {
id: channel.clone(), id: channel.clone(),
user: user_id.clone(), user: user_id.clone(),
} }
.p(channel.clone()) .p(channel.clone())
.await; .await;
} }
ClientMessage::Ping { data, responded } => { ClientMessage::EndTyping { channel } => {
if responded.is_none() { if !subscribed.read().await.contains(&channel) {
write continue;
.lock() }
.await
.send(config.encode(&EventV1::Pong { data })) EventV1::ChannelStopTyping {
.await id: channel.clone(),
.ok(); user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::Subscribe { server_id } => {
let mut servers = active_servers.lock().await;
let has_item = servers.contains_key(&server_id);
servers.insert(server_id, ());
if !has_item {
// Poke the listener to adjust subscriptions
topic_signal_s.send(()).await.ok();
}
}
ClientMessage::Ping { data, responded } => {
if responded.is_none() {
write
.lock()
.await
.send(config.encode(&EventV1::Pong { data }))
.await
.ok();
}
}
_ => {}
} }
} }
_ => {}
} }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.7.4" version = "0.7.11"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+26 -5
View File
@@ -46,19 +46,40 @@ webhooks_enabled = false
[features.limits] [features.limits]
[features.limits.default] [features.limits.global]
group_size = 100 group_size = 100
bots = 5
message_length = 2000
message_embeds = 5 message_embeds = 5
message_replies = 5 message_replies = 5
message_attachments = 5
message_reactions = 20 message_reactions = 20
servers = 100
server_emoji = 100 server_emoji = 100
server_roles = 200 server_roles = 200
server_channels = 200 server_channels = 200
new_user_days = 3
[features.limits.new_user]
outgoing_friend_requests = 5
bots = 2
message_length = 2000
message_attachments = 5
servers = 100
attachment_size = 20000000
avatar_size = 4000000
background_size = 6000000
icon_size = 2500000
banner_size = 6000000
emoji_size = 500000
[features.limits.default]
outgoing_friend_requests = 10
bots = 5
message_length = 2000
message_attachments = 5
servers = 100
attachment_size = 20000000 attachment_size = 20000000
avatar_size = 4000000 avatar_size = 4000000
background_size = 6000000 background_size = 6000000
+17 -6
View File
@@ -105,19 +105,27 @@ pub struct Api {
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimits { pub struct GlobalLimits {
pub group_size: usize, pub group_size: usize,
pub bots: usize,
pub message_length: usize,
pub message_replies: usize,
pub message_attachments: usize,
pub message_embeds: usize, pub message_embeds: usize,
pub message_replies: usize,
pub message_reactions: usize, pub message_reactions: usize,
pub servers: usize,
pub server_emoji: usize, pub server_emoji: usize,
pub server_roles: usize, pub server_roles: usize,
pub server_channels: usize, pub server_channels: usize,
pub new_user_days: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimits {
pub outgoing_friend_requests: usize,
pub bots: usize,
pub message_length: usize,
pub message_attachments: usize,
pub servers: usize,
pub attachment_size: usize, pub attachment_size: usize,
pub avatar_size: usize, pub avatar_size: usize,
pub background_size: usize, pub background_size: usize,
@@ -128,6 +136,9 @@ pub struct FeaturesLimits {
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimitsCollection { pub struct FeaturesLimitsCollection {
pub global: GlobalLimits,
pub new_user: FeaturesLimits,
pub default: FeaturesLimits, pub default: FeaturesLimits,
#[serde(flatten)] #[serde(flatten)]
+6 -6
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.7.4" version = "0.7.11"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.7.4", path = "../config" } revolt-config = { version = "0.7.11", path = "../config" }
revolt-result = { version = "0.7.4", path = "../result" } revolt-result = { version = "0.7.11", path = "../result" }
revolt-models = { version = "0.7.4", path = "../models", features = [ revolt-models = { version = "0.7.11", path = "../models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.7.4", path = "../presence" } revolt-presence = { version = "0.7.11", path = "../presence" }
revolt-permissions = { version = "0.7.4", path = "../permissions", features = [ revolt-permissions = { version = "0.7.11", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
+6 -1
View File
@@ -249,7 +249,7 @@ impl EventV1 {
// TODO: this should be captured by member list in the future and not immediately fanned out to users // TODO: this should be captured by member list in the future and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await { if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members { for member in members {
self.clone().p(member.id.server).await; self.clone().server(member.id.server).await;
} }
} }
} }
@@ -259,6 +259,11 @@ impl EventV1 {
self.p(format!("{id}!")).await; self.p(format!("{id}!")).await;
} }
/// Publish server member event
pub async fn server(self, id: String) {
self.p(format!("{id}u")).await;
}
/// Publish internal global event /// Publish internal global event
pub async fn global(self) { pub async fn global(self) {
self.p("global".to_string()).await; self.p("global".to_string()).await;
@@ -8,5 +8,6 @@ pub enum ClientMessage {
Authenticate { token: String }, Authenticate { token: String },
BeginTyping { channel: String }, BeginTyping { channel: String },
EndTyping { channel: String }, EndTyping { channel: String },
Subscribe { server_id: String },
Ping { data: Ping, responded: Option<()> }, Ping { data: Ping, responded: Option<()> },
} }
@@ -80,8 +80,7 @@ impl Bot {
return Err(create_error!(IsBot)); return Err(create_error!(IsBot));
} }
let config = config().await; if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
return Err(create_error!(ReachedMaximumBots)); return Err(create_error!(ReachedMaximumBots));
} }
@@ -201,9 +201,9 @@ impl Channel {
update_server: bool, update_server: bool,
) -> Result<Channel> { ) -> Result<Channel> {
let config = config().await; let config = config().await;
if server.channels.len() > config.features.limits.default.server_channels { if server.channels.len() > config.features.limits.global.server_channels {
return Err(create_error!(TooManyChannels { return Err(create_error!(TooManyChannels {
max: config.features.limits.default.server_channels, max: config.features.limits.global.server_channels,
})); }));
}; };
@@ -263,9 +263,9 @@ impl Channel {
data.users.insert(owner_id.to_string()); data.users.insert(owner_id.to_string());
let config = config().await; let config = config().await;
if data.users.len() > config.features.limits.default.group_size { if data.users.len() > config.features.limits.global.group_size {
return Err(create_error!(GroupTooLarge { return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size, max: config.features.limits.global.group_size,
})); }));
} }
@@ -346,9 +346,9 @@ impl Channel {
} }
let config = config().await; let config = config().await;
if recipients.len() >= config.features.limits.default.group_size { if recipients.len() >= config.features.limits.global.group_size {
return Err(create_error!(GroupTooLarge { return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size max: config.features.limits.global.group_size
})); }));
} }
@@ -377,6 +377,8 @@ impl Channel {
username: &user.username, 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,
self, self,
false, false,
) )
@@ -688,6 +690,8 @@ impl Channel {
username: name, username: name,
avatar: None, avatar: None,
}, },
None,
None,
self, self,
false, false,
) )
@@ -723,6 +727,8 @@ impl Channel {
username: &user.username, 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,
self, self,
false, false,
) )
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use indexmap::{IndexMap, IndexSet}; use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
use revolt_config::config; use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{ use revolt_models::v0::{
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
@@ -207,11 +207,15 @@ impl Default for Message {
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
impl Message { impl Message {
/// Create message from API data /// Create message from API data
#[allow(clippy::too_many_arguments)]
pub async fn create_from_api( pub async fn create_from_api(
db: &Database, db: &Database,
channel: Channel, channel: Channel,
data: DataMessageSend, data: DataMessageSend,
author: MessageAuthor<'_>, author: MessageAuthor<'_>,
user: Option<v0::User>,
member: Option<v0::Member>,
limits: FeaturesLimits,
mut idempotency: IdempotencyKey, mut idempotency: IdempotencyKey,
generate_embeds: bool, generate_embeds: bool,
allow_mentions: bool, allow_mentions: bool,
@@ -221,7 +225,7 @@ impl Message {
Message::validate_sum( Message::validate_sum(
&data.content, &data.content,
data.embeds.as_deref().unwrap_or_default(), data.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length, limits.message_length,
)?; )?;
idempotency idempotency
@@ -288,9 +292,9 @@ impl Message {
// Verify replies are valid. // Verify replies are valid.
let mut replies = HashSet::new(); let mut replies = HashSet::new();
if let Some(entries) = data.replies { if let Some(entries) = data.replies {
if entries.len() > config.features.limits.default.message_replies { if entries.len() > config.features.limits.global.message_replies {
return Err(create_error!(TooManyReplies { return Err(create_error!(TooManyReplies {
max: config.features.limits.default.message_replies, max: config.features.limits.global.message_replies,
})); }));
} }
@@ -320,20 +324,20 @@ impl Message {
if data if data
.attachments .attachments
.as_ref() .as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments) .is_some_and(|v| v.len() > limits.message_attachments)
{ {
return Err(create_error!(TooManyAttachments { return Err(create_error!(TooManyAttachments {
max: config.features.limits.default.message_attachments, max: limits.message_attachments,
})); }));
} }
if data if data
.embeds .embeds
.as_ref() .as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds) .is_some_and(|v| v.len() > config.features.limits.global.message_embeds)
{ {
return Err(create_error!(TooManyEmbeds { return Err(create_error!(TooManyEmbeds {
max: config.features.limits.default.message_embeds, max: config.features.limits.global.message_embeds,
})); }));
} }
@@ -360,7 +364,9 @@ impl Message {
message.nonce = Some(idempotency.into_key()); message.nonce = Some(idempotency.into_key());
// Send the message // Send the message
message.send(db, author, &channel, generate_embeds).await?; message
.send(db, author, user, member, &channel, generate_embeds)
.await?;
Ok(message) Ok(message)
} }
@@ -369,13 +375,15 @@ impl Message {
pub async fn send_without_notifications( pub async fn send_without_notifications(
&mut self, &mut self,
db: &Database, db: &Database,
user: Option<v0::User>,
member: Option<v0::Member>,
is_dm: bool, is_dm: bool,
generate_embeds: bool, generate_embeds: bool,
) -> Result<()> { ) -> Result<()> {
db.insert_message(self).await?; db.insert_message(self).await?;
// Fan out events // Fan out events
EventV1::Message(self.clone().into()) EventV1::Message(self.clone().into_model(user, member))
.p(self.channel.to_string()) .p(self.channel.to_string())
.await; .await;
@@ -416,11 +424,15 @@ impl Message {
&mut self, &mut self,
db: &Database, db: &Database,
author: MessageAuthor<'_>, author: MessageAuthor<'_>,
user: Option<v0::User>,
member: Option<v0::Member>,
channel: &Channel, channel: &Channel,
generate_embeds: bool, generate_embeds: bool,
) -> Result<()> { ) -> Result<()> {
self.send_without_notifications( self.send_without_notifications(
db, db,
user,
member,
matches!(channel, Channel::DirectMessage { .. }), matches!(channel, Channel::DirectMessage { .. }),
generate_embeds, generate_embeds,
) )
@@ -436,7 +448,12 @@ impl Message {
_ => vec![], _ => vec![],
} }
}, },
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await, PushNotification::from(
self.clone().into_model(None, None),
Some(author),
&channel.id(),
)
.await,
) )
.await; .await;
@@ -498,13 +515,41 @@ impl Message {
.fetch_messages(query) .fetch_messages(query)
.await? .await?
.into_iter() .into_iter()
.map(Into::into) .map(|msg| msg.into_model(None, None))
.collect(); .collect();
if let Some(true) = include_users { if let Some(true) = include_users {
let user_ids = messages let user_ids = messages
.iter() .iter()
.map(|m| m.author.clone()) .flat_map(|m| {
let mut users = vec![m.author.clone()];
if let Some(system) = &m.system {
match system {
v0::SystemMessage::ChannelDescriptionChanged { by } => {
users.push(by.clone())
}
v0::SystemMessage::ChannelIconChanged { by } => users.push(by.clone()),
v0::SystemMessage::ChannelOwnershipChanged { from, to, .. } => {
users.push(from.clone());
users.push(to.clone())
}
v0::SystemMessage::ChannelRenamed { by, .. } => users.push(by.clone()),
v0::SystemMessage::UserAdded { by, id, .. }
| v0::SystemMessage::UserRemove { by, id, .. } => {
users.push(by.clone());
users.push(id.clone());
}
v0::SystemMessage::UserBanned { id, .. }
| v0::SystemMessage::UserKicked { id, .. }
| v0::SystemMessage::UserJoined { id, .. }
| v0::SystemMessage::UserLeft { id, .. } => {
users.push(id.clone());
}
v0::SystemMessage::Text { .. } => {}
}
}
users
})
.collect::<HashSet<String>>() .collect::<HashSet<String>>()
.into_iter() .into_iter()
.collect::<Vec<String>>(); .collect::<Vec<String>>();
@@ -588,7 +633,7 @@ impl Message {
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> { pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
// Check how many reactions are already on the message // Check how many reactions are already on the message
let config = config().await; let config = config().await;
if self.reactions.len() >= config.features.limits.default.message_reactions if self.reactions.len() >= config.features.limits.global.message_reactions
&& !self.reactions.contains_key(emoji) && !self.reactions.contains_key(emoji)
{ {
return Err(create_error!(InvalidOperation)); return Err(create_error!(InvalidOperation));
@@ -753,7 +798,7 @@ impl Interactions {
if let Some(reactions) = &self.reactions { if let Some(reactions) = &self.reactions {
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?; permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
if reactions.len() > config.features.limits.default.message_reactions { if reactions.len() > config.features.limits.global.message_reactions {
return Err(create_error!(InvalidOperation)); return Err(create_error!(InvalidOperation));
} }
@@ -150,7 +150,7 @@ impl Member {
id: user.id.clone(), id: user.id.clone(),
} }
.into_message(id.to_string()) .into_message(id.to_string())
.send_without_notifications(db, false, false) .send_without_notifications(db, None, None, false, false)
.await .await
.ok(); .ok();
} }
@@ -250,7 +250,7 @@ impl Member {
} }
.into_message(id.to_string()) .into_message(id.to_string())
// TODO: support notifications here in the future? // TODO: support notifications here in the future?
.send_without_notifications(db, false, false) .send_without_notifications(db, None, None, false, false)
.await .await
.ok(); .ok();
} }
+41 -5
View File
@@ -1,10 +1,10 @@
use std::{collections::HashSet, time::Duration}; use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent}; use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use revolt_config::config; use revolt_config::{config, FeaturesLimits};
use revolt_models::v0; use revolt_models::v0;
use revolt_presence::filter_online; use revolt_presence::filter_online;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
@@ -197,6 +197,22 @@ impl User {
Ok(user) Ok(user)
} }
/// Get limits for this user
pub async fn limits(&self) -> FeaturesLimits {
let config = config().await;
if ulid::Ulid::from_str(&self.id)
.expect("`ulid`")
.datetime()
.elapsed()
.expect("time went backwards")
<= Duration::from_secs(86400u64 * config.features.limits.global.new_user_days as u64)
{
config.features.limits.new_user
} else {
config.features.limits.default
}
}
/// Get the relationship with another user /// Get the relationship with another user
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus { pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
if self.id == user_b { if self.id == user_b {
@@ -235,12 +251,11 @@ impl User {
/// Check if this user can acquire another server /// Check if this user can acquire another server
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> { pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
let config = config().await; if db.fetch_server_count(&self.id).await? <= self.limits().await.servers {
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
Ok(()) Ok(())
} else { } else {
Err(create_error!(TooManyServers { Err(create_error!(TooManyServers {
max: config.features.limits.default.servers max: self.limits().await.servers
})) }))
} }
} }
@@ -477,6 +492,7 @@ impl User {
RelationshipStatus::Blocked => Err(create_error!(Blocked)), RelationshipStatus::Blocked => Err(create_error!(Blocked)),
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)), RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
RelationshipStatus::Incoming => { RelationshipStatus::Incoming => {
// Accept incoming friend request
self.apply_relationship( self.apply_relationship(
db, db,
target, target,
@@ -486,6 +502,26 @@ impl User {
.await .await
} }
RelationshipStatus::None => { RelationshipStatus::None => {
// Get this user's current count of outgoing friend requests
let count = self
.relations
.as_ref()
.map(|relations| {
relations
.iter()
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
.count()
})
.unwrap_or_default();
// If we're over the limit, don't allow creating more requests
if count >= self.limits().await.outgoing_friend_requests {
return Err(create_error!(TooManyPendingFriendRequests {
max: self.limits().await.outgoing_friend_requests
}));
}
// Send the friend request
self.apply_relationship( self.apply_relationship(
db, db,
target, target,
@@ -56,7 +56,7 @@ pub async fn worker(db: Database) {
let embeds = generate( let embeds = generate(
task.content, task.content,
&config.hosts.january, &config.hosts.january,
config.features.limits.default.message_embeds, config.features.limits.global.message_embeds,
semaphore, semaphore,
) )
.await; .await;
+50 -32
View File
@@ -459,26 +459,28 @@ impl From<Metadata> for crate::Metadata {
} }
} }
impl From<crate::Message> for Message { impl crate::Message {
fn from(value: crate::Message) -> Self { pub fn into_model(self, user: Option<User>, member: Option<Member>) -> Message {
Message { Message {
id: value.id, id: self.id,
nonce: value.nonce, nonce: self.nonce,
channel: value.channel, channel: self.channel,
author: value.author, author: self.author,
webhook: value.webhook, user,
content: value.content, member,
system: value.system.map(|system| system.into()), webhook: self.webhook,
attachments: value content: self.content,
system: self.system.map(|system| system.into()),
attachments: self
.attachments .attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()), .map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited, edited: self.edited,
embeds: value.embeds, embeds: self.embeds,
mentions: value.mentions, mentions: self.mentions,
replies: value.replies, replies: self.replies,
reactions: value.reactions, reactions: self.reactions,
interactions: value.interactions.into(), interactions: self.interactions.into(),
masquerade: value.masquerade.map(|masq| masq.into()), masquerade: self.masquerade.map(|masq| masq.into()),
} }
} }
} }
@@ -490,6 +492,8 @@ impl From<crate::PartialMessage> for PartialMessage {
nonce: value.nonce, nonce: value.nonce,
channel: value.channel, channel: value.channel,
author: value.author, author: value.author,
user: None,
member: None,
webhook: value.webhook, webhook: value.webhook,
content: value.content, content: value.content,
system: value.system.map(|system| system.into()), system: value.system.map(|system| system.into()),
@@ -1009,11 +1013,6 @@ impl crate::User {
} else { } else {
None None
}, },
profile: if can_see_profile {
self.profile.map(|profile| profile.into())
} else {
None
},
flags: self.flags.unwrap_or_default() as u32, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
@@ -1086,11 +1085,6 @@ impl crate::User {
} else { } else {
None None
}, },
profile: if can_see_profile {
self.profile.map(|profile| profile.into())
} else {
None
},
flags: self.flags.unwrap_or_default() as u32, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
@@ -1099,7 +1093,33 @@ impl crate::User {
} }
} }
pub async fn into_self(self) -> User { /// Convert user object into user model without presence information
pub fn into_known_static<'a>(self, is_online: bool) -> User {
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
online: is_online
&& !matches!(
self.status,
Some(crate::UserStatus {
presence: Some(crate::Presence::Invisible),
..
})
),
status: self.status.map(|status| status.into()),
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship: RelationshipStatus::None, // events client will populate this from cache
id: self.id,
}
}
pub async fn into_self(self, force_online: bool) -> User {
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
@@ -1115,7 +1135,7 @@ impl crate::User {
}) })
.unwrap_or_default(), .unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32, badges: self.badges.unwrap_or_default() as u32,
online: revolt_presence::is_online(&self.id).await online: (force_online || revolt_presence::is_online(&self.id).await)
&& !matches!( && !matches!(
self.status, self.status,
Some(crate::UserStatus { Some(crate::UserStatus {
@@ -1124,7 +1144,6 @@ impl crate::User {
}) })
), ),
status: self.status.map(|status| status.into()), status: self.status.map(|status| status.into()),
profile: self.profile.map(|profile| profile.into()),
flags: self.flags.unwrap_or_default() as u32, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
@@ -1152,7 +1171,7 @@ impl From<User> for crate::User {
relations: None, relations: None,
badges: Some(value.badges as i32), badges: Some(value.badges as i32),
status: value.status.map(Into::into), status: value.status.map(Into::into),
profile: value.profile.map(Into::into), profile: None,
flags: Some(value.flags as i32), flags: Some(value.flags as i32),
privileged: value.privileged, privileged: value.privileged,
bot: value.bot.map(Into::into), bot: value.bot.map(Into::into),
@@ -1175,7 +1194,6 @@ impl From<crate::PartialUser> for PartialUser {
}), }),
badges: value.badges.map(|badges| badges as u32), badges: value.badges.map(|badges| badges as u32),
status: value.status.map(|status| status.into()), status: value.status.map(|status| status.into()),
profile: value.profile.map(|profile| profile.into()),
flags: value.flags.map(|flags| flags as u32), flags: value.flags.map(|flags| flags as u32),
privileged: value.privileged, privileged: value.privileged,
bot: value.bot.map(|bot| bot.into()), bot: value.bot.map(|bot| bot.into()),
+22 -2
View File
@@ -1,3 +1,5 @@
use std::str::FromStr;
use revolt_result::Result; use revolt_result::Result;
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
use rocket::request::FromParam; use rocket::request::FromParam;
@@ -44,9 +46,27 @@ impl Reference {
db.fetch_channel(&self.id).await db.fetch_channel(&self.id).await
} }
/// Fetch invite from Ref /// Fetch invite from Ref or create invite to server if discoverable
pub async fn as_invite(&self, db: &Database) -> Result<Invite> { pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
db.fetch_invite(&self.id).await if ulid::Ulid::from_str(&self.id).is_ok() {
let server = self.as_server(db).await?;
if !server.discoverable {
return Err(create_error!(NotFound));
}
Ok(Invite::Server {
code: self.id.to_string(),
server: server.id,
creator: server.owner,
channel: server
.channels
.into_iter()
.next()
.ok_or(create_error!(NotFound))?,
})
} else {
db.fetch_invite(&self.id).await
}
} }
/// Fetch message from Ref /// Fetch message from Ref
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.7.4" version = "0.7.11"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.7.4", path = "../config" } revolt-config = { version = "0.7.11", path = "../config" }
revolt-permissions = { version = "0.7.4", path = "../permissions" } revolt-permissions = { version = "0.7.11", path = "../permissions" }
# Utility # Utility
regex = "1" regex = "1"
+6
View File
@@ -31,6 +31,12 @@ auto_derived_partial!(
pub channel: String, pub channel: String,
/// Id of the user or webhook that sent this message /// Id of the user or webhook that sent this message
pub author: String, pub author: String,
/// The user that sent this message
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<User>,
/// The member that sent this message
#[serde(skip_serializing_if = "Option::is_none")]
pub member: Option<Member>,
/// The webhook that sent this message /// The webhook that sent this message
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub webhook: Option<MessageWebhook>, pub webhook: Option<MessageWebhook>,
-3
View File
@@ -50,9 +50,6 @@ auto_derived_partial!(
/// User's current status /// User's current status
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub status: Option<UserStatus>, pub status: Option<UserStatus>,
/// User's profile page
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub profile: Option<UserProfile>,
/// Enum of user flags /// Enum of user flags
#[cfg_attr( #[cfg_attr(
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.7.4" version = "0.7.11"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.7.4", path = "../result" } revolt-result = { version = "0.7.11", path = "../result" }
# Utility # Utility
auto_ops = "0.3.0" auto_ops = "0.3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.7.4" version = "0.7.11"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.7.4" version = "0.7.11"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+13
View File
@@ -1,3 +1,5 @@
use std::fmt::Display;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
#[macro_use] #[macro_use]
extern crate serde; extern crate serde;
@@ -28,6 +30,14 @@ pub struct Error {
pub location: String, pub location: String,
} }
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?} occurred in {}", self.error_type, self.location)
}
}
impl std::error::Error for Error {}
/// Possible error types /// Possible error types
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))] #[cfg_attr(feature = "serde", serde(tag = "type"))]
@@ -50,6 +60,9 @@ pub enum ErrorType {
Blocked, Blocked,
BlockedByOther, BlockedByOther,
NotFriends, NotFriends,
TooManyPendingFriendRequests {
max: usize,
},
// ? Channel related errors // ? Channel related errors
UnknownChannel, UnknownChannel,
+1
View File
@@ -25,6 +25,7 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::Blocked => Status::Conflict, ErrorType::Blocked => Status::Conflict,
ErrorType::BlockedByOther => Status::Forbidden, ErrorType::BlockedByOther => Status::Forbidden,
ErrorType::NotFriends => Status::Forbidden, ErrorType::NotFriends => Status::Forbidden,
ErrorType::TooManyPendingFriendRequests { .. } => Status::BadRequest,
ErrorType::UnknownChannel => Status::NotFound, ErrorType::UnknownChannel => Status::NotFound,
ErrorType::UnknownMessage => Status::NotFound, ErrorType::UnknownMessage => Status::NotFound,
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.7.4" version = "0.7.11"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -77,6 +77,7 @@ revolt-models = { path = "../core/models", features = [
"validator", "validator",
"rocket", "rocket",
] } ] }
revolt-presence = { path = "../core/presence" }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] } revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] } revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
+1 -2
View File
@@ -47,8 +47,7 @@ pub async fn web() -> Rocket<Build> {
authifier::database::MongoDb(client.database("revolt")), authifier::database::MongoDb(client.database("revolt")),
), ),
}, },
config: Default::default(), config: authifier_config().await,
// config: authifier_config().await,
event_channel: Some(sender), event_channel: Some(sender),
}; };
+1 -1
View File
@@ -24,7 +24,7 @@ pub async fn fetch_owned_bots(db: &State<Database>, user: User) -> Result<Json<O
users.sort_by(|a, b| a.id.cmp(&b.id)); users.sort_by(|a, b| a.id.cmp(&b.id));
Ok(Json(OwnedBotsResponse { Ok(Json(OwnedBotsResponse {
users: join_all(users.into_iter().map(|user| user.into_self())).await, users: join_all(users.into_iter().map(|user| user.into_self(false))).await,
bots: bots.into_iter().map(|bot| bot.into()).collect(), bots: bots.into_iter().map(|bot| bot.into()).collect(),
})) }))
} }
@@ -73,7 +73,7 @@ pub async fn edit(
return Err(create_error!(InvalidOperation)); return Err(create_error!(InvalidOperation));
} }
.into_message(channel.id().to_string()) .into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false) .send(db, user.as_author_for_system(), None, None, &channel, false)
.await .await
.ok(); .ok();
} }
@@ -151,7 +151,7 @@ pub async fn edit(
by: user.id.clone(), by: user.id.clone(),
} }
.into_message(channel.id().to_string()) .into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false) .send(db, user.as_author_for_system(), None, None, &channel, false)
.await .await
.ok(); .ok();
} }
@@ -161,7 +161,7 @@ pub async fn edit(
by: user.id.clone(), by: user.id.clone(),
} }
.into_message(channel.id().to_string()) .into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false) .send(db, user.as_author_for_system(), None, None, &channel, false)
.await .await
.ok(); .ok();
} }
@@ -171,7 +171,7 @@ pub async fn edit(
by: user.id.clone(), by: user.id.clone(),
} }
.into_message(channel.id().to_string()) .into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false) .send(db, user.as_author_for_system(), None, None, &channel, false)
.await .await
.ok(); .ok();
} }
@@ -30,11 +30,10 @@ pub async fn edit(
}) })
})?; })?;
let config = config().await;
Message::validate_sum( Message::validate_sum(
&edit.content, &edit.content,
edit.embeds.as_deref().unwrap_or_default(), edit.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length, user.limits().await.message_length,
)?; )?;
// Ensure we have permissions to send a message // Ensure we have permissions to send a message
@@ -98,5 +97,5 @@ pub async fn edit(
} }
} }
Ok(Json(message.into())) Ok(Json(message.into_model(None, None)))
} }
@@ -29,5 +29,5 @@ pub async fn fetch(
return Err(create_error!(NotFound)); return Err(create_error!(NotFound));
} }
Ok(Json(message.into())) Ok(Json(message.into_model(None, None)))
} }
@@ -5,6 +5,7 @@ use revolt_database::{
}; };
use revolt_database::{Interactions, Message}; use revolt_database::{Interactions, Message};
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::PermissionQuery;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::serde::json::Json; use rocket::serde::json::Json;
@@ -75,17 +76,34 @@ pub async fn message_send(
// Create the message // Create the message
let author: v0::User = user.clone().into(db, Some(&user)).await; let author: v0::User = user.clone().into(db, Some(&user)).await;
// Make sure we have server member (edge case if server owner)
query.are_we_a_member().await;
// Create model user / members
let model_user = user
.clone()
.into_known_static(revolt_presence::is_online(&user.id).await);
let model_member: Option<v0::Member> = query
.member_ref()
.as_ref()
.map(|member| member.clone().into_owned().into());
Ok(Json( Ok(Json(
Message::create_from_api( Message::create_from_api(
db, db,
channel, channel,
data, data,
v0::MessageAuthor::User(&author), v0::MessageAuthor::User(&author),
Some(model_user.clone()),
model_member.clone(),
user.limits().await,
idempotency, idempotency,
permissions.has_channel_permission(ChannelPermission::SendEmbeds), permissions.has_channel_permission(ChannelPermission::SendEmbeds),
allow_mentions, allow_mentions,
) )
.await? .await?
.into(), .into_model(Some(model_user), model_member),
)) ))
} }
@@ -45,9 +45,9 @@ pub async fn create_emoji(
// Check that we haven't hit the emoji limit // Check that we haven't hit the emoji limit
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?; let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
if emojis.len() >= config.features.limits.default.server_emoji { if emojis.len() >= config.features.limits.global.server_emoji {
return Err(create_error!(TooManyEmoji { return Err(create_error!(TooManyEmoji {
max: config.features.limits.default.server_emoji, max: config.features.limits.global.server_emoji,
})); }));
} }
} }
+1 -1
View File
@@ -48,7 +48,7 @@ pub async fn complete(
Ok(Json( Ok(Json(
User::create(db, data.username, session.user_id, None) User::create(db, data.username, session.user_id, None)
.await? .await?
.into_self() .into_self(false)
.await, .await,
)) ))
} }
+1 -1
View File
@@ -36,7 +36,7 @@ pub async fn list(
) )
.await? .await?
.into_iter() .into_iter()
.map(|u| u.into_self()), .map(|u| u.into_self(false)),
) )
.await; .await;
@@ -30,12 +30,12 @@ pub async fn edit(
}) })
})?; })?;
// Fetch server, target member and current permissions // Fetch server and target member
let mut server = server.as_server(db).await?; let mut server = server.as_server(db).await?;
let mut member = target.as_member(db, &server.id).await?; let mut member = target.as_member(db, &server.id).await?;
let mut query = DatabasePermissionQuery::new(db, &user)
.server(&server) // Fetch our currrent permissions
.member(&member); let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
let permissions = calculate_server_permissions(&mut query).await; let permissions = calculate_server_permissions(&mut query).await;
// Check permissions in server // Check permissions in server
@@ -34,9 +34,9 @@ pub async fn create(
.throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?; .throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?;
let config = config().await; let config = config().await;
if server.roles.len() >= config.features.limits.default.server_roles { if server.roles.len() >= config.features.limits.global.server_roles {
return Err(create_error!(TooManyRoles { return Err(create_error!(TooManyRoles {
max: config.features.limits.default.server_roles, max: config.features.limits.global.server_roles,
})); }));
}; };
+2 -2
View File
@@ -52,7 +52,7 @@ pub async fn edit(
&& data.flags.is_none() && data.flags.is_none()
&& data.remove.is_none() && data.remove.is_none()
{ {
return Ok(Json(user.into_self().await)); return Ok(Json(user.into_self(false).await));
} }
// 1. Remove fields from object // 1. Remove fields from object
@@ -126,5 +126,5 @@ pub async fn edit(
) )
.await?; .await?;
Ok(Json(user.into_self().await)) Ok(Json(user.into_self(false).await))
} }
+1 -1
View File
@@ -9,5 +9,5 @@ use rocket::serde::json::Json;
#[openapi(tag = "User Information")] #[openapi(tag = "User Information")]
#[get("/@me")] #[get("/@me")]
pub async fn fetch(user: User) -> Result<Json<v0::User>> { pub async fn fetch(user: User) -> Result<Json<v0::User>> {
Ok(Json(user.into_self().await)) Ok(Json(user.into_self(false).await))
} }
+1 -1
View File
@@ -15,7 +15,7 @@ use rocket::{serde::json::Json, State};
#[get("/<target>")] #[get("/<target>")]
pub async fn fetch(db: &State<Database>, user: User, target: Reference) -> Result<Json<v0::User>> { pub async fn fetch(db: &State<Database>, user: User, target: Reference) -> Result<Json<v0::User>> {
if user.id == target.id { if user.id == target.id {
return Ok(Json(user.into_self().await)); return Ok(Json(user.into_self(false).await));
} }
let target = target.as_user(db).await?; let target = target.as_user(db).await?;
@@ -1,3 +1,4 @@
use revolt_config::config;
use revolt_database::{ use revolt_database::{
util::{idempotency::IdempotencyKey, reference::Reference}, util::{idempotency::IdempotencyKey, reference::Reference},
Database, Message, Database, Message,
@@ -58,11 +59,14 @@ pub async fn webhook_execute(
channel, channel,
data, data,
v0::MessageAuthor::Webhook(&webhook.into()), v0::MessageAuthor::Webhook(&webhook.into()),
None,
None,
config().await.features.limits.default,
idempotency, idempotency,
true, true,
true, true,
) )
.await? .await?
.into(), .into_model(None, None),
)) ))
} }
@@ -1072,6 +1072,13 @@ pub async fn webhook_execute_github(
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
message.attach_sendable_embed(db, sendable_embed).await?; message.attach_sendable_embed(db, sendable_embed).await?;
message message
.send(db, MessageAuthor::Webhook(&webhook.into()), &channel, false) .send(
db,
MessageAuthor::Webhook(&webhook.into()),
None,
None,
&channel,
false,
)
.await .await
} }