Compare commits

..
Author SHA1 Message Date
Zomatree a6d3373c3f fix: use trust_cloudflare config value instead of env var 2025-09-18 21:09:53 +01:00
Zomatree a392b347a4 refactor: move ratelimits to a generic system for all web servers 2025-09-16 19:15:34 +01:00
Paul Makles a186732ca5 merge: pull request #428 from ElfFlu/bugfix/16byte-attachment-gain 2025-09-12 12:48:28 +01:00
Tom e635bc23ec fix: don't remove timeouts when a member leaves a server (#409) 2025-09-06 18:41:23 -07:00
Zomatree 3cb7da95e3 chore: add missing traits implementations to PermissionValue 2025-09-05 15:22:55 +01:00
Zomatree dea0f675dd fix: implement Serialize to ClientMessage 2025-09-05 13:07:52 +01:00
Zomatree 480f210ce8 feat: include member payload in ServerMemberJoin event 2025-08-24 06:32:38 +01:00
Zomatree caa8607468 feat: include groups and dms in fetch mutuals 2025-08-24 03:40:10 +01:00
ElfFlu 32e6600272 fix: remove authentication tag bytes from attachment download
Signed-off-by: ElfFlu <228066338+ElfFlu@users.noreply.github.com>
2025-08-23 19:05:32 +02:00
Zomatree 5335124306 fix: add license to revolt-parser 2025-08-20 18:55:52 +01:00
Zomatree 1561481eb4 feat: make message lexer use unowned string 2025-08-20 18:55:39 +01:00
IAmTomahawkx 8f9015a6ff fix: apply uname fix to january and autumn 2025-08-18 16:59:08 -07:00
Zomatree ed22b3a5ce chore: switch Reference to using an actual reference 2025-08-12 03:44:29 +01:00
Zomatree 65fbd36624 fix: allow for disabling default features 2025-08-12 02:35:09 +01:00
izzy 050ba16d4a fix: correct shebang for try-tag-and-release 2025-08-07 12:58:52 +02:00
izzy 65bc6c8fc6 chore: add just tool to nix shell 2025-08-07 12:58:15 +02:00
izzy 6ad3da5f35 fix: permit empty remove array in edit requests 2025-08-07 12:57:33 +02:00
izzy 947eb15771 fix: newly created roles should be ranked the lowest 2025-08-07 12:29:42 +02:00
IAmTomahawkx f4ee35fb09 fix: don't update the same field while trying to remove it
fixes #392
2025-08-06 14:18:40 -07:00
IAmTomahawkx 6048587d34 fix: rename openapi operation ids
fixes #406
2025-08-06 12:28:58 -07:00
Builderb 80cf8fc4e8 Increase limit of embed icon url due to new cdn changes causing longer url (#426)
Signed-off-by: Builderb <builderbgamer@gmail.com>
2025-08-03 11:54:22 -07:00
higgs01 4f54227495 chore: use mc alias set instead of removed mc config (#423) 2025-07-18 09:28:31 +01:00
izzy aab1734615 chore: bump version to 0.8.8 2025-06-08 11:57:57 +01:00
Paul Makles 40a41ffd64 merge: pull request #418 from revoltchat/feat/role-ranks-v2 2025-06-08 11:46:00 +01:00
izzy d30ceea373 test: complete test for editing role positions 2025-06-08 11:32:42 +01:00
izzy 3e8a401077 test: begin writing test for role editing 2025-06-08 11:18:47 +01:00
izzy 99f400bc7b fix: logic error in initial data check 2025-06-08 11:18:47 +01:00
izzy 73b576a75f fix: ensure server ID is fanned out with role ranks update
fix: ensure original order is correctly sorted on edit route
2025-06-08 11:18:47 +01:00
izzy 4e4e598daf refactor: minor formatting changes 2025-06-08 11:18:47 +01:00
Zomatree 77daf82b94 chore: refactor new role rankings code 2025-06-08 11:18:47 +01:00
Zomatree e00603f276 fix: route ranking 2025-06-08 11:18:47 +01:00
Zomatree 1b2c7b2fa1 feat: add roles migration 2025-06-08 11:18:47 +01:00
Zomatree c526095d4f feat: initial bulk role reorder route 2025-06-08 11:18:47 +01:00
izzy 8cc4bbea4d refactor: clean up clippy warnings 2025-06-07 17:50:11 +01:00
izzy 911ffc767e merge: remote-tracking branch 'origin/feat/store-session-hello-new' 2025-06-07 17:31:15 +01:00
izzy 1690df998d fix: local tests with overrides were missing prerequisites 2025-06-07 17:30:16 +01:00
izzy 519d3c08a8 chore: correct dependency order in justfile 2025-06-07 16:51:09 +01:00
IAmTomahawkx 9846d8aac2 fix(tests): add policy change value to text fixtures 2025-06-06 21:06:18 -07:00
IAmTomahawkx c74b6255dd don't use local authifier 2025-06-06 20:13:14 -07:00
IAmTomahawkx df91b8c990 store last login time of session
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-06-06 20:10:37 -07:00
izzy c4728c696d feat: policy changes API
chore: bump version to 0.8.7
2025-05-30 13:12:57 +01:00
189 changed files with 3912 additions and 2023 deletions
Generated
+1730 -1163
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -11,6 +11,8 @@ members = [
[patch.crates-io]
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
#authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
#rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
# I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just
+2 -1
View File
@@ -114,7 +114,8 @@ If you'd like to change anything, create a `Revolt.overrides.toml` file and spec
> And corresponding Revolt configuration:
>
> ```toml
> # Revolt.overrides.toml
> # Revolt.overrides.toml
> # and Revolt.test-overrides.toml
> [database]
> mongodb = "mongodb://127.0.0.1:14017"
> redis = "redis://127.0.0.1:14079/"
+1 -1
View File
@@ -34,7 +34,7 @@ services:
- minio
entrypoint: >
/bin/sh -c "while ! /usr/bin/mc ready minio; do
/usr/bin/mc config host add minio http://minio:9000 minioautumn minioautumn;
/usr/bin/mc alias set minio http://minio:9000 minioautumn minioautumn;
echo 'Waiting minio...' && sleep 1;
done; /usr/bin/mc mb minio/revolt-uploads; exit 0;"
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.8.6"
version = "0.8.8"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -41,7 +41,7 @@ revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.6", path = "../core/permissions" }
revolt-permissions = { version = "0.8.8", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis
+15
View File
@@ -100,6 +100,18 @@ impl State {
let user = self.clone_user();
self.cache.is_bot = user.bot.is_some();
// Fetch pending policy changes.
let policy_changes = if user.bot.is_some() {
vec![]
} else {
db.fetch_policy_changes()
.await?
.into_iter()
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.map(Into::into)
.collect()
};
// Find all relationships to the user.
let mut user_ids: HashSet<String> = user
.relations
@@ -227,6 +239,7 @@ impl State {
for channel in &channels {
self.insert_subscription(channel.id().to_string()).await;
}
Ok(EventV1::Ready {
users: if fields.contains(&ReadyPayloadFields::Users) {
Some(users)
@@ -252,6 +265,8 @@ impl State {
user_settings,
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
policy_changes,
})
}
+5
View File
@@ -17,6 +17,7 @@ use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_config::report_internal_error;
use revolt_database::{
events::{client::EventV1, server::ClientMessage},
iso8601_timestamp::Timestamp,
Database, User, UserHint,
};
use revolt_presence::{create_session, delete_session};
@@ -100,6 +101,10 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
info!("User {addr:?} authenticated as @{}", user.username);
db.update_session_last_seen(&session_id, Timestamp::now_utc())
.await
.ok();
// Create local state.
let mut state = State::from(user, session_id);
let user_id = state.cache.user_id.clone();
+5 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -11,8 +11,9 @@ description = "Revolt Backend: Configuration"
[features]
anyhow = ["dep:sentry-anyhow"]
report-macros = ["revolt-result"]
sentry = ["dep:sentry"]
test = ["async-std"]
default = ["test", "anyhow"]
default = ["test", "sentry"]
[dependencies]
# Utility
@@ -32,8 +33,8 @@ log = "0.4.14"
pretty_env_logger = "0.4.0"
# Sentry
sentry = "0.31.5"
sentry = { version = "0.31.5", optional = true }
sentry-anyhow = { version = "0.38.1", optional = true }
# Core
revolt-result = { version = "0.8.6", path = "../result", optional = true }
revolt-result = { version = "0.8.8", path = "../result", optional = true }
+29 -9
View File
@@ -6,10 +6,12 @@ use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
#[cfg(feature = "sentry")]
pub use sentry::{capture_error, capture_message, Level};
#[cfg(feature = "anyhow")]
pub use sentry_anyhow::capture_anyhow;
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! report_error {
( $expr: expr, $error: ident $( $tt:tt )? ) => {
@@ -24,7 +26,7 @@ macro_rules! report_error {
};
}
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! capture_internal_error {
( $expr: expr ) => {
@@ -35,7 +37,7 @@ macro_rules! capture_internal_error {
};
}
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! report_internal_error {
( $expr: expr ) => {
@@ -60,6 +62,9 @@ static CONFIG_SEARCH_PATHS: [&str; 3] = [
"/Revolt.toml",
];
/// Path to search for test overrides
static TEST_OVERRIDE_PATH: &str = "Revolt.test-overrides.toml";
/// Configuration builder
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
RwLock::new({
@@ -73,6 +78,20 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
include_str!("../Revolt.test.toml"),
FileFormat::Toml,
));
// recursively search upwards for an overrides file (if there is one)
if let Ok(cwd) = std::env::current_dir() {
let mut path = Some(cwd.as_path());
while let Some(current_path) = path {
let target_path = current_path.join(TEST_OVERRIDE_PATH);
if target_path.exists() {
builder = builder
.add_source(File::new(target_path.to_str().unwrap(), FileFormat::Toml));
}
path = current_path.parent();
}
}
}
for path in CONFIG_SEARCH_PATHS {
@@ -388,6 +407,11 @@ pub async fn read() -> Config {
pub async fn config() -> Settings {
let mut config = read().await.try_deserialize::<Settings>().unwrap();
// inject REDIS_URI for redis-kiss library
if std::env::var("REDIS_URL").is_err() {
std::env::set_var("REDIS_URI", config.database.redis.clone());
}
// auto-detect production nodes
if config.hosts.api.contains("https") && config.hosts.api.contains("revolt.chat") {
config.production = true;
@@ -397,6 +421,7 @@ pub async fn config() -> Settings {
}
/// Configure logging and common Rust variables
#[cfg(feature = "sentry")]
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
@@ -406,12 +431,6 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
}
if std::env::var("REDIS_URL").is_err() {
// Configure redis-kiss library
let config = config().await;
std::env::set_var("REDIS_URI", config.database.redis);
}
pretty_env_logger::init();
log::info!("Starting {release}");
@@ -428,6 +447,7 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
}
}
#[cfg(feature = "sentry")]
#[macro_export]
macro_rules! configure {
($application: ident) => {
+11 -16
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -10,12 +10,12 @@ description = "Revolt Backend: Database Implementation"
[features]
# Databases
mongodb = ["dep:mongodb", "bson"]
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
# ... Other
tasks = ["isahc", "linkify", "url-escape"]
async-std-runtime = ["async-std"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
axum-impl = ["axum"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
@@ -24,19 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.8.6", path = "../config", features = [
revolt-config = { version = "0.8.8", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.6", path = "../result" }
revolt-models = { version = "0.8.6", path = "../models", features = [
revolt-result = { version = "0.8.8", path = "../result" }
revolt-models = { version = "0.8.8", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.8.6", path = "../presence" }
revolt-permissions = { version = "0.8.6", path = "../permissions", features = [
revolt-presence = { version = "0.8.8", path = "../presence" }
revolt-permissions = { version = "0.8.8", path = "../permissions", features = [
"serde",
"bson",
] }
revolt-parser = { version = "0.8.6", path = "../parser" }
revolt-parser = { version = "0.8.8", path = "../parser" }
# Utility
log = "0.4"
@@ -91,13 +91,8 @@ rocket = { version = "0.5.1", default-features = false, features = [
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
# Notifications
fcm_v1 = "0.3.0"
web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.15", features = ["rocket_impl"] }
authifier = { version = "1.0.15" }
# RabbitMQ
amqprs = { version = "1.7.0" }
@@ -3,18 +3,21 @@
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Member",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "Member",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0002"
},
{
@@ -23,6 +26,9 @@
"channel_type": "Group",
"name": "My Group",
"owner": "__ID:0__",
"recipients": ["__ID:0__", "__ID:1__"]
"recipients": [
"__ID:0__",
"__ID:1__"
]
}
]
]
@@ -3,18 +3,21 @@
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Moderator",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "User",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
@@ -39,7 +42,9 @@
"_id": "__ID:4__",
"owner": "__ID:0__",
"name": "Server",
"channels": ["__ID:3__"],
"channels": [
"__ID:3__"
],
"roles": {
"__ID:5__": {
"name": "Moderator",
@@ -47,7 +52,7 @@
"a": 545270208,
"d": 0
},
"rank": 3
"rank": 1
},
"__ID:6__": {
"name": "Owner",
@@ -66,7 +71,9 @@
"user": "__ID:0__",
"server": "__ID:4__"
},
"roles": ["__ID:6__"],
"roles": [
"__ID:6__"
],
"joined_at": 1698318340195
},
{
@@ -75,7 +82,9 @@
"user": "__ID:1__",
"server": "__ID:4__"
},
"roles": ["__ID:5__"],
"roles": [
"__ID:5__"
],
"joined_at": 1698318340195
},
{
@@ -86,4 +95,4 @@
},
"joined_at": 1698318340195
}
]
]
+31 -12
View File
@@ -1,3 +1,4 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -13,6 +14,7 @@ use authifier::Authifier;
use rand::Rng;
use revolt_config::config;
#[cfg(feature = "mongodb")]
pub use self::mongodb::*;
pub use self::reference::*;
@@ -25,8 +27,10 @@ pub enum DatabaseInfo {
/// Use the mock database
Reference,
/// Connect to MongoDB
#[cfg(feature = "mongodb")]
MongoDb { uri: String, database_name: String },
/// Use existing MongoDB connection
#[cfg(feature = "mongodb")]
MongoDbFromClient(::mongodb::Client, String),
}
@@ -36,6 +40,7 @@ pub enum Database {
/// Mock database
Reference(ReferenceDb),
/// MongoDB database
#[cfg(feature = "mongodb")]
MongoDb(MongoDb),
}
@@ -45,7 +50,7 @@ impl DatabaseInfo {
pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
Ok(match self {
match self {
DatabaseInfo::Auto => {
if std::env::var("TEST_DB").is_ok() {
DatabaseInfo::Test(format!(
@@ -53,16 +58,20 @@ impl DatabaseInfo {
rand::thread_rng().gen_range(1_000_000..10_000_000)
))
.connect()
.await?
.await
} else if !config.database.mongodb.is_empty() {
DatabaseInfo::MongoDb {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name: "revolt".to_string(),
}
.connect()
.await?
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string())
} else {
DatabaseInfo::Reference.connect().await?
DatabaseInfo::Reference.connect().await
}
}
DatabaseInfo::Test(database_name) => {
@@ -70,30 +79,36 @@ impl DatabaseInfo {
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
.as_str()
{
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
"REFERENCE" => DatabaseInfo::Reference.connect().await,
"MONGODB" => {
DatabaseInfo::MongoDb {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name,
}
.connect()
.await?
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string())
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
}
}
DatabaseInfo::Reference => Database::Reference(Default::default()),
DatabaseInfo::Reference => Ok(Database::Reference(Default::default())),
#[cfg(feature = "mongodb")]
DatabaseInfo::MongoDb { uri, database_name } => {
let client = ::mongodb::Client::with_uri_str(uri)
.await
.map_err(|_| "Failed to init db connection.".to_string())?;
Database::MongoDb(MongoDb(client, database_name))
Ok(Database::MongoDb(MongoDb(client, database_name)))
}
#[cfg(feature = "mongodb")]
DatabaseInfo::MongoDbFromClient(client, database_name) => {
Database::MongoDb(MongoDb(client, database_name))
Ok(Database::MongoDb(MongoDb(client, database_name)))
}
})
}
}
}
@@ -219,12 +234,16 @@ impl Database {
Authifier {
database: match self {
Database::Reference(_) => Default::default(),
#[cfg(feature = "mongodb")]
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: auth_config,
#[cfg(feature = "tasks")]
event_channel: Some(crate::tasks::authifier_relay::sender()),
#[cfg(not(feature = "tasks"))]
event_channel: None,
}
}
}
@@ -10,7 +10,6 @@ use serde::de::DeserializeOwned;
use serde::Serialize;
database_derived!(
#[cfg(feature = "mongodb")]
/// MongoDB implementation
pub struct MongoDb(pub ::mongodb::Client, pub String);
);
@@ -4,8 +4,8 @@ use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
UserSettings, Webhook,
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
User, UserSettings, Webhook,
};
database_derived!(
@@ -21,6 +21,7 @@ database_derived!(
pub file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,
pub policy_changes: Arc<Mutex<HashMap<String, PolicyChange>>>,
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,
+13 -2
View File
@@ -6,7 +6,7 @@ use revolt_models::v0::{
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage,
FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message,
PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser,
PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, Webhook,
PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, Webhook,
};
use crate::Database;
@@ -62,6 +62,8 @@ pub enum EventV1 {
user_settings: Option<UserSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
channel_unreads: Option<Vec<ChannelUnread>>,
policy_changes: Vec<PolicyChange>,
},
/// Ping response
@@ -142,7 +144,13 @@ pub enum EventV1 {
},
/// User joins server
ServerMemberJoin { id: String, user: String },
ServerMemberJoin {
id: String,
// Deprecated: use member.id.user
#[deprecated = "Use member.id.user instead"]
user: String,
member: Member,
},
/// User left server
ServerMemberLeave {
@@ -163,6 +171,9 @@ pub enum EventV1 {
/// Server role deleted
ServerRoleDelete { id: String, role_id: String },
/// Server roles ranks updated
ServerRoleRanksUpdate { id: String, ranks: Vec<String> },
/// Update existing user
UserUpdate {
id: String,
+2 -2
View File
@@ -1,8 +1,8 @@
use serde::Deserialize;
use serde::{Serialize, Deserialize};
use super::client::Ping;
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ClientMessage {
Authenticate { token: String },
+4
View File
@@ -25,6 +25,9 @@ pub use mongodb;
#[macro_use]
extern crate bson;
#[cfg(not(feature = "async-std-runtime"))]
compile_error!("async-std-runtime feature must be enabled.");
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! query {
@@ -103,6 +106,7 @@ pub mod util;
pub use models::*;
pub mod events;
#[cfg(feature = "tasks")]
pub mod tasks;
mod amqp;
@@ -1,3 +1,4 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -64,6 +64,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create user_settings collection.");
db.create_collection("policy_changes")
.await
.expect("Failed to create policy_changes collection.");
db.create_collection("safety_reports")
.await
.expect("Failed to create safety_reports collection.");
@@ -1,14 +1,19 @@
use std::{collections::HashSet, ops::BitXor, time::Duration};
use std::{
collections::{HashMap, HashSet},
ops::BitXor,
time::Duration,
};
use crate::{
mongodb::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, User, DISCRIMINATOR_SEARCH_SPACE,
};
use bson::oid::ObjectId;
use bson::{oid::ObjectId, to_bson};
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use revolt_result::{Error, ErrorType};
@@ -21,7 +26,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 33; // MUST BE +1 to last migration
pub const LATEST_REVISION: i32 = 42; // MUST BE +1 to last migration
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -1139,6 +1144,88 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.unwrap();
}
if revision <= 40 {
info!(
"Running migration [revision |> 40 / 30-05-2025]: Set last policy acknowlegement date to now and create policy changes collection."
);
db.db()
.create_collection("policy_changes")
.await
.expect("Failed to create policy_changes collection.");
db.db()
.collection::<User>("users")
.update_many(
doc! {},
doc! {
"$set": {
"last_acknowledged_policy_change": to_bson(&Timestamp::now_utc())
.expect("failed to serialise timestamp")
}
},
)
.await
.expect("failed to update users");
}
if revision <= 41 {
info!(
"Running migration [revision 41 / 05-06-2025]: convert role ranks to uniform numbers."
);
#[derive(Serialize, Deserialize, Clone)]
struct Role {
pub rank: i64,
}
#[derive(Serialize, Deserialize, Clone)]
struct Server {
#[serde(rename = "_id")]
pub id: String,
#[serde(default = "HashMap::<String, Role>::new")]
pub roles: HashMap<String, Role>,
}
let mut servers = db
.db()
.collection::<Server>("servers")
.find(doc! {
"roles": {
"$exists": true,
"$ne": []
}
})
.await
.unwrap()
.filter_map(|s| async { s.ok() })
.boxed();
while let Some(server) = servers.next().await {
let mut ordered_roles = server.roles.clone().into_iter().collect::<Vec<_>>();
ordered_roles.sort_by(|(_, role_a), (_, role_b)| role_a.rank.cmp(&role_b.rank));
let ordered_roles = ordered_roles
.into_iter()
.map(|(id, _)| id)
.collect::<Vec<_>>();
let mut doc = doc! {};
for id in server.roles.keys() {
doc.insert(
format!("roles.{id}.rank"),
ordered_roles.iter().position(|x| id == x).unwrap() as i64,
);
}
db.db()
.collection::<Server>("servers")
.update_one(doc! { "_id": &server.id }, doc! { "$set": doc })
.await
.unwrap();
}
}
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision)
}
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{Bot, FieldsBot, PartialBot};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Invite;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::ChannelUnread;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -8,10 +8,13 @@ use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::{
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
events::client::EventV1, Database, File, PartialServer,
Server, SystemMessage, User, AMQP,
};
#[cfg(feature = "mongodb")]
use crate::IntoDocumentPath;
auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel {
@@ -646,10 +649,11 @@ impl Channel {
.private(user.to_string())
.await;
#[cfg(feature = "tasks")]
crate::tasks::ack::queue_ack(
self.id().to_string(),
user.to_string(),
AckEvent::AckMessage {
crate::tasks::ack::AckEvent::AckMessage {
id: message.to_string(),
},
)
@@ -766,6 +770,7 @@ impl Channel {
}
}
#[cfg(feature = "mongodb")]
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
@@ -1,5 +1,7 @@
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Emoji;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::FileHash;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -4,6 +4,7 @@ use crate::File;
use super::FileUsedFor;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -14,7 +14,6 @@ use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::{
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
permissions::DatabasePermissionQuery,
@@ -22,6 +21,9 @@ use crate::{
Channel, Database, Emoji, File, User, AMQP,
};
#[cfg(feature = "tasks")]
use crate::tasks::{self, ack::AckEvent};
auto_derived_partial!(
/// Message
pub struct Message {
@@ -616,9 +618,11 @@ impl Message {
.await;
// Update last_message_id
#[cfg(feature = "tasks")]
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
// Add mentions for affected users
#[cfg(feature = "tasks")]
if !mentions_elsewhere {
if let Some(mentions) = &self.mentions {
tasks::ack::queue_message(
@@ -637,6 +641,7 @@ impl Message {
}
// Generate embeds
#[cfg(feature = "tasks")]
if generate_embeds {
if let Some(content) = &self.content {
tasks::process_embeds::queue(
@@ -673,10 +678,12 @@ impl Message {
)
.await?;
if !self.has_suppressed_notifications()
&& (self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications
#[cfg(feature = "tasks")]
tasks::ack::queue_message(
self.channel.to_string(),
AckEvent::ProcessMessage {
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
+10 -1
View File
@@ -8,6 +8,7 @@ mod emojis;
mod file_hashes;
mod files;
mod messages;
mod policy_changes;
mod ratelimit_events;
mod safety_reports;
mod safety_snapshots;
@@ -27,6 +28,7 @@ pub use emojis::*;
pub use file_hashes::*;
pub use files::*;
pub use messages::*;
pub use policy_changes::*;
pub use ratelimit_events::*;
pub use safety_reports::*;
pub use safety_snapshots::*;
@@ -36,7 +38,10 @@ pub use servers::*;
pub use user_settings::*;
pub use users::*;
use crate::{Database, MongoDb, ReferenceDb};
use crate::{Database, ReferenceDb};
#[cfg(feature = "mongodb")]
use crate::MongoDb;
pub trait AbstractDatabase:
Sync
@@ -51,6 +56,7 @@ pub trait AbstractDatabase:
+ file_hashes::AbstractAttachmentHashes
+ files::AbstractAttachments
+ messages::AbstractMessages
+ policy_changes::AbstractPolicyChange
+ ratelimit_events::AbstractRatelimitEvents
+ safety_reports::AbstractReport
+ safety_snapshots::AbstractSnapshot
@@ -63,6 +69,8 @@ pub trait AbstractDatabase:
}
impl AbstractDatabase for ReferenceDb {}
#[cfg(feature = "mongodb")]
impl AbstractDatabase for MongoDb {}
impl std::ops::Deref for Database {
@@ -71,6 +79,7 @@ impl std::ops::Deref for Database {
fn deref(&self) -> &Self::Target {
match &self {
Database::Reference(dummy) => dummy,
#[cfg(feature = "mongodb")]
Database::MongoDb(mongo) => mongo,
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,20 @@
use iso8601_timestamp::Timestamp;
auto_derived!(
/// Platform policy change
pub struct PolicyChange {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Time at which this policy was created
pub created_time: Timestamp,
/// Time at which this policy is effective
pub effective_time: Timestamp,
/// Message shown to users
pub description: String,
/// URL with details about changes
pub url: String,
}
);
@@ -0,0 +1,16 @@
use revolt_result::Result;
use crate::PolicyChange;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractPolicyChange: Sync + Send {
/// Fetch all policy changes
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>>;
/// Acknowledge policy changes
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()>;
}
@@ -0,0 +1,46 @@
use bson::to_bson;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::MongoDb;
use crate::PolicyChange;
use crate::User;
use super::AbstractPolicyChange;
static COL: &str = "policy_changes";
#[async_trait]
impl AbstractPolicyChange for MongoDb {
/// Fetch all policy changes
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>> {
query!(self, find, COL, doc! {})
}
/// Acknowledge policy changes
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()> {
let latest_policy = self
.fetch_policy_changes()
.await?
.into_iter()
.map(|policy| policy.created_time)
.max()
.unwrap_or(Timestamp::UNIX_EPOCH);
self.col::<User>("users")
.update_one(
doc! {
"_id": user_id
},
doc! {
"$set": {
"last_acknowledged_policy_change": to_bson(&latest_policy)
.map_err(|_| create_database_error!("to_bson", "timestamp"))?
}
},
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
}
@@ -0,0 +1,31 @@
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::PolicyChange;
use crate::ReferenceDb;
use super::AbstractPolicyChange;
#[async_trait]
impl AbstractPolicyChange for ReferenceDb {
/// Fetch all policy changes
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>> {
let policy_changes = self.policy_changes.lock().await;
Ok(policy_changes.values().cloned().collect())
}
/// Acknowledge policy changes
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()> {
let mut users = self.users.lock().await;
let user = users.get_mut(user_id).expect("user doesn't exist");
user.last_acknowledged_policy_change = self
.fetch_policy_changes()
.await?
.into_iter()
.map(|policy| policy.created_time)
.max()
.unwrap_or(Timestamp::UNIX_EPOCH);
Ok(())
}
}
@@ -1,6 +1,8 @@
use std::time::Duration;
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Report;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Snapshot;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{MemberCompositeKey, ServerBan};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -30,6 +30,9 @@ auto_derived_partial!(
/// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Timestamp>,
// This value only exists in the database, not the models.
// If it is not-None, the database layer should return None to member fetching queries.
// pub pending_deletion_at: Option<Timestamp>
},
"PartialMember"
);
@@ -50,6 +53,7 @@ auto_derived!(
Avatar,
Roles,
Timeout,
JoinedAt,
}
/// Member removal intention
@@ -90,7 +94,7 @@ impl Member {
return Err(create_error!(AlreadyInServer));
}
let member = Member {
let mut member = Member {
id: MemberCompositeKey {
server: server.id.to_string(),
user: user.id.to_string(),
@@ -98,7 +102,9 @@ impl Member {
..Default::default()
};
db.insert_member(&member).await?;
if let Some(updated) = db.insert_or_merge_member(&member).await? {
member = updated;
}
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
@@ -124,6 +130,7 @@ impl Member {
EventV1::ServerMemberJoin {
id: server.id.clone(),
user: user.id.clone(),
member: member.clone().into(),
}
.p(server.id.clone())
.await;
@@ -186,6 +193,7 @@ impl Member {
pub fn remove_field(&mut self, field: &FieldsMember) {
match field {
FieldsMember::JoinedAt => (),
FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(),
@@ -224,7 +232,7 @@ impl Member {
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&self.id).await?;
db.soft_delete_member(&self.id).await?;
EventV1::ServerMemberLeave {
id: self.id.server.to_string(),
@@ -260,3 +268,74 @@ impl Member {
Ok(())
}
}
#[cfg(test)]
mod tests {
use iso8601_timestamp::{Duration, Timestamp};
use revolt_models::v0::DataCreateServer;
use crate::{Member, PartialMember, RemovalIntention, Server, User};
#[async_std::test]
async fn muted_member_rejoin() {
database_test!(|db| async move {
match db {
crate::Database::Reference(_) => return,
crate::Database::MongoDb(_) => (),
}
let owner = User::create(&db, "Server Owner".to_string(), None, None)
.await
.unwrap();
let kickable_user = User::create(&db, "Member".to_string(), None, None)
.await
.unwrap();
let server = Server::create(
&db,
DataCreateServer {
name: "Server".to_string(),
description: None,
nsfw: None,
},
&owner,
false,
)
.await
.unwrap()
.0;
Member::create(&db, &server, &owner, None).await.unwrap();
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
kickable_member
.update(
&db,
PartialMember {
timeout: Some(Timestamp::now_utc() + Duration::minutes(5)),
..Default::default()
},
vec![],
)
.await
.unwrap();
assert!(kickable_member.in_timeout());
kickable_member
.remove(&db, &server, RemovalIntention::Kick, false)
.await
.unwrap();
let kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
assert!(kickable_member.in_timeout())
});
}
}
@@ -1,16 +1,20 @@
use ::mongodb::SessionCursor;
#[cfg(feature = "mongodb")]
use ::mongodb::{ClientSession, SessionCursor};
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
MongoDb {
session: ::mongodb::ClientSession,
session: ClientSession,
cursor: Option<SessionCursor<Member>>,
},
@@ -22,7 +26,7 @@ pub enum ChunkedServerMembersGenerator {
impl ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
pub fn new_mongo(session: ClientSession, cursor: SessionCursor<Member>) -> Self {
ChunkedServerMembersGenerator::MongoDb {
session,
cursor: Some(cursor),
@@ -69,13 +73,13 @@ impl ChunkedServerMembersGenerator {
#[async_trait]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()>;
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>>;
/// Fetch a server member by their id
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
@@ -96,10 +100,10 @@ pub trait AbstractServerMembers: Sync + Send {
) -> Result<ChunkedServerMembersGenerator>;
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>>;
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>;
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>>;
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
@@ -115,6 +119,14 @@ pub trait AbstractServerMembers: Sync + Send {
remove: Vec<FieldsMember>,
) -> Result<()>;
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Marks a user as no longer a member of a server, while retaining the database value.
/// This is used to keep information such as timeouts in place, but will remove information such as join date and applied roles.
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Forcibly delete a server member by their id.
/// This will cancel any pending timeouts or other longer term actions, and they will not be reapplied on rejoin.
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Fetch all members who have been marked for deletion.
async fn remove_dangling_members(&self) -> Result<()>;
}
@@ -1,4 +1,6 @@
use bson::Document;
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use mongodb::options::ReadConcern;
use revolt_result::Result;
@@ -11,9 +13,42 @@ static COL: &str = "server_members";
#[async_trait]
impl AbstractServerMembers for MongoDb {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()> {
query!(self, insert_one, COL, &member).map(|_| ())
/// Insert a new server member (or use the existing member if one is found)
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
let existing: Result<Option<Document>> = query!(
self,
find_one,
COL,
doc! {
"_id.server": &member.id.server,
"_id.user": &member.id.user,
"pending_deletion_at": {"$exists": true}
}
);
// Update the existing record if it exist, otherwise make a new record
if existing.is_ok_and(|x| x.is_some()) {
self.col::<Member>(COL)
.find_one_and_update(
doc! {
"_id.server": &member.id.server,
"_id.user": &member.id.user,
},
doc! {
"$set": {
"joined_at": member.joined_at.duration_since(Timestamp::UNIX_EPOCH).whole_seconds(),
},
"$unset": {
"pending_deletion_at": ""
}
},
)
.return_document(mongodb::options::ReturnDocument::After)
.await
.map_err(|_| create_database_error!("update_one", COL))
} else {
query!(self, insert_one, COL, &member).map(|_| ())?;
Ok(None)
}
}
/// Fetch a server member by their id
@@ -24,18 +59,20 @@ impl AbstractServerMembers for MongoDb {
COL,
doc! {
"_id.server": server_id,
"_id.user": user_id
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id
"_id.server": server_id,
"pending_deletion_at": {"$exists": false}
})
.await
.map_err(|_| create_database_error!("find", COL))?
@@ -139,11 +176,12 @@ impl AbstractServerMembers for MongoDb {
}
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.user": user_id
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
})
.await
.map_err(|_| create_database_error!("find", COL))?
@@ -159,11 +197,12 @@ impl AbstractServerMembers for MongoDb {
}
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"pending_deletion_at": {"$exists": false},
"_id.user": {
"$in": ids
}
@@ -185,7 +224,8 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(doc! {
"_id.server": server_id
"_id.server": server_id,
"pending_deletion_at": {"$exists": false}
})
.await
.map(|c| c as usize)
@@ -196,7 +236,8 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(doc! {
"_id.user": user_id
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
})
.await
.map(|c| c as usize)
@@ -225,8 +266,42 @@ impl AbstractServerMembers for MongoDb {
.map(|_| ())
}
/// Marks a member for deletion.
/// This will remove the record if the user has no pending actions (eg. timeout),
/// otherwise will slate the record for deletion by revolt_crond once the actions expire.
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let member = self.fetch_member(&id.server, &id.user).await;
if let Ok(member) = member {
if member.in_timeout() {
self.col::<Document>(COL)
.update_many(
doc! {
"_id.server": &id.server,
"_id.user": &id.user,
},
doc! {
"$set": {"pending_deletion_at": format!("{}", member.timeout.unwrap().format())},
"$unset": {
"joined_at": "",
"avatar": "",
"nickname": "",
"roles": ""
}
},
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
} else {
self.force_delete_member(id).await
}
} else {
Err(create_database_error!("fetch_member", COL))
}
}
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
query!(
self,
delete_one,
@@ -238,11 +313,25 @@ impl AbstractServerMembers for MongoDb {
)
.map(|_| ())
}
async fn remove_dangling_members(&self) -> Result<()> {
let now = Timestamp::now_utc();
let date = bson::to_bson(&now).expect("Failed to serialize timestamp");
self.col::<Document>(COL)
.delete_many(doc! {
"pending_deletion_at": {"$lt": date}
})
.await
.map(|_| ())
.map_err(|_| create_database_error!("count_documents", COL))
}
}
impl IntoDocumentPath for FieldsMember {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsMember::JoinedAt => "joined_at",
FieldsMember::Avatar => "avatar",
FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles",
@@ -8,13 +8,13 @@ use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
#[async_trait]
impl AbstractServerMembers for ReferenceDb {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()> {
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
let mut server_members = self.server_members.lock().await;
if server_members.contains_key(&member.id) {
Err(create_database_error!("insert", "member"))
} else {
server_members.insert(member.id.clone(), member.clone());
Ok(())
Ok(None)
}
}
@@ -31,7 +31,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
@@ -105,7 +105,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
@@ -115,7 +115,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(ids
.iter()
@@ -169,8 +169,26 @@ impl AbstractServerMembers for ReferenceDb {
}
}
/// Soft delete a member
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await;
let member = server_members.get_mut(id);
if let Some(member) = member {
if member.in_timeout() {
panic!("Soft deletion is not implemented.")
} else if server_members.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await;
if server_members.remove(id).is_some() {
Ok(())
@@ -178,4 +196,8 @@ impl AbstractServerMembers for ReferenceDb {
Err(create_error!(NotFound))
}
}
async fn remove_dangling_members(&self) -> Result<()> {
todo!()
}
}
@@ -181,7 +181,7 @@ impl Server {
}
/// Update server data
pub async fn update<'a>(
pub async fn update(
&mut self,
db: &Database,
partial: PartialServer,
@@ -228,6 +228,13 @@ impl Server {
}
}
/// Ordered roles list
pub fn ordered_roles(&self) -> Vec<(String, Role)> {
let mut ordered_roles = self.roles.clone().into_iter().collect::<Vec<_>>();
ordered_roles.sort_by(|(_, role_a), (_, role_b)| role_a.rank.cmp(&role_b.rank));
ordered_roles
}
/// Set role permission on a server
pub async fn set_role_permission(
&mut self,
@@ -253,6 +260,37 @@ impl Server {
Err(create_error!(NotFound))
}
}
/// Reorders the server's roles rankings
pub async fn set_role_ordering(&mut self, db: &Database, new_order: Vec<String>) -> Result<()> {
// New order must always contain every role
debug_assert_eq!(self.roles.len(), new_order.len());
// Set the role's ranks to the positions in the vec
for (rank, id) in new_order.iter().enumerate() {
self.roles.get_mut(id).unwrap().rank = rank as i64;
}
db.update_server(
&self.id,
&PartialServer {
roles: Some(self.roles.clone()),
..Default::default()
},
Vec::new(),
)
.await?;
// Publish bulk update event
EventV1::ServerRoleRanksUpdate {
id: self.id.clone(),
ranks: new_order,
}
.p(self.id.clone())
.await;
Ok(())
}
}
impl Role {
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::UserSettings;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
+10 -3
View File
@@ -1,14 +1,21 @@
use axum::{extract::FromRequestParts, http::request::Parts};
use axum::{
extract::{FromRef, FromRequestParts},
http::request::Parts,
};
use revolt_result::{create_error, Error, Result};
use crate::{Database, User};
#[async_trait::async_trait]
impl FromRequestParts<Database> for User {
impl<S: Send + Sync> FromRequestParts<S> for User
where
Database: FromRef<S>,
{
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> {
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> {
let db = Database::from_ref(state);
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
let bot = db.fetch_bot_by_token(bot_token).await?;
db.fetch_user(&bot.id).await
@@ -7,11 +7,5 @@ mod rocket;
#[cfg(feature = "rocket-impl")]
mod schema;
#[cfg(feature = "axum-impl")]
pub use self::axum::*;
#[cfg(feature = "rocket-impl")]
pub use self::rocket::*;
#[cfg(feature = "rocket-impl")]
pub use self::schema::*;
pub use model::*;
pub use ops::*;
@@ -57,6 +57,8 @@ auto_derived_partial!(
/// Time until user is unsuspended
#[serde(skip_serializing_if = "Option::is_none")]
pub suspended_until: Option<Timestamp>,
/// Last acknowledged policy change
pub last_acknowledged_policy_change: Timestamp,
},
"PartialUser"
);
@@ -178,6 +180,7 @@ impl Default for User {
privileged: Default::default(),
bot: Default::default(),
suspended_until: Default::default(),
last_acknowledged_policy_change: Timestamp::UNIX_EPOCH,
}
}
}
@@ -200,6 +203,7 @@ impl User {
id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()),
discriminator: User::find_discriminator(db, &username, None).await?,
username,
last_acknowledged_policy_change: Timestamp::now_utc(),
..Default::default()
};
@@ -1,8 +1,10 @@
use authifier::models::Session;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -61,4 +63,6 @@ pub trait AbstractUsers: Sync + Send {
/// Remove push subscription for a session by session id (TODO: remove)
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()>;
async fn update_session_last_seen(&self, session_id: &str, when: Timestamp) -> Result<()>;
}
@@ -1,6 +1,7 @@
use ::mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions};
use authifier::models::Session;
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::DocumentId;
@@ -211,16 +212,34 @@ impl AbstractUsers for MongoDb {
partial: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
if remove.contains(&FieldsUser::StatusText) && partial.status.is_some() {
// stupid-ass workaround to fix mongo conflicting the same item
let _: Result<()> = query!(
self,
update_one_by_id,
COL,
id,
PartialUser {
..Default::default()
},
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ());
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
} else {
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
}
/// Set relationship with another user
@@ -317,7 +336,26 @@ impl AbstractUsers for MongoDb {
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
.map_err(|_| create_database_error!("update_one", "sessions"))
}
async fn update_session_last_seen(&self, session_id: &str, when: Timestamp) -> Result<()> {
let formatted: &str = &when.format();
self.col::<Session>("sessions")
.update_one(
doc! {
"_id": session_id
},
doc! {
"$set": {
"last_seen": formatted
}
},
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "sessions"))
}
}
@@ -1,4 +1,5 @@
use authifier::models::Session;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
@@ -168,4 +169,8 @@ impl AbstractUsers for ReferenceDb {
async fn remove_push_subscription_by_session_id(&self, _session_id: &str) -> Result<()> {
todo!()
}
async fn update_session_last_seen(&self, _session_id: &str, _when: Timestamp) -> Result<()> {
todo!()
}
}
@@ -4,7 +4,7 @@ use once_cell::sync::Lazy;
use crate::events::client::EventV1;
static Q: Lazy<(Sender<AuthifierEvent>, Receiver<AuthifierEvent>)> = Lazy::new(|| unbounded());
static Q: Lazy<(Sender<AuthifierEvent>, Receiver<AuthifierEvent>)> = Lazy::new(unbounded);
/// Get sender
pub fn sender() -> Sender<AuthifierEvent> {
+17 -3
View File
@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use revolt_models::v0::*;
use revolt_permissions::{calculate_user_permissions, UserPermission};
@@ -14,8 +15,7 @@ impl crate::Bot {
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
description: user
.profile
.map(|profile| profile.content)
.flatten()
.and_then(|profile| profile.content)
.unwrap_or_default(),
}
}
@@ -597,6 +597,17 @@ impl From<Masquerade> for crate::Masquerade {
}
}
impl From<crate::PolicyChange> for PolicyChange {
fn from(value: crate::PolicyChange) -> Self {
PolicyChange {
created_time: value.created_time,
effective_time: value.effective_time,
description: value.description,
url: value.url,
}
}
}
impl From<crate::Report> for Report {
fn from(value: crate::Report) -> Self {
Report {
@@ -696,6 +707,7 @@ impl From<crate::FieldsMember> for FieldsMember {
crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout,
crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt,
}
}
}
@@ -707,6 +719,7 @@ impl From<FieldsMember> for crate::FieldsMember {
FieldsMember::Nickname => crate::FieldsMember::Nickname,
FieldsMember::Roles => crate::FieldsMember::Roles,
FieldsMember::Timeout => crate::FieldsMember::Timeout,
FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt,
}
}
}
@@ -1102,7 +1115,7 @@ impl crate::User {
}
/// Convert user object into user model without presence information
pub async fn into_known_static<'a>(self, is_online: bool) -> User {
pub async fn into_known_static(self, is_online: bool) -> User {
let badges = self.get_badges().await;
User {
@@ -1188,6 +1201,7 @@ impl From<User> for crate::User {
privileged: value.privileged,
bot: value.bot.map(Into::into),
suspended_until: None,
last_acknowledged_policy_change: Timestamp::UNIX_EPOCH,
}
}
}
+1 -1
View File
@@ -54,7 +54,7 @@ use revolt_rocket_okapi::{
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
#[cfg(feature = "rocket-impl")]
impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
impl OpenApiFromRequest<'_> for IdempotencyKey {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
+1 -1
View File
@@ -104,7 +104,7 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
.unwrap_or_default();
self.cached_mutual_connection = Some(value);
matches!(value, true)
value
} else {
false
}
+19 -20
View File
@@ -14,41 +14,40 @@ use crate::{
};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
pub struct Reference {
pub struct Reference<'a> {
/// Id of object
pub id: String,
pub id: &'a str,
}
impl Reference {
impl<'a> Reference<'a> {
/// Create a Ref from an unchecked string
pub fn from_unchecked(id: String) -> Reference {
pub fn from_unchecked(id: &'a str) -> Reference<'a> {
Reference { id }
}
/// Fetch ban from Ref
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
db.fetch_ban(server, &self.id).await
db.fetch_ban(server, self.id).await
}
/// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await
db.fetch_bot(self.id).await
}
/// Fetch emoji from Ref
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
db.fetch_emoji(&self.id).await
db.fetch_emoji(self.id).await
}
/// Fetch channel from Ref
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
db.fetch_channel(&self.id).await
db.fetch_channel(self.id).await
}
/// Fetch invite from Ref or create invite to server if discoverable
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
if ulid::Ulid::from_str(&self.id).is_ok() {
if ulid::Ulid::from_str(self.id).is_ok() {
let server = self.as_server(db).await?;
if !server.discoverable {
return Err(create_error!(NotFound));
@@ -65,18 +64,18 @@ impl Reference {
.ok_or(create_error!(NotFound))?,
})
} else {
db.fetch_invite(&self.id).await
db.fetch_invite(self.id).await
}
}
/// Fetch message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(&self.id).await
db.fetch_message(self.id).await
}
/// Fetch message from Ref and validate channel
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
let msg = db.fetch_message(&self.id).await?;
let msg = db.fetch_message(self.id).await?;
if msg.channel != channel {
return Err(create_error!(NotFound));
}
@@ -86,36 +85,36 @@ impl Reference {
/// Fetch member from Ref
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
db.fetch_member(server, &self.id).await
db.fetch_member(server, self.id).await
}
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await
db.fetch_server(self.id).await
}
/// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> {
db.fetch_user(&self.id).await
db.fetch_user(self.id).await
}
/// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await
db.fetch_webhook(self.id).await
}
}
#[cfg(feature = "rocket-impl")]
impl<'r> FromParam<'r> for Reference {
impl<'r> FromParam<'r> for Reference<'r> {
type Error = &'r str;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
Ok(Reference::from_unchecked(param.into()))
Ok(Reference::from_unchecked(param))
}
}
#[cfg(feature = "rocket-impl")]
impl JsonSchema for Reference {
impl<'a> JsonSchema for Reference<'a> {
fn schema_name() -> String {
"Id".to_string()
}
@@ -39,7 +39,9 @@ pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String>
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
LoadedFixture::ServerMember(member) => {
db.insert_or_merge_member(&member).await.unwrap();
}
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-files"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,10 +20,10 @@ typenum = "1.17.0"
aws-config = "1.5.5"
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.8.6", path = "../config", features = [
revolt-config = { version = "0.8.8", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.6", path = "../result" }
revolt-result = { version = "0.8.8", path = "../result" }
# image processing
jxl-oxide = "0.8.1"
+3
View File
@@ -80,6 +80,9 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
.decrypt_in_place(nonce, b"", &mut buf)
.map_err(|_| create_error!(InternalError))?;
// Remove the authentication tag bytes that were added during encryption
buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES);
Ok(buf)
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,8 +20,8 @@ default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.8.6", path = "../config" }
revolt-permissions = { version = "0.8.6", path = "../permissions" }
revolt-config = { version = "0.8.8", path = "../config" }
revolt-permissions = { version = "0.8.8", path = "../permissions" }
# Utility
regex = "1.11"
+2 -2
View File
@@ -132,8 +132,8 @@ auto_derived!(
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
pub interactions_url: Option<String>,
/// Fields to remove from bot object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsBot>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsBot>,
}
/// Where we are inviting a bot to
+6 -6
View File
@@ -207,7 +207,7 @@ auto_derived!(
/// Fields to remove from channel
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Option<Vec<FieldsChannel>>,
pub remove: Vec<FieldsChannel>,
}
/// Create new group
@@ -310,14 +310,14 @@ impl Channel {
/// This returns a Result because the recipient name can't be determined here without a db call,
/// which can't be done since this is models, which can't reference the database crate.
///
/// If it returns Err, you need to fetch the name from the db.
pub fn name(&self) -> Result<&str, ()> {
/// If it returns None, you need to fetch the name from the db.
pub fn name(&self) -> Option<&str> {
match self {
Channel::DirectMessage { .. } => Err(()),
Channel::SavedMessages { .. } => Ok("Saved Messages"),
Channel::DirectMessage { .. } => None,
Channel::SavedMessages { .. } => Some("Saved Messages"),
Channel::TextChannel { name, .. }
| Channel::Group { name, .. }
| Channel::VoiceChannel { name, .. } => Ok(name),
| Channel::VoiceChannel { name, .. } => Some(name),
}
}
}
+1 -1
View File
@@ -215,7 +215,7 @@ auto_derived!(
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct SendableEmbed {
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub icon_url: Option<String>,
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub url: Option<String>,
+2
View File
@@ -7,6 +7,7 @@ mod embeds;
mod emojis;
mod files;
mod messages;
mod policy_changes;
mod safety_reports;
mod server_bans;
mod server_members;
@@ -23,6 +24,7 @@ pub use embeds::*;
pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use policy_changes::*;
pub use safety_reports::*;
pub use server_bans::*;
pub use server_members::*;
@@ -0,0 +1,16 @@
use iso8601_timestamp::Timestamp;
auto_derived!(
/// Platform policy change
pub struct PolicyChange {
/// Time at which this policy was created
pub created_time: Timestamp,
/// Time at which this policy is effective
pub effective_time: Timestamp,
/// Message shown to users
pub description: String,
/// URL with details about changes
pub url: String,
}
);
+3 -2
View File
@@ -77,6 +77,7 @@ auto_derived!(
Avatar,
Roles,
Timeout,
JoinedAt,
}
/// Member removal intention
@@ -124,7 +125,7 @@ auto_derived!(
/// Timestamp this member is timed out until
pub timeout: Option<Timestamp>,
/// Fields to remove from channel object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsMember>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsMember>,
}
);
+12 -5
View File
@@ -175,6 +175,8 @@ auto_derived!(
/// Ranking position
///
/// Smaller values take priority.
///
/// **Removed** - no effect, use the edit server role positions route
pub rank: Option<i64>,
}
@@ -247,8 +249,8 @@ auto_derived!(
pub analytics: Option<bool>,
/// Fields to remove from server object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsServer>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsServer>,
}
/// New role information
@@ -267,11 +269,11 @@ auto_derived!(
pub hoist: Option<bool>,
/// Ranking position
///
/// Smaller values take priority.
/// **Removed** - no effect, use the edit server role positions route
pub rank: Option<i64>,
/// Fields to remove from role object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsRole>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsRole>,
}
/// New role permissions
@@ -286,4 +288,9 @@ auto_derived!(
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
}
/// New role positions
pub struct DataEditRoleRanks {
pub ranks: Vec<String>,
}
);
+5 -3
View File
@@ -245,8 +245,8 @@ auto_derived!(
pub flags: Option<i32>,
/// Fields to remove from user object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsUser>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsUser>,
}
/// User flag reponse
@@ -255,12 +255,14 @@ auto_derived!(
pub flags: i32,
}
/// Mutual friends and servers response
/// Mutual friends, servers, groups and DMs response
pub struct MutualResponse {
/// Array of mutual user IDs that both users are friends with
pub users: Vec<String>,
/// Array of mutual server IDs that both users are in
pub servers: Vec<String>,
/// Array of mutual group and dm IDs that both users are in
pub channels: Vec<String>,
}
/// Bot information for if the user is a bot
+3 -2
View File
@@ -1,8 +1,9 @@
[package]
name = "revolt-parser"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
license = "MIT"
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Message Parser"
[dependencies]
+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.
+34 -23
View File
@@ -4,15 +4,18 @@ use logos::Logos;
#[derive(Debug, Clone, Logos, PartialEq)]
#[logos(skip "\n")]
pub enum MessageToken {
#[logos(subpattern id="[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")]
pub enum MessageToken<'a> {
#[token("\\")]
Escape,
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())]
#[regex("```[^`\n]*", |_| 3)]
#[regex("``", |_| 2)]
#[regex("`", |_| 1)]
CodeblockMarker(usize),
#[regex("<@[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
UserMention(String),
#[regex("<%[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
RoleMention(String),
#[regex("<@(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1])]
UserMention(&'a str),
#[regex("<%(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1],)]
RoleMention(&'a str),
#[token("@everyone")]
MentionEveryone,
#[token("@online")]
@@ -27,13 +30,13 @@ pub struct MessageResults {
pub mentions_online: bool
}
struct MessageParserIterator<I> {
struct MessageParserIterator<'a, I> {
inner: I,
temp: VecDeque<MessageToken>
temp: VecDeque<MessageToken<'a>>
}
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> {
type Item = MessageToken;
impl<'a, I: Iterator<Item = MessageToken<'a>>> Iterator for MessageParserIterator<'a, I> {
type Item = MessageToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
if !self.temp.is_empty() {
@@ -80,8 +83,8 @@ pub fn parse_message(text: &str) -> MessageResults {
match token {
MessageToken::Escape => {}
MessageToken::CodeblockMarker(_) => {},
MessageToken::UserMention(id) => { results.user_mentions.insert(id); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); },
MessageToken::UserMention(id) => { results.user_mentions.insert(id.to_string()); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id.to_string()); },
MessageToken::MentionEveryone => results.mentions_everyone = true,
MessageToken::MentionOnline => results.mentions_online = true,
};
@@ -106,7 +109,7 @@ mod tests {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
}
#[test]
@@ -114,7 +117,7 @@ mod tests {
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
}
#[test]
@@ -138,8 +141,8 @@ mod tests {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>();
assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
@@ -149,8 +152,8 @@ mod tests {
let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
@@ -170,15 +173,14 @@ mod tests {
assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_inline_codeblock_no_mentions() {
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
@@ -192,12 +194,21 @@ mod tests {
assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_codeblock_with_language_no_mentions() {
let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online```").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
}
#[test]
fn test_double_inline_codeblock() {
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Core
revolt-result = { version = "0.8.6", path = "../result" }
revolt-result = { version = "0.8.8", path = "../result" }
# Utility
auto_ops = "0.3.0"
+9 -1
View File
@@ -8,10 +8,18 @@ pub use server::*;
pub use user::*;
/// Holds a permission value to manipulate.
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PermissionValue(u64);
impl PermissionValue {
pub fn from_raw(value: u64) -> Self {
Self(value)
}
pub fn into_raw(self) -> u64 {
self.0
}
/// Apply a given override to this value
pub fn apply(&mut self, v: Override) {
self.allow(v.allow);
+4 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -15,6 +15,9 @@ redis-is-patched = []
# Async
async-std = { version = "1.8.0", features = ["attributes"] }
# Config for loading Redis URI
revolt-config = { version = "0.8.8", path = "../config" }
[dependencies]
# Utility
log = "0.4.17"
+2
View File
@@ -197,6 +197,8 @@ mod tests {
#[async_std::test]
async fn it_works() {
revolt_config::config().await;
// Clear the region before we start the tests:
clear_region(None).await;
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "revolt-ratelimits"
version = "0.8.8"
edition = "2024"
[features]
rocket = ["dep:rocket", "dep:revolt_rocket_okapi", "revolt-database/rocket-impl"]
axum = ["dep:axum", "revolt-database/axum-impl"]
default = ["rocket", "axum"]
[dependencies]
revolt-database = { version = "0.8.8", path = "../database"}
revolt-result = { version = "0.8.8", path = "../result" }
revolt-config = { version = "0.8.8", path = "../config" }
rocket = { version = "0.5.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
axum = { version = "0.7.5", optional = true, features = ["macros"] }
serde = { version = "1", features = ["derive"] }
authifier = { version = "1.0.15" }
dashmap = "5.2.0"
async-trait = "0.1.81"
log = "0.4"
+194
View File
@@ -0,0 +1,194 @@
use std::net::SocketAddr;
use async_trait::async_trait;
use axum::{
Json, RequestPartsExt, Router,
body::Body,
extract::{ConnectInfo, FromRef, FromRequestParts, State},
http::{HeaderValue, Request, StatusCode, request::Parts},
middleware::Next,
response::{IntoResponse, Response},
routing::get,
};
use revolt_database::{Database, User};
use revolt_config::config;
use crate::ratelimiter::{RatelimitInformation, Ratelimiter, RequestKind};
#[derive(Clone, Copy)]
pub struct AxumRequestKind;
impl RequestKind for AxumRequestKind {
type R<'a> = Parts;
}
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<AxumRequestKind>;
fn to_ip(parts: &Parts) -> String {
parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.map(|info| info.ip().to_string())
.unwrap_or_default()
}
async fn to_real_ip(parts: &Parts) -> String {
if config().await.api.security.trust_cloudflare {
parts
.headers
.get("CF-Connecting-IP")
.map(|x| x.to_str().unwrap().to_string())
.unwrap_or_else(|| to_ip(parts))
} else {
to_ip(parts)
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for Ratelimiter
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
type Rejection = Json<Ratelimiter>;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
if parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.is_none()
{
let storage = RatelimitStorage::from_ref(state);
let identifier = if let Ok(user) = parts.extract_with_state::<User, _>(state).await {
user.id
} else {
to_real_ip(parts).await
};
let (bucket, resource) = storage.resolver.resolve_bucket(parts);
let limit = storage.resolver.resolve_bucket_limit(bucket);
let ratelimiter =
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource));
parts.extensions.insert(ratelimiter.map_err(Json));
};
*parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.unwrap()
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for RatelimitInformation
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
type Rejection = Json<RatelimitInformation>;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
if parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.is_none()
{
let ratelimiter = parts.extract_with_state::<Ratelimiter, S>(state).await;
parts.extensions.insert(ratelimiter);
};
let ratelimiter = *parts
.extensions
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
.unwrap();
match ratelimiter {
Ok(ratelimter) => Ok(RatelimitInformation::Success(ratelimter)),
Err(ratelimiter) => Err(Json(RatelimitInformation::Failure {
retry_after: ratelimiter.reset,
})),
}
}
}
pub async fn ratelimit_middleware(
State(database): State<Database>,
State(ratelimit_storage): State<RatelimitStorage>,
request: Request<Body>,
next: Next,
) -> Response {
#[derive(axum::extract::FromRef)]
struct TempState {
database: Database,
ratelimit_storage: RatelimitStorage,
}
let state = TempState {
database,
ratelimit_storage,
};
let (mut parts, body) = request.into_parts();
let res = Ratelimiter::from_request_parts(&mut parts, &state).await;
let (Ok(ratelimiter) | Err(Json(ratelimiter))) = &res;
let mut response = if res.is_ok() {
let request = Request::from_parts(parts, body);
next.run(request).await
} else {
let ratelimit_info = RatelimitInformation::from_request_parts(&mut parts, &state).await;
ratelimit_info.map(Json).into_response()
};
let Ratelimiter {
key,
limit,
remaining,
reset,
} = ratelimiter;
let headers = response.headers_mut();
headers.insert(
"X-RateLimit-Limit",
HeaderValue::from_str(&limit.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Bucket",
HeaderValue::from_str(&key.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Remaining",
HeaderValue::from_str(&remaining.to_string()).unwrap(),
);
headers.insert(
"X-RateLimit-Reset-After",
HeaderValue::from_str(&reset.to_string()).unwrap(),
);
if res.is_err() {
*response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
};
response
}
async fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
Json(info)
}
pub fn routes<S: Clone + Send + Sync + 'static>() -> Router<S>
where
Database: FromRef<S>,
RatelimitStorage: FromRef<S>,
{
Router::new().route("/ratelimit", get(ratelimit_info))
}
+7
View File
@@ -0,0 +1,7 @@
pub mod ratelimiter;
#[cfg(feature = "rocket")]
pub mod rocket;
#[cfg(feature = "axum")]
pub mod axum;
+145
View File
@@ -0,0 +1,145 @@
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
use std::ops::Add;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::Serialize;
use dashmap::DashMap;
pub trait RequestKind {
type R<'a>;
}
pub trait RatelimitResolver<R>: Send + Sync {
fn resolve_bucket<'a>(&self, request: &'a R) -> (&'a str, Option<&'a str>);
fn resolve_bucket_limit(&self, bucket: &str) -> u32;
}
#[derive(Clone)]
pub struct RatelimitStorage<K: RequestKind> {
pub resolver: Arc<dyn for<'a> RatelimitResolver<K::R<'a>>>,
pub map: Arc<DashMap<u64, Entry>>,
}
impl<K: RequestKind> RatelimitStorage<K> {
pub fn new<R: for<'a> RatelimitResolver<K::R<'a>> + 'static>(resolver: R) -> Self {
Self {
resolver: Arc::new(resolver),
map: Arc::new(DashMap::new()),
}
}
}
/// Ratelimit Bucket
#[derive(Clone, Copy, Debug)]
pub struct Entry {
used: u32,
reset: u128,
}
/// Get the current time from Unix Epoch as a Duration
fn now() -> Duration {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards...")
}
impl Entry {
/// Find bucket by its key
pub fn from(map: &DashMap<u64, Entry>, key: u64) -> Entry {
map.get(&key).map(|x| *x).unwrap_or_else(|| Entry {
used: 0,
reset: now().add(Duration::from_secs(10)).as_millis(),
})
}
/// Deduct one unit from the bucket and save
pub fn deduct(&mut self) {
let current_time = now().as_millis();
if current_time > self.reset {
self.used = 1;
self.reset = now().add(Duration::from_secs(10)).as_millis();
} else {
self.used += 1;
}
}
/// Save information
pub fn save(self, map: &DashMap<u64, Entry>, key: u64) {
map.insert(key, self);
}
/// Get remaining units in the bucket
pub fn get_remaining(&self, limit: u32) -> u32 {
if now().as_millis() > self.reset {
limit
} else {
limit - self.used
}
}
/// Get how long bucket has until reset
pub fn left_until_reset(&self) -> u128 {
let current_time = now().as_millis();
self.reset.saturating_sub(current_time)
}
}
/// Ratelimit Guard
#[derive(Serialize, Clone, Copy, Debug)]
#[allow(dead_code)]
pub struct Ratelimiter {
pub key: u64,
pub limit: u32,
pub remaining: u32,
pub reset: u128,
}
impl Ratelimiter {
/// Generate guard from identifier and target bucket
pub fn from(
map: &DashMap<u64, Entry>,
identifier: &str,
limit: u32,
(bucket, resource): (&str, Option<&str>),
) -> Result<Ratelimiter, Ratelimiter> {
let mut key = DefaultHasher::new();
key.write(identifier.as_bytes());
key.write(bucket.as_bytes());
if let Some(id) = resource {
key.write(id.as_bytes());
}
let key = key.finish();
let mut entry = Entry::from(map, key);
let remaining = entry.get_remaining(limit);
let reset = entry.left_until_reset();
let mut ratelimiter = Ratelimiter {
key,
limit,
remaining,
reset,
};
if remaining == 0 {
return Err(ratelimiter);
}
entry.deduct();
entry.save(map, key);
ratelimiter.remaining -= 1;
ratelimiter.reset = entry.left_until_reset();
Ok(ratelimiter)
}
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum RatelimitInformation {
Success(Ratelimiter),
Failure { retry_after: u128 },
}
+163
View File
@@ -0,0 +1,163 @@
use async_trait::async_trait;
use log::info;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::uri::Origin;
use rocket::http::{Method, Status};
use rocket::request::{FromRequest, Outcome};
use rocket::serde::json::Json;
use rocket::{Data, Request, Response, State};
use revolt_config::config;
use revolt_rocket_okapi::r#gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use authifier::models::Session;
use crate::ratelimiter::RequestKind;
use crate::ratelimiter::{RatelimitInformation, Ratelimiter};
#[derive(Clone, Copy)]
pub struct RocketRequestKind;
impl RequestKind for RocketRequestKind {
type R<'a> = Request<'a>;
}
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<RocketRequestKind>;
/// Find the remote IP of the client
fn to_ip(request: &'_ rocket::Request<'_>) -> String {
request
.remote()
.map(|x| x.ip().to_string())
.unwrap_or_default()
}
/// Find the actual IP of the client
async fn to_real_ip(request: &'_ rocket::Request<'_>) -> String {
if config().await.api.security.trust_cloudflare {
request
.headers()
.get_one("CF-Connecting-IP")
.map(|x| x.to_string())
.unwrap_or_else(|| to_ip(request))
} else {
to_ip(request)
}
}
#[async_trait]
impl<'r> FromRequest<'r> for Ratelimiter {
type Error = Ratelimiter;
async fn from_request<'a>(request: &'r rocket::Request<'a>) -> Outcome<Self, Self::Error> {
let ratelimiter = request
.local_cache_async(async {
use rocket::outcome::Outcome;
let storage = request.guard::<&State<RatelimitStorage>>().await.unwrap();
let identifier = if let Outcome::Success(session) = request.guard::<Session>().await
{
session.id
} else {
to_real_ip(request).await
};
let (bucket, resource) = storage.resolver.resolve_bucket(request);
let limit = storage.resolver.resolve_bucket_limit(bucket);
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource))
})
.await;
match ratelimiter {
Ok(ratelimiter) => Outcome::Success(*ratelimiter),
Err(ratelimiter) => Outcome::Error((Status::TooManyRequests, *ratelimiter)),
}
}
}
impl OpenApiFromRequest<'_> for Ratelimiter {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::None)
}
}
/// Attach ratelimiter to the Rocket application
pub struct RatelimitFairing;
#[async_trait]
impl Fairing for RatelimitFairing {
fn info(&self) -> Info {
Info {
name: "Ratelimiter",
kind: Kind::Request | Kind::Response,
}
}
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
use rocket::outcome::Outcome;
if let Outcome::Error(_) = request.guard::<Ratelimiter>().await {
info!(
"User rate-limited on route {}! (IP = {:?})",
request.uri(),
to_real_ip(request).await
);
request.set_method(Method::Get);
request.set_uri(Origin::parse("/ratelimit").unwrap())
}
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
let guard = request.guard::<Ratelimiter>().await;
let (Outcome::Success(ratelimiter) | Outcome::Error((_, ratelimiter))) = guard else {
unreachable!()
};
let Ratelimiter {
key,
limit,
remaining,
reset,
} = ratelimiter;
response.set_raw_header("X-RateLimit-Limit", limit.to_string());
response.set_raw_header("X-RateLimit-Bucket", key.to_string());
response.set_raw_header("X-RateLimit-Remaining", remaining.to_string());
response.set_raw_header("X-RateLimit-Reset-After", reset.to_string());
if guard.is_error() {
response.set_status(Status::TooManyRequests);
}
}
}
#[async_trait]
impl<'r> FromRequest<'r> for RatelimitInformation {
type Error = u128;
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
let info = match request.guard::<Ratelimiter>().await {
Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter),
Outcome::Error((_, ratelimiter)) => RatelimitInformation::Failure {
retry_after: ratelimiter.reset,
},
_ => unreachable!(),
};
Outcome::Success(info)
}
}
#[rocket::get("/ratelimit")]
fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
Json(info)
}
pub fn routes() -> Vec<rocket::Route> {
rocket::routes![ratelimit_info]
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-crond"
version = "0.8.6"
version = "0.8.8"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
edition = "2021"
@@ -16,7 +16,7 @@ log = "0.4"
tokio = { version = "1" }
# Core
revolt-database = { version = "0.8.6", path = "../../core/database" }
revolt-result = { version = "0.8.6", path = "../../core/result" }
revolt-config = { version = "0.8.6", path = "../../core/config" }
revolt-files = { version = "0.8.6", path = "../../core/files" }
revolt-database = { version = "0.8.8", path = "../../core/database" }
revolt-result = { version = "0.8.8", path = "../../core/result" }
revolt-config = { version = "0.8.8", path = "../../core/config" }
revolt-files = { version = "0.8.8", path = "../../core/files" }
+3 -2
View File
@@ -1,7 +1,7 @@
use revolt_config::configure;
use revolt_database::DatabaseInfo;
use revolt_result::Result;
use tasks::{file_deletion, prune_dangling_files};
use tasks::{file_deletion, prune_dangling_files, prune_members};
use tokio::try_join;
pub mod tasks;
@@ -13,7 +13,8 @@ async fn main() -> Result<()> {
let db = DatabaseInfo::Auto.connect().await.expect("database");
try_join!(
file_deletion::task(db.clone()),
prune_dangling_files::task(db)
prune_dangling_files::task(db.clone()),
prune_members::task(db.clone())
)
.map(|_| ())
}
+1
View File
@@ -1,2 +1,3 @@
pub mod file_deletion;
pub mod prune_dangling_files;
pub mod prune_members;
@@ -0,0 +1,18 @@
use std::time::Duration;
use log::warn;
use revolt_database::Database;
use revolt_result::Result;
use tokio::time::sleep;
pub async fn task(db: Database) -> Result<()> {
loop {
let success = db.remove_dangling_members().await;
if let Err(s) = success {
revolt_config::capture_error(&s);
warn!("Failed to prune dangling members: {:?}", &s);
}
sleep(Duration::from_secs(90)).await;
}
}
+7 -6
View File
@@ -1,19 +1,20 @@
[package]
name = "revolt-pushd"
version = "0.8.6"
version = "0.8.8"
edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies]
revolt-result = { version = "0.8.6", path = "../../core/result" }
revolt-config = { version = "0.8.6", path = "../../core/config", features = [
revolt-result = { version = "0.8.8", path = "../../core/result" }
revolt-config = { version = "0.8.8", path = "../../core/config", features = [
"report-macros",
"anyhow"
] }
revolt-database = { version = "0.8.6", path = "../../core/database" }
revolt-models = { version = "0.8.6", path = "../../core/models", features = [
revolt-database = { version = "0.8.8", path = "../../core/database" }
revolt-models = { version = "0.8.8", path = "../../core/models", features = [
"validator",
] }
revolt-presence = { version = "0.8.6", path = "../../core/presence", features = [
revolt-presence = { version = "0.8.8", path = "../../core/presence", features = [
"redis-is-patched",
] }
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.8.6"
version = "0.8.8"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -81,6 +81,7 @@ revolt-models = { path = "../core/models", features = [
revolt-presence = { path = "../core/presence" }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] }
[build-dependencies]
vergen = "7.5.0"
@@ -0,0 +1,114 @@
[
{
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Moderator",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "User",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "Channel",
"_id": "__ID:3__",
"channel_type": "TextChannel",
"name": "General",
"server": "__ID:4__",
"default_permissions": {
"a": 0,
"d": 1048576
},
"role_permissions": {
"__ID:5__": {
"a": 1048576,
"d": 0
}
}
},
{
"_object_type": "Server",
"_id": "__ID:4__",
"owner": "__ID:0__",
"name": "Server",
"channels": [
"__ID:3__"
],
"roles": {
"__ID:5__": {
"name": "Moderator",
"permissions": {
"a": 545270216,
"d": 0
},
"rank": 1
},
"__ID:6__": {
"name": "Owner",
"permissions": {
"a": 0,
"d": 0
},
"rank": 0
},
"__ID:7__": {
"name": "Lower Rank 1",
"permissions": {
"a": 0,
"d": 0
},
"rank": 2
},
"__ID:8__": {
"name": "Lower Rank 2",
"permissions": {
"a": 0,
"d": 0
},
"rank": 2
}
},
"default_permissions": 4000322560
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:0__",
"server": "__ID:4__"
},
"roles": [
"__ID:6__"
],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:1__",
"server": "__ID:4__"
},
"roles": [
"__ID:5__"
],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:2__",
"server": "__ID:4__"
},
"joined_at": 1698318340195
}
]
+7 -2
View File
@@ -11,6 +11,7 @@ pub mod util;
use revolt_config::config;
use revolt_database::events::client::EventV1;
use revolt_database::AMQP;
use revolt_ratelimits::rocket as ratelimiter;
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics;
@@ -122,18 +123,22 @@ pub async fn web() -> Rocket<Build> {
let rocket = rocket::build();
let prometheus = PrometheusMetrics::new();
// Ratelimits
let ratelimits = ratelimiter::RatelimitStorage::new(util::ratelimits::DeltaRatelimits);
routes::mount(config, rocket)
.attach(prometheus.clone())
.mount("/metrics", prometheus)
.mount("/", rocket_cors::catch_all_options_routes())
.mount("/", util::ratelimiter::routes())
.mount("/", ratelimiter::routes())
.mount("/swagger/", swagger)
.mount("/0.8/swagger/", swagger_0_8)
.manage(authifier)
.manage(db)
.manage(amqp)
.manage(cors.clone())
.attach(util::ratelimiter::RatelimitFairing)
.manage(ratelimits)
.attach(ratelimiter::RatelimitFairing)
.attach(cors)
.configure(rocket::Config {
limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
+1 -1
View File
@@ -11,7 +11,7 @@ use rocket_empty::EmptyResponse;
pub async fn delete_bot(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
) -> Result<EmptyResponse> {
let bot = target.as_bot(db).await?;
if bot.owner != user.id {
+3 -4
View File
@@ -14,7 +14,7 @@ use validator::Validate;
pub async fn edit_bot(
db: &State<Database>,
user: User,
target: Reference,
target: Reference<'_>,
data: Json<DataEditBot>,
) -> Result<Json<v0::BotWithUserResponse>> {
let data = data.into_inner();
@@ -37,7 +37,7 @@ pub async fn edit_bot(
if data.public.is_none()
&& data.analytics.is_none()
&& data.interactions_url.is_none()
&& data.remove.is_none()
&& data.remove.is_empty()
{
return Ok(Json(v0::BotWithUserResponse {
bot: bot.into(),
@@ -64,7 +64,6 @@ pub async fn edit_bot(
db,
partial,
remove
.unwrap_or_default()
.into_iter()
.map(|v| v.into())
.collect(),
@@ -100,7 +99,7 @@ mod test {
.body(
json!(v0::DataEditBot {
public: Some(true),
remove: Some(vec![FieldsBot::Token]),
remove: vec![FieldsBot::Token],
..Default::default()
})
.to_string(),
+1 -1
View File
@@ -11,7 +11,7 @@ use rocket::{serde::json::Json, State};
pub async fn fetch_bot(
db: &State<Database>,
user: User,
bot: Reference,
bot: Reference<'_>,
) -> Result<Json<FetchBotResponse>> {
if user.bot.is_some() {
return Err(create_error!(IsBot));
+3 -3
View File
@@ -13,10 +13,10 @@ use rocket::State;
pub async fn fetch_public_bot(
db: &State<Database>,
user: Option<User>,
target: Reference,
target: Reference<'_>,
) -> Result<Json<PublicBot>> {
let bot = db.fetch_bot(&target.id).await?;
if !bot.public && user.map_or(true, |x| x.id != bot.owner) {
let bot = db.fetch_bot(target.id).await?;
if !bot.public && user.is_none_or(|x| x.id != bot.owner) {
return Err(create_error!(NotFound));
}
+3 -3
View File
@@ -20,7 +20,7 @@ pub async fn invite_bot(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
target: Reference<'_>,
dest: Json<v0::InviteBotDestination>,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
@@ -168,8 +168,8 @@ mod test {
.await;
match event {
EventV1::ServerMemberJoin { user, .. } => {
assert_eq!(bot.id, user);
EventV1::ServerMemberJoin { member, .. } => {
assert_eq!(bot.id, member.id.user);
}
_ => unreachable!(),
}
@@ -15,8 +15,8 @@ use rocket_empty::EmptyResponse;
pub async fn ack(
db: &State<Database>,
user: User,
target: Reference,
message: Reference,
target: Reference<'_>,
message: Reference<'_>,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(create_error!(IsBot));
@@ -29,7 +29,7 @@ pub async fn ack(
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
channel
.ack(&user.id, &message.id)
.ack(&user.id, message.id)
.await
.map(|_| EmptyResponse)
}
@@ -17,7 +17,7 @@ pub async fn delete(
db: &State<Database>,
amqp: &State<AMQP>,
user: User,
target: Reference,
target: Reference<'_>,
options: v0::OptionsChannelDelete,
) -> Result<EmptyResponse> {
let mut channel = target.as_channel(db).await?;

Some files were not shown because too many files have changed in this diff Show More