Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb4435d700 | ||
|
|
4c6e78e1a5 | ||
|
|
da9a91e05f | ||
|
|
506968634e | ||
|
|
a5d0cdf0dd | ||
|
|
c59aad43c8 | ||
|
|
19e72babc9 | ||
|
|
bfcdb13d6d | ||
|
|
878c1a83ad | ||
|
|
471e0f55e4 | ||
|
|
08127218ce | ||
|
|
b69202c222 | ||
|
|
b3494b12fc | ||
|
|
7703475868 | ||
|
|
4be3bdc4c3 | ||
|
|
3e26e7e89d | ||
|
|
13b95d383e | ||
|
|
3c76005c33 | ||
|
|
3065e153d5 | ||
|
|
a1eeedabb9 |
@@ -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
|
||||
@@ -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
|
||||
@@ -10,6 +10,7 @@ on:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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,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 = ""
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
] }
|
||||
|
||||
@@ -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 +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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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>"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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*
|
||||
@@ -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 +0,0 @@
|
||||
../../LICENSE
|
||||
@@ -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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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
|
||||
@@ -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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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>
|
||||
@@ -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
|
||||
@@ -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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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">
|
||||
 
|
||||
</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>
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
@@ -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()),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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": ¤t_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
|
||||
}
|
||||
}
|
||||