Compare commits

..

52 commits

Author SHA1 Message Date
Jacob Taylor
cda40e8f9f more funny settings (part 3 of 12)
Some checks failed
Checks / Prefligit / prefligit (push) Failing after 1s
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 3s
Checks / Rust / Clippy (push) Failing after 17s
Checks / Rust / Cargo Test (push) Failing after 15s
2025-06-30 16:59:48 -07:00
Jacob Taylor
4d49133504 sender_workers scaling. this time, with feeling! 2025-06-30 16:59:48 -07:00
Jacob Taylor
66c2fe8315 vehicle loan documentation now available at window 7 2025-06-30 16:59:48 -07:00
Jacob Taylor
8111b89d47 lock the getter instead ??? c/o M 2025-06-30 16:59:48 -07:00
Jacob Taylor
6958bc150b make fetching key room events less smart 2025-06-30 16:59:48 -07:00
Jacob Taylor
05b0f2e365 change rocksdb stats level to 3
scale rocksdb background jobs and subcompactions

change rocksdb default error level to info from error

delete unused num_threads function

fix warns from cargo
2025-06-30 16:59:48 -07:00
nexy7574
513b057e9d modify more log strings so they're more useful than not 2025-06-30 16:59:48 -07:00
nexy7574
5c4a9942f4 When in doubt, log all the things 2025-06-30 16:59:48 -07:00
nexy7574
09b5c3420e log which room struggled to get mainline depth 2025-06-30 16:59:48 -07:00
nexy7574
3a634ff1b2 more logs 2025-06-30 16:59:48 -07:00
nexy7574
70ba586ffd Unsafe, untested, and potentially overeager PDU sanity checks 2025-06-30 16:59:48 -07:00
nexy7574
f0c6391eaa Fix room ID check 2025-06-30 16:59:48 -07:00
nexy7574
cfc5fab813 Kick up a fuss when m.room.create is unfindable 2025-06-30 16:59:48 -07:00
nexy7574
4ff1553b56 Note about ruma#2064 in TODO 2025-06-30 16:59:48 -07:00
nexy7574
6e9e252391 fix an auth rule not applying correctly 2025-06-30 16:59:48 -07:00
Jacob Taylor
c93228b8b7 upgrade some settings to enable 5g in continuwuity
enable converged 6g at the edge in continuwuity

better stateinfo_cache_capacity default

better roomid_spacehierarchy_cache_capacity

make sender workers default better and clamp value to core count

update sender workers documentation

add more parallelism_scaled and make them public

update 1 document
2025-06-30 16:59:48 -07:00
Jacob Taylor
0c2d8aa7bf add futures::FutureExt to make cb15ac3c01 work 2025-06-30 16:59:48 -07:00
Jason Volk
7da679328b Mitigate large futures
Signed-off-by: Jason Volk <jason@zemos.net>
2025-06-30 16:59:48 -07:00
Jacob Taylor
cc1b841eb3 bump the number of allowed immutable memtables by 1, to allow for greater flood protection
this should probably not be applied if you have rocksdb_atomic_flush = false (the default)
2025-06-30 16:59:48 -07:00
Jacob Taylor
bc901aece7 probably incorrectly delete support for non-standardized matrix srv record 2025-06-30 16:59:48 -07:00
Jade Ellis
9dfcf3538f fix: Filter out invalid replacements from bundled aggregations 2025-06-30 16:59:48 -07:00
Jade Ellis
e1127c5d70 feat: Add bundled aggregations support
Add support for the m.replace and m.reference bundled
aggregations.
This should fix plenty of subtle client issues.
Threads are not included in the new code as they have
historically been written to the database. Replacing the
old system would result in issues when switching away from
continuwuity, so saved for later.
Some TODOs have been left re event visibility and ignored users.
These should be OK for now, though.
2025-06-30 16:59:48 -07:00
Jade Ellis
57ad8c7fef refactor: Promote handling unsigned data out of timeline
Also fixes:
- Transaction IDs leaking in event route
- Age not being set for event relations or threads
- Both of the above for search results

Notes down concern with relations table
2025-06-30 16:59:48 -07:00
Jade Ellis
17930708d8
chore: Add second ko-fi as custom link
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 2s
Release Docker Image / define-variables (push) Failing after 12s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 6s
Checks / Rust / Clippy (push) Failing after 14s
Checks / Rust / Cargo Test (push) Failing after 11s
2025-06-29 23:06:26 +01:00
Jade Ellis
ec9d3d613e
chore: Add funding
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 3s
Release Docker Image / define-variables (push) Failing after 1s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 3s
Checks / Rust / Clippy (push) Failing after 14s
Checks / Rust / Cargo Test (push) Failing after 15s
2025-06-29 23:02:15 +01:00
nexy7574
d4862b8ead style: Remove redundant, unused functions
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 13s
Release Docker Image / define-variables (push) Failing after 5s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 5s
Checks / Rust / Clippy (push) Failing after 11s
Checks / Rust / Cargo Test (push) Failing after 12s
2025-06-29 15:38:01 +00:00
Jade Ellis
acb74faa07 feat: Pass sender through admin commands 2025-06-29 15:38:01 +00:00
Jade Ellis
ecc6fda98b feat: Record metadata about user suspensions 2025-06-29 15:38:01 +00:00
nexy7574
13e17d52e0 style: Remove unnecessary imports (clippy) 2025-06-29 15:38:01 +00:00
nexy7574
d8a27eeb54 fix: Failing open on database errors
oops
2025-06-29 15:38:01 +00:00
nexy7574
eb2e3b3bb7 fix: Missing suspensions shouldn't error
Turns out copying and pasting the function
above verbatim actually introduces more
problems than it solves!
2025-06-29 15:38:01 +00:00
nexy7574
72f8cb3038 feat: Do not allow suspended users to send typing statuses 2025-06-29 15:38:01 +00:00
nexy7574
1124097bd1 feat: Only allow private read receipts when suspended 2025-06-29 15:38:01 +00:00
nexy7574
08527a2880 feat: Prevent suspended users upgrading rooms 2025-06-29 15:38:01 +00:00
nexy7574
8e06571e7c feat: Prevent suspended users uploading media 2025-06-29 15:38:01 +00:00
nexy7574
90180916eb feat: Prevent suspended users performing room changes
Prevents kicks, bans, unbans, and alias modification
2025-06-29 15:38:01 +00:00
nexy7574
d0548ec064 feat: Forbid suspended users from sending state events 2025-06-29 15:38:01 +00:00
nexy7574
1ff8af8e9e style: Remove unneeded statements (clippy) 2025-06-29 15:38:01 +00:00
nexy7574
cc864dc8bb feat: Do not allow suspending admin users 2025-06-29 15:38:01 +00:00
nexy7574
8791a9b851 fix: Inappropriate empty check
I once again, assumed `true` is actually `false`.
2025-06-29 15:38:01 +00:00
nexy7574
968c0e236c fix: Create the column appropriately 2025-06-29 15:38:01 +00:00
nexy7574
5d5350a9fe feat: Prevent suspended users creating new rooms 2025-06-29 15:38:01 +00:00
nexy7574
e127c4e5a2 feat: Add un/suspend admin commands 2025-06-29 15:38:01 +00:00
nexy7574
a94128e698 feat: Prevent suspended users joining/knocking on rooms 2025-06-29 15:38:01 +00:00
nexy7574
a6ba9e3045 feat: Prevent suspended users changing their profile 2025-06-29 15:38:01 +00:00
nexy7574
286974cb9a feat: Prevent suspended users redacting events 2025-06-29 15:38:01 +00:00
nexy7574
accfda2586 feat: Prevent suspended users sending events 2025-06-29 15:38:01 +00:00
nexy7574
fac9e090cd feat: Add suspension helper to user service 2025-06-29 15:38:01 +00:00
nexy7574
b4bdd1ee65
chore: Update ruwuma
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 11s
Release Docker Image / define-variables (push) Failing after 3s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 3s
Checks / Rust / Clippy (push) Failing after 20s
Checks / Rust / Cargo Test (push) Failing after 19s
Fixes the wrong field name being serialised
2025-06-29 13:43:27 +01:00
nexy7574
4b5e8df95c
fix: Add missing init fields 2025-06-29 13:29:27 +01:00
nexy7574
d63c8b9fca
feat: Support passing through MSC4293 redact_events 2025-06-29 13:16:31 +01:00
nexy7574
9b6ac6c45f fix: Ignore existing membership when room is disconnected
Some checks failed
Documentation / Build and Deploy Documentation (push) Has been skipped
Checks / Prefligit / prefligit (push) Failing after 7s
Release Docker Image / define-variables (push) Failing after 3s
Release Docker Image / build-image (linux/amd64, release, linux-amd64, base) (push) Has been skipped
Release Docker Image / build-image (linux/arm64, release, linux-arm64, base) (push) Has been skipped
Release Docker Image / merge (push) Has been skipped
Checks / Rust / Format (push) Failing after 7s
Checks / Rust / Clippy (push) Failing after 20s
Checks / Rust / Cargo Test (push) Failing after 16s
2025-06-29 12:14:20 +00:00
23 changed files with 318 additions and 90 deletions

5
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,5 @@
github: [JadedBlueEyes]
# Doesn't support an array, so we can only list nex
ko_fi: nexy7574
custom:
- https://ko-fi.com/JadedBlueEyes

22
Cargo.lock generated
View file

@ -3798,7 +3798,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.10.1" version = "0.10.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"assign", "assign",
"js_int", "js_int",
@ -3818,7 +3818,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-appservice-api" name = "ruma-appservice-api"
version = "0.10.0" version = "0.10.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3830,7 +3830,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-client-api" name = "ruma-client-api"
version = "0.18.0" version = "0.18.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@ -3853,7 +3853,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-common" name = "ruma-common"
version = "0.13.0" version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@ -3885,7 +3885,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-events" name = "ruma-events"
version = "0.28.1" version = "0.28.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap 2.9.0", "indexmap 2.9.0",
@ -3910,7 +3910,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-federation-api" name = "ruma-federation-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"bytes", "bytes",
"headers", "headers",
@ -3932,7 +3932,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identifiers-validation" name = "ruma-identifiers-validation"
version = "0.9.5" version = "0.9.5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror 2.0.12", "thiserror 2.0.12",
@ -3941,7 +3941,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identity-service-api" name = "ruma-identity-service-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3951,7 +3951,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-macros" name = "ruma-macros"
version = "0.13.0" version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro-crate", "proc-macro-crate",
@ -3966,7 +3966,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-push-gateway-api" name = "ruma-push-gateway-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3978,7 +3978,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-signatures" name = "ruma-signatures"
version = "0.15.0" version = "0.15.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=d6870a7fb7f6cccff63f7fd0ff6c581bad80e983#d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=9b65f83981f6f53d185ce77da37aaef9dfd764a9#9b65f83981f6f53d185ce77da37aaef9dfd764a9"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",

View file

@ -350,7 +350,7 @@ version = "0.1.2"
[workspace.dependencies.ruma] [workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma" git = "https://forgejo.ellis.link/continuwuation/ruwuma"
#branch = "conduwuit-changes" #branch = "conduwuit-changes"
rev = "d6870a7fb7f6cccff63f7fd0ff6c581bad80e983" rev = "9b65f83981f6f53d185ce77da37aaef9dfd764a9"
features = [ features = [
"compat", "compat",
"rand", "rand",

View file

@ -7,13 +7,14 @@ use futures::{
io::{AsyncWriteExt, BufWriter}, io::{AsyncWriteExt, BufWriter},
lock::Mutex, lock::Mutex,
}; };
use ruma::EventId; use ruma::{EventId, UserId};
pub(crate) struct Context<'a> { pub(crate) struct Context<'a> {
pub(crate) services: &'a Services, pub(crate) services: &'a Services,
pub(crate) body: &'a [&'a str], pub(crate) body: &'a [&'a str],
pub(crate) timer: SystemTime, pub(crate) timer: SystemTime,
pub(crate) reply_id: Option<&'a EventId>, pub(crate) reply_id: Option<&'a EventId>,
pub(crate) sender: Option<&'a UserId>,
pub(crate) output: Mutex<BufWriter<Vec<u8>>>, pub(crate) output: Mutex<BufWriter<Vec<u8>>>,
} }
@ -36,4 +37,10 @@ impl Context<'_> {
output.write_all(s.as_bytes()).map_err(Into::into).await output.write_all(s.as_bytes()).map_err(Into::into).await
}) })
} }
/// Get the sender as a string, or service user ID if not available
pub(crate) fn sender_or_service_user(&self) -> &UserId {
self.sender
.unwrap_or_else(|| self.services.globals.server_user.as_ref())
}
} }

View file

@ -63,6 +63,7 @@ async fn process_command(services: Arc<Services>, input: &CommandInput) -> Proce
body: &body, body: &body,
timer: SystemTime::now(), timer: SystemTime::now(),
reply_id: input.reply_id.as_deref(), reply_id: input.reply_id.as_deref(),
sender: input.sender.as_deref(),
output: BufWriter::new(Vec::new()).into(), output: BufWriter::new(Vec::new()).into(),
}; };

View file

@ -225,6 +225,47 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
.await .await
} }
#[admin_command]
pub(super) async fn suspend(&self, user_id: String) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
if user_id == self.services.globals.server_user {
return Err!("Not allowed to suspend the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot be suspended.");
}
// TODO: Record the actual user that sent the suspension where possible
self.services
.users
.suspend_account(&user_id, self.sender_or_service_user())
.await;
self.write_str(&format!("User {user_id} has been suspended."))
.await
}
#[admin_command]
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
let user_id = parse_local_user_id(self.services, &user_id)?;
if user_id == self.services.globals.server_user {
return Err!("Not allowed to unsuspend the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
self.services.users.unsuspend_account(&user_id).await;
self.write_str(&format!("User {user_id} has been unsuspended."))
.await
}
#[admin_command] #[admin_command]
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result { pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
let user_id = parse_local_user_id(self.services, &username)?; let user_id = parse_local_user_id(self.services, &username)?;

View file

@ -59,6 +59,28 @@ pub(super) enum UserCommand {
force: bool, force: bool,
}, },
/// - Suspend a user
///
/// Suspended users are able to log in, sync, and read messages, but are not
/// able to send events nor redact them, cannot change their profile, and
/// are unable to join, invite to, or knock on rooms.
///
/// Suspended users can still leave rooms and deactivate their account.
/// Suspending them effectively makes them read-only.
Suspend {
/// Username of the user to suspend
user_id: String,
},
/// - Unsuspend a user
///
/// Reverses the effects of the `suspend` command, allowing the user to send
/// messages, change their profile, create room invites, etc.
Unsuspend {
/// Username of the user to unsuspend
user_id: String,
},
/// - List local users in the database /// - List local users in the database
#[clap(alias = "list")] #[clap(alias = "list")]
ListUsers, ListUsers,

View file

@ -18,6 +18,9 @@ pub(crate) async fn create_alias_route(
body: Ruma<create_alias::v3::Request>, body: Ruma<create_alias::v3::Request>,
) -> Result<create_alias::v3::Response> { ) -> Result<create_alias::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
services services
.rooms .rooms
@ -63,6 +66,9 @@ pub(crate) async fn delete_alias_route(
body: Ruma<delete_alias::v3::Request>, body: Ruma<delete_alias::v3::Request>,
) -> Result<delete_alias::v3::Response> { ) -> Result<delete_alias::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
services services
.rooms .rooms

View file

@ -128,6 +128,9 @@ pub(crate) async fn set_room_visibility_route(
// Return 404 if the room doesn't exist // Return 404 if the room doesn't exist
return Err!(Request(NotFound("Room not found"))); return Err!(Request(NotFound("Room not found")));
} }
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if services if services
.users .users

View file

@ -52,6 +52,9 @@ pub(crate) async fn create_content_route(
body: Ruma<create_content::v3::Request>, body: Ruma<create_content::v3::Request>,
) -> Result<create_content::v3::Response> { ) -> Result<create_content::v3::Response> {
let user = body.sender_user.as_ref().expect("user is authenticated"); let user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let filename = body.filename.as_deref(); let filename = body.filename.as_deref();
let content_type = body.content_type.as_deref(); let content_type = body.content_type.as_deref();

View file

@ -182,6 +182,9 @@ pub(crate) async fn join_room_by_id_route(
body: Ruma<join_room_by_id::v3::Request>, body: Ruma<join_room_by_id::v3::Request>,
) -> Result<join_room_by_id::v3::Response> { ) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
banned_room_check( banned_room_check(
&services, &services,
@ -253,6 +256,9 @@ pub(crate) async fn join_room_by_id_or_alias_route(
let sender_user = body.sender_user.as_deref().expect("user is authenticated"); let sender_user = body.sender_user.as_deref().expect("user is authenticated");
let appservice_info = &body.appservice_info; let appservice_info = &body.appservice_info;
let body = body.body; let body = body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) { let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) {
| Ok(room_id) => { | Ok(room_id) => {
@ -374,6 +380,9 @@ pub(crate) async fn knock_room_route(
) -> Result<knock_room::v3::Response> { ) -> Result<knock_room::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.sender_user();
let body = &body.body; let body = &body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) { let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) {
| Ok(room_id) => { | Ok(room_id) => {
@ -498,6 +507,9 @@ pub(crate) async fn invite_user_route(
body: Ruma<invite_user::v3::Request>, body: Ruma<invite_user::v3::Request>,
) -> Result<invite_user::v3::Response> { ) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites { if !services.users.is_admin(sender_user).await && services.config.block_non_admin_invites {
debug_error!( debug_error!(
@ -572,6 +584,10 @@ pub(crate) async fn kick_user_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<kick_user::v3::Request>, body: Ruma<kick_user::v3::Request>,
) -> Result<kick_user::v3::Response> { ) -> Result<kick_user::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await; let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let Ok(event) = services let Ok(event) = services
@ -607,7 +623,7 @@ pub(crate) async fn kick_user_route(
third_party_invite: None, third_party_invite: None,
..event ..event
}), }),
body.sender_user(), sender_user,
&body.room_id, &body.room_id,
&state_lock, &state_lock,
) )
@ -631,6 +647,10 @@ pub(crate) async fn ban_user_route(
return Err!(Request(Forbidden("You cannot ban yourself."))); return Err!(Request(Forbidden("You cannot ban yourself.")));
} }
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await; let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let current_member_content = services let current_member_content = services
@ -652,6 +672,7 @@ pub(crate) async fn ban_user_route(
is_direct: None, is_direct: None,
join_authorized_via_users_server: None, join_authorized_via_users_server: None,
third_party_invite: None, third_party_invite: None,
redact_events: body.redact_events,
..current_member_content ..current_member_content
}), }),
sender_user, sender_user,
@ -672,6 +693,10 @@ pub(crate) async fn unban_user_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<unban_user::v3::Request>, body: Ruma<unban_user::v3::Request>,
) -> Result<unban_user::v3::Response> { ) -> Result<unban_user::v3::Response> {
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await; let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
let current_member_content = services let current_member_content = services
@ -700,7 +725,7 @@ pub(crate) async fn unban_user_route(
is_direct: None, is_direct: None,
..current_member_content ..current_member_content
}), }),
body.sender_user(), sender_user,
&body.room_id, &body.room_id,
&state_lock, &state_lock,
) )
@ -931,24 +956,32 @@ pub async fn join_room_by_id_helper(
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() }); return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
} }
if let Ok(membership) = services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.await
{
if membership.membership == MembershipState::Ban {
debug_warn!("{sender_user} is banned from {room_id} but attempted to join");
return Err!(Request(Forbidden("You are banned from the room.")));
}
}
let server_in_room = services let server_in_room = services
.rooms .rooms
.state_cache .state_cache
.server_in_room(services.globals.server_name(), room_id) .server_in_room(services.globals.server_name(), room_id)
.await; .await;
// Only check our known membership if we're already in the room.
// See: https://forgejo.ellis.link/continuwuation/continuwuity/issues/855
let membership = if server_in_room {
services
.rooms
.state_accessor
.get_member(room_id, sender_user)
.await
} else {
debug!("Ignoring local state for join {room_id}, we aren't in the room yet.");
Ok(RoomMemberEventContent::new(MembershipState::Leave))
};
if let Ok(m) = membership {
if m.membership == MembershipState::Ban {
debug_warn!("{sender_user} is banned from {room_id} but attempted to join");
// TODO: return reason
return Err!(Request(Forbidden("You are banned from the room.")));
}
}
let local_join = server_in_room let local_join = server_in_room
|| servers.is_empty() || servers.is_empty()
|| (servers.len() == 1 && services.globals.server_is_ours(&servers[0])); || (servers.len() == 1 && services.globals.server_is_ours(&servers[0]));
@ -1825,6 +1858,7 @@ pub async fn leave_room(
displayname: None, displayname: None,
third_party_invite: None, third_party_invite: None,
blurhash: None, blurhash: None,
redact_events: None,
}; };
let is_banned = services.rooms.metadata.is_banned(room_id); let is_banned = services.rooms.metadata.is_banned(room_id);

View file

@ -36,6 +36,9 @@ pub(crate) async fn set_displayname_route(
body: Ruma<set_display_name::v3::Request>, body: Ruma<set_display_name::v3::Request>,
) -> Result<set_display_name::v3::Response> { ) -> Result<set_display_name::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if *sender_user != body.user_id && body.appservice_info.is_none() { if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user"))); return Err!(Request(Forbidden("You cannot update the profile of another user")));
@ -125,6 +128,9 @@ pub(crate) async fn set_avatar_url_route(
body: Ruma<set_avatar_url::v3::Request>, body: Ruma<set_avatar_url::v3::Request>,
) -> Result<set_avatar_url::v3::Response> { ) -> Result<set_avatar_url::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
if *sender_user != body.user_id && body.appservice_info.is_none() { if *sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update the profile of another user"))); return Err!(Request(Forbidden("You cannot update the profile of another user")));
@ -343,6 +349,7 @@ pub async fn update_displayname(
reason: None, reason: None,
is_direct: None, is_direct: None,
third_party_invite: None, third_party_invite: None,
redact_events: None,
}); });
Ok((pdu, room_id)) Ok((pdu, room_id))
@ -396,6 +403,7 @@ pub async fn update_avatar_url(
reason: None, reason: None,
is_direct: None, is_direct: None,
third_party_invite: None, third_party_invite: None,
redact_events: None,
}); });
Ok((pdu, room_id)) Ok((pdu, room_id))

View file

@ -58,29 +58,34 @@ pub(crate) async fn set_read_marker_route(
} }
if let Some(event) = &body.read_receipt { if let Some(event) = &body.read_receipt {
let receipt_content = BTreeMap::from_iter([( if !services.users.is_suspended(sender_user).await? {
event.to_owned(), let receipt_content = BTreeMap::from_iter([(
BTreeMap::from_iter([( event.to_owned(),
ReceiptType::Read, BTreeMap::from_iter([(
BTreeMap::from_iter([(sender_user.to_owned(), ruma::events::receipt::Receipt { ReceiptType::Read,
ts: Some(MilliSecondsSinceUnixEpoch::now()), BTreeMap::from_iter([(
thread: ReceiptThread::Unthreaded, sender_user.to_owned(),
})]), ruma::events::receipt::Receipt {
)]), ts: Some(MilliSecondsSinceUnixEpoch::now()),
)]); thread: ReceiptThread::Unthreaded,
},
)]),
)]),
)]);
services services
.rooms .rooms
.read_receipt .read_receipt
.readreceipt_update( .readreceipt_update(
sender_user, sender_user,
&body.room_id, &body.room_id,
&ruma::events::receipt::ReceiptEvent { &ruma::events::receipt::ReceiptEvent {
content: ruma::events::receipt::ReceiptEventContent(receipt_content), content: ruma::events::receipt::ReceiptEventContent(receipt_content),
room_id: body.room_id.clone(), room_id: body.room_id.clone(),
}, },
) )
.await; .await;
}
} }
if let Some(event) = &body.private_read_receipt { if let Some(event) = &body.private_read_receipt {

View file

@ -1,5 +1,5 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{Result, matrix::pdu::PduBuilder}; use conduwuit::{Err, Result, matrix::pdu::PduBuilder};
use ruma::{ use ruma::{
api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent, api::client::redact::redact_event, events::room::redaction::RoomRedactionEventContent,
}; };
@ -17,6 +17,10 @@ pub(crate) async fn redact_event_route(
) -> Result<redact_event::v3::Response> { ) -> Result<redact_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let body = body.body; let body = body.body;
if services.users.is_suspended(sender_user).await? {
// TODO: Users can redact their own messages while suspended
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await; let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;

View file

@ -70,6 +70,10 @@ pub(crate) async fn create_room_route(
)); ));
} }
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let room_id: OwnedRoomId = match &body.room_id { let room_id: OwnedRoomId = match &body.room_id {
| Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?, | Some(custom_room_id) => custom_room_id_check(&services, custom_room_id)?,
| _ => RoomId::new(&services.server.name), | _ => RoomId::new(&services.server.name),

View file

@ -2,7 +2,7 @@ use std::cmp::max;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{
Error, Result, err, info, Err, Error, Result, err, info,
matrix::{StateKey, pdu::PduBuilder}, matrix::{StateKey, pdu::PduBuilder},
}; };
use futures::StreamExt; use futures::StreamExt;
@ -63,6 +63,10 @@ pub(crate) async fn upgrade_room_route(
)); ));
} }
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
// Create a replacement room // Create a replacement room
let replacement_room = RoomId::new(services.globals.server_name()); let replacement_room = RoomId::new(services.globals.server_name());
@ -189,6 +193,7 @@ pub(crate) async fn upgrade_room_route(
blurhash: services.users.blurhash(sender_user).await.ok(), blurhash: services.users.blurhash(sender_user).await.ok(),
reason: None, reason: None,
join_authorized_via_users_server: None, join_authorized_via_users_server: None,
redact_events: None,
}) })
.expect("event is valid, we just created it"), .expect("event is valid, we just created it"),
unsigned: None, unsigned: None,

View file

@ -23,6 +23,9 @@ pub(crate) async fn send_message_event_route(
let sender_user = body.sender_user(); let sender_user = body.sender_user();
let sender_device = body.sender_device.as_deref(); let sender_device = body.sender_device.as_deref();
let appservice_info = body.appservice_info.as_ref(); let appservice_info = body.appservice_info.as_ref();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
// Forbid m.room.encrypted if encryption is disabled // Forbid m.room.encrypted if encryption is disabled
if MessageLikeEventType::RoomEncrypted == body.event_type && !services.config.allow_encryption if MessageLikeEventType::RoomEncrypted == body.event_type && !services.config.allow_encryption

View file

@ -34,6 +34,10 @@ pub(crate) async fn send_state_event_for_key_route(
) -> Result<send_state_event::v3::Response> { ) -> Result<send_state_event::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
Ok(send_state_event::v3::Response { Ok(send_state_event::v3::Response {
event_id: send_state_event_for_key_helper( event_id: send_state_event_for_key_helper(
&services, &services,

View file

@ -26,41 +26,42 @@ pub(crate) async fn create_typing_event_route(
{ {
return Err!(Request(Forbidden("You are not in this room."))); return Err!(Request(Forbidden("You are not in this room.")));
} }
if !services.users.is_suspended(sender_user).await? {
match body.state { match body.state {
| Typing::Yes(duration) => { | Typing::Yes(duration) => {
let duration = utils::clamp( let duration = utils::clamp(
duration.as_millis().try_into().unwrap_or(u64::MAX), duration.as_millis().try_into().unwrap_or(u64::MAX),
services
.server
.config
.typing_client_timeout_min_s
.try_mul(1000)?,
services
.server
.config
.typing_client_timeout_max_s
.try_mul(1000)?,
);
services services
.server .rooms
.config .typing
.typing_client_timeout_min_s .typing_add(
.try_mul(1000)?, sender_user,
&body.room_id,
utils::millis_since_unix_epoch()
.checked_add(duration)
.expect("user typing timeout should not get this high"),
)
.await?;
},
| _ => {
services services
.server .rooms
.config .typing
.typing_client_timeout_max_s .typing_remove(sender_user, &body.room_id)
.try_mul(1000)?, .await?;
); },
services }
.rooms
.typing
.typing_add(
sender_user,
&body.room_id,
utils::millis_since_unix_epoch()
.checked_add(duration)
.expect("user typing timeout should not get this high"),
)
.await?;
},
| _ => {
services
.rooms
.typing
.typing_remove(sender_user, &body.room_id)
.await?;
},
} }
// ping presence // ping presence

View file

@ -378,6 +378,10 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "userid_password", name: "userid_password",
..descriptor::RANDOM ..descriptor::RANDOM
}, },
Descriptor {
name: "userid_suspension",
..descriptor::RANDOM_SMALL
},
Descriptor { Descriptor {
name: "userid_presenceid", name: "userid_presenceid",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL

View file

@ -44,11 +44,13 @@ struct Services {
services: StdRwLock<Option<Weak<crate::Services>>>, services: StdRwLock<Option<Weak<crate::Services>>>,
} }
/// Inputs to a command are a multi-line string and optional reply_id. /// Inputs to a command are a multi-line string, optional reply_id, and optional
/// sender.
#[derive(Debug)] #[derive(Debug)]
pub struct CommandInput { pub struct CommandInput {
pub command: String, pub command: String,
pub reply_id: Option<OwnedEventId>, pub reply_id: Option<OwnedEventId>,
pub sender: Option<Box<UserId>>,
} }
/// Prototype of the tab-completer. The input is buffered text when tab /// Prototype of the tab-completer. The input is buffered text when tab
@ -161,7 +163,22 @@ impl Service {
pub fn command(&self, command: String, reply_id: Option<OwnedEventId>) -> Result<()> { pub fn command(&self, command: String, reply_id: Option<OwnedEventId>) -> Result<()> {
self.channel self.channel
.0 .0
.send(CommandInput { command, reply_id }) .send(CommandInput { command, reply_id, sender: None })
.map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
}
/// Posts a command to the command processor queue with sender information
/// and returns. Processing will take place on the service worker's task
/// asynchronously. Errors if the queue is full.
pub fn command_with_sender(
&self,
command: String,
reply_id: Option<OwnedEventId>,
sender: Box<UserId>,
) -> Result<()> {
self.channel
.0
.send(CommandInput { command, reply_id, sender: Some(sender) })
.map_err(|e| err!("Failed to enqueue admin command: {e:?}")) .map_err(|e| err!("Failed to enqueue admin command: {e:?}"))
} }
@ -172,7 +189,7 @@ impl Service {
command: String, command: String,
reply_id: Option<OwnedEventId>, reply_id: Option<OwnedEventId>,
) -> ProcessorResult { ) -> ProcessorResult {
self.process_command(CommandInput { command, reply_id }) self.process_command(CommandInput { command, reply_id, sender: None })
.await .await
} }

View file

@ -532,9 +532,11 @@ impl Service {
self.services.search.index_pdu(shortroomid, &pdu_id, &body); self.services.search.index_pdu(shortroomid, &pdu_id, &body);
if self.services.admin.is_admin_command(pdu, &body).await { if self.services.admin.is_admin_command(pdu, &body).await {
self.services self.services.admin.command_with_sender(
.admin body,
.command(body, Some((*pdu.event_id).into()))?; Some((*pdu.event_id).into()),
pdu.sender.clone().into(),
)?;
} }
} }
}, },

View file

@ -16,10 +16,21 @@ use ruma::{
}, },
serde::Raw, serde::Raw,
}; };
use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use crate::{Dep, account_data, admin, globals, rooms}; use crate::{Dep, account_data, admin, globals, rooms};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSuspension {
/// Whether the user is currently suspended
pub suspended: bool,
/// When the user was suspended (Unix timestamp in milliseconds)
pub suspended_at: u64,
/// User ID of who suspended this user
pub suspended_by: String,
}
pub struct Service { pub struct Service {
services: Services, services: Services,
db: Data, db: Data,
@ -52,6 +63,7 @@ struct Data {
userid_lastonetimekeyupdate: Arc<Map>, userid_lastonetimekeyupdate: Arc<Map>,
userid_masterkeyid: Arc<Map>, userid_masterkeyid: Arc<Map>,
userid_password: Arc<Map>, userid_password: Arc<Map>,
userid_suspension: Arc<Map>,
userid_selfsigningkeyid: Arc<Map>, userid_selfsigningkeyid: Arc<Map>,
userid_usersigningkeyid: Arc<Map>, userid_usersigningkeyid: Arc<Map>,
useridprofilekey_value: Arc<Map>, useridprofilekey_value: Arc<Map>,
@ -87,6 +99,7 @@ impl crate::Service for Service {
userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(), userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(),
userid_masterkeyid: args.db["userid_masterkeyid"].clone(), userid_masterkeyid: args.db["userid_masterkeyid"].clone(),
userid_password: args.db["userid_password"].clone(), userid_password: args.db["userid_password"].clone(),
userid_suspension: args.db["userid_suspension"].clone(),
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(), userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(), userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
useridprofilekey_value: args.db["useridprofilekey_value"].clone(), useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
@ -143,6 +156,23 @@ impl Service {
Ok(()) Ok(())
} }
/// Suspend account, placing it in a read-only state
pub async fn suspend_account(&self, user_id: &UserId, suspending_user: &UserId) {
self.db.userid_suspension.raw_put(
user_id,
Json(UserSuspension {
suspended: true,
suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
suspended_by: suspending_user.to_string(),
}),
);
}
/// Unsuspend account, placing it in a read-write state
pub async fn unsuspend_account(&self, user_id: &UserId) {
self.db.userid_suspension.remove(user_id);
}
/// Check if a user has an account on this homeserver. /// Check if a user has an account on this homeserver.
#[inline] #[inline]
pub async fn exists(&self, user_id: &UserId) -> bool { pub async fn exists(&self, user_id: &UserId) -> bool {
@ -159,6 +189,25 @@ impl Service {
.await .await
} }
/// Check if account is suspended
pub async fn is_suspended(&self, user_id: &UserId) -> Result<bool> {
match self
.db
.userid_suspension
.get(user_id)
.await
.deserialized::<UserSuspension>()
{
| Ok(s) => Ok(s.suspended),
| Err(e) =>
if e.is_not_found() {
Ok(false)
} else {
Err(e)
},
}
}
/// Check if account is active, infallible /// Check if account is active, infallible
pub async fn is_active(&self, user_id: &UserId) -> bool { pub async fn is_active(&self, user_id: &UserId) -> bool {
!self.is_deactivated(user_id).await.unwrap_or(true) !self.is_deactivated(user_id).await.unwrap_or(true)