Compare commits

...
35 Commits
Author SHA1 Message Date
Paul Makles 0fc59b8e2a fix: add validator dependency to database 2024-04-07 23:49:09 +01:00
Paul Makles 265d3ba8ef chore: bump version to 0.7.0 2024-04-07 23:45:29 +01:00
Paul Makles 20d398d02c chore: remove quark dependency from delta 🎉
closes #283

fix: allow setting port and use_tls from config
closes #143
2024-04-07 23:41:31 +01:00
Paul Makles 6bc74749d2 refactor(quark): port message_query, message_search
#283
2024-04-07 23:13:45 +01:00
Paul Makles 301676fb54 refactor(quark): port report_content & code clean-up
#283
2024-04-07 22:57:55 +01:00
Paul Makles 6b488f347e feat(core/database): implement reports & snapshots 2024-04-07 22:42:46 +01:00
Paul Makles 1a8e43d280 ci: also update the first job 2024-04-07 22:29:24 +01:00
Paul Makles acb4190f70 fix: delete associated server objects 2024-04-07 22:26:56 +01:00
Paul Makles 9e42567050 refactor(quark): port roles_create, roles_delete, roles_edit, roles_fetch, server_ack, server_delete, server_edit, server_fetch
#283
2024-04-07 22:26:56 +01:00
Sophie L 78fb6e1982 fix(ci): remnants from branch rename 2024-04-07 22:26:31 +01:00
Paul Makles 9de963fb56 refactor(quark): port member_edit, member_experimental_query, member_fetch_all, member_remove, permissions_set_default, permissions_set
#283
2024-04-07 21:35:45 +01:00
Paul Makles 03340a2b5a refactor(quark): port ban_create, ban_list, ban_remove, emoji_list, invites_fetch
#283
2024-04-07 20:48:36 +01:00
Paul Makles b2d3344ddd refactor(quark): port edit_user, fetch_dms, fetch_profile, fetch_self, fetch_user_flags, fetch_user, find_mutual, open_dm
#283
2024-04-07 19:38:08 +01:00
Paul Makles 49bb235938 refactor(quark): port add_friend, block_user, change_username, remove_friend, send_friend_request, unblock_user
#283
2024-04-07 18:30:59 +01:00
Paul Makles 4e7049d51e fix: fully qualify create_error in create_database_error macro 2024-04-07 18:11:37 +01:00
Paul Makles 2de7598f8d refactor(quark): port get/set_settings, get_unreads
#283
2024-04-07 18:10:11 +01:00
Paul Makles f6a565385e refactor(quark): port invite_delete, invite_fetch, invite_join 2024-04-07 17:47:59 +01:00
Paul Makles 569bd1d5e1 refactor(quark): port message_react, message_send, message_unreact, permissions_set_default, permissions_set
#283
2024-04-07 17:12:59 +01:00
Paul Makles aca1fe6dff refactor(quark): port message_edit, message_fetch, voice_join 2024-04-07 16:47:46 +01:00
Paul Makles aded2e3239 fix: add elevation check to role edit route 2024-04-07 15:52:14 +01:00
Paul Makles 54a4eff623 refactor(quark): port channel_ack, channel_delete, channel_ edit, group_remove_member, invite_create, members_fetch, message_bulk_delete, message_clear_reactions, message_delete
#283
2024-04-07 15:41:43 +01:00
Paul Makles 50c36dcefd docs: update environment variables [skip ci] 2024-03-29 11:06:09 +00:00
insertish f16c275bcb ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2024-03-28 13:37:54 +00:00
Paul Makles 74ff072199 docs: deployment lifecycle added to README 2024-03-13 22:43:49 +00:00
Paul Makles 52431bd1dd docs: update README [skip ci] 2024-03-13 22:08:31 +00:00
Paul Makles fa67e87467 chore: switch branch name to main [skip ci] 2024-03-13 22:08:23 +00:00
Bob Bobs 8f9b18444a chore(bonfire): log errors from websocket stream 2024-03-13 21:47:34 +00:00
Paul Makles 7b18e983c9 chore: faster tag and release procedure 2024-02-12 21:40:51 +00:00
Paul Makles c0e64c9052 chore: bump version 2024-02-12 21:34:49 +00:00
Paul Makles 0d54ccd342 chore: update justfile for faster releases 2024-02-12 21:33:01 +00:00
Paul Makles d4d5b23c7d fix: use a valid default permission set for groups 2024-02-12 21:23:38 +00:00
Paul Makles a641034895 test: write a unit test for group chat permissions 2024-02-12 21:19:36 +00:00
Paul Makles 9dd24526ef chore: clippy recommendations 2024-02-12 21:13:31 +00:00
Bob Bobs 6d4f772d78 fix(ci): only start run command in background 2024-02-12 21:12:24 +00:00
Bob Bobs 4c4dada3da fix(ci): build API then run in background to prevent timeout 2024-02-11 14:16:31 +00:00
169 changed files with 4472 additions and 1927 deletions
+25
View File
@@ -1,5 +1,7 @@
# MongoDB URI
MONGODB=mongodb://localhost
AUTUMN_MONGO_URI=mongodb://database
REDIS_URI=redis://localhost/
# URL to where the Revolt app is publicly accessible
REVOLT_APP_URL=http://local.revolt.chat:5000
@@ -80,3 +82,26 @@ REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nf
##
# VOSO_MANAGE_TOKEN=CHANGEME
##
## Autumn configuration
##
# S3 Region
AUTUMN_S3_REGION=minio
# S3 Endpoint
AUTUMN_S3_ENDPOINT=http://minio:9000
# MinIO Root User
MINIO_ROOT_USER=minioautumn
# MinIO Root Password
MINIO_ROOT_PASSWORD=minioautumn
# AWS Access Key ID
AWS_ACCESS_KEY_ID=minioautumn
# AWS Secret Key
AWS_SECRET_ACCESS_KEY=minioautumn
+2 -2
View File
@@ -3,7 +3,7 @@ name: Docker Test & Publish
on:
push:
branches:
- "master"
- "main"
tags:
- "*"
paths-ignore:
@@ -15,7 +15,7 @@ on:
- "README"
pull_request:
branches:
- "master"
- "main"
paths:
- "Dockerfile"
workflow_dispatch:
+7 -7
View File
@@ -3,7 +3,7 @@ name: Rust build, test, and generate specification
on:
push:
pull_request:
branches: [master]
branches: [main]
env:
CARGO_TERM_COLOR: always
@@ -53,20 +53,20 @@ jobs:
cargo nextest run
- name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
env:
TEST_DB: REFERENCE
run: |
cargo run --bin revolt-delta &
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
- name: Wait for API to go up
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: nev7n/wait_for_response@v1
with:
url: "http://localhost:8000/"
- name: Checkout API repository
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: actions/checkout@v3
with:
repository: revoltchat/api
@@ -74,11 +74,11 @@ jobs:
token: ${{ secrets.PAT }}
- name: Download OpenAPI specification
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
- name: Commit changes
if: github.event_name != 'pull_request' && github.ref_name == 'master'
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: EndBug/add-and-commit@v4
with:
cwd: "api"
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectV2(number: 3) {
projectV2(number: 5) {
id
fields(first:20) {
nodes {
@@ -35,7 +35,7 @@ jobs:
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
Generated
+16 -10
View File
@@ -3580,7 +3580,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"async-std",
"async-tungstenite",
@@ -3600,19 +3600,23 @@ dependencies = [
[[package]]
name = "revolt-config"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"async-std",
"cached",
"config",
"dotenv",
"futures-locks",
"log",
"once_cell",
"pretty_env_logger",
"sentry",
"serde",
]
[[package]]
name = "revolt-database"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"async-lock",
"async-recursion",
@@ -3652,12 +3656,13 @@ dependencies = [
"ulid 1.0.0",
"unicode-segmentation",
"url-escape",
"validator 0.16.0",
"web-push 0.10.0",
]
[[package]]
name = "revolt-delta"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"async-channel",
"async-std",
@@ -3669,6 +3674,7 @@ dependencies = [
"env_logger",
"futures",
"impl_ops",
"iso8601-timestamp 0.2.11",
"lettre",
"linkify 0.6.0",
"log",
@@ -3684,7 +3690,6 @@ dependencies = [
"revolt-database",
"revolt-models",
"revolt-permissions",
"revolt-quark",
"revolt-result",
"revolt_rocket_okapi",
"rocket",
@@ -3703,7 +3708,7 @@ dependencies = [
[[package]]
name = "revolt-models"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"indexmap",
"iso8601-timestamp 0.2.11",
@@ -3712,6 +3717,7 @@ dependencies = [
"revolt-config",
"revolt-permissions",
"revolt_optional_struct",
"rocket",
"schemars",
"serde",
"validator 0.16.0",
@@ -3719,7 +3725,7 @@ dependencies = [
[[package]]
name = "revolt-permissions"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"async-std",
"async-trait",
@@ -3734,7 +3740,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"async-std",
"log",
@@ -3745,7 +3751,7 @@ dependencies = [
[[package]]
name = "revolt-quark"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"async-lock",
"async-recursion",
@@ -3798,7 +3804,7 @@ dependencies = [
[[package]]
name = "revolt-result"
version = "0.6.12"
version = "0.7.0"
dependencies = [
"revolt_okapi",
"revolt_rocket_okapi",
+98 -20
View File
@@ -2,34 +2,112 @@
This is a monorepo for the Revolt backend.
| Crate | Path | Description |
| ---------------- | ---------------------------------------------- | ------------------------------------ |
| `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
| `quark` | [crates/quark](crates/quark) | Models and logic |
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer |
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer |
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |-->
| 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**) |
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
## Resources
### Revolt
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing)
## Minimum Supported Rust Version
Rust 1.70 or higher.
## Contributing
## Development Guide
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
Please note that a pull request should only take care of one issue so that we can review it quickly.
Before getting started, you'll want to install:
- Rust toolchain (rustup recommended)
- Docker
- Git
- mold (optional, faster compilation)
> A **default.nix** is available for Nix users!
> Just run `nix-shell` and continue.
Now you can clone and build the project:
```bash
git clone https://github.com/revoltchat/backend revolt-backend
cd revolt-backend
cargo build
```
If you want to run the API and event servers:
```bash
# create environment file (will be deprecated in future)
cp .env.example .env
# (optionally) copy the default configuration file
cp crates/core/config/Revolt.toml Revolt.toml
# configure as necessary...
# start other necessary services
docker compose up -d
# run the API server
cargo run --bin revolt-delta
# run the events server
cargo run --bin revolt-bonfire
# hint:
# mold -run <cargo build, cargo run, etc...>
```
You can start a web client by doing the following:
```bash
# if you do not have yarn yet and have a modern Node.js:
corepack enable
# clone the web client and run it:
git clone --recursive https://github.com/revoltchat/revite
cd revite
yarn
yarn build:deps
yarn dev --port 3001
```
Then go to https://local.revolt.chat:3001
## Deployment Guide
### Cutting new crate releases
Begin by bumping crate versions:
```bash
just patch # 0.0.X
just minor # 0.X.0
just major # X.0.0
```
Then commit the changes to package files.
Proceed to publish all the new crates:
```bash
just publish
```
### Cutting new binary releases
Tag and push a new release by running:
```bash
just release
```
## License
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). Please check individual crates for further license information.
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
**Individual crates may supply their own licenses!**
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.6.12"
version = "0.7.0"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -37,4 +37,4 @@ revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
sentry = "0.31.5"
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }
fred = { version = "8.0.1", features = ["subscriber-client"] }
+26 -11
View File
@@ -118,7 +118,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
// Create a PubSub connection to poll on.
let listener = listener(db, &mut state, addr, &config, &write).fuse();
// Read from WebSocket stream.
let worker = worker(user_id.clone(), &config, read, &write).fuse();
let worker = worker(addr, user_id.clone(), &config, read, &write).fuse();
// Pin both tasks.
pin_mut!(listener, worker);
@@ -186,7 +186,7 @@ async fn listener(
// Handle incoming events.
let Ok(message) = message_rx.recv().await.map_err(|e| {
info!("Error while consuming pub/sub messages: {e:?}");
warn!("Error while consuming pub/sub messages: {e:?}");
sentry::capture_error(&e);
}) else {
return;
@@ -210,27 +210,42 @@ async fn listener(
return;
};
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send {
continue;
}
if should_send
&& write
.lock()
.await
.send(config.encode(&event))
.await
.is_err()
{
let result = write.lock().await.send(config.encode(&event)).await;
if let Err(e) = result {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
warn!("Error while sending an event to {addr:?}: {e:?}");
}
return;
}
}
}
async fn worker(
addr: SocketAddr,
user_id: String,
config: &ProtocolConfiguration,
mut read: WsReader,
write: &Mutex<WsWriter>,
) {
while let Ok(Some(msg)) = read.try_next().await {
loop {
let result = read.try_next().await;
let msg = match result {
Ok(Some(msg)) => msg,
Ok(None) => return,
Err(e) => {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
warn!("Error while reading an event from {addr:?}: {e:?}");
}
return;
}
};
let Ok(payload) = config.decode(&msg) else {
continue;
};
+9 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.6.12"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -14,6 +14,7 @@ default = ["test"]
[dependencies]
# Utility
dotenv = "0.15.0"
config = "0.13.3"
cached = "0.44.0"
once_cell = "1.18.0"
@@ -24,3 +25,10 @@ serde = { version = "1", features = ["derive"] }
# Async
futures-locks = "0.7.1"
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Logging
log = "0.4.14"
pretty_env_logger = "0.4.0"
# Sentry
sentry = "0.31.5"
+4
View File
@@ -1,3 +1,5 @@
sentry_dsn = ""
[database]
mongodb = "mongodb://database"
redis = "redis://redis/"
@@ -33,6 +35,7 @@ api_key = ""
[api.security]
authifier_shield_key = ""
voso_legacy_token = ""
trust_cloudflare = false
[api.security.captcha]
hcaptcha_key = ""
@@ -42,6 +45,7 @@ hcaptcha_sitekey = ""
max_concurrent_connections = 50
[features]
webhooks_enabled = false
[features.limits]
+71
View File
@@ -56,6 +56,9 @@ pub struct ApiSmtp {
pub username: String,
pub password: String,
pub from_address: String,
pub reply_to: Option<String>,
pub port: Option<i32>,
pub use_tls: Option<bool>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -80,6 +83,7 @@ pub struct ApiSecurity {
pub authifier_shield_key: String,
pub voso_legacy_token: String,
pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool,
}
#[derive(Deserialize, Debug, Clone)]
@@ -131,6 +135,7 @@ pub struct FeaturesLimitsCollection {
#[derive(Deserialize, Debug, Clone)]
pub struct Features {
pub limits: FeaturesLimitsCollection,
pub webhooks_enabled: bool,
}
#[derive(Deserialize, Debug, Clone)]
@@ -139,6 +144,31 @@ pub struct Settings {
pub hosts: Hosts,
pub api: Api,
pub features: Features,
pub sentry_dsn: String,
}
impl Settings {
pub fn preflight_checks(&self) {
if self.api.smtp.host.is_empty() {
#[cfg(not(debug_assertions))]
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
panic!("Running in production without email is not recommended, set REVOLT_UNSAFE_NO_EMAIL=1 to override.");
}
#[cfg(debug_assertions)]
log::warn!("No SMTP settings specified! Remember to configure email.");
}
if self.api.security.captcha.hcaptcha_key.is_empty() {
#[cfg(not(debug_assertions))]
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
panic!("Running in production without CAPTCHA is not recommended, set REVOLT_UNSAFE_NO_CAPTCHA=1 to override.");
}
#[cfg(debug_assertions)]
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
}
}
}
pub async fn init() {
@@ -157,6 +187,47 @@ pub async fn config() -> Settings {
read().await.try_deserialize::<Settings>().unwrap()
}
/// Configure logging and common Rust variables
pub async fn setup_logging(release: &'static str) -> Option<sentry::ClientInitGuard> {
dotenv::dotenv().ok();
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
if std::env::var("ROCKET_ADDRESS").is_err() {
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
}
pretty_env_logger::init();
log::info!("Starting {release}");
let config = config().await;
if config.sentry_dsn.is_empty() {
None
} else {
Some(sentry::init((
config.sentry_dsn,
sentry::ClientOptions {
release: Some(release.into()),
..Default::default()
},
)))
}
}
#[macro_export]
macro_rules! configure {
() => {
let _sentry = $crate::setup_logging(concat!(
env!("CARGO_PKG_NAME"),
"@",
env!("CARGO_PKG_VERSION")
))
.await;
};
}
#[cfg(feature = "test")]
#[cfg(test)]
mod tests {
+9 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.6.12"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,11 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.6.12", path = "../config" }
revolt-result = { version = "0.6.12", path = "../result" }
revolt-models = { version = "0.6.12", path = "../models" }
revolt-presence = { version = "0.6.12", path = "../presence" }
revolt-permissions = { version = "0.6.12", path = "../permissions", features = [
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 = [
"validator",
] }
revolt-presence = { version = "0.7.0", path = "../presence" }
revolt-permissions = { version = "0.7.0", path = "../permissions", features = [
"serde",
"bson",
] }
@@ -45,6 +47,7 @@ decancer = "1.6.2"
deadqueue = "0.2.4"
linkify = { optional = true, version = "0.8.1" }
url-escape = { optional = true, version = "0.1.1" }
validator = { version = "0.16", features = ["derive"] }
isahc = { optional = true, version = "1.7", features = ["json"] }
# Serialisation
@@ -0,0 +1,28 @@
[
{
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Member",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "Member",
"discriminator": "0002"
},
{
"_object_type": "Channel",
"_id": "__ID:3__",
"channel_type": "Group",
"name": "My Group",
"owner": "__ID:0__",
"recipients": ["__ID:0__", "__ID:1__"]
}
]
@@ -4,7 +4,8 @@ use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, User, UserSettings, Webhook,
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
UserSettings, Webhook,
};
database_derived!(
@@ -25,7 +26,7 @@ database_derived!(
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
}
);
+3 -3
View File
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use revolt_models::v0::{
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, User,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
UserSettings, Webhook,
};
use revolt_result::Error;
@@ -176,8 +176,8 @@ pub enum EventV1 {
/// Delete emoji
EmojiDelete { id: String },
/*/// New report
ReportCreate(Report), */
/// New report
ReportCreate(Report),
/// New channel
ChannelCreate(Channel),
@@ -93,7 +93,7 @@ impl Bot {
Some(id.to_string()),
Some(PartialUser {
bot: Some(BotInformation {
owner: id.to_string(),
owner: owner.id.to_string(),
}),
..Default::default()
}),
@@ -1,12 +1,12 @@
use revolt_result::{create_error, Result};
use crate::Database;
use crate::{Channel, Database, User};
/* static ALPHABET: [char; 54] = [
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',
]; */
];
auto_derived!(
/// Invite
@@ -56,9 +56,13 @@ impl Invite {
}
/// Create a new invite from given information
/*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> {
pub async fn create_channel_invite(
db: &Database,
creator: &User,
channel: &Channel,
) -> Result<Invite> {
let code = nanoid::nanoid!(8, &ALPHABET);
let invite = match &target {
let invite = match &channel {
Channel::Group { id, .. } => Ok(Invite::Group {
code,
creator: creator.id.clone(),
@@ -72,12 +76,12 @@ impl Invite {
channel: id.clone(),
})
}
_ => Err(Error::InvalidOperation),
_ => Err(create_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> {
@@ -5,10 +5,11 @@ use revolt_models::v0::{self, MessageAuthor};
use revolt_permissions::OverrideField;
use revolt_result::Result;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::{
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server,
SystemMessage, User,
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
Server, SystemMessage, User,
};
auto_derived!(
@@ -295,6 +296,43 @@ impl Channel {
Ok(channel)
}
/// Create a DM (or return the existing one / saved messages)
pub async fn create_dm(db: &Database, user_a: &User, user_b: &User) -> Result<Channel> {
// Try to find existing channel
if let Ok(channel) = db.find_direct_message_channel(&user_a.id, &user_b.id).await {
Ok(channel)
} else {
let channel = if user_a.id == user_b.id {
// Create a new saved messages channel
Channel::SavedMessages {
id: Ulid::new().to_string(),
user: user_a.id.to_string(),
}
} else {
// Create a new DM channel
Channel::DirectMessage {
id: Ulid::new().to_string(),
active: true, // show by default
recipients: vec![user_a.id.clone(), user_b.id.clone()],
last_message_id: None,
}
};
db.insert_channel(&channel).await?;
match &channel {
Channel::DirectMessage { .. } => {
let event = EventV1::ChannelCreate(channel.clone().into());
event.clone().private(user_a.id.clone()).await;
event.private(user_b.id.clone()).await;
}
_ => {}
};
Ok(channel)
}
}
/// Add user to a group
pub async fn add_user_to_group(
&mut self,
@@ -307,6 +345,13 @@ impl Channel {
return Err(create_error!(AlreadyInGroup));
}
let config = config().await;
if recipients.len() >= config.features.limits.default.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size
}));
}
recipients.push(String::from(&user.id));
}
@@ -582,6 +627,28 @@ impl Channel {
}
}
/// 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(())
}
/// Remove user from a group
pub async fn remove_user_from_group(
&self,
@@ -627,8 +694,7 @@ impl Channel {
.await
.ok();
} else {
db.delete_channel(self).await?;
return Ok(());
return self.delete(db).await;
}
}
@@ -675,6 +741,9 @@ impl Channel {
pub async fn delete(&self, db: &Database) -> Result<()> {
let id = self.id().to_string();
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
// TODO: missing functionality:
// - group invites
// - channels list / categories list on server
db.delete_channel(self).await
}
}
@@ -695,6 +764,32 @@ mod tests {
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[async_std::test]
async fn permissions_group_channel() {
database_test!(|db| async move {
fixture!(db, "group_with_members",
owner user 0
member1 user 1
member2 user 2
channel channel 3);
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &member1).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &member2).channel(&channel);
assert!(!calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
});
}
#[async_std::test]
async fn permissions_text_channel() {
database_test!(|db| async move {
@@ -1,6 +1,6 @@
use super::AbstractChannels;
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::Document;
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::{Bson, Document};
use futures::StreamExt;
use revolt_permissions::OverrideField;
use revolt_result::Result;
@@ -188,6 +188,125 @@ impl AbstractChannels for MongoDb {
// Delete a channel
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(|_| create_database_error!("update_one", "servers"))?;
}
// Delete associated attachments
self.delete_many_attachments(doc! {
"object_id": &id
})
.await?;
// Delete the channel itself
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
}
}
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(|_| create_database_error!("delete_many", "channel_invites"))?;
// Delete unread message objects on channels.
self.col::<Document>("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "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(|_| create_database_error!("delete_many", "webhooks"))
.map(|_| ())
}
}
@@ -115,3 +115,21 @@ impl AbstractAttachments for MongoDb {
.map_err(|_| create_database_error!("update_one", COL))
}
}
impl MongoDb {
pub async fn delete_many_attachments(&self, projection: Document) -> Result<()> {
self.col::<Document>(COL)
.update_many(
projection,
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
}
}
@@ -4,18 +4,19 @@ use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_models::v0::{
self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification,
ReplyIntent, SendableEmbed, RE_MENTION,
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::idempotency::IdempotencyKey,
Channel, Database, Emoji, File,
Channel, Database, Emoji, File, User,
};
auto_derived_partial!(
@@ -155,6 +156,7 @@ auto_derived!(
}
/// Message Filter
#[derive(Default)]
pub struct MessageFilter {
/// Parent channel ID
pub channel: Option<String>,
@@ -441,6 +443,93 @@ impl Message {
Ok(())
}
/// Create text embed from sendable embed
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
embed.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let media = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
.await?,
)
} else {
None
};
Ok(Embed::Text(Text {
icon_url: embed.icon_url,
url: embed.url,
title: embed.title,
description: embed.description,
media: media.map(|m| m.into()),
colour: embed.colour,
}))
}
/// 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.into(),
}
.p(self.channel.clone())
.await;
Ok(())
}
/// Helper function to fetch many messages with users
pub async fn fetch_with_users(
db: &Database,
query: MessageQuery,
perspective: &User,
include_users: Option<bool>,
server_id: Option<String>,
) -> Result<BulkMessageResponse> {
let messages: Vec<v0::Message> = db
.fetch_messages(query)
.await?
.into_iter()
.map(Into::into)
.collect();
if let Some(true) = include_users {
let user_ids = messages
.iter()
.map(|m| m.author.clone())
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<String>>();
let users = User::fetch_many_ids_as_mutuals(db, perspective, &user_ids).await?;
Ok(BulkMessageResponse::MessagesAndUsers {
messages,
users,
members: if let Some(server_id) = server_id {
Some(
db.fetch_members(&server_id, &user_ids)
.await?
.into_iter()
.map(Into::into)
.collect(),
)
} else {
None
},
})
} else {
Ok(BulkMessageResponse::JustMessages(messages))
}
}
/// Append content to message
pub async fn append(
db: &Database,
@@ -495,6 +584,40 @@ impl Message {
Ok(())
}
/// 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
let config = config().await;
if self.reactions.len() >= config.features.limits.default.message_reactions
&& !self.reactions.contains_key(emoji)
{
return Err(create_error!(InvalidOperation));
}
// Check if the emoji is whitelisted
if !self.interactions.can_use(emoji) {
return Err(create_error!(InvalidOperation));
}
// Check if the emoji is usable by us
if !Emoji::can_use(db, emoji).await? {
return Err(create_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
}
/// Validate the sum of content of a message is under threshold
pub fn validate_sum(
content: &Option<String>,
@@ -518,6 +641,95 @@ impl Message {
Err(create_error!(PayloadTooLarge))
}
}
/// 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<()> {
let valid_ids = db
.fetch_messages_by_id(&ids)
.await?
.into_iter()
.filter(|msg| msg.channel == channel)
.map(|msg| msg.id)
.collect::<Vec<String>>();
db.delete_messages(channel, &valid_ids).await?;
EventV1::BulkMessageDelete {
channel: channel.to_string(),
ids: valid_ids,
}
.p(channel.to_string())
.await;
Ok(())
}
/// 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(create_error!(NotFound));
}
users.len() == 1
} else {
return Err(create_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
}
}
impl SystemMessage {
@@ -16,6 +16,9 @@ pub trait AbstractMessages: Sync + Send {
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
@@ -4,7 +4,9 @@ use mongodb::options::FindOptions;
use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage};
use crate::{
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
};
use super::AbstractMessages;
@@ -159,6 +161,21 @@ impl AbstractMessages for MongoDb {
}
}
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
self.find_with_options(
COL,
doc! {
"ids": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
@@ -278,3 +295,57 @@ impl AbstractMessages for MongoDb {
.map_err(|_| create_database_error!("delete_many", COL))
}
}
impl MongoDb {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone();
for_attachments.insert(
"attachments",
doc! {
"$exists": 1_i32
},
);
// Check if there are any attachments we need to delete.
let message_ids_with_attachments = self
.find_with_options::<_, DocumentId>(
COL,
for_attachments,
FindOptions::builder()
.projection(doc! { "_id": 1_i32 })
.build(),
)
.await
.map_err(|_| create_database_error!("find_many", "attachments"))?
.into_iter()
.map(|x| x.id)
.collect::<Vec<String>>();
// If we found any, mark them as deleted.
if !message_ids_with_attachments.is_empty() {
self.col::<Document>("attachments")
.update_many(
doc! {
"message_id": {
"$in": message_ids_with_attachments
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "attachments"))?;
}
// And then delete said messages.
self.col::<Document>(COL)
.delete_many(projection, None)
.await
.map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL))
}
}
@@ -1,3 +1,4 @@
use futures::future::try_join_all;
use indexmap::IndexSet;
use revolt_result::Result;
@@ -176,6 +177,11 @@ impl AbstractMessages for ReferenceDb {
}*/
}
/// Fetch multiple messages by given IDs
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
try_join_all(ids.iter().map(|id| self.fetch_message(id))).await
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
let mut messages = self.messages.lock().await;
+6
View File
@@ -8,6 +8,8 @@ mod emojis;
mod files;
mod messages;
mod ratelimit_events;
mod safety_reports;
mod safety_snapshots;
mod server_bans;
mod server_members;
mod servers;
@@ -24,6 +26,8 @@ pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use ratelimit_events::*;
pub use safety_reports::*;
pub use safety_snapshots::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
@@ -45,6 +49,8 @@ pub trait AbstractDatabase:
+ files::AbstractAttachments
+ messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents
+ safety_reports::AbstractReport
+ safety_snapshots::AbstractSnapshot
+ server_bans::AbstractServerBans
+ server_members::AbstractServerMembers
+ servers::AbstractServers
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,22 @@
use revolt_models::v0::{ReportStatus, ReportedContent};
auto_derived!(
/// User-generated platform moderation report
pub struct Report {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Id of the user creating this report
pub author_id: String,
/// Reported content
pub content: ReportedContent,
/// Additional report context
pub additional_context: String,
/// Status of the report
#[serde(flatten)]
pub status: ReportStatus,
/// Additional notes included on the report
#[serde(default)]
pub notes: String,
}
);
@@ -0,0 +1,12 @@
use revolt_result::Result;
use crate::Report;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractReport: Sync + Send {
/// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()>;
}
@@ -0,0 +1,16 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::Report;
use super::AbstractReport;
static COL: &str = "safety_reports";
#[async_trait]
impl AbstractReport for MongoDb {
/// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()> {
query!(self, insert_one, COL, &report).map(|_| ())
}
}
@@ -0,0 +1,20 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::Report;
use super::AbstractReport;
#[async_trait]
impl AbstractReport for ReferenceDb {
/// Insert a new report into the database
async fn insert_report(&self, report: &Report) -> Result<()> {
let mut reports = self.safety_reports.lock().await;
if reports.contains_key(&report.id) {
Err(create_database_error!("insert", "report"))
} else {
reports.insert(report.id.to_string(), report.clone());
Ok(())
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,121 @@
use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, Server, User};
auto_derived!(
/// Snapshot of some content
pub struct Snapshot {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Report parent Id
pub report_id: String,
/// Snapshot of content
pub content: SnapshotContent,
}
/// Enum to map into different models
/// that can be saved in a snapshot
#[serde(tag = "_type")]
pub enum SnapshotContent {
Message {
/// Context before the message
#[serde(rename = "_prior_context", default)]
prior_context: Vec<Message>,
/// Context after the message
#[serde(rename = "_leading_context", default)]
leading_context: Vec<Message>,
/// Message
#[serde(flatten)]
message: Message,
},
Server(Server),
User(User),
}
);
impl SnapshotContent {
/// Generate snapshot from a given message
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: prior_context.into_iter().map(Into::into).collect(),
leading_context: leading_context.into_iter().map(Into::into).collect(),
},
files,
))
}
/// Generate snapshot from a given server
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))
}
/// Generate snapshot from a given user
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))
}
}
@@ -0,0 +1,12 @@
use revolt_result::Result;
use crate::Snapshot;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractSnapshot: Sync + Send {
/// Insert a new snapshot into the database
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>;
}
@@ -0,0 +1,16 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::Snapshot;
use super::AbstractSnapshot;
static COL: &str = "safety_snapshots";
#[async_trait]
impl AbstractSnapshot for MongoDb {
/// Insert a new snapshot into the database
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
query!(self, insert_one, COL, &snapshot).map(|_| ())
}
}
@@ -0,0 +1,20 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::Snapshot;
use super::AbstractSnapshot;
#[async_trait]
impl AbstractSnapshot for ReferenceDb {
/// Insert a new report into the database
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
let mut snapshots = self.safety_snapshots.lock().await;
if snapshots.contains_key(&snapshot.id) {
Err(create_database_error!("insert", "snapshot"))
} else {
snapshots.insert(snapshot.id.to_string(), snapshot.clone());
Ok(())
}
}
}
@@ -1,4 +1,6 @@
use crate::MemberCompositeKey;
use revolt_result::Result;
use crate::{Database, MemberCompositeKey, Server};
auto_derived!(
/// Server Ban
@@ -10,3 +12,24 @@ auto_derived!(
pub reason: Option<String>,
}
);
impl ServerBan {
/// Create ban
pub async fn create(
db: &Database,
server: &Server,
user_id: &str,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan {
id: MemberCompositeKey {
server: server.id.to_string(),
user: user_id.to_string(),
},
reason,
};
db.insert_ban(&ban).await?;
Ok(ban)
}
}
@@ -215,4 +215,47 @@ impl Member {
false
}
}
/// Remove member from server
pub async fn remove(
self,
db: &Database,
server: &Server,
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&self.id).await?;
EventV1::ServerMemberLeave {
id: self.id.server.to_string(),
user: self.id.user.to_string(),
}
.p(self.id.server.to_string())
.await;
if !silent {
if let Some(id) = server
.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: self.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user },
}
.into_message(id.to_string())
// TODO: support notifications here in the future?
.send_without_notifications(db, false, false)
.await
.ok();
}
}
Ok(())
}
}
@@ -253,71 +253,6 @@ impl Server {
Err(create_error!(NotFound))
}
}
/*
/// 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?;
/* // TODO: 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 Role {
@@ -1,4 +1,4 @@
use bson::{to_document, Document};
use bson::{to_document, Bson, Document};
use futures::StreamExt;
use revolt_result::Result;
@@ -67,6 +67,7 @@ impl AbstractServers for MongoDb {
/// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()> {
self.delete_associated_server_objects(id).await?;
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
@@ -181,3 +182,81 @@ impl IntoDocumentPath for FieldsRole {
})
}
}
impl MongoDb {
pub async fn delete_associated_server_objects(&self, server_id: &str) -> Result<()> {
// Find all channels
let channels: Vec<String> = self
.col::<Document>("channels")
.find(
doc! {
"server": server_id
},
None,
)
.await
.map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async {
s.map(|d| d.get_str("_id").map(|s| s.to_string()).ok())
.ok()
.flatten()
})
.collect()
.await;
// Check if there are any attachments we need to delete.
self.delete_bulk_messages(doc! {
"channel": {
"$in": &channels
}
})
.await?;
// Delete all emoji.
self.col::<Document>("emojis")
.delete_many(
doc! {
"parent.id": &server_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "emojis"))?;
// Delete all channels.
self.col::<Document>("channels")
.delete_many(
doc! {
"server": &server_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "channels"))?;
// Delete any associated objects, e.g. unreads and invites.
self.delete_associated_channel_objects(Bson::Document(doc! { "$in": &channels }))
.await?;
// Delete members and bans.
for with in &["server_members", "server_bans"] {
self.col::<Document>(with)
.delete_many(
doc! {
"_id.server": &server_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", with))?;
}
// Update many attachments with parent id.
self.delete_many_attachments(doc! {
"object_id": &server_id
})
.await?;
Ok(())
}
}
+32 -21
View File
@@ -5,7 +5,9 @@ use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_config::config;
use revolt_result::{create_error, Error, ErrorType, Result};
use revolt_models::v0;
use revolt_presence::filter_online;
use revolt_result::{create_error, Result};
use ulid::Ulid;
auto_derived_partial!(
@@ -94,20 +96,22 @@ auto_derived!(
}
/// User's active status
#[derive(Default)]
pub struct UserStatus {
/// Custom status text
#[serde(skip_serializing_if = "String::is_empty", default)]
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
/// Current presence option
#[serde(skip_serializing_if = "Option::is_none")]
pub presence: Option<Presence>,
}
/// User's profile
#[derive(Default)]
pub struct UserProfile {
/// Text content on user's profile
#[serde(skip_serializing_if = "String::is_empty", default)]
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<File>,
@@ -262,7 +266,27 @@ impl User {
Ok(username)
}
// Find a free discriminator for a given username
/// 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(
db: &Database,
perspective: &User,
ids: &[String],
) -> Result<Vec<v0::User>> {
let online_ids = filter_online(ids).await;
Ok(db
.fetch_users(ids)
.await?
.into_iter()
.map(|user| {
let is_online = online_ids.contains(&user.id);
user.into_known(perspective, is_online)
})
.collect())
}
/// Find a free discriminator for a given username
pub async fn find_discriminator(
db: &Database,
username: &str,
@@ -348,19 +372,6 @@ impl User {
}
}
/// Check whether a username is already in use by another user
#[allow(dead_code)]
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
match db.fetch_user_by_username(username).await {
Ok(_) => Ok(true),
Err(Error {
error_type: ErrorType::NotFound,
..
}) => Ok(false),
Err(error) => Err(error),
}
}
/// Set a relationship to another user
pub async fn set_relationship(
&mut self,
@@ -554,7 +565,7 @@ impl User {
FieldsUser::Avatar => self.avatar = None,
FieldsUser::StatusText => {
if let Some(x) = self.status.as_mut() {
x.text = String::new();
x.text = None;
}
}
FieldsUser::StatusPresence => {
@@ -564,7 +575,7 @@ impl User {
}
FieldsUser::ProfileContent => {
if let Some(x) = self.profile.as_mut() {
x.content = String::new();
x.content = None;
}
}
FieldsUser::ProfileBackground => {
+1 -1
View File
@@ -14,7 +14,7 @@ pub trait AbstractUsers: Sync + Send {
async fn fetch_user(&self, id: &str) -> Result<User>;
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User>;
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
@@ -25,13 +25,14 @@ impl AbstractUsers for MongoDb {
}
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
query!(
self,
find_one_with_options,
COL,
doc! {
"username": username
"username": username,
"discriminator": discriminator
},
FindOneOptions::builder()
.collation(
@@ -28,12 +28,14 @@ impl AbstractUsers for ReferenceDb {
}
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
let users = self.users.lock().await;
let lowercase = username.to_lowercase();
users
.values()
.find(|user| user.username.to_lowercase() == lowercase)
.find(|user| {
user.username.to_lowercase() == lowercase && user.discriminator == discriminator
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
+154
View File
@@ -15,6 +15,7 @@ impl crate::Bot {
description: user
.profile
.map(|profile| profile.content)
.flatten()
.unwrap_or_default(),
}
}
@@ -444,6 +445,19 @@ impl From<Masquerade> for crate::Masquerade {
}
}
impl From<crate::Report> for Report {
fn from(value: crate::Report) -> Self {
Report {
id: value.id,
author_id: value.author_id,
content: value.content,
additional_context: value.additional_context,
status: value.status,
notes: value.notes,
}
}
}
impl From<crate::ServerBan> for ServerBan {
fn from(value: crate::ServerBan) -> Self {
ServerBan {
@@ -499,6 +513,17 @@ impl From<crate::FieldsMember> for FieldsMember {
}
}
impl From<FieldsMember> for crate::FieldsMember {
fn from(value: FieldsMember) -> crate::FieldsMember {
match value {
FieldsMember::Avatar => crate::FieldsMember::Avatar,
FieldsMember::Nickname => crate::FieldsMember::Nickname,
FieldsMember::Roles => crate::FieldsMember::Roles,
FieldsMember::Timeout => crate::FieldsMember::Timeout,
}
}
}
impl From<crate::RemovalIntention> for RemovalIntention {
fn from(value: crate::RemovalIntention) -> Self {
match value {
@@ -575,6 +600,18 @@ impl From<crate::FieldsServer> for FieldsServer {
}
}
impl From<FieldsServer> for crate::FieldsServer {
fn from(value: FieldsServer) -> crate::FieldsServer {
match value {
FieldsServer::Banner => crate::FieldsServer::Banner,
FieldsServer::Categories => crate::FieldsServer::Categories,
FieldsServer::Description => crate::FieldsServer::Description,
FieldsServer::Icon => crate::FieldsServer::Icon,
FieldsServer::SystemMessages => crate::FieldsServer::SystemMessages,
}
}
}
impl From<crate::Category> for Category {
fn from(value: crate::Category) -> Self {
Category {
@@ -585,6 +622,16 @@ impl From<crate::Category> for Category {
}
}
impl From<Category> for crate::Category {
fn from(value: Category) -> Self {
crate::Category {
id: value.id,
title: value.title,
channels: value.channels,
}
}
}
impl From<crate::SystemMessageChannels> for SystemMessageChannels {
fn from(value: crate::SystemMessageChannels) -> Self {
SystemMessageChannels {
@@ -596,6 +643,17 @@ impl From<crate::SystemMessageChannels> for SystemMessageChannels {
}
}
impl From<SystemMessageChannels> for crate::SystemMessageChannels {
fn from(value: SystemMessageChannels) -> Self {
crate::SystemMessageChannels {
user_joined: value.user_joined,
user_left: value.user_left,
user_kicked: value.user_kicked,
user_banned: value.user_banned,
}
}
}
impl From<crate::Role> for Role {
fn from(value: crate::Role) -> Self {
Role {
@@ -628,6 +686,14 @@ impl From<crate::FieldsRole> for FieldsRole {
}
}
impl From<FieldsRole> for crate::FieldsRole {
fn from(value: FieldsRole) -> Self {
match value {
FieldsRole::Colour => crate::FieldsRole::Colour,
}
}
}
impl crate::User {
pub async fn into<'a, P>(self, db: &Database, perspective: P) -> User
where
@@ -701,6 +767,75 @@ impl crate::User {
}
}
/// Convert user object into user model assuming mutual connection
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
where
P: Into<Option<&'a crate::User>>,
{
let perspective = perspective.into();
let (relationship, can_see_profile) = if self.bot.is_some() {
(RelationshipStatus::None, true)
} else if let Some(perspective) = perspective {
if perspective.id == self.id {
(RelationshipStatus::User, true)
} else {
let relationship = perspective
.relations
.as_ref()
.map(|relations| {
relations
.iter()
.find(|relationship| relationship.id == self.id)
.map(|relationship| relationship.status.clone().into())
.unwrap_or_default()
})
.unwrap_or_default();
let can_see_profile = relationship != RelationshipStatus::BlockedOther;
(relationship, can_see_profile)
}
} else {
(RelationshipStatus::None, false)
};
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: if let Some(crate::User { id, .. }) = perspective {
if id == &self.id {
self.relations
.unwrap_or_default()
.into_iter()
.map(|relation| relation.into())
.collect()
} else {
vec![]
}
} else {
vec![]
},
badges: self.badges.unwrap_or_default() as u32,
status: if can_see_profile {
self.status.map(|status| status.into())
} else {
None
},
profile: if can_see_profile {
self.profile.map(|profile| profile.into())
} else {
None
},
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship,
online: can_see_profile && is_online,
id: self.id,
}
}
pub async fn into_self(self) -> User {
User {
username: self.username,
@@ -727,6 +862,13 @@ impl crate::User {
id: self.id,
}
}
pub fn as_author_for_system(&self) -> MessageAuthor {
MessageAuthor::System {
username: &self.username,
avatar: self.avatar.as_ref().map(|file| file.id.as_ref()),
}
}
}
impl From<crate::PartialUser> for PartialUser {
@@ -814,6 +956,18 @@ impl From<crate::Presence> for Presence {
}
}
impl From<Presence> for crate::Presence {
fn from(value: Presence) -> crate::Presence {
match value {
Presence::Online => crate::Presence::Online,
Presence::Idle => crate::Presence::Idle,
Presence::Focus => crate::Presence::Focus,
Presence::Busy => crate::Presence::Busy,
Presence::Invisible => crate::Presence::Invisible,
}
}
}
impl From<crate::UserStatus> for UserStatus {
fn from(value: crate::UserStatus) -> Self {
UserStatus {
-2
View File
@@ -2,6 +2,4 @@ pub mod bridge;
pub mod idempotency;
pub mod permissions;
pub mod reference;
#[cfg(test)]
pub mod test_fixtures;
+9 -2
View File
@@ -2,7 +2,7 @@ use std::borrow::Cow;
use revolt_permissions::{
calculate_user_permissions, ChannelType, Override, PermissionQuery, PermissionValue,
RelationshipStatus,
RelationshipStatus, DEFAULT_PERMISSION_DIRECT_MESSAGE,
};
use crate::{Channel, Database, Member, Server, User};
@@ -209,7 +209,7 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
match channel {
Cow::Borrowed(Channel::Group { permissions, .. })
| Cow::Owned(Channel::Group { permissions, .. }) => Override {
allow: permissions.unwrap_or_default() as u64,
allow: permissions.unwrap_or(*DEFAULT_PERMISSION_DIRECT_MESSAGE as i64) as u64,
deny: 0,
},
Cow::Borrowed(Channel::TextChannel {
@@ -464,6 +464,13 @@ impl<'a> DatabasePermissionQuery<'a> {
pub fn member_ref(&self) -> &Option<Cow<Member>> {
&self.member
}
/// Get the known member's current ranking
pub fn get_member_rank(&self) -> Option<i64> {
self.member
.as_ref()
.map(|member| member.get_ranking(self.server.as_ref().unwrap()))
}
}
/// Short-hand for creating a permission calculator
+28 -1
View File
@@ -7,7 +7,9 @@ use schemars::{
JsonSchema,
};
use crate::{Bot, Channel, Database, Emoji, Message, Server, User, Webhook};
use crate::{
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
@@ -22,6 +24,11 @@ impl Reference {
Reference { id }
}
/// Fetch ban from Ref
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
db.fetch_ban(server, &self.id).await
}
/// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await
@@ -37,11 +44,31 @@ impl Reference {
db.fetch_channel(&self.id).await
}
/// Fetch invite from Ref
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
db.fetch_invite(&self.id).await
}
/// Fetch message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(&self.id).await
}
/// Fetch message from Ref and validate channel
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
let msg = db.fetch_message(&self.id).await?;
if msg.channel != channel {
return Err(create_error!(NotFound));
}
Ok(msg)
}
/// Fetch member from Ref
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
db.fetch_member(server, &self.id).await
}
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await
@@ -92,7 +92,7 @@ macro_rules! fixture {
let fixtures = $crate::util::test_fixtures::load_fixture(
&$database,
include_str!(concat!("../../../fixtures/", $name, ".json")),
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/", $name, ".json")),
)
.await;
+8 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.6.12"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -12,20 +12,24 @@ description = "Revolt Backend: API Models"
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
schemas = ["dep:schemars", "revolt-permissions/schemas"]
validator = ["dep:validator"]
rocket = ["dep:rocket"]
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
default = ["serde", "partials"]
default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.6.12", path = "../config" }
revolt-permissions = { version = "0.6.12", path = "../permissions" }
revolt-config = { version = "0.7.0", path = "../config" }
revolt-permissions = { version = "0.7.0", path = "../permissions" }
# Utility
regex = "1"
indexmap = "1.9.3"
once_cell = "1.17.1"
# Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
# Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
@@ -1,3 +1,5 @@
use super::{Channel, File, Server, User};
auto_derived!(
/// Invite
pub enum Invite {
@@ -24,4 +26,77 @@ auto_derived!(
channel: String,
},
}
/// Public invite response
#[allow(clippy::large_enum_variant)]
#[serde(tag = "type")]
pub enum InviteResponse {
/// Server channel invite
Server {
/// Invite code
code: String,
/// Id of the server
server_id: String,
/// Name of the server
server_name: String,
/// Attachment for server icon
#[serde(skip_serializing_if = "Option::is_none")]
server_icon: Option<File>,
/// Attachment for server banner
#[serde(skip_serializing_if = "Option::is_none")]
server_banner: Option<File>,
/// Enum of server flags
#[serde(skip_serializing_if = "Option::is_none")]
server_flags: Option<i32>,
/// Id of server channel
channel_id: String,
/// Name of server channel
channel_name: String,
/// Description of server channel
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
/// Name of user who created the invite
user_name: String,
/// Avatar of the user who created the invite
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
/// Number of members in this server
member_count: i64,
},
/// Group channel invite
Group {
/// Invite code
code: String,
/// Id of group channel
channel_id: String,
/// Name of group channel
channel_name: String,
/// Description of group channel
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
/// Name of user who created the invite
user_name: String,
/// Avatar of the user who created the invite
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
},
}
/// Invite join response
#[serde(tag = "type")]
#[allow(clippy::large_enum_variant)]
pub enum InviteJoinResponse {
Server {
/// Channels in the server
channels: Vec<Channel>,
/// Server we are joining
server: Server,
},
Group {
/// Group channel we are joining
channel: Channel,
/// Members of this group
users: Vec<User>,
},
}
);
+36 -1
View File
@@ -1,8 +1,11 @@
use super::File;
use revolt_permissions::OverrideField;
use revolt_permissions::{Override, OverrideField};
use std::collections::{HashMap, HashSet};
#[cfg(feature = "rocket")]
use rocket::FromForm;
auto_derived!(
/// Channel
#[serde(tag = "channel_type")]
@@ -258,6 +261,38 @@ auto_derived!(
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
}
/// New default permissions
#[serde(untagged)]
pub enum DataDefaultChannelPermissions {
Value {
/// Permission values to set for members in a `Group`
permissions: u64,
},
Field {
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
permissions: Override,
},
}
/// New role permissions
pub struct DataSetRolePermissions {
/// Allow / deny values to set for this role
pub permissions: Override,
}
/// Options when deleting a channel
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
}
/// Voice server token response
pub struct LegacyCreateVoiceUserResponse {
/// Token for authenticating with the voice server
token: String,
}
);
impl Channel {
+121 -10
View File
@@ -8,9 +8,12 @@ use revolt_config::config;
#[cfg(feature = "validator")]
use validator::Validate;
#[cfg(feature = "rocket")]
use rocket::{FromForm, FromFormField};
use iso8601_timestamp::Timestamp;
use super::{Embed, File, MessageWebhook, User, Webhook, RE_COLOUR};
use super::{Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR};
pub static RE_MENTION: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
@@ -66,6 +69,24 @@ auto_derived_partial!(
);
auto_derived!(
/// Bulk Message Response
#[serde(untagged)]
pub enum BulkMessageResponse {
JustMessages(
/// List of messages
Vec<Message>,
),
MessagesAndUsers {
/// List of messages
messages: Vec<Message>,
/// List of users
users: Vec<User>,
/// List of members
#[serde(skip_serializing_if = "Option::is_none")]
members: Option<Vec<Member>>,
},
}
/// System Event
#[serde(tag = "type")]
pub enum SystemMessage {
@@ -136,6 +157,7 @@ auto_derived!(
///
/// Sort used for retrieving messages
#[derive(Default)]
#[cfg_attr(feature = "rocket", derive(FromFormField))]
pub enum MessageSort {
/// Sort by the most relevant messages
#[default]
@@ -169,16 +191,19 @@ auto_derived!(
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct SendableEmbed {
#[validate(length(min = 1, max = 128))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub icon_url: Option<String>,
#[validate(length(min = 1, max = 256))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub url: Option<String>,
#[validate(length(min = 1, max = 100))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
pub title: Option<String>,
#[validate(length(min = 1, max = 2000))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
pub description: Option<String>,
pub media: Option<String>,
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
#[cfg_attr(
feature = "validator",
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
)]
pub colour: Option<String>,
}
@@ -196,11 +221,11 @@ auto_derived!(
/// Unique token to prevent duplicate message sending
///
/// **This is deprecated and replaced by `Idempotency-Key`!**
#[validate(length(min = 1, max = 64))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
pub nonce: Option<String>,
/// Message content to send
#[validate(length(min = 0, max = 2000))]
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
pub content: Option<String>,
/// Attachments to include in message
pub attachments: Option<Vec<String>>,
@@ -209,14 +234,100 @@ auto_derived!(
/// Embeds to include in message
///
/// Text embed content contributes to the content length cap
#[validate]
#[cfg_attr(feature = "validator", validate)]
pub embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message
#[validate]
#[cfg_attr(feature = "validator", validate)]
pub masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
pub interactions: Option<Interactions>,
}
/// Options for querying messages
#[cfg_attr(feature = "validator", derive(Validate))]
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsQueryMessages {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
pub limit: Option<i64>,
/// Message id before which messages should be fetched
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub before: Option<String>,
/// Message id after which messages should be fetched
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub after: Option<String>,
/// Message sort direction
pub sort: Option<MessageSort>,
/// Message id to search around
///
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
/// It will also take half of limit rounded as the limits to each side.
/// It also fetches the message ID specified.
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub nearby: Option<String>,
/// Whether to include user (and member, if server channel) objects
pub include_users: Option<bool>,
}
/// Options for searching for messages
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataMessageSearch {
/// Full-text search query
///
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
pub query: String,
/// Maximum number of messages to fetch
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
pub limit: Option<i64>,
/// Message id before which messages should be fetched
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub before: Option<String>,
/// Message id after which messages should be fetched
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
pub after: Option<String>,
/// Message sort direction
///
/// By default, it will be sorted by latest.
#[cfg_attr(feature = "serde", serde(default = "MessageSort::default"))]
pub sort: MessageSort,
/// Whether to include user (and member, if server channel) objects
pub include_users: Option<bool>,
}
/// Changes to make to message
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditMessage {
/// New message content
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
pub content: Option<String>,
/// Embeds to include in the message
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 10)))]
pub embeds: Option<Vec<SendableEmbed>>,
}
/// Options for bulk deleting messages
#[cfg_attr(
feature = "validator",
cfg_attr(feature = "validator", derive(Validate))
)]
pub struct OptionsBulkDelete {
/// Message IDs
#[validate(length(min = 1, max = 100))]
pub ids: Vec<String>,
}
/// Options for removing reaction
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsUnreact {
/// Remove a specific user's reaction
pub user_id: Option<String>,
/// Remove all reactions
pub remove_all: Option<bool>,
}
);
/// Message Author Abstraction
+2
View File
@@ -7,6 +7,7 @@ mod embeds;
mod emojis;
mod files;
mod messages;
mod safety_reports;
mod server_bans;
mod server_members;
mod servers;
@@ -22,6 +23,7 @@ pub use embeds::*;
pub use emojis::*;
pub use files::*;
pub use messages::*;
pub use safety_reports::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
+148
View File
@@ -0,0 +1,148 @@
use iso8601_timestamp::Timestamp;
auto_derived!(
/// User-generated platform moderation report
pub struct Report {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Id of the user creating this report
pub author_id: String,
/// Reported content
pub content: ReportedContent,
/// Additional report context
pub additional_context: String,
/// Status of the report
#[serde(flatten)]
pub status: ReportStatus,
/// Additional notes included on the report
#[serde(default)]
pub notes: String,
}
/// Reason for reporting content (message or server)
pub enum ContentReportReason {
/// No reason has been specified
NoneSpecified,
/// Illegal content catch-all reason
Illegal,
/// Selling or facilitating use of drugs or other illegal goods
IllegalGoods,
/// Extortion or blackmail
IllegalExtortion,
/// Revenge or child pornography
IllegalPornography,
/// Illegal hacking activity
IllegalHacking,
/// Extreme violence, gore, or animal cruelty
/// With exception to violence potrayed in media / creative arts
ExtremeViolence,
/// Content that promotes harm to others / self
PromotesHarm,
/// Unsolicited advertisements
UnsolicitedSpam,
/// This is a raid
Raid,
/// Spam or platform abuse
SpamAbuse,
/// Scams or fraud
ScamsFraud,
/// Distribution of malware or malicious links
Malware,
/// Harassment or abuse targeted at another user
Harassment,
}
/// Reason for reporting a user
pub enum UserReportReason {
/// No reason has been specified
NoneSpecified,
/// Unsolicited advertisements
UnsolicitedSpam,
/// User is sending spam or otherwise abusing the platform
SpamAbuse,
/// User's profile contains inappropriate content for a general audience
InappropriateProfile,
/// User is impersonating another user
Impersonation,
/// User is evading a ban
BanEvasion,
/// User is not of minimum age to use the platform
Underage,
}
/// The content being reported
#[serde(tag = "type")]
pub enum ReportedContent {
/// Report a message
Message {
/// ID of the message
id: String,
/// Reason for reporting message
report_reason: ContentReportReason,
},
/// Report a server
Server {
/// ID of the server
id: String,
/// Reason for reporting server
report_reason: ContentReportReason,
},
/// Report a user
User {
/// ID of the user
id: String,
/// Reason for reporting a user
report_reason: UserReportReason,
/// Message context
message_id: Option<String>,
},
}
/// Status of the report
#[serde(tag = "status")]
pub enum ReportStatus {
/// Report is waiting for triage / action
Created {},
/// Report was rejected
Rejected {
rejection_reason: String,
closed_at: Option<Timestamp>,
},
/// Report was actioned and resolved
Resolved { closed_at: Option<Timestamp> },
}
/// Just the status of the report
pub enum ReportStatusString {
/// Report is waiting for triage / action
Created,
/// Report was rejected
Rejected,
/// Report was actioned and resolved
Resolved,
}
);
+44 -1
View File
@@ -1,4 +1,7 @@
use super::MemberCompositeKey;
use super::{File, MemberCompositeKey, User};
#[cfg(feature = "validator")]
use validator::Validate;
auto_derived!(
/// Server Ban
@@ -9,4 +12,44 @@ auto_derived!(
/// Reason for ban creation
pub reason: Option<String>,
}
/// Information for new server ban
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataBanCreate {
/// Ban reason
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub reason: Option<String>,
}
/// Just enough information to list a ban
pub struct BannedUser {
/// Id of the banned user
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
pub id: String,
/// Username of the banned user
pub username: String,
/// Discriminator of the banned user
pub discriminator: String,
/// Avatar of the banned user
pub avatar: Option<File>,
}
/// Ban list result
pub struct BanListResult {
/// Users objects
pub users: Vec<BannedUser>,
/// Ban objects
pub bans: Vec<ServerBan>,
}
);
impl From<User> for BannedUser {
fn from(user: User) -> Self {
BannedUser {
id: user.id,
username: user.username,
discriminator: user.discriminator,
avatar: user.avatar,
}
}
}
+51 -1
View File
@@ -1,9 +1,17 @@
use super::File;
use std::collections::HashMap;
use super::{File, Role, User};
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use regex::Regex;
#[cfg(feature = "validator")]
use validator::Validate;
#[cfg(feature = "rocket")]
use rocket::FromForm;
/// Regex for valid role colours
///
/// Allows the use of named colours, rgb(a), variables and all gradients.
@@ -77,4 +85,46 @@ auto_derived!(
Kick,
Ban,
}
/// Member response
#[serde(untagged)]
pub enum MemberResponse {
Member(Member),
MemberWithRoles {
member: Member,
roles: HashMap<String, Role>,
},
}
/// Options for fetching all members
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsFetchAllMembers {
/// Whether to exclude offline users
pub exclude_offline: Option<bool>,
}
/// Response with all members
pub struct AllMemberResponse {
/// List of members
pub members: Vec<Member>,
/// List of users
pub users: Vec<User>,
}
/// New member information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataMemberEdit {
/// Member nickname
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub nickname: Option<String>,
/// Attachment Id to set for avatar
pub avatar: Option<String>,
/// Array of role ids
pub roles: Option<Vec<String>>,
/// Timestamp this member is timed out until
pub timeout: Option<Timestamp>,
/// Fields to remove from channel object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsMember>>,
}
);
+121 -2
View File
@@ -1,11 +1,14 @@
use super::{Channel, File};
use super::{Channel, File, RE_COLOUR};
use revolt_permissions::OverrideField;
use revolt_permissions::{Override, OverrideField};
use std::collections::HashMap;
#[cfg(feature = "validator")]
use validator::Validate;
#[cfg(feature = "rocket")]
use rocket::FromForm;
auto_derived_partial!(
/// Server
pub struct Server {
@@ -120,10 +123,13 @@ auto_derived!(
}
/// Channel category
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct Category {
/// Unique ID for this category
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub id: String,
/// Title for this category
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub title: String,
/// Channels in this category
pub channels: Vec<String>,
@@ -160,6 +166,26 @@ auto_derived!(
pub nsfw: Option<bool>,
}
/// Information about new role to create
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateRole {
/// Role name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: String,
/// Ranking position
///
/// Smaller values take priority.
pub rank: Option<i64>,
}
/// Response after creating new role
pub struct NewRoleResponse {
/// Id of the role
pub id: String,
/// New role
pub role: Role,
}
/// Information returned when creating server
pub struct CreateServerLegacyResponse {
/// Server object
@@ -167,4 +193,97 @@ auto_derived!(
/// Default channels
pub channels: Vec<Channel>,
}
/// Options when fetching server
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsFetchServer {
/// Whether to include channels
pub include_channels: Option<bool>,
}
/// Fetch server information
#[serde(untagged)]
pub enum FetchServerResponse {
JustServer(Server),
ServerWithChannels {
#[serde(flatten)]
server: Server,
channels: Vec<Channel>,
},
}
/// New server information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditServer {
/// Server name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: Option<String>,
/// Server description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
pub description: Option<String>,
/// Attachment Id for icon
pub icon: Option<String>,
/// Attachment Id for banner
pub banner: Option<String>,
/// Category structure for server
#[cfg_attr(feature = "validator", validate)]
pub categories: Option<Vec<Category>>,
/// System message configuration
pub system_messages: Option<SystemMessageChannels>,
/// Bitfield of server flags
#[cfg_attr(feature = "validator", serde(skip_serializing_if = "Option::is_none"))]
pub flags: Option<i32>,
// Whether this server is age-restricted
// nsfw: Option<bool>,
/// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg)
pub discoverable: Option<bool>,
/// Whether analytics should be collected for this server
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
pub analytics: Option<bool>,
/// Fields to remove from server object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsServer>>,
}
/// New role information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditRole {
/// Role name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub name: Option<String>,
/// Role colour
#[cfg_attr(
feature = "validator",
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
)]
pub colour: Option<String>,
/// Whether this role should be displayed separately
pub hoist: Option<bool>,
/// Ranking position
///
/// Smaller values take priority.
pub rank: Option<i64>,
/// Fields to remove from role object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsRole>>,
}
/// New role permissions
pub struct DataSetServerRolePermission {
/// Allow / deny values for the role in this server.
pub permissions: Override,
}
/// Options when leaving a server
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsServerDelete {
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
}
);
@@ -1,6 +1,26 @@
#[cfg(feature = "rocket")]
use rocket::FromForm;
use std::collections::HashMap;
/// HashMap of user settings
/// Each key is mapped to a tuple consisting of the
/// revision timestamp and serialised data (in JSON format)
pub type UserSettings = HashMap<String, (i64, String)>;
auto_derived!(
/// Options for fetching settings
pub struct OptionsFetchSettings {
/// Keys to fetch
pub keys: Vec<String>,
}
/// Additional options for inserting settings
#[cfg_attr(feature = "rocket", derive(FromForm))]
pub struct OptionsSetSettings {
/// Timestamp of settings change.
///
/// Used to avoid feedback loops.
pub timestamp: Option<i64>,
}
);
+86 -4
View File
@@ -3,12 +3,21 @@ use regex::Regex;
use super::File;
#[cfg(feature = "validator")]
use validator::Validate;
/// Regex for valid usernames
///
/// Block zero width space
/// Block lookalike characters
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
/// Regex for valid display names
///
/// Block zero width space
/// Block newline and carriage return
pub static RE_DISPLAY_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap());
auto_derived_partial!(
/// User
pub struct User {
@@ -123,20 +132,26 @@ auto_derived!(
}
/// User's active status
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct UserStatus {
/// Custom status text
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub text: String,
#[validate(length(min = 0, max = 128))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub text: Option<String>,
/// Current presence option
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub presence: Option<Presence>,
}
/// User's profile
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct UserProfile {
/// Text content on user's profile
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
pub content: String,
#[validate(length(min = 0, max = 2000))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub content: Option<String>,
/// Background visible on user's profile
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub background: Option<File>,
@@ -182,12 +197,79 @@ auto_derived!(
Spam = 8,
}
/// New user profile data
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataUserProfile {
/// Text to set as user profile description
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub content: Option<String>,
/// Attachment Id for background
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub background: Option<String>,
}
/// New user information
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditUser {
/// New display name
#[cfg_attr(
feature = "validator",
validate(length(min = 2, max = 32), regex = "RE_DISPLAY_NAME")
)]
pub display_name: Option<String>,
/// Attachment Id for avatar
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
pub avatar: Option<String>,
/// New user status
#[cfg_attr(feature = "validator", validate)]
pub status: Option<UserStatus>,
/// New user profile data
///
/// This is applied as a partial.
#[cfg_attr(feature = "validator", validate)]
pub profile: Option<DataUserProfile>,
/// Bitfield of user badges
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub badges: Option<i32>,
/// Enum of user flags
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub flags: Option<i32>,
/// Fields to remove from user object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsUser>>,
}
/// User flag reponse
pub struct FlagResponse {
/// Flags
pub flags: i32,
}
/// Mutual friends and servers response
pub struct MutualResponse {
/// Array of mutual user IDs that both users are friends with
pub users: Vec<String>,
/// Array of mutual server IDs that both users are in
pub servers: Vec<String>,
}
/// Bot information for if the user is a bot
pub struct BotInformation {
/// Id of the owner of this bot
#[cfg_attr(feature = "serde", serde(rename = "owner"))]
pub owner_id: String,
}
/// User lookup information
pub struct DataSendFriendRequest {
/// Username and discriminator combo separated by #
pub username: String,
}
);
pub trait CheckRelationship {
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.6.12"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Core
revolt-result = { version = "0.6.5", path = "../result" }
revolt-result = { version = "0.7.0", path = "../result" }
# Utility
auto_ops = "0.3.0"
+1 -1
View File
@@ -111,7 +111,7 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) ->
query.set_server_from_channel().await;
if query.are_we_server_owner().await {
return ChannelPermission::GrantAllSafe.into();
ChannelPermission::GrantAllSafe.into()
} else if query.are_we_a_member().await {
let mut permissions = calculate_server_permissions(query).await;
permissions.apply(query.get_default_channel_permissions().await);
+40
View File
@@ -53,6 +53,17 @@ impl PermissionValue {
self.has(permission as u64)
}
/// Throw if missing user permission
pub fn throw_if_lacking_user_permission(&self, permission: UserPermission) -> Result<()> {
if self.has_user_permission(permission) {
Ok(())
} else {
Err(create_error!(MissingPermission {
permission: permission.to_string()
}))
}
}
/// Throw if missing channel permission
pub fn throw_if_lacking_channel_permission(&self, permission: ChannelPermission) -> Result<()> {
if self.has_channel_permission(permission) {
@@ -63,6 +74,35 @@ impl PermissionValue {
}))
}
}
/// Throw an error if we cannot grant permissions on either allows or denies
/// going from the previous given value to the next given value.
///
/// We need to check any:
/// - allows added (permissions now granted)
/// - denies removed (permissions now neutral or granted)
pub async fn throw_permission_override<C>(
&self,
current_value: C,
next_value: &Override,
) -> Result<()>
where
C: Into<Option<Override>>,
{
let current_value = current_value.into();
if let Some(current_value) = current_value {
if !self.has(!current_value.allows() & next_value.allows())
|| !self.has(current_value.denies() & !next_value.denies())
{
return Err(create_error!(CannotGiveMissingPermissions));
}
} else if !self.has(next_value.allows()) {
return Err(create_error!(CannotGiveMissingPermissions));
}
Ok(())
}
}
impl From<i64> for PermissionValue {
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.6.12"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.6.12"
version = "0.7.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -147,7 +147,7 @@ macro_rules! create_error {
#[macro_export]
macro_rules! create_database_error {
( $operation: expr, $collection: expr ) => {
create_error!(DatabaseError {
$crate::create_error!(DatabaseError {
operation: $operation.to_string(),
collection: $collection.to_string()
})
+7 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.6.12"
version = "0.7.0"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -36,6 +36,7 @@ nanoid = "0.4.0"
serde_json = "1.0.57"
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.16", features = ["derive"] }
iso8601-timestamp = { version = "0.2.11", features = [] }
# async
futures = "0.3.8"
@@ -64,9 +65,6 @@ rocket_prometheus = "0.10.0-rc.3"
schemars = "0.8.8"
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
# quark
revolt-quark = { path = "../quark" }
# core
authifier = "1.0.8"
revolt-config = { path = "../core/config" }
@@ -74,7 +72,11 @@ revolt-database = { path = "../core/database", features = [
"rocket-impl",
"redis-is-patched",
] }
revolt-models = { path = "../core/models", features = ["schemas", "validator"] }
revolt-models = { path = "../core/models", features = [
"schemas",
"validator",
"rocket",
] }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
+89 -17
View File
@@ -8,6 +8,8 @@ extern crate serde_json;
pub mod routes;
pub mod util;
use revolt_config::config;
use revolt_database::events::client::EventV1;
use revolt_database::{Database, MongoDb};
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
@@ -16,19 +18,24 @@ use std::net::Ipv4Addr;
use std::str::FromStr;
use async_std::channel::unbounded;
use revolt_quark::authifier::{Authifier, AuthifierEvent};
use revolt_quark::events::client::EventV1;
use revolt_quark::DatabaseInfo;
use authifier::config::{
Captcha, Config as AuthifierConfig, EmailVerificationConfig, ResolveIp, SMTPSettings, Shield,
Template, Templates,
};
use authifier::{Authifier, AuthifierEvent};
use rocket::data::ToByteUnit;
pub async fn web() -> Rocket<Build> {
// Get settings
let config = config().await;
// Ensure environment variables are present
config.preflight_checks();
// Setup database
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
db.migrate_database().await.unwrap();
// Legacy database setup from quark
let legacy_db = DatabaseInfo::Auto.connect().await.unwrap();
// Setup Authifier event channel
let (sender, receiver) = unbounded();
@@ -40,7 +47,8 @@ pub async fn web() -> Rocket<Build> {
authifier::database::MongoDb(client.database("revolt")),
),
},
config: revolt_quark::util::authifier::config(),
config: Default::default(),
// config: authifier_config().await,
event_channel: Some(sender),
};
@@ -65,10 +73,6 @@ pub async fn web() -> Rocket<Build> {
db.clone(),
authifier.database.clone(),
));
async_std::task::spawn(revolt_quark::tasks::start_workers(
legacy_db.clone(),
authifier.database.clone(),
));
// Configure CORS
let cors = CorsOptions {
@@ -97,7 +101,7 @@ pub async fn web() -> Rocket<Build> {
let rocket = rocket::build();
let prometheus = PrometheusMetrics::new();
routes::mount(rocket)
routes::mount(config, rocket)
.attach(prometheus.clone())
.mount("/metrics", prometheus)
.mount("/", rocket_cors::catch_all_options_routes())
@@ -105,7 +109,6 @@ pub async fn web() -> Rocket<Build> {
.mount("/swagger/", swagger)
.manage(authifier)
.manage(db)
.manage(legacy_db)
.manage(cors.clone())
.attach(util::ratelimiter::RatelimitFairing)
.attach(cors)
@@ -116,13 +119,82 @@ pub async fn web() -> Rocket<Build> {
})
}
pub async fn authifier_config() -> AuthifierConfig {
let config = config().await;
let mut auth_config = AuthifierConfig {
email_verification: if !config.api.smtp.host.is_empty() {
EmailVerificationConfig::Enabled {
smtp: SMTPSettings {
from: config.api.smtp.from_address,
host: config.api.smtp.host,
username: config.api.smtp.username,
password: config.api.smtp.password,
reply_to: Some(
config
.api
.smtp
.reply_to
.unwrap_or("support@revolt.chat".into()),
),
port: config.api.smtp.port,
use_tls: config.api.smtp.use_tls,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("templates/deletion.html").into()),
},
welcome: None,
},
}
} else {
EmailVerificationConfig::Disabled
},
..Default::default()
};
auth_config.invite_only = config.api.registration.invite_only;
if !config.api.security.captcha.hcaptcha_key.is_empty() {
auth_config.captcha = Captcha::HCaptcha {
secret: config.api.security.captcha.hcaptcha_key,
};
}
if !config.api.security.authifier_shield_key.is_empty() {
auth_config.shield = Shield::Enabled {
api_key: config.api.security.authifier_shield_key,
strict: false,
};
}
if config.api.security.trust_cloudflare {
auth_config.resolve_ip = ResolveIp::Cloudflare;
}
auth_config
}
#[launch]
async fn rocket() -> _ {
// Configure logging and environment
revolt_quark::configure!();
// Ensure environment variables are present
revolt_quark::variables::delta::preflight_checks();
revolt_config::configure!();
// Start web server
web().await
-8
View File
@@ -1,8 +0,0 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod stats;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![stats::stats]
}
-13
View File
@@ -1,13 +0,0 @@
use revolt_quark::models::stats::Stats;
use revolt_quark::{Db, Result};
use rocket::serde::json::Json;
/// # Query Stats
///
/// Fetch various technical statistics.
#[openapi(tag = "Admin")]
#[get("/stats")]
pub async fn stats(db: &Db) -> Result<Json<Stats>> {
Ok(Json(db.generate_stats().await?))
}
@@ -1,23 +1,84 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Acknowledge Message
///
/// Lets the server and all other clients know that we've seen this message id in this channel.
#[openapi(tag = "Messaging")]
#[put("/<target>/ack/<message>")]
pub async fn req(db: &Db, user: User, target: Ref, message: Ref) -> Result<EmptyResponse> {
pub async fn ack(
db: &State<Database>,
user: User,
target: Reference,
message: Reference,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
channel
.ack(&user.id, &message.id)
.await
.map(|_| EmptyResponse)
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel};
use revolt_models::v0::DataCreateGroup;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_ack_channel() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
DataCreateGroup {
..Default::default()
},
user.id.clone(),
)
.await
.expect("`Channel`");
let message_id = ulid::Ulid::new().to_string();
let response = harness
.client
.put(format!("/channels/{}/ack/{}", group.id(), message_id))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
let event = harness
.wait_for_event(&format!("{}!", user.id), |event| match event {
EventV1::ChannelAck { id, .. } => id == &group.id(),
_ => false,
})
.await;
match event {
EventV1::ChannelAck {
message_id: m_id, ..
} => assert_eq!(m_id, message_id),
_ => unreachable!(),
};
}
}
@@ -1,34 +1,32 @@
use revolt_quark::{
models::{channel::PartialChannel, Channel, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, PartialChannel, User,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
leave_silently: Option<bool>,
}
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Close Channel
///
/// Deletes a server channel, leaves a group or closes a group.
#[openapi(tag = "Channel Information")]
#[delete("/<target>?<options..>")]
pub async fn req(
db: &Db,
pub async fn delete(
db: &State<Database>,
user: User,
target: Ref,
options: OptionsChannelDelete,
target: Reference,
options: v0::OptionsChannelDelete,
) -> Result<EmptyResponse> {
let mut channel = target.as_channel(db).await?;
let mut perms = perms(&user).channel(&channel);
perms.throw_permission(db, Permission::ViewChannel).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
match &channel {
Channel::SavedMessages { .. } => Err(Error::NoEffect),
Channel::SavedMessages { .. } => Err(create_error!(NoEffect)),
Channel::DirectMessage { .. } => channel
.update(
db,
@@ -41,20 +39,58 @@ pub async fn req(
.await
.map(|_| EmptyResponse),
Channel::Group { .. } => channel
.remove_user_from_group(
db,
&user.id,
None,
options.leave_silently.unwrap_or_default(),
)
.remove_user_from_group(db, &user, None, options.leave_silently.unwrap_or_default())
.await
.map(|_| EmptyResponse),
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
perms
.throw_permission(db, Permission::ManageChannel)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
channel.delete(db).await.map(|_| EmptyResponse)
}
}
}
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{events::client::EventV1, Channel};
use revolt_models::v0::DataCreateGroup;
use rocket::http::{Header, Status};
#[rocket::async_test]
async fn success_delete_group() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
DataCreateGroup {
..Default::default()
},
user.id.clone(),
)
.await
.expect("`Channel`");
let response = harness
.client
.delete(format!("/channels/{}", group.id()))
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelDelete { id, .. } => id == &group.id(),
_ => false,
})
.await;
}
// TEST: member leaves group (no delete)
// TEST: no effect with saved messages
// TEST: DM set to inactive
// TEST: server channel deleted
}
@@ -1,60 +1,36 @@
use revolt_quark::{
models::{
channel::{Channel, FieldsChannel, PartialChannel},
message::SystemMessage,
File, User,
},
perms, Database, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, File, PartialChannel, SystemMessage, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Channel Details
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditChannel {
/// Channel name
#[validate(length(min = 1, max = 32))]
name: Option<String>,
/// Channel description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Group owner
owner: Option<String>,
/// Icon
///
/// Provide an Autumn attachment Id.
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
/// Whether this channel is age-restricted
nsfw: Option<bool>,
/// Whether this channel is archived
archived: Option<bool>,
#[validate(length(min = 1))]
remove: Option<Vec<FieldsChannel>>,
}
/// # Edit Channel
///
/// Edit a channel object by its id.
#[openapi(tag = "Channel Information")]
#[patch("/<target>", data = "<data>")]
pub async fn req(
pub async fn edit(
db: &State<Database>,
user: User,
target: Ref,
data: Json<DataEditChannel>,
) -> Result<Json<Channel>> {
target: Reference,
data: Json<v0::DataEditChannel>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
data.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let mut channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageChannel)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
if data.name.is_none()
&& data.description.is_none()
@@ -63,7 +39,7 @@ pub async fn req(
&& data.owner.is_none()
&& data.remove.is_none()
{
return Ok(Json(channel));
return Ok(Json(channel.into()));
}
let mut partial: PartialChannel = Default::default();
@@ -76,12 +52,12 @@ pub async fn req(
{
// Make sure we are the owner of this group
if owner != &user.id {
return Err(Error::NotOwner);
return Err(create_error!(NotOwner));
}
// Ensure user is part of group
if !recipients.contains(&new_owner) {
return Err(Error::NotInGroup);
return Err(create_error!(NotInGroup));
}
// Transfer ownership
@@ -94,10 +70,10 @@ pub async fn req(
to: new_owner,
}
} else {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
@@ -128,7 +104,7 @@ pub async fn req(
..
} => {
if let Some(fields) = &data.remove {
if fields.contains(&FieldsChannel::Icon) {
if fields.contains(&v0::FieldsChannel::Icon) {
if let Some(icon) = &icon {
db.mark_attachment_as_deleted(&icon.id).await?;
}
@@ -136,10 +112,10 @@ pub async fn req(
for field in fields {
match field {
FieldsChannel::Description => {
v0::FieldsChannel::Description => {
description.take();
}
FieldsChannel::Icon => {
v0::FieldsChannel::Icon => {
icon.take();
}
_ => {}
@@ -175,7 +151,7 @@ pub async fn req(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
@@ -185,26 +161,36 @@ pub async fn req(
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
if partial.icon.is_some() {
SystemMessage::ChannelIconChanged { by: user.id }
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
SystemMessage::ChannelIconChanged {
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.send(db, user.as_author_for_system(), &channel, false)
.await
.ok();
}
}
channel
.update(db, partial, data.remove.unwrap_or_default())
.update(
db,
partial,
data.remove
.unwrap_or_default()
.into_iter()
.map(|f| f.into())
.collect(),
)
.await?;
}
_ => return Err(Error::InvalidOperation),
_ => return Err(create_error!(InvalidOperation)),
};
Ok(Json(channel))
Ok(Json(channel.into()))
}
@@ -13,7 +13,7 @@ use rocket::{serde::json::Json, State};
/// Fetch channel by its id.
#[openapi(tag = "Channel Information")]
#[get("/<target>")]
pub async fn fetch_channel(
pub async fn fetch(
db: &State<Database>,
user: User,
target: Reference,
@@ -13,7 +13,7 @@ use rocket_empty::EmptyResponse;
/// Adds another user to the group.
#[openapi(tag = "Groups")]
#[put("/<group_id>/recipients/<member_id>")]
pub async fn req(
pub async fn add_member(
db: &State<Database>,
user: User,
group_id: Reference,
@@ -1,16 +1,23 @@
use revolt_quark::{
models::{Channel, User},
Db, EmptyResponse, Error, Permission, Ref, Result,
};
use revolt_database::{util::reference::Reference, Channel, Database, User};
use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Remove Member from Group
///
/// Removes a user from the group.
#[openapi(tag = "Groups")]
#[delete("/<target>/recipients/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
pub async fn remove_member(
db: &State<Database>,
user: User,
target: Reference,
member: Reference,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?;
@@ -20,24 +27,26 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyR
owner, recipients, ..
} => {
if &user.id != owner {
return Error::from_permission(Permission::ManageChannel);
return Err(create_error!(MissingPermission {
permission: ChannelPermission::ManageChannel.to_string()
}));
}
let member = member.as_user(db).await?;
if user.id == member.id {
return Err(Error::CannotRemoveYourself);
return Err(create_error!(CannotRemoveYourself));
}
if !recipients.iter().any(|x| *x == member.id) {
return Err(Error::NotInGroup);
return Err(create_error!(NotInGroup));
}
channel
.remove_user_from_group(db, &member.id, Some(&user.id), false)
.remove_user_from_group(db, &member, Some(&user.id), false)
.await
.map(|_| EmptyResponse)
}
_ => Err(Error::InvalidOperation),
_ => Err(create_error!(InvalidOperation)),
}
}
@@ -1,9 +1,12 @@
use revolt_quark::{
models::{Invite, User},
perms, Db, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Invite, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use rocket::serde::json::Json;
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Create Invite
///
@@ -12,16 +15,23 @@ use rocket::serde::json::Json;
/// Channel must be a `TextChannel`.
#[openapi(tag = "Channel Invites")]
#[post("/<target>/invites")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Invite>> {
pub async fn create_invite(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<v0::Invite>> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::InviteOthers)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
Invite::create(db, &user, &channel).await.map(Json)
Invite::create_channel_invite(db, &user, &channel)
.await
.map(|invite| invite.into())
.map(Json)
}
@@ -1,31 +1,36 @@
use revolt_quark::{
models::{Channel, User},
perms, Db, Error, Permission, Ref, Result,
use futures::future::join_all;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User,
};
use rocket::serde::json::Json;
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Fetch Group Members
///
/// Retrieves all users who are part of this group.
///
/// This may not return full user information if users are not friends but have mutual connections.
#[openapi(tag = "Groups")]
#[get("/<target>/members")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> {
pub async fn fetch_members(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<Vec<v0::User>>> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
if let Channel::Group { recipients, .. } = channel {
Ok(Json(
db.fetch_users(&recipients)
.await?
.into_iter()
.map(|x| x.with_relationship(&user))
.collect::<Vec<User>>(),
User::fetch_many_ids_as_mutuals(db, &user, &recipients).await?,
))
} else {
Err(Error::InvalidOperation)
Err(create_error!(InvalidOperation))
}
}
@@ -1,20 +1,16 @@
use chrono::Utc;
use revolt_quark::{
models::{Message, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Message, User,
};
use rocket::serde::json::Json;
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use serde::Deserialize;
use validator::Validate;
/// # Search Parameters
#[derive(Validate, Deserialize, JsonSchema)]
pub struct OptionsBulkDelete {
/// Message IDs
#[validate(length(min = 1, max = 100))]
ids: Vec<String>,
}
/// # Bulk Delete Messages
///
/// Delete multiple messages you've sent or one you have permission to delete.
@@ -24,34 +20,37 @@ pub struct OptionsBulkDelete {
/// Messages must have been sent within the past 1 week.
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
pub async fn req(
db: &Db,
pub async fn bulk_delete_messages(
db: &State<Database>,
user: User,
target: Ref,
options: Json<OptionsBulkDelete>,
target: Reference,
options: Json<v0::OptionsBulkDelete>,
) -> Result<EmptyResponse> {
let options = options.into_inner();
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
for id in &options.ids {
if ulid::Ulid::from_string(id)
.map_err(|_| Error::InvalidOperation)?
.map_err(|_| create_error!(InvalidOperation))?
.datetime()
.signed_duration_since(Utc::now())
.num_days()
.abs()
> 7
{
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
}
perms(&user)
.channel(&target.as_channel(db).await?)
.throw_permission(db, Permission::ManageMessages)
.await?;
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
Message::bulk_delete(db, &target.id, options.ids)
.await
@@ -1,7 +1,11 @@
use revolt_quark::{
models::{message::PartialMessage, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, PartialMessage, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Remove All Reactions from Message
///
@@ -10,15 +14,20 @@ use revolt_quark::{
/// Requires `ManageMessages` permission.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions")]
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
pub async fn clear_reactions(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageMessages)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
// Fetch relevant message
let mut message = msg.as_message_in(db, channel.id()).await?;
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
// Clear reactions
message
@@ -1,21 +1,31 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Delete Message
///
/// Delete a message you've sent or one you have permission to delete.
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/<msg>", rank = 2)]
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let message = msg.as_message(db).await?;
if message.channel != target.id {
return Err(Error::NotFound);
}
pub async fn delete(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<EmptyResponse> {
let message = msg.as_message_in_channel(db, &target.id).await?;
if message.author != user.id {
perms(&user)
.channel(&target.as_channel(db).await?)
.throw_permission(db, Permission::ManageMessages)
.await?;
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
message.delete(db).await.map(|_| EmptyResponse)
@@ -1,60 +1,54 @@
use revolt_quark::{
models::message::{PartialMessage, SendableEmbed},
models::{Message, User},
perms,
types::january::Embed,
Db, Error, Permission, Ref, Result, Timestamp,
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_database::{
tasks,
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Message, PartialMessage, User,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{self, Embed};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Message Details
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditMessage {
/// New message content
#[validate(length(min = 1, max = 2000))]
content: Option<String>,
/// Embeds to include in the message
#[validate(length(min = 0, max = 10))]
embeds: Option<Vec<SendableEmbed>>,
}
/// # Edit Message
///
/// Edits a message that you've previously sent.
#[openapi(tag = "Messaging")]
#[patch("/<target>/messages/<msg>", data = "<edit>")]
pub async fn req(
db: &Db,
pub async fn edit(
db: &State<Database>,
user: User,
target: Ref,
msg: Ref,
edit: Json<DataEditMessage>,
) -> Result<Json<Message>> {
target: Reference,
msg: Reference,
edit: Json<v0::DataEditMessage>,
) -> Result<Json<v0::Message>> {
let edit = edit.into_inner();
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
edit.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let config = config().await;
Message::validate_sum(
&edit.content,
edit.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
)?;
// Ensure we have permissions to send a message
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::SendMessage)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
let mut message = msg.as_message(db).await?;
if message.channel != channel.id() {
return Err(Error::NotFound);
}
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?;
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
if message.author != user.id {
return Err(Error::CannotEditMessage);
return Err(create_error!(CannotEditMessage));
}
Message::validate_sum(&edit.content, edit.embeds.as_deref().unwrap_or_default())?;
message.edited = Some(Timestamp::now_utc());
let mut partial = PartialMessage {
edited: message.edited,
@@ -79,14 +73,12 @@ pub async fn req(
// 3. Replace if we are given new embeds
if let Some(embeds) = edit.embeds {
// Ensure we have permissions to send embeds
permissions
.throw_permission_and_view_channel(db, Permission::SendEmbeds)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendEmbeds)?;
new_embeds.clear();
for embed in embeds {
new_embeds.push(embed.clone().into_embed(db, &message.id).await?);
new_embeds.push(message.create_embed(db, embed).await?);
}
}
@@ -95,12 +87,9 @@ pub async fn req(
message.update(db, partial).await?;
// Queue up a task for processing embeds if the we have sufficient permissions
if permissions
.has_permission(db, Permission::SendEmbeds)
.await?
{
if permissions.has_channel_permission(ChannelPermission::SendEmbeds) {
if let Some(content) = edit.content {
revolt_quark::tasks::process_embeds::queue(
tasks::process_embeds::queue(
message.channel.to_string(),
message.id.to_string(),
content,
@@ -109,5 +98,5 @@ pub async fn req(
}
}
Ok(Json(message))
Ok(Json(message.into()))
}
@@ -12,14 +12,13 @@ use rocket::{serde::json::Json, State};
/// Retrieves a message by its id.
#[openapi(tag = "Messaging")]
#[get("/<target>/messages/<msg>")]
pub async fn req(
pub async fn fetch(
db: &State<Database>,
user: User,
target: Reference,
msg: Reference,
) -> Result<Json<v0::Message>> {
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
@@ -1,69 +1,42 @@
use revolt_quark::{
models::{
message::{
BulkMessageResponse, MessageFilter, MessageQuery, MessageSort, MessageTimePeriod,
},
User,
},
perms, Db, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{self, MessageSort};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsQueryMessages {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
/// Message id before which messages should be fetched
#[validate(length(min = 26, max = 26))]
before: Option<String>,
/// Message id after which messages should be fetched
#[validate(length(min = 26, max = 26))]
after: Option<String>,
/// Message sort direction
sort: Option<MessageSort>,
/// Message id to search around
///
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
/// It will also take half of limit rounded as the limits to each side.
/// It also fetches the message ID specified.
#[validate(length(min = 26, max = 26))]
nearby: Option<String>,
/// Whether to include user (and member, if server channel) objects
include_users: Option<bool>,
}
/// # Fetch Messages
///
/// Fetch multiple messages.
#[openapi(tag = "Messaging")]
#[get("/<target>/messages?<options..>")]
pub async fn req(
db: &Db,
pub async fn query(
db: &State<Database>,
user: User,
target: Ref,
options: OptionsQueryMessages,
) -> Result<Json<BulkMessageResponse>> {
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
target: Reference,
options: v0::OptionsQueryMessages,
) -> Result<Json<v0::BulkMessageResponse>> {
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
if let Some(MessageSort::Relevance) = options.sort {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ReadMessageHistory)
.await?;
let OptionsQueryMessages {
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ReadMessageHistory)?;
let v0::OptionsQueryMessages {
limit,
before,
after,
@@ -72,8 +45,9 @@ pub async fn req(
include_users,
} = options;
let messages = db
.fetch_messages(MessageQuery {
Message::fetch_with_users(
db,
MessageQuery {
filter: MessageFilter {
channel: Some(channel.id().to_string()),
..Default::default()
@@ -88,10 +62,16 @@ pub async fn req(
}
},
limit,
})
.await?;
BulkMessageResponse::transform(db, Some(&channel), messages, &user, include_users)
.await
.map(Json)
},
&user,
include_users,
match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server)
}
_ => None,
},
)
.await
.map(Json)
}
@@ -1,4 +1,11 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Add Reaction to Message
///
@@ -6,20 +13,20 @@ use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Resu
#[openapi(tag = "Interactions")]
#[put("/<target>/messages/<msg>/reactions/<emoji>")]
pub async fn react_message(
db: &Db,
db: &State<Database>,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
target: Reference,
msg: Reference,
emoji: Reference,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::React)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::React)?;
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
let message = msg.as_message_in_channel(db, &channel.id()).await?;
// Add the reaction
message
@@ -1,71 +1,43 @@
use revolt_quark::{
models::{
message::{
BulkMessageResponse, MessageFilter, MessageQuery, MessageSort, MessageTimePeriod,
},
User,
},
perms, Db, Error, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, User,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// # Search Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct OptionsMessageSearch {
/// Full-text search query
///
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
#[validate(length(min = 1, max = 64))]
query: String,
/// Maximum number of messages to fetch
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
/// Message id before which messages should be fetched
#[validate(length(min = 26, max = 26))]
before: Option<String>,
/// Message id after which messages should be fetched
#[validate(length(min = 26, max = 26))]
after: Option<String>,
/// Message sort direction
///
/// By default, it will be sorted by latest.
#[serde(default = "MessageSort::default")]
sort: MessageSort,
/// Whether to include user (and member, if server channel) objects
include_users: Option<bool>,
}
/// # Search for Messages
///
/// This route searches for messages within the given parameters.
#[openapi(tag = "Messaging")]
#[post("/<target>/search", data = "<options>")]
pub async fn req(
db: &Db,
pub async fn search(
db: &State<Database>,
user: User,
target: Ref,
options: Json<OptionsMessageSearch>,
) -> Result<Json<BulkMessageResponse>> {
target: Reference,
options: Json<v0::DataMessageSearch>,
) -> Result<Json<v0::BulkMessageResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
let options = options.into_inner();
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
options.validate().map_err(|error| {
create_error!(FailedValidation {
error: error.to_string()
})
})?;
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ReadMessageHistory)
.await?;
let OptionsMessageSearch {
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ReadMessageHistory)?;
let v0::DataMessageSearch {
query,
limit,
before,
@@ -74,8 +46,9 @@ pub async fn req(
include_users,
} = options;
let messages = db
.fetch_messages(MessageQuery {
Message::fetch_with_users(
db,
MessageQuery {
filter: MessageFilter {
channel: Some(channel.id().to_string()),
query: Some(query),
@@ -87,10 +60,16 @@ pub async fn req(
sort: Some(sort),
},
limit,
})
.await?;
BulkMessageResponse::transform(db, Some(&channel), messages, &user, include_users)
.await
.map(Json)
},
&user,
include_users,
match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Some(server)
}
_ => None,
},
)
.await
.map(Json)
}
@@ -32,7 +32,6 @@ pub async fn message_send(
// Ensure we have permissions to send a message
let channel = target.as_channel(db).await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?;
@@ -65,13 +64,8 @@ pub async fn message_send(
// Disallow mentions for new users (TRUST-0: <12 hours age) in public servers
let allow_mentions = if let Some(server) = query.server_ref() {
if server.discoverable {
if (Utc::now() - ulid::Ulid::from_string(&user.id).unwrap().datetime())
< Duration::hours(12)
{
false
} else {
true
}
(Utc::now() - ulid::Ulid::from_string(&user.id).unwrap().datetime())
>= Duration::hours(12)
} else {
true
}
@@ -1,14 +1,12 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
use serde::{Deserialize, Serialize};
/// # Query Parameters
#[derive(Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsUnreact {
/// Remove a specific user's reaction
user_id: Option<String>,
/// Remove all reactions
remove_all: Option<bool>,
}
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, User,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Remove Reaction(s) to Message
///
@@ -18,29 +16,27 @@ pub struct OptionsUnreact {
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions/<emoji>?<options..>")]
pub async fn unreact_message(
db: &Db,
db: &State<Database>,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
options: OptionsUnreact,
target: Reference,
msg: Reference,
emoji: Reference,
options: v0::OptionsUnreact,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::React)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
// Check if we need to escalate permissions
let remove_all = options.remove_all.unwrap_or_default();
if options.user_id.is_some() || remove_all {
permissions
.throw_permission(db, Permission::ManageMessages)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
let message = msg.as_message_in_channel(db, &channel.id()).await?;
// Check if we should wipe all of this reaction
if remove_all {
+19 -19
View File
@@ -28,29 +28,29 @@ mod webhook_fetch_all;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
channel_ack::req,
channel_fetch::fetch_channel,
members_fetch::req,
channel_delete::req,
channel_edit::req,
invite_create::req,
channel_ack::ack,
channel_fetch::fetch,
members_fetch::fetch_members,
channel_delete::delete,
channel_edit::edit,
invite_create::create_invite,
message_send::message_send,
message_query::req,
message_search::req,
message_fetch::req,
message_edit::req,
message_bulk_delete::req,
message_delete::req,
message_query::query,
message_search::search,
message_fetch::fetch,
message_edit::edit,
message_bulk_delete::bulk_delete_messages,
message_delete::delete,
group_create::create_group,
group_add_member::req,
group_remove_member::req,
voice_join::req,
permissions_set::req,
permissions_set_default::req,
group_add_member::add_member,
group_remove_member::remove_member,
voice_join::call,
permissions_set::set_role_permissions,
permissions_set_default::set_default_permissions,
message_react::react_message,
message_unreact::unreact_message,
message_clear_reactions::clear_reactions,
webhook_create::req,
webhook_fetch_all::req,
webhook_create::create_webhook,
webhook_fetch_all::fetch_webhooks,
]
}
@@ -1,17 +1,12 @@
use rocket::serde::json::Json;
use serde::Deserialize;
use revolt_quark::{
models::{Channel, User},
perms, Db, Error, Override, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User,
};
/// # Permission Value
#[derive(Deserialize, JsonSchema)]
pub struct Data {
/// Allow / deny values to set for this role
permissions: Override,
}
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, Override};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use serde::Deserialize;
/// # Set Role Permission
///
@@ -20,40 +15,39 @@ pub struct Data {
/// Channel must be a `TextChannel` or `VoiceChannel`.
#[openapi(tag = "Channel Permissions")]
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn req(
db: &Db,
pub async fn set_role_permissions(
db: &State<Database>,
user: User,
target: Ref,
target: Reference,
role_id: String,
data: Json<Data>,
) -> Result<Json<Channel>> {
data: Json<v0::DataSetRolePermissions>,
) -> Result<Json<v0::Channel>> {
let mut channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
permissions
.throw_permission_and_view_channel(db, Permission::ManagePermissions)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?;
if let Some(server) = permissions.server.get() {
if let Some(server) = query.server_ref() {
if let Some(role) = server.roles.get(&role_id) {
if role.rank <= permissions.get_member_rank().unwrap_or(i64::MIN) {
return Err(Error::NotElevated);
if role.rank <= query.get_member_rank().unwrap_or(i64::MIN) {
return Err(create_error!(NotElevated));
}
let current_value: Override = role.permissions.into();
permissions
.throw_permission_override(db, current_value, data.permissions)
.throw_permission_override(current_value, &data.permissions)
.await?;
channel
.set_role_permission(db, &role_id, data.permissions.into())
.set_role_permission(db, &role_id, data.permissions.clone().into())
.await?;
Ok(Json(channel))
Ok(Json(channel.into()))
} else {
Err(Error::NotFound)
Err(create_error!(NotFound))
}
} else {
Err(Error::InvalidOperation)
Err(create_error!(InvalidOperation))
}
}
@@ -1,24 +1,11 @@
use rocket::serde::json::Json;
use serde::Deserialize;
use revolt_quark::{
models::{channel::PartialChannel, Channel, User},
perms, Db, Error, Override, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, PartialChannel, User,
};
/// # Permission Value
#[derive(Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum DataDefaultChannelPermissions {
Value {
/// Permission values to set for members in a `Group`
permissions: u64,
},
Field {
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
permissions: Override,
},
}
use revolt_models::v0::{self, DataDefaultChannelPermissions};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Set Default Permission
///
@@ -27,19 +14,19 @@ pub enum DataDefaultChannelPermissions {
/// Channel must be a `Group`, `TextChannel` or `VoiceChannel`.
#[openapi(tag = "Channel Permissions")]
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn req(
db: &Db,
pub async fn set_default_permissions(
db: &State<Database>,
user: User,
target: Ref,
data: Json<DataDefaultChannelPermissions>,
) -> Result<Json<Channel>> {
target: Reference,
data: Json<v0::DataDefaultChannelPermissions>,
) -> Result<Json<v0::Channel>> {
let data = data.into_inner();
let mut channel = target.as_channel(db).await?;
let mut perm = perms(&user).channel(&channel);
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let permissions = calculate_channel_permissions(&mut query).await;
perm.throw_permission_and_view_channel(db, Permission::ManagePermissions)
.await?;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?;
match &channel {
Channel::Group { .. } => {
@@ -55,7 +42,7 @@ pub async fn req(
)
.await?;
} else {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
}
Channel::TextChannel {
@@ -66,30 +53,27 @@ pub async fn req(
default_permissions,
..
} => {
if let DataDefaultChannelPermissions::Field { permissions } = data {
perm.throw_permission_override(
db,
default_permissions.map(|x| x.into()),
permissions,
)
.await?;
if let DataDefaultChannelPermissions::Field { permissions: field } = data {
permissions
.throw_permission_override(default_permissions.map(|x| x.into()), &field)
.await?;
channel
.update(
db,
PartialChannel {
default_permissions: Some(permissions.into()),
default_permissions: Some(field.into()),
..Default::default()
},
vec![],
)
.await?;
} else {
return Err(Error::InvalidOperation);
return Err(create_error!(InvalidOperation));
}
}
_ => return Err(Error::InvalidOperation),
_ => return Err(create_error!(InvalidOperation)),
}
Ok(Json(channel))
Ok(Json(channel.into()))
}
+40 -35
View File
@@ -1,40 +1,37 @@
use revolt_quark::{
models::{Channel, User},
perms,
variables::delta::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL},
Db, Error, Permission, Ref, Result,
use revolt_config::config;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
/// # Voice Server Token Response
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct CreateVoiceUserResponse {
/// Token for authenticating with the voice server
token: String,
}
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Join Call
///
/// Asks the voice server for a token to join the call.
#[openapi(tag = "Voice")]
#[post("/<target>/join_call")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUserResponse>> {
pub async fn call(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<v0::LegacyCreateVoiceUserResponse>> {
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
calculate_channel_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::Connect)?;
permissions
.throw_permission_and_view_channel(db, Permission::Connect)
.await?;
if !*USE_VOSO {
return Err(Error::VosoUnavailable);
let config = config().await;
if config.api.security.voso_legacy_token.is_empty() {
return Err(create_error!(VosoUnavailable));
}
match channel {
Channel::SavedMessages { .. } | Channel::TextChannel { .. } => {
return Err(Error::CannotJoinCall)
return Err(create_error!(CannotJoinCall))
}
_ => {}
}
@@ -44,33 +41,41 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUse
// - If not, create it.
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/room/{}", *VOSO_URL, channel.id()))
.get(&format!(
"{}/room/{}",
config.hosts.voso_legacy,
channel.id()
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
config.api.security.voso_legacy_token.clone(),
)
.send()
.await;
match result {
Err(_) => return Err(Error::VosoUnavailable),
Err(_) => return Err(create_error!(VosoUnavailable)),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => (),
reqwest::StatusCode::NOT_FOUND => {
if (client
.post(&format!("{}/room/{}", *VOSO_URL, channel.id()))
.post(&format!(
"{}/room/{}",
config.hosts.voso_legacy,
channel.id()
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
config.api.security.voso_legacy_token.clone(),
)
.send()
.await)
.is_err()
{
return Err(Error::VosoUnavailable);
return Err(create_error!(VosoUnavailable));
}
}
_ => return Err(Error::VosoUnavailable),
_ => return Err(create_error!(VosoUnavailable)),
},
}
@@ -78,13 +83,13 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUse
if let Ok(response) = client
.post(&format!(
"{}/room/{}/user/{}",
*VOSO_URL,
config.hosts.voso_legacy,
channel.id(),
user.id
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
config.api.security.voso_legacy_token,
)
.send()
.await
@@ -92,9 +97,9 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUse
response
.json()
.await
.map_err(|_| Error::InvalidOperation)
.map_err(|_| create_error!(InvalidOperation))
.map(Json)
} else {
Err(Error::VosoUnavailable)
Err(create_error!(VosoUnavailable))
}
}
@@ -16,7 +16,7 @@ use validator::Validate;
/// Creates a webhook which 3rd party platforms can use to send messages
#[openapi(tag = "Webhooks")]
#[post("/<target>/webhooks", data = "<data>")]
pub async fn req(
pub async fn create_webhook(
db: &State<Database>,
user: User,
target: Reference,
@@ -12,7 +12,7 @@ use rocket::{serde::json::Json, State};
/// Gets all webhooks inside the channel
#[openapi(tag = "Webhooks")]
#[get("/<channel_id>/webhooks")]
pub async fn req(
pub async fn fetch_webhooks(
db: &State<Database>,
user: User,
channel_id: Reference,
@@ -1,14 +1,18 @@
use revolt_quark::{
models::{Invite, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Invite, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Delete Invite
///
/// Delete an invite by its id.
#[openapi(tag = "Invites")]
#[delete("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
pub async fn delete(db: &State<Database>, user: User, target: Reference) -> Result<EmptyResponse> {
let invite = target.as_invite(db).await?;
if user.id == invite.creator() {
@@ -17,10 +21,10 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
match invite {
Invite::Server { code, server, .. } => {
let server = db.fetch_server(&server).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageServer)
.await?;
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
calculate_server_permissions(&mut query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?;
db.delete_invite(&code).await
}
+11 -68
View File
@@ -1,71 +1,14 @@
use revolt_quark::models::{Channel, File, Invite};
use revolt_quark::{Db, Ref, Result};
use rocket::serde::json::Json;
use serde::Serialize;
/// # Invite
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Debug, Clone, JsonSchema)]
#[serde(tag = "type")]
pub enum InviteResponse {
/// Server channel invite
Server {
/// Invite code
code: String,
/// Id of the server
server_id: String,
/// Name of the server
server_name: String,
/// Attachment for server icon
#[serde(skip_serializing_if = "Option::is_none")]
server_icon: Option<File>,
/// Attachment for server banner
#[serde(skip_serializing_if = "Option::is_none")]
server_banner: Option<File>,
/// Enum of server flags
#[serde(skip_serializing_if = "Option::is_none")]
server_flags: Option<i32>,
/// Id of server channel
channel_id: String,
/// Name of server channel
channel_name: String,
/// Description of server channel
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
/// Name of user who created the invite
user_name: String,
/// Avatar of the user who created the invite
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
/// Number of members in this server
member_count: i64,
},
/// Group channel invite
Group {
/// Invite code
code: String,
/// Id of group channel
channel_id: String,
/// Name of group channel
channel_name: String,
/// Description of group channel
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
/// Name of user who created the invite
user_name: String,
/// Avatar of the user who created the invite
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
},
}
use revolt_database::{util::reference::Reference, Channel, Database, Invite};
use revolt_models::v0;
use revolt_result::Result;
use rocket::{serde::json::Json, State};
/// # Fetch Invite
///
/// Fetch an invite by its id.
#[openapi(tag = "Invites")]
#[get("/<target>")]
pub async fn req(db: &Db, target: Ref) -> Result<Json<InviteResponse>> {
pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::InviteResponse>> {
Ok(Json(match target.as_invite(db).await? {
Invite::Server {
channel, creator, ..
@@ -90,19 +33,19 @@ pub async fn req(db: &Db, target: Ref) -> Result<Json<InviteResponse>> {
} => {
let server = db.fetch_server(&server).await?;
InviteResponse::Server {
v0::InviteResponse::Server {
code: target.id,
member_count: db.fetch_member_count(&server.id).await? as i64,
server_id: server.id,
server_name: server.name,
server_icon: server.icon,
server_banner: server.banner,
server_icon: server.icon.map(|f| f.into()),
server_banner: server.banner.map(|f| f.into()),
server_flags: server.flags,
channel_id: id,
channel_name: name,
channel_description: description,
user_name: user.username,
user_avatar: user.avatar,
user_avatar: user.avatar.map(|f| f.into()),
}
}
_ => unreachable!(),
@@ -120,13 +63,13 @@ pub async fn req(db: &Db, target: Ref) -> Result<Json<InviteResponse>> {
name,
description,
..
} => InviteResponse::Group {
} => v0::InviteResponse::Group {
code: target.id,
channel_id: id,
channel_name: name,
channel_description: description,
user_name: user.username,
user_avatar: user.avatar,
user_avatar: user.avatar.map(|f| f.into()),
},
_ => unreachable!(),
}
+31 -31
View File
@@ -1,47 +1,47 @@
use revolt_quark::{
models::{Channel, Invite, Server, User},
variables::delta::MAX_SERVER_COUNT,
Db, Error, Ref, Result,
};
use rocket::serde::json::Json;
use serde::Serialize;
/// # Join Response
#[derive(Serialize, JsonSchema)]
#[serde(tag = "type")]
pub enum InviteJoinResponse {
Server {
/// Channels in the server
channels: Vec<Channel>,
/// Server we are joining
server: Server,
},
}
use revolt_database::{util::reference::Reference, Channel, Database, Invite, Member, User};
use revolt_models::v0::{self, InviteJoinResponse};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// # Join Invite
///
/// Join an invite by its ID.
/// Join an invite by its ID
#[openapi(tag = "Invites")]
#[post("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<InviteJoinResponse>> {
pub async fn join(
db: &State<Database>,
user: User,
target: Reference,
) -> Result<Json<v0::InviteJoinResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
return Err(create_error!(IsBot));
}
if !user.can_acquire_server(db).await? {
return Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
});
}
user.can_acquire_server(db).await?;
let invite = target.as_invite(db).await?;
match &invite {
Invite::Server { server, .. } => {
let server = db.fetch_server(server).await?;
let channels = server.create_member(db, user, None).await?;
Ok(Json(InviteJoinResponse::Server { channels, server }))
let channels = Member::create(db, &server, &user, None).await?;
Ok(Json(InviteJoinResponse::Server {
channels: channels.into_iter().map(|c| c.into()).collect(),
server: server.into(),
}))
}
Invite::Group {
channel, creator, ..
} => {
let mut channel = db.fetch_channel(channel).await?;
channel.add_user_to_group(db, &user, creator).await?;
if let Channel::Group { recipients, .. } = &channel {
Ok(Json(InviteJoinResponse::Group {
users: User::fetch_many_ids_as_mutuals(db, &user, recipients).await?,
channel: channel.into(),
}))
} else {
unreachable!()
}
}
_ => unreachable!(),
}
}
+5 -1
View File
@@ -6,5 +6,9 @@ mod invite_fetch;
mod invite_join;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![invite_fetch::req, invite_join::req, invite_delete::req]
openapi_get_routes_spec![
invite_fetch::fetch,
invite_join::join,
invite_delete::delete
]
}
+3 -6
View File
@@ -1,10 +1,9 @@
use revolt_quark::variables::delta::IS_STAGING;
use revolt_config::{config, Settings};
use revolt_rocket_okapi::{revolt_okapi::openapi3::OpenApi, settings::OpenApiSettings};
pub use rocket::http::Status;
pub use rocket::response::Redirect;
use rocket::{Build, Rocket};
mod admin;
mod bots;
mod channels;
mod customisation;
@@ -18,15 +17,14 @@ mod sync;
mod users;
mod webhooks;
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
pub fn mount(config: Settings, mut rocket: Rocket<Build>) -> Rocket<Build> {
let settings = OpenApiSettings::default();
if *IS_STAGING {
if config.features.webhooks_enabled {
mount_endpoints_and_merged_docs! {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root],
"/admin" => admin::routes(),
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
@@ -47,7 +45,6 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root],
"/admin" => admin::routes(),
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
+2 -2
View File
@@ -1,8 +1,8 @@
use authifier::models::Session;
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_database::{Database, User};
use revolt_models::v0;
use revolt_quark::authifier::models::Session;
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
@@ -28,7 +28,7 @@ pub struct DataOnboard {
/// This sets a new username, completes onboarding and allows a user to start using Revolt.
#[openapi(tag = "Onboarding")]
#[post("/complete", data = "<data>")]
pub async fn req(
pub async fn complete(
db: &State<Database>,
session: Session,
user: Option<User>,
+4 -2
View File
@@ -1,4 +1,6 @@
use revolt_quark::{authifier::models::Session, models::User};
use authifier::models::Session;
use revolt_database::User;
use rocket::serde::json::Json;
use serde::Serialize;
@@ -14,7 +16,7 @@ pub struct DataHello {
/// This will tell you whether the current account requires onboarding or whether you can continue to send requests as usual. You may skip calling this if you're restoring an existing session.
#[openapi(tag = "Onboarding")]
#[get("/hello")]
pub async fn req(_session: Session, user: Option<User>) -> Json<DataHello> {
pub async fn hello(_session: Session, user: Option<User>) -> Json<DataHello> {
Json(DataHello {
onboarding: user.is_none(),
})

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