Compare commits

..
34 Commits
Author SHA1 Message Date
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
Paul Makles 4868205df4 chore: bump version to 0.7.4 2024-06-11 12:52:45 +01:00
Paul Makles 96fb0eecca fix: don't allow sending typing notification to unknown channel
closes #177
2024-06-11 12:51:52 +01:00
Paul Makles 2cb20618da feat(bonfire): add disconnection mechanism
feat(bonfire): handle session deletion and logout events
feat(core): trigger logout on bot token reset
2024-06-11 12:38:32 +01:00
Paul Makles 92e948aabc chore: update README [skip ci] 2024-06-09 14:11:30 +01:00
Paul Makles cb4435d700 chore: bump version to 0.7.3 2024-06-09 13:49:38 +01:00
Paul Makles 4c6e78e1a5 chore: relicense some core crates to MIT
chore: update/create LICENSE files
closes #278
closes #277
2024-06-09 13:47:22 +01:00
Paul Makles da9a91e05f fix(bonfire): send InvalidSession error if token is not sent successfully
closes #289
2024-06-09 13:42:12 +01:00
Paul Makles 506968634e fix(core/database): set do not set online status if presence is Invisible
closes #34
2024-06-09 13:41:51 +01:00
Paul Makles a5d0cdf0dd fix(core/database): skip insertion / deletion if no invites need to be corrected 2024-06-04 18:46:11 +01:00
43 changed files with 901 additions and 314 deletions
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,15 +3452,18 @@ dependencies = [
[[package]] [[package]]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.7.2" version = "0.7.10"
dependencies = [ dependencies = [
"async-channel 2.3.1",
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
"authifier",
"bincode", "bincode",
"fred", "fred",
"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",
@@ -3435,7 +3482,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-config" name = "revolt-config"
version = "0.7.2" version = "0.7.10"
dependencies = [ dependencies = [
"async-std", "async-std",
"cached", "cached",
@@ -3451,7 +3498,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-database" name = "revolt-database"
version = "0.7.2" version = "0.7.10"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"async-recursion", "async-recursion",
@@ -3497,9 +3544,9 @@ dependencies = [
[[package]] [[package]]
name = "revolt-delta" name = "revolt-delta"
version = "0.7.2" version = "0.7.10"
dependencies = [ dependencies = [
"async-channel", "async-channel 1.6.1",
"async-std", "async-std",
"authifier", "authifier",
"bitfield", "bitfield",
@@ -3543,7 +3590,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-models" name = "revolt-models"
version = "0.7.2" version = "0.7.10"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"iso8601-timestamp 0.2.11", "iso8601-timestamp 0.2.11",
@@ -3560,7 +3607,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.7.2" version = "0.7.10"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3575,7 +3622,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-presence" name = "revolt-presence"
version = "0.7.2" version = "0.7.10"
dependencies = [ dependencies = [
"async-std", "async-std",
"log", "log",
@@ -3586,7 +3633,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-result" name = "revolt-result"
version = "0.7.2" version = "0.7.10"
dependencies = [ dependencies = [
"revolt_okapi", "revolt_okapi",
"revolt_rocket_okapi", "revolt_rocket_okapi",
@@ -4361,7 +4408,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",
] ]
+4 -1
View File
@@ -1,4 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE With the exception of crates that specify their own LICENSE file,
the following license applies to the source code of this project.
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+39
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,23 @@ 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
First, start the required services:
```sh
docker compose -f docker-compose.db.yml up -d
```
Now run tests for whichever database:
```sh
TEST_DB=REFERENCE cargo nextest run
TEST_DB=MONGOBD cargo nextest run
```
## License ## License
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
+5 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.7.2" version = "0.7.10"
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"
@@ -34,11 +36,12 @@ async-std = { version = "1.8.0", features = [
] } ] }
# core # core
authifier = { version = "1.0.8" }
revolt-result = { path = "../core/result" } 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.2", path = "../core/permissions" } revolt-permissions = { version = "0.7.10", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis # redis
+34 -24
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
@@ -182,19 +183,23 @@ impl State {
users.push(user.into_self().await); users.push(user.into_self().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,9 @@ 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;
} }
} }
@@ -540,11 +550,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
+68 -21
View File
@@ -1,6 +1,12 @@
use std::collections::{HashMap, HashSet}; use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::Duration,
};
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
@@ -30,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>,
@@ -43,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(),
@@ -58,14 +66,17 @@ impl Default for Cache {
pub struct State { pub struct State {
pub cache: Cache, pub cache: Cache,
pub session_id: String,
pub private_topic: String, pub private_topic: String,
subscribed: HashSet<String>, pub state: SubscriptionStateChange,
state: SubscriptionStateChange,
pub subscribed: Arc<RwLock<HashSet<String>>>,
pub active_servers: Arc<Mutex<LruTimeCache<String, ()>>>,
} }
impl State { impl State {
/// Create state from User /// Create state from User
pub fn from(user: User) -> State { pub fn from(user: User, session_id: String) -> State {
let mut subscribed = HashSet::new(); let mut subscribed = HashSet::new();
let private_topic = format!("{}!", user.id); let private_topic = format!("{}!", user.id);
subscribed.insert(private_topic.clone()); subscribed.insert(private_topic.clone());
@@ -80,22 +91,61 @@ impl State {
State { State {
cache, cache,
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,
private_topic, private_topic,
state: SubscriptionStateChange::Reset, state: SubscriptionStateChange::Reset,
} }
} }
/// Apply currently queued state /// Apply currently queued state
pub 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;
if let SubscriptionStateChange::Change { add, remove } = &state { if let SubscriptionStateChange::Change { add, remove } = &state {
for id in add { for id in add {
self.subscribed.insert(id.clone()); subscribed.insert(id.clone());
} }
for id in remove { for id in remove {
self.subscribed.remove(id); subscribed.remove(id);
} }
} }
@@ -107,20 +157,16 @@ impl State {
self.cache.users.get(&self.cache.user_id).unwrap().clone() self.cache.users.get(&self.cache.user_id).unwrap().clone()
} }
/// Iterate through all subscriptions
pub fn iter_subscriptions(&self) -> std::collections::hash_set::Iter<'_, std::string::String> {
self.subscribed.iter()
}
/// Reset the current state /// Reset the current state
pub fn reset_state(&mut self) { pub async fn reset_state(&mut self) {
self.state = SubscriptionStateChange::Reset; self.state = SubscriptionStateChange::Reset;
self.subscribed.clear(); self.subscribed.write().await.clear();
} }
/// Add a new subscription /// Add a new subscription
pub fn insert_subscription(&mut self, subscription: String) { pub async fn insert_subscription(&mut self, subscription: String) {
if self.subscribed.contains(&subscription) { let mut subscribed = self.subscribed.write().await;
if subscribed.contains(&subscription) {
return; return;
} }
@@ -137,12 +183,13 @@ impl State {
SubscriptionStateChange::Reset => {} SubscriptionStateChange::Reset => {}
} }
self.subscribed.insert(subscription); subscribed.insert(subscription);
} }
/// Remove existing subscription /// Remove existing subscription
pub fn remove_subscription(&mut self, subscription: &str) { pub async fn remove_subscription(&mut self, subscription: &str) {
if !self.subscribed.contains(&subscription.to_string()) { let mut subscribed = self.subscribed.write().await;
if !subscribed.contains(&subscription.to_string()) {
return; return;
} }
@@ -159,6 +206,6 @@ impl State {
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"), SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
} }
self.subscribed.remove(subscription); subscribed.remove(subscription);
} }
} }
+361 -116
View File
@@ -1,13 +1,15 @@
use std::net::SocketAddr; use std::{collections::HashSet, net::SocketAddr, sync::Arc};
use async_tungstenite::WebSocketStream; use async_tungstenite::WebSocketStream;
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,
}; };
@@ -18,7 +20,13 @@ use revolt_database::{
}; };
use revolt_presence::{create_session, delete_session}; use revolt_presence::{create_session, delete_session};
use async_std::{net::TcpStream, sync::Mutex}; use async_std::{
net::TcpStream,
sync::{Mutex, RwLock},
task::spawn,
};
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};
@@ -42,10 +50,12 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
else { else {
return; return;
}; };
// Verify we've received a valid config, otherwise we should just drop the connection. // Verify we've received a valid config, otherwise we should just drop the connection.
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(),
@@ -57,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;
} }
@@ -69,35 +77,46 @@ 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();
return; return;
}; };
let user = match User::from_token(db, token, UserHint::Any).await {
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
Ok(user) => user, Ok(user) => user,
Err(err) => { Err(err) => {
write.send(config.encode(&err)).await.ok(); write.send(config.encode(&err)).await.ok();
return; return;
} }
}; };
info!("User {addr:?} authenticated as @{}", user.username); info!("User {addr:?} authenticated as @{}", user.username);
// Create local state. // Create local state.
let mut state = State::from(user); let mut state = State::from(user, session_id);
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;
} }
@@ -110,20 +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 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, 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;
@@ -134,29 +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() { match state.apply_state().await {
SubscriptionStateChange::Reset => { SubscriptionStateChange::Reset => {
subscriber.unsubscribe_all().await.unwrap(); if let Err(err) = subscriber.unsubscribe_all().await {
for id in state.iter_subscriptions() { error!("Unsubscribe all failed: {err:?}");
subscriber.subscribe(id).await.unwrap(); sentry::capture_error(&err);
break 'out;
}
let subscribed = state.subscribed.read().await;
for id in subscribed.iter() {
if let Err(err) = subscriber.subscribe(id).await {
error!("Subscribe failed: {err:?}");
sentry::capture_error(&err);
break 'out;
}
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -167,112 +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 {
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 {
warn!("Failed to deserialise an event for {}!", message.channel);
return;
};
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; pin_mut!(t1, t2, t3, t4);
if let Err(e) = result {
use async_tungstenite::tungstenite::Error; select! {
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) { _ = t4 => {
warn!("Error while sending an event to {addr:?}: {e:?}"); 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;
}
};
let event = match *REDIS_PAYLOAD_TYPE {
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 {
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 {
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;
}
} }
return;
} }
} }
if let Err(err) = subscriber.quit().await {
error!("{}", err);
sentry::capture_error(&err);
}
} }
async fn worker( #[allow(clippy::too_many_arguments)]
async fn worker_with_kill_signal(
addr: SocketAddr, addr: SocketAddr,
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<()>,
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(
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<()>,
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,
Err(e) => {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
warn!("Error while reading an event from {addr:?}: {e:?}");
}
return;
}
};
let Ok(payload) = config.decode(&msg) else { pin_mut!(t1, t2);
continue;
}; select! {
match payload { _ = t2 => {
ClientMessage::BeginTyping { channel } => { return;
EventV1::ChannelStartTyping { },
id: channel.clone(), result = t1 => {
user: user_id.clone(), let msg = match result {
} Ok(Some(msg)) => msg,
.p(channel.clone()) Ok(None) => {
.await; warn!("Received a None message!");
} sentry::capture_message("Received a None message!", Level::Warning);
ClientMessage::EndTyping { channel } => { return;
EventV1::ChannelStopTyping { }
id: channel.clone(), Err(e) => {
user: user_id.clone(), use async_tungstenite::tungstenite::Error;
} if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
.p(channel.clone()) let err = format!("Error while reading an event from {addr:?}: {e:?}");
.await; warn!("{}", err);
} sentry::capture_message(&err, Level::Warning);
ClientMessage::Ping { data, responded } => { }
if responded.is_none() {
write return;
.lock() }
.await };
.send(config.encode(&EventV1::Pong { data }))
.await let Ok(payload) = config.decode(&msg) else {
.ok(); continue;
};
match payload {
ClientMessage::BeginTyping { channel } => {
if !subscribed.read().await.contains(&channel) {
continue;
}
EventV1::ChannelStartTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::EndTyping { channel } => {
if !subscribed.read().await.contains(&channel) {
continue;
}
EventV1::ChannelStopTyping {
id: channel.clone(),
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();
}
}
_ => {}
} }
} }
_ => {}
} }
} }
} }
+2 -2
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.7.2" version = "0.7.10"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Configuration" description = "Revolt Backend: Configuration"
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+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.2" version = "0.7.10"
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.2", path = "../config" } revolt-config = { version = "0.7.10", path = "../config" }
revolt-result = { version = "0.7.2", path = "../result" } revolt-result = { version = "0.7.10", path = "../result" }
revolt-models = { version = "0.7.2", path = "../models", features = [ revolt-models = { version = "0.7.10", path = "../models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.7.2", path = "../presence" } revolt-presence = { version = "0.7.10", path = "../presence" }
revolt-permissions = { version = "0.7.2", path = "../permissions", features = [ revolt-permissions = { version = "0.7.10", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
+8 -1
View File
@@ -48,6 +48,8 @@ pub enum EventV1 {
/// Successfully authenticated /// Successfully authenticated
Authenticated, Authenticated,
/// Logged out
Logout,
/// Basic data to cache /// Basic data to cache
Ready { Ready {
users: Vec<User>, users: Vec<User>,
@@ -247,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;
} }
} }
} }
@@ -257,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<()> },
} }
@@ -1051,22 +1051,24 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}) })
.collect::<Vec<Invite>>(); .collect::<Vec<Invite>>();
db.db() if !invites.is_empty() {
.collection("channel_invites") db.db()
.insert_many(invites, None) .collection("channel_invites")
.await .insert_many(invites, None)
.expect("failed to insert corrected invite"); .await
.expect("failed to insert corrected invite");
db.db() db.db()
.collection::<Outer>("channel_invites") .collection::<Outer>("channel_invites")
.delete_many( .delete_many(
doc! { doc! {
"type": { "$exists": false } "type": { "$exists": false }
}, },
None, None,
) )
.await .await
.expect("failed to find invites"); .expect("failed to find invites");
}
} }
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`. // Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
@@ -2,7 +2,7 @@ use revolt_config::config;
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use crate::{BotInformation, Database, PartialUser, User}; use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
auto_derived_partial!( auto_derived_partial!(
/// Bot /// Bot
@@ -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));
} }
@@ -142,6 +141,10 @@ impl Bot {
db.update_bot(&self.id, &partial, remove).await?; db.update_bot(&self.id, &partial, remove).await?;
if partial.token.is_some() {
EventV1::Logout.private(self.id.clone()).await;
}
self.apply_options(partial); self.apply_options(partial);
Ok(()) Ok(())
} }
@@ -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
})); }));
} }
@@ -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,13 @@ 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<'_>,
limits: FeaturesLimits,
mut idempotency: IdempotencyKey, mut idempotency: IdempotencyKey,
generate_embeds: bool, generate_embeds: bool,
allow_mentions: bool, allow_mentions: bool,
@@ -221,7 +223,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 +290,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 +322,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,
})); }));
} }
@@ -504,7 +506,35 @@ impl Message {
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 +618,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 +783,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));
} }
+52 -12
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
})) }))
} }
} }
@@ -276,23 +291,27 @@ impl User {
Ok(username) Ok(username)
} }
/// Find a user from a given token and hint /// Find a user and session ID from a given token and hint
#[async_recursion] #[async_recursion]
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> { pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<(User, String)> {
match hint { match hint {
UserHint::Bot => { UserHint::Bot => Ok((
db.fetch_user( db.fetch_user(
&db.fetch_bot_by_token(token) &db.fetch_bot_by_token(token)
.await .await
.map_err(|_| create_error!(InvalidSession))? .map_err(|_| create_error!(InvalidSession))?
.id, .id,
) )
.await .await?,
String::new(),
)),
UserHint::User => {
let session = db.fetch_session_by_token(token).await?;
Ok((db.fetch_user(&session.user_id).await?, session.id))
} }
UserHint::User => db.fetch_user_by_token(token).await,
UserHint::Any => { UserHint::Any => {
if let Ok(user) = User::from_token(db, token, UserHint::User).await { if let Ok(result) = User::from_token(db, token, UserHint::User).await {
Ok(user) Ok(result)
} else { } else {
User::from_token(db, token, UserHint::Bot).await User::from_token(db, token, UserHint::Bot).await
} }
@@ -473,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,
@@ -482,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,
+3 -2
View File
@@ -1,3 +1,4 @@
use authifier::models::Session;
use revolt_result::Result; use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
@@ -16,8 +17,8 @@ pub trait AbstractUsers: Sync + Send {
/// Fetch a user from the database by their username /// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>; async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
/// Fetch a user from the database by their session token /// Fetch a session from the database by token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>; async fn fetch_session_by_token(&self, token: &str) -> Result<Session>;
/// Fetch multiple users by their ids /// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>; async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
@@ -46,10 +46,9 @@ impl AbstractUsers for MongoDb {
.ok_or_else(|| create_error!(NotFound)) .ok_or_else(|| create_error!(NotFound))
} }
/// Fetch a user from the database by their session token /// Fetch a session from the database by token
async fn fetch_user_by_token(&self, token: &str) -> Result<User> { async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
let session = self self.col::<Session>("sessions")
.col::<Session>("sessions")
.find_one( .find_one(
doc! { doc! {
"token": token "token": token
@@ -58,9 +57,7 @@ impl AbstractUsers for MongoDb {
) )
.await .await
.map_err(|_| create_database_error!("find_one", "sessions"))? .map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession))?; .ok_or_else(|| create_error!(InvalidSession))
self.fetch_user(&session.user_id).await
} }
/// Fetch multiple users by their ids /// Fetch multiple users by their ids
@@ -1,3 +1,4 @@
use authifier::models::Session;
use revolt_result::Result; use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
@@ -40,8 +41,8 @@ impl AbstractUsers for ReferenceDb {
.ok_or_else(|| create_error!(NotFound)) .ok_or_else(|| create_error!(NotFound))
} }
/// Fetch a user from the database by their session token /// Fetch a session from the database by token
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> { async fn fetch_session_by_token(&self, _token: &str) -> Result<Session> {
todo!() todo!()
} }
@@ -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;
+27 -16
View File
@@ -995,21 +995,24 @@ impl crate::User {
vec![] vec![]
}, },
badges: self.badges.unwrap_or_default() as u32, badges: self.badges.unwrap_or_default() as u32,
online: can_see_profile
&& revolt_presence::is_online(&self.id).await
&& !matches!(
self.status,
Some(crate::UserStatus {
presence: Some(crate::Presence::Invisible),
..
})
),
status: if can_see_profile { status: if can_see_profile {
self.status.map(|status| status.into()) self.status.map(|status| status.into())
} 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()),
relationship, relationship,
online: can_see_profile && revolt_presence::is_online(&self.id).await,
id: self.id, id: self.id,
} }
} }
@@ -1064,21 +1067,24 @@ impl crate::User {
vec![] vec![]
}, },
badges: self.badges.unwrap_or_default() as u32, badges: self.badges.unwrap_or_default() as u32,
online: can_see_profile
&& is_online
&& !matches!(
self.status,
Some(crate::UserStatus {
presence: Some(crate::Presence::Invisible),
..
})
),
status: if can_see_profile { status: if can_see_profile {
self.status.map(|status| status.into()) self.status.map(|status| status.into())
} 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()),
relationship, relationship,
online: can_see_profile && is_online,
id: self.id, id: self.id,
} }
} }
@@ -1099,13 +1105,19 @@ 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
&& !matches!(
self.status,
Some(crate::UserStatus {
presence: Some(crate::Presence::Invisible),
..
})
),
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()),
relationship: RelationshipStatus::User, relationship: RelationshipStatus::User,
online: revolt_presence::is_online(&self.id).await,
id: self.id, id: self.id,
} }
} }
@@ -1129,7 +1141,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),
@@ -1152,7 +1164,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
+4 -4
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.7.2" version = "0.7.10"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: API Models" description = "Revolt Backend: API Models"
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.7.2", path = "../config" } revolt-config = { version = "0.7.10", path = "../config" }
revolt-permissions = { version = "0.7.2", path = "../permissions" } revolt-permissions = { version = "0.7.10", path = "../permissions" }
# Utility # Utility
regex = "1" regex = "1"
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-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(
+3 -3
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.7.2" version = "0.7.10"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Permission Logic" description = "Revolt Backend: Permission Logic"
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.7.2", path = "../result" } revolt-result = { version = "0.7.10", path = "../result" }
# Utility # Utility
auto_ops = "0.3.0" auto_ops = "0.3.0"
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.7.2" version = "0.7.10"
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>"]
+2 -2
View File
@@ -1,8 +1,8 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.7.2" version = "0.7.10"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Result and Error types" description = "Revolt Backend: Result and Error types"
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+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,
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.7.2" version = "0.7.10"
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"
+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),
}; };
@@ -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
@@ -81,6 +81,7 @@ pub async fn message_send(
channel, channel,
data, data,
v0::MessageAuthor::User(&author), v0::MessageAuthor::User(&author),
user.limits().await,
idempotency, idempotency,
permissions.has_channel_permission(ChannelPermission::SendEmbeds), permissions.has_channel_permission(ChannelPermission::SendEmbeds),
allow_mentions, allow_mentions,
@@ -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,
})); }));
} }
} }
@@ -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,
})); }));
}; };
@@ -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,6 +59,7 @@ pub async fn webhook_execute(
channel, channel,
data, data,
v0::MessageAuthor::Webhook(&webhook.into()), v0::MessageAuthor::Webhook(&webhook.into()),
config().await.features.limits.default,
idempotency, idempotency,
true, true,
true, true,