Compare commits

..
160 Commits
Author SHA1 Message Date
Paul Makles 13b95d383e fix: import std::env in config 2024-04-08 22:46:35 +01:00
Paul Makles 3c76005c33 ci: strip references to quark 2024-04-08 22:00:33 +01:00
Paul Makles 3065e153d5 refactor: delete quark crate (closes #239) 2024-04-08 21:56:35 +01:00
Paul Makles a1eeedabb9 refactor(bonfire): remove quark dependency 2024-04-08 21:49:19 +01:00
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
Paul Makles cf82972a80 chore: bump version 2024-02-10 23:51:04 +00:00
Bob Bobs ea00f0fec1 fix(delta): set ratelimit headers on 429 responses 2024-02-10 23:49:15 +00:00
Bob Bobs 226dbca6e0 fix(core/models): use IndexMap for reactions 2024-02-10 23:45:34 +00:00
Bob Bobs e4b5a23f71 fix(delta): MemberResponse only include roles member has 2024-02-10 23:44:20 +00:00
Bob Bobs 9663caa1ff fix(delta): use untagged enum for MemberResponse 2024-02-10 23:44:20 +00:00
Paul Makles f9f0f409a8 chore: bump version to 0.6.11 2024-02-10 01:05:08 +00:00
Paul Makles 8f4d6c864b feat: block mentions from low trust users (account created < 12 hours) 2024-02-10 01:04:52 +00:00
Paul Makles 513461c99a chore: bump version 2024-02-06 13:43:00 +00:00
ToastXC 41ef76ff3e feat: member fetching with roles (#299) 2024-02-06 13:20:35 +00:00
Paul Makles c451e431cd merge: pull request #301 from TheBobBobs/bonfire-redis
fix(bonfire): random disconnects from redis
2024-02-06 13:18:55 +00:00
Amy 09a848f5a6 fix: don't exceed the max emoji limit by one
closes #295
2024-02-06 13:17:13 +00:00
Bob Bobs 308193377d fix(bonfire): use REDIS_URI env var 2024-02-02 10:31:11 -07:00
Bob Bobs 3d3326e68a (bonfire): parse correct PayloadType 2024-02-01 20:07:12 -07:00
Bob Bobs 88c2232a6c fix(bonfire): use fred for redis
https://github.com/aembke/fred.rs
2024-02-01 19:00:45 -07:00
Bob Bobs 25016efbc2 fix(database): fetch all server emojis instead of one 2023-12-02 11:46:18 +00:00
Bob Bobs db63ac4064 fix: add emojis to ServerCreate event 2023-12-02 11:46:18 +00:00
TheBobBobs 1f593a1708 fix: allow reacting with existing emojis when at limit 2023-12-02 11:45:23 +00:00
TheBobBobs 219c16a69c fix: set relationship for users in BulkMessageResponse 2023-12-01 20:54:59 +00:00
Paul Makles aed8f69f34 fix(database): set channel ids for new server object 2023-11-20 10:54:48 +00:00
Paul Makles 196a856c68 refactor: move and update fixme / todo comments 2023-10-28 14:53:39 +01:00
Paul Makles 9bf60e87e9 fix: validate masquerade on new message models 2023-10-28 14:53:28 +01:00
Paul Makles b90e7a4412 fix: prevent potential double fetching of cached server 2023-10-28 14:53:12 +01:00
Paul Makles cb813e91f0 chore: cut new 0.6.9 release 2023-10-28 14:39:10 +01:00
Paul Makles a6bc9fcbb3 refactor: implement Server::create 2023-10-28 14:25:29 +01:00
Paul Makles cd0737911b refactor: use fixtures to populate db
refactor: method for creating server channels
2023-10-27 16:31:37 +01:00
Paul Makles 7705354d4d ci: use reference db for generating spec 2023-10-26 15:07:03 +01:00
Paul Makles 39ac8b2278 refactor: don't create unnecessary reference 2023-10-26 14:12:28 +01:00
arslee07 c5494aa1ca fix: respect Permission::SendEmbeds when editing a message 2023-10-26 14:12:28 +01:00
Paul Makles 1a96bf5a4b chore: remove redundant file [skip ci] 2023-10-26 14:05:01 +01:00
Paul Makles 37a1244751 test: ignore hello world test because it breaks cargo test? 2023-10-26 14:05:01 +01:00
Paul Makles bf39f18f8d fix: always generate new test database 2023-10-26 14:05:01 +01:00
Paul Makles 95be7dc87a ci: use nextest because cargo test sig sevs 2023-10-26 14:05:01 +01:00
Paul Makles 9be171c7b6 fix: don't copy the old test file 2023-10-26 14:05:01 +01:00
Paul Makles 786fbb8ae4 chore: disable broken tests 2023-10-26 14:05:01 +01:00
Paul Makles 6c0210e8a1 feat: always use test configuration during tests 2023-10-26 14:05:01 +01:00
Paul Makles 456bf7b42a fix: check server ownership for text channel
refactor: minor cleanup of code
2023-10-26 14:05:01 +01:00
Paul Makles 21ffea0f9f fix(core/database): store member during permission query 2023-10-26 14:05:01 +01:00
Paul Makles 96d90215d2 fix(core/models): validator feature flag not properly gatekept 2023-10-26 14:05:01 +01:00
Paul Makles 29dcf714f2 chore: start work on test for permission issue 2023-10-26 14:05:01 +01:00
Paul Makles cb97004d3f feat(core): provide user profile where appropriate 2023-10-16 09:33:36 +01:00
Paul Makles 0b6e5dd9db chore: add nix file to vscode config 2023-10-16 09:23:11 +01:00
Paul Makles 7132877201 feat(core): validation for files in reference db 2023-10-16 09:22:53 +01:00
Paul Makles 4d42fb74e7 fix(core): publish user settings event 2023-10-16 09:22:17 +01:00
Paul Makles 866ace3381 refactor: use config for max bot count 2023-10-16 09:21:48 +01:00
Paul Makles 0798e81862 feat(core): implement ratelimit events for reference db 2023-10-16 09:21:34 +01:00
Paul Makles 2fa5ac41ac refactor: tests for more group routes 2023-09-22 19:04:17 +01:00
Paul Makles 25ae1555a2 chore: add nextest and fmt to nix 2023-09-22 19:03:49 +01:00
Paul Makles 881888bd1c chore: add timeout for nextest runs 2023-09-22 19:03:29 +01:00
Paul Makles 41f20c2239 ci: use custom Revolt.toml for tests 2023-09-10 15:46:00 +01:00
Paul Makles 38f1381fea chore: switch to resolver=2 2023-09-10 15:19:23 +01:00
Paul Makles 6838b0b1f0 refactor: ensure in-memory database consistency
test: update authifier to allow for in-memory tests
ci: explicitly run in-memory and mongo tests
2023-09-10 14:49:27 +01:00
Paul Makles 055deab105 fix: allow bots to authenticate with new routes 2023-09-10 14:48:13 +01:00
Paul Makles 6d2073120d fix: carry over send embed permission when sending [skip ci] 2023-09-10 12:41:23 +01:00
Paul Makles 9789909061 feat(delta): rewrite emoji routes w/o quark
#283
2023-09-05 12:36:18 +01:00
Paul Makles 895de86f1e fix(core/config): use uppercase filename 2023-09-03 19:00:17 +01:00
Paul Makles 98b8c5403b feat(delta): rewrite channel / message fetch 2023-09-03 18:27:21 +01:00
Paul Makles aa7c75c60e chore: ignore Revolt.toml 2023-09-03 18:12:15 +01:00
Paul Makles 4270f0c5d4 feat(core): implement create group 2023-09-03 18:12:08 +01:00
Paul Makles eb1f45d208 fix(core/database): use attachments collection name 2023-09-03 17:43:13 +01:00
Paul Makles 9929f7499a refactor(core): use core crates for sending messages 2023-09-03 16:31:39 +01:00
Paul Makles 279d9ef1b5 refactor(core): remove quark references from webhook routes 2023-09-03 15:52:18 +01:00
Paul Makles 5a9bb9e68d fix: convert index map/set to hash map/set
fix: idempotency token should use correct crate cfg
2023-08-27 15:51:20 +01:00
Paul Makles a8fc9e928a fix: import validator crate directly in derive 2023-08-27 15:45:23 +01:00
Paul Makles da231ae390 fix: remove indexmap from models 2023-08-27 15:43:57 +01:00
Paul Makles 7a31da4189 fix: add indexmap/serde to model depends 2023-08-27 15:40:05 +01:00
Paul Makles 7d0b5392b5 fix: should include crate in build script too 2023-08-27 15:33:28 +01:00
Paul Makles 1e1d44c0e4 fix: add config crate to Dockerfile
chore: bump release version
2023-08-27 15:30:53 +01:00
Paul Makles 1c4588967f merge: branch 'insert/dev-branch' 2023-08-27 15:18:52 +01:00
Paul Makles 853366a297 feat: add support for Firebase Cloud Messaging 2023-08-27 15:18:19 +01:00
Paul Makles 7568f49755 test: add tests to bot routes 2023-08-27 15:01:22 +01:00
Paul Makles 0542788567 feat(delta): test harness for web server 2023-08-27 13:01:21 +01:00
Paul Makles 73f7b8f007 feat: ensure bot invites work correctly 2023-08-27 12:21:48 +01:00
Paul Makles c2412190a8 refactor: solve clippy hints 2023-08-27 11:23:02 +01:00
Paul Makles 5372296dc0 feat(core): implement message tasks
includes:
- acknowledgements
- last_message_id
- embeds
- web push
2023-08-27 11:17:03 +01:00
Paul Makles 41a47bdf8f feat: add revolt_config crate 2023-08-27 11:15:20 +01:00
Paul Makles 51c26e324d refactor: remove quark/web 2023-08-26 16:19:00 +01:00
Paul Makles 3a55d00c6a chore: disable nix environment extension 2023-08-26 16:17:21 +01:00
Paul Makles 004e84ece9 fix: revoke all channel permissions if can't view 2023-08-26 16:17:20 +01:00
Paul Makles 3d0914ce52 chore: add nix shell configuration 2023-08-26 16:17:19 +01:00
Paul Makles bde432cb75 chore: strip bot model from quark 2023-08-26 16:16:48 +01:00
Paul Makles 28d225952a chore: add separate debug Docker build script 2023-08-26 15:34:25 +01:00
Paul Makles 2fb9e7a802 merge: branch 'master' into insert/dev-branch 2023-08-26 13:34:44 +01:00
Paul Makles 431046cb4c chore: cut new release with redis patches and debug profile 2023-08-26 13:33:05 +01:00
Zomatree 92032cd9bc chore: Log errors from failed pub/sub messages 2023-08-26 13:14:24 +01:00
Lea bfdd713d38 fix: allow fetching mutuals for bots 2023-08-26 13:14:15 +01:00
Paul Makles 430df03b20 chore: bump sentry crate 2023-08-10 09:42:44 +01:00
Paul Makles f5f70287e7 feat: add SENTRY_DSN env variable 2023-08-10 09:40:41 +01:00
Paul Makles d5ba4ebf0c feat(core/database): implement permissions backend 2023-08-05 17:42:23 +01:00
Paul Makles 3bfead4ff1 feat(core/database): system message helper 2023-08-05 17:03:16 +01:00
Paul Makles d04621b1d6 feat(core): implement message db methods 2023-08-05 16:59:58 +01:00
Paul Makles a681df04bd feat(core): permissions query, finish bots impl 2023-08-05 16:14:47 +01:00
Paul Makles 9f3c1036d0 chore: refine user and bot creation methods 2023-08-05 12:06:43 +01:00
Paul Makles c9011ac692 chore: migrate bots create route 2023-08-05 11:24:02 +01:00
Paul Makles 42f977f536 feat: start work on migrating routes 2023-08-03 22:11:48 +01:00
Paul Makles a0580c5f5a chore: remove legacy query route 2023-08-03 19:43:05 +01:00
Paul Makles 121a9cd87c feat(core): implement message model
closes #273
2023-08-03 19:32:47 +01:00
Paul Makles d87d608d9e chore(core): flesh out implementations for channels / servers 2023-08-03 18:27:13 +01:00
Paul Makles bd9ecc1519 fix: missing conversion 2023-08-02 00:31:19 +01:00
Paul Makles f5a113da47 feat(core): implement server ban
closes #274
2023-08-01 21:19:07 +01:00
Paul Makles 11fdb0c1dc feat(core): implement emoji model
closes #270
2023-08-01 20:45:58 +01:00
Paul Makles e0033ceb12 feat(core): implement channel unreads model
closes #269
2023-08-01 17:41:43 +01:00
Paul Makles 7318ec6ef6 chore(core/models): add cfg_attr where necessary 2023-08-01 15:54:56 +01:00
Paul Makles a516c7adcf feat(core): implement channel invite model
closes #268
2023-08-01 15:53:43 +01:00
Paul Makles c48109ca66 docs: add note for 'Id' casing 2023-08-01 15:51:57 +01:00
Paul Makles f0ef781cd7 chore: add MSRV to README [skip ci]
closes #254
2023-08-01 15:04:45 +01:00
Paul Makles d9dd040db4 feat(core/models): implement server members model
closes #280
2023-08-01 15:00:07 +01:00
Paul Makles fec947b4c8 feat(core/models): implement server model
closes #279
2023-08-01 14:55:25 +01:00
Paul Makles ec6df36c25 chore: strip legacy admin API 2023-08-01 14:35:36 +01:00
Paul Makles 76150db293 chore: add prometheus metrics 2023-07-04 17:52:40 +01:00
Paul Makles 815c3fe99e chore: cut a new release (0.6.4) 2023-07-02 08:45:39 +01:00
Zomatree 54878e8e8d feat: add webhook permissions 2023-07-02 08:41:38 +01:00
TheBobBobs 6e4798f1d4 fix(bonfire): populate cache for calculating permissions (#259) 2023-07-02 08:40:15 +01:00
TheBobBobs 55bd6fb087 fix: incorrect owner rank 2023-07-02 08:35:49 +01:00
Paul Makles 49035f4817 feat: ratelimit user edit route and discriminator changes 2023-06-15 19:24:53 +01:00
Paul Makles c0ebaa0bd3 chore: include discriminator in ban list response 2023-06-15 19:23:47 +01:00
Paul Makles b98b244fc3 fix: enable staging for API spec generation 2023-06-15 15:21:00 +01:00
Paul Makles c8d5128b0c chore: add additional sanitisation 2023-06-11 12:26:18 +01:00
Paul Makles 0578a05a05 feat: add remove "displayname" field 2023-06-11 12:05:08 +01:00
Paul Makles 9a412b3e08 fix: actually update the display name 2023-06-11 11:50:21 +01:00
Paul Makles 8deec1f80a fix: display name is optional 2023-06-11 10:56:03 +01:00
Paul Makles 26afbeed84 chore: extend discriminator block list 2023-06-11 10:49:51 +01:00
386 changed files with 13406 additions and 14044 deletions
+2
View File
@@ -0,0 +1,2 @@
[profile.default]
slow-timeout = { period = "3s", terminate-after = 2 }
+28
View File
@@ -1,5 +1,7 @@
# MongoDB URI # MongoDB URI
MONGODB=mongodb://localhost MONGODB=mongodb://localhost
AUTUMN_MONGO_URI=mongodb://database
REDIS_URI=redis://localhost/
# URL to where the Revolt app is publicly accessible # URL to where the Revolt app is publicly accessible
REVOLT_APP_URL=http://local.revolt.chat:5000 REVOLT_APP_URL=http://local.revolt.chat:5000
@@ -59,6 +61,9 @@ REVOLT_UNSAFE_NO_EMAIL=1
## Application Settings ## Application Settings
## ##
# Whether to enable staging only features
REVOLT_IS_STAGING=1
# Whether to only allow users to sign up if they have an invite code # Whether to only allow users to sign up if they have an invite code
REVOLT_INVITE_ONLY=0 REVOLT_INVITE_ONLY=0
@@ -77,3 +82,26 @@ REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nf
## ##
# VOSO_MANAGE_TOKEN=CHANGEME # 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: on:
push: push:
branches: branches:
- "master" - "main"
tags: tags:
- "*" - "*"
paths-ignore: paths-ignore:
@@ -15,7 +15,7 @@ on:
- "README" - "README"
pull_request: pull_request:
branches: branches:
- "master" - "main"
paths: paths:
- "Dockerfile" - "Dockerfile"
workflow_dispatch: workflow_dispatch:
+28 -21
View File
@@ -3,7 +3,7 @@ name: Rust build, test, and generate specification
on: on:
push: push:
pull_request: pull_request:
branches: [master] branches: [main]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -20,6 +20,11 @@ jobs:
toolchain: stable toolchain: stable
override: true override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Install cargo-nextest
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-nextest
locked: true
- name: Run cargo build - name: Run cargo build
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -30,36 +35,38 @@ jobs:
run: | run: |
docker-compose -f docker-compose.db.yml up -d docker-compose -f docker-compose.db.yml up -d
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
- name: Run cargo test (with MongoDB)
uses: actions-rs/cargo@v1
env:
MONGODB: mongodb://localhost
with:
command: test
- name: Copy .env.example - name: Copy .env.example
if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: | run: |
cp .env.example .env cp .env.example .env
- name: Start API in background - name: Run cargo test
if: github.event_name != 'pull_request' && github.ref_name == 'master' env:
TEST_DB: REFERENCE
run: | run: |
cargo run --bin revolt-delta & cargo nextest run
- name: Run cargo test (with MongoDB)
env:
TEST_DB: MONGODB
MONGODB: mongodb://localhost
run: |
cargo nextest run
- name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'main'
env:
TEST_DB: REFERENCE
run: |
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
- name: Wait for API to go up - 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 uses: nev7n/wait_for_response@v1
with: with:
url: "http://localhost:8000/" url: "http://localhost:8000/"
- name: Checkout API repository - 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 uses: actions/checkout@v3
with: with:
repository: revoltchat/api repository: revoltchat/api
@@ -67,11 +74,11 @@ jobs:
token: ${{ secrets.PAT }} token: ${{ secrets.PAT }}
- name: Download OpenAPI specification - 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 run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
- name: Commit changes - 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 uses: EndBug/add-and-commit@v4
with: with:
cwd: "api" cwd: "api"
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
gh api graphql -f query=' gh api graphql -f query='
query { query {
organization(login: "revoltchat"){ organization(login: "revoltchat"){
projectV2(number: 3) { projectV2(number: 5) {
id id
fields(first:20) { fields(first:20) {
nodes { nodes {
@@ -35,7 +35,7 @@ jobs:
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV 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 '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 - name: Add PR to project
env: env:
+3
View File
@@ -1,6 +1,9 @@
Rocket.toml Rocket.toml
Revolt.toml
target target
.data .data
.env .env
.vercel .vercel
.DS_Store
+5 -3
View File
@@ -1,4 +1,6 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy" "rust-analyzer.checkOnSave.command": "clippy",
} "nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
}
Generated
+1327 -539
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -1,6 +1,10 @@
[workspace] [workspace]
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"] resolver = "2"
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
[patch.crates-io] [patch.crates-io]
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" } # mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
redis = { git = "https://github.com/insertish/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" } redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
+1 -1
View File
@@ -19,7 +19,7 @@ RUN sh /tmp/build-image-layer.sh tools
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/ COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/ COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/quark/Cargo.toml ./crates/quark/ COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/ COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/ COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
+28
View File
@@ -0,0 +1,28 @@
# Build Stage
FROM rust:1.70.0-slim
USER 0:0
WORKDIR /home/rust/src
# Install build requirements
RUN apt-get update && \
apt-get install -y \
make \
pkg-config \
libssl-dev
COPY scripts/build-image-layer.sh /tmp/
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps
COPY crates ./crates
RUN sh /tmp/build-image-layer.sh apps
+98 -17
View File
@@ -2,30 +2,111 @@
This is a monorepo for the Revolt backend. This is a monorepo for the Revolt backend.
| Crate | Path | Description | | Crate | Path | Description |
| ---------------- | ---------------------------------------------- | ------------------------------------ | | ------------------ | -------------------------------------------------- | ----------------------------- |
| `delta` | [crates/delta](crates/delta) | REST API server | | `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | | `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |
| `quark` | [crates/quark](crates/quark) | Models and logic | | `core/models` | [crates/core/models](crates/core/models) | Core: API Models |
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer | | `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer | | `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |--> | `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |
| `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo. Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
## Resources ## Minimum Supported Rust Version
### Revolt Rust 1.70 or higher.
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here) ## Development Guide
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing)
## Contributing Before getting started, you'll want to install:
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing). - Rust toolchain (rustup recommended)
Please note that a pull request should only take care of one issue so that we can review it quickly. - 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 ## 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!**
+6
View File
@@ -2,6 +2,10 @@
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project. Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
## Writing Style
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
## `core/database` crate ## `core/database` crate
w.r.t. `model.rs` files w.r.t. `model.rs` files
@@ -22,6 +26,7 @@ w.r.t. `model.rs` files
); );
``` ```
- `auto_derived!` macro accepts multiple entries and should be used as such: - `auto_derived!` macro accepts multiple entries and should be used as such:
```rust ```rust
auto_derived!( auto_derived!(
/// Optional fields on server object /// Optional fields on server object
@@ -31,6 +36,7 @@ w.r.t. `model.rs` files
pub enum FieldsRole { .. } pub enum FieldsRole { .. }
); );
``` ```
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root: - If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
```rust ```rust
#[serde(skip_serializing_if = "crate::if_false", default)] #[serde(skip_serializing_if = "crate::if_false", default)]
+16 -3
View File
@@ -1,15 +1,28 @@
disallowed-methods = [ disallowed-methods = [
# Shouldn't need to access these directly # Shouldn't need to access these directly
"revolt_database::models::bots::model::Bot::remove_field", "revolt_database::models::bots::model::Bot::remove_field",
"revolt_database::models::messages::model::Message::attach_sendable_embed",
"revolt_database::models::users::model::User::set_relationship",
"revolt_database::models::users::model::User::apply_relationship",
# Prefer to use Object::create() # Prefer to use Object::create()
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::insert_account_strike", "revolt_database::models::bots::ops::AbstractBots::insert_bot",
"revolt_database::models::channel_invites::ops::AbstractChannelInvites::insert_invite",
"revolt_database::models::channel_unreads::ops::AbstractChannelUnreads::acknowledge_message",
"revolt_database::models::channel_webhooks::ops::AbstractWebhooks::insert_webhook",
"revolt_database::models::channels::ops::AbstractChannels::insert_channel",
"revolt_database::models::emojis::ops::AbstractEmojis::insert_emoji",
"revolt_database::models::files::ops::AbstractAttachments::insert_attachment",
"revolt_database::models::messages::ops::AbstractMessages::insert_message",
"revolt_database::models::ratelimit_events::ops::AbstractRatelimitEvents::insert_ratelimit_event",
"revolt_database::models::server_bans::ops::AbstractServerBans::insert_ban",
"revolt_database::models::server_members::ops::AbstractServerMembers::insert_member",
"revolt_database::models::servers::ops::AbstractServers::insert_server",
"revolt_database::models::users::ops::AbstractUsers::insert_user",
# Prefer to use Object::update(&self) # Prefer to use Object::update(&self)
"revolt_database::models::bots::ops::AbstractBots::update_bot", "revolt_database::models::bots::ops::AbstractBots::update_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
# Prefer to use Object::delete(&self) # Prefer to use Object::delete(&self)
"revolt_database::models::bots::ops::AbstractBots::delete_bot", "revolt_database::models::bots::ops::AbstractBots::delete_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
] ]
+20 -6
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.6.0" version = "0.7.1"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -9,15 +9,17 @@ edition = "2021"
[dependencies] [dependencies]
# util # util
log = "*" log = "*"
sentry = "0.31.5"
lru = "0.7.6"
ulid = "0.5.0"
once_cell = "1.9.0" once_cell = "1.9.0"
redis-kiss = "0.1.4"
# parsing # parsing
querystring = "1.1.0" querystring = "1.1.0"
# quark
revolt-quark = { path = "../quark" }
# serde # serde
bincode = "1.3.3"
serde_json = "1.0.79" serde_json = "1.0.79"
rmp-serde = "1.0.0" rmp-serde = "1.0.0"
serde = "1.0.136" serde = "1.0.136"
@@ -25,7 +27,19 @@ serde = "1.0.136"
# async # async
futures = "0.3.21" futures = "0.3.21"
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] } async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] } async-std = { version = "1.8.0", features = [
"tokio1",
"tokio02",
"attributes",
] }
# core # core
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] } revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.7.1", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }
+5 -5
View File
@@ -1,6 +1,6 @@
use async_tungstenite::tungstenite::{handshake, Message}; use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender; use futures::channel::oneshot::Sender;
use revolt_quark::{Error, Result}; use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Enumeration of supported protocol formats /// Enumeration of supported protocol formats
@@ -37,16 +37,16 @@ impl ProtocolConfiguration {
match self.format { match self.format {
ProtocolFormat::Json => { ProtocolFormat::Json => {
if let Message::Text(text) = msg { if let Message::Text(text) = msg {
serde_json::from_str(text).map_err(|_| Error::InternalError) serde_json::from_str(text).map_err(|_| create_error!(InternalError))
} else { } else {
Err(Error::InternalError) Err(create_error!(InternalError))
} }
} }
ProtocolFormat::Msgpack => { ProtocolFormat::Msgpack => {
if let Message::Binary(buf) = msg { if let Message::Binary(buf) = msg {
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError) rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
} else { } else {
Err(Error::InternalError) Err(create_error!(InternalError))
} }
} }
} }
+4 -2
View File
@@ -1,5 +1,5 @@
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use revolt_quark::{Database, DatabaseInfo}; use revolt_database::{Database, DatabaseInfo};
static DBCONN: OnceCell<Database> = OnceCell::new(); static DBCONN: OnceCell<Database> = OnceCell::new();
@@ -10,7 +10,9 @@ pub async fn connect() {
.await .await
.expect("Failed to connect to the database."); .expect("Failed to connect to the database.");
DBCONN.set(database).expect("Setting `Database`"); if DBCONN.set(database).is_err() {
panic!("couldn't set database")
}
} }
/// Get a reference to the current database. /// Get a reference to the current database.
@@ -1,21 +1,15 @@
use std::collections::HashSet; use std::collections::HashSet;
use crate::{ use revolt_database::{
get_relationship, events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
models::{ MemberCompositeKey, Presence, RelationshipStatus,
server_member::FieldsMember,
user::{PartialUser, Presence, RelationshipStatus},
Channel, Member, User,
},
perms, Database, Permission, Result,
}; };
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_presence::filter_online; use revolt_presence::filter_online;
use revolt_result::Result;
use super::{ use super::state::{Cache, State};
client::EventV1,
state::{Cache, State},
};
/// Cache Manager /// Cache Manager
impl Cache { impl Cache {
@@ -23,23 +17,24 @@ impl Cache {
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool { pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
match &channel { match &channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => { Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
let member = self.members.values().find(|x| &x.id.server == server); let member = self.members.get(server);
let server = self.servers.get(server); let server = self.servers.get(server);
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel); let mut query =
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
.channel(&channel);
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
if let Some(member) = member { if let Some(member) = member {
perms.member.set_ref(member); query = query.member(&member);
} }
if let Some(server) = server { if let Some(server) = server {
perms.server.set_ref(server); query = query.server(server);
} }
perms calculate_channel_permissions(&mut query)
.has_permission(db, Permission::ViewChannel)
.await .await
.unwrap_or_default() .has_channel_permission(ChannelPermission::ViewChannel)
} }
_ => true, _ => true,
} }
@@ -64,7 +59,7 @@ impl Cache {
/// Check whether we can subscribe to another user /// Check whether we can subscribe to another user
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool { pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
if let Some(user) = self.users.get(&self.user_id) { if let Some(user) = self.users.get(&self.user_id) {
match get_relationship(user, user_id) { match user.relationship_with(user_id) {
RelationshipStatus::Friend RelationshipStatus::Friend
| RelationshipStatus::Incoming | RelationshipStatus::Incoming
| RelationshipStatus::Outgoing | RelationshipStatus::Outgoing
@@ -96,7 +91,7 @@ impl Cache {
impl State { impl State {
/// Generate a Ready packet for the current user /// Generate a Ready packet for the current user
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> { pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
let mut user = self.clone_user(); let user = self.clone_user();
// Find all relationships to the user. // Find all relationships to the user.
let mut user_ids: HashSet<String> = user let mut user_ids: HashSet<String> = user
@@ -107,9 +102,15 @@ impl State {
// Fetch all memberships with their corresponding servers. // Fetch all memberships with their corresponding servers.
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?; let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect(); let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
let servers = db.fetch_servers(&server_ids).await?; let servers = db.fetch_servers(&server_ids).await?;
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
// Collect channel ids from servers. // Collect channel ids from servers.
let mut channel_ids = vec![]; let mut channel_ids = vec![];
@@ -136,7 +137,6 @@ impl State {
// Fetch presence data for known users. // Fetch presence data for known users.
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await; let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
user.online = Some(true);
// Fetch user data. // Fetch user data.
let users = db let users = db
@@ -149,45 +149,37 @@ impl State {
.await?; .await?;
// Fetch customisations. // Fetch customisations.
let emojis = Some( let emojis = db
db.fetch_emoji_by_parent_ids( .fetch_emoji_by_parent_ids(
&servers &servers
.iter() .iter()
.map(|x| x.id.to_string()) .map(|x| x.id.to_string())
.collect::<Vec<String>>(), .collect::<Vec<String>>(),
) )
.await?, .await?;
);
// Copy data into local state cache. // Copy data into local state cache.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect(); self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache self.cache
.users .users
.insert(self.cache.user_id.clone(), user.clone()); .insert(self.cache.user_id.clone(), user.clone());
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache.channels = channels self.cache.channels = channels
.iter() .iter()
.cloned() .cloned()
.map(|x| (x.id().to_string(), x)) .map(|x| (x.id().to_string(), x))
.collect(); .collect();
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
// Make all users appear from our perspective. // Make all users appear from our perspective.
let mut users: Vec<User> = users let mut users: Vec<v0::User> = users
.into_iter() .into_iter()
.map(|mut x| { .map(|other_user| {
x.online = Some(online_ids.contains(&x.id)); let is_online = online_ids.contains(&other_user.id);
x.with_relationship(&user) other_user.into_known(&user, is_online)
}) })
.collect(); .collect();
// Make sure we see our own user correctly. // Make sure we see our own user correctly.
user.relationship = Some(RelationshipStatus::User); users.push(user.into_self().await);
users.push(user.foreign());
// Set subscription state internally. // Set subscription state internally.
self.reset_state(); self.reset_state();
@@ -207,10 +199,10 @@ impl State {
Ok(EventV1::Ready { Ok(EventV1::Ready {
users, users,
servers, servers: servers.into_iter().map(Into::into).collect(),
channels, channels: channels.into_iter().map(Into::into).collect(),
members, members: members.into_iter().map(Into::into).collect(),
emojis, emojis: emojis.into_iter().map(Into::into).collect(),
}) })
} }
@@ -272,7 +264,7 @@ impl State {
.insert(channel.id().to_string(), channel.clone()); .insert(channel.id().to_string(), channel.clone());
self.insert_subscription(channel.id().to_string()); self.insert_subscription(channel.id().to_string());
bulk_events.push(EventV1::ChannelCreate(channel)); bulk_events.push(EventV1::ChannelCreate(channel.into()));
} }
} }
} }
@@ -297,7 +289,7 @@ impl State {
} { } {
let event = EventV1::UserUpdate { let event = EventV1::UserUpdate {
id: self.cache.user_id.clone(), id: self.cache.user_id.clone(),
data: PartialUser { data: v0::PartialUser {
online: Some(target), online: Some(target),
..Default::default() ..Default::default()
}, },
@@ -345,7 +337,7 @@ impl State {
EventV1::ChannelCreate(channel) => { EventV1::ChannelCreate(channel) => {
let id = channel.id().to_string(); let id = channel.id().to_string();
self.insert_subscription(id.clone()); self.insert_subscription(id.clone());
self.cache.channels.insert(id, channel.clone()); self.cache.channels.insert(id, channel.clone().into());
} }
EventV1::ChannelUpdate { EventV1::ChannelUpdate {
id, data, clear, .. id, data, clear, ..
@@ -353,15 +345,21 @@ impl State {
let could_view: bool = if let Some(channel) = self.cache.channels.get(id) { let could_view: bool = if let Some(channel) = self.cache.channels.get(id) {
self.cache.can_view_channel(db, channel).await self.cache.can_view_channel(db, channel).await
} else { } else {
true false
}; };
if let Some(channel) = self.cache.channels.get_mut(id) { if let Some(channel) = self.cache.channels.get_mut(id) {
for field in clear { for field in clear {
channel.remove(field); channel.remove_field(&field.clone().into());
} }
channel.apply_options(data.clone()); channel.apply_options(data.clone().into());
}
if !self.cache.channels.contains_key(id) {
if let Ok(channel) = db.fetch_channel(id).await {
self.cache.channels.insert(id.clone(), channel);
}
} }
if let Some(channel) = self.cache.channels.get(id) { if let Some(channel) = self.cache.channels.get(id) {
@@ -369,7 +367,7 @@ impl State {
if could_view != can_view { if could_view != can_view {
if can_view { if can_view {
queue_add = Some(id.clone()); queue_add = Some(id.clone());
*event = EventV1::ChannelCreate(channel.clone()); *event = EventV1::ChannelCreate(channel.clone().into());
} else { } else {
queue_remove = Some(id.clone()); queue_remove = Some(id.clone());
*event = EventV1::ChannelDelete { id: id.clone() }; *event = EventV1::ChannelDelete { id: id.clone() };
@@ -396,14 +394,23 @@ impl State {
id, id,
server, server,
channels, channels,
emojis: _,
} => { } => {
self.insert_subscription(id.clone()); self.insert_subscription(id.clone());
self.cache.servers.insert(id.to_string(), server.clone()); self.cache.servers.insert(id.clone(), server.clone().into());
let member = Member {
id: MemberCompositeKey {
server: server.id.clone(),
user: self.cache.user_id.clone(),
},
..Default::default()
};
self.cache.members.insert(id.clone(), member);
for channel in channels { for channel in channels {
self.cache self.cache
.channels .channels
.insert(channel.id().to_string(), channel.clone()); .insert(channel.id().to_string(), channel.clone().into());
} }
queue_server = Some(id.clone()); queue_server = Some(id.clone());
@@ -413,10 +420,10 @@ impl State {
} => { } => {
if let Some(server) = self.cache.servers.get_mut(id) { if let Some(server) = self.cache.servers.get_mut(id) {
for field in clear { for field in clear {
server.remove(field); server.remove_field(&field.clone().into());
} }
server.apply_options(data.clone()); server.apply_options(data.clone().into());
} }
if data.default_permissions.is_some() { if data.default_permissions.is_some() {
@@ -436,6 +443,7 @@ impl State {
self.cache.channels.remove(channel); self.cache.channels.remove(channel);
} }
} }
self.cache.members.remove(id);
} }
} }
EventV1::ServerDelete { id } => { EventV1::ServerDelete { id } => {
@@ -447,18 +455,19 @@ impl State {
self.cache.channels.remove(channel); self.cache.channels.remove(channel);
} }
} }
self.cache.members.remove(id);
} }
EventV1::ServerMemberUpdate { id, data, clear } => { EventV1::ServerMemberUpdate { id, data, clear } => {
if id.user == self.cache.user_id { if id.user == self.cache.user_id {
if let Some(member) = self.cache.members.get_mut(&id.server) { if let Some(member) = self.cache.members.get_mut(&id.server) {
for field in &clear.clone() { for field in &clear.clone() {
member.remove(field); member.remove_field(&field.clone().into());
} }
member.apply_options(data.clone()); member.apply_options(data.clone().into());
} }
if data.roles.is_some() || clear.contains(&FieldsMember::Roles) { if data.roles.is_some() || clear.contains(&v0::FieldsMember::Roles) {
queue_server = Some(id.server.clone()); queue_server = Some(id.server.clone());
} }
} }
@@ -473,10 +482,10 @@ impl State {
if let Some(server) = self.cache.servers.get_mut(id) { if let Some(server) = self.cache.servers.get_mut(id) {
if let Some(role) = server.roles.get_mut(role_id) { if let Some(role) = server.roles.get_mut(role_id) {
for field in &clear.clone() { for field in &clear.clone() {
role.remove(field); role.remove_field(&field.clone().into());
} }
role.apply_options(data.clone()); role.apply_options(data.clone().into());
} }
} }
@@ -512,7 +521,7 @@ impl State {
*event_id = None; *event_id = None;
} }
EventV1::UserRelationship { id, user, .. } => { EventV1::UserRelationship { id, user, .. } => {
self.cache.users.insert(id.clone(), user.clone()); self.cache.users.insert(id.clone(), user.clone().into());
if self.cache.can_subscribe_to_user(id) { if self.cache.can_subscribe_to_user(id) {
self.insert_subscription(id.clone()); self.insert_subscription(id.clone());
@@ -541,40 +550,3 @@ impl State {
true true
} }
} }
impl EventV1 {
/// Publish helper wrapper
pub async fn p(self, channel: String) {
#[cfg(not(debug_assertions))]
redis_kiss::p(channel, self).await;
#[cfg(debug_assertions)]
info!("Publishing event to {channel}: {self:?}");
#[cfg(debug_assertions)]
redis_kiss::publish(channel, self).await.unwrap();
}
/// Publish user event
pub async fn p_user(self, id: String, db: &Database) {
self.clone().p(id.clone()).await;
// ! FIXME: this should be captured by member list in the future
// ! and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members {
self.clone().p(member.id.server).await;
}
}
}
/// Publish private event
pub async fn private(self, id: String) {
self.p(format!("{id}!")).await;
}
/// Publish internal global event
pub async fn global(self) {
self.p("global".to_string()).await;
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod r#impl;
pub mod state;
@@ -1,8 +1,7 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use lru::LruCache; use lru::LruCache;
use revolt_database::{Channel, Member, Server, User};
use crate::models::{Channel, Member, Server, User};
/// Enumeration representing some change in subscriptions /// Enumeration representing some change in subscriptions
pub enum SubscriptionStateChange { pub enum SubscriptionStateChange {
+7 -2
View File
@@ -7,6 +7,7 @@ use revolt_presence::clear_region;
extern crate log; extern crate log;
pub mod config; pub mod config;
pub mod events;
mod database; mod database;
mod websocket; mod websocket;
@@ -14,7 +15,7 @@ mod websocket;
#[async_std::main] #[async_std::main]
async fn main() { async fn main() {
// Configure requirements for Bonfire. // Configure requirements for Bonfire.
revolt_quark::configure!(); revolt_config::configure!();
database::connect().await; database::connect().await;
// Clean up the current region information. // Clean up the current region information.
@@ -29,6 +30,10 @@ async fn main() {
// Start accepting new connections and spawn a client for each connection. // Start accepting new connections and spawn a client for each connection.
while let Ok((stream, addr)) = listener.accept().await { while let Ok((stream, addr)) = listener.accept().await {
websocket::spawn_client(database::get_db(), stream, addr); async_std::task::spawn(async move {
info!("User connected from {addr:?}");
websocket::client(database::get_db(), stream, addr).await;
info!("User disconnected from {addr:?}");
});
} }
} }
+257 -231
View File
@@ -1,252 +1,278 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt}; use async_tungstenite::WebSocketStream;
use revolt_presence::{create_session, delete_session}; use fred::{
use revolt_quark::{ interfaces::{ClientLike, EventInterface, PubsubInterface},
events::{ types::RedisConfig,
client::EventV1,
server::ClientMessage,
state::{State, SubscriptionStateChange},
},
models::{user::UserHint, User},
redis_kiss, Database,
}; };
use futures::{
channel::oneshot,
pin_mut, select,
stream::{SplitSink, SplitStream},
FutureExt, SinkExt, StreamExt, TryStreamExt,
};
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_database::{
events::{client::EventV1, server::ClientMessage},
Database, User, UserHint,
};
use revolt_presence::{create_session, delete_session};
use async_std::{net::TcpStream, sync::Mutex, task}; use async_std::{net::TcpStream, sync::Mutex};
use crate::config::WebsocketHandshakeCallback; use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
use crate::events::state::{State, SubscriptionStateChange};
/// Spawn a new WebSocket client worker given access to the database, type WsReader = SplitStream<WebSocketStream<TcpStream>>;
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
/// Start a new WebSocket client worker given access to the database,
/// the relevant TCP stream and the remote address of the client. /// the relevant TCP stream and the remote address of the client.
pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) { pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
// Spawn a new Async task to work on. // Upgrade the TCP connection to a WebSocket connection.
task::spawn(async move { // In this process, we also parse any additional parameters given.
info!("User connected from {addr:?}"); // e.g. wss://example.com?format=json&version=1
let (sender, receiver) = oneshot::channel();
let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
stream,
WebsocketHandshakeCallback::from(sender),
None,
)
.await
else {
return;
};
// Verify we've received a valid config, otherwise we should just drop the connection.
let Ok(mut config) = receiver.await else {
return;
};
info!(
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
config.get_protocol_version(),
config.get_protocol_format()
);
// Upgrade the TCP connection to a WebSocket connection. // Split the socket for simultaneously read and write.
// In this process, we also parse any additional parameters given. let (mut write, mut read) = ws.split();
// e.g. wss://example.com?format=json&version=1
let (sender, receiver) = oneshot::channel(); // If the user has not provided authentication, request information.
if let Ok(ws) = async_tungstenite::accept_hdr_async_with_config( if config.get_session_token().is_none() {
stream, while let Ok(message) = read.try_next().await {
WebsocketHandshakeCallback::from(sender), if let Ok(ClientMessage::Authenticate { token }) =
None, config.decode(message.as_ref().unwrap())
) {
config.set_session_token(token);
break;
}
}
}
// Try to authenticate the user.
let Some(token) = config.get_session_token().as_ref() else {
return;
};
let user = match User::from_token(db, token, UserHint::Any).await {
Ok(user) => user,
Err(err) => {
write.send(config.encode(&err)).await.ok();
return;
}
};
info!("User {addr:?} authenticated as @{}", user.username);
// Create local state.
let mut state = State::from(user);
let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated.
if write
.send(config.encode(&EventV1::Authenticated))
.await .await
{ .is_err()
// Verify we've received a valid config, otherwise we should just drop the connection. {
if let Ok(mut config) = receiver.await { return;
info!( }
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
config.get_protocol_version(),
config.get_protocol_format()
);
// Split the socket for simultaneously read and write. // Download required data to local cache and send Ready payload.
let (write, mut read) = ws.split(); let Ok(ready_payload) = state.generate_ready_payload(db).await else {
let write = Mutex::new(write); return;
};
if write.send(config.encode(&ready_payload)).await.is_err() {
return;
}
// If the user has not provided authentication, request information. // Create presence session.
if config.get_session_token().is_none() { let (first_session, session_id) = create_session(&user_id, 0).await;
'outer: while let Ok(message) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) = // If this was the first session, notify other users that we just went online.
config.decode(message.as_ref().unwrap()) if first_session {
{ state.broadcast_presence_change(true).await;
config.set_session_token(token); }
break 'outer;
} {
} let write = Mutex::new(write);
// Create a PubSub connection to poll on.
let listener = listener(db, &mut state, addr, &config, &write).fuse();
// Read from WebSocket stream.
let worker = worker(addr, user_id.clone(), &config, read, &write).fuse();
// Pin both tasks.
pin_mut!(listener, worker);
// Wait for either disconnect or for listener to die.
select!(
() = listener => {},
() = worker => {}
);
}
// Clean up presence session.
let last_session = delete_session(&user_id, session_id).await;
// If this was the last session, notify other users that we just went offline.
if last_session {
state.broadcast_presence_change(false).await;
}
}
async fn listener(
db: &'static Database,
state: &mut State,
addr: SocketAddr,
config: &ProtocolConfiguration,
write: &Mutex<WsWriter>,
) {
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
let Ok(subscriber) = fred::types::Builder::from_config(redis_config).build_subscriber_client()
else {
return;
};
if subscriber.init().await.is_err() {
return;
};
let mut message_rx = subscriber.message_rx();
loop {
// Check for state changes for subscriptions.
match state.apply_state() {
SubscriptionStateChange::Reset => {
subscriber.unsubscribe_all().await.unwrap();
for id in state.iter_subscriptions() {
subscriber.subscribe(id).await.unwrap();
} }
// Try to authenticate the user. #[cfg(debug_assertions)]
if let Some(token) = config.get_session_token().as_ref() { info!("{addr:?} has reset their subscriptions");
match User::from_token(db, token, UserHint::Any).await { }
Ok(user) => { SubscriptionStateChange::Change { add, remove } => {
info!("User {addr:?} authenticated as @{}", user.username); for id in remove {
#[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}");
// Create local state. subscriber.unsubscribe(id).await.unwrap();
let mut state = State::from(user); }
let user_id = state.cache.user_id.clone();
// Create presence session. for id in add {
let (first_session, session_id) = create_session(&user_id, 0).await; #[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
// Notify socket we have authenticated. subscriber.subscribe(id).await.unwrap();
write
.lock()
.await
.send(config.encode(&EventV1::Authenticated))
.await
.ok();
// Download required data to local cache and send Ready payload.
if let Ok(ready_payload) = state.generate_ready_payload(db).await {
write
.lock()
.await
.send(config.encode(&ready_payload))
.await
.ok();
// If this was the first session, notify other users that we just went online.
if first_session {
state.broadcast_presence_change(true).await;
}
// Create a PubSub connection to poll on.
let listener = async {
if let Ok(mut conn) = redis_kiss::open_pubsub_connection().await
{
loop {
// Check for state changes for subscriptions.
match state.apply_state() {
SubscriptionStateChange::Reset => {
for id in state.iter_subscriptions() {
conn.subscribe(id).await.unwrap();
}
#[cfg(debug_assertions)]
info!("{addr:?} has reset their subscriptions");
}
SubscriptionStateChange::Change { add, remove } => {
for id in remove {
#[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}");
conn.unsubscribe(id).await.unwrap();
}
for id in add {
#[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
conn.subscribe(id).await.unwrap();
}
}
SubscriptionStateChange::None => {}
}
// * Debug logging of current subscriptions.
/*#[cfg(debug_assertions)]
info!(
"User {addr:?} is subscribed to {:?}",
state
.iter_subscriptions()
.collect::<Vec<&String>>()
);*/
// Handle incoming events.
match conn.on_message().next().await.map(|item| {
(
item.get_channel_name().to_string(),
redis_kiss::decode_payload::<EventV1>(&item),
)
}) {
Some((channel, item)) => {
if let Ok(mut event) = item {
if state
.handle_incoming_event_v1(
db, &mut event,
)
.await
&& write.lock().await
.send(config.encode(&event))
.await
.is_err()
{
break;
}
} else {
warn!("Failed to deserialise an event for {channel}!");
}
}
// No more data, assume we disconnected or otherwise
// something bad occurred, so disconnect user.
None => break,
}
}
}
}
.fuse();
// Read from WebSocket stream.
let worker =
async {
while let Ok(Some(msg)) = read.try_next().await {
if let Ok(payload) = config.decode(&msg) {
match payload {
ClientMessage::BeginTyping { channel } => {
EventV1::ChannelStartTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::EndTyping { channel } => {
EventV1::ChannelStopTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::Ping { data, responded } => {
if responded.is_none() {
write
.lock()
.await
.send(config.encode(
&EventV1::Pong { data },
))
.await
.ok();
}
}
_ => {}
}
}
}
}
.fuse();
// Pin both tasks.
pin_mut!(listener, worker);
// Wait for either disconnect or for listener to die.
select!(
() = listener => {},
() = worker => {}
);
// * Combine the streams back once we are ready to disconnect.
/* ws = read.reunite(write).unwrap(); */
}
// Clean up presence session.
let last_session = delete_session(&user_id, session_id).await;
// If this was the last session, notify other users that we just went offline.
if last_session {
state.broadcast_presence_change(false).await;
}
}
Err(err) => {
write.lock().await.send(config.encode(&err)).await.ok();
}
}
} }
} }
SubscriptionStateChange::None => {}
// * Disconnect the WebSocket if it isn't already.
/*ws.close(Some(CloseFrame {
code: CloseCode::Normal,
reason: std::borrow::Cow::from(""),
}))
.await
.unwrap();*/
} }
info!("User disconnected from {addr:?}"); // Handle incoming events.
}); let Ok(message) = message_rx.recv().await.map_err(|e| {
warn!("Error while consuming pub/sub messages: {e:?}");
sentry::capture_error(&e);
}) else {
return;
};
let event = match *REDIS_PAYLOAD_TYPE {
PayloadType::Json => message
.value
.as_str()
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
PayloadType::Msgpack => message
.value
.as_bytes()
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
PayloadType::Bincode => message
.value
.as_bytes()
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
};
let Some(mut event) = event else {
warn!("Failed to deserialise an event for {}!", message.channel);
return;
};
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send {
continue;
}
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>,
) {
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;
};
match payload {
ClientMessage::BeginTyping { channel } => {
EventV1::ChannelStartTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::EndTyping { channel } => {
EventV1::ChannelStopTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::Ping { data, responded } => {
if responded.is_none() {
write
.lock()
.await
.send(config.encode(&EventV1::Pong { data }))
.await
.ok();
}
}
_ => {}
}
}
} }
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "revolt-config"
version = "0.7.1"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Configuration"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
test = ["async-std"]
default = ["test"]
[dependencies]
# Utility
dotenv = "0.15.0"
config = "0.13.3"
cached = "0.44.0"
once_cell = "1.18.0"
# Serde
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"
+3
View File
@@ -0,0 +1,3 @@
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"
+70
View File
@@ -0,0 +1,70 @@
sentry_dsn = ""
[database]
mongodb = "mongodb://database"
redis = "redis://redis/"
[hosts]
app = "http://local.revolt.chat"
api = "http://local.revolt.chat/api"
events = "ws://local.revolt.chat/ws"
autumn = "http://local.revolt.chat/autumn"
january = "http://local.revolt.chat/january"
voso_legacy = ""
voso_legacy_ws = ""
[api]
staging = false
[api.registration]
invite_only = false
[api.smtp]
host = ""
username = ""
password = ""
from_address = ""
[api.vapid]
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
[api.fcm]
api_key = ""
[api.security]
authifier_shield_key = ""
voso_legacy_token = ""
trust_cloudflare = false
[api.security.captcha]
hcaptcha_key = ""
hcaptcha_sitekey = ""
[api.workers]
max_concurrent_connections = 50
[features]
webhooks_enabled = false
[features.limits]
[features.limits.default]
group_size = 100
bots = 5
message_length = 2000
message_embeds = 5
message_replies = 5
message_attachments = 5
message_reactions = 20
servers = 100
server_emoji = 100
server_roles = 200
server_channels = 200
attachment_size = 20000000
avatar_size = 4000000
background_size = 6000000
icon_size = 2500000
banner_size = 6000000
emoji_size = 500000
+243
View File
@@ -0,0 +1,243 @@
use std::collections::HashMap;
use cached::proc_macro::cached;
use config::{Config, File, FileFormat};
use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
#[cfg(not(debug_assertions))]
use std::env;
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
RwLock::new({
let mut builder = Config::builder().add_source(File::from_str(
include_str!("../Revolt.toml"),
FileFormat::Toml,
));
if std::env::var("TEST_DB").is_ok() {
builder = builder.add_source(File::from_str(
include_str!("../Revolt.test.toml"),
FileFormat::Toml,
));
} else if std::path::Path::new("Revolt.toml").exists() {
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
}
builder.build().unwrap()
})
});
// https://gifbox.me/view/gT5mqxYKCZv-twilight-meow
#[derive(Deserialize, Debug, Clone)]
pub struct Database {
pub mongodb: String,
pub redis: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Hosts {
pub app: String,
pub api: String,
pub events: String,
pub autumn: String,
pub january: String,
pub voso_legacy: String,
pub voso_legacy_ws: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiRegistration {
pub invite_only: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSmtp {
pub host: String,
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)]
pub struct ApiVapid {
pub private_key: String,
pub public_key: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiFcm {
pub api_key: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurityCaptcha {
pub hcaptcha_key: String,
pub hcaptcha_sitekey: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurity {
pub authifier_shield_key: String,
pub voso_legacy_token: String,
pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiWorkers {
pub max_concurrent_connections: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Api {
pub staging: bool,
pub registration: ApiRegistration,
pub smtp: ApiSmtp,
pub vapid: ApiVapid,
pub fcm: ApiFcm,
pub security: ApiSecurity,
pub workers: ApiWorkers,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimits {
pub group_size: usize,
pub bots: usize,
pub message_length: usize,
pub message_replies: usize,
pub message_attachments: usize,
pub message_embeds: usize,
pub message_reactions: usize,
pub servers: usize,
pub server_emoji: usize,
pub server_roles: usize,
pub server_channels: usize,
pub attachment_size: usize,
pub avatar_size: usize,
pub background_size: usize,
pub icon_size: usize,
pub banner_size: usize,
pub emoji_size: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimitsCollection {
pub default: FeaturesLimits,
#[serde(flatten)]
pub roles: HashMap<String, FeaturesLimits>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Features {
pub limits: FeaturesLimitsCollection,
pub webhooks_enabled: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Settings {
pub database: Database,
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() {
println!(
":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m",
config().await
);
}
pub async fn read() -> Config {
CONFIG_BUILDER.read().await.clone()
}
#[cached(time = 30)]
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 {
use crate::init;
#[async_std::test]
async fn it_works() {
init().await;
}
}
+38 -13
View File
@@ -1,38 +1,54 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.6.0" version = "0.7.1"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ] authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Database Implementation" description = "Revolt Backend: Database Implementation"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
# Databases # Databases
mongodb = [ "dep:mongodb", "bson" ] mongodb = ["dep:mongodb", "bson"]
# ... Other # ... Other
async-std-runtime = [ "async-std" ] tasks = ["isahc", "linkify", "url-escape"]
rocket-impl = [ "rocket", "schemars" ] async-std-runtime = ["async-std"]
redis-is-patched = [ "revolt-presence/redis-is-patched" ] rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
# Default Features # Default Features
default = [ "mongodb", "async-std-runtime" ] default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.6.0", path = "../result" } revolt-config = { version = "0.7.1", path = "../config" }
revolt-models = { version = "0.6.0", path = "../models" } revolt-result = { version = "0.7.1", path = "../result" }
revolt-presence = { version = "0.6.0", path = "../presence" } revolt-models = { version = "0.7.1", path = "../models", features = [
revolt-permissions = { version = "0.6.0", path = "../permissions", features = [ "serde", "bson" ] } "validator",
] }
revolt-presence = { version = "0.7.1", path = "../presence" }
revolt-permissions = { version = "0.7.1", path = "../permissions", features = [
"serde",
"bson",
] }
# Utility # Utility
log = "0.4" log = "0.4"
lru = "0.11.0"
rand = "0.8.5" rand = "0.8.5"
ulid = "1.0.0" ulid = "1.0.0"
nanoid = "0.4.0" nanoid = "0.4.0"
base64 = "0.21.3"
once_cell = "1.17" once_cell = "1.17"
indexmap = "1.9.1"
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 # Serialisation
serde_json = "1" serde_json = "1"
@@ -53,6 +69,7 @@ regex = "1"
# Async Language Features # Async Language Features
futures = "0.3.19" futures = "0.3.19"
async-lock = "2.8.0"
async-trait = "0.1.51" async-trait = "0.1.51"
async-recursion = "1.0.4" async-recursion = "1.0.4"
@@ -61,7 +78,15 @@ async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Rocket Impl # Rocket Impl
schemars = { version = "0.8.8", optional = true } schemars = { version = "0.8.8", optional = true }
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"], optional = true } rocket = { version = "0.5.0-rc.2", default-features = false, features = [
"json",
], optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.9.1", optional = true }
# Notifications
fcm = "0.9.2"
web-push = "0.10.0"
# Authifier # Authifier
authifier = { version = "1.0" } authifier = { version = "1.0.8" }
@@ -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__"]
}
]
@@ -0,0 +1,89 @@
[
{
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Moderator",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "User",
"discriminator": "0001"
},
{
"_object_type": "Channel",
"_id": "__ID:3__",
"channel_type": "TextChannel",
"name": "General",
"server": "__ID:4__",
"default_permissions": {
"a": 0,
"d": 1048576
},
"role_permissions": {
"__ID:5__": {
"a": 1048576,
"d": 0
}
}
},
{
"_object_type": "Server",
"_id": "__ID:4__",
"owner": "__ID:0__",
"name": "Server",
"channels": ["__ID:3__"],
"roles": {
"__ID:5__": {
"name": "Moderator",
"permissions": {
"a": 545270208,
"d": 0
},
"rank": 3
},
"__ID:6__": {
"name": "Owner",
"permissions": {
"a": 0,
"d": 0
},
"rank": 0
}
},
"default_permissions": 4000322560
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:0__",
"server": "__ID:4__"
},
"roles": ["__ID:6__"],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:1__",
"server": "__ID:4__"
},
"roles": ["__ID:5__"],
"joined_at": 1698318340195
},
{
"_object_type": "ServerMember",
"_id": {
"user": "__ID:2__",
"server": "__ID:4__"
},
"joined_at": 1698318340195
}
]
+32 -10
View File
@@ -1,6 +1,9 @@
mod mongodb; mod mongodb;
mod reference; mod reference;
use rand::Rng;
use revolt_config::config;
pub use self::mongodb::*; pub use self::mongodb::*;
pub use self::reference::*; pub use self::reference::*;
@@ -31,25 +34,44 @@ impl DatabaseInfo {
/// Create a database client from the given database information /// Create a database client from the given database information
#[async_recursion] #[async_recursion]
pub async fn connect(self) -> Result<Database, String> { pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
Ok(match self { Ok(match self {
DatabaseInfo::Auto => { DatabaseInfo::Auto => {
if let Ok(uri) = std::env::var("MONGODB") { if std::env::var("TEST_DB").is_ok() {
return DatabaseInfo::MongoDb { DatabaseInfo::Test(format!(
uri, "revolt_test_{}",
rand::thread_rng().gen_range(1_000_000..10_000_000)
))
.connect()
.await?
} else if !config.database.mongodb.is_empty() {
DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name: "revolt".to_string(), database_name: "revolt".to_string(),
} }
.connect() .connect()
.await; .await?
} else {
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Test(database_name) => { DatabaseInfo::Test(database_name) => {
if let Ok(uri) = std::env::var("MONGODB") { match std::env::var("TEST_DB")
return DatabaseInfo::MongoDb { uri, database_name }.connect().await; .expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
.as_str()
{
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
"MONGODB" => {
DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name,
}
.connect()
.await?
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
} }
DatabaseInfo::Reference.connect().await?
} }
DatabaseInfo::Reference => Database::Reference(Default::default()), DatabaseInfo::Reference => Database::Reference(Default::default()),
DatabaseInfo::MongoDb { uri, database_name } => { DatabaseInfo::MongoDb { uri, database_name } => {
+1 -1
View File
@@ -238,6 +238,6 @@ pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_j
let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap(); let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap();
v.into_iter() v.into_iter()
.filter(|(_k, v)| !v.is_null()) .filter(|(_k, v)| !v.is_null())
.map(|(k, v)| (prefix.to_owned() + &k, v)) .map(|(k, v)| (format!("{}{}", prefix.to_owned(), k), v))
.collect() .collect()
} }
+13 -12
View File
@@ -3,29 +3,30 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex; use futures::lock::Mutex;
use crate::{ use crate::{
AccountStrike, Bot, Channel, File, Member, MemberCompositeKey, Server, User, UserSettings, Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
Webhook, MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
UserSettings, Webhook,
}; };
database_derived!( database_derived!(
/// Reference implementation /// Reference implementation
#[derive(Default)] #[derive(Default)]
pub struct ReferenceDb { pub struct ReferenceDb {
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>, pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>, pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>, pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>, pub users: Arc<Mutex<HashMap<String, User>>>,
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>, pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>, pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub files: Arc<Mutex<HashMap<String, File>>>, pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
pub server_bans: Arc<Mutex<HashMap<String, ()>>>, pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
pub emoji: Arc<Mutex<HashMap<String, ()>>>,
pub messages: Arc<Mutex<HashMap<String, ()>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
} }
); );
+10 -15
View File
@@ -2,7 +2,10 @@ use authifier::AuthifierEvent;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use revolt_models::v0::{ use revolt_models::v0::{
Channel, FieldsChannel, FieldsWebhook, PartialChannel, PartialWebhook, Webhook, AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
UserSettings, Webhook,
}; };
use revolt_result::Error; use revolt_result::Error;
@@ -45,18 +48,17 @@ pub enum EventV1 {
/// Successfully authenticated /// Successfully authenticated
Authenticated, Authenticated,
/* /// Basic data to cache /// Basic data to cache
Ready { Ready {
users: Vec<User>, users: Vec<User>,
servers: Vec<Server>, servers: Vec<Server>,
channels: Vec<Channel>, channels: Vec<Channel>,
members: Vec<Member>, members: Vec<Member>,
emojis: Option<Vec<Emoji>>, emojis: Vec<Emoji>,
}, },
/// Ping response /// Ping response
Pong { data: Ping }, Pong { data: Ping },
/// New message /// New message
Message(Message), Message(Message),
@@ -108,6 +110,7 @@ pub enum EventV1 {
id: String, id: String,
server: Server, server: Server,
channels: Vec<Channel>, channels: Vec<Channel>,
emojis: Vec<Emoji>,
}, },
/// Update existing server /// Update existing server
@@ -153,13 +156,7 @@ pub enum EventV1 {
}, },
/// Relationship with another user changed /// Relationship with another user changed
UserRelationship { UserRelationship { id: String, user: User },
id: String,
user: User,
// ! this field can be deprecated
status: RelationshipStatus,
},
/// Settings updated remotely /// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings }, UserSettingsUpdate { id: String, update: UserSettings },
@@ -173,7 +170,6 @@ pub enum EventV1 {
/// ///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear. /// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 }, UserPlatformWipe { user_id: String, flags: i32 },
/// New emoji /// New emoji
EmojiCreate(Emoji), EmojiCreate(Emoji),
@@ -181,7 +177,7 @@ pub enum EventV1 {
EmojiDelete { id: String }, EmojiDelete { id: String },
/// New report /// New report
ReportCreate(Report), */ ReportCreate(Report),
/// New channel /// New channel
ChannelCreate(Channel), ChannelCreate(Channel),
@@ -248,8 +244,7 @@ impl EventV1 {
pub async fn p_user(self, id: String, db: &Database) { pub async fn p_user(self, id: String, db: &Database) {
self.clone().p(id.clone()).await; self.clone().p(id.clone()).await;
// ! FIXME: this should be captured by member list in the future // TODO: this should be captured by member list in the future and not immediately fanned out to users
// ! and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await { if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members { for member in members {
self.clone().p(member.id.server).await; self.clone().p(member.id.server).await;
+1
View File
@@ -1 +1,2 @@
pub mod client; pub mod client;
pub mod server;
+2 -1
View File
@@ -43,7 +43,7 @@ macro_rules! auto_derived {
macro_rules! auto_derived_partial { macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => { ( $item:item, $name:expr ) => {
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)] #[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
#[optional_name = $name] #[optional_name = $name]
#[opt_skip_serializing_none] #[opt_skip_serializing_none]
@@ -81,6 +81,7 @@ pub mod util;
pub use models::*; pub use models::*;
pub mod events; pub mod events;
pub mod tasks;
/// Utility function to check if a boolean value is false /// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool { pub fn if_false(t: &bool) -> bool {
@@ -18,7 +18,7 @@ mod tests {
db.migrate_database().await.unwrap(); db.migrate_database().await.unwrap();
// Migrate the existing database // Migrate the existing database
db.migrate_database().await.unwrap() db.migrate_database().await.unwrap();
}); });
} }
} }
@@ -76,6 +76,10 @@ pub async fn create_database(db: &MongoDb) {
.await .await
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
db.create_collection("ratelimit_events", None)
.await
.expect("Failed to create ratelimit_events collection.");
db.create_collection( db.create_collection(
"pubsub", "pubsub",
CreateCollectionOptions::builder() CreateCollectionOptions::builder()
@@ -209,5 +213,24 @@ pub async fn create_database(db: &MongoDb) {
.await .await
.expect("Failed to save migration info."); .expect("Failed to save migration info.");
db.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
.await
.expect("Failed to create ratelimit_events index.");
info!("Created database."); info!("Created database.");
} }
@@ -9,6 +9,7 @@ use crate::{
}; };
use futures::StreamExt; use futures::StreamExt;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@@ -18,7 +19,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 25; pub const LATEST_REVISION: i32 = 26;
pub async fn migrate_database(db: &MongoDb) { pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations"); let migrations = db.col::<Document>("migrations");
@@ -696,7 +697,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
if revision <= 19 { if revision <= 19 {
info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable."); info!(
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
);
db.db() db.db()
.create_collection("safety_reports", None) .create_collection("safety_reports", None)
@@ -707,19 +710,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.create_collection("safety_snapshots", None) .create_collection("safety_snapshots", None)
.await .await
.unwrap(); .unwrap();
db.col::<Document>("safety_reports")
.update_many(
doc! {},
doc! {
"$set": {
"status": "Created"
}
},
None,
)
.await
.unwrap();
} }
if revision <= 20 { if revision <= 20 {
@@ -945,10 +935,56 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
) )
.await .await
.expect("Failed to create username index."); .expect("Failed to create username index.");
};
if revision <= 25 {
info!("Running migration [revision 25 / 11-06-2023]: Add permissions to webhooks.");
db.col::<Document>("webhooks")
.update_many(
doc! {},
doc! {
"$set": {
"permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64
}
},
None,
)
.await
.expect("Failed to update webhooks.");
}
if revision <= 25 {
info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index.");
db.db()
.create_collection("ratelimit_events", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
.await
.expect("Failed to create ratelimit_events index.");
} }
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`. // Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION LATEST_REVISION.max(revision)
} }
+87 -26
View File
@@ -1,6 +1,8 @@
use revolt_config::config;
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid;
use crate::Database; use crate::{BotInformation, Database, PartialUser, User};
auto_derived_partial!( auto_derived_partial!(
/// Bot /// Bot
@@ -49,8 +51,70 @@ auto_derived!(
} }
); );
#[allow(clippy::derivable_impls)]
impl Default for Bot {
fn default() -> Self {
Self {
id: Default::default(),
owner: Default::default(),
token: Default::default(),
public: Default::default(),
analytics: Default::default(),
discoverable: Default::default(),
interactions_url: Default::default(),
terms_of_service_url: Default::default(),
privacy_policy_url: Default::default(),
flags: Default::default(),
}
}
}
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
impl Bot { impl Bot {
/// Create a new bot
pub async fn create<D>(db: &Database, username: String, owner: &User, data: D) -> Result<Bot>
where
D: Into<Option<PartialBot>>,
{
if owner.bot.is_some() {
return Err(create_error!(IsBot));
}
let config = config().await;
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
return Err(create_error!(ReachedMaximumBots));
}
let id = Ulid::new().to_string();
User::create(
db,
username,
Some(id.to_string()),
Some(PartialUser {
bot: Some(BotInformation {
owner: owner.id.to_string(),
}),
..Default::default()
}),
)
.await?;
let mut bot = Bot {
id,
owner: owner.id.to_string(),
token: nanoid::nanoid!(64),
..Default::default()
};
if let Some(data) = data.into() {
bot.apply_options(data);
}
db.insert_bot(&bot).await?;
Ok(bot)
}
/// Remove a field from this object /// Remove a field from this object
pub fn remove_field(&mut self, field: &FieldsBot) { pub fn remove_field(&mut self, field: &FieldsBot) {
match field { match field {
@@ -96,27 +160,24 @@ mod tests {
#[async_std::test] #[async_std::test]
async fn crud() { async fn crud() {
database_test!(|db| async move { database_test!(|db| async move {
let bot_id = "bot"; let owner = User::create(&db, "Owner".to_string(), None, None)
let user_id = "user"; .await
let token = "my_token"; .unwrap();
let user = User { let bot = Bot::create(
id: bot_id.to_string(), &db,
username: "Bot Name".to_string(), "Bot Name".to_string(),
..Default::default() &owner,
}; PartialBot {
token: Some("my token".to_string()),
interactions_url: Some("some url".to_string()),
..Default::default()
},
)
.await
.unwrap();
db.insert_user(&user).await.unwrap(); assert!(!bot.interactions_url.is_empty());
let bot = Bot {
id: bot_id.to_string(),
owner: user_id.to_string(),
token: token.to_string(),
interactions_url: "some url".to_string(),
..Default::default()
};
db.insert_bot(&bot).await.unwrap();
let mut updated_bot = bot.clone(); let mut updated_bot = bot.clone();
updated_bot updated_bot
@@ -131,9 +192,9 @@ mod tests {
.await .await
.unwrap(); .unwrap();
let fetched_bot1 = db.fetch_bot(bot_id).await.unwrap(); let fetched_bot1 = db.fetch_bot(&bot.id).await.unwrap();
let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap(); let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap();
let fetched_bots = db.fetch_bots_by_user(user_id).await.unwrap(); let fetched_bots = db.fetch_bots_by_user(&owner.id).await.unwrap();
assert!(!bot.public); assert!(!bot.public);
assert!(fetched_bot1.public); assert!(fetched_bot1.public);
@@ -143,12 +204,12 @@ mod tests {
assert_eq!(updated_bot, fetched_bot1); assert_eq!(updated_bot, fetched_bot1);
assert_eq!(fetched_bot1, fetched_bot2); assert_eq!(fetched_bot1, fetched_bot2);
assert_eq!(fetched_bot1, fetched_bots[0]); assert_eq!(fetched_bot1, fetched_bots[0]);
assert_eq!(1, db.get_number_of_bots_by_user(user_id).await.unwrap()); assert_eq!(1, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
bot.delete(&db).await.unwrap(); bot.delete(&db).await.unwrap();
assert!(db.fetch_bot(bot_id).await.is_err()); assert!(db.fetch_bot(&bot.id).await.is_err());
assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap()); assert_eq!(0, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
assert_eq!(db.fetch_user(bot_id).await.unwrap().flags, Some(2)) assert_eq!(db.fetch_user(&bot.id).await.unwrap().flags, Some(2))
}); });
} }
} }
@@ -1,16 +1,45 @@
use nanoid::nanoid; use revolt_result::{create_error, Result};
use crate::{ use crate::{Channel, Database, User};
models::{Channel, Invite, User},
Database, Error, Result,
};
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', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z' 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
]; ];
auto_derived!(
/// Invite
pub enum Invite {
/// Invite to a specific server channel
Server {
/// Invite code
#[serde(rename = "_id")]
code: String,
/// Id of the server this invite points to
server: String,
/// Id of user who created this invite
creator: String,
/// Id of the server channel this invite points to
channel: String,
},
/// Invite to a group channel
Group {
/// Invite code
#[serde(rename = "_id")]
code: String,
/// Id of user who created this invite
creator: String,
/// Id of the group channel this invite points to
channel: String,
}, /* User {
code: String,
user: String
} */
}
);
#[allow(clippy::disallowed_methods)]
impl Invite { impl Invite {
/// Get the invite code for this invite /// Get the invite code for this invite
pub fn code(&'_ self) -> &'_ str { pub fn code(&'_ self) -> &'_ str {
@@ -27,9 +56,13 @@ impl Invite {
} }
/// Create a new invite from given information /// Create a new invite from given information
pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result<Invite> { pub async fn create_channel_invite(
let code = nanoid!(8, &ALPHABET); db: &Database,
let invite = match &target { creator: &User,
channel: &Channel,
) -> Result<Invite> {
let code = nanoid::nanoid!(8, &ALPHABET);
let invite = match &channel {
Channel::Group { id, .. } => Ok(Invite::Group { Channel::Group { id, .. } => Ok(Invite::Group {
code, code,
creator: creator.id.clone(), creator: creator.id.clone(),
@@ -43,7 +76,7 @@ impl Invite {
channel: id.clone(), channel: id.clone(),
}) })
} }
_ => Err(Error::InvalidOperation), _ => Err(create_error!(InvalidOperation)),
}?; }?;
db.insert_invite(&invite).await?; db.insert_invite(&invite).await?;
@@ -67,6 +100,6 @@ impl Invite {
} }
} }
Err(Error::NotFound) Err(create_error!(NotFound))
} }
} }
@@ -1,17 +1,21 @@
use crate::models::Invite; use revolt_result::Result;
use crate::Result;
use crate::Invite;
#[async_trait]
pub trait AbstractChannelInvite: Sync + Send { mod mongodb;
/// Fetch an invite by its id mod reference;
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
#[async_trait]
/// Insert a new invite into the database pub trait AbstractChannelInvites: Sync + Send {
async fn insert_invite(&self, invite: &Invite) -> Result<()>; /// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
/// Delete an invite by its id
async fn delete_invite(&self, code: &str) -> Result<()>; /// Fetch an invite by its id
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server: &str) -> Result<Vec<Invite>>; /// Fetch all invites for a server
} async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>>;
/// Delete an invite by its id
async fn delete_invite(&self, code: &str) -> Result<()>;
}
@@ -0,0 +1,50 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::Invite;
use crate::MongoDb;
use super::AbstractChannelInvites;
static COL: &str = "channel_invites";
#[async_trait]
impl AbstractChannelInvites for MongoDb {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
query!(self, insert_one, COL, &invite).map(|_| ())
}
/// Fetch an invite by the code
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
query!(self, find_one_by_id, COL, code)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
Ok(self
.col::<Invite>(COL)
.find(
doc! {
"server": server_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Delete an invite by its code
async fn delete_invite(&self, code: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, code).map(|_| ())
}
}
@@ -0,0 +1,52 @@
use revolt_result::Result;
use crate::Invite;
use crate::ReferenceDb;
use super::AbstractChannelInvites;
#[async_trait]
impl AbstractChannelInvites for ReferenceDb {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
let mut invites = self.channel_invites.lock().await;
if invites.contains_key(invite.code()) {
Err(create_database_error!("insert", "invite"))
} else {
invites.insert(invite.code().to_string(), invite.clone());
Ok(())
}
}
/// Fetch an invite by the code
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
let invites = self.channel_invites.lock().await;
invites
.get(code)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
let invites = self.channel_invites.lock().await;
Ok(invites
.values()
.filter(|invite| match invite {
Invite::Server { server, .. } => server == server_id,
_ => false,
})
.cloned()
.collect())
}
/// Delete an invite by its code
async fn delete_invite(&self, code: &str) -> Result<()> {
let mut invites = self.channel_invites.lock().await;
if invites.remove(code).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,24 @@
auto_derived!(
/// Channel Unread
pub struct ChannelUnread {
/// Composite key pointing to a user's view of a channel
#[serde(rename = "_id")]
pub id: ChannelCompositeKey,
/// Id of the last message read in this channel by a user
#[serde(skip_serializing_if = "Option::is_none")]
pub last_id: Option<String>,
/// Array of message ids that mention the user
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
}
/// Composite primary key consisting of channel and user id
#[derive(Hash)]
pub struct ChannelCompositeKey {
/// Channel Id
pub channel: String,
/// User Id
pub user: String,
}
);
@@ -0,0 +1,31 @@
use revolt_result::Result;
use crate::ChannelUnread;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannelUnreads: Sync + Send {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()>;
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()>;
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
}
@@ -1,69 +1,71 @@
use bson::Document; use bson::Document;
use mongodb::options::UpdateOptions; use mongodb::options::UpdateOptions;
use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use crate::models::channel_unread::ChannelUnread; use crate::ChannelUnread;
use crate::{AbstractChannelUnread, Error, Result}; use crate::MongoDb;
use super::super::MongoDb; use super::AbstractChannelUnreads;
static COL: &str = "channel_unreads"; static COL: &str = "channel_unreads";
#[async_trait] #[async_trait]
impl AbstractChannelUnread for MongoDb { impl AbstractChannelUnreads for MongoDb {
async fn acknowledge_message(&self, channel: &str, user: &str, message: &str) -> Result<()> { /// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
self.col::<Document>(COL) self.col::<Document>(COL)
.update_one( .update_one(
doc! { doc! {
"_id.channel": channel, "_id.channel": channel_id,
"_id.user": user, "_id.user": user_id,
}, },
doc! { doc! {
"$unset": { "$unset": {
"mentions": 1_i32 "mentions": 1_i32
}, },
"$set": { "$set": {
"last_id": message "last_id": message_id
} }
}, },
UpdateOptions::builder().upsert(true).build(), UpdateOptions::builder().upsert(true).build(),
) )
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| Error::DatabaseError { .map_err(|_| create_database_error!("update_one", COL))
operation: "update_one",
with: "channel_unread",
})
} }
async fn acknowledge_channels(&self, user: &str, channels: &[String]) -> Result<()> { /// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string(); let current_time = Ulid::new().to_string();
self.col::<Document>(COL) self.col::<Document>(COL)
.delete_many( .delete_many(
doc! { doc! {
"_id.channel": { "_id.channel": {
"$in": channels "$in": channel_ids
}, },
"_id.user": user "_id.user": user_id
}, },
None, None,
) )
.await .await
.map_err(|_| Error::DatabaseError { .map_err(|_| create_database_error!("delete_many", COL))?;
operation: "delete_many",
with: "channel_unreads",
})?;
self.col::<Document>(COL) self.col::<Document>(COL)
.insert_many( .insert_many(
channels channel_ids
.iter() .iter()
.map(|channel| { .map(|channel_id| {
doc! { doc! {
"_id": { "_id": {
"channel": channel, "channel": channel_id,
"user": user "user": user_id
}, },
"last_id": &current_time "last_id": &current_time
} }
@@ -72,29 +74,27 @@ impl AbstractChannelUnread for MongoDb {
None, None,
) )
.await .await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "channel_unreads",
})
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
} }
/// Add a mention.
async fn add_mention_to_unread<'a>( async fn add_mention_to_unread<'a>(
&self, &self,
channel: &str, channel_id: &str,
user: &str, user_id: &str,
ids: &[String], message_ids: &[String],
) -> Result<()> { ) -> Result<()> {
self.col::<Document>(COL) self.col::<Document>(COL)
.update_one( .update_one(
doc! { doc! {
"_id.channel": channel, "_id.channel": channel_id,
"_id.user": user, "_id.user": user_id,
}, },
doc! { doc! {
"$push": { "$push": {
"mentions": { "mentions": {
"$each": ids "$each": message_ids
} }
} }
}, },
@@ -102,19 +102,18 @@ impl AbstractChannelUnread for MongoDb {
) )
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| Error::DatabaseError { .map_err(|_| create_database_error!("update_one", COL))
operation: "update_one",
with: "channel_unread",
})
} }
async fn fetch_unreads(&self, user: &str) -> Result<Vec<ChannelUnread>> { /// Fetch all channel unreads for a user.
self.find( async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query!(
self,
find,
COL, COL,
doc! { doc! {
"_id.user": user "_id.user": user_id
}, }
) )
.await
} }
} }
@@ -0,0 +1,90 @@
use revolt_result::Result;
use ulid::Ulid;
use crate::{ChannelCompositeKey, ChannelUnread, ReferenceDb};
use super::AbstractChannelUnreads;
#[async_trait]
impl AbstractChannelUnreads for ReferenceDb {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(unread) = unreads.get_mut(&key) {
unread.mentions = None;
unread.last_id.replace(message_id.to_string());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: Some(message_id.to_string()),
mentions: None,
},
);
}
Ok(())
}
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
for channel_id in channel_ids {
#[allow(clippy::disallowed_methods)]
self.acknowledge_message(channel_id, user_id, &current_time)
.await?;
}
Ok(())
}
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(unread) = unreads.get_mut(&key) {
unread.mentions.replace(message_ids.to_vec());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: None,
mentions: Some(message_ids.to_vec()),
},
);
}
Ok(())
}
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads
.values()
.filter(|unread| unread.id.user == user_id)
.cloned()
.collect())
}
}
@@ -20,6 +20,9 @@ auto_derived_partial!(
/// The channel this webhook belongs to /// The channel this webhook belongs to
pub channel_id: String, pub channel_id: String,
/// The permissions of the webhook
pub permissions: u64,
/// The private token for the webhook /// The private token for the webhook
pub token: Option<String>, pub token: Option<String>,
}, },
@@ -33,6 +36,20 @@ auto_derived!(
} }
); );
#[allow(clippy::derivable_impls)]
impl Default for Webhook {
fn default() -> Self {
Self {
id: Default::default(),
name: Default::default(),
avatar: None,
channel_id: Default::default(),
permissions: Default::default(),
token: Default::default(),
}
}
}
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
impl Webhook { impl Webhook {
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(&self, db: &Database) -> Result<()> {
@@ -117,7 +134,7 @@ mod tests {
id: webhook_id.to_string(), id: webhook_id.to_string(),
name: "Webhook Name".to_string(), name: "Webhook Name".to_string(),
channel_id: channel_id.to_string(), channel_id: channel_id.to_string(),
avatar: Some(Default::default()), avatar: None,
..Default::default() ..Default::default()
}; };
+326 -37
View File
@@ -1,12 +1,19 @@
use std::collections::HashMap; use std::collections::HashMap;
use revolt_config::config;
use revolt_models::v0::{self, MessageAuthor};
use revolt_permissions::OverrideField; use revolt_permissions::OverrideField;
use revolt_result::Result; use revolt_result::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::{events::client::EventV1, Database, File, IntoDocumentPath}; use crate::{
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
Server, SystemMessage, User,
};
auto_derived!( auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel { pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages /// Personal "Saved Notes" channel which allows users to save messages
SavedMessages { SavedMessages {
@@ -164,52 +171,221 @@ auto_derived!(
} }
); );
#[allow(clippy::disallowed_methods)]
impl Channel { impl Channel {
/// Create a channel /* /// Create a channel
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_channel(self).await?; db.insert_channel(self).await?;
let event = EventV1::ChannelCreate(self.clone().into());
match self {
Self::SavedMessages { user, .. } => event.private(user.clone()).await,
Self::DirectMessage { recipients, .. } | Self::Group { recipients, .. } => {
for recipient in recipients {
event.clone().private(recipient.clone()).await;
}
}
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => {
event.p(server.clone()).await;
}
}
Ok(()) Ok(())
}*/
/// Create a new server channel
pub async fn create_server_channel(
db: &Database,
server: &mut Server,
data: v0::DataCreateServerChannel,
update_server: bool,
) -> Result<Channel> {
let config = config().await;
if server.channels.len() > config.features.limits.default.server_channels {
return Err(create_error!(TooManyChannels {
max: config.features.limits.default.server_channels,
}));
};
let id = ulid::Ulid::new().to_string();
let channel = match data.channel_type {
v0::LegacyServerChannelType::Text => Channel::TextChannel {
id: id.clone(),
server: server.id.to_owned(),
name: data.name,
description: data.description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
},
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
id: id.clone(),
server: server.id.to_owned(),
name: data.name,
description: data.description,
icon: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
},
};
db.insert_channel(&channel).await?;
if update_server {
server
.update(
db,
PartialServer {
channels: Some([server.channels.clone(), [id].into()].concat()),
..Default::default()
},
vec![],
)
.await?;
EventV1::ChannelCreate(channel.clone().into())
.p(server.id.clone())
.await;
}
Ok(channel)
}
/// Create a group
pub async fn create_group(
db: &Database,
mut data: v0::DataCreateGroup,
owner_id: String,
) -> Result<Channel> {
data.users.insert(owner_id.to_string());
let config = config().await;
if data.users.len() > config.features.limits.default.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.default.group_size,
}));
}
let recipients = data.users.into_iter().collect::<Vec<String>>();
let channel = Channel::Group {
id: ulid::Ulid::new().to_string(),
name: data.name,
owner: owner_id,
description: data.description,
recipients: recipients.clone(),
icon: None,
last_message_id: None,
permissions: None,
nsfw: data.nsfw.unwrap_or(false),
};
db.insert_channel(&channel).await?;
let event = EventV1::ChannelCreate(channel.clone().into());
for recipient in recipients {
event.clone().private(recipient).await;
}
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 /// Add user to a group
pub async fn add_user_to_group( pub async fn add_user_to_group(
&mut self, &mut self,
db: &Database, db: &Database,
user_id: &str, user: &User,
_by_id: &str, by_id: &str,
) -> Result<()> { ) -> Result<()> {
if let Channel::Group { recipients, .. } = self { if let Channel::Group { recipients, .. } = self {
if recipients.contains(&String::from(user_id)) { if recipients.contains(&String::from(&user.id)) {
return Err(create_error!(AlreadyInGroup)); return Err(create_error!(AlreadyInGroup));
} }
recipients.push(String::from(user_id)); 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));
} }
match &self { match &self {
Channel::Group { id, .. } => { Channel::Group { id, .. } => {
db.add_user_to_group(id, user_id).await?; db.add_user_to_group(id, &user.id).await?;
EventV1::ChannelGroupJoin { EventV1::ChannelGroupJoin {
id: id.to_string(), id: id.to_string(),
user: user_id.to_string(), user: user.id.to_string(),
} }
.p(id.to_string()) .p(id.to_string())
.await; .await;
EventV1::ChannelCreate(self.clone().into()) SystemMessage::UserAdded {
.private(user_id.to_string()) id: user.id.to_string(),
.await; by: by_id.to_string(),
/* TODO: SystemMessage::UserAdded {
id: user.to_string(),
by: by.to_string(),
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await .await
.ok(); */ .ok();
EventV1::ChannelCreate(self.clone().into())
.private(user.id.to_string())
.await;
Ok(()) Ok(())
} }
@@ -238,7 +414,7 @@ impl Channel {
} }
} }
/// Get a reference to this channel's id /// Clone this channel's id
pub fn id(&self) -> String { pub fn id(&self) -> String {
match self { match self {
Channel::DirectMessage { id, .. } Channel::DirectMessage { id, .. }
@@ -293,7 +469,7 @@ impl Channel {
} }
/// Update channel data /// Update channel data
pub async fn update<'a>( pub async fn update(
&mut self, &mut self,
db: &Database, db: &Database,
partial: PartialChannel, partial: PartialChannel,
@@ -305,8 +481,19 @@ impl Channel {
self.apply_options(partial.clone()); self.apply_options(partial.clone());
db.update_channel(&self.id(), &partial, remove.clone()) let id = self.id().to_string();
.await?; db.update_channel(&id, &partial, remove.clone()).await?;
EventV1::ChannelUpdate {
id: id.clone(),
data: partial.into(),
clear: remove.into_iter().map(|v| v.into()).collect(),
}
.p(match self {
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
_ => id,
})
.await;
Ok(()) Ok(())
} }
@@ -440,23 +627,46 @@ 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 /// Remove user from a group
pub async fn remove_user_from_group( pub async fn remove_user_from_group(
&self, &self,
db: &Database, db: &Database,
user_id: &str, user: &User,
_by_id: Option<&str>, by_id: Option<&str>,
silent: bool, silent: bool,
) -> Result<()> { ) -> Result<()> {
match &self { match &self {
Channel::Group { Channel::Group {
id, id,
name,
owner, owner,
recipients, recipients,
.. ..
} => { } => {
if user_id == owner { if &user.id == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != user_id) { if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
db.update_channel( db.update_channel(
id, id,
&PartialChannel { &PartialChannel {
@@ -467,42 +677,57 @@ impl Channel {
) )
.await?; .await?;
/* TODO: SystemMessage::ChannelOwnershipChanged { SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(), from: owner.to_string(),
to: new_owner.into(), to: new_owner.to_string(),
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: name,
avatar: None,
},
self,
false,
)
.await .await
.ok(); */ .ok();
} else { } else {
db.delete_channel(self).await?; return self.delete(db).await;
return Ok(());
} }
} }
EventV1::ChannelGroupLeave { EventV1::ChannelGroupLeave {
id: id.to_string(), id: id.to_string(),
user: user_id.to_string(), user: user.id.to_string(),
} }
.p(id.to_string()) .p(id.to_string())
.await; .await;
if !silent { if !silent {
/* TODO: if let Some(_by) = by_id { if let Some(by) = by_id {
SystemMessage::UserRemove { SystemMessage::UserRemove {
id: user_id.to_string(), id: user.id.to_string(),
by: by.to_string(), by: by.to_string(),
} }
} else { } else {
SystemMessage::UserLeft { SystemMessage::UserLeft {
id: user_id.to_string(), id: user.id.to_string(),
} }
} }
.into_message(id.to_string()) .into_message(id.to_string())
.create(db, self, None) .send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await .await
.ok(); */ .ok();
} }
Ok(()) Ok(())
@@ -514,6 +739,11 @@ impl Channel {
/// Delete a channel /// Delete a channel
pub async fn delete(&self, db: &Database) -> Result<()> { 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 db.delete_channel(self).await
} }
} }
@@ -527,3 +757,62 @@ impl IntoDocumentPath for FieldsChannel {
}) })
} }
} }
#[cfg(test)]
mod tests {
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
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 {
fixture!(db, "server_with_roles",
owner user 0
moderator user 1
user 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, &moderator).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &user).channel(&channel);
assert!(!calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
});
}
}
@@ -1,6 +1,6 @@
use super::AbstractChannels; use super::AbstractChannels;
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel}; use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::Document; use bson::{Bson, Document};
use futures::StreamExt; use futures::StreamExt;
use revolt_permissions::OverrideField; use revolt_permissions::OverrideField;
use revolt_result::Result; use revolt_result::Result;
@@ -188,6 +188,125 @@ impl AbstractChannels for MongoDb {
// Delete a channel // Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> { 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(|_| ()) 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(|_| ())
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -1,19 +1,49 @@
use std::{collections::HashSet, str::FromStr}; use std::collections::HashSet;
use once_cell::sync::Lazy; use std::str::FromStr;
use once_cell::sync::Lazy;
use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use crate::{ use crate::events::client::EventV1;
events::client::EventV1, use crate::Database;
models::{emoji::EmojiParent, Emoji},
Database, Result,
};
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| include_str!(crate::asset!("emojis.txt")) static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
.split('\n') include_str!("unicode_emoji.txt")
.map(|x| x.into()) .split('\n')
.collect()); .map(|x| x.into())
.collect()
});
auto_derived!(
/// Emoji
pub struct Emoji {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// What owns this emoji
pub parent: EmojiParent,
/// Uploader user id
pub creator_id: String,
/// Emoji name
pub name: String,
/// Whether the emoji is animated
#[serde(skip_serializing_if = "crate::if_false", default)]
pub animated: bool,
/// Whether the emoji is marked as nsfw
#[serde(skip_serializing_if = "crate::if_false", default)]
pub nsfw: bool,
}
/// Parent Id of the emoji
#[serde(tag = "type")]
pub enum EmojiParent {
Server { id: String },
Detached,
}
);
#[allow(clippy::disallowed_methods)]
impl Emoji { impl Emoji {
/// Get parent id /// Get parent id
fn parent(&self) -> &str { fn parent(&self) -> &str {
@@ -26,7 +56,8 @@ impl Emoji {
/// Create an emoji /// Create an emoji
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_emoji(self).await?; db.insert_emoji(self).await?;
EventV1::EmojiCreate(self.clone())
EventV1::EmojiCreate(self.clone().into())
.p(self.parent().to_string()) .p(self.parent().to_string())
.await; .await;
@@ -1,8 +1,15 @@
use crate::models::Emoji; use revolt_result::Result;
use crate::Result;
use crate::Emoji;
mod mongodb;
mod reference;
#[async_trait] #[async_trait]
pub trait AbstractEmoji: Sync + Send { pub trait AbstractEmojis: Sync + Send {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Fetch an emoji by its id /// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji>; async fn fetch_emoji(&self, id: &str) -> Result<Emoji>;
@@ -12,9 +19,6 @@ pub trait AbstractEmoji: Sync + Send {
/// Fetch emoji by their parent ids /// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>; async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Detach an emoji by its id /// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>; async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
} }
@@ -1,49 +1,52 @@
use bson::Document; use bson::Document;
use revolt_result::Result;
use crate::models::Emoji; use crate::Emoji;
use crate::{AbstractEmoji, Error, Result}; use crate::MongoDb;
use super::super::MongoDb; use super::AbstractEmojis;
static COL: &str = "emojis"; static COL: &str = "emojis";
#[async_trait] #[async_trait]
impl AbstractEmoji for MongoDb { impl AbstractEmojis for MongoDb {
/// Fetch an emoji by its id /// Insert emoji into database.
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> { async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
self.find_one_by_id(COL, id).await query!(self, insert_one, COL, &emoji).map(|_| ())
} }
/// Fetch emoji by their ids /// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> { async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
self.find( query!(
self,
find,
COL, COL,
doc! { doc! {
"parent.id": parent_id "parent.id": parent_id
}, }
) )
.await
} }
/// Fetch emoji by their parent ids /// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> { async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
self.find( query!(
self,
find,
COL, COL,
doc! { doc! {
"parent.id": { "parent.id": {
"$in": parent_ids "$in": parent_ids
} }
}, }
) )
.await
} }
/// Insert emoji into database. /// Detach an emoji by its id
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
self.insert_one(COL, emoji).await.map(|_| ())
}
/// Delete an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> { async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
self.col::<Document>(COL) self.col::<Document>(COL)
.update_one( .update_one(
@@ -61,9 +64,6 @@ impl AbstractEmoji for MongoDb {
) )
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| Error::DatabaseError { .map_err(|_| create_database_error!("update_one", COL))
operation: "update_one",
with: "emojis",
})
} }
} }
@@ -0,0 +1,67 @@
use revolt_result::Result;
use crate::Emoji;
use crate::EmojiParent;
use crate::ReferenceDb;
use super::AbstractEmojis;
#[async_trait]
impl AbstractEmojis for ReferenceDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
let mut emojis = self.emojis.lock().await;
if emojis.contains_key(&emoji.id) {
Err(create_database_error!("insert", "emoji"))
} else {
emojis.insert(emoji.id.to_string(), emoji.clone());
Ok(())
}
}
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
let emojis = self.emojis.lock().await;
emojis
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
let emojis = self.emojis.lock().await;
Ok(emojis
.values()
.filter(|emoji| match &emoji.parent {
EmojiParent::Server { id } => id == parent_id,
_ => false,
})
.cloned()
.collect())
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
let emojis = self.emojis.lock().await;
Ok(emojis
.values()
.filter(|emoji| match &emoji.parent {
EmojiParent::Server { id } => parent_ids.contains(id),
_ => false,
})
.cloned()
.collect())
}
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
let mut emojis = self.emojis.lock().await;
if let Some(bot) = emojis.get_mut(&emoji.id) {
bot.parent = EmojiParent::Detached;
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -6,7 +6,7 @@ use crate::MongoDb;
use super::AbstractAttachments; use super::AbstractAttachments;
static COL: &str = "bots"; static COL: &str = "attachments";
#[async_trait] #[async_trait]
impl AbstractAttachments for MongoDb { impl AbstractAttachments for MongoDb {
@@ -115,3 +115,21 @@ impl AbstractAttachments for MongoDb {
.map_err(|_| create_database_error!("update_one", COL)) .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))
}
}
@@ -22,15 +22,25 @@ impl AbstractAttachments for ReferenceDb {
async fn find_and_use_attachment( async fn find_and_use_attachment(
&self, &self,
id: &str, id: &str,
_tag: &str, tag: &str,
_parent_type: &str, parent_type: &str,
_parent_id: &str, parent_id: &str,
) -> Result<File> { ) -> Result<File> {
let mut files = self.files.lock().await; let mut files = self.files.lock().await;
if let Some(file) = files.get_mut(id) { if let Some(file) = files.get_mut(id) {
// TODO: check tag if file.tag == tag {
// TODO: set parent ID match parent_type {
Ok(file.clone()) "message" => file.message_id = Some(parent_id.to_owned()),
"user" => file.user_id = Some(parent_id.to_owned()),
"object" => file.object_id = Some(parent_id.to_owned()),
"server" => file.server_id = Some(parent_id.to_owned()),
_ => unreachable!(),
}
Ok(file.clone())
} else {
Err(create_error!(NotFound))
}
} else { } else {
Err(create_error!(NotFound)) Err(create_error!(NotFound))
} }
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,787 @@
use std::collections::HashSet;
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_models::v0::{
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, User,
};
auto_derived_partial!(
/// Message
pub struct Message {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Unique value generated by client sending this message
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
/// Id of the channel this message was sent in
pub channel: String,
/// Id of the user or webhook that sent this message
pub author: String,
/// The webhook that sent this message
#[serde(skip_serializing_if = "Option::is_none")]
pub webhook: Option<MessageWebhook>,
/// Message content
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
/// System message
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<SystemMessage>,
/// Array of attachments
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<File>>,
/// Time at which this message was last edited
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<Timestamp>,
/// Attached embeds to this message
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
/// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
/// Hashmap of emoji IDs to array of user IDs
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub reactions: IndexMap<String, IndexSet<String>>,
/// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,
},
"PartialMessage"
);
auto_derived!(
/// System Event
#[serde(tag = "type")]
pub enum SystemMessage {
#[serde(rename = "text")]
Text { content: String },
#[serde(rename = "user_added")]
UserAdded { id: String, by: String },
#[serde(rename = "user_remove")]
UserRemove { id: String, by: String },
#[serde(rename = "user_joined")]
UserJoined { id: String },
#[serde(rename = "user_left")]
UserLeft { id: String },
#[serde(rename = "user_kicked")]
UserKicked { id: String },
#[serde(rename = "user_banned")]
UserBanned { id: String },
#[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String },
#[serde(rename = "channel_description_changed")]
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
ChannelOwnershipChanged { from: String, to: String },
}
/// Name and / or avatar override information
pub struct Masquerade {
/// Replace the display name shown on this message
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// Replace the avatar shown on this message (URL to image file)
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>,
/// Replace the display role colour shown on this message
///
/// Must have `ManageRole` permission to use
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
}
/// Information to guide interactions on this message
#[derive(Default)]
pub struct Interactions {
/// Reactions which should always appear and be distinct
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reactions: Option<IndexSet<String>>,
/// Whether reactions should be restricted to the given list
///
/// Can only be set to true if reactions list is of at least length 1
#[serde(skip_serializing_if = "crate::if_false", default)]
pub restrict_reactions: bool,
}
/// Appended Information
pub struct AppendMessage {
/// Additional embeds to include in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
}
/// Message Time Period
///
/// Filter and sort messages by time
#[serde(untagged)]
pub enum MessageTimePeriod {
Relative {
/// 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.
nearby: String,
},
Absolute {
/// Message id before which messages should be fetched
before: Option<String>,
/// Message id after which messages should be fetched
after: Option<String>,
/// Message sort direction
sort: Option<MessageSort>,
},
}
/// Message Filter
#[derive(Default)]
pub struct MessageFilter {
/// Parent channel ID
pub channel: Option<String>,
/// Message author ID
pub author: Option<String>,
/// Search query
pub query: Option<String>,
}
/// Message Query
pub struct MessageQuery {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
pub limit: Option<i64>,
/// Filter to apply
#[serde(flatten)]
pub filter: MessageFilter,
/// Time period to fetch
#[serde(flatten)]
pub time_period: MessageTimePeriod,
}
);
#[allow(clippy::derivable_impls)]
impl Default for Message {
fn default() -> Self {
Self {
id: Default::default(),
nonce: None,
channel: Default::default(),
author: Default::default(),
webhook: None,
content: None,
system: None,
attachments: None,
edited: None,
embeds: None,
mentions: None,
replies: None,
reactions: Default::default(),
interactions: Default::default(),
masquerade: None,
}
}
}
#[allow(clippy::disallowed_methods)]
impl Message {
/// Create message from API data
pub async fn create_from_api(
db: &Database,
channel: Channel,
data: DataMessageSend,
author: MessageAuthor<'_>,
mut idempotency: IdempotencyKey,
generate_embeds: bool,
allow_mentions: bool,
) -> Result<Message> {
let config = config().await;
Message::validate_sum(
&data.content,
data.embeds.as_deref().unwrap_or_default(),
config.features.limits.default.message_length,
)?;
idempotency
.consume_nonce(data.nonce)
.await
.map_err(|_| create_error!(InvalidOperation))?;
// Check the message is not empty
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
{
return Err(create_error!(EmptyMessage));
}
// Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
let disallowed = if let Some(list) = &interactions.reactions {
list.is_empty()
} else {
true
};
if disallowed {
return Err(create_error!(InvalidProperty));
}
}
}
let (author_id, webhook) = match &author {
MessageAuthor::User(user) => (user.id.clone(), None),
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
MessageAuthor::System { .. } => ("00000000000000000000000000".to_string(), None),
};
// Start constructing the message
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id.clone(),
channel: channel.id(),
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
interactions: data
.interactions
.map(|interactions| interactions.into())
.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
..Default::default()
};
// Parse mentions in message.
let mut mentions = HashSet::new();
if allow_mentions {
if let Some(content) = &data.content {
for capture in RE_MENTION.captures_iter(content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
}
}
}
// Verify replies are valid.
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > config.features.limits.default.message_replies {
return Err(create_error!(TooManyReplies {
max: config.features.limits.default.message_replies,
}));
}
for ReplyIntent { id, mention } in entries {
let message = db.fetch_message(&id).await?;
if mention && allow_mentions {
mentions.insert(message.author.to_owned());
}
replies.insert(message.id);
}
}
if !mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect());
}
if !replies.is_empty() {
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
// Add attachments to message.
let mut attachments = vec![];
if data
.attachments
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
{
return Err(create_error!(TooManyAttachments {
max: config.features.limits.default.message_attachments,
}));
}
if data
.embeds
.as_ref()
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
{
return Err(create_error!(TooManyEmbeds {
max: config.features.limits.default.message_embeds,
}));
}
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
attachments.push(
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
.await?,
);
}
if !attachments.is_empty() {
message.attachments.replace(attachments);
}
// Process included embeds.
for sendable_embed in data.embeds.unwrap_or_default() {
message.attach_sendable_embed(db, sendable_embed).await?;
}
// Set content
message.content = data.content;
// Pass-through nonce value for clients
message.nonce = Some(idempotency.into_key());
// Send the message
message.send(db, author, &channel, generate_embeds).await?;
Ok(message)
}
/// Send a message without any notifications
pub async fn send_without_notifications(
&mut self,
db: &Database,
is_dm: bool,
generate_embeds: bool,
) -> Result<()> {
db.insert_message(self).await?;
// Fan out events
EventV1::Message(self.clone().into())
.p(self.channel.to_string())
.await;
// Update last_message_id
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
// Add mentions for affected users
if let Some(mentions) = &self.mentions {
for user in mentions {
tasks::ack::queue(
self.channel.to_string(),
user.to_string(),
AckEvent::AddMention {
ids: vec![self.id.to_string()],
},
)
.await;
}
}
// Generate embeds
if generate_embeds {
if let Some(content) = &self.content {
tasks::process_embeds::queue(
self.channel.to_string(),
self.id.to_string(),
content.clone(),
)
.await;
}
}
Ok(())
}
/// Send a message
pub async fn send(
&mut self,
db: &Database,
author: MessageAuthor<'_>,
channel: &Channel,
generate_embeds: bool,
) -> Result<()> {
self.send_without_notifications(
db,
matches!(channel, Channel::DirectMessage { .. }),
generate_embeds,
)
.await?;
// Push out Web Push notifications
crate::tasks::web_push::queue(
{
match channel {
Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => recipients.clone(),
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
_ => vec![],
}
},
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
)
.await;
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,
id: String,
channel: String,
append: AppendMessage,
) -> Result<()> {
db.append_message(&id, &append).await?;
EventV1::MessageAppend {
id,
channel: channel.to_string(),
append: append.into(),
}
.p(channel)
.await;
Ok(())
}
/// Convert sendable embed to text embed and attach to message
pub async fn attach_sendable_embed(
&mut self,
db: &Database,
embed: v0::SendableEmbed,
) -> Result<()> {
let media: Option<v0::File> = if let Some(id) = embed.media {
Some(
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
.await?
.into(),
)
} else {
None
};
let embed = v0::Embed::Text(v0::Text {
icon_url: embed.icon_url,
url: embed.url,
title: embed.title,
description: embed.description,
media,
colour: embed.colour,
});
if let Some(embeds) = &mut self.embeds {
embeds.push(embed);
} else {
self.embeds = Some(vec![embed]);
}
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>,
embeds: &[SendableEmbed],
max_length: usize,
) -> Result<()> {
let mut running_total = 0;
if let Some(content) = content {
running_total += content.len();
}
for embed in embeds {
if let Some(desc) = &embed.description {
running_total += desc.len();
}
}
if running_total <= max_length {
Ok(())
} else {
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 {
pub fn into_message(self, channel: String) -> Message {
Message {
id: Ulid::new().to_string(),
channel,
author: "00000000000000000000000000".to_string(),
system: Some(self),
..Default::default()
}
}
}
impl Interactions {
/// Validate interactions info is correct
pub async fn validate(&self, db: &Database, permissions: &PermissionValue) -> Result<()> {
let config = config().await;
if let Some(reactions) = &self.reactions {
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
if reactions.len() > config.features.limits.default.message_reactions {
return Err(create_error!(InvalidOperation));
}
for reaction in reactions {
if !Emoji::can_use(db, reaction).await? {
return Err(create_error!(InvalidOperation));
}
}
}
Ok(())
}
/// Check if we can use a given emoji to react
pub fn can_use(&self, emoji: &str) -> bool {
if self.restrict_reactions {
if let Some(reactions) = &self.reactions {
reactions.contains(emoji)
} else {
false
}
} else {
true
}
}
/// Check if default initialisation of fields
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}
@@ -1,13 +1,23 @@
use crate::models::message::{AppendMessage, Message, MessageQuery, PartialMessage}; use revolt_result::Result;
use crate::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
mod mongodb;
mod reference;
#[async_trait] #[async_trait]
pub trait AbstractMessage: Sync + Send { pub trait AbstractMessages: Sync + Send {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()>;
/// Fetch a message by its id /// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message>; async fn fetch_message(&self, id: &str) -> Result<Message>;
/// Insert a new message into the database /// Fetch multiple messages by given query
async fn insert_message(&self, message: &Message) -> Result<()>; 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 /// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>; async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
@@ -15,15 +25,6 @@ pub trait AbstractMessage: Sync + Send {
/// Append information to a given message /// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>; async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()>;
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()>;
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
/// Add a new reaction to a message /// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>; async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
@@ -32,4 +33,10 @@ pub trait AbstractMessage: Sync + Send {
/// Remove reaction from a message /// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>; async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()>;
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>;
} }
@@ -1,146 +1,30 @@
use bson::{to_bson, Document}; use bson::{to_bson, Document};
use futures::try_join; use futures::try_join;
use mongodb::options::FindOptions; use mongodb::options::FindOptions;
use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::models::message::{ use crate::{
AppendMessage, Message, MessageQuery, MessageSort, MessageTimePeriod, PartialMessage, AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
}; };
use crate::r#impl::mongo::DocumentId;
use crate::{AbstractMessage, Error, Result};
use super::super::MongoDb; use super::AbstractMessages;
static COL: &str = "messages"; static COL: &str = "messages";
impl MongoDb {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone();
for_attachments.insert(
"attachments",
doc! {
"$exists": 1_i32
},
);
// Check if there are any attachments we need to delete.
let message_ids_with_attachments = self
.find_with_options::<_, DocumentId>(
COL,
for_attachments,
FindOptions::builder()
.projection(doc! { "_id": 1_i32 })
.build(),
)
.await?
.into_iter()
.map(|x| x.id)
.collect::<Vec<String>>();
// If we found any, mark them as deleted.
if !message_ids_with_attachments.is_empty() {
self.col::<Document>("attachments")
.update_many(
doc! {
"message_id": {
"$in": message_ids_with_attachments
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "attachments",
})?;
}
// And then delete said messages.
self.col::<Document>(COL)
.delete_many(projection, None)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "messages",
})
}
}
#[async_trait] #[async_trait]
impl AbstractMessage for MongoDb { impl AbstractMessages for MongoDb {
async fn fetch_message(&self, id: &str) -> Result<Message> { /// Insert a new message into the database
self.find_one_by_id(COL, id).await
}
async fn insert_message(&self, message: &Message) -> Result<()> { async fn insert_message(&self, message: &Message) -> Result<()> {
self.insert_one(COL, message).await.map(|_| ()) query!(self, insert_one, COL, &message).map(|_| ())
} }
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> { /// Fetch a message by its id
self.update_one_by_id(COL, id, message, vec![], None) async fn fetch_message(&self, id: &str) -> Result<Message> {
.await query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
.map(|_| ())
}
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
let mut query = doc! {};
if let Some(embeds) = &append.embeds {
if !embeds.is_empty() {
query.insert(
"$push",
doc! {
"embeds": {
"$each": to_bson(embeds)
.map_err(|_| Error::DatabaseError {
operation: "to_bson",
with: "embeds"
})?
}
},
);
}
}
if query.is_empty() {
return Ok(());
}
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
query,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
.map(|_| ())
}
async fn delete_message(&self, id: &str) -> Result<()> {
self.delete_one_by_id(COL, id).await.map(|_| ())
}
async fn delete_messages(&self, channel: &str, ids: Vec<String>) -> Result<()> {
self.delete_bulk_messages(doc! {
"channel": channel,
"_id": {
"$in": ids
}
})
.await
} }
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> { async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
let mut filter = doc! {}; let mut filter = doc! {};
@@ -212,7 +96,8 @@ impl AbstractMessage for MongoDb {
}) })
.build(), .build(),
) )
)?; )
.map_err(|_| create_database_error!("find", COL))?;
Ok([a, b].concat()) Ok([a, b].concat())
} }
@@ -271,10 +156,66 @@ impl AbstractMessage for MongoDb {
.build(), .build(),
) )
.await .await
.map_err(|_| create_database_error!("find", COL))
} }
} }
} }
/// 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(|_| ())
}
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
let mut query = doc! {};
if let Some(embeds) = &append.embeds {
if !embeds.is_empty() {
query.insert(
"$push",
doc! {
"embeds": {
"$each": to_bson(embeds)
.map_err(|_| create_database_error!("to_bson", "embeds"))?
}
},
);
}
}
if query.is_empty() {
return Ok(());
}
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
query,
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Add a new reaction to a message /// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> { async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL) self.col::<Document>(COL)
@@ -291,10 +232,7 @@ impl AbstractMessage for MongoDb {
) )
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| Error::DatabaseError { .map_err(|_| create_database_error!("update_one", COL))
operation: "update_one",
with: "message",
})
} }
/// Remove a reaction from a message /// Remove a reaction from a message
@@ -313,10 +251,7 @@ impl AbstractMessage for MongoDb {
) )
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| Error::DatabaseError { .map_err(|_| create_database_error!("update_one", COL))
operation: "update_one",
with: "message",
})
} }
/// Remove reaction from a message /// Remove reaction from a message
@@ -335,9 +270,82 @@ impl AbstractMessage for MongoDb {
) )
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| Error::DatabaseError { .map_err(|_| create_database_error!("update_one", COL))
operation: "update_one", }
with: "message",
}) /// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.delete_many(
doc! {
"channel": channel,
"_id": {
"$in": ids
}
},
None,
)
.await
.map(|_| ())
.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))
} }
} }
@@ -0,0 +1,278 @@
use futures::future::try_join_all;
use indexmap::IndexSet;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
use super::AbstractMessages;
#[async_trait]
impl AbstractMessages for ReferenceDb {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()> {
let mut messages = self.messages.lock().await;
if messages.contains_key(&message.id) {
Err(create_database_error!("insert", "message"))
} else {
messages.insert(message.id.to_string(), message.clone());
Ok(())
}
}
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message> {
let messages = self.messages.lock().await;
messages
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
let messages = self.messages.lock().await;
let matched_messages = messages
.values()
.filter(|message| {
if let Some(channel) = &query.filter.channel {
if &message.channel != channel {
return false;
}
}
if let Some(author) = &query.filter.author {
if &message.author != author {
return false;
}
}
if let Some(query) = &query.filter.query {
if let Some(content) = &message.content {
if !content.to_lowercase().contains(query) {
return false;
}
} else {
return false;
}
}
true
})
.cloned()
.collect();
// FIXME: sorting, etc (will be required for tests)
Ok(matched_messages)
/*
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
// 3. Apply message time period
match query.time_period {
MessageTimePeriod::Relative { nearby } => {
// 3.1. Prepare filters
let mut older_message_filter = filter.clone();
let mut newer_message_filter = filter;
older_message_filter.insert(
"_id",
doc! {
"$lt": &nearby
},
);
newer_message_filter.insert(
"_id",
doc! {
"$gte": &nearby
},
);
// 3.2. Execute in both directions
let (a, b) = try_join!(
self.find_with_options::<_, Message>(
COL,
newer_message_filter,
FindOptions::builder()
.limit(limit / 2 + 1)
.sort(doc! {
"_id": 1_i32
})
.build(),
),
self.find_with_options::<_, Message>(
COL,
older_message_filter,
FindOptions::builder()
.limit(limit / 2)
.sort(doc! {
"_id": -1_i32
})
.build(),
)
)
.map_err(|_| create_database_error!("find", COL))?;
Ok([a, b].concat())
}
MessageTimePeriod::Absolute {
before,
after,
sort,
} => {
// 3.1. Apply message ID filter
if let Some(doc) = match (before, after) {
(Some(before), Some(after)) => Some(doc! {
"$lt": before,
"$gt": after
}),
(Some(before), _) => Some(doc! {
"$lt": before
}),
(_, Some(after)) => Some(doc! {
"$gt": after
}),
_ => None,
} {
filter.insert("_id", doc);
}
// 3.2. Execute with given message sort
self.find_with_options(
COL,
filter,
FindOptions::builder()
.limit(limit)
.sort(match sort.unwrap_or(MessageSort::Latest) {
// Sort by relevance, fallback to latest
MessageSort::Relevance => {
if is_search_query {
doc! {
"score": {
"$meta": "textScore"
}
}
} else {
doc! {
"_id": -1_i32
}
}
}
// Sort by latest first
MessageSort::Latest => doc! {
"_id": -1_i32
},
// Sort by oldest first
MessageSort::Oldest => doc! {
"_id": 1_i32
},
})
.build(),
)
.await
.map_err(|_| create_database_error!("find", COL))
}
}*/
}
/// 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;
if let Some(message_data) = messages.get_mut(id) {
message_data.apply_options(message.to_owned());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message_data) = messages.get_mut(id) {
if let Some(embeds) = &append.embeds {
if !embeds.is_empty() {
if let Some(embeds_data) = &mut message_data.embeds {
embeds_data.extend(embeds.clone());
} else {
message_data.embeds = Some(embeds.clone());
}
}
}
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) {
if let Some(users) = message.reactions.get_mut(emoji) {
users.insert(user.to_string());
} else {
message
.reactions
.insert(emoji.to_string(), IndexSet::from([user.to_string()]));
}
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) {
if let Some(users) = message.reactions.get_mut(emoji) {
users.remove(&user.to_string());
}
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) {
message.reactions.remove(emoji);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a message from the database by its id
async fn delete_message(&self, id: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if messages.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.messages
.lock()
.await
.retain(|id, message| message.channel != channel && !ids.contains(id));
Ok(())
}
}
+24 -3
View File
@@ -1,9 +1,16 @@
mod admin_migrations; mod admin_migrations;
mod bots; mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks; mod channel_webhooks;
mod channels; mod channels;
mod emojis;
mod files; mod files;
mod safety_strikes; mod messages;
mod ratelimit_events;
mod safety_reports;
mod safety_snapshots;
mod server_bans;
mod server_members; mod server_members;
mod servers; mod servers;
mod user_settings; mod user_settings;
@@ -11,10 +18,17 @@ mod users;
pub use admin_migrations::*; pub use admin_migrations::*;
pub use bots::*; pub use bots::*;
pub use channel_invites::*;
pub use channel_unreads::*;
pub use channel_webhooks::*; pub use channel_webhooks::*;
pub use channels::*; pub use channels::*;
pub use emojis::*;
pub use files::*; pub use files::*;
pub use safety_strikes::*; 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 server_members::*;
pub use servers::*; pub use servers::*;
pub use user_settings::*; pub use user_settings::*;
@@ -28,9 +42,16 @@ pub trait AbstractDatabase:
+ admin_migrations::AbstractMigrations + admin_migrations::AbstractMigrations
+ bots::AbstractBots + bots::AbstractBots
+ channels::AbstractChannels + channels::AbstractChannels
+ channel_invites::AbstractChannelInvites
+ channel_unreads::AbstractChannelUnreads
+ channel_webhooks::AbstractWebhooks + channel_webhooks::AbstractWebhooks
+ emojis::AbstractEmojis
+ files::AbstractAttachments + files::AbstractAttachments
+ safety_strikes::AbstractAccountStrikes + messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents
+ safety_reports::AbstractReport
+ safety_snapshots::AbstractSnapshot
+ server_bans::AbstractServerBans
+ server_members::AbstractServerMembers + server_members::AbstractServerMembers
+ servers::AbstractServers + servers::AbstractServers
+ user_settings::AbstractUserSettings + user_settings::AbstractUserSettings
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,47 @@
use std::fmt;
use revolt_result::Result;
use ulid::Ulid;
use crate::Database;
auto_derived!(
/// Ratelimit Event
pub struct RatelimitEvent {
/// Id
#[serde(rename = "_id")]
pub id: String,
/// Relevant Object Id
pub target_id: String,
/// Type of event
pub event_type: RatelimitEventType,
}
/// Event type
pub enum RatelimitEventType {
DiscriminatorChange,
}
);
impl fmt::Display for RatelimitEventType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::Debug::fmt(self, f)
}
}
#[allow(clippy::disallowed_methods)]
impl RatelimitEvent {
/// Create ratelimit event
pub async fn create(
db: &Database,
target_id: String,
event_type: RatelimitEventType,
) -> Result<()> {
db.insert_ratelimit_event(&RatelimitEvent {
id: Ulid::new().to_string(),
target_id,
event_type,
})
.await
}
}
@@ -0,0 +1,20 @@
use std::time::Duration;
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractRatelimitEvents: Sync + Send {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()>;
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool>;
}
@@ -0,0 +1,40 @@
use std::time::{Duration, SystemTime};
use super::AbstractRatelimitEvents;
use crate::{MongoDb, RatelimitEvent, RatelimitEventType};
use revolt_result::Result;
use ulid::Ulid;
static COL: &str = "ratelimit_events";
#[async_trait]
impl AbstractRatelimitEvents for MongoDb {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
query!(self, insert_one, COL, &event).map(|_| ())
}
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool> {
self.col::<RatelimitEvent>(COL)
.count_documents(
doc! {
"_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
"target_id": target_id,
"event_type": event_type.to_string()
},
None,
)
.await
.map(|c| c as usize >= count)
.map_err(|_| create_database_error!("count_documents", COL))
}
}
@@ -0,0 +1,46 @@
use std::cmp::Ordering;
use std::time::Duration;
use std::time::SystemTime;
use super::AbstractRatelimitEvents;
use crate::RatelimitEvent;
use crate::RatelimitEventType;
use crate::ReferenceDb;
use revolt_result::Result;
use ulid::Ulid;
#[async_trait]
impl AbstractRatelimitEvents for ReferenceDb {
/// Insert a new ratelimit event
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
let mut ratelimit_events = self.ratelimit_events.lock().await;
if ratelimit_events.contains_key(&event.id) {
Err(create_database_error!("insert", "message"))
} else {
ratelimit_events.insert(event.id.to_string(), event.clone());
Ok(())
}
}
/// Count number of events in given duration and check if we've hit the limit
async fn has_ratelimited(
&self,
target_id: &str,
event_type: RatelimitEventType,
period: Duration,
count: usize,
) -> Result<bool> {
let ratelimit_events = self.ratelimit_events.lock().await;
let gte_cmp_id = Ulid::from_datetime(SystemTime::now() - period).to_string();
Ok(ratelimit_events
.iter()
.filter(|(id, event)| {
id.cmp(&&gte_cmp_id) == Ordering::Greater
&& event.target_id == target_id
&& event.event_type == event_type
})
.count()
>= count)
}
}
@@ -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::*;
@@ -1,13 +1,44 @@
use crate::{ use revolt_models::v0::MessageSort;
models::{ use revolt_result::Result;
message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod},
snapshot::SnapshotContent, use crate::{Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, Server, User};
Message, Server, User,
}, auto_derived!(
Database, Result, /// 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 { impl SnapshotContent {
/// Generate snapshot from a given message
pub async fn generate_from_message( pub async fn generate_from_message(
db: &Database, db: &Database,
message: Message, message: Message,
@@ -54,13 +85,14 @@ impl SnapshotContent {
Ok(( Ok((
SnapshotContent::Message { SnapshotContent::Message {
message, message,
prior_context, prior_context: prior_context.into_iter().map(Into::into).collect(),
leading_context, leading_context: leading_context.into_iter().map(Into::into).collect(),
}, },
files, files,
)) ))
} }
/// Generate snapshot from a given server
pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec<String>)> { pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec<String>)> {
// Collect server's icon and banner // Collect server's icon and banner
let files = [&server.icon, &server.banner] let files = [&server.icon, &server.banner]
@@ -71,6 +103,7 @@ impl SnapshotContent {
Ok((SnapshotContent::Server(server), files)) Ok((SnapshotContent::Server(server), files))
} }
/// Generate snapshot from a given user
pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> { pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> {
// Collect user's avatar and profile background // Collect user's avatar and profile background
let files = [ let files = [
@@ -1,11 +1,12 @@
use crate::models::Snapshot; use revolt_result::Result;
use crate::Result;
use crate::Snapshot;
mod mongodb;
mod reference;
#[async_trait] #[async_trait]
pub trait AbstractSnapshot: Sync + Send { pub trait AbstractSnapshot: Sync + Send {
/// Insert a new snapshot into the database /// Insert a new snapshot into the database
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>; async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>;
/// Fetch a snapshots by a report's id
async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>>;
} }
@@ -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,124 +0,0 @@
use revolt_result::Result;
use crate::Database;
auto_derived_partial!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[serde(rename = "_id")]
pub id: String,
/// Id of reported user
pub user_id: String,
/// Id of moderator
pub moderator_id: String,
/// Attached reason
pub reason: String,
},
"PartialAccountStrike"
);
#[allow(clippy::disallowed_methods)]
impl AccountStrike {
pub async fn create(
db: &Database,
user_id: String,
reason: String,
moderator_id: String,
) -> Result<AccountStrike> {
let strike = AccountStrike {
id: ulid::Ulid::new().to_string(),
user_id,
moderator_id,
reason,
};
db.insert_account_strike(&strike).await?;
Ok(strike)
}
/// Update this strike
pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> {
db.update_account_strike(&self.id, &partial).await?;
self.apply_options(partial);
Ok(())
}
/// Delete this strike
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_account_strike(&self.id).await
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{AccountStrike, PartialAccountStrike};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let user_id = "user";
let strike = AccountStrike::create(
&db,
user_id.to_string(),
"reason 1".to_string(),
"moderator_id".to_string(),
)
.await
.unwrap();
let mut updated_strike = strike.clone();
updated_strike
.update(
&db,
PartialAccountStrike {
reason: Some("new reason".to_string()),
..Default::default()
},
)
.await
.unwrap();
let strike2 = AccountStrike::create(
&db,
user_id.to_string(),
"reason 2".to_string(),
"moderator_id".to_string(),
)
.await
.unwrap();
let strikes = db.fetch_account_strikes_by_user(user_id).await.unwrap();
let ids = strikes
.iter()
.cloned()
.map(|strike| strike.id)
.collect::<HashSet<String>>();
assert!(ids.contains(&strike.id));
assert!(ids.contains(&strike2.id));
let fetched_strike = strikes
.into_iter()
.find(|entry| entry.id == strike.id)
.unwrap();
assert_eq!(fetched_strike, updated_strike);
assert_ne!(fetched_strike, strike);
strike.delete(&db).await.unwrap();
assert_eq!(
1,
db.fetch_account_strikes_by_user(user_id)
.await
.unwrap()
.len()
)
});
}
}
@@ -1,24 +0,0 @@
use revolt_result::Result;
use crate::{AccountStrike, PartialAccountStrike};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAccountStrikes: Sync + Send {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()>;
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike>;
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>>;
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()>;
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()>;
}
@@ -1,55 +0,0 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::MongoDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
static COL: &str = "safety_strikes";
#[async_trait]
impl AbstractAccountStrikes for MongoDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
query!(self, insert_one, COL, &strike).map(|_| ())
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
Ok(self
.col::<AccountStrike>(COL)
.find(
doc! {
"user_id": user_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
@@ -1,60 +0,0 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
#[async_trait]
impl AbstractAccountStrikes for ReferenceDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.contains_key(&strike.id) {
Err(create_database_error!("insert", "strike"))
} else {
strikes.insert(strike.id.to_string(), strike.clone());
Ok(())
}
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
let strikes = self.account_strikes.lock().await;
strikes
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
let strikes = self.account_strikes.lock().await;
Ok(strikes
.values()
.filter(|strike| strike.user_id == user_id)
.cloned()
.collect())
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if let Some(strike) = strikes.get_mut(id) {
strike.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,35 @@
use revolt_result::Result;
use crate::{Database, MemberCompositeKey, Server};
auto_derived!(
/// Server Ban
pub struct ServerBan {
/// Unique member id
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Reason for ban creation
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)
}
}
@@ -1,18 +1,21 @@
use crate::models::server_member::MemberCompositeKey; use revolt_result::Result;
use crate::models::ServerBan;
use crate::Result; use crate::{MemberCompositeKey, ServerBan};
#[async_trait] mod mongodb;
pub trait AbstractServerBan: Sync + Send { mod reference;
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server: &str, user: &str) -> Result<ServerBan>; #[async_trait]
pub trait AbstractServerBans: Sync + Send {
/// Fetch all bans in a server /// Insert new ban into database
async fn fetch_bans(&self, server: &str) -> Result<Vec<ServerBan>>; async fn insert_ban(&self, ban: &ServerBan) -> Result<()>;
/// Insert new ban into database /// Fetch a server ban by server and user id
async fn insert_ban(&self, ban: &ServerBan) -> Result<()>; async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan>;
/// Delete a ban from the database /// Fetch all bans in a server
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()>; async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>>;
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()>;
}
@@ -0,0 +1,56 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::{MemberCompositeKey, ServerBan};
use super::AbstractServerBans;
static COL: &str = "server_bans";
#[async_trait]
impl AbstractServerBans for MongoDb {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
query!(self, insert_one, COL, &ban).map(|_| ())
}
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
query!(
self,
find_one,
COL,
doc! {
"_id.server": server_id,
"_id.user": user_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
query!(
self,
find,
COL,
doc! {
"_id.server": server_id
}
)
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
query!(
self,
delete_one,
COL,
doc! {
"_id.server": &id.server,
"_id.user": &id.user
}
)
.map(|_| ())
}
}
@@ -0,0 +1,52 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{MemberCompositeKey, ServerBan};
use super::AbstractServerBans;
#[async_trait]
impl AbstractServerBans for ReferenceDb {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
let mut server_bans = self.server_bans.lock().await;
if server_bans.contains_key(&ban.id) {
Err(create_database_error!("insert", "ban"))
} else {
server_bans.insert(ban.id.clone(), ban.clone());
Ok(())
}
}
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
let server_bans = self.server_bans.lock().await;
server_bans
.get(&MemberCompositeKey {
server: server_id.to_string(),
user: user_id.to_string(),
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all bans in a server
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
let server_bans = self.server_bans.lock().await;
Ok(server_bans
.values()
.filter(|member| member.id.server == server_id)
.cloned()
.collect())
}
/// Delete a ban from the database
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_bans = self.server_bans.lock().await;
if server_bans.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}
@@ -1,7 +1,11 @@
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
use revolt_result::Result; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use crate::{Database, File, Server}; use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
Server, SystemMessage, User,
};
auto_derived_partial!( auto_derived_partial!(
/// Server Member /// Server Member
@@ -11,8 +15,7 @@ auto_derived_partial!(
pub id: MemberCompositeKey, pub id: MemberCompositeKey,
/// Time at which this user joined the server /// Time at which this user joined the server
#[serde(skip_serializing_if = "Option::is_none")] pub joined_at: Timestamp,
pub joined_at: Option<Timestamp>,
/// Member's nickname /// Member's nickname
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -57,7 +60,104 @@ auto_derived!(
} }
); );
impl Default for Member {
fn default() -> Self {
Self {
id: Default::default(),
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
}
}
}
#[allow(clippy::disallowed_methods)]
impl Member { impl Member {
/// Create a new member in a server
pub async fn create(
db: &Database,
server: &Server,
user: &User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
return Err(create_error!(Banned));
}
if db.fetch_member(&server.id, &user.id).await.is_ok() {
return Err(create_error!(AlreadyInServer));
}
let member = Member {
id: MemberCompositeKey {
server: server.id.to_string(),
user: user.id.to_string(),
},
..Default::default()
};
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let query = DatabasePermissionQuery::new(db, user).server(server);
let existing_channels = db.fetch_channels(&server.channels).await?;
for channel in existing_channels {
let mut channel_query = query.clone().channel(&channel);
if calculate_channel_permissions(&mut channel_query)
.await
.has_channel_permission(ChannelPermission::ViewChannel)
{
channels.push(channel);
}
}
}
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
EventV1::ServerMemberJoin {
id: server.id.clone(),
user: user.id.clone(),
}
.p(server.id.clone())
.await;
EventV1::ServerCreate {
id: server.id.clone(),
server: server.clone().into(),
channels: channels
.clone()
.into_iter()
.map(|channel| channel.into())
.collect(),
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
}
.private(user.id.clone())
.await;
if let Some(id) = server
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.send_without_notifications(db, false, false)
.await
.ok();
}
Ok(channels)
}
/// Update member data /// Update member data
pub async fn update<'a>( pub async fn update<'a>(
&mut self, &mut self,
@@ -73,13 +173,13 @@ impl Member {
db.update_member(&self.id, &partial, remove.clone()).await?; db.update_member(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::ServerMemberUpdate { EventV1::ServerMemberUpdate {
id: self.id.clone(), id: self.id.clone().into(),
data: partial, data: partial.into(),
clear: remove, clear: remove.into_iter().map(|field| field.into()).collect(),
} }
.p(self.id.server.clone()) .p(self.id.server.clone())
.await; */ .await;
Ok(()) Ok(())
} }
@@ -115,4 +215,47 @@ impl Member {
false 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(())
}
} }
+98 -157
View File
@@ -1,10 +1,11 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use revolt_permissions::OverrideField; use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_permissions::{OverrideField, DEFAULT_PERMISSION_SERVER};
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use crate::{Database, File}; use crate::{events::client::EventV1, Channel, Database, File, User};
auto_derived_partial!( auto_derived_partial!(
/// Server /// Server
@@ -22,7 +23,7 @@ auto_derived_partial!(
pub description: Option<String>, pub description: Option<String>,
/// Channels within this server /// Channels within this server
// ! FIXME: this may be redundant // TODO: investigate if this is redundant and can be removed
pub channels: Vec<String>, pub channels: Vec<String>,
/// Categories for this server /// Categories for this server
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -128,10 +129,55 @@ auto_derived!(
} }
); );
#[allow(clippy::disallowed_methods)]
impl Server { impl Server {
/// Create a server /// Create a server
pub async fn create(&self, db: &Database) -> Result<()> { pub async fn create(
db.insert_server(self).await db: &Database,
data: v0::DataCreateServer,
owner: &User,
create_default_channels: bool,
) -> Result<(Server, Vec<Channel>)> {
let mut server = Server {
id: ulid::Ulid::new().to_string(),
owner: owner.id.to_string(),
name: data.name,
description: data.description,
channels: vec![],
nsfw: data.nsfw.unwrap_or(false),
default_permissions: *DEFAULT_PERMISSION_SERVER as i64,
analytics: false,
banner: None,
categories: None,
discoverable: false,
flags: None,
icon: None,
roles: HashMap::new(),
system_messages: None,
};
let channels: Vec<Channel> = if create_default_channels {
vec![
Channel::create_server_channel(
db,
&mut server,
DataCreateServerChannel {
channel_type: v0::LegacyServerChannelType::Text,
name: "General".to_string(),
..Default::default()
},
false,
)
.await?,
]
} else {
vec![]
};
server.channels = channels.iter().map(|c| c.id()).collect();
db.insert_server(&server).await?;
Ok((server, channels))
} }
/// Update server data /// Update server data
@@ -149,24 +195,24 @@ impl Server {
db.update_server(&self.id, &partial, remove.clone()).await?; db.update_server(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::ServerUpdate { EventV1::ServerUpdate {
id: self.id.clone(), id: self.id.clone(),
data: partial, data: partial.into(),
clear: remove, clear: remove.into_iter().map(|v| v.into()).collect(),
} }
.p(self.id.clone()) .p(self.id.clone())
.await; */ .await;
Ok(()) Ok(())
} }
/// Delete a server /// Delete a server
pub async fn delete(self, db: &Database) -> Result<()> { pub async fn delete(self, db: &Database) -> Result<()> {
/* // TODO: EventV1::ServerDelete { EventV1::ServerDelete {
id: self.id.clone(), id: self.id.clone(),
} }
.p(self.id.clone()) .p(self.id.clone())
.await; */ .await;
db.delete_server(&self.id).await db.delete_server(&self.id).await
} }
@@ -207,144 +253,6 @@ impl Server {
Err(create_error!(NotFound)) Err(create_error!(NotFound))
} }
} }
/* /// Create a new member in a server
pub async fn create_member(
&self,
db: &Database,
user: User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
return Err(Error::Banned);
}
let member = Member {
id: MemberCompositeKey {
server: self.id.clone(),
user: user.id.clone(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
};
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let perm = perms(&user).server(self).member(&member);
let existing_channels = db.fetch_channels(&self.channels).await?;
for channel in existing_channels {
if perm
.clone()
.channel(&channel)
.has_permission(db, Permission::ViewChannel)
.await?
{
channels.push(channel);
}
}
}
/* // TODO: EventV1::ServerMemberJoin {
id: self.id.clone(),
user: user.id.clone(),
}
.p(self.id.clone())
.await;
EventV1::ServerCreate {
id: self.id.clone(),
server: self.clone(),
channels: channels.clone(),
}
.private(user.id.clone())
.await; */
if let Some(id) = self
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
Ok(channels)
}
/// Remove a member from a server
pub async fn remove_member(
&self,
db: &Database,
member: Member,
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&member.id).await?;
/* // 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 { impl Role {
@@ -364,14 +272,14 @@ impl Role {
let role_id = Ulid::new().to_string(); let role_id = Ulid::new().to_string();
db.insert_role(server_id, &role_id, self).await?; db.insert_role(server_id, &role_id, self).await?;
/* // TODO: EventV1::ServerRoleUpdate { EventV1::ServerRoleUpdate {
id: server_id.to_string(), id: server_id.to_string(),
role_id: role_id.to_string(), role_id: role_id.to_string(),
data: self.clone().into_optional(), data: self.clone().into_optional().into(),
clear: vec![], clear: vec![],
} }
.p(server_id.to_string()) .p(server_id.to_string())
.await; */ .await;
Ok(role_id) Ok(role_id)
} }
@@ -394,14 +302,14 @@ impl Role {
db.update_role(server_id, role_id, &partial, remove.clone()) db.update_role(server_id, role_id, &partial, remove.clone())
.await?; .await?;
/* // TODO: EventV1::ServerRoleUpdate { EventV1::ServerRoleUpdate {
id: server_id.to_string(), id: server_id.to_string(),
role_id: role_id.to_string(), role_id: role_id.to_string(),
data: partial, data: partial.into(),
clear: vec![], clear: vec![],
} }
.p(server_id.to_string()) .p(server_id.to_string())
.await; */ .await;
Ok(()) Ok(())
} }
@@ -415,12 +323,12 @@ impl Role {
/// Delete a role /// Delete a role
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> { pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
/* // TODO: EventV1::ServerRoleDelete { EventV1::ServerRoleDelete {
id: server_id.to_string(), id: server_id.to_string(),
role_id: role_id.to_string(), role_id: role_id.to_string(),
} }
.p(server_id.to_string()) .p(server_id.to_string())
.await; */ .await;
db.delete_role(server_id, role_id).await db.delete_role(server_id, role_id).await
} }
@@ -449,3 +357,36 @@ impl SystemMessageChannels {
ids ids
} }
} }
#[cfg(test)]
mod tests {
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[async_std::test]
async fn permissions() {
database_test!(|db| async move {
fixture!(db, "server_with_roles",
owner user 0
moderator user 1
user user 2
server server 4);
let mut query = DatabasePermissionQuery::new(&db, &owner).server(&server);
assert!(calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::GrantAllSafe));
let mut query = DatabasePermissionQuery::new(&db, &moderator).server(&server);
assert!(calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::BanMembers));
let mut query = DatabasePermissionQuery::new(&db, &user).server(&server);
assert!(!calculate_server_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::BanMembers));
});
}
}
@@ -1,4 +1,4 @@
use bson::{to_document, Document}; use bson::{to_document, Bson, Document};
use futures::StreamExt; use futures::StreamExt;
use revolt_result::Result; use revolt_result::Result;
@@ -67,6 +67,7 @@ impl AbstractServers for MongoDb {
/// Delete a server by its id /// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()> { async fn delete_server(&self, id: &str) -> Result<()> {
self.delete_associated_server_objects(id).await?;
query!(self, delete_one_by_id, COL, id).map(|_| ()) 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(())
}
}
@@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::Database; use crate::{events::client::EventV1, Database};
use revolt_result::Result; use revolt_result::Result;
@@ -16,12 +16,12 @@ impl UserSettingsImpl for UserSettings {
async fn set(self, db: &Database, user: &str) -> Result<()> { async fn set(self, db: &Database, user: &str) -> Result<()> {
db.set_user_settings(user, &self).await?; db.set_user_settings(user, &self).await?;
/* // TODO: EventV1::UserSettingsUpdate { EventV1::UserSettingsUpdate {
id: user.to_string(), id: user.to_string(),
update: self, update: self,
} }
.private(user.to_string()) .private(user.to_string())
.await; */ .await;
Ok(()) Ok(())
} }
@@ -2,8 +2,12 @@ mod model;
mod ops; mod ops;
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
mod rocket; mod rocket;
#[cfg(feature = "rocket-impl")]
mod schema;
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]
pub use self::rocket::*; pub use self::rocket::*;
#[cfg(feature = "rocket-impl")]
pub use self::schema::*;
pub use model::*; pub use model::*;
pub use ops::*; pub use ops::*;
+469 -43
View File
@@ -1,9 +1,14 @@
use std::collections::HashSet; use std::{collections::HashSet, time::Duration};
use crate::{Database, File}; use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use revolt_result::{Error, ErrorType, Result}; use rand::seq::SliceRandom;
use revolt_config::config;
use revolt_models::v0;
use revolt_presence::filter_online;
use revolt_result::{create_error, Result};
use ulid::Ulid;
auto_derived_partial!( auto_derived_partial!(
/// # User /// # User
@@ -16,7 +21,8 @@ auto_derived_partial!(
/// Discriminator /// Discriminator
pub discriminator: String, pub discriminator: String,
/// Display name /// Display name
pub display_name: String, #[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
/// Avatar attachment /// Avatar attachment
pub avatar: Option<File>, pub avatar: Option<File>,
@@ -48,6 +54,15 @@ auto_derived_partial!(
); );
auto_derived!( auto_derived!(
/// Optional fields on user object
pub enum FieldsUser {
Avatar,
StatusText,
StatusPresence,
ProfileContent,
ProfileBackground,
}
/// User's relationship with another user (or themselves) /// User's relationship with another user (or themselves)
pub enum RelationshipStatus { pub enum RelationshipStatus {
None, None,
@@ -81,20 +96,22 @@ auto_derived!(
} }
/// User's active status /// User's active status
#[derive(Default)]
pub struct UserStatus { pub struct UserStatus {
/// Custom status text /// Custom status text
#[serde(skip_serializing_if = "String::is_empty", default)] #[serde(skip_serializing_if = "Option::is_none")]
pub text: String, pub text: Option<String>,
/// Current presence option /// Current presence option
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub presence: Option<Presence>, pub presence: Option<Presence>,
} }
/// User's profile /// User's profile
#[derive(Default)]
pub struct UserProfile { pub struct UserProfile {
/// Text content on user's profile /// Text content on user's profile
#[serde(skip_serializing_if = "String::is_empty", default)] #[serde(skip_serializing_if = "Option::is_none")]
pub content: String, pub content: Option<String>,
/// Background visible on user's profile /// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<File>, pub background: Option<File>,
@@ -106,27 +123,447 @@ auto_derived!(
pub owner: String, pub owner: String,
} }
/// Optional fields on user object /// Enumeration providing a hint to the type of user we are handling
pub enum FieldsUser { pub enum UserHint {
Avatar, /// Could be either a user or a bot
StatusText, Any,
StatusPresence, /// Only match bots
ProfileContent, Bot,
ProfileBackground, /// Only match users
User,
} }
); );
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
let mut set = (2..9999)
.map(|v| format!("{:0>4}", v))
.collect::<HashSet<String>>();
for discrim in [
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
] {
set.remove(&format!("{:0>4}", discrim));
}
set.into_iter().collect()
});
#[allow(clippy::derivable_impls)]
impl Default for User {
fn default() -> Self {
Self {
id: Default::default(),
username: Default::default(),
discriminator: Default::default(),
display_name: Default::default(),
avatar: Default::default(),
relations: Default::default(),
badges: Default::default(),
status: Default::default(),
profile: Default::default(),
flags: Default::default(),
privileged: Default::default(),
bot: Default::default(),
}
}
}
#[allow(clippy::disallowed_methods)]
impl User { impl User {
/// Check whether a username is already in use by another user /// Create a new user
#[allow(dead_code)] pub async fn create<I, D>(
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> { db: &Database,
match db.fetch_user_by_username(username).await { username: String,
Ok(_) => Ok(true), account_id: I,
Err(Error { data: D,
error_type: ErrorType::NotFound, ) -> Result<User>
.. where
}) => Ok(false), I: Into<Option<String>>,
Err(error) => Err(error), D: Into<Option<PartialUser>>,
{
let username = User::validate_username(username)?;
let mut user = User {
id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()),
discriminator: User::find_discriminator(db, &username, None).await?,
username,
..Default::default()
};
if let Some(data) = data.into() {
user.apply_options(data);
}
db.insert_user(&user).await?;
Ok(user)
}
/// Get the relationship with another user
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
if self.id == user_b {
return RelationshipStatus::User;
}
if let Some(relations) = &self.relations {
if let Some(relationship) = relations.iter().find(|x| x.id == user_b) {
return relationship.status.clone();
}
}
RelationshipStatus::None
}
pub fn is_friends_with(&self, user_b: &str) -> bool {
matches!(
self.relationship_with(user_b),
RelationshipStatus::Friend | RelationshipStatus::User
)
}
/// Check whether two users have a mutual connection
///
/// This will check if user and user_b share a server or a group.
pub async fn has_mutual_connection(&self, db: &Database, user_b: &str) -> Result<bool> {
Ok(!db
.fetch_mutual_server_ids(&self.id, user_b)
.await?
.is_empty()
|| !db
.fetch_mutual_channel_ids(&self.id, user_b)
.await?
.is_empty())
}
/// Check if this user can acquire another server
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
let config = config().await;
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
Ok(())
} else {
Err(create_error!(TooManyServers {
max: config.features.limits.default.servers
}))
}
}
/// Sanitise and validate a username can be used
pub fn validate_username(username: String) -> Result<String> {
// Copy the username for validation
let username_lowercase = username.to_lowercase();
// Block homoglyphs
if decancer::cure(&username_lowercase).into_str() != username_lowercase {
return Err(create_error!(InvalidUsername));
}
// Ensure the username itself isn't blocked
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"];
for username in BLOCKED_USERNAMES {
if username_lowercase == *username {
return Err(create_error!(InvalidUsername));
}
}
// Ensure none of the following substrings show up in the username
const BLOCKED_SUBSTRINGS: &[&str] = &["```"];
for substr in BLOCKED_SUBSTRINGS {
if username_lowercase.contains(substr) {
return Err(create_error!(InvalidUsername));
}
}
Ok(username)
}
/// Find a user from a given token and hint
#[async_recursion]
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> {
match hint {
UserHint::Bot => {
db.fetch_user(
&db.fetch_bot_by_token(token)
.await
.map_err(|_| create_error!(InternalError))?
.id,
)
.await
}
UserHint::User => db.fetch_user_by_token(token).await,
UserHint::Any => {
if let Ok(user) = User::from_token(db, token, UserHint::User).await {
Ok(user)
} else {
User::from_token(db, token, UserHint::Bot).await
}
}
}
}
/// 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,
preferred: Option<(String, String)>,
) -> Result<String> {
let search_space: &HashSet<String> = &DISCRIMINATOR_SEARCH_SPACE;
let used_discriminators: HashSet<String> = db
.fetch_discriminators_in_use(username)
.await?
.into_iter()
.collect();
let available_discriminators: Vec<&String> =
search_space.difference(&used_discriminators).collect();
if available_discriminators.is_empty() {
return Err(create_error!(UsernameTaken));
}
if let Some((preferred, target_id)) = preferred {
if available_discriminators.contains(&&preferred) {
return Ok(preferred);
} else {
if db
.has_ratelimited(
&target_id,
crate::RatelimitEventType::DiscriminatorChange,
Duration::from_secs(60 * 60 * 24),
1,
)
.await?
{
return Err(create_error!(DiscriminatorChangeRatelimited));
}
RatelimitEvent::create(
db,
target_id,
crate::RatelimitEventType::DiscriminatorChange,
)
.await?;
}
}
let mut rng = rand::thread_rng();
Ok(available_discriminators
.choose(&mut rng)
.expect("we can assert this has an element")
.to_string())
}
/// Update a user's username
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
let username = User::validate_username(username)?;
if self.username.to_lowercase() == username.to_lowercase() {
self.update(
db,
PartialUser {
username: Some(username),
..Default::default()
},
vec![],
)
.await
} else {
self.update(
db,
PartialUser {
discriminator: Some(
User::find_discriminator(
db,
&username,
Some((self.discriminator.to_string(), self.id.clone())),
)
.await?,
),
username: Some(username),
..Default::default()
},
vec![],
)
.await
}
}
/// Set a relationship to another user
pub async fn set_relationship(
&mut self,
db: &Database,
user_b: &User,
status: RelationshipStatus,
) -> Result<()> {
db.set_relationship(&self.id, &user_b.id, &status).await?;
if let RelationshipStatus::None | RelationshipStatus::User = status {
if let Some(relations) = &mut self.relations {
relations.retain(|relation| relation.id != user_b.id);
}
} else {
let relation = Relationship {
id: user_b.id.to_string(),
status,
};
if let Some(relations) = &mut self.relations {
relations.retain(|relation| relation.id != user_b.id);
relations.push(relation);
} else {
self.relations = Some(vec![relation]);
}
}
Ok(())
}
/// Apply a certain relationship between two users
pub async fn apply_relationship(
&mut self,
db: &Database,
target: &mut User,
local: RelationshipStatus,
remote: RelationshipStatus,
) -> Result<()> {
target.set_relationship(db, self, remote).await?;
self.set_relationship(db, target, local).await?;
EventV1::UserRelationship {
id: target.id.clone(),
user: self.clone().into(db, Some(&*target)).await,
}
.private(target.id.clone())
.await;
EventV1::UserRelationship {
id: self.id.clone(),
user: target.clone().into(db, Some(&*self)).await,
}
.private(self.id.clone())
.await;
Ok(())
}
/// Add another user as a friend
pub async fn add_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::User => Err(create_error!(NoEffect)),
RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
RelationshipStatus::Outgoing => Err(create_error!(AlreadySentRequest)),
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
RelationshipStatus::Incoming => {
self.apply_relationship(
db,
target,
RelationshipStatus::Friend,
RelationshipStatus::Friend,
)
.await
}
RelationshipStatus::None => {
self.apply_relationship(
db,
target,
RelationshipStatus::Outgoing,
RelationshipStatus::Incoming,
)
.await
}
}
}
/// Remove another user as a friend
pub async fn remove_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::Friend
| RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => {
self.apply_relationship(
db,
target,
RelationshipStatus::None,
RelationshipStatus::None,
)
.await
}
_ => Err(create_error!(NoEffect)),
}
}
/// Block another user
pub async fn block_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(create_error!(NoEffect)),
RelationshipStatus::BlockedOther => {
self.apply_relationship(
db,
target,
RelationshipStatus::Blocked,
RelationshipStatus::Blocked,
)
.await
}
RelationshipStatus::None
| RelationshipStatus::Friend
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing => {
self.apply_relationship(
db,
target,
RelationshipStatus::Blocked,
RelationshipStatus::BlockedOther,
)
.await
}
}
}
/// Unblock another user
pub async fn unblock_user(&mut self, db: &Database, target: &mut User) -> Result<()> {
match self.relationship_with(&target.id) {
RelationshipStatus::Blocked => match target.relationship_with(&self.id) {
RelationshipStatus::Blocked => {
self.apply_relationship(
db,
target,
RelationshipStatus::BlockedOther,
RelationshipStatus::Blocked,
)
.await
}
RelationshipStatus::BlockedOther => {
self.apply_relationship(
db,
target,
RelationshipStatus::None,
RelationshipStatus::None,
)
.await
}
_ => Err(create_error!(InternalError)),
},
_ => Err(create_error!(NoEffect)),
} }
} }
@@ -144,13 +581,14 @@ impl User {
self.apply_options(partial.clone()); self.apply_options(partial.clone());
db.update_user(&self.id, &partial, remove.clone()).await?; db.update_user(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::UserUpdate { EventV1::UserUpdate {
id: self.id.clone(), id: self.id.clone(),
data: partial, data: partial.into(),
clear: remove, clear: remove.into_iter().map(|v| v.into()).collect(),
event_id: Some(Ulid::new().to_string()),
} }
.p_user(self.id.clone(), db) .p_user(self.id.clone(), db)
.await; */ .await;
Ok(()) Ok(())
} }
@@ -161,7 +599,7 @@ impl User {
FieldsUser::Avatar => self.avatar = None, FieldsUser::Avatar => self.avatar = None,
FieldsUser::StatusText => { FieldsUser::StatusText => {
if let Some(x) = self.status.as_mut() { if let Some(x) = self.status.as_mut() {
x.text = String::new(); x.text = None;
} }
} }
FieldsUser::StatusPresence => { FieldsUser::StatusPresence => {
@@ -171,7 +609,7 @@ impl User {
} }
FieldsUser::ProfileContent => { FieldsUser::ProfileContent => {
if let Some(x) = self.profile.as_mut() { if let Some(x) = self.profile.as_mut() {
x.content = String::new(); x.content = None;
} }
} }
FieldsUser::ProfileBackground => { FieldsUser::ProfileBackground => {
@@ -202,15 +640,3 @@ impl User {
.await .await
} }
} }
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
let mut set = (2..9999)
.map(|v| format!("{:0>4}", v))
.collect::<HashSet<String>>();
for discrim in [1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999] {
set.remove(&discrim.to_string());
}
set.into_iter().collect()
});
+4 -1
View File
@@ -14,7 +14,7 @@ pub trait AbstractUsers: Sync + Send {
async fn fetch_user(&self, id: &str) -> Result<User>; async fn fetch_user(&self, id: &str) -> Result<User>;
/// Fetch a user from the database by their username /// 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 /// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>; async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
@@ -22,6 +22,9 @@ pub trait AbstractUsers: Sync + Send {
/// Fetch multiple users by their ids /// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>; async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
/// Fetch all discriminators in use for a username
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>>;
/// Fetch ids of users that both users are friends with /// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>; async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
@@ -25,13 +25,14 @@ impl AbstractUsers for MongoDb {
} }
/// Fetch a user from the database by their username /// 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!( query!(
self, self,
find_one_with_options, find_one_with_options,
COL, COL,
doc! { doc! {
"username": username "username": username,
"discriminator": discriminator
}, },
FindOneOptions::builder() FindOneOptions::builder()
.collation( .collation(
@@ -87,6 +88,39 @@ impl AbstractUsers for MongoDb {
.await) .await)
} }
/// Fetch all discriminators in use for a username
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
#[derive(Deserialize)]
struct UserDocument {
discriminator: String,
}
Ok(self
.col::<UserDocument>(COL)
.find(
doc! {
"username": username
},
FindOptions::builder()
.collation(
Collation::builder()
.locale("en")
.strength(CollationStrength::Secondary)
.build(),
)
.projection(doc! { "_id": 0, "discriminator": 1 })
.build(),
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { s.ok() })
.collect::<Vec<UserDocument>>()
.await
.into_iter()
.map(|user| user.discriminator)
.collect::<Vec<String>>())
}
/// Fetch ids of users that both users are friends with /// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self Ok(self
@@ -1,7 +1,7 @@
use revolt_result::Result; use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User}; use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
use crate::{ReferenceDb, Relationship};
use super::AbstractUsers; use super::AbstractUsers;
@@ -28,12 +28,14 @@ impl AbstractUsers for ReferenceDb {
} }
/// Fetch a user from the database by their username /// 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 users = self.users.lock().await;
let lowercase = username.to_lowercase(); let lowercase = username.to_lowercase();
users users
.values() .values()
.find(|user| user.username.to_lowercase() == lowercase) .find(|user| {
user.username.to_lowercase() == lowercase && user.discriminator == discriminator
})
.cloned() .cloned()
.ok_or_else(|| create_error!(NotFound)) .ok_or_else(|| create_error!(NotFound))
} }
@@ -56,6 +58,18 @@ impl AbstractUsers for ReferenceDb {
.collect() .collect()
} }
/// Fetch all discriminators in use for a username
async fn fetch_discriminators_in_use(&self, username: &str) -> Result<Vec<String>> {
let users = self.users.lock().await;
let lowercase = username.to_lowercase();
Ok(users
.values()
.filter(|user| user.username.to_lowercase() == lowercase)
.map(|user| &user.discriminator)
.cloned()
.collect())
}
/// Fetch ids of users that both users are friends with /// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
todo!() todo!()
@@ -94,19 +108,49 @@ impl AbstractUsers for ReferenceDb {
/// Set relationship with another user /// Set relationship with another user
/// ///
/// This should use pull_relationship if relationship is None. /// This should use pull_relationship if relationship is None or User.
async fn set_relationship( async fn set_relationship(
&self, &self,
_user_id: &str, user_id: &str,
_target_id: &str, target_id: &str,
_relationship: &RelationshipStatus, relationship: &RelationshipStatus,
) -> Result<()> { ) -> Result<()> {
todo!() if let RelationshipStatus::User | RelationshipStatus::None = &relationship {
self.pull_relationship(user_id, target_id).await
} else {
let mut users = self.users.lock().await;
let user = users
.get_mut(user_id)
.ok_or_else(|| create_error!(NotFound))?;
let relation = Relationship {
id: target_id.to_string(),
status: relationship.clone(),
};
if let Some(relations) = &mut user.relations {
relations.retain(|relation| relation.id != target_id);
relations.push(relation);
} else {
user.relations = Some(vec![relation]);
}
Ok(())
}
} }
/// Remove relationship with another user /// Remove relationship with another user
async fn pull_relationship(&self, _user_id: &str, _target_id: &str) -> Result<()> { async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
todo!() let mut users = self.users.lock().await;
let user = users
.get_mut(user_id)
.ok_or_else(|| create_error!(NotFound))?;
if let Some(relations) = &mut user.relations {
relations.retain(|relation| relation.id != target_id);
}
Ok(())
} }
/// Delete a user by their id /// Delete a user by their id

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