Compare commits

...
16 Commits
Author SHA1 Message Date
Paul Makles a5d0cdf0dd fix(core/database): skip insertion / deletion if no invites need to be corrected 2024-06-04 18:46:11 +01:00
Paul Makles c59aad43c8 chore: bump version to 0.7.2 2024-05-16 15:44:09 +01:00
Paul Makles 19e72babc9 fix: must not use replace_one as it expects no change in _id 2024-05-16 15:43:33 +01:00
Paul Makles bfcdb13d6d chore: remove cla workflow 2024-05-16 15:33:07 +01:00
Zomatree 878c1a83ad fix: continue fix 2024-05-15 22:57:34 +01:00
Zomatree 471e0f55e4 fix: Use correct enum tagging for invites 2024-05-15 22:48:20 +01:00
Paul Makles 08127218ce ci: fix path to pages artifact
ci: ignore changes to documentation for docker/rust builds
2024-05-11 17:50:52 +01:00
Paul Makles b69202c222 ci: add deployment action for documentation 2024-05-11 17:42:50 +01:00
Paul Makles b3494b12fc docs: create mdbook 2024-05-11 17:42:08 +01:00
Paul Makles 7703475868 chore: split sentry dsn into separate options per-app 2024-04-08 23:25:53 +01:00
Paul Makles 4be3bdc4c3 fix: return more appropriate error on token fetch fail 2024-04-08 23:24:49 +01:00
Paul Makles 3e26e7e89d fix: use correct id to lookup user from session 2024-04-08 23:24:33 +01:00
Paul Makles 13b95d383e fix: import std::env in config 2024-04-08 22:46:35 +01:00
Paul Makles 3c76005c33 ci: strip references to quark 2024-04-08 22:00:33 +01:00
Paul Makles 3065e153d5 refactor: delete quark crate (closes #239) 2024-04-08 21:56:35 +01:00
Paul Makles a1eeedabb9 refactor(bonfire): remove quark dependency 2024-04-08 21:49:19 +01:00
174 changed files with 739 additions and 13192 deletions

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
name: Build documentation
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write # To push a branch
pages: write # To push to a GitHub Pages site
id-token: write # To update the deployment status
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install latest mdbook
run: |
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
mkdir mdbook
curl -sSL $url | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
- name: Build Book
run: |
cd doc
mdbook build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "doc/book"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-36
View File
@@ -1,36 +0,0 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
jobs:
CLAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: cla-assistant/github-action@v2.1.3-beta
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN : ${{ secrets.PAT }}
with:
path-to-signatures: 'signatures/version1/cla.json'
path-to-document: 'https://github.com/revoltchat/cla/blob/master/CLA.md' # e.g. a CLA or a DCO document
# branch should not be protected
branch: 'master'
allowlist: insertish,bot*
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
remote-organization-name: revoltchat
remote-repository-name: cla
create-file-commit-message: 'cla(create): creating file for storing CLA Signatures'
signed-commit-message: 'cla(sign): $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
#use-dco-flag: true - If you are using DCO instead of CLA
+1
View File
@@ -10,6 +10,7 @@ on:
- ".github/**" - ".github/**"
- "!.github/workflows/docker.yml" - "!.github/workflows/docker.yml"
- ".vscode/**" - ".vscode/**"
- "doc/**"
- ".gitignore" - ".gitignore"
- "LICENSE" - "LICENSE"
- "README" - "README"
+8
View File
@@ -2,6 +2,14 @@ name: Rust build, test, and generate specification
on: on:
push: push:
paths-ignore:
- ".github/**"
- "!.github/workflows/rust.yaml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request: pull_request:
branches: [main] branches: [main]
Generated
+103 -416
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"] members = ["crates/delta", "crates/bonfire", "crates/core/*"]
[patch.crates-io] [patch.crates-io]
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" } # mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
-1
View File
@@ -19,7 +19,6 @@ RUN sh /tmp/build-image-layer.sh tools
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/ COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/ COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/quark/Cargo.toml ./crates/quark/
COPY crates/core/config/Cargo.toml ./crates/core/config/ COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/ COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/ COPY crates/core/models/Cargo.toml ./crates/core/models/
-1
View File
@@ -15,7 +15,6 @@ COPY scripts/build-image-layer.sh /tmp/
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/ COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/ COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/quark/Cargo.toml ./crates/quark/
COPY crates/core/config/Cargo.toml ./crates/core/config/ COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/ COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/ COPY crates/core/models/Cargo.toml ./crates/core/models/
+10 -11
View File
@@ -2,17 +2,16 @@
This is a monorepo for the Revolt backend. This is a monorepo for the Revolt backend.
| Crate | Path | Description | | Crate | Path | Description |
| ------------------ | -------------------------------------------------- | --------------------------------- | | ------------------ | -------------------------------------------------- | ----------------------------- |
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration | | `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation | | `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models | | `core/models` | [crates/core/models](crates/core/models) | Core: API Models |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | | `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | | `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | | `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |
| `delta` | [crates/delta](crates/delta) | REST API server | | `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | | `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
| `quark` | [crates/quark](crates/quark) | Models and logic (**DEPRECATED**) |
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo. Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
+10 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.7.0" version = "0.7.2"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -9,14 +9,15 @@ edition = "2021"
[dependencies] [dependencies]
# util # util
log = "*" log = "*"
sentry = "0.31.5"
lru = "0.7.6"
ulid = "0.5.0"
once_cell = "1.9.0" once_cell = "1.9.0"
redis-kiss = "0.1.4"
# parsing # parsing
querystring = "1.1.0" querystring = "1.1.0"
# quark
revolt-quark = { path = "../quark" }
# serde # serde
bincode = "1.3.3" bincode = "1.3.3"
serde_json = "1.0.79" serde_json = "1.0.79"
@@ -33,8 +34,12 @@ async-std = { version = "1.8.0", features = [
] } ] }
# core # core
revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.7.2", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
sentry = "0.31.5"
# redis # redis
fred = { version = "8.0.1", features = ["subscriber-client"] } fred = { version = "8.0.1", features = ["subscriber-client"] }
+5 -5
View File
@@ -1,6 +1,6 @@
use async_tungstenite::tungstenite::{handshake, Message}; use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender; use futures::channel::oneshot::Sender;
use revolt_quark::{Error, Result}; use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Enumeration of supported protocol formats /// Enumeration of supported protocol formats
@@ -37,16 +37,16 @@ impl ProtocolConfiguration {
match self.format { match self.format {
ProtocolFormat::Json => { ProtocolFormat::Json => {
if let Message::Text(text) = msg { if let Message::Text(text) = msg {
serde_json::from_str(text).map_err(|_| Error::InternalError) serde_json::from_str(text).map_err(|_| create_error!(InternalError))
} else { } else {
Err(Error::InternalError) Err(create_error!(InternalError))
} }
} }
ProtocolFormat::Msgpack => { ProtocolFormat::Msgpack => {
if let Message::Binary(buf) = msg { if let Message::Binary(buf) = msg {
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError) rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
} else { } else {
Err(Error::InternalError) Err(create_error!(InternalError))
} }
} }
} }
+4 -2
View File
@@ -1,5 +1,5 @@
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use revolt_quark::{Database, DatabaseInfo}; use revolt_database::{Database, DatabaseInfo};
static DBCONN: OnceCell<Database> = OnceCell::new(); static DBCONN: OnceCell<Database> = OnceCell::new();
@@ -10,7 +10,9 @@ pub async fn connect() {
.await .await
.expect("Failed to connect to the database."); .expect("Failed to connect to the database.");
DBCONN.set(database).expect("Setting `Database`"); if DBCONN.set(database).is_err() {
panic!("couldn't set database")
}
} }
/// Get a reference to the current database. /// Get a reference to the current database.
@@ -1,21 +1,15 @@
use std::collections::HashSet; use std::collections::HashSet;
use crate::{ use revolt_database::{
get_relationship, events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
models::{ MemberCompositeKey, Presence, RelationshipStatus,
server_member::FieldsMember,
user::{PartialUser, Presence, RelationshipStatus},
Channel, Member, User,
},
perms, Database, Permission, Result,
}; };
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_presence::filter_online; use revolt_presence::filter_online;
use revolt_result::Result;
use super::{ use super::state::{Cache, State};
client::EventV1,
state::{Cache, State},
};
/// Cache Manager /// Cache Manager
impl Cache { impl Cache {
@@ -25,20 +19,22 @@ impl Cache {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
let member = self.members.get(server); let member = self.members.get(server);
let server = self.servers.get(server); let server = self.servers.get(server);
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel); let mut query =
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
.channel(&channel);
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
if let Some(member) = member { if let Some(member) = member {
perms.member.set_ref(member); query = query.member(&member);
} }
if let Some(server) = server { if let Some(server) = server {
perms.server.set_ref(server); query = query.server(server);
} }
perms calculate_channel_permissions(&mut query)
.has_permission(db, Permission::ViewChannel)
.await .await
.unwrap_or_default() .has_channel_permission(ChannelPermission::ViewChannel)
} }
_ => true, _ => true,
} }
@@ -63,7 +59,7 @@ impl Cache {
/// Check whether we can subscribe to another user /// Check whether we can subscribe to another user
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool { pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
if let Some(user) = self.users.get(&self.user_id) { if let Some(user) = self.users.get(&self.user_id) {
match get_relationship(user, user_id) { match user.relationship_with(user_id) {
RelationshipStatus::Friend RelationshipStatus::Friend
| RelationshipStatus::Incoming | RelationshipStatus::Incoming
| RelationshipStatus::Outgoing | RelationshipStatus::Outgoing
@@ -95,7 +91,7 @@ impl Cache {
impl State { impl State {
/// Generate a Ready packet for the current user /// Generate a Ready packet for the current user
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> { pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
let mut user = self.clone_user(); let user = self.clone_user();
// Find all relationships to the user. // Find all relationships to the user.
let mut user_ids: HashSet<String> = user let mut user_ids: HashSet<String> = user
@@ -141,7 +137,6 @@ impl State {
// Fetch presence data for known users. // Fetch presence data for known users.
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await; let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
user.online = Some(true);
// Fetch user data. // Fetch user data.
let users = db let users = db
@@ -154,15 +149,14 @@ impl State {
.await?; .await?;
// Fetch customisations. // Fetch customisations.
let emojis = Some( let emojis = db
db.fetch_emoji_by_parent_ids( .fetch_emoji_by_parent_ids(
&servers &servers
.iter() .iter()
.map(|x| x.id.to_string()) .map(|x| x.id.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.await?, .await?;
);
// Copy data into local state cache. // Copy data into local state cache.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect(); self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
@@ -176,17 +170,16 @@ impl State {
.collect(); .collect();
// Make all users appear from our perspective. // Make all users appear from our perspective.
let mut users: Vec<User> = users let mut users: Vec<v0::User> = users
.into_iter() .into_iter()
.map(|mut x| { .map(|other_user| {
x.online = Some(online_ids.contains(&x.id)); let is_online = online_ids.contains(&other_user.id);
x.with_relationship(&user) other_user.into_known(&user, is_online)
}) })
.collect(); .collect();
// Make sure we see our own user correctly. // Make sure we see our own user correctly.
user.relationship = Some(RelationshipStatus::User); users.push(user.into_self().await);
users.push(user.foreign());
// Set subscription state internally. // Set subscription state internally.
self.reset_state(); self.reset_state();
@@ -206,10 +199,10 @@ impl State {
Ok(EventV1::Ready { Ok(EventV1::Ready {
users, users,
servers, servers: servers.into_iter().map(Into::into).collect(),
channels, channels: channels.into_iter().map(Into::into).collect(),
members, members: members.into_iter().map(Into::into).collect(),
emojis, emojis: emojis.into_iter().map(Into::into).collect(),
}) })
} }
@@ -271,7 +264,7 @@ impl State {
.insert(channel.id().to_string(), channel.clone()); .insert(channel.id().to_string(), channel.clone());
self.insert_subscription(channel.id().to_string()); self.insert_subscription(channel.id().to_string());
bulk_events.push(EventV1::ChannelCreate(channel)); bulk_events.push(EventV1::ChannelCreate(channel.into()));
} }
} }
} }
@@ -296,7 +289,7 @@ impl State {
} { } {
let event = EventV1::UserUpdate { let event = EventV1::UserUpdate {
id: self.cache.user_id.clone(), id: self.cache.user_id.clone(),
data: PartialUser { data: v0::PartialUser {
online: Some(target), online: Some(target),
..Default::default() ..Default::default()
}, },
@@ -344,7 +337,7 @@ impl State {
EventV1::ChannelCreate(channel) => { EventV1::ChannelCreate(channel) => {
let id = channel.id().to_string(); let id = channel.id().to_string();
self.insert_subscription(id.clone()); self.insert_subscription(id.clone());
self.cache.channels.insert(id, channel.clone()); self.cache.channels.insert(id, channel.clone().into());
} }
EventV1::ChannelUpdate { EventV1::ChannelUpdate {
id, data, clear, .. id, data, clear, ..
@@ -357,10 +350,10 @@ impl State {
if let Some(channel) = self.cache.channels.get_mut(id) { if let Some(channel) = self.cache.channels.get_mut(id) {
for field in clear { for field in clear {
channel.remove(field); channel.remove_field(&field.clone().into());
} }
channel.apply_options(data.clone()); channel.apply_options(data.clone().into());
} }
if !self.cache.channels.contains_key(id) { if !self.cache.channels.contains_key(id) {
@@ -374,7 +367,7 @@ impl State {
if could_view != can_view { if could_view != can_view {
if can_view { if can_view {
queue_add = Some(id.clone()); queue_add = Some(id.clone());
*event = EventV1::ChannelCreate(channel.clone()); *event = EventV1::ChannelCreate(channel.clone().into());
} else { } else {
queue_remove = Some(id.clone()); queue_remove = Some(id.clone());
*event = EventV1::ChannelDelete { id: id.clone() }; *event = EventV1::ChannelDelete { id: id.clone() };
@@ -404,14 +397,20 @@ impl State {
emojis: _, emojis: _,
} => { } => {
self.insert_subscription(id.clone()); self.insert_subscription(id.clone());
self.cache.servers.insert(id.clone(), server.clone()); self.cache.servers.insert(id.clone(), server.clone().into());
let member = Member::new(id.clone(), self.cache.user_id.clone()); let member = Member {
id: MemberCompositeKey {
server: server.id.clone(),
user: self.cache.user_id.clone(),
},
..Default::default()
};
self.cache.members.insert(id.clone(), member); self.cache.members.insert(id.clone(), member);
for channel in channels { for channel in channels {
self.cache self.cache
.channels .channels
.insert(channel.id().to_string(), channel.clone()); .insert(channel.id().to_string(), channel.clone().into());
} }
queue_server = Some(id.clone()); queue_server = Some(id.clone());
@@ -421,10 +420,10 @@ impl State {
} => { } => {
if let Some(server) = self.cache.servers.get_mut(id) { if let Some(server) = self.cache.servers.get_mut(id) {
for field in clear { for field in clear {
server.remove(field); server.remove_field(&field.clone().into());
} }
server.apply_options(data.clone()); server.apply_options(data.clone().into());
} }
if data.default_permissions.is_some() { if data.default_permissions.is_some() {
@@ -462,13 +461,13 @@ impl State {
if id.user == self.cache.user_id { if id.user == self.cache.user_id {
if let Some(member) = self.cache.members.get_mut(&id.server) { if let Some(member) = self.cache.members.get_mut(&id.server) {
for field in &clear.clone() { for field in &clear.clone() {
member.remove(field); member.remove_field(&field.clone().into());
} }
member.apply_options(data.clone()); member.apply_options(data.clone().into());
} }
if data.roles.is_some() || clear.contains(&FieldsMember::Roles) { if data.roles.is_some() || clear.contains(&v0::FieldsMember::Roles) {
queue_server = Some(id.server.clone()); queue_server = Some(id.server.clone());
} }
} }
@@ -483,10 +482,10 @@ impl State {
if let Some(server) = self.cache.servers.get_mut(id) { if let Some(server) = self.cache.servers.get_mut(id) {
if let Some(role) = server.roles.get_mut(role_id) { if let Some(role) = server.roles.get_mut(role_id) {
for field in &clear.clone() { for field in &clear.clone() {
role.remove(field); role.remove_field(&field.clone().into());
} }
role.apply_options(data.clone()); role.apply_options(data.clone().into());
} }
} }
@@ -522,7 +521,7 @@ impl State {
*event_id = None; *event_id = None;
} }
EventV1::UserRelationship { id, user, .. } => { EventV1::UserRelationship { id, user, .. } => {
self.cache.users.insert(id.clone(), user.clone()); self.cache.users.insert(id.clone(), user.clone().into());
if self.cache.can_subscribe_to_user(id) { if self.cache.can_subscribe_to_user(id) {
self.insert_subscription(id.clone()); self.insert_subscription(id.clone());
@@ -551,39 +550,3 @@ impl State {
true true
} }
} }
impl EventV1 {
/// Publish helper wrapper
pub async fn p(self, channel: String) {
#[cfg(not(debug_assertions))]
redis_kiss::p(channel, self).await;
#[cfg(debug_assertions)]
info!("Publishing event to {channel}: {self:?}");
#[cfg(debug_assertions)]
redis_kiss::publish(channel, self).await.unwrap();
}
/// Publish user event
pub async fn p_user(self, id: String, db: &Database) {
self.clone().p(id.clone()).await;
// TODO: this should be captured by member list in the future and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members {
self.clone().p(member.id.server).await;
}
}
}
/// Publish private event
pub async fn private(self, id: String) {
self.p(format!("{id}!")).await;
}
/// Publish internal global event
pub async fn global(self) {
self.p("global".to_string()).await;
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod r#impl;
pub mod state;
@@ -1,8 +1,7 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use lru::LruCache; use lru::LruCache;
use revolt_database::{Channel, Member, Server, User};
use crate::models::{Channel, Member, Server, User};
/// Enumeration representing some change in subscriptions /// Enumeration representing some change in subscriptions
pub enum SubscriptionStateChange { pub enum SubscriptionStateChange {
+2 -1
View File
@@ -7,6 +7,7 @@ use revolt_presence::clear_region;
extern crate log; extern crate log;
pub mod config; pub mod config;
pub mod events;
mod database; mod database;
mod websocket; mod websocket;
@@ -14,7 +15,7 @@ mod websocket;
#[async_std::main] #[async_std::main]
async fn main() { async fn main() {
// Configure requirements for Bonfire. // Configure requirements for Bonfire.
revolt_quark::configure!(); revolt_config::configure!(events);
database::connect().await; database::connect().await;
// Clean up the current region information. // Clean up the current region information.
+6 -10
View File
@@ -11,21 +11,17 @@ use futures::{
stream::{SplitSink, SplitStream}, stream::{SplitSink, SplitStream},
FutureExt, SinkExt, StreamExt, TryStreamExt, FutureExt, SinkExt, StreamExt, TryStreamExt,
}; };
use revolt_presence::{create_session, delete_session}; use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_quark::{ use revolt_database::{
events::{ events::{client::EventV1, server::ClientMessage},
client::EventV1, Database, User, UserHint,
server::ClientMessage,
state::{State, SubscriptionStateChange},
},
models::{user::UserHint, User},
redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI},
Database,
}; };
use revolt_presence::{create_session, delete_session};
use async_std::{net::TcpStream, sync::Mutex}; use async_std::{net::TcpStream, sync::Mutex};
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback}; use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
use crate::events::state::{State, SubscriptionStateChange};
type WsReader = SplitStream<WebSocketStream<TcpStream>>; type WsReader = SplitStream<WebSocketStream<TcpStream>>;
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>; type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.7.0" version = "0.7.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+4 -3
View File
@@ -1,5 +1,3 @@
sentry_dsn = ""
[database] [database]
mongodb = "mongodb://database" mongodb = "mongodb://database"
redis = "redis://redis/" redis = "redis://redis/"
@@ -14,7 +12,6 @@ voso_legacy = ""
voso_legacy_ws = "" voso_legacy_ws = ""
[api] [api]
staging = false
[api.registration] [api.registration]
invite_only = false invite_only = false
@@ -68,3 +65,7 @@ background_size = 6000000
icon_size = 2500000 icon_size = 2500000
banner_size = 6000000 banner_size = 6000000
emoji_size = 500000 emoji_size = 500000
[sentry]
api = ""
events = ""
+19 -12
View File
@@ -6,6 +6,9 @@ use futures_locks::RwLock;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::Deserialize; use serde::Deserialize;
#[cfg(not(debug_assertions))]
use std::env;
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| { static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
RwLock::new({ RwLock::new({
let mut builder = Config::builder().add_source(File::from_str( let mut builder = Config::builder().add_source(File::from_str(
@@ -93,7 +96,6 @@ pub struct ApiWorkers {
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct Api { pub struct Api {
pub staging: bool,
pub registration: ApiRegistration, pub registration: ApiRegistration,
pub smtp: ApiSmtp, pub smtp: ApiSmtp,
pub vapid: ApiVapid, pub vapid: ApiVapid,
@@ -138,13 +140,19 @@ pub struct Features {
pub webhooks_enabled: bool, pub webhooks_enabled: bool,
} }
#[derive(Deserialize, Debug, Clone)]
pub struct Sentry {
pub api: String,
pub events: String,
}
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct Settings { pub struct Settings {
pub database: Database, pub database: Database,
pub hosts: Hosts, pub hosts: Hosts,
pub api: Api, pub api: Api,
pub features: Features, pub features: Features,
pub sentry_dsn: String, pub sentry: Sentry,
} }
impl Settings { impl Settings {
@@ -188,7 +196,7 @@ pub async fn config() -> Settings {
} }
/// Configure logging and common Rust variables /// Configure logging and common Rust variables
pub async fn setup_logging(release: &'static str) -> Option<sentry::ClientInitGuard> { pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
if std::env::var("RUST_LOG").is_err() { if std::env::var("RUST_LOG").is_err() {
@@ -202,12 +210,11 @@ pub async fn setup_logging(release: &'static str) -> Option<sentry::ClientInitGu
pretty_env_logger::init(); pretty_env_logger::init();
log::info!("Starting {release}"); log::info!("Starting {release}");
let config = config().await; if dsn.is_empty() {
if config.sentry_dsn.is_empty() {
None None
} else { } else {
Some(sentry::init(( Some(sentry::init((
config.sentry_dsn, dsn,
sentry::ClientOptions { sentry::ClientOptions {
release: Some(release.into()), release: Some(release.into()),
..Default::default() ..Default::default()
@@ -218,12 +225,12 @@ pub async fn setup_logging(release: &'static str) -> Option<sentry::ClientInitGu
#[macro_export] #[macro_export]
macro_rules! configure { macro_rules! configure {
() => { ($application: ident) => {
let _sentry = $crate::setup_logging(concat!( let config = $crate::config().await;
env!("CARGO_PKG_NAME"), let _sentry = $crate::setup_logging(
"@", concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")),
env!("CARGO_PKG_VERSION") config.sentry.$application,
)) )
.await; .await;
}; };
} }
+6 -6
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.7.0" version = "0.7.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.7.0", path = "../config" } revolt-config = { version = "0.7.2", path = "../config" }
revolt-result = { version = "0.7.0", path = "../result" } revolt-result = { version = "0.7.2", path = "../result" }
revolt-models = { version = "0.7.0", path = "../models", features = [ revolt-models = { version = "0.7.2", path = "../models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.7.0", path = "../presence" } revolt-presence = { version = "0.7.2", path = "../presence" }
revolt-permissions = { version = "0.7.0", path = "../permissions", features = [ revolt-permissions = { version = "0.7.2", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
+4 -4
View File
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use revolt_models::v0::{ use revolt_models::v0::{
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer, AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User, PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
UserSettings, Webhook, UserSettings, Webhook,
}; };
@@ -48,17 +48,17 @@ pub enum EventV1 {
/// Successfully authenticated /// Successfully authenticated
Authenticated, Authenticated,
/* /// Basic data to cache /// Basic data to cache
Ready { Ready {
users: Vec<User>, users: Vec<User>,
servers: Vec<Server>, servers: Vec<Server>,
channels: Vec<Channel>, channels: Vec<Channel>,
members: Vec<Member>, members: Vec<Member>,
emojis: Option<Vec<Emoji>>, emojis: Vec<Emoji>,
}, },
/// Ping response /// Ping response
Pong { data: Ping }, */ Pong { data: Ping },
/// New message /// New message
Message(Message), Message(Message),
+1
View File
@@ -1 +1,2 @@
pub mod client; pub mod client;
pub mod server;
File renamed without changes.
@@ -5,8 +5,9 @@ use crate::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document}, bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions, options::FindOptions,
}, },
MongoDb, DISCRIMINATOR_SEARCH_SPACE, Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
}; };
use bson::oid::ObjectId;
use futures::StreamExt; use futures::StreamExt;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS; use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
@@ -19,7 +20,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 26; pub const LATEST_REVISION: i32 = 27;
pub async fn migrate_database(db: &MongoDb) { pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations"); let migrations = db.col::<Document>("migrations");
@@ -983,6 +984,93 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to create ratelimit_events index."); .expect("Failed to create ratelimit_events index.");
} }
if revision <= 26 {
info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging.");
auto_derived!(
pub enum OldInvite {
Server {
#[serde(rename = "_id")]
code: String,
server: String,
creator: String,
channel: String,
},
Group {
#[serde(rename = "_id")]
code: String,
creator: String,
channel: String,
},
}
);
#[derive(serde::Serialize, serde::Deserialize)]
struct Outer {
_id: ObjectId,
#[serde(flatten)]
invite: OldInvite,
}
let invites = db
.db()
.collection::<Outer>("channel_invites")
.find(
doc! {
"type": { "$exists": false }
},
None,
)
.await
.expect("failed to find invites")
.filter_map(|s| async { s.ok() })
.collect::<Vec<Outer>>()
.await
.into_iter()
.map(|invite| match invite.invite {
OldInvite::Server {
code,
server,
creator,
channel,
} => Invite::Server {
code,
server,
creator,
channel,
},
OldInvite::Group {
code,
creator,
channel,
} => Invite::Group {
code,
creator,
channel,
},
})
.collect::<Vec<Invite>>();
if !invites.is_empty() {
db.db()
.collection("channel_invites")
.insert_many(invites, None)
.await
.expect("failed to insert corrected invite");
db.db()
.collection::<Outer>("channel_invites")
.delete_many(
doc! {
"type": { "$exists": false }
},
None,
)
.await
.expect("failed to find invites");
}
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`. // Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.
@@ -10,6 +10,7 @@ static ALPHABET: [char; 54] = [
auto_derived!( auto_derived!(
/// Invite /// Invite
#[serde(tag = "type")]
pub enum Invite { pub enum Invite {
/// Invite to a specific server channel /// Invite to a specific server channel
Server { Server {
@@ -122,6 +122,16 @@ auto_derived!(
/// Id of the owner of this bot /// Id of the owner of this bot
pub owner: String, pub owner: String,
} }
/// Enumeration providing a hint to the type of user we are handling
pub enum UserHint {
/// Could be either a user or a bot
Any,
/// Only match bots
Bot,
/// Only match users
User,
}
); );
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| { pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
@@ -266,6 +276,30 @@ impl User {
Ok(username) Ok(username)
} }
/// Find a user from a given token and hint
#[async_recursion]
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> {
match hint {
UserHint::Bot => {
db.fetch_user(
&db.fetch_bot_by_token(token)
.await
.map_err(|_| create_error!(InvalidSession))?
.id,
)
.await
}
UserHint::User => db.fetch_user_by_token(token).await,
UserHint::Any => {
if let Ok(user) = User::from_token(db, token, UserHint::User).await {
Ok(user)
} else {
User::from_token(db, token, UserHint::Bot).await
}
}
}
}
/// Helper function to fetch many users as a mutually connected user /// Helper function to fetch many users as a mutually connected user
/// (while optimising the online ID query) /// (while optimising the online ID query)
pub async fn fetch_many_ids_as_mutuals( pub async fn fetch_many_ids_as_mutuals(
@@ -60,7 +60,7 @@ impl AbstractUsers for MongoDb {
.map_err(|_| create_database_error!("find_one", "sessions"))? .map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession))?; .ok_or_else(|| create_error!(InvalidSession))?;
self.fetch_user(&session.id).await self.fetch_user(&session.user_id).await
} }
/// Fetch multiple users by their ids /// Fetch multiple users by their ids
+292
View File
@@ -220,6 +220,86 @@ impl From<crate::Channel> for Channel {
} }
} }
impl From<Channel> for crate::Channel {
fn from(value: Channel) -> crate::Channel {
match value {
Channel::SavedMessages { id, user } => crate::Channel::SavedMessages { id, user },
Channel::DirectMessage {
id,
active,
recipients,
last_message_id,
} => crate::Channel::DirectMessage {
id,
active,
recipients,
last_message_id,
},
Channel::Group {
id,
name,
owner,
description,
recipients,
icon,
last_message_id,
permissions,
nsfw,
} => crate::Channel::Group {
id,
name,
owner,
description,
recipients,
icon: icon.map(|file| file.into()),
last_message_id,
permissions,
nsfw,
},
Channel::TextChannel {
id,
server,
name,
description,
icon,
last_message_id,
default_permissions,
role_permissions,
nsfw,
} => crate::Channel::TextChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
last_message_id,
default_permissions,
role_permissions,
nsfw,
},
Channel::VoiceChannel {
id,
server,
name,
description,
icon,
default_permissions,
role_permissions,
nsfw,
} => crate::Channel::VoiceChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
nsfw,
},
}
}
}
impl From<crate::PartialChannel> for PartialChannel { impl From<crate::PartialChannel> for PartialChannel {
fn from(value: crate::PartialChannel) -> Self { fn from(value: crate::PartialChannel) -> Self {
PartialChannel { PartialChannel {
@@ -237,6 +317,23 @@ impl From<crate::PartialChannel> for PartialChannel {
} }
} }
impl From<PartialChannel> for crate::PartialChannel {
fn from(value: PartialChannel) -> crate::PartialChannel {
crate::PartialChannel {
name: value.name,
owner: value.owner,
description: value.description,
icon: value.icon.map(|file| file.into()),
nsfw: value.nsfw,
active: value.active,
permissions: value.permissions,
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
}
}
}
impl From<FieldsChannel> for crate::FieldsChannel { impl From<FieldsChannel> for crate::FieldsChannel {
fn from(value: FieldsChannel) -> Self { fn from(value: FieldsChannel) -> Self {
match value { match value {
@@ -307,6 +404,25 @@ impl From<crate::File> for File {
} }
} }
impl From<File> for crate::File {
fn from(value: File) -> crate::File {
crate::File {
id: value.id,
tag: value.tag,
filename: value.filename,
metadata: value.metadata.into(),
content_type: value.content_type,
size: value.size,
deleted: value.deleted,
reported: value.reported,
message_id: value.message_id,
user_id: value.user_id,
server_id: value.server_id,
object_id: value.object_id,
}
}
}
impl From<crate::Metadata> for Metadata { impl From<crate::Metadata> for Metadata {
fn from(value: crate::Metadata) -> Self { fn from(value: crate::Metadata) -> Self {
match value { match value {
@@ -325,6 +441,24 @@ impl From<crate::Metadata> for Metadata {
} }
} }
impl From<Metadata> for crate::Metadata {
fn from(value: Metadata) -> crate::Metadata {
match value {
Metadata::File => crate::Metadata::File,
Metadata::Text => crate::Metadata::Text,
Metadata::Image { width, height } => crate::Metadata::Image {
width: width as isize,
height: height as isize,
},
Metadata::Video { width, height } => crate::Metadata::Video {
width: width as isize,
height: height as isize,
},
Metadata::Audio => crate::Metadata::Audio,
}
}
}
impl From<crate::Message> for Message { impl From<crate::Message> for Message {
fn from(value: crate::Message) -> Self { fn from(value: crate::Message) -> Self {
Message { Message {
@@ -480,6 +614,19 @@ impl From<crate::Member> for Member {
} }
} }
impl From<Member> for crate::Member {
fn from(value: Member) -> crate::Member {
crate::Member {
id: value.id.into(),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::PartialMember> for PartialMember { impl From<crate::PartialMember> for PartialMember {
fn from(value: crate::PartialMember) -> Self { fn from(value: crate::PartialMember) -> Self {
PartialMember { PartialMember {
@@ -493,6 +640,19 @@ impl From<crate::PartialMember> for PartialMember {
} }
} }
impl From<PartialMember> for crate::PartialMember {
fn from(value: PartialMember) -> crate::PartialMember {
crate::PartialMember {
id: value.id.map(|id| id.into()),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::MemberCompositeKey> for MemberCompositeKey { impl From<crate::MemberCompositeKey> for MemberCompositeKey {
fn from(value: crate::MemberCompositeKey) -> Self { fn from(value: crate::MemberCompositeKey) -> Self {
MemberCompositeKey { MemberCompositeKey {
@@ -502,6 +662,15 @@ impl From<crate::MemberCompositeKey> for MemberCompositeKey {
} }
} }
impl From<MemberCompositeKey> for crate::MemberCompositeKey {
fn from(value: MemberCompositeKey) -> crate::MemberCompositeKey {
crate::MemberCompositeKey {
server: value.server,
user: value.user,
}
}
}
impl From<crate::FieldsMember> for FieldsMember { impl From<crate::FieldsMember> for FieldsMember {
fn from(value: crate::FieldsMember) -> Self { fn from(value: crate::FieldsMember) -> Self {
match value { match value {
@@ -562,6 +731,34 @@ impl From<crate::Server> for Server {
} }
} }
impl From<Server> for crate::Server {
fn from(value: Server) -> crate::Server {
crate::Server {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: Some(value.flags as i32),
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::PartialServer> for PartialServer { impl From<crate::PartialServer> for PartialServer {
fn from(value: crate::PartialServer) -> Self { fn from(value: crate::PartialServer) -> Self {
PartialServer { PartialServer {
@@ -588,6 +785,32 @@ impl From<crate::PartialServer> for PartialServer {
} }
} }
impl From<PartialServer> for crate::PartialServer {
fn from(value: PartialServer) -> crate::PartialServer {
crate::PartialServer {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.map(|roles| roles.into_iter().map(|(k, v)| (k, v.into())).collect()),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.map(|v| v as i32),
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::FieldsServer> for FieldsServer { impl From<crate::FieldsServer> for FieldsServer {
fn from(value: crate::FieldsServer) -> Self { fn from(value: crate::FieldsServer) -> Self {
match value { match value {
@@ -666,6 +889,18 @@ impl From<crate::Role> for Role {
} }
} }
impl From<Role> for crate::Role {
fn from(value: Role) -> crate::Role {
crate::Role {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::PartialRole> for PartialRole { impl From<crate::PartialRole> for PartialRole {
fn from(value: crate::PartialRole) -> Self { fn from(value: crate::PartialRole) -> Self {
PartialRole { PartialRole {
@@ -678,6 +913,18 @@ impl From<crate::PartialRole> for PartialRole {
} }
} }
impl From<PartialRole> for crate::PartialRole {
fn from(value: PartialRole) -> crate::PartialRole {
crate::PartialRole {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::FieldsRole> for FieldsRole { impl From<crate::FieldsRole> for FieldsRole {
fn from(value: crate::FieldsRole) -> Self { fn from(value: crate::FieldsRole) -> Self {
match value { match value {
@@ -871,6 +1118,25 @@ impl crate::User {
} }
} }
impl From<User> for crate::User {
fn from(value: User) -> crate::User {
crate::User {
id: value.id,
username: value.username,
discriminator: value.discriminator,
display_name: value.display_name,
avatar: value.avatar.map(Into::into),
relations: None,
badges: Some(value.badges as i32),
status: value.status.map(Into::into),
profile: value.profile.map(Into::into),
flags: Some(value.flags as i32),
privileged: value.privileged,
bot: value.bot.map(Into::into),
}
}
}
impl From<crate::PartialUser> for PartialUser { impl From<crate::PartialUser> for PartialUser {
fn from(value: crate::PartialUser) -> Self { fn from(value: crate::PartialUser) -> Self {
PartialUser { PartialUser {
@@ -977,6 +1243,15 @@ impl From<crate::UserStatus> for UserStatus {
} }
} }
impl From<UserStatus> for crate::UserStatus {
fn from(value: UserStatus) -> crate::UserStatus {
crate::UserStatus {
text: value.text,
presence: value.presence.map(|presence| presence.into()),
}
}
}
impl From<crate::UserProfile> for UserProfile { impl From<crate::UserProfile> for UserProfile {
fn from(value: crate::UserProfile) -> Self { fn from(value: crate::UserProfile) -> Self {
UserProfile { UserProfile {
@@ -986,6 +1261,15 @@ impl From<crate::UserProfile> for UserProfile {
} }
} }
impl From<UserProfile> for crate::UserProfile {
fn from(value: UserProfile) -> crate::UserProfile {
crate::UserProfile {
content: value.content,
background: value.background.map(|file| file.into()),
}
}
}
impl From<crate::BotInformation> for BotInformation { impl From<crate::BotInformation> for BotInformation {
fn from(value: crate::BotInformation) -> Self { fn from(value: crate::BotInformation) -> Self {
BotInformation { BotInformation {
@@ -993,3 +1277,11 @@ impl From<crate::BotInformation> for BotInformation {
} }
} }
} }
impl From<BotInformation> for crate::BotInformation {
fn from(value: BotInformation) -> crate::BotInformation {
crate::BotInformation {
owner: value.owner_id,
}
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.7.0" version = "0.7.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.7.0", path = "../config" } revolt-config = { version = "0.7.2", path = "../config" }
revolt-permissions = { version = "0.7.0", path = "../permissions" } revolt-permissions = { version = "0.7.2", path = "../permissions" }
# Utility # Utility
regex = "1" regex = "1"
+10 -1
View File
@@ -30,7 +30,16 @@ macro_rules! auto_derived_partial {
#[derive( #[derive(
OptionalStruct, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, OptionalStruct, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema,
)] )]
#[optional_derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] #[optional_derive(
Debug,
Clone,
Eq,
PartialEq,
Serialize,
Deserialize,
JsonSchema,
Default
)]
#[optional_name = $name] #[optional_name = $name]
#[opt_skip_serializing_none] #[opt_skip_serializing_none]
#[opt_some_priority] #[opt_some_priority]
@@ -2,6 +2,7 @@ use super::{Channel, File, Server, User};
auto_derived!( auto_derived!(
/// Invite /// Invite
#[serde(tag = "type")]
pub enum Invite { pub enum Invite {
/// Invite to a specific server channel /// Invite to a specific server channel
Server { Server {
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.7.0" version = "0.7.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.7.0", path = "../result" } revolt-result = { version = "0.7.2", path = "../result" }
# Utility # Utility
auto_ops = "0.3.0" auto_ops = "0.3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.7.0" version = "0.7.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.7.0" version = "0.7.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -12,7 +12,7 @@ description = "Revolt Backend: Result and Error types"
serde = ["dep:serde"] serde = ["dep:serde"]
schemas = ["dep:schemars"] schemas = ["dep:schemars"]
rocket = ["dep:rocket", "dep:serde_json"] rocket = ["dep:rocket", "dep:serde_json"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi"] okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
default = ["serde"] default = ["serde"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.7.0" version = "0.7.2"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
+1 -1
View File
@@ -194,7 +194,7 @@ pub async fn authifier_config() -> AuthifierConfig {
#[launch] #[launch]
async fn rocket() -> _ { async fn rocket() -> _ {
// Configure logging and environment // Configure logging and environment
revolt_config::configure!(); revolt_config::configure!(api);
// Start web server // Start web server
web().await web().await
-1
View File
@@ -101,7 +101,6 @@ impl TestHarness {
let mut stream = self.sub.on_message(); let mut stream = self.sub.on_message();
while let Some(item) = stream.next().await { while let Some(item) = stream.next().await {
let item = item.unwrap();
let msg_topic = item.get_channel_name(); let msg_topic = item.get_channel_name();
let payload: EventV1 = redis_kiss::decode_payload(&item).unwrap(); let payload: EventV1 = redis_kiss::decode_payload(&item).unwrap();
-14
View File
@@ -1,14 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# Database
.minio
.data*
-100
View File
@@ -1,100 +0,0 @@
[package]
name = "revolt-quark"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
mongo = ["mongodb"]
rocket_impl = [
"rocket",
"rocket_empty",
"rocket_cors",
"lru",
"dashmap",
"authifier/database-mongodb",
"authifier/rocket_impl",
"authifier/okapi_impl",
]
test = ["async-std", "mongo", "mongodb/async-std-runtime", "rocket_impl"]
default = ["test"]
[dependencies]
# Serialisation
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] }
validator = { version = "0.16", features = ["derive"] }
iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] }
# Formats
bincode = "1.3.3"
serde_json = "1.0.78"
bson = { version = "2.1.0", features = ["chrono-0_4"] }
# Spec Generation
schemars = "0.8.8"
revolt_okapi = "0.9.1"
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
# Databases
redis-kiss = { version = "0.1.4" }
mongodb = { optional = true, version = "2.1.0", default-features = false }
# Async
futures = "0.3.19"
deadqueue = "0.2.1"
async-trait = "0.1.51"
async-recursion = "1.0.0"
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Logging
log = "0.4.14"
pretty_env_logger = "0.4.0"
# Util
rand = "0.8.5"
ulid = "0.5.0"
regex = "1.5.5"
nanoid = "0.4.0"
linkify = "0.8.1"
dotenv = "0.15.0"
indexmap = "1.9.1"
decancer = "1.6.2"
impl_ops = "0.1.1"
num_enum = "0.5.6"
reqwest = "0.11.10"
bitfield = "0.13.2"
once_cell = "1.17.1"
async-lock = "2.6.0"
lru = { version = "0.7.6", optional = true }
dashmap = { version = "5.2.0", optional = true }
# Web Push
base64 = "0.13.0"
web-push = "0.7.2"
# Implementations
rocket_http = { optional = true, version = "0.5.0-rc.2" }
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false, features = [
"json",
] }
rocket_empty = { version = "0.1.1", optional = true, features = ["schema"] }
rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" }
# Authifier
authifier = { version = "1.0.8", features = ["async-std-runtime"] }
# Sentry
sentry = "0.31.5"
# Core
revolt-result = { path = "../core/result", features = ["serde", "schemas"] }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
revolt-database = { path = "../core/database" }
revolt-models = { path = "../core/models" }
-1
View File
@@ -1 +0,0 @@
../../LICENSE
File diff suppressed because it is too large. Load diff
-138
View File
@@ -1,138 +0,0 @@
<!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">
<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}}
</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">
<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%;">
<tbody>
<tr>
<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;">
<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%">
<tbody>
<tr>
<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;">
<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>
</tr>
</tbody>
</table>
<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">
&#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">
<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%">
<tbody>
<tr>
<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>
<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">
&#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;">
<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>
</tr>
</tbody>
</table>
</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>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<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>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -1,30 +0,0 @@
<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">Account Deletion</h1>
<p>
You requested to have your account deleted, if you did not perform
this action please take measures to secure your account immediately.
</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
</div>
</div>
</body>
</html>
@@ -1,7 +0,0 @@
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}}
Sent by Revolt
Made in the EU
-138
View File
@@ -1,138 +0,0 @@
<!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">
<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}}
</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">
<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%;">
<tbody>
<tr>
<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;">
<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%">
<tbody>
<tr>
<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;">
<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>
</tr>
</tbody>
</table>
<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">
&#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">
<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%">
<tbody>
<tr>
<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%">
<tbody>
<tr>
<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;">
<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>
</tr>
</tbody>
</table>
</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>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<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>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -1,26 +0,0 @@
<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
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">Password Reset</h1>
<p>You requested a password reset, click below to continue.</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Reset</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
</div>
</div>
</body>
</html>
-7
View File
@@ -1,7 +0,0 @@
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}}
Sent by Revolt
Made in the EU
-138
View File
@@ -1,138 +0,0 @@
<!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">
<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}}
</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">
<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%;">
<tbody>
<tr>
<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;">
<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%">
<tbody>
<tr>
<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;">
<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>
</tr>
</tbody>
</table>
<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">
&#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">
<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%">
<tbody>
<tr>
<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%">
<tbody>
<tr>
<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;">
<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>
</tr>
</tbody>
</table>
</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>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<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>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -1,27 +0,0 @@
<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">Almost there!</h1>
<p>To complete your sign up, we just need to verify your email.</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
</div>
</div>
</body>
</html>
-8
View File
@@ -1,8 +0,0 @@
Almost there!
To complete your sign up, we just need to verify your email.
Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Revolt
Made in the EU
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

-85
View File
@@ -1,85 +0,0 @@
use std::env;
use std::ops::Deref;
use crate::r#impl::{DummyDb, MongoDb};
use crate::AbstractDatabase;
/// Database information to use to create a client
pub enum DatabaseInfo {
/// Auto-detect the database in use
Auto,
/// Use the mock database
Dummy,
/// Connect to MongoDB
MongoDb(String),
/// Use existing MongoDB connection
MongoDbFromClient(mongodb::Client),
}
/// Database
#[derive(Debug, Clone)]
pub enum Database {
/// Mock database
Dummy(DummyDb),
/// MongoDB database
MongoDb(MongoDb),
}
impl DatabaseInfo {
/// Create a database client from the given database information
#[async_recursion]
pub async fn connect(self) -> Result<Database, String> {
Ok(match self {
DatabaseInfo::Auto => {
if let Ok(test_db) = env::var("TEST_DB") {
return match test_db.as_str() {
"REFERENCE" => DatabaseInfo::Dummy.connect().await,
"MONGODB" => {
DatabaseInfo::MongoDb(env::var("MONGODB").expect("`MONGODB` env"))
.connect()
.await
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
};
}
if let Ok(uri) = env::var("MONGODB") {
return DatabaseInfo::MongoDb(uri).connect().await;
}
DatabaseInfo::Dummy.connect().await?
}
DatabaseInfo::Dummy => Database::Dummy(DummyDb),
DatabaseInfo::MongoDb(uri) => {
let client = mongodb::Client::with_uri_str(uri)
.await
.map_err(|_| "Failed to init db connection.".to_string())?;
Database::MongoDb(MongoDb(client))
}
DatabaseInfo::MongoDbFromClient(client) => Database::MongoDb(MongoDb(client)),
})
}
}
impl Deref for Database {
type Target = dyn AbstractDatabase;
fn deref(&self) -> &Self::Target {
match self {
Database::Dummy(dummy) => dummy,
Database::MongoDb(mongo) => mongo,
}
}
}
impl From<Database> for revolt_database::Database {
fn from(val: Database) -> Self {
match val {
Database::Dummy(_) => revolt_database::Database::Reference(Default::default()),
Database::MongoDb(MongoDb(client)) => revolt_database::Database::MongoDb(
revolt_database::MongoDb(client, "revolt".to_string()),
),
}
}
}
-237
View File
@@ -1,237 +0,0 @@
use authifier::AuthifierEvent;
use revolt_models::v0::{FieldsWebhook, PartialWebhook, Webhook};
use serde::{Deserialize, Serialize};
use crate::models::channel::{FieldsChannel, PartialChannel};
use crate::models::message::{AppendMessage, PartialMessage};
use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer};
use crate::models::server_member::{FieldsMember, MemberCompositeKey, PartialMember};
use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus};
use crate::models::{Channel, Emoji, Member, Message, Report, Server, User, UserSettings};
use crate::Error;
/// WebSocket Client Errors
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "error")]
pub enum WebSocketError {
LabelMe,
InternalError { at: String },
InvalidSession,
OnboardingNotFinished,
AlreadyAuthenticated,
MalformedData { msg: String },
}
/// Ping Packet
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Ping {
Binary(Vec<u8>),
Number(usize),
}
/// Untagged Error
#[derive(Serialize)]
#[serde(untagged)]
pub enum ErrorEvent {
Error(WebSocketError),
APIError(Error),
}
/// Protocol Events
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum EventV1 {
/// Multiple events
Bulk { v: Vec<EventV1> },
/// Successfully authenticated
Authenticated,
/// Basic data to cache
Ready {
users: Vec<User>,
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
emojis: Option<Vec<Emoji>>,
},
/// Ping response
Pong { data: Ping },
/// New message
Message(Message),
/// Update existing message
MessageUpdate {
id: String,
channel: String,
data: PartialMessage,
},
/// Append information to existing message
MessageAppend {
id: String,
channel: String,
append: AppendMessage,
},
/// Delete message
MessageDelete { id: String, channel: String },
/// New reaction to a message
MessageReact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove user's reaction from message
MessageUnreact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove a reaction from message
MessageRemoveReaction {
id: String,
channel_id: String,
emoji_id: String,
},
/// Bulk delete messages
BulkMessageDelete { channel: String, ids: Vec<String> },
/// New channel
ChannelCreate(Channel),
/// Update existing channel
ChannelUpdate {
id: String,
data: PartialChannel,
clear: Vec<FieldsChannel>,
},
/// Delete channel
ChannelDelete { id: String },
/// User joins a group
ChannelGroupJoin { id: String, user: String },
/// User leaves a group
ChannelGroupLeave { id: String, user: String },
/// User started typing in a channel
ChannelStartTyping { id: String, user: String },
/// User stopped typing in a channel
ChannelStopTyping { id: String, user: String },
/// User acknowledged message in channel
ChannelAck {
id: String,
user: String,
message_id: String,
},
/// New server
ServerCreate {
id: String,
server: Server,
channels: Vec<Channel>,
emojis: Vec<Emoji>,
},
/// Update existing server
ServerUpdate {
id: String,
data: PartialServer,
clear: Vec<FieldsServer>,
},
/// Delete server
ServerDelete { id: String },
/// Update existing server member
ServerMemberUpdate {
id: MemberCompositeKey,
data: PartialMember,
clear: Vec<FieldsMember>,
},
/// User joins server
ServerMemberJoin { id: String, user: String },
/// User left server
ServerMemberLeave { id: String, user: String },
/// Server role created or updated
ServerRoleUpdate {
id: String,
role_id: String,
data: PartialRole,
clear: Vec<FieldsRole>,
},
/// Server role deleted
ServerRoleDelete { id: String, role_id: String },
/// Update existing user
UserUpdate {
id: String,
data: PartialUser,
clear: Vec<FieldsUser>,
event_id: Option<String>,
},
/// Relationship with another user changed
UserRelationship {
id: String,
user: User,
// ! this field can be deprecated
status: RelationshipStatus,
},
/// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings },
/// User has been platform banned or deleted their account
///
/// Clients should remove the following associated data:
/// - Messages
/// - DM Channels
/// - Relationships
/// - Server Memberships
///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 },
/// New emoji
EmojiCreate(Emoji),
/// Delete emoji
EmojiDelete { id: String },
/// New webhook
WebhookCreate(Webhook),
/// Update existing webhook
WebhookUpdate {
id: String,
data: PartialWebhook,
remove: Vec<FieldsWebhook>,
},
/// Delete webhook
WebhookDelete { id: String },
/// New report
ReportCreate(Report),
/// Auth events
Auth(AuthifierEvent),
}
-4
View File
@@ -1,4 +0,0 @@
pub mod client;
pub mod r#impl;
pub mod server;
pub mod state;
@@ -1,10 +0,0 @@
use crate::{models::stats::Stats, AbstractStats, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractStats for DummyDb {
async fn generate_stats(&self) -> Result<Stats> {
todo!()
}
}
@@ -1,84 +0,0 @@
use crate::models::{channel::{Channel, FieldsChannel, PartialChannel}};
use crate::{AbstractAttachment, AbstractChannel, Error, OverrideField, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractChannel for DummyDb {
async fn fetch_channel(&self, id: &str) -> Result<Channel> {
Ok(Channel::Group {
id: id.into(),
name: "group".into(),
owner: "owner".into(),
description: None,
recipients: vec!["owner".into()],
icon: Some(
self.find_and_use_attachment("dummy", "dummy", "dummy", "dummy")
.await?,
),
last_message_id: None,
permissions: None,
nsfw: false,
})
}
async fn fetch_channels<'a>(&self, _ids: &'a [String]) -> Result<Vec<Channel>> {
Ok(vec![self.fetch_channel("sus").await.unwrap()])
}
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
info!("Insert {channel:?}");
Ok(())
}
async fn update_channel(
&self,
id: &str,
channel: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
info!("Update {id} with {channel:?} and remove {remove:?}");
Ok(())
}
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
info!("Delete {channel:?}");
Ok(())
}
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
Ok(vec![self.fetch_channel(user_id).await?])
}
async fn find_saved_messages_channel(&self, user: &str) -> Result<Channel> {
self.fetch_channel(user).await
}
async fn find_direct_message_channel(&self, _user_a: &str, _user_b: &str) -> Result<Channel> {
Err(Error::NotFound)
}
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
info!("Added {user} to {channel}");
Ok(())
}
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
info!("Removed {user} from {channel}");
Ok(())
}
async fn set_channel_role_permission(
&self,
channel: &str,
role: &str,
permissions: OverrideField,
) -> Result<()> {
info!("Updating permissions for role {role} in {channel} with {permissions:?}");
Ok(())
}
}
@@ -1,30 +0,0 @@
use crate::models::Invite;
use crate::{AbstractChannelInvite, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractChannelInvite for DummyDb {
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
Ok(Invite::Server {
code: code.into(),
server: "server".into(),
creator: "creator".into(),
channel: "channel".into(),
})
}
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
info!("Insert {invite:?}");
Ok(())
}
async fn delete_invite(&self, code: &str) -> Result<()> {
info!("Delete {code}");
Ok(())
}
async fn fetch_invites_for_server(&self, server: &str) -> Result<Vec<Invite>> {
Ok(vec![self.fetch_invite(server).await.unwrap()])
}
}
@@ -1,31 +0,0 @@
use crate::models::channel_unread::ChannelUnread;
use crate::{AbstractChannelUnread, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractChannelUnread for DummyDb {
async fn acknowledge_message(&self, channel: &str, user: &str, message: &str) -> Result<()> {
info!("Acknowledged {message} in {channel} for {user}");
Ok(())
}
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> {
info!("Acknowledged {channels:?} for {user}");
Ok(())
}
async fn add_mention_to_unread<'a>(
&self,
channel: &str,
user: &str,
ids: &[String],
) -> Result<()> {
info!("Added mentions for {user} in {channel}: {ids:?}");
Ok(())
}
async fn fetch_unreads(&self, _user: &str) -> Result<Vec<ChannelUnread>> {
Ok(vec![])
}
}
@@ -1,65 +0,0 @@
use crate::models::message::{AppendMessage, Message, MessageQuery, PartialMessage};
use crate::{AbstractMessage, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractMessage for DummyDb {
async fn fetch_message(&self, id: &str) -> Result<Message> {
Ok(Message {
id: id.into(),
channel: "channel".into(),
author: "author".into(),
content: Some("message content".into()),
..Default::default()
})
}
async fn insert_message(&self, message: &Message) -> Result<()> {
info!("Insert {message:?}");
Ok(())
}
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
info!("Update {id} with {message:?}");
Ok(())
}
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
info!("Append {id} with {append:?}");
Ok(())
}
async fn delete_message(&self, id: &str) -> Result<()> {
info!("Delete {id}");
Ok(())
}
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> {
info!("Delete {ids:?} in {channel}");
Ok(())
}
async fn fetch_messages(&self, _query: MessageQuery) -> Result<Vec<Message>> {
Ok(vec![])
}
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
info!("Add to {id} with {emoji} and {user}");
Ok(())
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
info!("Remove {emoji} from {id} for {user}");
Ok(())
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
info!("Clear {emoji} on {id}");
Ok(())
}
}
@@ -1,47 +0,0 @@
use crate::models::attachment::File;
use crate::{AbstractAttachment, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractAttachment for DummyDb {
async fn find_and_use_attachment(
&self,
attachment_id: &str,
tag: &str,
_parent_type: &str,
parent_id: &str,
) -> Result<File> {
Ok(File {
id: attachment_id.into(),
tag: tag.into(),
filename: "file.txt".into(),
content_type: "plain/text".into(),
size: 100,
object_id: Some(parent_id.into()),
..Default::default()
})
}
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
info!("Insert {attachment:?}");
Ok(())
}
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
info!("Marked {id} as reported");
Ok(())
}
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
info!("Marked {id} as deleted");
Ok(())
}
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
info!("Marked {ids:?} as deleted");
Ok(())
}
}
@@ -1,42 +0,0 @@
use crate::models::emoji::EmojiParent;
use crate::models::Emoji;
use crate::{AbstractEmoji, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractEmoji for DummyDb {
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
Ok(Emoji {
id: id.into(),
name: id.into(),
parent: EmojiParent::Server { id: id.into() },
creator_id: id.into(),
animated: false,
nsfw: false,
})
}
/// Fetch emoji by their ids
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
Ok(vec![self.fetch_emoji(parent_id).await?])
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, _parent_ids: &[String]) -> Result<Vec<Emoji>> {
Ok(vec![])
}
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
info!("Insert {emoji:?}");
Ok(())
}
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
info!("Detach {emoji:?}");
Ok(())
}
}
-38
View File
@@ -1,38 +0,0 @@
use crate::AbstractDatabase;
pub mod admin {
pub mod stats;
}
pub mod media {
pub mod attachment;
pub mod emoji;
}
pub mod channels {
pub mod channel;
pub mod channel_invite;
pub mod channel_unread;
pub mod message;
}
pub mod servers {
pub mod server;
pub mod server_ban;
pub mod server_member;
}
pub mod users {
pub mod user;
pub mod user_settings;
}
pub mod safety {
pub mod report;
pub mod snapshot;
}
#[derive(Debug, Clone)]
pub struct DummyDb;
impl AbstractDatabase for DummyDb {}
@@ -1,25 +0,0 @@
use crate::models::report::PartialReport;
use crate::models::Report;
use crate::{AbstractReport, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractReport for DummyDb {
async fn insert_report(&self, report: &Report) -> Result<()> {
info!("Insert {:?}", report);
Ok(())
}
async fn update_report(&self, _id: &str, _report: &PartialReport) -> Result<()> {
todo!()
}
async fn fetch_report(&self, _report_id: &str) -> Result<Report> {
todo!()
}
async fn fetch_reports(&self) -> Result<Vec<Report>> {
Ok(vec![])
}
}
@@ -1,16 +0,0 @@
use crate::models::Snapshot;
use crate::{AbstractSnapshot, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractSnapshot for DummyDb {
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
info!("Insert {:?}", snapshot);
Ok(())
}
async fn fetch_snapshots(&self, _report_id: &str) -> Result<Vec<Snapshot>> {
todo!()
}
}
@@ -1,78 +0,0 @@
use crate::models::server::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
use crate::{AbstractServer, Result, DEFAULT_PERMISSION_SERVER};
use super::super::DummyDb;
#[async_trait]
impl AbstractServer for DummyDb {
async fn fetch_server(&self, id: &str) -> Result<Server> {
Ok(Server {
id: id.into(),
owner: "owner".into(),
name: "server".into(),
description: Some("server description".into()),
channels: vec!["channel".into()],
categories: None,
system_messages: None,
roles: std::collections::HashMap::new(),
default_permissions: *DEFAULT_PERMISSION_SERVER as i64,
icon: None,
banner: None,
flags: None,
nsfw: false,
analytics: true,
discoverable: true,
})
}
async fn fetch_servers<'a>(&self, _ids: &'a [String]) -> Result<Vec<Server>> {
Ok(vec![self.fetch_server("sus").await.unwrap()])
}
async fn insert_server(&self, server: &Server) -> Result<()> {
info!("Insert {server:?}");
Ok(())
}
async fn update_server(
&self,
id: &str,
server: &PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()> {
info!("Update {id} with {server:?} and remove {remove:?}");
Ok(())
}
async fn delete_server(&self, server: &Server) -> Result<()> {
info!("Delete {server:?}");
Ok(())
}
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
info!("Create {role:?} on {server_id} as {role_id}");
Ok(())
}
async fn update_role(
&self,
server_id: &str,
role_id: &str,
role: &PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()> {
info!("Update {role_id} on {server_id} with {role:?} and remove {remove:?}");
Ok(())
}
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
info!("Delete {role_id} on {server_id}");
Ok(())
}
}
@@ -1,32 +0,0 @@
use crate::models::server_member::MemberCompositeKey;
use crate::models::ServerBan;
use crate::{AbstractServerBan, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractServerBan for DummyDb {
async fn fetch_ban(&self, server: &str, user: &str) -> Result<ServerBan> {
Ok(ServerBan {
id: MemberCompositeKey {
server: server.into(),
user: user.into(),
},
reason: Some("ban reason".into()),
})
}
async fn fetch_bans(&self, server: &str) -> Result<Vec<ServerBan>> {
Ok(vec![self.fetch_ban(server, "user").await.unwrap()])
}
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
info!("Insert {ban:?}");
Ok(())
}
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
info!("Delete {id:?}");
Ok(())
}
}
@@ -1,56 +0,0 @@
use crate::models::server_member::{
FieldsMember, Member, MemberCompositeKey, MemberWithRoles, PartialMember,
};
use crate::{AbstractServerMember, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractServerMember for DummyDb {
async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> {
Ok(Member::new(server.into(), user.into()))
}
async fn fetch_member_with_roles(&self, server: &str, user: &str) -> Result<MemberWithRoles> {
Ok(MemberWithRoles::new(server.into(), user.into()))
}
async fn insert_member(&self, member: &Member) -> Result<()> {
info!("Create {member:?}");
Ok(())
}
async fn update_member(
&self,
id: &MemberCompositeKey,
member: &PartialMember,
remove: Vec<FieldsMember>,
) -> Result<()> {
info!("Update {id:?} with {member:?} and remove {remove:?}");
Ok(())
}
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
info!("Delete {id:?}");
Ok(())
}
async fn fetch_all_members<'a>(&self, server: &str) -> Result<Vec<Member>> {
Ok(vec![self.fetch_member(server, "member").await.unwrap()])
}
async fn fetch_all_memberships<'a>(&self, user: &str) -> Result<Vec<Member>> {
Ok(vec![self.fetch_member("server", user).await.unwrap()])
}
async fn fetch_members<'a>(&self, server: &str, _ids: &'a [String]) -> Result<Vec<Member>> {
Ok(vec![self.fetch_member(server, "member").await.unwrap()])
}
async fn fetch_member_count(&self, _server: &str) -> Result<usize> {
Ok(100)
}
async fn fetch_server_count(&self, _user: &str) -> Result<usize> {
Ok(5)
}
}
-79
View File
@@ -1,79 +0,0 @@
use crate::models::user::{FieldsUser, PartialUser, RelationshipStatus, User};
use crate::{AbstractUser, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractUser for DummyDb {
async fn fetch_user(&self, id: &str) -> Result<User> {
Ok(User {
id: id.into(),
username: "username".into(),
discriminator: "0000".into(),
..Default::default()
})
}
async fn fetch_user_by_username(&self, username: &str, _discriminator: &str) -> Result<User> {
self.fetch_user(username).await
}
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
self.fetch_user(token).await
}
async fn insert_user(&self, user: &User) -> Result<()> {
info!("Insert {:?}", user);
Ok(())
}
async fn update_user(
&self,
id: &str,
user: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
info!("Update {id} with {user:?} and remove {remove:?}");
Ok(())
}
async fn delete_user(&self, id: &str) -> Result<()> {
info!("Delete {id}");
Ok(())
}
async fn fetch_users<'a>(&self, _id: &'a [String]) -> Result<Vec<User>> {
Ok(vec![self.fetch_user("id").await.unwrap()])
}
async fn fetch_discriminators_in_use(&self, _username: &str) -> Result<Vec<String>> {
Ok(vec![])
}
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
Ok(vec!["a".into()])
}
async fn fetch_mutual_channel_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
Ok(vec!["b".into()])
}
async fn fetch_mutual_server_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
Ok(vec!["c".into()])
}
async fn set_relationship(
&self,
user_id: &str,
target_id: &str,
relationship: &RelationshipStatus,
) -> Result<()> {
info!("Set relationship from {user_id} to {target_id} as {relationship:?}");
Ok(())
}
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
info!("Removing relationship from {user_id} to {target_id}");
Ok(())
}
}
@@ -1,25 +0,0 @@
use crate::models::UserSettings;
use crate::{AbstractUserSettings, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractUserSettings for DummyDb {
async fn fetch_user_settings(
&'_ self,
_id: &str,
_filter: &'_ [String],
) -> Result<UserSettings> {
Ok(std::collections::HashMap::new())
}
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()> {
info!("Set {id} to {settings:?}");
Ok(())
}
async fn delete_user_settings(&self, id: &str) -> Result<()> {
info!("Delete {id}");
Ok(())
}
}
@@ -1,566 +0,0 @@
use std::collections::HashSet;
use revolt_database::util::idempotency::IdempotencyKey;
use ulid::Ulid;
use crate::{
events::client::EventV1,
models::{
channel::{FieldsChannel, PartialChannel},
message::{DataMessageSend, Message, Reply, SystemMessage, RE_MENTION},
Channel,
},
tasks::{ack::AckEvent, process_embeds},
types::push::MessageAuthor,
variables::delta::{MAX_ATTACHMENT_COUNT, MAX_EMBED_COUNT, MAX_REPLY_COUNT},
Database, Error, OverrideField, Ref, Result,
};
impl Channel {
/// Get a reference to this channel's id
pub fn id(&'_ self) -> &'_ str {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
}
}
/// Represent channel as its id
pub fn as_id(self) -> String {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
}
}
/// Map out whether it is a direct DM
pub fn is_direct_dm(&self) -> bool {
matches!(self, Channel::DirectMessage { .. })
}
/// Create a channel
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_channel(self).await?;
let event = EventV1::ChannelCreate(self.clone());
match self {
Self::SavedMessages { user, .. } => event.private(user.clone()).await,
Self::DirectMessage { recipients, .. } | Self::Group { recipients, .. } => {
for recipient in recipients {
event.clone().private(recipient.clone()).await;
}
}
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => {
event.p(server.clone()).await;
}
}
Ok(())
}
/// Update channel data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
for field in &remove {
self.remove(field);
}
self.apply_options(partial.clone());
let id = self.id().to_string();
db.update_channel(&id, &partial, remove.clone()).await?;
EventV1::ChannelUpdate {
id: id.clone(),
data: partial,
clear: remove,
}
.p(match self {
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
_ => id,
})
.await;
Ok(())
}
/// Delete a channel
pub async fn delete(self, db: &Database) -> Result<()> {
let id = self.id().to_string();
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
db.delete_channel(&self).await
}
/// Remove a field from Channel object
pub fn remove(&mut self, field: &FieldsChannel) {
match field {
FieldsChannel::Description => match self {
Self::Group { description, .. }
| Self::TextChannel { description, .. }
| Self::VoiceChannel { description, .. } => {
description.take();
}
_ => {}
},
FieldsChannel::Icon => match self {
Self::Group { icon, .. }
| Self::TextChannel { icon, .. }
| Self::VoiceChannel { icon, .. } => {
icon.take();
}
_ => {}
},
FieldsChannel::DefaultPermissions => match self {
Self::TextChannel {
default_permissions,
..
}
| Self::VoiceChannel {
default_permissions,
..
} => {
default_permissions.take();
}
_ => {}
},
}
}
/// Apply partial channel to channel
pub fn apply_options(&mut self, partial: PartialChannel) {
// ! FIXME: maybe flatten channel object?
match self {
Self::DirectMessage { active, .. } => {
if let Some(v) = partial.active {
*active = v;
}
}
Self::Group {
name,
owner,
description,
icon,
nsfw,
permissions,
..
} => {
if let Some(v) = partial.name {
*name = v;
}
if let Some(v) = partial.owner {
*owner = v;
}
if let Some(v) = partial.description {
description.replace(v);
}
if let Some(v) = partial.icon {
icon.replace(v);
}
if let Some(v) = partial.nsfw {
*nsfw = v;
}
if let Some(v) = partial.permissions {
permissions.replace(v);
}
}
Self::TextChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
..
}
| Self::VoiceChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
..
} => {
if let Some(v) = partial.name {
*name = v;
}
if let Some(v) = partial.description {
description.replace(v);
}
if let Some(v) = partial.icon {
icon.replace(v);
}
if let Some(v) = partial.nsfw {
*nsfw = v;
}
if let Some(v) = partial.role_permissions {
*role_permissions = v;
}
if let Some(v) = partial.default_permissions {
default_permissions.replace(v);
}
}
_ => {}
}
}
/// Acknowledge a message
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
EventV1::ChannelAck {
id: self.id().to_string(),
user: user.to_string(),
message_id: message.to_string(),
}
.private(user.to_string())
.await;
crate::tasks::ack::queue(
self.id().to_string(),
user.to_string(),
AckEvent::AckMessage {
id: message.to_string(),
},
)
.await;
Ok(())
}
/// Add user to a group
pub async fn add_user_to_group(&mut self, db: &Database, user: &str, by: &str) -> Result<()> {
if let Channel::Group { recipients, .. } = self {
let user = user.to_string();
if recipients.contains(&user) {
return Err(Error::AlreadyInGroup);
}
recipients.push(user);
}
match &self {
Channel::Group { id, .. } => {
db.add_user_to_group(id, user).await?;
EventV1::ChannelGroupJoin {
id: id.to_string(),
user: user.to_string(),
}
.p(id.to_string())
.await;
EventV1::ChannelCreate(self.clone())
.private(user.to_string())
.await;
SystemMessage::UserAdded {
id: user.to_string(),
by: by.to_string(),
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok();
Ok(())
}
_ => Err(Error::InvalidOperation),
}
}
/// Remove user from a group
pub async fn remove_user_from_group(
&self,
db: &Database,
user: &str,
by: Option<&str>,
silent: bool,
) -> Result<()> {
match &self {
Channel::Group {
id,
owner,
recipients,
..
} => {
if user == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != user) {
db.update_channel(
id,
&PartialChannel {
owner: Some(new_owner.into()),
..Default::default()
},
vec![],
)
.await?;
SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(),
to: new_owner.into(),
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok();
} else {
db.delete_channel(self).await?;
return Ok(());
}
}
db.remove_user_from_group(id, user).await?;
EventV1::ChannelGroupLeave {
id: id.to_string(),
user: user.to_string(),
}
.p(id.to_string())
.await;
if !silent {
if let Some(by) = by {
SystemMessage::UserRemove {
id: user.to_string(),
by: by.to_string(),
}
} else {
SystemMessage::UserLeft {
id: user.to_string(),
}
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok();
}
Ok(())
}
_ => Err(Error::InvalidOperation),
}
}
/// Set role permission on a channel
pub async fn set_role_permission(
&mut self,
db: &Database,
role: &str,
permissions: OverrideField,
) -> Result<()> {
match self {
Channel::TextChannel {
id,
server,
role_permissions,
..
}
| Channel::VoiceChannel {
id,
server,
role_permissions,
..
} => {
db.set_channel_role_permission(id, role, permissions)
.await?;
role_permissions.insert(role.to_string(), permissions);
EventV1::ChannelUpdate {
id: id.clone(),
data: PartialChannel {
role_permissions: Some(role_permissions.clone()),
..Default::default()
},
clear: vec![],
}
.p(server.clone())
.await;
Ok(())
}
_ => Err(Error::InvalidOperation),
}
}
/// Creates a message in a channel
pub async fn send_message(
&self,
db: &Database,
data: DataMessageSend,
author: MessageAuthor<'_>,
mut idempotency: IdempotencyKey,
generate_embeds: bool,
) -> Result<Message> {
Message::validate_sum(&data.content, data.embeds.as_deref().unwrap_or_default())?;
idempotency
.consume_nonce(data.nonce)
.await
.map_err(|_| Error::InvalidOperation)?;
// Check the message is not empty
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
{
return Err(Error::EmptyMessage);
}
// Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
let disallowed = if let Some(list) = &interactions.reactions {
list.is_empty()
} else {
true
};
if disallowed {
return Err(Error::InvalidProperty);
}
}
}
let (author_id, webhook) = match &author {
MessageAuthor::User(user) => (user.id.clone(), None),
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
};
// Start constructing the message
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id.clone(),
channel: self.id().to_string(),
masquerade: data.masquerade,
interactions: data.interactions.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
..Default::default()
};
// Parse mentions in message.
let mut mentions = HashSet::new();
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());
}
}
}
// Verify replies are valid.
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > *MAX_REPLY_COUNT {
return Err(Error::TooManyReplies {
max: *MAX_REPLY_COUNT,
});
}
for Reply { id, mention } in entries {
let message = Ref::from_unchecked(id).as_message(db).await?;
if mention {
mentions.insert(message.author.to_owned());
}
replies.insert(message.id);
}
}
if !mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect());
}
if !replies.is_empty() {
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
// Add attachments to message.
let mut attachments = vec![];
if data
.attachments
.as_ref()
.is_some_and(|v| v.len() > *MAX_ATTACHMENT_COUNT)
{
return Err(Error::TooManyAttachments {
max: *MAX_ATTACHMENT_COUNT,
});
}
if data
.embeds
.as_ref()
.is_some_and(|v| v.len() > *MAX_EMBED_COUNT)
{
return Err(Error::TooManyEmbeds {
max: *MAX_EMBED_COUNT,
});
}
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
attachments.push(
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
.await?,
);
}
if !attachments.is_empty() {
message.attachments.replace(attachments);
}
// Process included embeds.
let mut embeds = vec![];
for sendable_embed in data.embeds.unwrap_or_default() {
embeds.push(sendable_embed.into_embed(db, &message_id).await?)
}
if !embeds.is_empty() {
message.embeds.replace(embeds);
}
// Set content
message.content = data.content;
// Pass-through nonce value for clients
message.nonce = Some(idempotency.into_key());
message.create(db, self, Some(author)).await?;
// Queue up a task for processing embeds
if generate_embeds {
if let Some(content) = &message.content {
process_embeds::queue(
self.id().to_string(),
message.id.to_string(),
content.clone(),
)
.await;
}
}
Ok(message)
}
}
@@ -1,72 +0,0 @@
use nanoid::nanoid;
use crate::{
models::{Channel, Invite, User},
Database, Error, Result,
};
static ALPHABET: [char; 54] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'
];
impl Invite {
/// Get the invite code for this invite
pub fn code(&'_ self) -> &'_ str {
match self {
Invite::Server { code, .. } | Invite::Group { code, .. } => code,
}
}
/// Get the ID of the user who created this invite
pub fn creator(&'_ self) -> &'_ str {
match self {
Invite::Server { creator, .. } | Invite::Group { creator, .. } => creator,
}
}
/// Create a new invite from given information
pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result<Invite> {
let code = nanoid!(8, &ALPHABET);
let invite = match &target {
Channel::Group { id, .. } => Ok(Invite::Group {
code,
creator: creator.id.clone(),
channel: id.clone(),
}),
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
Ok(Invite::Server {
code,
creator: creator.id.clone(),
server: server.clone(),
channel: id.clone(),
})
}
_ => Err(Error::InvalidOperation),
}?;
db.insert_invite(&invite).await?;
Ok(invite)
}
/// Resolve an invite by its ID or by a public server ID
pub async fn find(db: &Database, code: &str) -> Result<Invite> {
if let Ok(invite) = db.fetch_invite(code).await {
return Ok(invite);
} else if let Ok(server) = db.fetch_server(code).await {
if server.discoverable {
if let Some(channel) = server.channels.into_iter().next() {
return Ok(Invite::Server {
code: code.to_string(),
server: server.id,
creator: server.owner,
channel,
});
}
}
}
Err(Error::NotFound)
}
}
@@ -1 +0,0 @@
@@ -1,494 +0,0 @@
use std::collections::HashSet;
use revolt_presence::filter_online;
use serde_json::json;
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
models::{
message::{
AppendMessage, BulkMessageResponse, Interactions, PartialMessage, SendableEmbed,
SystemMessage, DataMessageSend,
},
Channel, Emoji, Message, User,
},
permissions::PermissionCalculator,
tasks::ack::AckEvent,
types::{
january::{Embed, Text},
push::{MessageAuthor, PushNotification},
},
Database, Error, Permission, Result,
};
impl Message {
/// Create a message
pub async fn create_no_web_push(
&mut self,
db: &Database,
channel: &str,
is_direct_dm: bool,
) -> Result<()> {
db.insert_message(self).await?;
// Fan out events
EventV1::Message(self.clone()).p(channel.to_string()).await;
// Update last_message_id
crate::tasks::last_message_id::queue(
channel.to_string(),
self.id.to_string(),
is_direct_dm,
)
.await;
// Add mentions for affected users
if let Some(mentions) = &self.mentions {
for user in mentions {
crate::tasks::ack::queue(
channel.to_string(),
user.to_string(),
AckEvent::AddMention {
ids: vec![self.id.to_string()],
},
)
.await;
}
}
Ok(())
}
/// Create a message and Web Push events
pub async fn create(
&mut self,
db: &Database,
channel: &Channel,
sender: Option<MessageAuthor<'_>>,
) -> Result<()> {
self.create_no_web_push(db, channel.id(), channel.is_direct_dm())
.await?;
// Push out Web Push notifications
crate::tasks::web_push::queue(
{
let mut target_ids = vec![];
match &channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => {
target_ids = (&recipients.iter().cloned().collect::<HashSet<String>>()
- &filter_online(recipients).await)
.into_iter()
.collect::<Vec<String>>();
}
Channel::TextChannel { .. } => {
if let Some(mentions) = &self.mentions {
target_ids.append(&mut mentions.clone());
}
}
_ => {}
};
target_ids
},
json!(PushNotification::new(self.clone(), sender, channel.id())).to_string(),
)
.await;
Ok(())
}
/// Update message data
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
self.apply_options(partial.clone());
db.update_message(&self.id, &partial).await?;
EventV1::MessageUpdate {
id: self.id.clone(),
channel: self.channel.clone(),
data: partial,
}
.p(self.channel.clone())
.await;
Ok(())
}
/// Append message data
pub async fn append(
db: &Database,
id: String,
channel: String,
append: AppendMessage,
) -> Result<()> {
db.append_message(&id, &append).await?;
EventV1::MessageAppend {
id,
channel: channel.to_string(),
append,
}
.p(channel)
.await;
Ok(())
}
/// Delete a message
pub async fn delete(self, db: &Database) -> Result<()> {
let file_ids: Vec<String> = self
.attachments
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
.unwrap_or_default();
if !file_ids.is_empty() {
db.mark_attachments_as_deleted(&file_ids).await?;
}
db.delete_message(&self.id).await?;
EventV1::MessageDelete {
id: self.id,
channel: self.channel.clone(),
}
.p(self.channel)
.await;
Ok(())
}
/// Bulk delete messages
pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec<String>) -> Result<()> {
db.delete_messages(channel, ids.clone()).await?;
EventV1::BulkMessageDelete {
channel: channel.to_string(),
ids,
}
.p(channel.to_string())
.await;
Ok(())
}
/// Validate the sum of content of a message is under threshold
pub fn validate_sum(content: &Option<String>, embeds: &[SendableEmbed]) -> Result<()> {
let mut running_total = 0;
if let Some(content) = content {
running_total += content.len();
}
for embed in embeds {
if let Some(desc) = &embed.description {
running_total += desc.len();
}
}
if running_total <= 2000 {
Ok(())
} else {
Err(Error::PayloadTooLarge)
}
}
/// Add a reaction to a message
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
// Check how many reactions are already on the message
if self.reactions.len() >= 20 && !self.reactions.contains_key(emoji) {
return Err(Error::InvalidOperation);
}
// Check if the emoji is whitelisted
if !self.interactions.can_use(emoji) {
return Err(Error::InvalidOperation);
}
// Check if the emoji is usable by us
if !Emoji::can_use(db, emoji).await? {
return Err(Error::InvalidOperation);
}
// Send reaction event
EventV1::MessageReact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.id.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
// Add emoji
db.add_reaction(&self.id, emoji, &user.id).await
}
/// Remove a reaction from a message
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
// Check if it actually exists
let empty = if let Some(users) = self.reactions.get(emoji) {
if !users.contains(user) {
return Err(Error::NotFound);
}
users.len() == 1
} else {
return Err(Error::NotFound);
};
// Send reaction event
EventV1::MessageUnreact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
if empty {
// If empty, remove the reaction entirely
db.clear_reaction(&self.id, emoji).await
} else {
// Otherwise only remove that one reaction
db.remove_reaction(&self.id, emoji, user).await
}
}
/// Remove a reaction from a message
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
// Send reaction event
EventV1::MessageRemoveReaction {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
// Write to database
db.clear_reaction(&self.id, emoji).await
}
pub fn is_webhook(&self) -> bool {
self.webhook.is_some()
}
}
pub trait IntoUsers {
fn get_user_ids(&self) -> Vec<String>;
}
impl IntoUsers for Message {
fn get_user_ids(&self) -> Vec<String> {
let mut ids = Vec::new();
if !self.is_webhook() {
ids.push(self.author.clone());
};
if let Some(msg) = &self.system {
match msg {
SystemMessage::UserAdded { id, by, .. }
| SystemMessage::UserRemove { id, by, .. } => {
ids.push(id.clone());
ids.push(by.clone());
}
SystemMessage::UserJoined { id, .. }
| SystemMessage::UserLeft { id, .. }
| SystemMessage::UserKicked { id, .. }
| SystemMessage::UserBanned { id, .. } => ids.push(id.clone()),
SystemMessage::ChannelRenamed { by, .. }
| SystemMessage::ChannelDescriptionChanged { by, .. }
| SystemMessage::ChannelIconChanged { by, .. } => ids.push(by.clone()),
_ => {}
}
}
ids
}
}
impl IntoUsers for Vec<Message> {
fn get_user_ids(&self) -> Vec<String> {
let mut ids = HashSet::new();
for message in self {
ids.extend(&mut message.get_user_ids().into_iter());
}
ids.into_iter().collect()
}
}
impl SystemMessage {
pub fn into_message(self, channel: String) -> Message {
Message {
id: Ulid::new().to_string(),
channel,
author: "00000000000000000000000000".to_string(),
system: Some(self),
..Default::default()
}
}
}
impl From<SystemMessage> for String {
fn from(s: SystemMessage) -> String {
match s {
SystemMessage::Text { content } => content,
SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(),
SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(),
SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(),
SystemMessage::UserLeft { .. } => "User left the channel.".to_string(),
SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(),
SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(),
SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(),
SystemMessage::ChannelDescriptionChanged { .. } => {
"Channel description changed.".to_string()
}
SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(),
SystemMessage::ChannelOwnershipChanged { .. } => {
"Channel ownership changed.".to_string()
}
}
}
}
impl SendableEmbed {
pub async fn into_embed(self, db: &Database, message_id: &str) -> Result<Embed> {
self.validate()
.map_err(|error| Error::FailedValidation { error })?;
let media = if let Some(id) = self.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", message_id)
.await?,
)
} else {
None
};
Ok(Embed::Text(Text {
icon_url: self.icon_url,
url: self.url,
title: self.title,
description: self.description,
media,
colour: self.colour,
}))
}
}
impl BulkMessageResponse {
pub async fn transform(
db: &Database,
channel: Option<&Channel>,
messages: Vec<Message>,
user: &User,
include_users: Option<bool>,
) -> Result<BulkMessageResponse> {
if let Some(true) = include_users {
let user_ids = messages.get_user_ids();
let users = User::fetch_foreign_users(db, &user_ids)
.await?
.into_iter()
.map(|x| x.with_relationship(user))
.collect();
Ok(match channel {
Some(Channel::TextChannel { server, .. })
| Some(Channel::VoiceChannel { server, .. }) => {
BulkMessageResponse::MessagesAndUsers {
messages,
users,
members: Some(db.fetch_members(server, &user_ids).await?),
}
}
_ => BulkMessageResponse::MessagesAndUsers {
messages,
users,
members: None,
},
})
} else {
Ok(BulkMessageResponse::JustMessages(messages))
}
}
}
impl Interactions {
/// Validate interactions info is correct
pub async fn validate(
&self,
db: &Database,
permissions: &mut PermissionCalculator<'_>,
) -> Result<()> {
if let Some(reactions) = &self.reactions {
permissions.throw_permission(db, Permission::React).await?;
if reactions.len() > 20 {
return Err(Error::InvalidOperation);
}
for reaction in reactions {
if !Emoji::can_use(db, reaction).await? {
return Err(Error::InvalidOperation);
}
}
}
Ok(())
}
/// Check if we can use a given emoji to react
pub fn can_use(&self, emoji: &str) -> bool {
if self.restrict_reactions {
if let Some(reactions) = &self.reactions {
reactions.contains(emoji)
} else {
false
}
} else {
true
}
}
/// Check if default initialisation of fields
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}
fn throw_permission(permissions: u64, permission: Permission) -> Result<()> {
if (permission as u64) & permissions == (permission as u64) {
Ok(())
} else {
Err(Error::MissingPermission { permission })
}
}
impl DataMessageSend {
pub fn validate_webhook_permissions(
&self,
permissions: u64,
) -> Result<()> {
throw_permission(permissions, Permission::SendMessage)?;
if self.attachments.as_ref().map_or(false, |v| !v.is_empty()) {
throw_permission(permissions, Permission::UploadFiles)?;
};
if self.embeds.as_ref().map_or(false, |v| !v.is_empty()) {
throw_permission(permissions, Permission::SendEmbeds)?;
};
if self.masquerade.is_some() {
throw_permission(permissions, Permission::Masquerade)?;
};
if self.interactions.is_some() {
throw_permission(permissions, Permission::React)?;
};
Ok(())
}
}
@@ -1,38 +0,0 @@
use crate::{models::File, Database, Result};
impl File {
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "attachments", "message", parent)
.await
}
pub async fn use_background(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "backgrounds", "user", parent)
.await
}
pub async fn use_avatar(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "avatars", "user", parent)
.await
}
pub async fn use_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "icons", "object", parent)
.await
}
pub async fn use_server_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "icons", "object", parent)
.await
}
pub async fn use_banner(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "banners", "server", parent)
.await
}
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "emojis", "object", parent)
.await
}
}
@@ -1,56 +0,0 @@
use std::{collections::HashSet, str::FromStr};
use once_cell::sync::Lazy;
use ulid::Ulid;
use crate::{
events::client::EventV1,
models::{emoji::EmojiParent, Emoji},
Database, Result,
};
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| include_str!(crate::asset!("emojis.txt"))
.split('\n')
.map(|x| x.into())
.collect());
impl Emoji {
/// Get parent id
fn parent(&self) -> &str {
match &self.parent {
EmojiParent::Server { id } => id,
EmojiParent::Detached => "",
}
}
/// Create an emoji
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_emoji(self).await?;
EventV1::EmojiCreate(self.clone())
.p(self.parent().to_string())
.await;
Ok(())
}
/// Delete an emoji
pub async fn delete(self, db: &Database) -> Result<()> {
EventV1::EmojiDelete {
id: self.id.to_string(),
}
.p(self.parent().to_string())
.await;
db.detach_emoji(&self).await
}
/// Check whether we can use a given emoji
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
if Ulid::from_str(emoji).is_ok() {
db.fetch_emoji(emoji).await?;
Ok(true)
} else {
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
}
}
}
-29
View File
@@ -1,29 +0,0 @@
//! Database agnostic implementations.
pub mod media {
pub mod attachment;
pub mod emoji;
}
pub mod channels {
pub mod channel;
pub mod channel_invite;
pub mod channel_unread;
pub mod message;
}
pub mod servers {
pub mod server;
pub mod server_ban;
pub mod server_member;
}
pub mod users {
pub mod user;
pub mod user_settings;
}
pub mod safety {
pub mod report;
pub mod snapshot;
}
@@ -1,30 +0,0 @@
use iso8601_timestamp::Timestamp;
use crate::{
models::report::PartialReport,
models::{report::ReportStatus, Report},
Database, Result,
};
impl Report {
/// Update report data
pub async fn update(&mut self, db: &Database, partial: PartialReport) -> Result<()> {
self.apply_options(partial.clone());
match &mut self.status {
ReportStatus::Created {} => {}
ReportStatus::Rejected { closed_at, .. } => {
if closed_at.is_none() {
closed_at.replace(Timestamp::now_utc());
}
}
ReportStatus::Resolved { closed_at } => {
if closed_at.is_none() {
closed_at.replace(Timestamp::now_utc());
}
}
}
db.update_report(&self.id, &partial).await
}
}
@@ -1,88 +0,0 @@
use crate::{
models::{
message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod},
snapshot::SnapshotContent,
Message, Server, User,
},
Database, Result,
};
impl SnapshotContent {
pub async fn generate_from_message(
db: &Database,
message: Message,
) -> Result<(SnapshotContent, Vec<String>)> {
// Collect message attachments
let files = message
.attachments
.as_ref()
.map(|attachments| attachments.iter().map(|x| x.id.to_string()).collect())
.unwrap_or_default();
// Collect prior context
let prior_context = db
.fetch_messages(MessageQuery {
filter: MessageFilter {
channel: Some(message.channel.to_string()),
..Default::default()
},
limit: Some(15),
time_period: MessageTimePeriod::Absolute {
before: Some(message.id.to_string()),
after: None,
sort: Some(MessageSort::Latest),
},
})
.await?;
// Collect leading context
let leading_context = db
.fetch_messages(MessageQuery {
filter: MessageFilter {
channel: Some(message.channel.to_string()),
..Default::default()
},
limit: Some(15),
time_period: MessageTimePeriod::Absolute {
before: None,
after: Some(message.id.to_string()),
sort: Some(MessageSort::Oldest),
},
})
.await?;
Ok((
SnapshotContent::Message {
message,
prior_context,
leading_context,
},
files,
))
}
pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec<String>)> {
// Collect server's icon and banner
let files = [&server.icon, &server.banner]
.iter()
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
.collect();
Ok((SnapshotContent::Server(server), files))
}
pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> {
// Collect user's avatar and profile background
let files = [
user.avatar.as_ref(),
user.profile
.as_ref()
.and_then(|profile| profile.background.as_ref()),
]
.iter()
.filter_map(|x| x.as_ref().map(|x| x.id.to_string()))
.collect();
Ok((SnapshotContent::User(user), files))
}
}
@@ -1,330 +0,0 @@
use std::collections::HashSet;
use ulid::Ulid;
use crate::{
events::client::EventV1,
models::{
message::SystemMessage,
server::{
FieldsRole, FieldsServer, PartialRole, PartialServer, Role, SystemMessageChannels,
},
server_member::{MemberCompositeKey, RemovalIntention},
Channel, Member, Server, ServerBan, User,
},
perms, Database, Error, OverrideField, Permission, Result,
};
impl Role {
/// Into optional struct
pub fn into_optional(self) -> PartialRole {
PartialRole {
name: Some(self.name),
permissions: Some(self.permissions),
colour: self.colour,
hoist: Some(self.hoist),
rank: Some(self.rank),
}
}
/// Create a role
pub async fn create(&self, db: &Database, server_id: &str) -> Result<String> {
let role_id = Ulid::new().to_string();
db.insert_role(server_id, &role_id, self).await?;
EventV1::ServerRoleUpdate {
id: server_id.to_string(),
role_id: role_id.to_string(),
data: self.clone().into_optional(),
clear: vec![],
}
.p(server_id.to_string())
.await;
Ok(role_id)
}
/// Update server data
pub async fn update<'a>(
&mut self,
db: &Database,
server_id: &str,
role_id: &str,
partial: PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()> {
for field in &remove {
self.remove(field);
}
self.apply_options(partial.clone());
db.update_role(server_id, role_id, &partial, remove.clone())
.await?;
EventV1::ServerRoleUpdate {
id: server_id.to_string(),
role_id: role_id.to_string(),
data: partial,
clear: vec![],
}
.p(server_id.to_string())
.await;
Ok(())
}
/// Delete a role
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
EventV1::ServerRoleDelete {
id: server_id.to_string(),
role_id: role_id.to_string(),
}
.p(server_id.to_string())
.await;
db.delete_role(server_id, role_id).await
}
/// Remove field from Role
pub fn remove(&mut self, field: &FieldsRole) {
match field {
FieldsRole::Colour => self.colour = None,
}
}
}
impl Server {
/// Create a server
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_server(self).await
}
/// Update server data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()> {
for field in &remove {
self.remove(field);
}
self.apply_options(partial.clone());
db.update_server(&self.id, &partial, remove.clone()).await?;
EventV1::ServerUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
}
.p(self.id.clone())
.await;
Ok(())
}
/// Delete a server
pub async fn delete(self, db: &Database) -> Result<()> {
EventV1::ServerDelete {
id: self.id.clone(),
}
.p(self.id.clone())
.await;
db.delete_server(&self).await
}
/// Remove a field from Server
pub fn remove(&mut self, field: &FieldsServer) {
match field {
FieldsServer::Description => self.description = None,
FieldsServer::Categories => self.categories = None,
FieldsServer::SystemMessages => self.system_messages = None,
FieldsServer::Icon => self.icon = None,
FieldsServer::Banner => self.banner = None,
}
}
/// Set role permission on a server
pub async fn set_role_permission(
&mut self,
db: &Database,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
if let Some(role) = self.roles.get_mut(role_id) {
role.update(
db,
&self.id,
role_id,
PartialRole {
permissions: Some(permissions),
..Default::default()
},
vec![],
)
.await?;
Ok(())
} else {
Err(Error::NotFound)
}
}
/// Create a new member in a server
pub async fn create_member(
&self,
db: &Database,
user: User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
return Err(Error::Banned);
}
let member = Member::new(self.id.clone(), user.id.clone());
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let perm = perms(&user).server(self).member(&member);
let existing_channels = db.fetch_channels(&self.channels).await?;
for channel in existing_channels {
if perm
.clone()
.channel(&channel)
.has_permission(db, Permission::ViewChannel)
.await?
{
channels.push(channel);
}
}
}
let emojis = db.fetch_emoji_by_parent_id(&self.id).await?;
EventV1::ServerMemberJoin {
id: self.id.clone(),
user: user.id.clone(),
}
.p(self.id.clone())
.await;
EventV1::ServerCreate {
id: self.id.clone(),
server: self.clone(),
channels: channels.clone(),
emojis,
}
.private(user.id.clone())
.await;
if let Some(id) = self
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
Ok(channels)
}
/// Remove a member from a server
pub async fn remove_member(
&self,
db: &Database,
member: Member,
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&member.id).await?;
EventV1::ServerMemberLeave {
id: self.id.to_string(),
user: member.id.user.clone(),
}
.p(member.id.server)
.await;
if !silent {
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
RemovalIntention::Leave => x.user_left.as_ref(),
RemovalIntention::Kick => x.user_kicked.as_ref(),
RemovalIntention::Ban => x.user_banned.as_ref(),
}) {
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
}
Ok(())
}
/// Create ban
pub async fn ban_user(
self,
db: &Database,
id: MemberCompositeKey,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan { id, reason };
db.insert_ban(&ban).await?;
Ok(ban)
}
/// Ban a member from a server
pub async fn ban_member(
self,
db: &Database,
member: Member,
reason: Option<String>,
) -> Result<ServerBan> {
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
.await?;
self.ban_user(db, member.id, reason).await
}
}
impl SystemMessageChannels {
pub fn into_channel_ids(self) -> HashSet<String> {
let mut ids = HashSet::new();
if let Some(id) = self.user_joined {
ids.insert(id);
}
if let Some(id) = self.user_left {
ids.insert(id);
}
if let Some(id) = self.user_kicked {
ids.insert(id);
}
if let Some(id) = self.user_banned {
ids.insert(id);
}
ids
}
}
@@ -1 +0,0 @@
@@ -1,95 +0,0 @@
use std::collections::HashMap;
use iso8601_timestamp::Timestamp;
use crate::{
events::client::EventV1,
models::{
server_member::{FieldsMember, MemberCompositeKey, MemberWithRoles, PartialMember},
Member, Server,
},
Database, Result,
};
impl Member {
pub fn new(server_id: String, user_id: String) -> Self {
Self {
id: MemberCompositeKey {
server: server_id,
user: user_id,
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
}
}
/// Update member data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialMember,
remove: Vec<FieldsMember>,
) -> Result<()> {
for field in &remove {
self.remove(field);
}
self.apply_options(partial.clone());
db.update_member(&self.id, &partial, remove.clone()).await?;
EventV1::ServerMemberUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
}
.p(self.id.server.clone())
.await;
Ok(())
}
/// Get this user's current ranking
pub fn get_ranking(&self, server: &Server) -> i64 {
let mut value = i64::MAX;
for role in &self.roles {
if let Some(role) = server.roles.get(role) {
if role.rank < value {
value = role.rank;
}
}
}
value
}
/// Check whether this member is in timeout
pub fn in_timeout(&self) -> bool {
if let Some(timeout) = self.timeout {
*timeout > *Timestamp::now_utc()
} else {
false
}
}
pub fn remove(&mut self, field: &FieldsMember) {
match field {
FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None,
}
}
}
impl MemberWithRoles {
pub fn new(server_id: String, user_id: String) -> Self {
Self {
member: Member::new(server_id, user_id),
roles: HashMap::new(),
}
}
}
-510
View File
@@ -1,510 +0,0 @@
use crate::events::client::EventV1;
use crate::models::user::{
Badges, FieldsUser, PartialUser, Presence, RelationshipStatus, User, UserHint,
};
use crate::permissions::defn::UserPerms;
use crate::permissions::r#impl::user::get_relationship;
use crate::{perms, Database, Error, Result};
use futures::try_join;
use impl_ops::impl_op_ex_commutative;
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_database::RatelimitEventType;
use revolt_presence::filter_online;
use std::collections::HashSet;
use std::ops;
use std::time::Duration;
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
impl User {
/// Update user data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
for field in &remove {
self.remove(field);
}
self.apply_options(partial.clone());
db.update_user(&self.id, &partial, remove.clone()).await?;
EventV1::UserUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
event_id: Some(ulid::Ulid::new().to_string()),
}
.p_user(self.id.clone(), db)
.await;
Ok(())
}
/// Remove a field from User object
pub fn remove(&mut self, field: &FieldsUser) {
match field {
FieldsUser::Avatar => self.avatar = None,
FieldsUser::StatusText => {
if let Some(x) = self.status.as_mut() {
x.text = None;
}
}
FieldsUser::StatusPresence => {
if let Some(x) = self.status.as_mut() {
x.presence = None;
}
}
FieldsUser::ProfileContent => {
if let Some(x) = self.profile.as_mut() {
x.content = None;
}
}
FieldsUser::ProfileBackground => {
if let Some(x) = self.profile.as_mut() {
x.background = None;
}
}
FieldsUser::DisplayName => self.display_name = None,
}
}
/// Mutate the user object to remove redundant information
#[must_use]
pub fn foreign(mut self) -> User {
self.profile = None;
self.relations = None;
let mut badges = self.badges.unwrap_or(0);
if let Ok(id) = ulid::Ulid::from_string(&self.id) {
// Yes, this is hard-coded
// No, I don't care + ratio
if id.datetime().timestamp_millis() < 1629638578431 {
badges = badges + Badges::EarlyAdopter;
}
}
self.badges = Some(badges);
if let Some(status) = &self.status {
if let Some(presence) = &status.presence {
if presence == &Presence::Invisible {
self.status = None;
self.online = Some(false);
}
}
}
self
}
/// Fetch foreign users by a list of IDs
pub async fn fetch_foreign_users(db: &Database, user_ids: &[String]) -> Result<Vec<User>> {
let online_ids = filter_online(user_ids).await;
Ok(db
.fetch_users(user_ids)
.await?
.into_iter()
.map(|mut user| {
user.online = Some(online_ids.contains(&user.id));
user.foreign()
})
.collect::<Vec<User>>())
}
/// Mutate the user object to include relationship (if it does not already exist)
#[must_use]
pub fn with_relationship(self, perspective: &User) -> User {
let mut user = self.foreign();
if user.relationship.is_none() {
user.relationship = Some(get_relationship(perspective, &user.id));
}
user
}
/// Mutate user object with given permission
#[must_use]
pub fn apply_permission(mut self, permission: &UserPerms) -> User {
if !permission.get_view_profile() {
self.status = None;
}
self
}
/// Helper function to apply relationship and permission
#[must_use]
pub fn with_perspective(self, perspective: &User, permission: &UserPerms) -> User {
self.with_relationship(perspective)
.apply_permission(permission)
}
/// Helper function to calculate perspective
pub async fn with_auto_perspective(self, db: &Database, perspective: &User) -> User {
let user = self.with_relationship(perspective);
let permissions = perms(perspective).user(&user).calc_user(db).await;
user.apply_permission(&permissions)
}
/// Check whether two users have a mutual connection
///
/// This will check if user and user_b share a server or a group.
pub async fn has_mutual_connection(&self, db: &Database, user_b: &str) -> Result<bool> {
Ok(!db
.fetch_mutual_server_ids(&self.id, user_b)
.await?
.is_empty()
|| !db
.fetch_mutual_channel_ids(&self.id, user_b)
.await?
.is_empty())
}
/// Check if this user can acquire another server
pub async fn can_acquire_server(&self, db: &Database) -> Result<bool> {
// hardcoded max server count
// NB. fixed in new crate
Ok(db.fetch_server_count(&self.id).await? <= 100)
}
/// Sanitise and validate a username can be used
pub fn validate_username(username: String) -> Result<String> {
// Copy the username for validation
let username_lowercase = username.to_lowercase();
// Block homoglyphs
if decancer::cure(&username_lowercase).into_str() != username_lowercase {
return Err(Error::InvalidUsername);
}
// Ensure the username itself isn't blocked
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"];
for username in BLOCKED_USERNAMES {
if username_lowercase == *username {
return Err(Error::InvalidUsername);
}
}
// Ensure none of the following substrings show up in the username
const BLOCKED_SUBSTRINGS: &[&str] = &["```"];
for substr in BLOCKED_SUBSTRINGS {
if username_lowercase.contains(substr) {
return Err(Error::InvalidUsername);
}
}
Ok(username)
}
// Find a free discriminator for a given username
pub async fn find_discriminator(
db: &Database,
username: &str,
preferred: Option<(String, String)>,
) -> Result<String> {
let search_space: &HashSet<String> = &DISCRIMINATOR_SEARCH_SPACE_QUARK;
let used_discriminators: HashSet<String> = db
.fetch_discriminators_in_use(username)
.await?
.into_iter()
.collect();
let available_discriminators: Vec<&String> =
search_space.difference(&used_discriminators).collect();
if available_discriminators.is_empty() {
return Err(Error::UsernameTaken);
}
if let Some((preferred, target_id)) = preferred {
if available_discriminators.contains(&&preferred) {
return Ok(preferred);
} else {
let rvdb: revolt_database::Database = db.clone().into();
if rvdb
.has_ratelimited(
&target_id,
RatelimitEventType::DiscriminatorChange,
Duration::from_secs(60 * 60 * 24),
1,
)
.await
.map_err(Error::from_core)?
{
return Err(Error::DiscriminatorChangeRatelimited);
}
// FIXME: don't access directly?
#[allow(clippy::disallowed_methods)]
rvdb.insert_ratelimit_event(&revolt_database::RatelimitEvent {
id: ulid::Ulid::new().to_string(),
target_id,
event_type: RatelimitEventType::DiscriminatorChange,
})
.await
.map_err(Error::from_core)?;
}
}
let mut rng = rand::thread_rng();
Ok(available_discriminators
.choose(&mut rng)
.expect("we can assert this has an element")
.to_string())
}
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
let username = User::validate_username(username)?;
if self.username.to_lowercase() == username.to_lowercase() {
self.update(
db,
PartialUser {
username: Some(username),
..Default::default()
},
vec![],
)
.await
} else {
self.update(
db,
PartialUser {
discriminator: Some(
User::find_discriminator(
db,
&username,
Some((self.discriminator.to_string(), self.id.clone())),
)
.await?,
),
username: Some(username),
..Default::default()
},
vec![],
)
.await
}
}
/// Apply a certain relationship between two users
pub async fn apply_relationship(
&self,
db: &Database,
target: &mut User,
local: RelationshipStatus,
remote: RelationshipStatus,
) -> Result<()> {
if try_join!(
db.set_relationship(&self.id, &target.id, &local),
db.set_relationship(&target.id, &self.id, &remote)
)
.is_err()
{
return Err(Error::DatabaseError {
operation: "update_one",
with: "user",
});
}
EventV1::UserRelationship {
id: target.id.clone(),
user: self.clone().with_relationship(target),
status: remote,
}
.private(target.id.clone())
.await;
EventV1::UserRelationship {
id: self.id.clone(),
user: target.clone().with_relationship(self),
status: local.clone(),
}
.private(self.id.clone())
.await;
target.relationship.replace(local);
Ok(())
}
/// Add another user as a friend
pub async fn add_friend(&self, db: &Database, target: &mut User) -> Result<()> {
match get_relationship(self, &target.id) {
RelationshipStatus::User => Err(Error::NoEffect),
RelationshipStatus::Friend => Err(Error::AlreadyFriends),
RelationshipStatus::Outgoing => Err(Error::AlreadySentRequest),
RelationshipStatus::Blocked => Err(Error::Blocked),
RelationshipStatus::BlockedOther => Err(Error::BlockedByOther),
RelationshipStatus::Incoming => {
self.apply_relationship(
db,
target,
RelationshipStatus::Friend,
RelationshipStatus::Friend,
)
.await
}
RelationshipStatus::None => {
self.apply_relationship(
db,
target,
RelationshipStatus::Outgoing,
RelationshipStatus::Incoming,
)
.await
}
}
}
/// Remove another user as a friend
pub async fn remove_friend(&self, db: &Database, target: &mut User) -> Result<()> {
match get_relationship(self, &target.id) {
RelationshipStatus::Friend
| RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => {
self.apply_relationship(
db,
target,
RelationshipStatus::None,
RelationshipStatus::None,
)
.await
}
_ => Err(Error::NoEffect),
}
}
/// Block another user
pub async fn block_user(&self, db: &Database, target: &mut User) -> Result<()> {
match get_relationship(self, &target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
RelationshipStatus::BlockedOther => {
self.apply_relationship(
db,
target,
RelationshipStatus::Blocked,
RelationshipStatus::Blocked,
)
.await
}
RelationshipStatus::None
| RelationshipStatus::Friend
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing => {
self.apply_relationship(
db,
target,
RelationshipStatus::Blocked,
RelationshipStatus::BlockedOther,
)
.await
}
}
}
/// Unblock another user
pub async fn unblock_user(&self, db: &Database, target: &mut User) -> Result<()> {
match get_relationship(self, &target.id) {
RelationshipStatus::Blocked => match get_relationship(target, &self.id) {
RelationshipStatus::Blocked => {
self.apply_relationship(
db,
target,
RelationshipStatus::BlockedOther,
RelationshipStatus::Blocked,
)
.await
}
RelationshipStatus::BlockedOther => {
self.apply_relationship(
db,
target,
RelationshipStatus::None,
RelationshipStatus::None,
)
.await
}
_ => Err(Error::InternalError),
},
_ => Err(Error::NoEffect),
}
}
/// Check whether this user has another user blocked
pub fn has_blocked(&self, user: &str) -> bool {
matches!(
get_relationship(self, user),
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther
)
}
/// Mark as deleted
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
self.update(
db,
PartialUser {
username: Some(format!("Deleted User {}", self.id)),
flags: Some(2),
..Default::default()
},
vec![
FieldsUser::Avatar,
FieldsUser::StatusText,
FieldsUser::StatusPresence,
FieldsUser::ProfileContent,
FieldsUser::ProfileBackground,
],
)
.await
}
/// Find a user from a given token and hint
#[async_recursion]
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> {
match hint {
UserHint::Bot => {
let rvdb: revolt_database::Database = db.clone().into();
db.fetch_user(
&rvdb
.fetch_bot_by_token(token)
.await
.map_err(|_| Error::InternalError)?
.id,
)
.await
}
UserHint::User => db.fetch_user_by_token(token).await,
UserHint::Any => {
if let Ok(user) = User::from_token(db, token, UserHint::User).await {
Ok(user)
} else {
User::from_token(db, token, UserHint::Bot).await
}
}
}
}
}
pub static DISCRIMINATOR_SEARCH_SPACE_QUARK: Lazy<HashSet<String>> = Lazy::new(|| {
let mut set = (2..9999)
.map(|v| format!("{:0>4}", v))
.collect::<HashSet<String>>();
for discrim in [
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
] {
set.remove(&format!("{:0>4}", discrim));
}
set.into_iter().collect()
});
@@ -1,22 +0,0 @@
use crate::{events::client::EventV1, models::UserSettings, Database, Result};
#[async_trait]
pub trait UserSettingsImpl {
async fn set(self, db: &Database, user: &str) -> Result<()>;
}
#[async_trait]
impl UserSettingsImpl for UserSettings {
async fn set(self, db: &Database, user: &str) -> Result<()> {
db.set_user_settings(user, &self).await?;
EventV1::UserSettingsUpdate {
id: user.to_string(),
update: self,
}
.private(user.to_string())
.await;
Ok(())
}
}
-10
View File
@@ -1,10 +0,0 @@
mod dummy;
mod generic;
mod mongo;
#[cfg(feature = "rocket_impl")]
mod rocket;
pub use self::generic::users::user_settings::UserSettingsImpl;
pub use dummy::DummyDb;
pub use mongo::MongoDb;
@@ -1,90 +0,0 @@
use std::collections::HashMap;
use bson::{from_document, Document};
use futures::StreamExt;
use crate::{
models::stats::{Index, Stats},
AbstractStats, Error, Result,
};
use super::super::MongoDb;
#[async_trait]
impl AbstractStats for MongoDb {
async fn generate_stats(&self) -> Result<Stats> {
let mut indices = HashMap::new();
let mut coll_stats = HashMap::new();
let collection_names =
self.db()
.list_collection_names(None)
.await
.map_err(|_| Error::DatabaseError {
operation: "list_collection_names",
with: "database",
})?;
for collection in collection_names {
indices.insert(
collection.to_string(),
self.col::<Document>(&collection)
.aggregate(
vec![doc! {
"$indexStats": { }
}],
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "aggregate",
with: "col",
})?
.filter_map(|s| async { s.ok() })
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|doc| from_document(doc).ok())
.collect::<Vec<Index>>(),
);
coll_stats.insert(
collection.to_string(),
self.col::<Document>(&collection)
.aggregate(
vec![doc! {
"$collStats": {
"latencyStats": {
"histograms": true
},
"storageStats": {},
"count": {},
"queryExecStats": {}
}
}],
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "aggregate",
with: "col",
})?
.filter_map(|s| async { s.ok() })
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|doc| from_document(doc).ok())
.next()
.ok_or(Error::DatabaseError {
operation: "next aggregation",
with: "col",
})?,
);
}
Ok(Stats {
indices,
coll_stats,
})
}
}
@@ -1,317 +0,0 @@
use bson::{Bson, Document};
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
use crate::r#impl::mongo::IntoDocumentPath;
use crate::{AbstractChannel, AbstractServer, Error, OverrideField, Result};
use super::super::MongoDb;
static COL: &str = "channels";
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,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_invites",
})?;
// Delete unread message objects on channels.
self.col::<Document>("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_unreads",
})
.map(|_| ())?;
// update many attachments with parent id
// Delete all webhooks on this channel.
self.col::<Document>("webhooks")
.delete_many(
doc! {
"channel": &id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "webhooks",
})
.map(|_| ())
}
}
#[async_trait]
impl AbstractChannel for MongoDb {
async fn fetch_channel(&self, id: &str) -> Result<Channel> {
self.find_one_by_id(COL, id).await
}
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
self.find(
COL,
doc! {
"_id": {
"$in": ids
}
},
)
.await
}
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
self.insert_one(COL, channel).await.map(|_| ())
}
async fn update_channel(
&self,
id: &str,
channel: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
self.update_one_by_id(
COL,
id,
channel,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None,
)
.await
.map(|_| ())
}
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let id = channel.id().to_string();
let server_id = match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server)
}
_ => None,
};
// Delete invites and unreads.
self.delete_associated_channel_objects(Bson::String(id.to_string()))
.await?;
// Delete messages.
self.delete_bulk_messages(doc! {
"channel": &id
})
.await?;
// Remove from server object.
if let Some(server) = server_id {
let server = self.fetch_server(server).await?;
let mut update = doc! {
"$pull": {
"channels": &id
}
};
if let Some(sys) = &server.system_messages {
let mut unset = doc! {};
if let Some(cid) = &sys.user_joined {
if &id == cid {
unset.insert("system_messages.user_joined", 1_i32);
}
}
if let Some(cid) = &sys.user_left {
if &id == cid {
unset.insert("system_messages.user_left", 1_i32);
}
}
if let Some(cid) = &sys.user_kicked {
if &id == cid {
unset.insert("system_messages.user_kicked", 1_i32);
}
}
if let Some(cid) = &sys.user_banned {
if &id == cid {
unset.insert("system_messages.user_banned", 1_i32);
}
}
if !unset.is_empty() {
update.insert("$unset", unset);
}
}
self.col::<Document>("servers")
.update_one(
doc! {
"_id": server.id
},
update,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "servers",
})?;
}
// Delete associated attachments
self.delete_many_attachments(doc! {
"object_id": &id
})
.await?;
// Delete the channel itself
self.delete_one_by_id(COL, &id).await.map(|_| ())
}
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
self.find(
COL,
doc! {
"$or": [
{
"$or": [
{
"channel_type": "DirectMessage"
},
{
"channel_type": "Group"
}
],
"recipients": user_id
},
{
"channel_type": "SavedMessages",
"user": user_id
}
]
},
)
.await
}
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
self.find_one(
COL,
doc! {
"channel_type": "SavedMessages",
"user": user_id
},
)
.await
}
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
self.find_one(
COL,
if user_a == user_b {
doc! {
"channel_type": "SavedMessages",
"user": user_a
}
} else {
doc! {
"channel_type": "DirectMessage",
"recipients": {
"$all": [ user_a, user_b ]
}
}
},
)
.await
}
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$push": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})
}
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$pull": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})
}
async fn set_channel_role_permission(
&self,
channel: &str,
role: &str,
permissions: OverrideField,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! { "_id": channel },
doc! {
"$set": {
"role_permissions.".to_owned() + role: permissions
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})
}
}
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsChannel::DefaultPermissions => "default_permissions",
FieldsChannel::Description => "description",
FieldsChannel::Icon => "icon",
})
}
}
@@ -1,31 +0,0 @@
use crate::models::Invite;
use crate::{AbstractChannelInvite, Result};
use super::super::MongoDb;
static COL: &str = "channel_invites";
#[async_trait]
impl AbstractChannelInvite for MongoDb {
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
self.find_one_by_id(COL, code).await
}
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
self.insert_one(COL, invite).await.map(|_| ())
}
async fn delete_invite(&self, code: &str) -> Result<()> {
self.delete_one_by_id(COL, code).await.map(|_| ())
}
async fn fetch_invites_for_server(&self, server: &str) -> Result<Vec<Invite>> {
self.find(
COL,
doc! {
"server": server
},
)
.await
}
}
@@ -1,120 +0,0 @@
use bson::Document;
use mongodb::options::UpdateOptions;
use ulid::Ulid;
use crate::models::channel_unread::ChannelUnread;
use crate::{AbstractChannelUnread, Error, Result};
use super::super::MongoDb;
static COL: &str = "channel_unreads";
#[async_trait]
impl AbstractChannelUnread for MongoDb {
async fn acknowledge_message(&self, channel: &str, user: &str, message: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id.channel": channel,
"_id.user": user,
},
doc! {
"$unset": {
"mentions": 1_i32
},
"$set": {
"last_id": message
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel_unread",
})
}
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL)
.delete_many(
doc! {
"_id.channel": {
"$in": channels
},
"_id.user": user
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_unreads",
})?;
self.col::<Document>(COL)
.insert_many(
channels
.iter()
.map(|channel| {
doc! {
"_id": {
"channel": channel,
"user": user
},
"last_id": &current_time
}
})
.collect::<Vec<Document>>(),
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "channel_unreads",
})
.map(|_| ())
}
async fn add_mention_to_unread<'a>(
&self,
channel: &str,
user: &str,
ids: &[String],
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id.channel": channel,
"_id.user": user,
},
doc! {
"$push": {
"mentions": {
"$each": ids
}
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel_unread",
})
}
async fn fetch_unreads(&self, user: &str) -> Result<Vec<ChannelUnread>> {
self.find(
COL,
doc! {
"_id.user": user
},
)
.await
}
}
@@ -1,343 +0,0 @@
use bson::{to_bson, Document};
use futures::try_join;
use mongodb::options::FindOptions;
use crate::models::message::{
AppendMessage, Message, MessageQuery, MessageSort, MessageTimePeriod, PartialMessage,
};
use crate::r#impl::mongo::DocumentId;
use crate::{AbstractMessage, Error, Result};
use super::super::MongoDb;
static COL: &str = "messages";
impl MongoDb {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone();
for_attachments.insert(
"attachments",
doc! {
"$exists": 1_i32
},
);
// Check if there are any attachments we need to delete.
let message_ids_with_attachments = self
.find_with_options::<_, DocumentId>(
COL,
for_attachments,
FindOptions::builder()
.projection(doc! { "_id": 1_i32 })
.build(),
)
.await?
.into_iter()
.map(|x| x.id)
.collect::<Vec<String>>();
// If we found any, mark them as deleted.
if !message_ids_with_attachments.is_empty() {
self.col::<Document>("attachments")
.update_many(
doc! {
"message_id": {
"$in": message_ids_with_attachments
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "attachments",
})?;
}
// And then delete said messages.
self.col::<Document>(COL)
.delete_many(projection, None)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "messages",
})
}
}
#[async_trait]
impl AbstractMessage for MongoDb {
async fn fetch_message(&self, id: &str) -> Result<Message> {
self.find_one_by_id(COL, id).await
}
async fn insert_message(&self, message: &Message) -> Result<()> {
self.insert_one(COL, message).await.map(|_| ())
}
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
self.update_one_by_id(COL, id, message, vec![], None)
.await
.map(|_| ())
}
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
let mut query = doc! {};
if let Some(embeds) = &append.embeds {
if !embeds.is_empty() {
query.insert(
"$push",
doc! {
"embeds": {
"$each": to_bson(embeds)
.map_err(|_| Error::DatabaseError {
operation: "to_bson",
with: "embeds"
})?
}
},
);
}
}
if query.is_empty() {
return Ok(());
}
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
query,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
.map(|_| ())
}
async fn delete_message(&self, id: &str) -> Result<()> {
self.delete_one_by_id(COL, id).await.map(|_| ())
}
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> {
self.delete_bulk_messages(doc! {
"channel": channel,
"_id": {
"$in": ids
}
})
.await
}
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
let mut filter = doc! {};
// 1. Apply message filters
if let Some(channel) = query.filter.channel {
filter.insert("channel", channel);
}
if let Some(author) = query.filter.author {
filter.insert("author", author);
}
let is_search_query = if let Some(query) = query.filter.query {
filter.insert(
"$text",
doc! {
"$search": query
},
);
true
} else {
false
};
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
// 3. Apply message time period
match query.time_period {
MessageTimePeriod::Relative { nearby } => {
// 3.1. Prepare filters
let mut older_message_filter = filter.clone();
let mut newer_message_filter = filter;
older_message_filter.insert(
"_id",
doc! {
"$lt": &nearby
},
);
newer_message_filter.insert(
"_id",
doc! {
"$gte": &nearby
},
);
// 3.2. Execute in both directions
let (a, b) = try_join!(
self.find_with_options::<_, Message>(
COL,
newer_message_filter,
FindOptions::builder()
.limit(limit / 2 + 1)
.sort(doc! {
"_id": 1_i32
})
.build(),
),
self.find_with_options::<_, Message>(
COL,
older_message_filter,
FindOptions::builder()
.limit(limit / 2)
.sort(doc! {
"_id": -1_i32
})
.build(),
)
)?;
Ok([a, b].concat())
}
MessageTimePeriod::Absolute {
before,
after,
sort,
} => {
// 3.1. Apply message ID filter
if let Some(doc) = match (before, after) {
(Some(before), Some(after)) => Some(doc! {
"$lt": before,
"$gt": after
}),
(Some(before), _) => Some(doc! {
"$lt": before
}),
(_, Some(after)) => Some(doc! {
"$gt": after
}),
_ => None,
} {
filter.insert("_id", doc);
}
// 3.2. Execute with given message sort
self.find_with_options(
COL,
filter,
FindOptions::builder()
.limit(limit)
.sort(match sort.unwrap_or(MessageSort::Latest) {
// Sort by relevance, fallback to latest
MessageSort::Relevance => {
if is_search_query {
doc! {
"score": {
"$meta": "textScore"
}
}
} else {
doc! {
"_id": -1_i32
}
}
}
// Sort by latest first
MessageSort::Latest => doc! {
"_id": -1_i32
},
// Sort by oldest first
MessageSort::Oldest => doc! {
"_id": 1_i32
},
})
.build(),
)
.await
}
}
}
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$addToSet": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$pull": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$unset": {
format!("reactions.{emoji}"): 1
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
}
@@ -1,148 +0,0 @@
use bson::Document;
use crate::models::attachment::File;
use crate::{AbstractAttachment, Error, Result};
use super::super::MongoDb;
static COL: &str = "attachments";
impl MongoDb {
pub async fn delete_many_attachments(&self, projection: Document) -> Result<()> {
self.col::<Document>(COL)
.update_many(
projection,
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "attachment",
})
}
}
#[async_trait]
impl AbstractAttachment for MongoDb {
async fn find_and_use_attachment(
&self,
attachment_id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
) -> Result<File> {
let key = format!("{parent_type}_id");
match self
.find_one::<File>(
COL,
doc! {
"_id": attachment_id,
"tag": tag,
&key: {
"$exists": false
}
},
)
.await
{
Ok(file) => {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": &file.id
},
doc! {
"$set": {
key: parent_id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
Ok(file)
}
Err(Error::NotFound) => Err(Error::UnknownAttachment),
Err(error) => Err(error),
}
}
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
self.insert_one(COL, attachment).await.map(|_| ())
}
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$set": {
"reported": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})
}
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})
}
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.update_many(
doc! {
"_id": {
"$in": ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update",
with: "attachments",
})
}
}
@@ -1,69 +0,0 @@
use bson::Document;
use crate::models::Emoji;
use crate::{AbstractEmoji, Error, Result};
use super::super::MongoDb;
static COL: &str = "emojis";
#[async_trait]
impl AbstractEmoji for MongoDb {
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
self.find_one_by_id(COL, id).await
}
/// Fetch emoji by their ids
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
self.find(
COL,
doc! {
"parent.id": parent_id
},
)
.await
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
self.find(
COL,
doc! {
"parent.id": {
"$in": parent_ids
}
},
)
.await
}
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
self.insert_one(COL, emoji).await.map(|_| ())
}
/// Delete an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": &emoji.id
},
doc! {
"$set": {
"parent": {
"type": "Detached"
}
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "emojis",
})
}
}
-263
View File
@@ -1,263 +0,0 @@
use std::ops::Deref;
use bson::{to_document, Document};
use futures::StreamExt;
use mongodb::{
options::{FindOneOptions, FindOptions},
results::{DeleteResult, InsertOneResult, UpdateResult},
};
use rocket::serde::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result};
pub mod admin {
pub mod stats;
}
pub mod media {
pub mod attachment;
pub mod emoji;
}
pub mod channels {
pub mod channel;
pub mod channel_invite;
pub mod channel_unread;
pub mod message;
}
pub mod servers {
pub mod server;
pub mod server_ban;
pub mod server_member;
}
pub mod users {
pub mod user;
pub mod user_settings;
}
pub mod safety {
pub mod report;
pub mod snapshot;
}
#[derive(Debug, Clone)]
pub struct MongoDb(pub mongodb::Client);
impl AbstractDatabase for MongoDb {}
impl Deref for MongoDb {
type Target = mongodb::Client;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl MongoDb {
pub fn db(&self) -> mongodb::Database {
self.database("revolt")
}
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
self.db().collection(collection)
}
async fn insert_one<T: Serialize>(
&self,
collection: &'static str,
document: T,
) -> Result<InsertOneResult> {
self.col::<T>(collection)
.insert_one(document, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: collection,
})
}
async fn find_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
options: O,
) -> Result<Vec<T>>
where
O: Into<Option<FindOptions>>,
{
let result = self.col::<T>(collection).find(projection, options).await;
Ok(if cfg!(debug_assertions) {
result.unwrap()
} else {
result.map_err(|_| Error::DatabaseError {
operation: "find",
with: collection,
})?
}
.filter_map(|s| async {
if cfg!(debug_assertions) {
// Hard fail on invalid documents
Some(s.unwrap())
} else {
s.ok()
}
})
.collect::<Vec<T>>()
.await)
}
async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
) -> Result<Vec<T>> {
self.find_with_options(collection, projection, None).await
}
async fn find_one_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
options: O,
) -> Result<T>
where
O: Into<Option<FindOneOptions>>,
{
self.col::<T>(collection)
.find_one(projection, options)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: collection,
})?
.ok_or(Error::NotFound)
}
async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
) -> Result<T> {
self.find_one_with_options(collection, projection, None)
.await
}
async fn find_one_by_id<T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
id: &str,
) -> Result<T> {
self.find_one(
collection,
doc! {
"_id": id
},
)
.await
}
async fn update_one<P, T: Serialize>(
&self,
collection: &'static str,
projection: Document,
partial: T,
remove: Vec<&dyn IntoDocumentPath>,
prefix: P,
) -> Result<UpdateResult>
where
P: Into<Option<String>>,
{
let prefix = prefix.into();
let mut unset = doc! {};
for field in remove {
if let Some(path) = field.as_path() {
if let Some(prefix) = &prefix {
unset.insert(prefix.to_owned() + path, 1_i32);
} else {
unset.insert(path, 1_i32);
}
}
}
let query = doc! {
"$unset": unset,
"$set": if let Some(prefix) = &prefix {
to_document(&prefix_keys(&partial, prefix))
} else {
to_document(&partial)
}.map_err(|_| Error::DatabaseError {
operation: "to_document",
with: collection
})?
};
self.col::<Document>(collection)
.update_one(projection, query, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: collection,
})
}
async fn update_one_by_id<P, T: Serialize>(
&self,
collection: &'static str,
id: &str,
partial: T,
remove: Vec<&dyn IntoDocumentPath>,
prefix: P,
) -> Result<UpdateResult>
where
P: Into<Option<String>>,
{
self.update_one(
collection,
doc! {
"_id": id
},
partial,
remove,
prefix,
)
.await
}
async fn delete_one(
&self,
collection: &'static str,
projection: Document,
) -> Result<DeleteResult> {
self.col::<Document>(collection)
.delete_one(projection, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: collection,
})
}
async fn delete_one_by_id(&self, collection: &'static str, id: &str) -> Result<DeleteResult> {
self.delete_one(
collection,
doc! {
"_id": id
},
)
.await
}
}
#[derive(Deserialize)]
pub struct DocumentId {
#[serde(rename = "_id")]
pub id: String,
}
pub trait IntoDocumentPath: Send + Sync {
fn as_path(&self) -> Option<&'static str>;
}
@@ -1,28 +0,0 @@
use crate::models::report::PartialReport;
use crate::models::Report;
use crate::{AbstractReport, Result};
use super::super::MongoDb;
static COL: &str = "safety_reports";
#[async_trait]
impl AbstractReport for MongoDb {
async fn insert_report(&self, report: &Report) -> Result<()> {
self.insert_one(COL, report).await.map(|_| ())
}
async fn update_report(&self, id: &str, report: &PartialReport) -> Result<()> {
self.update_one_by_id(COL, id, report, vec![], None)
.await
.map(|_| ())
}
async fn fetch_report(&self, report_id: &str) -> Result<Report> {
self.find_one_by_id(COL, report_id).await
}
async fn fetch_reports(&self) -> Result<Vec<Report>> {
self.find(COL, doc! {}).await
}
}
Loaded 100 of 174 files, more files were not shown because too many files have changed in this diff. Show more