Compare commits

...
20 Commits
Author SHA1 Message Date
Paul Makles cb4435d700 chore: bump version to 0.7.3 2024-06-09 13:49:38 +01:00
Paul Makles 4c6e78e1a5 chore: relicense some core crates to MIT
chore: update/create LICENSE files
closes #278
closes #277
2024-06-09 13:47:22 +01:00
Paul Makles da9a91e05f fix(bonfire): send InvalidSession error if token is not sent successfully
closes #289
2024-06-09 13:42:12 +01:00
Paul Makles 506968634e fix(core/database): set do not set online status if presence is Invisible
closes #34
2024-06-09 13:41:51 +01:00
Paul Makles a5d0cdf0dd fix(core/database): skip insertion / deletion if no invites need to be corrected 2024-06-04 18:46:11 +01:00
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
179 changed files with 811 additions and 13200 deletions
+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/workflows/docker.yml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
+8
View File
@@ -2,6 +2,14 @@ name: Rust build, test, and generate specification
on:
push:
paths-ignore:
- ".github/**"
- "!.github/workflows/rust.yaml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request:
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]
resolver = "2"
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
[patch.crates-io]
# 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 crates/bonfire/Cargo.toml ./crates/bonfire/
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/database/Cargo.toml ./crates/core/database/
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 crates/bonfire/Cargo.toml ./crates/bonfire/
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/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/
+4 -1
View File
@@ -1,4 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE
With the exception of crates that specify their own LICENSE file,
the following license applies to the source code of this project.
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+10 -11
View File
@@ -2,17 +2,16 @@
This is a monorepo for the Revolt backend.
| Crate | Path | Description |
| ------------------ | -------------------------------------------------- | --------------------------------- |
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |
| `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
| `quark` | [crates/quark](crates/quark) | Models and logic (**DEPRECATED**) |
| Crate | Path | Description |
| ------------------ | -------------------------------------------------- | ----------------------------- |
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |
| `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
+10 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.7.0"
version = "0.7.3"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -9,14 +9,15 @@ edition = "2021"
[dependencies]
# util
log = "*"
sentry = "0.31.5"
lru = "0.7.6"
ulid = "0.5.0"
once_cell = "1.9.0"
redis-kiss = "0.1.4"
# parsing
querystring = "1.1.0"
# quark
revolt-quark = { path = "../quark" }
# serde
bincode = "1.3.3"
serde_json = "1.0.79"
@@ -33,8 +34,12 @@ async-std = { version = "1.8.0", features = [
] }
# 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.3", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
sentry = "0.31.5"
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }
+5 -5
View File
@@ -1,6 +1,6 @@
use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender;
use revolt_quark::{Error, Result};
use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize};
/// Enumeration of supported protocol formats
@@ -37,16 +37,16 @@ impl ProtocolConfiguration {
match self.format {
ProtocolFormat::Json => {
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 {
Err(Error::InternalError)
Err(create_error!(InternalError))
}
}
ProtocolFormat::Msgpack => {
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 {
Err(Error::InternalError)
Err(create_error!(InternalError))
}
}
}
+4 -2
View File
@@ -1,5 +1,5 @@
use once_cell::sync::OnceCell;
use revolt_quark::{Database, DatabaseInfo};
use revolt_database::{Database, DatabaseInfo};
static DBCONN: OnceCell<Database> = OnceCell::new();
@@ -10,7 +10,9 @@ pub async fn connect() {
.await
.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.
@@ -1,21 +1,15 @@
use std::collections::HashSet;
use crate::{
get_relationship,
models::{
server_member::FieldsMember,
user::{PartialUser, Presence, RelationshipStatus},
Channel, Member, User,
},
perms, Database, Permission, Result,
use revolt_database::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
MemberCompositeKey, Presence, RelationshipStatus,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_presence::filter_online;
use revolt_result::Result;
use super::{
client::EventV1,
state::{Cache, State},
};
use super::state::{Cache, State};
/// Cache Manager
impl Cache {
@@ -25,20 +19,22 @@ impl Cache {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
let member = self.members.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 {
perms.member.set_ref(member);
query = query.member(&member);
}
if let Some(server) = server {
perms.server.set_ref(server);
query = query.server(server);
}
perms
.has_permission(db, Permission::ViewChannel)
calculate_channel_permissions(&mut query)
.await
.unwrap_or_default()
.has_channel_permission(ChannelPermission::ViewChannel)
}
_ => true,
}
@@ -63,7 +59,7 @@ impl Cache {
/// Check whether we can subscribe to another user
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
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::Incoming
| RelationshipStatus::Outgoing
@@ -95,7 +91,7 @@ impl Cache {
impl State {
/// Generate a Ready packet for the current user
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.
let mut user_ids: HashSet<String> = user
@@ -141,7 +137,6 @@ impl State {
// Fetch presence data for known users.
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
user.online = Some(true);
// Fetch user data.
let users = db
@@ -154,15 +149,14 @@ impl State {
.await?;
// Fetch customisations.
let emojis = Some(
db.fetch_emoji_by_parent_ids(
let emojis = db
.fetch_emoji_by_parent_ids(
&servers
.iter()
.map(|x| x.id.to_string())
.collect::<Vec<String>>(),
)
.await?,
);
.await?;
// Copy data into local state cache.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
@@ -176,17 +170,16 @@ impl State {
.collect();
// Make all users appear from our perspective.
let mut users: Vec<User> = users
let mut users: Vec<v0::User> = users
.into_iter()
.map(|mut x| {
x.online = Some(online_ids.contains(&x.id));
x.with_relationship(&user)
.map(|other_user| {
let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online)
})
.collect();
// Make sure we see our own user correctly.
user.relationship = Some(RelationshipStatus::User);
users.push(user.foreign());
users.push(user.into_self().await);
// Set subscription state internally.
self.reset_state();
@@ -206,10 +199,10 @@ impl State {
Ok(EventV1::Ready {
users,
servers,
channels,
members,
emojis,
servers: servers.into_iter().map(Into::into).collect(),
channels: channels.into_iter().map(Into::into).collect(),
members: members.into_iter().map(Into::into).collect(),
emojis: emojis.into_iter().map(Into::into).collect(),
})
}
@@ -271,7 +264,7 @@ impl State {
.insert(channel.id().to_string(), channel.clone());
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 {
id: self.cache.user_id.clone(),
data: PartialUser {
data: v0::PartialUser {
online: Some(target),
..Default::default()
},
@@ -344,7 +337,7 @@ impl State {
EventV1::ChannelCreate(channel) => {
let id = channel.id().to_string();
self.insert_subscription(id.clone());
self.cache.channels.insert(id, channel.clone());
self.cache.channels.insert(id, channel.clone().into());
}
EventV1::ChannelUpdate {
id, data, clear, ..
@@ -357,10 +350,10 @@ impl State {
if let Some(channel) = self.cache.channels.get_mut(id) {
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) {
@@ -374,7 +367,7 @@ impl State {
if could_view != can_view {
if can_view {
queue_add = Some(id.clone());
*event = EventV1::ChannelCreate(channel.clone());
*event = EventV1::ChannelCreate(channel.clone().into());
} else {
queue_remove = Some(id.clone());
*event = EventV1::ChannelDelete { id: id.clone() };
@@ -404,14 +397,20 @@ impl State {
emojis: _,
} => {
self.insert_subscription(id.clone());
self.cache.servers.insert(id.clone(), server.clone());
let member = Member::new(id.clone(), self.cache.user_id.clone());
self.cache.servers.insert(id.clone(), server.clone().into());
let member = Member {
id: MemberCompositeKey {
server: server.id.clone(),
user: self.cache.user_id.clone(),
},
..Default::default()
};
self.cache.members.insert(id.clone(), member);
for channel in channels {
self.cache
.channels
.insert(channel.id().to_string(), channel.clone());
.insert(channel.id().to_string(), channel.clone().into());
}
queue_server = Some(id.clone());
@@ -421,10 +420,10 @@ impl State {
} => {
if let Some(server) = self.cache.servers.get_mut(id) {
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() {
@@ -462,13 +461,13 @@ impl State {
if id.user == self.cache.user_id {
if let Some(member) = self.cache.members.get_mut(&id.server) {
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());
}
}
@@ -483,10 +482,10 @@ impl State {
if let Some(server) = self.cache.servers.get_mut(id) {
if let Some(role) = server.roles.get_mut(role_id) {
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;
}
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) {
self.insert_subscription(id.clone());
@@ -551,39 +550,3 @@ impl State {
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 lru::LruCache;
use crate::models::{Channel, Member, Server, User};
use revolt_database::{Channel, Member, Server, User};
/// Enumeration representing some change in subscriptions
pub enum SubscriptionStateChange {
+2 -1
View File
@@ -7,6 +7,7 @@ use revolt_presence::clear_region;
extern crate log;
pub mod config;
pub mod events;
mod database;
mod websocket;
@@ -14,7 +15,7 @@ mod websocket;
#[async_std::main]
async fn main() {
// Configure requirements for Bonfire.
revolt_quark::configure!();
revolt_config::configure!(events);
database::connect().await;
// Clean up the current region information.
+8 -10
View File
@@ -11,21 +11,18 @@ use futures::{
stream::{SplitSink, SplitStream},
FutureExt, SinkExt, StreamExt, TryStreamExt,
};
use revolt_presence::{create_session, delete_session};
use revolt_quark::{
events::{
client::EventV1,
server::ClientMessage,
state::{State, SubscriptionStateChange},
},
models::{user::UserHint, User},
redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI},
Database,
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_database::{
events::{client::EventV1, server::ClientMessage},
Database, User, UserHint,
};
use revolt_presence::{create_session, delete_session};
use async_std::{net::TcpStream, sync::Mutex};
use revolt_result::create_error;
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
use crate::events::state::{State, SubscriptionStateChange};
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
@@ -73,6 +70,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
// Try to authenticate the user.
let Some(token) = config.get_session_token().as_ref() else {
write.send(config.encode(&create_error!(InvalidSession))).await.ok();
return;
};
let user = match User::from_token(db, token, UserHint::Any).await {
+2 -2
View File
@@ -1,8 +1,8 @@
[package]
name = "revolt-config"
version = "0.7.0"
version = "0.7.3"
edition = "2021"
license = "AGPL-3.0-or-later"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Configuration"
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+4 -3
View File
@@ -1,5 +1,3 @@
sentry_dsn = ""
[database]
mongodb = "mongodb://database"
redis = "redis://redis/"
@@ -14,7 +12,6 @@ voso_legacy = ""
voso_legacy_ws = ""
[api]
staging = false
[api.registration]
invite_only = false
@@ -68,3 +65,7 @@ background_size = 6000000
icon_size = 2500000
banner_size = 6000000
emoji_size = 500000
[sentry]
api = ""
events = ""
+19 -12
View File
@@ -6,6 +6,9 @@ use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
#[cfg(not(debug_assertions))]
use std::env;
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
RwLock::new({
let mut builder = Config::builder().add_source(File::from_str(
@@ -93,7 +96,6 @@ pub struct ApiWorkers {
#[derive(Deserialize, Debug, Clone)]
pub struct Api {
pub staging: bool,
pub registration: ApiRegistration,
pub smtp: ApiSmtp,
pub vapid: ApiVapid,
@@ -138,13 +140,19 @@ pub struct Features {
pub webhooks_enabled: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Sentry {
pub api: String,
pub events: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Settings {
pub database: Database,
pub hosts: Hosts,
pub api: Api,
pub features: Features,
pub sentry_dsn: String,
pub sentry: Sentry,
}
impl Settings {
@@ -188,7 +196,7 @@ pub async fn config() -> Settings {
}
/// 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();
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();
log::info!("Starting {release}");
let config = config().await;
if config.sentry_dsn.is_empty() {
if dsn.is_empty() {
None
} else {
Some(sentry::init((
config.sentry_dsn,
dsn,
sentry::ClientOptions {
release: Some(release.into()),
..Default::default()
@@ -218,12 +225,12 @@ pub async fn setup_logging(release: &'static str) -> Option<sentry::ClientInitGu
#[macro_export]
macro_rules! configure {
() => {
let _sentry = $crate::setup_logging(concat!(
env!("CARGO_PKG_NAME"),
"@",
env!("CARGO_PKG_VERSION")
))
($application: ident) => {
let config = $crate::config().await;
let _sentry = $crate::setup_logging(
concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")),
config.sentry.$application,
)
.await;
};
}
+6 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.7.0"
version = "0.7.3"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.7.0", path = "../config" }
revolt-result = { version = "0.7.0", path = "../result" }
revolt-models = { version = "0.7.0", path = "../models", features = [
revolt-config = { version = "0.7.3", path = "../config" }
revolt-result = { version = "0.7.3", path = "../result" }
revolt-models = { version = "0.7.3", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.7.0", path = "../presence" }
revolt-permissions = { version = "0.7.0", path = "../permissions", features = [
revolt-presence = { version = "0.7.3", path = "../presence" }
revolt-permissions = { version = "0.7.3", path = "../permissions", features = [
"serde",
"bson",
] }
+4 -4
View File
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use revolt_models::v0::{
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,
UserSettings, Webhook,
};
@@ -48,17 +48,17 @@ pub enum EventV1 {
/// Successfully authenticated
Authenticated,
/* /// Basic data to cache
/// Basic data to cache
Ready {
users: Vec<User>,
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
emojis: Option<Vec<Emoji>>,
emojis: Vec<Emoji>,
},
/// Ping response
Pong { data: Ping }, */
Pong { data: Ping },
/// New message
Message(Message),
+1
View File
@@ -1 +1,2 @@
pub mod client;
pub mod server;
@@ -5,8 +5,9 @@ use crate::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
};
use bson::oid::ObjectId;
use futures::StreamExt;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
@@ -19,7 +20,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 26;
pub const LATEST_REVISION: i32 = 27;
pub async fn migrate_database(db: &MongoDb) {
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.");
}
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`.
// Reminder to update LATEST_REVISION when adding new migrations.
@@ -10,6 +10,7 @@ static ALPHABET: [char; 54] = [
auto_derived!(
/// Invite
#[serde(tag = "type")]
pub enum Invite {
/// Invite to a specific server channel
Server {
@@ -122,6 +122,16 @@ auto_derived!(
/// Id of the owner of this bot
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(|| {
@@ -266,6 +276,30 @@ impl User {
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
/// (while optimising the online ID query)
pub async fn fetch_many_ids_as_mutuals(
@@ -60,7 +60,7 @@ impl AbstractUsers for MongoDb {
.map_err(|_| create_database_error!("find_one", "sessions"))?
.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
+318 -3
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 {
fn from(value: crate::PartialChannel) -> Self {
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 {
fn from(value: FieldsChannel) -> Self {
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 {
fn from(value: crate::Metadata) -> Self {
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 {
fn from(value: crate::Message) -> Self {
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 {
fn from(value: crate::PartialMember) -> Self {
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 {
fn from(value: crate::MemberCompositeKey) -> Self {
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 {
fn from(value: crate::FieldsMember) -> Self {
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 {
fn from(value: crate::PartialServer) -> Self {
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 {
fn from(value: crate::FieldsServer) -> Self {
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 {
fn from(value: crate::PartialRole) -> Self {
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 {
fn from(value: crate::FieldsRole) -> Self {
match value {
@@ -748,6 +995,15 @@ impl crate::User {
vec![]
},
badges: self.badges.unwrap_or_default() as u32,
online: can_see_profile
&& revolt_presence::is_online(&self.id).await
&& !matches!(
self.status,
Some(crate::UserStatus {
presence: Some(crate::Presence::Invisible),
..
})
),
status: if can_see_profile {
self.status.map(|status| status.into())
} else {
@@ -762,7 +1018,6 @@ impl crate::User {
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship,
online: can_see_profile && revolt_presence::is_online(&self.id).await,
id: self.id,
}
}
@@ -817,6 +1072,15 @@ impl crate::User {
vec![]
},
badges: self.badges.unwrap_or_default() as u32,
online: can_see_profile
&& is_online
&& !matches!(
self.status,
Some(crate::UserStatus {
presence: Some(crate::Presence::Invisible),
..
})
),
status: if can_see_profile {
self.status.map(|status| status.into())
} else {
@@ -831,7 +1095,6 @@ impl crate::User {
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship,
online: can_see_profile && is_online,
id: self.id,
}
}
@@ -852,13 +1115,20 @@ impl crate::User {
})
.unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32,
online: revolt_presence::is_online(&self.id).await
&& !matches!(
self.status,
Some(crate::UserStatus {
presence: Some(crate::Presence::Invisible),
..
})
),
status: self.status.map(|status| status.into()),
profile: self.profile.map(|profile| profile.into()),
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship: RelationshipStatus::User,
online: revolt_presence::is_online(&self.id).await,
id: self.id,
}
}
@@ -871,6 +1141,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 {
fn from(value: crate::PartialUser) -> Self {
PartialUser {
@@ -977,6 +1266,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 {
fn from(value: crate::UserProfile) -> Self {
UserProfile {
@@ -986,6 +1284,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 {
fn from(value: crate::BotInformation) -> Self {
BotInformation {
@@ -993,3 +1300,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,
}
}
}
+4 -4
View File
@@ -1,8 +1,8 @@
[package]
name = "revolt-models"
version = "0.7.0"
version = "0.7.3"
edition = "2021"
license = "AGPL-3.0-or-later"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: API Models"
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.7.0", path = "../config" }
revolt-permissions = { version = "0.7.0", path = "../permissions" }
revolt-config = { version = "0.7.3", path = "../config" }
revolt-permissions = { version = "0.7.3", path = "../permissions" }
# Utility
regex = "1"
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+10 -1
View File
@@ -30,7 +30,16 @@ macro_rules! auto_derived_partial {
#[derive(
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]
#[opt_skip_serializing_none]
#[opt_some_priority]
@@ -2,6 +2,7 @@ use super::{Channel, File, Server, User};
auto_derived!(
/// Invite
#[serde(tag = "type")]
pub enum Invite {
/// Invite to a specific server channel
Server {
+3 -3
View File
@@ -1,8 +1,8 @@
[package]
name = "revolt-permissions"
version = "0.7.0"
version = "0.7.3"
edition = "2021"
license = "AGPL-3.0-or-later"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Permission Logic"
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Core
revolt-result = { version = "0.7.0", path = "../result" }
revolt-result = { version = "0.7.3", path = "../result" }
# Utility
auto_ops = "0.3.0"
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.7.0"
version = "0.7.3"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+3 -3
View File
@@ -1,8 +1,8 @@
[package]
name = "revolt-result"
version = "0.7.0"
version = "0.7.3"
edition = "2021"
license = "AGPL-3.0-or-later"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Result and Error types"
@@ -12,7 +12,7 @@ description = "Revolt Backend: Result and Error types"
serde = ["dep:serde"]
schemas = ["dep:schemars"]
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"]
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.7.0"
version = "0.7.3"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
+1 -1
View File
@@ -194,7 +194,7 @@ pub async fn authifier_config() -> AuthifierConfig {
#[launch]
async fn rocket() -> _ {
// Configure logging and environment
revolt_config::configure!();
revolt_config::configure!(api);
// Start web server
web().await
-1
View File
@@ -101,7 +101,6 @@ impl TestHarness {
let mut stream = self.sub.on_message();
while let Some(item) = stream.next().await {
let item = item.unwrap();
let msg_topic = item.get_channel_name();
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
}
}

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