Compare commits

...
Author SHA1 Message Date
Zomatree c020ef97ab fix: add MoveChannels permission and check permissions when moving channels 2025-07-31 14:39:46 +01:00
Zomatree e346464720 fix: reference correct function name 2025-06-08 01:45:20 +01:00
Zomatree 3ab5fda21d chore: add unit test 2025-05-07 15:30:23 +01:00
Zomatree 9e17a5e73a feat: add parent and update permission calculators 2025-05-07 00:55:03 +01:00
Zomatree c7f09ab9d0 feat: initial category permissions and rework of category crud routes 2025-05-06 17:48:38 +01:00
Paul Makles f75d635c81 feat: new parser for mention everything (#411)
* feat: implement custom parser for mentions

* feat: handle backticks in codeblocks and escapes

* chore: remove fancy-regex

* fix: dont serialise empty role mentions

* chore: cleanup role mentions logic

* chore: update lockfile

* fix: actually use everyone and online mentions from content

* fix: broken merge

---------

Signed-off-by: Zomatree <me@zomatree.live>
2025-04-28 17:08:06 +01:00
ELginas 29b6ac3acf docs: correct Revolt config override filename (#410) 2025-04-25 11:27:16 +01:00
Zomatree a1b0e4767a fix: add back missing early adopter badge 2025-04-22 20:23:20 +01:00
Zomatree f4281c7050 fix: dont leak invisible presence to others 2025-04-22 19:53:55 +01:00
Zomatree d2e83c94f3 fix: show full branch name on github webhook messages 2025-04-22 16:13:03 +01:00
Tom 2540860129 feat: Add Mass Mentions to the backend (#394)
* feat: create base of push daemon

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

* Add outbound senders

* Make web_push send to rabbit instead (temp stuff)

* feat: stability and friend requests

* make vapid fr stuff not suck

* swap naming of queue

* move pushd into daemons folder

* fix cargo file for move into daemons folder

* feat: probably working fcm push notifs

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

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

* add pushd to docker

* mix: Remove old code, add stuff to pushd

* fix: lockfile

* feat: update rocket to 5.0.1

* fix: fix queues and ack bugs

* Move rabbit messsage processing into ack queue

* chore: update readme

* chore: optimizations for ack database hits

* pushd flowchart

* misc: update flowchart

* exit dependancy hell

* add rocket_impl flag to authifier

* make the tests file of delta actually compile

* fix: don't silence every push message

* fix: don't silence all messages

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

* validate mentions at a server membership level

* put back that import that was actually important

* minor fix to lockfile

* update delta authifier

* feat: proper permissions for push notifications

* add unit test for mention sanitization

* remove local file dependancy on authifier

* update ports to proper defaults

* fixTM the node bindings

* Theoretically configure docker releases for pushd

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

* declare exchange in pushd and delta

* fix createbuckets script

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

* fix: reference db implementation

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

* fix: remove finally redundant code

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

* fix: changes

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

* fix: other changes

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

* fix: make channel name return result

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

* Add role mention parsing

* feat: update to mongo 3.1, add member generator.

* integrate mass mentions into pushd

* patch redis-rs with updated versions

* feat: chunk role mentions

* move permission bits to 37/38 to avoid livekit conflict

* change role mention format to <%id>

* fix the lockfile from merge

* fix: PR change requests

* feat: add tests

* fix: i am a dumbass

* fix: tests, again

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-04-05 10:45:04 -07:00
izzy 93a5de769d ci: downgrade lockfile 2025-04-02 12:14:31 +01:00
izzy 717d5e9a88 chore: bump version to 0.8.4
chore: enable webhooks in test builds
2025-04-02 12:11:46 +01:00
Paul Makles 18f06467bb Merge pull request #408 from shaksternano/main
feat: add option to send message with missing replies
2025-04-02 12:05:30 +01:00
Builderb 8d88ea9963 feat: allow bots to manage emojis (#407)
* Remove bot check on emoji create

Signed-off-by: Builderb <builderbgamer@gmail.com>

* Remove bot check on emoji delete

Signed-off-by: Builderb <builderbgamer@gmail.com>

---------

Signed-off-by: Builderb <builderbgamer@gmail.com>
2025-04-02 12:05:04 +01:00
Paul Makles e3e1cab02e Merge pull request #399 from StupidRepo/main
fix: allow message pinning and unpinning for DM-like channels
2025-04-02 12:04:44 +01:00
Nils Ponsard 00bd56b687 feat: allow to set an icon when creating a group
Signed-off-by: Nils Ponsard <nilsponsard@gmail.com>
2025-04-02 12:03:38 +01:00
ShaksterNano a7727bba5c feat: add option to send message with missing replies
Signed-off-by: ShaksterNano <54268387+shaksternano@users.noreply.github.com>
2025-03-31 22:26:30 +01:00
Paul Makles 6b8005f530 Merge pull request #403 from phazeschift/tests
test: Add tests for invite/fetch
2025-03-31 11:42:24 +00:00
phazeschift 714fba7ea6 test: add delete server channel test
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-03-16 14:15:45 -07:00
phazeschift ac05fb385c test: add utilities for common setup
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-03-15 19:08:23 -07:00
phazeschift 537c147c4b test: Add tests for invite/fetch
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-03-14 17:17:44 -07:00
Bradlee Barnes 7bee609b84 Merge branch 'revoltchat:main' into main 2025-03-11 02:32:47 +00:00
IAmTomahawkx 859c0214ec chore: ignore python venv and dev script for user generation
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-03-09 21:40:21 -07:00
Bradlee Barnes 5eea0bbc9a fix: remove SavedMessages and Group due to given permissions
Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>
2025-03-08 00:50:57 +00:00
Bradlee Barnes 15dec656a8 fix: allow message pinning and unpinning for DM-like channels
Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>
2025-03-07 22:39:04 +00:00
Paul Makles bcf6561c0c fix: include production default value for config 2025-02-25 14:24:05 +00:00
Paul Makles b177a3e201 fix: don't bump the lockfile version 2025-02-25 11:08:00 +00:00
Paul Makles d1236bf8f2 chore: bump version to 0.8.3 2025-02-24 22:18:10 +00:00
Paul Makles 01490f5723 feat: add company information to email footers 2025-02-24 22:17:48 +00:00
Paul Makles 3ceefb840b chore: clarify cargo deny / tomls 2025-02-18 11:53:21 +00:00
110 changed files with 6520 additions and 4732 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
[profile.default]
slow-timeout = { period = "3s", terminate-after = 2 }
slow-timeout = { period = "5s", terminate-after = 10 }
+2
View File
@@ -5,6 +5,8 @@ compose.override.yml
target
.data
.env
.venv/
venv/
.vercel
.DS_Store
Generated
+2334 -1756
View File
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -11,10 +11,7 @@ members = [
]
[patch.crates-io]
redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
# I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just
+1 -1
View File
@@ -1,5 +1,5 @@
# ⚠️ This configuration is intended for development environment.
# If you'd like to override anything, create a Revolt.override.toml
# If you'd like to override anything, create a Revolt.overrides.toml
[database]
# MongoDB connection URL
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-nodejs-bindings"
version = "0.8.2"
version = "0.8.4"
description = "Node.js bindings for the Revolt software"
authors = ["Paul Makles <me@insrt.uk>"]
license = "MIT"
@@ -20,6 +20,6 @@ serde = { version = "1", features = ["derive"] }
async-std = "1.12.0"
revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-result = { version = "0.8.2", path = "../../core/result" }
revolt-database = { version = "0.8.2", path = "../../core/database" }
revolt-config = { version = "0.8.4", path = "../../core/config" }
revolt-result = { version = "0.8.4", path = "../../core/result" }
revolt-database = { version = "0.8.4", path = "../../core/database" }
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.8.2"
version = "0.8.4"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -36,12 +36,12 @@ async-std = { version = "1.8.0", features = [
] }
# core
authifier = { version = "1.0.9" }
authifier = { version = "1.0.10" }
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.2", path = "../core/permissions" }
revolt-permissions = { version = "0.8.4", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis
+5 -5
View File
@@ -1,5 +1,6 @@
use std::collections::HashSet;
use futures::future::join_all;
use revolt_database::{
events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery,
@@ -198,13 +199,12 @@ impl State {
.collect();
// Make all users appear from our perspective.
let mut users: Vec<v0::User> = users
let mut users: Vec<v0::User> = join_all(users
.into_iter()
.map(|other_user| {
.map(|other_user| async {
let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online)
})
.collect();
other_user.into_known(&user, is_online).await
})).await;
// Make sure we see our own user correctly.
users.push(user.into_self(true).await);
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.8.2"
version = "0.8.4"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -34,4 +34,4 @@ pretty_env_logger = "0.4.0"
sentry = "0.31.5"
# Core
revolt-result = { version = "0.8.2", path = "../result", optional = true }
revolt-result = { version = "0.8.4", path = "../result", optional = true }
+3
View File
@@ -7,3 +7,6 @@ host = "127.0.0.1"
port = 5672
username = "rabbituser"
password = "rabbitpass"
[features]
webhooks_enabled = true
+21 -5
View File
@@ -1,3 +1,5 @@
production = false
[database]
# MongoDB connection URL
# Defaults to the container name specified in self-hosted
@@ -62,19 +64,28 @@ hcaptcha_sitekey = ""
# Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50
[api.users]
[pushd]
# this changes the names of the queues to not overlap
# prod/beta if they happen to be on the same exchange/instance.
# Usually they have to be, so that messages sent from one or the other get sent to everyone
production = true
# Changes how many users are processed in each chunk when resolving role/everyone mentions.
# Increasing this will resolve mentions faster, but will consume more memory while resolving.
mass_mention_chunk_size = 200
# none of these should need changing
exchange = "revolt.notifications"
message_queue = "notifications.origin.message"
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices
mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices
[pushd.vapid]
queue = "notifications.outbound.vapid"
@@ -171,9 +182,13 @@ default_bucket = "revolt-uploads"
[features]
# Bucket to upload to by default
# Feature gate options
webhooks_enabled = false
# Enable push notifications for mass pings (everyone, online, roles)
# When false this will still ping in-client but will not send notifications from pushd
mass_mentions_send_notifications = true
# Can role/everyone pings be used at all
mass_mentions_enabled = true
[features.limits]
@@ -185,6 +200,7 @@ message_reactions = 20
server_emoji = 100
server_roles = 200
server_channels = 200
server_categories = 50
# How many hours since creation a user is considered new
new_user_hours = 72
+27 -1
View File
@@ -175,19 +175,29 @@ pub struct ApiWorkers {
pub max_concurrent_connections: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiUsers {
pub early_adopter_cutoff: Option<u64>
}
#[derive(Deserialize, Debug, Clone)]
pub struct Api {
pub registration: ApiRegistration,
pub smtp: ApiSmtp,
pub security: ApiSecurity,
pub workers: ApiWorkers,
pub users: ApiUsers,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Pushd {
pub production: bool,
pub exchange: String,
pub mass_mention_chunk_size: usize,
// Queues
pub message_queue: String,
pub mass_mention_queue: String,
pub fr_accepted_queue: String,
pub fr_received_queue: String,
pub generic_queue: String,
@@ -214,6 +224,10 @@ impl Pushd {
self.get_routing_key(self.message_queue.clone())
}
pub fn get_mass_mention_routing_key(&self) -> String {
self.get_routing_key(self.mass_mention_queue.clone())
}
pub fn get_fr_accepted_routing_key(&self) -> String {
self.get_routing_key(self.fr_accepted_queue.clone())
}
@@ -267,6 +281,7 @@ pub struct GlobalLimits {
pub server_emoji: usize,
pub server_roles: usize,
pub server_channels: usize,
pub server_categories: usize,
pub new_user_hours: usize,
@@ -314,6 +329,9 @@ impl Default for FeaturesAdvanced {
pub struct Features {
pub limits: FeaturesLimitsCollection,
pub webhooks_enabled: bool,
pub mass_mentions_send_notifications: bool,
pub mass_mentions_enabled: bool,
#[serde(default)]
pub advanced: FeaturesAdvanced,
}
@@ -337,6 +355,7 @@ pub struct Settings {
pub files: Files,
pub features: Features,
pub sentry: Sentry,
pub production: bool,
}
impl Settings {
@@ -364,7 +383,14 @@ pub async fn read() -> Config {
#[cached(time = 30)]
pub async fn config() -> Settings {
read().await.try_deserialize::<Settings>().unwrap()
let mut config = read().await.try_deserialize::<Settings>().unwrap();
// auto-detect production nodes
if config.hosts.api.contains("https") && config.hosts.api.contains("revolt.chat") {
config.production = true;
}
config
}
/// Configure logging and common Rust variables
+9 -8
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.8.2"
version = "0.8.4"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -24,18 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.8.2", path = "../config", features = [
revolt-config = { version = "0.8.4", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.2", path = "../result" }
revolt-models = { version = "0.8.2", path = "../models", features = [
revolt-result = { version = "0.8.4", path = "../result" }
revolt-models = { version = "0.8.4", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.8.2", path = "../presence" }
revolt-permissions = { version = "0.8.2", path = "../permissions", features = [
revolt-presence = { version = "0.8.4", path = "../presence" }
revolt-permissions = { version = "0.8.4", path = "../permissions", features = [
"serde",
"bson",
] }
revolt-parser = { version = "0.1.0", path = "../parser" }
# Utility
log = "0.4"
@@ -64,7 +65,7 @@ redis-kiss = { version = "0.1.4" }
# Database
bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "2.1.0", default-features = false }
mongodb = { optional = true, version = "3.1.0" }
# Database Migration
unicode-segmentation = "1.10.1"
@@ -96,7 +97,7 @@ web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.9", features = ["rocket_impl"] }
authifier = { version = "1.0.10", features = ["rocket_impl"] }
# RabbitMQ
amqprs = { version = "1.7.0" }
+32
View File
@@ -170,6 +170,38 @@ impl AMQP {
.await
}
pub async fn mass_mention_message_sent(
&self,
server_id: String,
payload: Vec<PushNotification>,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = MassMessageSentPayload {
notifications: payload,
server_id,
};
let payload = to_string(&payload).unwrap();
let routing_key = config.pushd.get_mass_mention_routing_key();
debug!(
"Sending mass mention payload on channel {}: {}",
routing_key, payload
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
.finish(),
payload.into(),
BasicPublishArguments::new(&config.pushd.exchange, routing_key.as_str()),
)
.await
}
pub async fn ack_message(
&self,
user_id: String,
+44 -20
View File
@@ -120,26 +120,50 @@ impl Database {
use_tls: config.api.smtp.use_tls,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("../../templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("../../templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../../templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("../../templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("../../templates/deletion.html").into()),
},
welcome: None,
templates: if config.production {
Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("../../templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("../../templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../../templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("../../templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("../../templates/deletion.html").into()),
},
welcome: None,
}
} else {
Templates {
verify: Template {
title: "Verify your account.".into(),
text: include_str!("../../templates/verify.whitelabel.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: None,
},
reset: Template {
title: "Reset your password.".into(),
text: include_str!("../../templates/reset.whitelabel.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: None,
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.whitelabel.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: None,
},
welcome: None,
}
},
}
} else {
+10 -8
View File
@@ -31,17 +31,17 @@ impl MongoDb {
}
/// Get a collection by its name
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
pub fn col<T: Send + Sync>(&self, collection: &str) -> mongodb::Collection<T> {
self.db().collection(collection)
}
/// Insert one document into a collection
pub async fn insert_one<T: Serialize>(
pub async fn insert_one<T: Serialize + Send + Sync>(
&self,
collection: &'static str,
document: T,
) -> Result<InsertOneResult> {
self.col::<T>(collection).insert_one(document, None).await
self.col::<T>(collection).insert_one(document).await
}
/// Count documents by projection
@@ -51,7 +51,7 @@ impl MongoDb {
projection: Document,
) -> Result<u64> {
self.col::<Document>(collection)
.count_documents(projection, None)
.count_documents(projection)
.await
}
@@ -67,7 +67,8 @@ impl MongoDb {
{
Ok(self
.col::<T>(collection)
.find(projection, options)
.find(projection)
.with_options(options)
.await?
.filter_map(|s| async {
if cfg!(debug_assertions) {
@@ -101,7 +102,8 @@ impl MongoDb {
O: Into<Option<FindOneOptions>>,
{
self.col::<T>(collection)
.find_one(projection, options)
.find_one(projection)
.with_options(options)
.await
}
@@ -165,7 +167,7 @@ impl MongoDb {
};
self.col::<Document>(collection)
.update_one(projection, query, None)
.update_one(projection, query)
.await
}
@@ -200,7 +202,7 @@ impl MongoDb {
projection: Document,
) -> Result<DeleteResult> {
self.col::<Document>(collection)
.delete_one(projection, None)
.delete_one(projection)
.await
}
@@ -11,6 +11,12 @@ pub struct MessageSentPayload {
pub users: Vec<String>,
}
#[derive(Serialize, Deserialize)]
pub struct MassMessageSentPayload {
pub notifications: Vec<PushNotification>,
pub server_id: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct FRAcceptedPayload {
pub accepted_user: User,
@@ -10,7 +10,7 @@ impl AbstractMigrations for MongoDb {
#[cfg(test)]
/// Drop the database
async fn drop_database(&self) {
self.db().drop(None).await.ok();
self.db().drop().await.ok();
}
/// Migrate the database
@@ -18,7 +18,7 @@ impl AbstractMigrations for MongoDb {
info!("Migrating the database.");
let list = self
.list_database_names(None, None)
.list_database_names()
.await
.expect("Failed to fetch database names.");
@@ -8,278 +8,254 @@ pub async fn create_database(db: &MongoDb) {
info!("Creating database.");
let db = db.db();
db.create_collection("accounts", None)
db.create_collection("accounts")
.await
.expect("Failed to create accounts collection.");
db.create_collection("users", None)
db.create_collection("users")
.await
.expect("Failed to create users collection.");
db.create_collection("channels", None)
db.create_collection("channels")
.await
.expect("Failed to create channels collection.");
db.create_collection("messages", None)
db.create_collection("messages")
.await
.expect("Failed to create messages collection.");
db.create_collection("servers", None)
db.create_collection("servers")
.await
.expect("Failed to create servers collection.");
db.create_collection("server_members", None)
db.create_collection("server_members")
.await
.expect("Failed to create server_members collection.");
db.create_collection("server_bans", None)
db.create_collection("server_bans")
.await
.expect("Failed to create server_bans collection.");
db.create_collection("channel_invites", None)
db.create_collection("channel_invites")
.await
.expect("Failed to create channel_invites collection.");
db.create_collection("channel_unreads", None)
db.create_collection("channel_unreads")
.await
.expect("Failed to create channel_unreads collection.");
db.create_collection("channel_webhooks", None)
db.create_collection("channel_webhooks")
.await
.expect("Failed to create channel_webhooks collection.");
db.create_collection("migrations", None)
db.create_collection("migrations")
.await
.expect("Failed to create migrations collection.");
db.create_collection("attachments", None)
db.create_collection("attachments")
.await
.expect("Failed to create attachments collection.");
db.create_collection("attachment_hashes", None)
db.create_collection("attachment_hashes")
.await
.expect("Failed to create attachment_hashes collection.");
db.create_collection("user_settings", None)
db.create_collection("user_settings")
.await
.expect("Failed to create user_settings collection.");
db.create_collection("safety_reports", None)
db.create_collection("safety_reports")
.await
.expect("Failed to create safety_reports collection.");
db.create_collection("safety_snapshots", None)
db.create_collection("safety_snapshots")
.await
.expect("Failed to create safety_snapshots collection.");
db.create_collection("safety_strikes", None)
db.create_collection("safety_strikes")
.await
.expect("Failed to create safety_strikes collection.");
db.create_collection("bots", None)
db.create_collection("bots")
.await
.expect("Failed to create bots collection.");
db.create_collection("ratelimit_events", None)
db.create_collection("ratelimit_events")
.await
.expect("Failed to create ratelimit_events collection.");
db.create_collection(
"pubsub",
CreateCollectionOptions::builder()
.capped(true)
.size(1_000_000)
.build(),
)
.await
.expect("Failed to create pubsub collection.");
db.create_collection("pubsub")
.with_options(
CreateCollectionOptions::builder()
.capped(true)
.size(1_000_000)
.build(),
)
.await
.expect("Failed to create pubsub collection.");
db.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
db.run_command(doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
]
},
None,
)
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
})
.await
.expect("Failed to create username index.");
db.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
db.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
"name": "content"
},
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
{
"key": {
"author": 1_i32
},
"name": "author"
"name": "channel_id_compound"
},
{
"key": {
"author": 1_i32
},
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
"name": "author"
},
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
]
},
None,
)
"name": "channel_pinned_compound"
},
]
})
.await
.expect("Failed to create message index.");
db.run_command(
doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
db.run_command(doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
})
.await
.expect("Failed to create channel_unreads index.");
db.run_command(
doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
db.run_command(doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
})
.await
.expect("Failed to create server_members index.");
db.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
db.run_command(doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
},
None,
)
"name": "hash"
},
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
})
.await
.expect("Failed to create attachments index.");
db.run_command(
doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
},
None,
)
db.run_command(doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
})
.await
.expect("Failed to create attachment_hashes index.");
db.collection("migrations")
.insert_one(
doc! {
"_id": 0_i32,
"revision": LATEST_REVISION
},
None,
)
.insert_one(doc! {
"_id": 0_i32,
"revision": LATEST_REVISION
})
.await
.expect("Failed to save migration info.");
db.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
db.run_command(doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
})
.await
.expect("Failed to create ratelimit_events index.");
@@ -26,7 +26,7 @@ pub const LATEST_REVISION: i32 = 31;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
let data = migrations
.find_one(None, None)
.find_one(doc! {})
.await
.expect("Failed to fetch migration data.");
@@ -46,7 +46,6 @@ pub async fn migrate_database(db: &MongoDb) {
"revision": revision
}
},
None,
)
.await
.expect("Failed to commit migration information.");
@@ -74,7 +73,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_many(
doc! { "attachment": { "$exists": 1_i32 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0_i32 } },
None,
)
.await
.expect("Failed to update messages.");
@@ -83,7 +81,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_many(
doc! {},
doc! { "$set": { "tag": "attachments", "size": 0_i32 } },
None,
)
.await
.expect("Failed to update attachments.");
@@ -93,7 +90,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
db.db()
.create_collection("servers", None)
.create_collection("servers")
.await
.expect("Failed to create servers collection.");
}
@@ -103,12 +100,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let messages = db.col::<Document>("messages");
let mut cursor = messages
.find(
doc! {
"attachment": {
"$exists": 1_i32
}
},
.find(doc! {
"attachment": {
"$exists": 1_i32
}
})
.with_options(
FindOptions::builder()
.projection(doc! {
"_id": 1_i32,
@@ -128,19 +125,18 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_one(
doc! { "_id": id },
doc! { "$unset": { "attachment": 1_i32 }, "$set": { "attachments": attachments } },
None,
)
.await
.unwrap();
}
db.db()
.create_collection("channel_unreads", None)
.create_collection("channel_unreads")
.await
.expect("Failed to create channel_unreads collection.");
db.db()
.create_collection("user_settings", None)
.create_collection("user_settings")
.await
.expect("Failed to create user_settings collection.");
}
@@ -149,17 +145,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 4 / 2021-06-01]: Add more server collections.");
db.db()
.create_collection("server_members", None)
.create_collection("server_members")
.await
.expect("Failed to create server_members collection.");
db.db()
.create_collection("server_bans", None)
.create_collection("server_bans")
.await
.expect("Failed to create server_bans collection.");
db.db()
.create_collection("channel_invites", None)
.create_collection("channel_invites")
.await
.expect("Failed to create channel_invites collection.");
}
@@ -182,7 +178,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! {
"$set": to_document(&server).unwrap()
},
None,
)
.await
.expect("Failed to migrate servers.");
@@ -192,20 +187,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 6 / 2021-07-09]: Add message text index.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
}
]
})
.await
.expect("Failed to create message index.");
}
@@ -214,7 +206,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 7 / 2021-08-11]: Add message text index.");
db.db()
.create_collection("bots", None)
.create_collection("bots")
.await
.expect("Failed to create bots collection.");
}
@@ -223,22 +215,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1.");
db.db()
.run_command(
doc! {
"dropIndexes": "accounts",
"index": ["email", "email_normalised"]
},
None,
)
.run_command(doc! {
"dropIndexes": "accounts",
"index": ["email", "email_normalised"]
})
.await
.expect("Failed to delete legacy account indexes.");
let col = db.col::<Document>("sessions");
let mut cursor = db
.col::<Document>("accounts")
.find(doc! {}, None)
.await
.unwrap();
let mut cursor = db.col::<Document>("accounts").find(doc! {}).await.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(account) = doc {
@@ -267,7 +252,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc.insert("subscription", sub);
}
col.insert_one(doc, None).await.ok();
col.insert_one(doc).await.ok();
}
} else {
info!("Account doesn't have any sessions!");
@@ -288,7 +273,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
}
},
None,
)
.await
.unwrap();
@@ -297,11 +281,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 9 {
info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id.");
let mut cursor = db
.col::<Document>("channels")
.find(doc! {}, None)
.await
.unwrap();
let mut cursor = db.col::<Document>("channels").find(doc! {}).await.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(channel) = doc {
@@ -340,7 +320,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"last_message": 1_i32,
}
},
None,
)
.await
.unwrap();
@@ -362,7 +341,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"nonce": 1_i32,
}
},
None,
)
.await
.unwrap();
@@ -375,7 +353,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"nonce": 1_i32,
}
},
None,
)
.await
.unwrap();
@@ -385,70 +362,61 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32
},
"name": "channel"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32
},
"name": "channel"
}
]
})
.await
.expect("Failed to create message index.");
db.db()
.run_command(
doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
.run_command(doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
})
.await
.expect("Failed to create channel_unreads index.");
db.db()
.run_command(
doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
.run_command(doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
})
.await
.expect("Failed to create server_members index.");
}
@@ -457,21 +425,18 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 12 / 2021-11-21]: Add indexes to database.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
}
]
})
.await
.expect("Failed to create message index.");
}
@@ -485,7 +450,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
async_std::task::sleep(Duration::from_secs(10)).await;
let servers = db.col::<Document>("servers");
let mut cursor = servers.find(doc! {}, None).await.unwrap();
let mut cursor = servers.find(doc! {}).await.unwrap();
while let Some(Ok(mut document)) = cursor.next().await {
let id = document.get_str("_id").unwrap().to_string();
@@ -529,13 +494,13 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
servers
.update_one(doc! { "_id": id }, doc! { "$set": update }, None)
.update_one(doc! { "_id": id }, doc! { "$set": update })
.await
.unwrap();
}
let channels = db.col::<Document>("channels");
let mut cursor = channels.find(doc! {}, None).await.unwrap();
let mut cursor = channels.find(doc! {}).await.unwrap();
while let Some(Ok(document)) = cursor.next().await {
let id = document.get_str("_id").unwrap().to_string();
@@ -575,7 +540,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
channels
.update_one(doc! { "_id": id }, update, None)
.update_one(doc! { "_id": id }, update)
.await
.unwrap();
}
@@ -596,7 +561,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"content": "system"
}
},
None,
)
.await
.unwrap();
@@ -621,25 +585,22 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.unwrap();
db.db()
.create_collection("emojis", None)
.create_collection("emojis")
.await
.expect("Failed to create emojis collection.");
db.db()
.run_command(
doc! {
"createIndexes": "emojis",
"indexes": [
{
"key": {
"parent.id": 1_i32,
},
"name": "parent_id"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "emojis",
"indexes": [
{
"key": {
"parent.id": 1_i32,
},
"name": "parent_id"
}
]
})
.await
.expect("Failed to create emoji parent index.");
}
@@ -652,10 +613,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! {},
doc! {
"$set": {
"joined_at": DateTime::now().to_rfc3339_string()
"joined_at": DateTime::now().try_to_rfc3339_string().expect("Failed to convert the date to rfc3339")
}
},
None,
)
.await
.expect("Failed to update server members.");
@@ -666,13 +626,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if db
.db()
.run_command(
doc! {
"dropIndexes": "messages",
"index": ["channel"]
},
None,
)
.run_command(doc! {
"dropIndexes": "messages",
"index": ["channel"]
})
.await
.is_err()
{
@@ -680,20 +637,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"author": 1_i32,
},
"name": "author"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"author": 1_i32,
},
"name": "author"
}
]
})
.await
.expect("Failed to create messages author index.");
}
@@ -703,35 +657,26 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
);
db.db()
.create_collection("safety_reports", None)
.await
.unwrap();
db.db().create_collection("safety_reports").await.unwrap();
db.db()
.create_collection("safety_snapshots", None)
.await
.unwrap();
db.db().create_collection("safety_snapshots").await.unwrap();
}
if revision <= 20 {
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
db.db()
.run_command(
doc! {
"createIndexes": "safety_snapshots",
"indexes": [
{
"key": {
"report_id": 1_i32
},
"name": "report_id"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "safety_snapshots",
"indexes": [
{
"key": {
"report_id": 1_i32
},
"name": "report_id"
}
]
})
.await
.expect("Failed to create safety snapshot index.");
}
@@ -739,10 +684,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 21 {
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
db.db()
.create_collection("safety_strikes", None)
.await
.unwrap();
db.db().create_collection("safety_strikes").await.unwrap();
}
if revision <= 22 {
@@ -756,7 +698,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4"
}
},
None,
)
.await
.expect("Failed to update server members.");
@@ -766,13 +707,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
db.db()
.run_command(
doc! {
"dropIndexes": "users",
"index": "username"
},
None,
)
.run_command(doc! {
"dropIndexes": "users",
"index": "username"
})
.await
.expect("Failed to drop existing username index.");
@@ -787,7 +725,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let users: Vec<UserInformation> = db
.col::<UserInformation>("users")
.find(doc! {}, None)
.find(doc! {})
.await
.unwrap()
.map(|doc| doc.expect("id and username"))
@@ -839,7 +777,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"discriminator": discriminator
}
},
None,
)
.await
.unwrap();
@@ -887,7 +824,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"display_name": &info.username
}
},
None,
)
.await
.unwrap();
@@ -898,43 +834,37 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 24 {
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
db.db()
.create_collection("channel_webhooks", None)
.await
.ok();
db.db().create_collection("channel_webhooks").await.ok();
db.db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
.run_command(doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
]
},
None,
)
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
})
.await
.expect("Failed to create username index.");
};
@@ -950,7 +880,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64
}
},
None,
)
.await
.expect("Failed to update webhooks.");
@@ -959,28 +888,22 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 25 {
info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index.");
db.db()
.create_collection("ratelimit_events", None)
.await
.ok();
db.db().create_collection("ratelimit_events").await.ok();
db.db()
.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
})
.await
.expect("Failed to create ratelimit_events index.");
}
@@ -1016,12 +939,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let invites = db
.db()
.collection::<Outer>("channel_invites")
.find(
doc! {
"type": { "$exists": false }
},
None,
)
.find(doc! {
"type": { "$exists": false }
})
.await
.expect("failed to find invites")
.filter_map(|s| async { s.ok() })
@@ -1055,18 +975,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if !invites.is_empty() {
db.db()
.collection("channel_invites")
.insert_many(invites, None)
.insert_many(invites)
.await
.expect("failed to insert corrected invite");
db.db()
.collection::<Outer>("channel_invites")
.delete_many(
doc! {
"type": { "$exists": false }
},
None,
)
.delete_many(doc! {
"type": { "$exists": false }
})
.await
.expect("failed to find invites");
}
@@ -1076,21 +993,18 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
}
]
})
.await
.expect("Failed to create message index.");
}
@@ -1098,44 +1012,35 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 28 {
info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn.");
db.db()
.create_collection("attachment_hashes", None)
.await
.ok();
db.db().create_collection("attachment_hashes").await.ok();
db.db()
.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
}
]
})
.await
.expect("Failed to create attachments index.");
db.db()
.run_command(
doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
})
.await
.expect("Failed to create attachment_hashes index.");
}
@@ -1146,20 +1051,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 30 / 29-09-2024]: Add index for used_for.id to attachments.");
db.db()
.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
})
.await
.expect("Failed to create attachments index.");
}
@@ -1176,7 +1078,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let webhooks = db
.db()
.collection::<WebhookShell>("channel_webhooks")
.find(doc! {}, None)
.find(doc! {})
.await
.expect("webhooks")
.filter_map(|s| async { s.ok() })
@@ -1207,7 +1109,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"creator_id": creator_id
}
},
None,
)
.await
.expect("update webhook");
@@ -1218,7 +1119,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}) => {
db.db()
.collection::<WebhookShell>("channel_webhooks")
.delete_one(doc! { "_id": webhook._id }, None)
.delete_one(doc! { "_id": webhook._id })
.await
.expect("failed to delete invalid webhook");
}
@@ -24,12 +24,9 @@ impl AbstractChannelInvites for MongoDb {
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
Ok(self
.col::<Invite>(COL)
.find(
doc! {
"server": server_id,
},
None,
)
.find(doc! {
"server": server_id,
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -26,6 +26,14 @@ pub trait AbstractChannelUnreads: Sync + Send {
message_ids: &[String],
) -> Result<()>;
/// Add a mention.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()>;
/// Fetch all unreads with mentions for a user.
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
@@ -37,6 +37,8 @@ impl AbstractChannelUnreads for MongoDb {
"last_id": message_id
}
},
)
.with_options(
FindOneAndUpdateOptions::builder()
.upsert(true)
.return_document(ReturnDocument::After)
@@ -51,15 +53,12 @@ impl AbstractChannelUnreads for MongoDb {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL)
.delete_many(
doc! {
"_id.channel": {
"$in": channel_ids
},
"_id.user": user_id
.delete_many(doc! {
"_id.channel": {
"$in": channel_ids
},
None,
)
"_id.user": user_id
})
.await
.map_err(|_| create_database_error!("delete_many", COL))?;
@@ -77,7 +76,6 @@ impl AbstractChannelUnreads for MongoDb {
}
})
.collect::<Vec<Document>>(),
None,
)
.await
.map(|_| ())
@@ -104,13 +102,42 @@ impl AbstractChannelUnreads for MongoDb {
}
}
},
UpdateOptions::builder().upsert(true).build(),
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Add a mention to multiple users.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()> {
self.col::<Document>(COL)
.update_many(
doc! {
"_id.channel": channel_id,
"_id.user": {
"$in": user_ids
},
},
doc! {
"$push": {
"mentions": {
"$each": message_ids
}
}
},
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
}
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query!(
@@ -78,6 +78,38 @@ impl AbstractChannelUnreads for ReferenceDb {
Ok(())
}
/// Add a mention to multiple users.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
for user_id in user_ids {
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(unread) = unreads.get_mut(&key) {
unread.mentions.replace(message_ids.to_vec());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: None,
mentions: Some(message_ids.to_vec()),
},
);
}
}
Ok(())
}
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads
@@ -24,12 +24,9 @@ impl AbstractWebhooks for MongoDb {
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
Ok(self
.col::<Webhook>(COL)
.find(
doc! {
"channel_id": channel_id,
},
None,
)
.find(doc! {
"channel_id": channel_id,
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -100,6 +100,10 @@ auto_derived!(
)]
role_permissions: HashMap<String, OverrideField>,
/// Category the channel is in
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
@@ -131,6 +135,10 @@ auto_derived!(
)]
role_permissions: HashMap<String, OverrideField>,
/// Category the channel is in
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
@@ -161,6 +169,8 @@ auto_derived!(
pub default_permissions: Option<OverrideField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
}
/// Optional fields on channel object
@@ -219,6 +229,7 @@ impl Channel {
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
parent: None
},
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
id: id.clone(),
@@ -229,6 +240,7 @@ impl Channel {
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
parent: None
},
};
@@ -269,16 +281,24 @@ impl Channel {
}));
}
let id = ulid::Ulid::new().to_string();
let icon = if let Some(icon_id) = data.icon {
Some(File::use_channel_icon(db, &icon_id, &id, &owner_id).await?)
} else {
None
};
let recipients = data.users.into_iter().collect::<Vec<String>>();
let channel = Channel::Group {
id: ulid::Ulid::new().to_string(),
id,
name: data.name,
owner: owner_id,
description: data.description,
recipients: recipients.clone(),
icon: None,
icon,
last_message_id: None,
permissions: None,
@@ -429,6 +449,13 @@ impl Channel {
}
}
pub fn parent(&self) -> Option<&str> {
match self {
Channel::TextChannel { parent, .. } | Channel::VoiceChannel { parent, .. } => parent.as_deref(),
_ => None
}
}
/// Set role permission on a channel
pub async fn set_role_permission(
&mut self,
@@ -23,14 +23,11 @@ impl AbstractChannels for MongoDb {
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
Ok(self
.col::<Channel>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.find(doc! {
"_id": {
"$in": ids
}
})
.await
.map_err(|_| create_database_error!("fetch", "channels"))?
.filter_map(|s| async {
@@ -119,7 +116,6 @@ impl AbstractChannels for MongoDb {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
@@ -141,7 +137,6 @@ impl AbstractChannels for MongoDb {
"role_permissions.".to_owned() + role: permissions
}
},
None,
)
.await
.map(|_| ())
@@ -179,7 +174,6 @@ impl AbstractChannels for MongoDb {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
@@ -253,7 +247,6 @@ impl AbstractChannels for MongoDb {
"_id": server.id
},
update,
None,
)
.await
.map_err(|_| create_database_error!("update_one", "servers"))?;
@@ -274,23 +267,17 @@ impl MongoDb {
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
// Delete all invites to these channels.
self.col::<Document>("channel_invites")
.delete_many(
doc! {
"channel": &id
},
None,
)
.delete_many(doc! {
"channel": &id
})
.await
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
// Delete unread message objects on channels.
self.col::<Document>("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.delete_many(doc! {
"_id.channel": &id
})
.await
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
.map(|_| ())?;
@@ -299,12 +286,9 @@ impl MongoDb {
// Delete all webhooks on this channel.
self.col::<Document>("webhooks")
.delete_many(
doc! {
"channel": &id
},
None,
)
.delete_many(doc! {
"channel": &id
})
.await
.map_err(|_| create_database_error!("delete_many", "webhooks"))
.map(|_| ())
@@ -60,7 +60,6 @@ impl AbstractEmojis for MongoDb {
}
}
},
None,
)
.await
.map(|_| ())
@@ -42,7 +42,6 @@ impl AbstractAttachmentHashes for MongoDb {
"iv": nonce
}
},
None,
)
.await
.map(|_| ())
@@ -110,7 +110,6 @@ impl AbstractAttachments for MongoDb {
"uploader_id": uploader_id
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_one", COL))?;
@@ -130,7 +129,6 @@ impl AbstractAttachments for MongoDb {
"reported": true
}
},
None,
)
.await
.map(|_| ())
@@ -149,7 +147,6 @@ impl AbstractAttachments for MongoDb {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
@@ -170,7 +167,6 @@ impl AbstractAttachments for MongoDb {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
@@ -193,7 +189,6 @@ impl MongoDb {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
+208 -41
View File
@@ -5,17 +5,20 @@ use iso8601_timestamp::Timestamp;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
use revolt_result::{ErrorType, Result};
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::{bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey},
util::{
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
permissions::DatabasePermissionQuery,
},
Channel, Database, Emoji, File, User, AMQP,
};
@@ -53,6 +56,9 @@ auto_derived_partial!(
/// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
/// Array of role ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub role_mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
@@ -71,7 +77,7 @@ auto_derived_partial!(
/// Bitfield of message flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
pub flags: Option<u32>,
},
"PartialMessage"
);
@@ -199,6 +205,30 @@ auto_derived!(
}
);
pub struct MessageFlagsValue(pub u32);
impl MessageFlagsValue {
pub fn has(&self, flag: MessageFlags) -> bool {
self.has_value(flag as u32)
}
pub fn has_value(&self, bit: u32) -> bool {
let mask = 1 << bit;
self.0 & mask == mask
}
pub fn set(&mut self, flag: MessageFlags, toggle: bool) -> &mut Self {
self.set_value(flag as u32, toggle)
}
pub fn set_value(&mut self, bit: u32, toggle: bool) -> &mut Self {
if toggle {
self.0 |= 1 << bit;
} else {
self.0 &= !(1 << bit);
}
self
}
}
#[allow(clippy::derivable_impls)]
impl Default for Message {
fn default() -> Self {
@@ -214,6 +244,7 @@ impl Default for Message {
edited: None,
embeds: None,
mentions: None,
role_mentions: None,
replies: None,
reactions: Default::default(),
interactions: Default::default(),
@@ -262,13 +293,43 @@ impl Message {
return Err(create_error!(EmptyMessage));
}
// Ensure flags are either not set or have permissible values
if let Some(flags) = &data.flags {
if flags != &0 && flags != &1 {
let allow_mass_mentions = allow_mentions && config.features.mass_mentions_enabled;
let mut mentions_everyone = false;
let mut mentions_online = false;
let mut suppress_notifications = false;
if let Some(raw_flags) = &data.flags {
if raw_flags > &7 {
// quick path to failure: bigger than all the bits combined
return Err(create_error!(InvalidProperty));
}
// First step of mass mention resolution
let flags = MessageFlagsValue(*raw_flags);
suppress_notifications = flags.has(MessageFlags::SuppressNotifications);
mentions_everyone = allow_mentions && flags.has(MessageFlags::MentionsEveryone);
mentions_online = allow_mentions && flags.has(MessageFlags::MentionsOnline);
// Not a bot, and attempting to set mention flags
if user.as_ref().is_some_and(|u| u.bot.as_ref().is_none())
&& (mentions_everyone || mentions_online)
{
return Err(create_error!(IsNotBot));
}
if mentions_everyone && mentions_online {
return Err(create_error!(InvalidFlagValue));
}
}
let server_id = match channel {
Channel::TextChannel { ref server, .. } | Channel::VoiceChannel { ref server, .. } => {
Some(server.clone())
}
_ => None,
};
// Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
@@ -302,18 +363,76 @@ impl Message {
.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
flags: data.flags.map(|v| v as i32),
flags: data.flags,
..Default::default()
};
// Parse mentions in message.
let mut mentions = HashSet::new();
if allow_mentions {
if let Some(content) = &data.content {
for capture in RE_MENTION.captures_iter(content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
let mut message_mentions = if let Some(raw_content) = &data.content {
revolt_parser::parse_message(raw_content)
} else {
revolt_parser::MessageResults::default()
};
message_mentions.mentions_everyone |= mentions_everyone;
message_mentions.mentions_online |= mentions_online;
let revolt_parser::MessageResults {
mut user_mentions,
mut role_mentions,
mut mentions_everyone,
mut mentions_online
} = message_mentions;
if allow_mass_mentions && server_id.is_some() && !role_mentions.is_empty() {
let server_data = db
.fetch_server(server_id.unwrap().as_str())
.await
.expect("Failed to fetch server");
role_mentions.retain(|role_id| server_data.roles.contains_key(role_id));
}
// Validate the user can perform a mass mention
if !config.features.mass_mentions_enabled
&& (mentions_everyone || mentions_online || !role_mentions.is_empty())
{
mentions_everyone = false;
mentions_online = false;
role_mentions.clear();
} else if mentions_everyone || mentions_online || !role_mentions.is_empty() {
debug!(
"Mentioned everyone: {}, mentioned online: {}, mentioned roles: {:?}",
mentions_everyone, mentions_online, &role_mentions
);
if let Some(user) = match author {
MessageAuthor::User(user) => Some(Ok(user)),
MessageAuthor::System { .. } => Some(Err(())), // DISALLOWED
MessageAuthor::Webhook(..) => None, // Bypass check
} {
if user.is_err() {
return Err(create_error!(InvalidProperty));
}
let owned_user: User = user.unwrap().to_owned().into();
let mut query = DatabasePermissionQuery::new(db, &owned_user).channel(&channel);
let perms = calculate_channel_permissions(&mut query).await;
if (mentions_everyone || mentions_online)
&& !perms.has_channel_permission(ChannelPermission::MentionEveryone)
{
return Err(create_error!(MissingPermission {
permission: ChannelPermission::MentionEveryone.to_string()
}));
}
if !role_mentions.is_empty()
&& !perms.has_channel_permission(ChannelPermission::MentionRoles)
{
return Err(create_error!(MissingPermission {
permission: ChannelPermission::MentionRoles.to_string()
}));
}
}
}
@@ -327,38 +446,54 @@ impl Message {
}));
}
for ReplyIntent { id, mention } in entries {
let message = db.fetch_message(&id).await?;
for ReplyIntent {
id,
mention,
fail_if_not_exists,
} in entries
{
match db.fetch_message(&id).await {
// Referenced message exists
Ok(message) => {
if mention && allow_mentions {
user_mentions.insert(message.author.to_owned());
}
if mention && allow_mentions {
mentions.insert(message.author.to_owned());
replies.insert(message.id);
}
// If the referenced message doesn't exist and fail_if_not_exists
// is set to false, send the message without the reply.
Err(e) => {
if !matches!(e.error_type, ErrorType::NotFound)
|| fail_if_not_exists.unwrap_or(true)
{
return Err(e);
}
}
}
replies.insert(message.id);
}
}
// Validate the mentions go to users in the channel/server
if !mentions.is_empty() {
if !user_mentions.is_empty() {
match channel {
Channel::DirectMessage { ref recipients, .. }
| Channel::Group { ref recipients, .. } => {
let recipients_hash: HashSet<&String, RandomState> =
HashSet::from_iter(recipients);
mentions.retain(|m| recipients_hash.contains(m));
let recipients_hash = HashSet::<&String, RandomState>::from_iter(recipients);
user_mentions.retain(|m| recipients_hash.contains(m));
role_mentions.clear();
}
Channel::TextChannel { ref server, .. }
| Channel::VoiceChannel { ref server, .. } => {
let mentions_vec = Vec::from_iter(mentions.iter().cloned());
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
if let Ok(valid_members) = valid_members {
let valid_mentions: HashSet<&String, RandomState> =
HashSet::from_iter(valid_members.iter().map(|m| &m.id.user));
let valid_mentions = HashSet::<&String, RandomState>::from_iter(valid_members.iter().map(|m| &m.id.user));
mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
if !mentions.is_empty() {
if !user_mentions.is_empty() {
// if there are still mentions, drill down to a channel-level
let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server)
@@ -368,8 +503,7 @@ impl Message {
.members_can_see_channel()
.await;
mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
user_mentions.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
}
} else {
revolt_config::capture_error(&valid_members.unwrap_err());
@@ -377,13 +511,19 @@ impl Message {
}
}
Channel::SavedMessages { .. } => {
mentions.clear();
user_mentions.clear();
}
}
}
if !mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect());
if !user_mentions.is_empty() {
message.mentions.replace(user_mentions.into_iter().collect());
}
if !role_mentions.is_empty() {
message
.role_mentions
.replace(role_mentions.into_iter().collect());
}
if !replies.is_empty() {
@@ -392,6 +532,15 @@ impl Message {
.replace(replies.into_iter().collect::<Vec<String>>());
}
// Calculate final message flags
let mut flag_value = MessageFlagsValue(0);
flag_value
.set(MessageFlags::SuppressNotifications, suppress_notifications)
.set(MessageFlags::MentionsEveryone, mentions_everyone)
.set(MessageFlags::MentionsOnline, mentions_online);
message.flags = Some(flag_value.0);
// Add attachments to message.
let mut attachments = vec![];
if data
@@ -470,7 +619,12 @@ impl Message {
tasks::ack::queue_message(
self.channel.to_string(),
AckEvent::ProcessMessage {
messages: vec![(None, self.clone(), mentions.clone(), true)],
messages: vec![(
None,
self.clone(),
mentions.clone(),
self.has_suppressed_notifications(),
)],
},
)
.await;
@@ -514,7 +668,9 @@ impl Message {
)
.await?;
if !self.has_suppressed_notifications() {
if !self.has_suppressed_notifications()
&& (self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications
tasks::ack::queue_message(
self.channel.to_string(),
@@ -537,7 +693,7 @@ impl Message {
}
_ => vec![],
},
self.has_suppressed_notifications(),
false, // branch already dictates this
)],
},
)
@@ -574,13 +730,24 @@ impl Message {
/// Whether this message has suppressed notifications
pub fn has_suppressed_notifications(&self) -> bool {
if let Some(flags) = self.flags {
flags & MessageFlags::SuppressNotifications as i32
== MessageFlags::SuppressNotifications as i32
flags & MessageFlags::SuppressNotifications as u32
== MessageFlags::SuppressNotifications as u32
} else {
false
}
}
pub fn contains_mass_push_mention(&self) -> bool {
let ping = if let Some(flags) = self.flags {
let flags = MessageFlagsValue(flags);
flags.has(MessageFlags::MentionsEveryone)
} else {
false
};
ping || self.role_mentions.is_some()
}
/// Update message data
pub async fn update(
&mut self,
@@ -228,7 +228,6 @@ impl AbstractMessages for MongoDb {
"_id": id
},
query,
None,
)
.await
.map(|_| ())
@@ -247,7 +246,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
@@ -266,7 +264,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
@@ -285,7 +282,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): 1
}
},
None,
)
.await
.map(|_| ())
@@ -300,15 +296,12 @@ impl AbstractMessages for MongoDb {
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.delete_many(
doc! {
"channel": channel,
"_id": {
"$in": ids
}
},
None,
)
.delete_many(doc! {
"channel": channel,
"_id": {
"$in": ids
}
})
.await
.map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL))
@@ -362,7 +355,6 @@ impl MongoDb {
"deleted": true
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "attachments"))?;
@@ -370,7 +362,7 @@ impl MongoDb {
// And then delete said messages.
self.col::<Document>(COL)
.delete_many(projection, None)
.delete_many(projection)
.await
.map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL))
@@ -23,16 +23,13 @@ impl AbstractRatelimitEvents for MongoDb {
count: usize,
) -> Result<bool> {
self.col::<RatelimitEvent>(COL)
.count_documents(
doc! {
"_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
"target_id": target_id,
"event_type": event_type.to_string()
.count_documents(doc! {
"_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
None,
)
"target_id": target_id,
"event_type": event_type.to_string()
})
.await
.map(|c| c as usize >= count)
.map_err(|_| create_database_error!("count_documents", COL))
@@ -1,3 +1,4 @@
use ::mongodb::SessionCursor;
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
@@ -5,6 +6,66 @@ use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
mod mongodb;
mod reference;
#[derive(Debug)]
pub enum ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
MongoDb {
session: ::mongodb::ClientSession,
cursor: Option<SessionCursor<Member>>,
},
Reference {
offset: i32,
data: Option<Vec<Member>>,
},
}
impl ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
ChunkedServerMembersGenerator::MongoDb {
session,
cursor: Some(cursor),
}
}
pub fn new_reference(data: Vec<Member>) -> Self {
ChunkedServerMembersGenerator::Reference {
offset: 0,
data: Some(data),
}
}
pub async fn next(&mut self) -> Option<Member> {
match self {
#[cfg(feature = "mongodb")]
ChunkedServerMembersGenerator::MongoDb { session, cursor } => {
if let Some(cursor) = cursor {
let value = cursor.next(session).await;
value.map(|val| val.expect("Failed to fetch the next member"))
} else {
warn!("Attempted to access a (MongoDb) server member generator without first setting a cursor");
None
}
}
ChunkedServerMembersGenerator::Reference { offset, data } => {
if let Some(data) = data {
if data.len() as i32 >= *offset {
None
} else {
let resp = &data[*offset as usize];
*offset += 1;
Some(resp.clone())
}
} else {
warn!("Attempted to access a (Reference) server member generator without first providing data");
None
}
}
}
}
}
#[async_trait]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
@@ -16,6 +77,24 @@ pub trait AbstractServerMembers: Sync + Send {
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator>;
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>>;
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator>;
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
@@ -1,10 +1,11 @@
use futures::StreamExt;
use mongodb::options::ReadConcern;
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractServerMembers;
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
static COL: &str = "server_members";
@@ -33,12 +34,9 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.server": server_id
},
None,
)
.find(doc! {
"_id.server": server_id
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -52,16 +50,101 @@ impl AbstractServerMembers for MongoDb {
.await)
}
/// Fetch all members in a server as a generator.
/// Uses config key pushd.mass_mention_chunk_size as the batch size.
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator> {
let config = revolt_config::config().await;
let mut session = self
.start_session()
.await
.map_err(|_| create_database_error!("start_session", COL))?;
session
.start_transaction()
.read_concern(ReadConcern::snapshot())
.await
.map_err(|_| create_database_error!("start_transaction", COL))?;
let cursor = self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id
})
.session(&mut session)
.batch_size(config.pushd.mass_mention_chunk_size as u32)
.await
.map_err(|_| create_database_error!("find", COL))?;
Ok(ChunkedServerMembersGenerator::new_mongo(session, cursor))
}
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"roles": {"$in": roles}
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator> {
let config = revolt_config::config().await;
let mut session = self
.start_session()
.await
.map_err(|_| create_database_error!("start_session", COL))?;
session
.start_transaction()
.read_concern(ReadConcern::snapshot())
.await
.map_err(|_| create_database_error!("start_transaction", COL))?;
let cursor = self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"roles": {"$in": roles}
})
.session(&mut session)
.batch_size(config.pushd.mass_mention_chunk_size as u32)
.await
.map_err(|_| create_database_error!("find", COL))?;
return Ok(ChunkedServerMembersGenerator::new_mongo(session, cursor));
}
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.user": user_id
},
None,
)
.find(doc! {
"_id.user": user_id
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -79,15 +162,12 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.server": server_id,
"_id.user": {
"$in": ids
}
},
None,
)
.find(doc! {
"_id.server": server_id,
"_id.user": {
"$in": ids
}
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -104,12 +184,9 @@ impl AbstractServerMembers for MongoDb {
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(
doc! {
"_id.server": server_id
},
None,
)
.count_documents(doc! {
"_id.server": server_id
})
.await
.map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL))
@@ -118,12 +195,9 @@ impl AbstractServerMembers for MongoDb {
/// Fetch server count of a user
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(
doc! {
"_id.user": user_id
},
None,
)
.count_documents(doc! {
"_id.user": user_id
})
.await
.map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL))
@@ -3,7 +3,7 @@ use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use super::AbstractServerMembers;
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
#[async_trait]
impl AbstractServerMembers for ReferenceDb {
@@ -40,6 +40,70 @@ impl AbstractServerMembers for ReferenceDb {
.collect())
}
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator> {
let server_members = self.server_members.lock().await;
let members = server_members
.clone()
.into_values()
.filter(move |member| member.id.server == server_id)
.collect();
// this is inefficient as shit but its the reference db so its fine
Ok(ChunkedServerMembersGenerator::new_reference(members))
}
/// Fetch all members that have any of the roles given
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.clone()
.into_values()
.filter(|member| {
member.id.server == server_id
&& !member
.roles
.iter()
.filter(|p| roles.contains(*p))
.collect::<Vec<&String>>()
.is_empty()
})
.collect())
}
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator> {
let server_members = self.server_members.lock().await;
let resp = server_members
.clone()
.into_values()
.filter(|member| {
member.id.server == server_id
&& !member
.roles
.iter()
.filter(|p| roles.contains(*p))
.collect::<Vec<&String>>()
.is_empty()
})
.collect();
return Ok(ChunkedServerMembersGenerator::new_reference(resp));
}
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
@@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet};
use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_models::v0::{self, DataCreateCategory, DataCreateServerChannel, DataEditCategory};
use revolt_permissions::{OverrideField, DEFAULT_PERMISSION_SERVER};
use revolt_result::Result;
use ulid::Ulid;
@@ -26,8 +26,8 @@ auto_derived_partial!(
// TODO: investigate if this is redundant and can be removed
pub channels: Vec<String>,
/// Categories for this server
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<Category>>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub categories: HashMap<String, Category>,
/// Configuration for sending system event messages
#[serde(skip_serializing_if = "Option::is_none")]
pub system_messages: Option<SystemMessageChannels>,
@@ -87,17 +87,29 @@ auto_derived_partial!(
"PartialRole"
);
auto_derived!(
auto_derived_partial!(
/// Channel category
pub struct Category {
/// Unique ID for this category
pub id: String,
/// Title for this category
pub title: String,
/// Default permissions assigned to users in this category
#[serde(skip_serializing_if = "Option::is_none")]
pub default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this category
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
pub role_permissions: HashMap<String, OverrideField>,
/// Channels in this category
pub channels: Vec<String>,
}
},
"PartialCategory"
);
auto_derived!(
/// System message channel assignments
pub struct SystemMessageChannels {
/// ID of channel to send user join messages in
@@ -127,6 +139,10 @@ auto_derived!(
pub enum FieldsRole {
Colour,
}
pub enum FieldsCategory {
DefaultPermissions
}
);
#[allow(clippy::disallowed_methods)]
@@ -149,7 +165,7 @@ impl Server {
analytics: false,
banner: None,
categories: None,
categories: HashMap::new(),
discoverable: false,
flags: None,
icon: None,
@@ -221,7 +237,7 @@ impl Server {
pub fn remove_field(&mut self, field: &FieldsServer) {
match field {
FieldsServer::Description => self.description = None,
FieldsServer::Categories => self.categories = None,
FieldsServer::Categories => self.categories.clear(),
FieldsServer::SystemMessages => self.system_messages = None,
FieldsServer::Icon => self.icon = None,
FieldsServer::Banner => self.banner = None,
@@ -334,6 +350,110 @@ impl Role {
}
}
impl Category {
pub fn remove_field(&mut self, field: FieldsCategory) {
match field {
FieldsCategory::DefaultPermissions => self.default_permissions = None
};
}
pub async fn create(db: &Database, server: &mut Server, data: DataCreateCategory) -> Result<Category> {
let channels = data.channels.clone().unwrap_or_default()
.into_iter()
.filter(|c| server.channels.contains(&c))
.collect();
let category = Category {
id: Ulid::new().to_string(),
title: data.title,
channels: channels,
default_permissions: None,
role_permissions: HashMap::new()
};
server.categories.insert(category.id.clone(), category.clone());
let partial_server = PartialServer { categories: Some(server.categories.clone()), ..Default::default() };
db.update_server(&server.id, &partial_server, Vec::new()).await?;
EventV1::ServerUpdate {
id: server.id.clone(),
data: partial_server.into(),
clear: Vec::new(),
}
.p(server.id.clone())
.await;
Ok(category)
}
pub async fn delete(&self, db: &Database, server: &mut Server) -> Result<()> {
// update the parent server model with the new category
server.categories.remove(&self.id);
let partial_server = PartialServer { categories: Some(server.categories.clone()), ..Default::default() };
db.update_server(&server.id, &partial_server, Vec::new()).await?;
EventV1::ServerUpdate {
id: server.id.clone(),
data: partial_server.into(),
clear: Vec::new(),
}
.p(server.id.clone())
.await;
Ok(())
}
pub async fn update(&mut self, db: &Database, server: &mut Server, partial: PartialCategory, remove: Vec<FieldsCategory>) -> Result<()> {
for field in remove {
self.remove_field(field);
};
self.apply_options(partial);
// update the parent server model with the new category
server.categories.insert(self.id.clone(), self.clone());
let partial_server = PartialServer { categories: Some(server.categories.clone()), ..Default::default() };
db.update_server(&server.id, &partial_server, Vec::new()).await?;
EventV1::ServerUpdate {
id: server.id.clone(),
data: partial_server.into(),
clear: Vec::new(),
}
.p(server.id.clone())
.await;
Ok(())
}
pub async fn set_role_permission(&mut self, db: &Database, server: &mut Server, role_id: String, role_override: OverrideField) -> Result<()> {
self.role_permissions.insert(role_id, role_override);
// update the parent server model with the new category
server.categories.insert(self.id.clone(), self.clone());
let partial_server = PartialServer { categories: Some(server.categories.clone()), ..Default::default() };
db.update_server(&server.id, &partial_server, Vec::new()).await?;
EventV1::ServerUpdate {
id: server.id.clone(),
data: partial_server.into(),
clear: Vec::new(),
}
.p(server.id.clone())
.await;
Ok(())
}
}
impl SystemMessageChannels {
pub fn into_channel_ids(self) -> HashSet<String> {
let mut ids = HashSet::new();
@@ -25,14 +25,11 @@ impl AbstractServers for MongoDb {
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
Ok(self
.col::<Server>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.find(doc! {
"_id": {
"$in": ids
}
})
.await
.map_err(|_| create_database_error!("find", "servers"))?
.filter_map(|s| async {
@@ -84,7 +81,6 @@ impl AbstractServers for MongoDb {
.map_err(|_| create_database_error!("to_document", "role"))?
}
},
None,
)
.await
.map(|_| ())
@@ -125,7 +121,6 @@ impl AbstractServers for MongoDb {
"roles": &role_id
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "server_members"))?;
@@ -140,7 +135,6 @@ impl AbstractServers for MongoDb {
"role_permissions.".to_owned() + role_id: 1_i32
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_one", "channels"))?;
@@ -155,7 +149,6 @@ impl AbstractServers for MongoDb {
"roles.".to_owned() + role_id: 1_i32
}
},
None,
)
.await
.map(|_| ())
@@ -188,12 +181,9 @@ impl MongoDb {
// Find all channels
let channels: Vec<String> = self
.col::<Document>("channels")
.find(
doc! {
"server": server_id
},
None,
)
.find(doc! {
"server": server_id
})
.await
.map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async {
@@ -225,19 +215,15 @@ impl MongoDb {
}
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "emojis"))?;
// Delete all channels.
self.col::<Document>("channels")
.delete_many(
doc! {
"server": &server_id
},
None,
)
.delete_many(doc! {
"server": &server_id
})
.await
.map_err(|_| create_database_error!("delete_many", "channels"))?;
@@ -248,12 +234,9 @@ impl MongoDb {
// Delete members and bans.
for with in &["server_members", "server_bans"] {
self.col::<Document>(with)
.delete_many(
doc! {
"_id.server": &server_id
},
None,
)
.delete_many(doc! {
"_id.server": &server_id
})
.await
.map_err(|_| create_database_error!("delete_many", with))?;
}
@@ -53,8 +53,8 @@ impl AbstractUserSettings for MongoDb {
doc! {
"$set": set
},
UpdateOptions::builder().upsert(true).build(),
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "user_settings"))
+21 -5
View File
@@ -3,11 +3,12 @@ use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP};
use authifier::config::{EmailVerificationConfig, Template};
use futures::future::join_all;
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{self, UserFlags};
use revolt_models::v0::{self, UserFlags, UserBadges};
use revolt_presence::filter_online;
use revolt_result::{create_error, Result};
use serde_json::json;
@@ -348,15 +349,16 @@ impl User {
) -> Result<Vec<v0::User>> {
let online_ids = filter_online(ids).await;
Ok(db
Ok(join_all(
db
.fetch_users(ids)
.await?
.into_iter()
.map(|user| {
.map(|user| async {
let is_online = online_ids.contains(&user.id);
user.into_known(perspective, is_online)
user.into_known(perspective, is_online).await
})
.collect())
).await)
}
/// Find a free discriminator for a given username
@@ -807,4 +809,18 @@ impl User {
)
.await
}
/// Gets the user's badges along with calculating any dynamic badges
pub async fn get_badges(&self) -> u32 {
let config = config().await;
let badges = self.badges.unwrap_or_default() as u32;
if let Some(cutoff) = config.api.users.early_adopter_cutoff {
if Ulid::from_string(&self.id).unwrap().timestamp_ms() < cutoff {
return badges + UserBadges::EarlyAdopter as u32
};
};
badges
}
}
@@ -49,12 +49,9 @@ impl AbstractUsers for MongoDb {
/// Fetch a session from the database by token
async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
self.col::<Session>("sessions")
.find_one(
doc! {
"token": token
},
None,
)
.find_one(doc! {
"token": token
})
.await
.map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession))
@@ -64,14 +61,11 @@ impl AbstractUsers for MongoDb {
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
Ok(self
.col::<User>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.find(doc! {
"_id": {
"$in": ids
}
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -94,10 +88,10 @@ impl AbstractUsers for MongoDb {
Ok(self
.col::<UserDocument>(COL)
.find(
doc! {
"username": username
},
.find(doc! {
"username": username
})
.with_options(
FindOptions::builder()
.collation(
Collation::builder()
@@ -122,15 +116,13 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>(COL)
.find(
doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.find(doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
]
})
.with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { s.ok() })
@@ -143,17 +135,15 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>("channels")
.find(
doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ user_a, user_b ]
}
.find(doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
"recipients": {
"$all": [ user_a, user_b ]
}
})
.with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
.await
.map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async { s.ok() })
@@ -166,49 +156,46 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>("server_members")
.aggregate(
vec![
doc! {
"$match": {
"_id.user": user_a
}
},
doc! {
"$lookup": {
"from": "server_members",
"as": "members",
"let": {
"server": "$_id.server"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{ "$eq": [ "$_id.user", user_b ] },
{ "$eq": [ "$_id.server", "$$server" ] }
]
}
.aggregate(vec![
doc! {
"$match": {
"_id.user": user_a
}
},
doc! {
"$lookup": {
"from": "server_members",
"as": "members",
"let": {
"server": "$_id.server"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{ "$eq": [ "$_id.user", user_b ] },
{ "$eq": [ "$_id.server", "$$server" ] }
]
}
}
]
}
},
doc! {
"$match": {
"members": {
"$size": 1_i32
}
]
}
},
doc! {
"$match": {
"members": {
"$size": 1_i32
}
},
doc! {
"$project": {
"_id": "$_id.server"
}
},
],
None,
)
}
},
doc! {
"$project": {
"_id": "$_id.server"
}
},
])
.await
.map_err(|_| create_database_error!("aggregate", "server_members"))?
.filter_map(|s| async { s.ok() })
@@ -284,7 +271,6 @@ impl AbstractUsers for MongoDb {
}
}
}],
None,
)
.await
.map(|_| ())
@@ -305,7 +291,6 @@ impl AbstractUsers for MongoDb {
}
}
},
None,
)
.await
.map(|_| ())
@@ -329,7 +314,6 @@ impl AbstractUsers for MongoDb {
"subscription": 1
}
},
None,
)
.await
.map(|_| ())
+55 -9
View File
@@ -14,6 +14,7 @@ use validator::HasLen;
use revolt_result::Result;
use super::DelayedTask;
use crate::Channel::{TextChannel, VoiceChannel};
/// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)]
@@ -120,17 +121,22 @@ pub async fn handle_ack_event(
);
// find all the users we'll be notifying
messages
.iter()
.for_each(|(_, _, recipents, _)| users.extend(recipents.iter()));
messages.iter().for_each(|(_, _, recipents, _)| {
users.extend(recipents.iter());
});
debug!("Found {} users to notify.", users.len());
for user in users {
let message_ids: Vec<String> = messages
.iter()
.filter(|(_, _, recipients, _)| recipients.contains(user))
.map(|(_, message, _, _)| message.id.clone())
.filter_map(|(_, message, recipients, _)| {
if recipients.contains(user) {
Some(message.id.clone())
} else {
None
}
})
.collect();
if !message_ids.is_empty() {
@@ -140,13 +146,18 @@ pub async fn handle_ack_event(
debug!("Added {} mentions for user {}", message_ids.len(), &user);
}
for (push, _, recipients, silenced) in messages {
if *silenced || recipients.is_empty() || push.is_none() {
let mut mass_mentions = vec![];
for (push, message, recipients, silenced) in messages {
if *silenced
|| push.is_none()
|| (recipients.is_empty() && !message.contains_mass_push_mention())
{
debug!(
"Rejecting push: silenced: {}, recipient count: {}, push exists: {:?}",
*silenced,
recipients.length(),
push
push.is_some()
);
continue;
}
@@ -162,6 +173,35 @@ pub async fn handle_ack_event(
{
revolt_config::capture_error(&err);
}
if message.contains_mass_push_mention() {
mass_mentions.push(push.clone().unwrap());
}
}
if !mass_mentions.is_empty() {
debug!(
"Sending mass mention push event to AMQP; channel {}",
&mass_mentions[0].message.channel
);
let channel = db
.fetch_channel(&mass_mentions[0].message.channel)
.await
.expect("Failed to fetch channel from db");
match channel {
TextChannel { server, .. } | VoiceChannel { server, .. } => {
if let Err(err) =
amqp.mass_mention_message_sent(server, mass_mentions).await
{
revolt_config::capture_error(&err);
}
}
_ => {
panic!("Unknown channel type when sending mass mention event");
}
}
}
}
};
@@ -192,7 +232,7 @@ pub async fn worker(db: Database, amqp: AMQP) {
revolt_config::capture_error(&err);
error!("{err:?} for {event:?}. ({user:?}, {channel})");
} else {
info!("User {user:?} ack in {channel} with {event:?}");
debug!("User {user:?} ack in {channel} with {event:?}");
}
}
}
@@ -224,6 +264,12 @@ pub async fn worker(db: Database, amqp: AMQP) {
// add the new message to the list of messages to be processed.
existing.append(new_data);
// if the message contains a mass mention, do not delay it any further.
if new_data[0].1.contains_mass_push_mention() {
task.run_immediately();
continue;
}
// put a cap on the amount of messages that can be queued, for particularly active channels
if (existing.length() as u16)
< revolt_config::config()
+9 -1
View File
@@ -26,6 +26,7 @@ pub fn start_workers(db: Database, amqp: AMQP) {
/// Task with additional information on when it should run
pub struct DelayedTask<T> {
pub data: T,
run_now: bool,
last_updated: Instant,
first_seen: Instant,
}
@@ -41,6 +42,7 @@ impl<T> DelayedTask<T> {
pub fn new(data: T) -> Self {
DelayedTask {
data,
run_now: false,
last_updated: Instant::now(),
first_seen: Instant::now(),
}
@@ -51,9 +53,15 @@ impl<T> DelayedTask<T> {
self.last_updated = Instant::now()
}
/// Flag the task to run right away, regardless of the time
pub fn run_immediately(&mut self) {
self.run_now = true
}
/// Check if a task should run yet
pub fn should_run(&self) -> bool {
self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT
self.run_now
|| self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT
|| self.last_updated.elapsed().as_secs() > SAVE_CONSTANT
}
}
+72 -28
View File
@@ -187,6 +187,7 @@ impl From<crate::Channel> for Channel {
last_message_id,
default_permissions,
role_permissions,
parent,
nsfw,
} => Channel::TextChannel {
id,
@@ -197,6 +198,7 @@ impl From<crate::Channel> for Channel {
last_message_id,
default_permissions,
role_permissions,
parent,
nsfw,
},
crate::Channel::VoiceChannel {
@@ -207,6 +209,7 @@ impl From<crate::Channel> for Channel {
icon,
default_permissions,
role_permissions,
parent,
nsfw,
} => Channel::VoiceChannel {
id,
@@ -216,6 +219,7 @@ impl From<crate::Channel> for Channel {
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
parent,
nsfw,
},
}
@@ -267,6 +271,7 @@ impl From<Channel> for crate::Channel {
last_message_id,
default_permissions,
role_permissions,
parent,
nsfw,
} => crate::Channel::TextChannel {
id,
@@ -277,6 +282,7 @@ impl From<Channel> for crate::Channel {
last_message_id,
default_permissions,
role_permissions,
parent,
nsfw,
},
Channel::VoiceChannel {
@@ -287,6 +293,7 @@ impl From<Channel> for crate::Channel {
icon,
default_permissions,
role_permissions,
parent,
nsfw,
} => crate::Channel::VoiceChannel {
id,
@@ -296,6 +303,7 @@ impl From<Channel> for crate::Channel {
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
parent,
nsfw,
},
}
@@ -315,6 +323,7 @@ impl From<crate::PartialChannel> for PartialChannel {
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
parent: value.parent,
}
}
}
@@ -332,6 +341,7 @@ impl From<PartialChannel> for crate::PartialChannel {
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
parent: value.parent,
}
}
}
@@ -483,6 +493,7 @@ impl crate::Message {
edited: self.edited,
embeds: self.embeds,
mentions: self.mentions,
role_mentions: self.role_mentions,
replies: self.replies,
reactions: self.reactions,
interactions: self.interactions.into(),
@@ -511,6 +522,7 @@ impl From<crate::PartialMessage> for PartialMessage {
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
role_mentions: value.role_mentions,
replies: value.replies,
reactions: value.reactions,
interactions: value.interactions.map(Into::into),
@@ -727,9 +739,7 @@ impl From<crate::Server> for Server {
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
categories: value.categories.into_iter().map(|(k, v)| (k, v.into())).collect(),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
@@ -755,9 +765,7 @@ impl From<Server> for crate::Server {
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
categories: value.categories.into_iter().map(|(k, v)| (k, v.into())).collect(),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
@@ -783,9 +791,7 @@ impl From<crate::PartialServer> for PartialServer {
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
categories: value.categories.map(|x| x.into_iter().map(|(k, v)| (k, v.into())).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
@@ -809,9 +815,7 @@ impl From<PartialServer> for crate::PartialServer {
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
categories: value.categories.map(|x| x.into_iter().map(|(k, v)| (k, v.into())).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
@@ -857,6 +861,8 @@ impl From<crate::Category> for Category {
id: value.id,
title: value.title,
channels: value.channels,
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
}
}
}
@@ -867,6 +873,24 @@ impl From<Category> for crate::Category {
id: value.id,
title: value.title,
channels: value.channels,
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
}
}
}
impl From<crate::FieldsCategory> for FieldsCategory {
fn from(value: crate::FieldsCategory) -> Self {
match value {
crate::FieldsCategory::DefaultPermissions => FieldsCategory::DefaultPermissions,
}
}
}
impl From<FieldsCategory> for crate::FieldsCategory {
fn from(value: FieldsCategory) -> crate::FieldsCategory {
match value {
FieldsCategory::DefaultPermissions => crate::FieldsCategory::DefaultPermissions,
}
}
}
@@ -992,6 +1016,8 @@ impl crate::User {
(RelationshipStatus::None, false)
};
let badges = self.get_badges().await;
User {
username: self.username,
discriminator: self.discriminator,
@@ -1010,7 +1036,7 @@ impl crate::User {
} else {
vec![]
},
badges: self.badges.unwrap_or_default() as u32,
badges,
online: can_see_profile
&& revolt_presence::is_online(&self.id).await
&& !matches!(
@@ -1021,7 +1047,7 @@ impl crate::User {
})
),
status: if can_see_profile {
self.status.map(|status| status.into())
self.status.and_then(|status| status.into(true))
} else {
None
},
@@ -1036,7 +1062,7 @@ impl crate::User {
/// Convert user object into user model assuming mutual connection
///
/// Relations will never be included, i.e. when we process ourselves
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
pub async fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
where
P: Into<Option<&'a crate::User>>,
{
@@ -1066,13 +1092,15 @@ impl crate::User {
(RelationshipStatus::None, false)
};
let badges = self.get_badges().await;
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
badges,
online: can_see_profile
&& is_online
&& !matches!(
@@ -1083,7 +1111,7 @@ impl crate::User {
})
),
status: if can_see_profile {
self.status.map(|status| status.into())
self.status.and_then(|status| status.into(true))
} else {
None
},
@@ -1096,14 +1124,16 @@ impl crate::User {
}
/// Convert user object into user model without presence information
pub fn into_known_static<'a>(self, is_online: bool) -> User {
pub async fn into_known_static<'a>(self, is_online: bool) -> User {
let badges = self.get_badges().await;
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
badges,
online: is_online
&& !matches!(
self.status,
@@ -1112,7 +1142,7 @@ impl crate::User {
..
})
),
status: self.status.map(|status| status.into()),
status: self.status.and_then(|status| status.into(true)),
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
@@ -1122,6 +1152,8 @@ impl crate::User {
}
pub async fn into_self(self, force_online: bool) -> User {
let badges = self.get_badges().await;
User {
username: self.username,
discriminator: self.discriminator,
@@ -1136,7 +1168,7 @@ impl crate::User {
.collect()
})
.unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32,
badges,
online: (force_online || revolt_presence::is_online(&self.id).await)
&& !matches!(
self.status,
@@ -1145,7 +1177,7 @@ impl crate::User {
..
})
),
status: self.status.map(|status| status.into()),
status: self.status.and_then(|status| status.into(true)),
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
@@ -1196,7 +1228,7 @@ impl From<crate::PartialUser> for PartialUser {
.collect()
}),
badges: value.badges.map(|badges| badges as u32),
status: value.status.map(|status| status.into()),
status: value.status.and_then(|status| status.into(false)),
flags: value.flags.map(|flags| flags as u32),
privileged: value.privileged,
bot: value.bot.map(|bot| bot.into()),
@@ -1285,11 +1317,23 @@ impl From<Presence> for crate::Presence {
}
}
impl From<crate::UserStatus> for UserStatus {
fn from(value: crate::UserStatus) -> Self {
UserStatus {
text: value.text,
presence: value.presence.map(|presence| presence.into()),
impl crate::UserStatus {
fn into(self, discard_invisible: bool) -> Option<UserStatus> {
let status = UserStatus {
text: self.text,
presence: self.presence.and_then(|presence| {
if discard_invisible && presence == crate::Presence::Invisible {
None
} else {
Some(presence.into())
}
}),
};
if status.text.is_none() && status.presence.is_none() {
None
} else {
Some(status)
}
}
}
@@ -5,7 +5,7 @@ use revolt_permissions::{
DEFAULT_PERMISSION_DIRECT_MESSAGE,
};
use crate::{Channel, Database, Member, Server, User};
use crate::{Category, Channel, Database, Member, Server, User};
#[derive(Clone)]
pub struct BulkDatabasePermissionQuery<'a> {
@@ -94,9 +94,28 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
}
}
pub async fn from_channel_id(self, channel_id: String) -> BulkDatabasePermissionQuery<'z> {
let channel = self
.database
.fetch_channel(channel_id.as_str())
.await
.expect("Valid channel id");
drop(channel_id);
BulkDatabasePermissionQuery {
channel: Some(channel),
..self
}
}
pub fn members(self, members: &'z [Member]) -> BulkDatabasePermissionQuery {
BulkDatabasePermissionQuery {
members: Some(members.to_owned()),
cached_member_perms: None,
users: None,
cached_members: None,
cached_users: None,
..self
}
}
@@ -104,6 +123,10 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
pub fn users(self, users: &'z [User]) -> BulkDatabasePermissionQuery {
BulkDatabasePermissionQuery {
users: Some(users.to_owned()),
cached_member_perms: None,
members: None,
cached_members: None,
cached_users: None,
..self
}
}
@@ -174,7 +197,7 @@ async fn calculate_members_permissions<'a>(
) -> HashMap<String, PermissionValue> {
let mut resp = HashMap::new();
let (_, channel_role_permissions, channel_default_permissions) = match query
let (_, channel_role_permissions, channel_default_permissions, parent) = match query
.channel
.as_ref()
.expect("A channel must be assigned to calculate channel permissions")
@@ -184,14 +207,16 @@ async fn calculate_members_permissions<'a>(
id,
role_permissions,
default_permissions,
parent,
..
}
| Channel::VoiceChannel {
id,
role_permissions,
default_permissions,
parent,
..
} => (id, role_permissions, default_permissions),
} => (id, role_permissions, default_permissions, parent),
_ => panic!("Calculation of member permissions must be done on a server channel"),
};
@@ -245,6 +270,8 @@ async fn calculate_members_permissions<'a>(
.map(|m| (&m.id.user, m)),
);
let category = parent.map(|parent| query.server.categories.get(&parent).expect("Channel parent doesnt exist"));
for user in users {
let member = members.get(&user.id);
@@ -275,11 +302,39 @@ async fn calculate_members_permissions<'a>(
// Get the user's server permissions
let mut permission = calculate_server_permissions(&query.server, user, member);
// Apply category overrides
if let Some(category) = category {
if let Some(defaults) = category.default_permissions {
permission.apply(defaults.into());
};
// Get the applicable role overrides for the category
let mut roles = category.role_permissions
.iter()
.filter(|(id, _)| member.roles.contains(id))
.filter_map(|(id, permission)| {
query.server.roles.get(id).map(|role| {
let v: Override = (*permission).into();
(role.rank, v)
})
})
.collect::<Vec<(i64, Override)>>();
roles.sort_by(|a, b| b.0.cmp(&a.0));
let overrides = roles.into_iter().map(|(_, v)| v);
for role_override in overrides {
permission.apply(role_override)
}
};
// apply channel overrides
if let Some(defaults) = channel_default_permissions {
permission.apply(defaults.into());
}
// Get the applicable role overrides
// Get the applicable role overrides for the channel
let mut roles = channel_role_permissions
.iter()
.filter(|(id, _)| member.roles.contains(id))
+52 -1
View File
@@ -5,7 +5,7 @@ use revolt_permissions::{
RelationshipStatus, DEFAULT_PERMISSION_DIRECT_MESSAGE,
};
use crate::{Channel, Database, Member, Server, User};
use crate::{Category, Channel, Database, Member, Server, User};
/// Permissions calculator
#[derive(Clone)]
@@ -16,6 +16,7 @@ pub struct DatabasePermissionQuery<'a> {
perspective: &'a User,
user: Option<Cow<'a, User>>,
channel: Option<Cow<'a, Channel>>,
category: Option<Cow<'a, Category>>,
server: Option<Cow<'a, Server>>,
member: Option<Cow<'a, Member>>,
@@ -288,6 +289,40 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
}
}
async fn get_default_category_permissions(&mut self) -> Override {
if let Some(category) = &self.category {
category.default_permissions.unwrap_or_default().into()
} else {
Default::default()
}
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
if let Some((server, category)) = self.server.as_ref().zip(self.category.as_ref()) {
let member_roles = self
.member
.as_ref()
.map(|member| member.roles.clone())
.unwrap_or_default();
let mut roles = category.role_permissions
.iter()
.filter(|(id, _)| member_roles.contains(id))
.filter_map(|(id, permission)| {
server.roles.get(id).map(|role| {
let v: Override = (*permission).into();
(role.rank, v)
})
})
.collect::<Vec<(i64, Override)>>();
roles.sort_by(|a, b| b.0.cmp(&a.0));
roles.into_iter().map(|(_, v)| v).collect()
} else {
vec![]
}
}
/// Do we own this group or saved messages channel if it is one of those?
async fn do_we_own_the_channel(&mut self) -> bool {
if let Some(channel) = &self.channel {
@@ -375,6 +410,14 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
}
}
}
async fn set_category_from_channel(&mut self) {
if let Some((server, channel)) = self.server.as_ref().zip(self.channel.as_ref()) {
let category = channel.parent().and_then(|id| server.categories.get(id)).cloned();
self.category = category.map(Cow::Owned);
}
}
}
impl<'a> DatabasePermissionQuery<'a> {
@@ -385,6 +428,7 @@ impl<'a> DatabasePermissionQuery<'a> {
perspective,
user: None,
channel: None,
category: None,
server: None,
member: None,
@@ -443,6 +487,13 @@ impl<'a> DatabasePermissionQuery<'a> {
}
}
pub fn category(self, category: &'a Category) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery {
category: Some(Cow::Borrowed(category)),
..self
}
}
/// Use member
pub fn member(self, member: &'a Member) -> DatabasePermissionQuery {
DatabasePermissionQuery {
+1 -1
View File
@@ -10,7 +10,7 @@ use schemars::{
};
use crate::{
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
Bot, Category, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook
};
/// Reference to some object in the database
+74 -453
View File
@@ -1,464 +1,93 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type="text/css">
body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-3:not(table),
.p-3:not(.btn) > tbody > tr > td,
.p-3.btn td a {
padding: 12px !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
</style>
</head>
<body
class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<tbody>
<tr>
<td
valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Account Deletion
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Account Deletion</h1>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
You requested to have your account
deleted, if you did not perform this
action please take measures to secure your
account immediately.
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">
You requested to have your account deleted, if you did not perform
this action please take measures to secure your account immediately.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="btn btn-primary p-3 fw-700"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
font-weight: 700 !important;
"
>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
border-radius: 6px;
font-weight: 700 !important;
margin: 0;
"
align="center"
bgcolor="#0d6efd"
>
<a
href="{{url}}"
style="
color: #ffffff;
font-size: 16px;
font-family: Helvetica, Arial,
sans-serif;
text-decoration: none;
border-radius: 6px;
line-height: 20px;
display: block;
font-weight: 700 !important;
white-space: nowrap;
background-color: #0d6efd;
padding: 12px;
border: 1px solid #0d6efd;
"
>Confirm</a
>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Confirm</a>
</td>
</tr>
</tbody>
@@ -467,66 +96,58 @@
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in Europe
</div>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
@@ -25,6 +25,14 @@
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
@@ -5,3 +5,10 @@ Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
@@ -0,0 +1,9 @@
You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html
@@ -0,0 +1,53 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Notice of Terms &amp; Policy Change</h1>
<p>
This email is to notify you that we're updating all of our policy
documents.
</p>
<p>
You can read our updated Terms of Service and Privacy Policy below:<br />
Effective. 24th March 2025
</p>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Terms of Service</a
>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Privacy Policy</a
>
<p>
Our Community Guidelines have also been updated to help us better
protect and safeguard the platform. You can check them out here:
</p>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Community Guidelines</a
>
</div>
<div class="text-muted text-center my-6">
Sent by Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
+72 -452
View File
@@ -1,462 +1,90 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type="text/css">
body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-3:not(table),
.p-3:not(.btn) > tbody > tr > td,
.p-3.btn td a {
padding: 12px !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
</style>
</head>
<body
class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<tbody>
<tr>
<td
valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Password Reset
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Password Reset</h1>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
You requested a password reset, click
below to continue.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">You requested a password reset, click below to continue.</p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="btn btn-primary p-3 fw-700"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
font-weight: 700 !important;
"
>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
border-radius: 6px;
font-weight: 700 !important;
margin: 0;
"
align="center"
bgcolor="#0d6efd"
>
<a
href="{{url}}"
style="
color: #ffffff;
font-size: 16px;
font-family: Helvetica, Arial,
sans-serif;
text-decoration: none;
border-radius: 6px;
line-height: 20px;
display: block;
font-weight: 700 !important;
white-space: nowrap;
background-color: #0d6efd;
padding: 12px;
border: 1px solid #0d6efd;
"
>Reset</a
>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Reset</a>
</td>
</tr>
</tbody>
@@ -465,66 +93,58 @@
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in Europe
</div>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
@@ -21,6 +21,14 @@
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
+7
View File
@@ -5,3 +5,10 @@ Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
@@ -0,0 +1,9 @@
You requested a password reset, if you did not perform this action you can safely ignore this email.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html
+82 -505
View File
@@ -1,408 +1,80 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.5.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type="text/css">
body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
</style>
</head>
<body
class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<tbody>
<tr>
<td
valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Account Suspended
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Account Suspended</h1>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Your account has been suspended, for one
or more reasons:
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">Your account has been suspended, for one or more reasons:</p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
@@ -411,193 +83,98 @@
<ul class="">
{{list}}
</ul>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p
style="display: {{duration_display}}; line-height: 24px; font-size: 16px; width: 100%; margin: 0;"
class=""
align="left"
>
You will be able to use your account again
in {{duration}} days.
<p style="display: {{duration_display}}; line-height: 24px; font-size: 16px; width: 100%; margin: 0;" class="" align="left">
You will be able to use your account again in {{duration}} days.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Further violations may result in a
permanent ban depending on severity,
please abide by the
<a
href="https://revolt.chat/aup"
style="color: #0d6efd"
>Acceptable Usage Policy</a
>.
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">
Further violations may result in a permanent ban depending on
severity, please abide by the
<a href="https://revolt.chat/aup" style="color: #0d6efd;">Acceptable Usage Policy</a>.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Ban evasion is prohibited and will be
dealt with accordingly.
</p>
<p style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">Ban evasion is prohibited and will be dealt with accordingly.</p>
</td>
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in Europe
</div>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
@@ -33,6 +33,14 @@
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
@@ -10,3 +10,10 @@ Ban evasion is prohibited and will be dealt with accordingly.
This email is intended for {{email}}
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
@@ -0,0 +1,8 @@
Your account has been suspended, for one or more reasons:
{{list}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html
+72 -452
View File
@@ -1,462 +1,90 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type="text/css">
body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-3:not(table),
.p-3:not(.btn) > tbody > tr > td,
.p-3.btn td a {
padding: 12px !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
</style>
</head>
<body
class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<tbody>
<tr>
<td
valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Almost there!
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Almost there!</h1>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
To complete your sign up, we just need to
verify your email.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">To complete your sign up, we just need to verify your email.</p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<table
class="btn btn-primary p-3 fw-700"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
font-weight: 700 !important;
"
>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 16px;
border-radius: 6px;
font-weight: 700 !important;
margin: 0;
"
align="center"
bgcolor="#0d6efd"
>
<a
href="{{url}}"
style="
color: #ffffff;
font-size: 16px;
font-family: Helvetica, Arial,
sans-serif;
text-decoration: none;
border-radius: 6px;
line-height: 20px;
display: block;
font-weight: 700 !important;
white-space: nowrap;
background-color: #0d6efd;
padding: 12px;
border: 1px solid #0d6efd;
"
>Confirm</a
>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Confirm</a>
</td>
</tr>
</tbody>
@@ -465,66 +93,58 @@
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in Europe
</div>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
@@ -22,6 +22,14 @@
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
@@ -6,3 +6,10 @@ Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
@@ -0,0 +1,10 @@
Almost there!
To complete your sign up, we just need to verify your email.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-files"
version = "0.8.2"
version = "0.8.4"
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.2", path = "../config", features = [
revolt-config = { version = "0.8.4", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.2", path = "../result" }
revolt-result = { version = "0.8.4", path = "../result" }
# image processing
jxl-oxide = "0.8.1"
+5 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.8.2"
version = "0.8.4"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,13 +20,14 @@ default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.8.2", path = "../config" }
revolt-permissions = { version = "0.8.2", path = "../permissions" }
revolt-config = { version = "0.8.4", path = "../config" }
revolt-permissions = { version = "0.8.4", path = "../permissions" }
# Utility
regex = "1"
regex = "1.11"
indexmap = "1.9.3"
once_cell = "1.17.1"
num_enum = "0.6.1"
# Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
+10
View File
@@ -101,6 +101,10 @@ auto_derived!(
)]
role_permissions: HashMap<String, OverrideField>,
/// Category the channel is in
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
/// Whether this channel is marked as not safe for work
#[cfg_attr(
feature = "serde",
@@ -138,6 +142,10 @@ auto_derived!(
)]
role_permissions: HashMap<String, OverrideField>,
/// Category the channel is in
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
/// Whether this channel is marked as not safe for work
#[cfg_attr(
feature = "serde",
@@ -170,6 +178,8 @@ auto_derived!(
pub default_permissions: Option<OverrideField>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub last_message_id: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub parent: Option<String>
}
/// Optional fields on channel object
+12 -5
View File
@@ -1,8 +1,6 @@
use std::time::SystemTime;
use indexmap::{IndexMap, IndexSet};
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_config::config;
#[cfg(feature = "validator")]
@@ -15,9 +13,6 @@ use iso8601_timestamp::Timestamp;
use super::{Channel, Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR};
pub static RE_MENTION: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
auto_derived_partial!(
/// Message
pub struct Message {
@@ -58,6 +53,9 @@ auto_derived_partial!(
/// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
/// Array of role ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub role_mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
@@ -239,6 +237,10 @@ auto_derived!(
pub id: String,
/// Whether this reply should mention the message's author
pub mention: bool,
/// Whether to error if the referenced message doesn't exist.
/// Otherwise, send a message without this reply.
/// Default is true.
pub fail_if_not_exists: Option<bool>,
}
/// Message to send
@@ -367,6 +369,11 @@ auto_derived!(
pub enum MessageFlags {
/// Message will not send push / desktop notifications
SuppressNotifications = 1,
/// Message will mention all users who can see the channel
MentionsEveryone = 2,
/// Message will mention all users who are online and can see the channel.
/// This cannot be true if MentionsEveryone is true
MentionsOnline = 3,
}
/// Optional fields on message
+40 -7
View File
@@ -28,8 +28,8 @@ auto_derived_partial!(
// TODO: investigate if this is redundant and can be removed
pub channels: Vec<String>,
/// Categories for this server
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub categories: Option<Vec<Category>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))]
pub categories: HashMap<String, Category>,
/// Configuration for sending system event messages
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub system_messages: Option<SystemMessageChannels>,
@@ -126,15 +126,52 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct Category {
/// Unique ID for this category
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub id: String,
/// Title for this category
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub title: String,
/// Default permissions assigned to users in this channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[cfg_attr(
feature = "serde",
serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)
)]
pub role_permissions: HashMap<String, OverrideField>,
/// Channels in this category
pub channels: Vec<String>,
}
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateCategory {
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub title: String,
/// Channels in this category
pub channels: Option<Vec<String>>,
}
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditCategory {
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub title: Option<String>,
/// Channels in this category
pub channels: Option<Vec<String>>,
/// Fields to remove from category object
pub remove: Option<Vec<FieldsCategory>>,
}
pub enum FieldsCategory {
DefaultPermissions
}
pub struct DataDefaultCategoryPermissions {
pub permissions: Override
}
/// System message channel assignments
pub struct SystemMessageChannels {
/// ID of channel to send user join messages in
@@ -226,10 +263,6 @@ auto_derived!(
pub icon: Option<String>,
/// Attachment Id for banner
pub banner: Option<String>,
/// Category structure for server
#[cfg_attr(feature = "validator", validate)]
pub categories: Option<Vec<Category>>,
/// System message configuration
pub system_messages: Option<SystemMessageChannels>,
+7
View File
@@ -0,0 +1,7 @@
[package]
name = "revolt-parser"
version = "0.1.0"
edition = "2021"
[dependencies]
logos = { version = "*" }
+245
View File
@@ -0,0 +1,245 @@
use std::collections::{HashSet, VecDeque};
use logos::Logos;
#[derive(Debug, Clone, Logos, PartialEq)]
#[logos(skip "\n")]
pub enum MessageToken {
#[token("\\")]
Escape,
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())]
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),
#[token("@everyone")]
MentionEveryone,
#[token("@online")]
MentionOnline
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct MessageResults {
pub user_mentions: HashSet<String>,
pub role_mentions: HashSet<String>,
pub mentions_everyone: bool,
pub mentions_online: bool
}
struct MessageParserIterator<I> {
inner: I,
temp: VecDeque<MessageToken>
}
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> {
type Item = MessageToken;
fn next(&mut self) -> Option<Self::Item> {
if !self.temp.is_empty() {
self.temp.pop_front()
} else {
let token = self.inner.next();
if token == Some(MessageToken::Escape) {
self.inner.next();
token
} else if let Some(MessageToken::CodeblockMarker(ty)) = token {
loop {
let next_token = self.inner.next();
if next_token == Some(MessageToken::CodeblockMarker(ty)) {
self.temp.clear();
self.temp.push_back(MessageToken::CodeblockMarker(ty));
break next_token
} else if let Some(token) = next_token {
self.temp.push_back(token);
} else {
break Some(MessageToken::CodeblockMarker(ty))
}
}
} else {
token
}
}
}
}
pub fn parse_message_iter(text: &str) -> impl Iterator<Item = MessageToken> + '_ {
MessageParserIterator {
inner: MessageToken::lexer(text).flatten(),
temp: VecDeque::new()
}
}
pub fn parse_message(text: &str) -> MessageResults {
let mut results = MessageResults::default();
for token in parse_message_iter(text) {
match token {
MessageToken::Escape => {}
MessageToken::CodeblockMarker(_) => {},
MessageToken::UserMention(id) => { results.user_mentions.insert(id); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); },
MessageToken::MentionEveryone => results.mentions_everyone = true,
MessageToken::MentionOnline => results.mentions_online = true,
};
};
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_nodes() {
let output = parse_message_iter("Hello everyone").collect::<Vec<_>>();
assert_eq!(output.len(), 0);
}
#[test]
fn test_simple_user_mention() {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
}
#[test]
fn test_simple_role_mention() {
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
}
#[test]
fn test_mention_everyone() {
let output = parse_message_iter("Hello @everyone.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::MentionEveryone);
}
#[test]
fn test_mention_online() {
let output = parse_message_iter("Hello @online.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::MentionOnline);
}
#[test]
fn test_everything() {
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[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
#[test]
fn test_everything_no_spaces() {
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[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
#[test]
fn test_codeblock_no_mentions() {
let output = parse_message_iter("```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online\n```").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
}
#[test]
fn test_uncontained_codeblock_should_mention() {
let output = parse_message_iter("```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
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[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);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_uncontained_inline_codeblock_should_mention() {
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
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[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_double_inline_codeblock() {
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(2));
assert_eq!(output[1], MessageToken::CodeblockMarker(2));
}
#[test]
fn test_double_inline_codeblock_with_backticks_inside() {
let output = parse_message_iter("``this `should` not `ping` @everyone``").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(2));
assert_eq!(output[1], MessageToken::CodeblockMarker(2));
}
#[test]
fn test_in_middle() {
let output = parse_message_iter("i am not pinging `@everyone`.").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[1], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_escaped_codeblock() {
let output = parse_message_iter("i am ~~not~~ pinging \\`@everyone` ok.").collect::<Vec<_>>();
assert_eq!(output.len(), 3);
assert_eq!(output[0], MessageToken::Escape);
assert_eq!(output[1], MessageToken::MentionEveryone);
assert_eq!(output[2], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_escape_mention() {
let output = parse_message_iter("i wont ping \\@everyone").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::Escape);
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.8.2"
version = "0.8.4"
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.2", path = "../result" }
revolt-result = { version = "0.8.4", path = "../result" }
# Utility
auto_ops = "0.3.0"
+29 -1
View File
@@ -68,6 +68,26 @@ pub async fn calculate_server_permissions<P: PermissionQuery>(query: &mut P) ->
permissions
}
pub async fn calculate_category_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
let mut permissions = calculate_server_permissions(query).await;
permissions.apply(query.get_default_category_permissions().await);
for role_override in query.get_our_category_role_overrides().await {
permissions.apply(role_override)
}
if !permissions.has_channel_permission(ChannelPermission::ViewChannel) {
permissions.revoke_all();
}
if query.are_we_timed_out().await {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
permissions
}
/// Calculate permissions against a channel
pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
if query.are_we_privileged().await {
@@ -109,11 +129,19 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
}
ChannelType::ServerChannel => {
query.set_server_from_channel().await;
query.set_category_from_channel().await;
if query.are_we_server_owner().await {
ChannelPermission::GrantAllSafe.into()
} else if query.are_we_a_member().await {
let mut permissions = calculate_server_permissions(query).await;
permissions.apply(query.get_default_category_permissions().await);
for role_override in query.get_our_category_role_overrides().await {
permissions.apply(role_override)
}
permissions.apply(query.get_default_channel_permissions().await);
for role_override in query.get_our_channel_role_overrides().await {
@@ -135,4 +163,4 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
}
ChannelType::Unknown => 0_u64.into(),
}
}
}
@@ -90,8 +90,16 @@ pub enum ChannelPermission {
/// Move members between voice channels
MoveMembers = 1 << 35,
// * Channel permissions two electric boogaloo
/// Mention everyone and online members
MentionEveryone = 1 << 37,
/// Mention roles
MentionRoles = 1 << 38,
MoveChannels = 1 << 39,
// * Misc. permissions
// % Bits 36 to 52: free area
// % Bits 40 to 52: free area
// % Bits 53 to 64: do not use
// * Grant all permissions
+157
View File
@@ -91,6 +91,18 @@ async fn validate_user_permissions() {
async fn set_server_from_channel(&mut self) {
unreachable!()
}
async fn get_default_category_permissions(&mut self) -> Override {
unreachable!()
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
unreachable!()
}
async fn set_category_from_channel(&mut self) {
unreachable!()
}
}
}
@@ -183,6 +195,18 @@ async fn validate_group_permissions() {
async fn set_server_from_channel(&mut self) {
unreachable!()
}
async fn get_default_category_permissions(&mut self) -> Override {
unreachable!()
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
unreachable!()
}
async fn set_category_from_channel(&mut self) {
unreachable!()
}
}
}
@@ -287,6 +311,18 @@ async fn validate_server_permissions() {
async fn set_server_from_channel(&mut self) {
// no-op
}
async fn get_default_category_permissions(&mut self) -> Override {
Override { allow: 0, deny: 0 }
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn set_category_from_channel(&mut self) {
// no-op
}
}
}
@@ -373,5 +409,126 @@ async fn validate_timed_out_member() {
async fn set_server_from_channel(&mut self) {
// no-op
}
async fn get_default_category_permissions(&mut self) -> Override {
Override { allow: 0, deny: 0 }
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn set_category_from_channel(&mut self) {
// no-op
}
}
}
#[async_std::test]
async fn validate_category_permissions() {
/// Scenario in which we are in a channel in a server and has a role
/// Server defaults let us send messages, view channels
/// Category default denies sending, allows reacting
/// Category role allows sending message
struct Scenario {}
let mut query = Scenario {};
let perms = calculate_channel_permissions(&mut query).await;
let value: u64 = perms.into();
assert_eq!(value, ChannelPermission::ViewChannel as u64 | ChannelPermission::React as u64 | ChannelPermission::SendMessage as u64);
#[async_trait]
impl PermissionQuery for Scenario {
async fn are_we_privileged(&mut self) -> bool {
false
}
async fn are_we_a_bot(&mut self) -> bool {
unreachable!()
}
async fn are_the_users_same(&mut self) -> bool {
unreachable!()
}
async fn user_relationship(&mut self) -> RelationshipStatus {
unreachable!()
}
async fn user_is_bot(&mut self) -> bool {
unreachable!()
}
async fn have_mutual_connection(&mut self) -> bool {
unreachable!()
}
async fn are_we_server_owner(&mut self) -> bool {
false
}
async fn are_we_a_member(&mut self) -> bool {
true
}
async fn get_default_server_permissions(&mut self) -> u64 {
ChannelPermission::SendMessage as u64 | ChannelPermission::ViewChannel as u64
}
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn are_we_timed_out(&mut self) -> bool {
false
}
async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::ServerChannel
}
async fn get_default_channel_permissions(&mut self) -> Override {
Override { allow: 0, deny: 0 }
}
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn do_we_own_the_channel(&mut self) -> bool {
unreachable!()
}
async fn are_we_part_of_the_channel(&mut self) -> bool {
unreachable!()
}
async fn set_recipient_as_user(&mut self) {
unreachable!()
}
async fn set_server_from_channel(&mut self) {
// no-op
}
async fn get_default_category_permissions(&mut self) -> Override {
Override {
allow: ChannelPermission::React as u64,
deny: ChannelPermission::SendMessage as u64
}
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
vec![
Override {
allow: ChannelPermission::SendMessage as u64,
deny: 0
}
]
}
async fn set_category_from_channel(&mut self) {
// no-op
}
}
}
+9
View File
@@ -51,6 +51,12 @@ pub trait PermissionQuery {
/// Get the ordered role overrides (from lowest to highest) for this member in this channel
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override>;
/// Get the default category permissions
async fn get_default_category_permissions(&mut self) -> Override;
/// Get the ordered role overrides (from lowest to highest) for this member in this category
async fn get_our_category_role_overrides(&mut self) -> Vec<Override>;
/// Do we own this group or saved messages channel if it is one of those?
async fn do_we_own_the_channel(&mut self) -> bool;
@@ -64,4 +70,7 @@ pub trait PermissionQuery {
/// Set the current server as the server owning this channel
/// (this will only ever be called for server channels, use unimplemented!() for other code paths)
async fn set_server_from_channel(&mut self);
//// SEts the current category as the category the channel is in
async fn set_category_from_channel(&mut self);
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.8.2"
version = "0.8.4"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.8.2"
version = "0.8.4"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
+5 -1
View File
@@ -1,5 +1,4 @@
use axum::{http::StatusCode, response::IntoResponse, Json};
use rocket::http::Status;
use crate::{Error, ErrorType};
@@ -43,15 +42,18 @@ impl IntoResponse for Error {
ErrorType::Banned => StatusCode::FORBIDDEN,
ErrorType::AlreadyInServer => StatusCode::CONFLICT,
ErrorType::CannotTimeoutYourself => StatusCode::BAD_REQUEST,
ErrorType::UnknownCategory => StatusCode::NOT_FOUND,
ErrorType::TooManyServers { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyEmbeds { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyEmoji { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyChannels { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyRoles { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyCategories { .. } => StatusCode::BAD_REQUEST,
ErrorType::ReachedMaximumBots => StatusCode::BAD_REQUEST,
ErrorType::IsBot => StatusCode::BAD_REQUEST,
ErrorType::IsNotBot => StatusCode::BAD_REQUEST,
ErrorType::BotIsPrivate => StatusCode::FORBIDDEN,
ErrorType::CannotReportYourself => StatusCode::BAD_REQUEST,
@@ -75,6 +77,8 @@ impl IntoResponse for Error {
ErrorType::NotFound => StatusCode::NOT_FOUND,
ErrorType::NoEffect => StatusCode::OK,
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
+11
View File
@@ -115,12 +115,17 @@ pub enum ErrorType {
TooManyRoles {
max: usize,
},
TooManyCategories {
max: usize,
},
UnknownCategory,
AlreadyInServer,
CannotTimeoutYourself,
// ? Bot related errors
ReachedMaximumBots,
IsBot,
IsNotBot,
BotIsPrivate,
// ? User safety related errors
@@ -148,6 +153,7 @@ pub enum ErrorType {
InvalidCredentials,
InvalidProperty,
InvalidSession,
InvalidFlagValue,
NotAuthenticated,
DuplicateNonce,
NotFound,
@@ -168,6 +174,11 @@ pub enum ErrorType {
// ? Legacy errors
VosoUnavailable,
// ? Feature flag disabled in the config
FeatureDisabled {
feature: String,
},
}
#[macro_export]
+6 -1
View File
@@ -42,21 +42,25 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotInGroup => Status::NotFound,
ErrorType::AlreadyPinned => Status::BadRequest,
ErrorType::NotPinned => Status::BadRequest,
ErrorType::InvalidFlagValue => Status::BadRequest,
ErrorType::UnknownServer => Status::NotFound,
ErrorType::InvalidRole => Status::NotFound,
ErrorType::Banned => Status::Forbidden,
ErrorType::AlreadyInServer => Status::Conflict,
ErrorType::CannotTimeoutYourself => Status::BadRequest,
ErrorType::UnknownCategory => Status::NotFound,
ErrorType::TooManyServers { .. } => Status::BadRequest,
ErrorType::TooManyEmbeds { .. } => Status::BadRequest,
ErrorType::TooManyEmoji { .. } => Status::BadRequest,
ErrorType::TooManyChannels { .. } => Status::BadRequest,
ErrorType::TooManyRoles { .. } => Status::BadRequest,
ErrorType::TooManyCategories { .. } => Status::BadRequest,
ErrorType::ReachedMaximumBots => Status::BadRequest,
ErrorType::IsBot => Status::BadRequest,
ErrorType::IsNotBot => Status::BadRequest,
ErrorType::BotIsPrivate => Status::Forbidden,
ErrorType::CannotReportYourself => Status::BadRequest,
@@ -80,6 +84,7 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotFound => Status::NotFound,
ErrorType::NoEffect => Status::Ok,
ErrorType::FailedValidation { .. } => Status::BadRequest,
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest,
ErrorType::FileTooSmall => Status::UnprocessableEntity,
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-crond"
version = "0.8.2"
version = "0.8.4"
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.2", path = "../../core/database" }
revolt-result = { version = "0.8.2", path = "../../core/result" }
revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-files = { version = "0.8.2", path = "../../core/files" }
revolt-database = { version = "0.8.4", path = "../../core/database" }
revolt-result = { version = "0.8.4", path = "../../core/result" }
revolt-config = { version = "0.8.4", path = "../../core/config" }
revolt-files = { version = "0.8.4", path = "../../core/files" }
+10 -5
View File
@@ -1,14 +1,18 @@
[package]
name = "revolt-pushd"
version = "0.8.2"
version = "0.8.4"
edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies]
revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-database = { version = "0.8.2", path = "../../core/database" }
revolt-models = { version = "0.8.2", path = "../../core/models", features = [
revolt-config = { version = "0.8.4", path = "../../core/config" }
revolt-database = { version = "0.8.4", path = "../../core/database" }
revolt-models = { version = "0.8.4", path = "../../core/models", features = [
"validator",
] }
revolt-presence = { version = "0.8.1", path = "../../core/presence", features = [
"redis-is-patched",
] }
amqprs = { version = "1.7.0" }
fcm_v1 = "0.3.0"
@@ -19,9 +23,10 @@ tokio = "1.39.2"
async-trait = "0.1.81"
ulid = "1.0.0"
authifier = "1.0.8"
authifier = "1.0.10"
log = "0.4.11"
pretty_env_logger = "0.4.0"
#serialization
serde_json = "1"
Binary file not shown.
@@ -71,8 +71,14 @@ impl AsyncConsumer for AckConsumer {
#[allow(clippy::disallowed_methods)]
let unreads = self.db.fetch_unread_mentions(&payload.user_id).await;
debug!("Processing unreads for {:}", &payload.user_id);
if let Ok(u) = &unreads {
if u.is_empty() {
debug!(
"Discarding unread task (no mentions found) for {:}",
&payload.user_id
);
return;
}
} else {
@@ -95,6 +101,10 @@ impl AsyncConsumer for AckConsumer {
.collect();
if apple_sessions.is_empty() {
debug!(
"Discarding unread task (no apn sessions found) for {:}",
&payload.user_id
);
return;
}
@@ -0,0 +1,275 @@
use std::{
collections::{HashMap, HashSet},
hash::RandomState,
};
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use async_trait::async_trait;
use revolt_database::{
events::rabbit::*, util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member,
MessageFlagsValue,
};
use revolt_models::v0::{MessageFlags, PushNotification};
pub struct MassMessageConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
}
impl Channeled for MassMessageConsumer {
fn get_connection(&self) -> Option<&Connection> {
if self.conn.is_none() {
None
} else {
Some(self.conn.as_ref().unwrap())
}
}
fn get_channel(&self) -> Option<&Channel> {
if self.channel.is_none() {
None
} else {
Some(self.channel.as_ref().unwrap())
}
}
fn set_connection(&mut self, conn: Connection) {
self.conn = Some(conn);
}
fn set_channel(&mut self, channel: Channel) {
self.channel = Some(channel)
}
}
impl MassMessageConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> MassMessageConsumer {
MassMessageConsumer {
db,
authifier_db,
conn: None,
channel: None,
}
}
async fn fire_notification_for_users(&mut self, push: &PushNotification, users: &[String]) {
if let Ok(sessions) = self
.authifier_db
.find_sessions_with_subscription(users)
.await
{
let config = revolt_config::config().await;
for session in sessions {
if let Some(sub) = session.subscription {
let mut sendable = PayloadToService {
notification: PayloadKind::MessageNotification(push.clone()),
token: sub.auth,
user_id: session.user_id,
session_id: session.id,
extras: HashMap::new(),
};
let args: BasicPublishArguments;
if sub.endpoint == "apn" {
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
} else if sub.endpoint == "fcm" {
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.fcm.queue.as_str(),
)
.finish();
} else {
// web push (vapid)
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.vapid.queue.as_str(),
)
.finish();
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable).unwrap();
publish_message(self, payload.into(), args).await;
}
}
}
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for MassMessageConsumer {
/// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
let config = revolt_config::config().await;
let content = String::from_utf8(content).unwrap();
let payload: MassMessageSentPayload = serde_json::from_str(content.as_str()).unwrap();
debug!("Received mass message event");
// We should only ever receive clumped messages from a single channel, so it's safe to reuse this many times.
let mut query: Option<BulkDatabasePermissionQuery<'_>> = None;
let query_db = self.db.clone();
for push in payload.notifications {
if query.is_none() {
query = Some(
BulkDatabasePermissionQuery::from_server_id(&query_db, &payload.server_id)
.await
.from_channel_id(push.channel.id().to_string()) // wrong channel model, so fetch the right one
.await,
);
}
let existing_mentions: HashSet<String, RandomState> =
if let Some(ref mentions) = push.message.mentions {
HashSet::from_iter(mentions.iter().cloned())
} else {
HashSet::new()
};
// KNOWN QUIRK: if you mention @online and role(s), the offline members with the role(s) wont get pinged
if let Some(ref query) = query {
let flags = MessageFlagsValue(push.message.flags);
if flags.has(MessageFlags::MentionsEveryone) {
let mut db_query = self
.db
.fetch_all_members_chunked(&payload.server_id)
.await
.expect("Failed to fetch members from database");
let mut exhausted = false;
let ack_chnl = vec![push.channel.id().to_string()];
loop {
let mut chunk: Vec<Member> = vec![];
for _ in 0..config.pushd.mass_mention_chunk_size {
if let Some(member) = db_query.next().await {
chunk.push(member);
} else {
exhausted = true;
break;
}
}
let userids: Vec<String> =
chunk.iter().map(|member| member.id.user.clone()).collect();
debug!("Userids in chunk: {:?}", userids);
if let Err(err) = self
.db
.add_mention_to_many_unreads(push.channel.id(), &userids, &ack_chnl)
.await
{
revolt_config::capture_error(&err);
}
// ignore anyone in this list
let online_users = revolt_presence::filter_online(&userids).await;
let target_users: Vec<String> = userids
.iter()
.filter(|id| {
!online_users.contains(*id) && !existing_mentions.contains(*id)
})
.cloned()
.collect();
debug!(
"Userids after filter: {:?} (online: {:?}",
target_users, online_users
);
self.fire_notification_for_users(&push, &target_users).await;
if exhausted {
break;
}
}
} else if let Some(roles) = &push.message.role_mentions {
// role mentions
let _role_members = self
.db
.fetch_all_members_with_roles_chunked(&payload.server_id, roles)
.await;
debug!("role members: {:?}", _role_members);
if _role_members.is_err() {
revolt_config::capture_error(&_role_members.err().unwrap());
return;
}
let mut role_members = _role_members.unwrap();
let mut chunk = vec![];
let mut exhausted = false;
while !exhausted {
chunk.clear();
for _ in 0..config.pushd.mass_mention_chunk_size {
if let Some(member) = role_members.next().await {
chunk.push(member);
} else {
exhausted = true;
break;
}
}
let mut q = query.clone().members(&chunk);
let viewing_members: Vec<String> = q
.members_can_see_channel()
.await
.iter()
.filter_map(|(uid, viewable)| {
if *viewable && !existing_mentions.contains(uid) {
Some(uid.clone())
} else {
None
}
})
.collect();
debug!("viewing members: {:?}", viewing_members);
let online = revolt_presence::filter_online(&viewing_members).await;
debug!("online: {:?}", online);
let targets: Vec<String> = viewing_members
.iter()
.filter(|m| !online.contains(*m))
.cloned()
.collect();
debug!("targets: {:?}", targets);
self.fire_notification_for_users(&push, &targets).await;
}
}
}
}
}
}
@@ -3,4 +3,5 @@ pub mod fr_accepted;
pub mod fr_received;
pub mod generic;
mod internal;
pub mod mass_mention;
pub mod message;
@@ -81,7 +81,7 @@ impl ApnsOutboundConsumer {
}
}
println!("Got badge count for APN: {}", mention_count);
debug!("Got badge count for APN: {}", mention_count);
return Some(mention_count);
}
@@ -188,6 +188,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(),
};
debug!(
"Sending friend request received for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
}
@@ -230,6 +234,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(),
};
debug!(
"Sending friend request accept for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
}
PayloadKind::Generic(alert) => {
@@ -259,6 +267,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(),
};
debug!(
"Sending generic notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
}
@@ -294,6 +306,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
channel_name: alert.channel.name().unwrap_or(&title),
};
debug!(
"Sending message notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await;
}
PayloadKind::BadgeUpdate(badge) => {
@@ -307,6 +323,7 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(),
};
debug!("Sending badge update for user: {:}", &payload.user_id);
resp = self.client.send(apn_payload).await;
}
}
@@ -321,6 +338,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
}),
..
}) => {
info!(
"Removing APNS subscription id {:} (user: {:}) due to invalid token",
&payload.session_id, &payload.user_id
);
if let Err(err) = self
.db
.remove_push_subscription_by_session_id(&payload.session_id)
+20 -6
View File
@@ -1,3 +1,6 @@
#[macro_use]
extern crate log;
use amqprs::{
channel::{
BasicConsumeArguments, Channel, ExchangeDeclareArguments, QueueBindArguments,
@@ -14,7 +17,7 @@ mod consumers;
use consumers::{
inbound::{
ack::AckConsumer, fr_accepted::FRAcceptedConsumer, fr_received::FRReceivedConsumer,
generic::GenericConsumer, message::MessageConsumer,
generic::GenericConsumer, mass_mention::MassMessageConsumer, message::MessageConsumer,
},
outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer},
};
@@ -22,6 +25,7 @@ use consumers::{
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
let config = config().await;
pretty_env_logger::init();
// Setup database
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
@@ -96,6 +100,17 @@ async fn main() {
.await,
);
connections.push(
make_queue_and_consume(
&config,
&config.pushd.mass_mention_queue,
config.pushd.get_mass_mention_routing_key().as_str(),
None,
MassMessageConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
if !config.pushd.apn.pkcs8.is_empty() {
connections.push(
make_queue_and_consume(
@@ -223,11 +238,10 @@ where
.manual_ack(false)
.finish();
channel.basic_consume(consumer, args).await.unwrap();
log::info!(
"Consuming routing key {} as queue {}",
routing_key,
queue_name
let routing_key = channel.basic_consume(consumer, args).await.unwrap();
info!(
"Consuming routing key {} as queue {}, tag {}",
routing_key, queue_name, routing_key
);
(channel, connection)
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.8.2"
version = "0.8.4"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -55,7 +55,7 @@ lettre = "0.10.0-alpha.4"
rocket = { version = "0.5.1", default-features = false, features = ["json"] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" }
rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_authifier = { version = "1.0.9" }
rocket_authifier = { version = "1.0.10" }
rocket_prometheus = "0.10.0-rc.3"
# spec generation
@@ -66,7 +66,7 @@ revolt_rocket_okapi = { version = "0.10.0", features = ["swagger"] }
amqprs = { version = "1.7.0" }
# core
authifier = "1.0.9"
authifier = "1.0.10"
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database", features = [
"rocket-impl",
@@ -99,5 +99,26 @@ mod test {
// TEST: member leaves group (no delete)
// TEST: no effect with saved messages
// TEST: DM set to inactive
// TEST: server channel deleted
#[rocket::async_test]
async fn success_delete_channel() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, channels) = harness.new_server(&user).await;
let response = TestHarness::with_session(
session,
harness
.client
.delete(format!("/channels/{}", channels[0].id())),
)
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness
.wait_for_event(channels[0].id(), |event| match event {
EventV1::ChannelDelete { id, .. } => id == channels[0].id(),
_ => false,
})
.await;
}
}
@@ -1,7 +1,4 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, PartialMessage, SystemMessage, User, AMQP,
};
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, Database, PartialMessage, SystemMessage, User, AMQP};
use revolt_models::v0::MessageAuthor;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
@@ -22,10 +19,12 @@ pub async fn message_pin(
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
if !matches!(channel, Channel::DirectMessage { .. }) {
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
@@ -84,7 +84,7 @@ pub async fn message_send(
// Create model user / members
let model_user = user
.clone()
.into_known_static(revolt_presence::is_online(&user.id).await);
.into_known_static(revolt_presence::is_online(&user.id).await).await;
let model_member: Option<v0::Member> = query
.member_ref()
@@ -117,10 +117,11 @@ mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{
util::{idempotency::IdempotencyKey, reference::Reference},
Channel, Member, Message, PartialChannel, PartialMember, Role, Server,
Channel, Member, Message, MessageFlagsValue, PartialChannel, PartialMember, Role, Server,
};
use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_models::v0::{self, DataCreateServerChannel, MessageFlags};
use revolt_permissions::{ChannelPermission, OverrideField};
use revolt_result::ErrorType;
#[rocket::async_test]
async fn message_mention_constraints() {
@@ -191,6 +192,7 @@ mod test {
d: ChannelPermission::ViewChannel as i64,
}),
last_message_id: None,
parent: None,
};
locked_channel
.update(&harness.db, partial, vec![])
@@ -323,4 +325,352 @@ mod test {
"Mention was scrubbed when the user can see the channel"
);
}
#[rocket::async_test]
async fn message_reply() {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let (server, channels) = harness.new_server(&user).await;
let channel = harness.new_channel(&server).await;
let (_, member, message) = harness.new_message(&user, &server, channels).await;
// Send a message with a reply
// Should succeed
let message_with_reply = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some("Message with reply".to_string()),
nonce: None,
attachments: None,
replies: Some(vec![v0::ReplyIntent {
id: message.id.clone(),
mention: false,
fail_if_not_exists: Some(true),
}]),
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
false,
)
.await
.expect("Failed to create message with reply");
assert!(
message_with_reply.replies.is_some(),
"Message replies is None",
);
let replies = message_with_reply.replies.unwrap();
assert!(!replies.is_empty(), "Message replies is empty",);
assert_eq!(replies[0], message.id, "Message reply ID does not match",);
// Delete the message
message
.clone()
.delete(&harness.db)
.await
.expect("Failed to delete message");
// Attempt to create messages with a reply to a deleted message
// fail_if_not_exists is set to false
// Should send the message without a reply
let message_with_missing_reply = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some("Message with missing reply".to_string()),
nonce: None,
attachments: None,
replies: Some(vec![v0::ReplyIntent {
id: message.id.clone(),
mention: false,
fail_if_not_exists: Some(false),
}]),
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("3".to_string()),
false,
false,
)
.await
.expect("Failed to create message with missing reply");
assert!(
message_with_missing_reply.replies.is_none()
|| message_with_missing_reply.replies.unwrap().is_empty(),
"Message replies exist when they shouldn't",
);
// fail_if_not_exists is set to true
// Should fail to send the message
Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some("Message with missing reply".to_string()),
nonce: None,
attachments: None,
replies: Some(vec![v0::ReplyIntent {
id: message.id.clone(),
mention: false,
fail_if_not_exists: Some(true),
}]),
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("4".to_string()),
false,
false,
)
.await
.expect_err("Created message with missing reply and true fail");
// fail_if_not_exists is not set
// Should fail to send the message
Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some("Message with missing reply".to_string()),
nonce: None,
attachments: None,
replies: Some(vec![v0::ReplyIntent {
id: message.id.clone(),
mention: false,
fail_if_not_exists: None,
}]),
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("4".to_string()),
false,
false,
)
.await
.expect_err("Created message with missing reply and none fail");
}
#[rocket::async_test]
async fn mass_mentions_test() {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let (_, _, other_user) = harness.new_user().await;
let (server, _) = harness.new_server(&user).await;
let channel = harness.new_channel(&server).await;
let (role_id, mut role) = harness
.new_role(
&server,
1,
Some(OverrideField {
a: (ChannelPermission::MentionEveryone as i64)
| (ChannelPermission::MentionRoles as i64),
d: 0,
}),
)
.await;
let (mut other_member, _) = Member::create(&harness.db, &server, &other_user, None)
.await
.expect("Failed to add test member");
// Send a message with an everyone and role mention.
// Should fail
let bad_message_with_mentions = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some(format!("Mentioning @everyone and role <%{}>", &role_id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(
&other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(
other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(other_member.clone().into()),
other_user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
true,
)
.await
.expect_err("Should not have created message with everyone and role pings");
assert!(
matches!(
bad_message_with_mentions.error_type,
ErrorType::MissingPermission { .. }
),
"Intentional permissions error did not return MissingPermission"
);
// Send a mass mention inside a codeblock.
// Should be undetected and therefor pass
let message_with_codeblock = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some(format!("Mentioning `@everyone` and role `<%{}>`", &role_id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(
&other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(
other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(other_member.clone().into()),
other_user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
true,
)
.await
.expect("Failed to create message with everyone and role pings in codeblocks");
assert!(
message_with_codeblock.flags.is_none()
|| !MessageFlagsValue(message_with_codeblock.flags.unwrap())
.has(MessageFlags::MentionsEveryone),
"Message flags mentions everyone when inside codeblock",
);
assert!(
message_with_codeblock.role_mentions.is_none(),
"Role mentions detected when inside codeblock"
);
other_member.roles.push(role_id.clone());
harness
.db
.update_member(
&other_member.id,
&PartialMember {
avatar: None,
id: None,
joined_at: None,
nickname: None,
roles: Some(vec![role_id.clone()]),
timeout: None,
},
vec![],
)
.await
.expect("Failed to add role to user");
// Send a message with an everyone and role mention.
// Should succeed
let message_with_mentions = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some(format!("Mentioning @everyone and role <%{}>", &role_id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(
&other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(
other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(other_member.clone().into()),
other_user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
true,
)
.await
.expect("Failed to create message with everyone and role pings");
assert!(
message_with_mentions.flags.is_some(),
"Message flags is None",
);
assert!(
MessageFlagsValue(message_with_mentions.flags.unwrap())
.has(MessageFlags::MentionsEveryone),
"Message flags does not mention everyone. Flag value is {}",
message_with_mentions.flags.unwrap()
);
assert!(
message_with_mentions.role_mentions.is_some(),
"Message has no role mentions"
);
}
}
@@ -1,7 +1,4 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, FieldsMessage, PartialMessage, SystemMessage, User, AMQP,
};
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, Database, FieldsMessage, PartialMessage, SystemMessage, User, AMQP};
use revolt_models::v0::MessageAuthor;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
@@ -22,10 +19,12 @@ pub async fn message_unpin(
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
if !matches!(channel, Channel::DirectMessage { .. }) {
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
@@ -27,11 +27,6 @@ pub async fn create_emoji(
})
})?;
// Bots cannot manage emojis
if user.bot.is_some() {
return Err(create_error!(IsBot));
}
// Validate we have permission to write into parent
match &data.parent {
v0::EmojiParent::Server { id } => {
@@ -18,10 +18,6 @@ pub async fn delete_emoji(
user: User,
emoji_id: Reference,
) -> Result<EmptyResponse> {
// Bots cannot manage emoji
if user.bot.is_some() {
return Err(create_error!(IsBot));
}
// Fetch the emoji
let emoji = emoji_id.as_emoji(db).await?;
@@ -76,3 +76,169 @@ pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::I
}
}))
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{Channel, Server};
use revolt_models::v0::{
DataCreateGroup, DataCreateServerChannel, Invite, InviteResponse, LegacyServerChannelType,
};
use rocket::http::Status;
#[rocket::async_test]
async fn success_fetch_group_invite() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
DataCreateGroup {
..Default::default()
},
user.id.clone(),
)
.await
.expect("`Channel`");
let create_response = TestHarness::with_session(
session,
harness
.client
.post(format!("/channels/{}/invites", group.id())),
)
.await;
assert_eq!(create_response.status(), Status::Ok);
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
let invite_code = match invite_from_create {
Invite::Group { code, .. } => code,
_ => unreachable!(),
};
let response = harness
.client
.get(format!("/invites/{}", invite_code))
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
match invite_response {
InviteResponse::Group {
code,
channel_id,
user_name,
..
} => {
assert_eq!(code, invite_code);
assert_eq!(channel_id, group.id());
assert_eq!(user_name, user.username);
}
_ => unreachable!(),
}
}
#[rocket::async_test]
async fn fail_fetch_missing_invite() {
let harness = TestHarness::new().await;
let response = harness
.client
.get(format!("/invites/{}", TestHarness::rand_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NotFound);
}
#[rocket::async_test]
async fn success_fetch_text_channel_invite() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, channels) = harness.new_server(&user).await;
let channel = channels.first().expect("Server Channel");
let create_response = TestHarness::with_session(
session,
harness
.client
.post(format!("/channels/{}/invites", channel.id())),
)
.await;
assert_eq!(create_response.status(), Status::Ok);
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
let invite_code = match invite_from_create {
Invite::Server { code, .. } => code,
_ => unreachable!(),
};
let response = harness
.client
.get(format!("/invites/{}", invite_code))
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
match invite_response {
InviteResponse::Server {
code,
channel_id,
user_name,
..
} => {
assert_eq!(code, invite_code);
assert_eq!(channel_id, channel.id());
assert_eq!(user_name, user.username);
}
_ => unreachable!(),
};
}
#[rocket::async_test]
async fn success_fetch_voice_channel_invite() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (server, _) = harness.new_server(&user).await;
let server_mut: &mut Server = &mut server.clone();
let channel = Channel::create_server_channel(
&harness.db,
server_mut,
DataCreateServerChannel {
channel_type: LegacyServerChannelType::Voice,
name: "Voice Channel".to_string(),
description: None,
nsfw: Some(false),
},
true,
)
.await
.expect("Failed to make new channel");
let create_response = TestHarness::with_session(
session,
harness
.client
.post(format!("/channels/{}/invites", channel.id())),
)
.await;
assert_eq!(create_response.status(), Status::Ok);
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
let invite_code = match invite_from_create {
Invite::Server { code, .. } => code,
_ => unreachable!(),
};
let response = harness
.client
.get(format!("/invites/{}", invite_code))
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
match invite_response {
InviteResponse::Server {
code,
channel_id,
user_name,
..
} => {
assert_eq!(code, invite_code);
assert_eq!(channel_id, channel.id());
assert_eq!(user_name, user.username);
}
_ => unreachable!(),
};
}
}
@@ -0,0 +1,45 @@
use revolt_config::config;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, Category, Database, Role, User
};
use revolt_models::v0;
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Create a category
///
/// Creates a new server category.
#[openapi(tag = "Server Categories")]
#[post("/<target>/categories", data = "<data>")]
pub async fn create(
db: &State<Database>,
user: User,
target: Reference,
data: Json<v0::DataCreateCategory>,
) -> Result<Json<v0::Category>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut server = target.as_server(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
let config = config().await;
if server.categories.len() >= config.features.limits.global.server_categories {
return Err(create_error!(TooManyCategories {
max: config.features.limits.global.server_categories,
}));
};
let category = Category::create(db, &mut server, data).await?;
Ok(Json(category.into()))
}
@@ -0,0 +1,38 @@
use revolt_config::config;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, Category, Database, PartialCategory, Role, User
};
use revolt_models::v0::{self, DataEditCategory};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use validator::Validate;
/// # Edits a category
///
/// Edits a server category.
#[openapi(tag = "Server Categories")]
#[delete("/<server>/categories/<category>")]
pub async fn delete(
db: &State<Database>,
user: User,
server: Reference,
category: String
) -> Result<EmptyResponse> {
let mut server = server.as_server(db).await?;
let category = server.categories
.get(&category)
.ok_or(create_error!(UnknownCategory))?
.clone();
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
category.delete(db, &mut server).await?;
Ok(EmptyResponse)
}
@@ -0,0 +1,107 @@
use revolt_config::config;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, Category, Channel, Database, PartialCategory, PartialChannel, Role, User
};
use revolt_models::v0::{self, DataEditCategory};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Edits a category
///
/// Edits a server category.
#[openapi(tag = "Server Categories")]
#[patch("/<server>/categories/<category>", data = "<data>")]
pub async fn edit(
db: &State<Database>,
user: User,
server: Reference,
category: String,
data: Json<v0::DataEditCategory>,
) -> Result<Json<v0::Category>> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut server = server.as_server(db).await?;
let mut category = server.categories
.get(&category)
.ok_or(create_error!(UnknownCategory))?
.clone();
let mut query = DatabasePermissionQuery::new(db, &user).server(&server).category(&category);
let permissions = calculate_server_permissions(&mut query)
.await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
let DataEditCategory {
title,
mut channels,
remove
} = data;
if channels.is_some() {
permissions.throw_if_lacking_channel_permission(ChannelPermission::MoveChannels)?;
}
// if any channel we are adding to this category are in another category we need to make sure we have MoveChannels in the category before moving it over.
for category in server.categories.values() {
if category.channels.iter().any(|c| channels.as_ref().is_some_and(|cs| cs.contains(c))) {
let mut category_query = query.clone().category(category);
calculate_server_permissions(&mut category_query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::MoveChannels)?;
}
}
// remove the channels from any existing categories to avoid it having two
for category in server.categories.values_mut() {
category.channels.retain(|c| channels.as_ref().is_some_and(|cs| cs.contains(c)));
}
// only keep channels which exist in the server
if let Some(channels) = &mut channels {
channels.retain(|c| server.channels.contains(c));
}
// unset parent from all channels which are removed from the category
for channel_id in &category.channels {
if channels.as_ref().is_some_and(|cs| !cs.contains(channel_id)) {
db.update_channel(channel_id, &PartialChannel { parent: None, ..Default::default() }, Vec::new()).await?;
};
};
// update the category with the new values
category.update(
db,
&mut server,
PartialCategory {
title,
channels: channels.clone(),
..Default::default()
},
remove
.map(|v| v.into_iter().map(Into::into).collect())
.unwrap_or_default()
).await?;
let channels = db.fetch_channels(&channels.unwrap_or_default()).await?;
// update all channels to have the parent set
for channel in channels {
if let Channel::TextChannel { ref parent, .. } | Channel::VoiceChannel { ref parent, .. } = channel {
if parent.as_ref() != Some(&category.id) {
db.update_channel(channel.id(), &PartialChannel { parent: Some(category.id.clone()), ..Default::default() }, Vec::new()).await?;
};
};
};
Ok(Json(category.into()))
}

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