Compare commits

..
167 Commits
Author SHA1 Message Date
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
Paul Makles 9975de01bc chore: remove webhooks from production 2023-06-11 10:09:50 +01:00
Paul Makles ac525466b8 chore: cut a new release (0.6.0) 2023-06-11 10:04:15 +01:00
Paul Makles c7a04e4559 fix: conflict resolution in migration
fix: write new username and display name
chore: restrict discriminator search space
2023-06-11 09:53:41 +01:00
Paul Makles 5bbe30edbc feat(core/database): migrate to discriminators 2023-06-11 09:16:24 +01:00
Paul Makles 31c7dc0577 feat: add discriminator and display name fields 2023-06-09 16:34:18 +01:00
Paul Makles aba5c7d8af fix: shouldn't prefix dep: 2023-06-04 19:45:47 +01:00
Paul Makles 2f4ea4cabb refactor(core/models): transitively apply feature flags 2023-06-04 19:45:31 +01:00
Paul Makles 7e801a31bd fix: make sure feature flags are respected 2023-06-04 19:43:56 +01:00
Paul Makles eb4870c9d5 fix: explicitly target Rust 1.70.0 for images 2023-06-04 19:42:42 +01:00
Paul Makles b1fdd7fc74 merge: pull request #251 from revoltchat/fix/zomatree/send-message-null 2023-06-04 19:29:07 +01:00
Paul MaklesandToastXC bf3e027067 feat: channel model implementation (#250)
Co-authored-by: ToastXC <100072983+toastxc@users.noreply.github.com>
2023-06-04 18:52:49 +01:00
Zomatree a7a7e49e18 fix: send message route not accepting null 2023-06-04 18:46:49 +01:00
Paul Makles 3c834d06d4 fix: must not restrict lowerbound for embeds 2023-06-03 17:49:31 +01:00
Paul Makles f5e6086d2a fix(core/database): test matches incorrect entry
[skip ci]
2023-06-03 17:15:43 +01:00
Paul Makles 3365a8e482 chore: force bind to 0.0.0.0 because it's just ignoring all environment variables??? 2023-06-03 17:05:25 +01:00
Paul Makles 268c2d1355 fix: re-order crate publishing 2023-06-03 14:11:44 +01:00
Paul Makles e9d6b963df chore: bump version to 0.6.0-rc.1 2023-06-03 14:06:04 +01:00
Paul Makles f9d9447203 feat: add additional validation for github webhook 2023-06-03 14:02:16 +01:00
Paul Makles a0002d0b43 feat: reintroduce permission checks for send 2023-06-03 14:00:17 +01:00
Paul Makles 23188032ca refactor(delta): port routes to core webhook models 2023-06-03 13:01:43 +01:00
Paul Makles f9f5a30e2c refactor(quark): strip webhook code 2023-06-03 13:01:28 +01:00
Paul Makles e393e17b59 feat(core/result): add rocket and okapi support 2023-06-03 13:01:12 +01:00
Paul Makles a29d8f6aab refactor(core/models): swap dependency order with db
feat(core/models): implement webhook model
2023-06-03 13:01:01 +01:00
Paul Makles 1dbbc3ed8d feat(core/database): implement webhook model 2023-06-03 13:00:05 +01:00
Paul Makles 410acb5b5c merge: branch 'master' into webhooks 2023-06-02 18:55:32 +01:00
Paul Makles 53c9160cd6 feat: implement creating strikes
feat: add moderator_id to strikes
fix: use correct collection for strikes
2023-05-31 19:04:20 +01:00
Paul Makles 28b1170633 ci: switch to stable Rust releases for builds 2023-05-31 17:22:12 +01:00
Zomatree 175866ce00 Merge remote-tracking branch 'revoltchat/master' into webhooks 2023-04-25 20:38:09 +01:00
Zomatree 8c5de18991 restrict webhooks to text and group dm channels 2023-04-25 20:30:16 +01:00
Zomatree 57f00a283b make request function names the same as file name 2023-04-20 17:52:10 +01:00
Zomatree 312e1dd41f update code to be inline with reviews 2023-04-20 17:45:48 +01:00
Zomatree 426d836a09 Merge remote-tracking branch 'revoltchat/master' into webhooks 2023-04-18 19:50:04 +01:00
Zomatree 2fad66d24a only send a subsect of the webhook in a message 2023-04-18 19:39:34 +01:00
Zomatree 83429f9f65 fix broken merge conflict 2023-04-01 23:09:29 +01:00
Angelo Kontaxis a60ae78176 Merge branch 'master' into webhooks 2023-04-01 22:52:11 +01:00
Zomatree 7b39f4e9d9 change how webhook information is in the webhook 2023-04-01 22:49:34 +01:00
Zomatree 01577fd798 avoid leaking the token in events 2023-03-11 23:24:10 +00:00
Zomatree fca91e1560 update upstream code 2023-03-01 11:49:28 +00:00
Angelo Kontaxis 3c881a40a6 Merge branch 'master' into webhooks 2023-02-23 17:26:59 +00:00
Zomatree c7849a82bf Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-27 17:27:32 +00:00
Zomatree 781d51df6f add seperate non token webhook routes 2023-01-27 17:26:32 +00:00
Zomatree 758b1e43e7 no longer make mesage_send.rs public 2023-01-24 22:54:09 +00:00
Zomatree 5cc8231c36 deduplicate message sending 2023-01-24 22:52:18 +00:00
Zomatree 7b447faefa add pull request events 2023-01-21 22:57:12 +00:00
Zomatree a017256e95 add more events 2023-01-20 20:15:12 +00:00
Zomatree 1050891d87 Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-20 20:04:18 +00:00
Zomatree a246cff7f0 Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-18 21:03:15 +00:00
Zomatree c218db5047 add push event 2022-12-30 22:44:36 +00:00
Zomatree c7f2f3614f star event 2022-12-30 18:08:43 +00:00
Zomatree c826d8cf07 add github webhook support 2022-11-27 06:52:44 +00:00
Zomatree 5cb2320760 inital webhook support 2022-11-26 23:07:22 +00:00
243 changed files with 16784 additions and 2971 deletions
+2
View File
@@ -0,0 +1,2 @@
[profile.default]
slow-timeout = { period = "3s", terminate-after = 2 }
+3
View File
@@ -59,6 +59,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
+20 -13
View File
@@ -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,25 +35,27 @@ 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: Run cargo test
env:
TEST_DB: REFERENCE
run: |
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 - name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'master' if: github.event_name != 'pull_request' && github.ref_name == 'master'
env:
TEST_DB: REFERENCE
run: | run: |
cargo run --bin revolt-delta & cargo run --bin revolt-delta &
+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
+1313 -198
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -1,6 +1,10 @@
[workspace] [workspace]
resolver = "2"
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"] members = ["crates/delta", "crates/bonfire", "crates/quark", "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" }
+2 -1
View File
@@ -1,5 +1,5 @@
# Build Stage # Build Stage
FROM --platform="${BUILDPLATFORM}" rustlang/rust:nightly-slim FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
USER 0:0 USER 0:0
WORKDIR /home/rust/src WORKDIR /home/rust/src
@@ -20,6 +20,7 @@ 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/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/
+29
View File
@@ -0,0 +1,29 @@
# 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/quark/Cargo.toml ./crates/quark/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/
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
+4
View File
@@ -21,6 +21,10 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers) - [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing) - [Contribution Guide](https://developers.revolt.chat/contributing)
## Minimum Supported Rust Version
Rust 1.70 or higher.
## Contributing ## Contributing
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing). The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
+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)]
+20 -4
View File
@@ -1,12 +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::update() # Prefer to use Object::create()
"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)
"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() # 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",
] ]
+12 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.5.20" version = "0.6.11"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -18,6 +18,7 @@ querystring = "1.1.0"
revolt-quark = { path = "../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 +26,15 @@ 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-presence = { path = "../core/presence", features = ["redis-is-patched"] }
sentry = "0.31.5"
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }
+5 -1
View File
@@ -29,6 +29,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:?}");
});
} }
} }
+238 -223
View File
@@ -1,6 +1,16 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt}; use async_tungstenite::WebSocketStream;
use fred::{
interfaces::{ClientLike, EventInterface, PubsubInterface},
types::RedisConfig,
};
use futures::{
channel::oneshot,
pin_mut, select,
stream::{SplitSink, SplitStream},
FutureExt, SinkExt, StreamExt, TryStreamExt,
};
use revolt_presence::{create_session, delete_session}; use revolt_presence::{create_session, delete_session};
use revolt_quark::{ use revolt_quark::{
events::{ events::{
@@ -9,244 +19,249 @@ use revolt_quark::{
state::{State, SubscriptionStateChange}, state::{State, SubscriptionStateChange},
}, },
models::{user::UserHint, User}, models::{user::UserHint, User},
redis_kiss, Database, redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI},
Database,
}; };
use async_std::{net::TcpStream, sync::Mutex, task}; use async_std::{net::TcpStream, sync::Mutex};
use crate::config::WebsocketHandshakeCallback; use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
/// 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(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| {
info!("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
&& write
.lock()
.await
.send(config.encode(&event))
.await
.is_err()
{
return;
}
}
}
async fn worker(
user_id: String,
config: &ProtocolConfiguration,
mut read: WsReader,
write: &Mutex<WsWriter>,
) {
while let Ok(Some(msg)) = read.try_next().await {
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();
}
}
_ => {}
}
}
} }
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "revolt-config"
version = "0.6.11"
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
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 }
+3
View File
@@ -0,0 +1,3 @@
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"
+66
View File
@@ -0,0 +1,66 @@
[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 = ""
[api.security.captcha]
hcaptcha_key = ""
hcaptcha_sitekey = ""
[api.workers]
max_concurrent_connections = 50
[features]
[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
+169
View File
@@ -0,0 +1,169 @@
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;
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,
}
#[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,
}
#[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,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Settings {
pub database: Database,
pub hosts: Hosts,
pub api: Api,
pub features: Features,
}
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()
}
#[cfg(feature = "test")]
#[cfg(test)]
mod tests {
use crate::init;
#[async_std::test]
async fn it_works() {
init().await;
}
}
+43 -10
View File
@@ -1,34 +1,51 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.0.2" version = "0.6.11"
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"]
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.0.2", path = "../result" } revolt-config = { version = "0.6.11", path = "../config" }
revolt-permissions = { version = "0.0.2", path = "../permissions", features = [ "serde" ] } revolt-result = { version = "0.6.11", path = "../result" }
revolt-models = { version = "0.6.11", path = "../models" }
revolt-presence = { version = "0.6.11", path = "../presence" }
revolt-permissions = { version = "0.6.11", path = "../permissions", features = [
"serde",
"bson",
] }
# Utility # Utility
log = "0.4" log = "0.4"
lru = "0.11.0"
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" }
isahc = { optional = true, version = "1.7", features = ["json"] }
# Serialisation # Serialisation
serde_json = "1" serde_json = "1"
@@ -36,12 +53,20 @@ revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] } iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
# Events
redis-kiss = { version = "0.1.4" }
# Database # Database
bson = { optional = true, version = "2.1.0" } bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "2.1.0", default-features = false } mongodb = { optional = true, version = "2.1.0", default-features = false }
# Database Migration
unicode-segmentation = "1.10.1"
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"
@@ -50,7 +75,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,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 -10
View File
@@ -2,27 +2,30 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex; use futures::lock::Mutex;
use crate::{AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings}; use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, 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 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 server_bans: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>, pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: 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, ()>>>,
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
} }
); );
+264
View File
@@ -0,0 +1,264 @@
use authifier::AuthifierEvent;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, User,
UserSettings, Webhook,
};
use revolt_result::Error;
use crate::Database;
/// WebSocket Client Errors
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "error")]
pub enum WebSocketError {
LabelMe,
InternalError { at: String },
InvalidSession,
OnboardingNotFinished,
AlreadyAuthenticated,
MalformedData { msg: String },
}
/// Ping Packet
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Ping {
Binary(Vec<u8>),
Number(usize),
}
/// Untagged Error
#[derive(Serialize)]
#[serde(untagged)]
pub enum ErrorEvent {
Error(WebSocketError),
APIError(Error),
}
/// Protocol Events
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum EventV1 {
/// Multiple events
Bulk { v: Vec<EventV1> },
/// Successfully authenticated
Authenticated,
/* /// Basic data to cache
Ready {
users: Vec<User>,
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
emojis: Option<Vec<Emoji>>,
},
/// Ping response
Pong { data: Ping }, */
/// New message
Message(Message),
/// Update existing message
MessageUpdate {
id: String,
channel: String,
data: PartialMessage,
},
/// Append information to existing message
MessageAppend {
id: String,
channel: String,
append: AppendMessage,
},
/// Delete message
MessageDelete { id: String, channel: String },
/// New reaction to a message
MessageReact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove user's reaction from message
MessageUnreact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove a reaction from message
MessageRemoveReaction {
id: String,
channel_id: String,
emoji_id: String,
},
/// Bulk delete messages
BulkMessageDelete { channel: String, ids: Vec<String> },
/// New server
ServerCreate {
id: String,
server: Server,
channels: Vec<Channel>,
emojis: Vec<Emoji>,
},
/// Update existing server
ServerUpdate {
id: String,
data: PartialServer,
clear: Vec<FieldsServer>,
},
/// Delete server
ServerDelete { id: String },
/// Update existing server member
ServerMemberUpdate {
id: MemberCompositeKey,
data: PartialMember,
clear: Vec<FieldsMember>,
},
/// User joins server
ServerMemberJoin { id: String, user: String },
/// User left server
ServerMemberLeave { id: String, user: String },
/// Server role created or updated
ServerRoleUpdate {
id: String,
role_id: String,
data: PartialRole,
clear: Vec<FieldsRole>,
},
/// Server role deleted
ServerRoleDelete { id: String, role_id: String },
/// Update existing user
UserUpdate {
id: String,
data: PartialUser,
clear: Vec<FieldsUser>,
event_id: Option<String>,
},
/// Relationship with another user changed
UserRelationship { id: String, user: User },
/// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings },
/// User has been platform banned or deleted their account
///
/// Clients should remove the following associated data:
/// - Messages
/// - DM Channels
/// - Relationships
/// - Server Memberships
///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 },
/// New emoji
EmojiCreate(Emoji),
/// Delete emoji
EmojiDelete { id: String },
/*/// New report
ReportCreate(Report), */
/// New channel
ChannelCreate(Channel),
/// Update existing channel
ChannelUpdate {
id: String,
data: PartialChannel,
clear: Vec<FieldsChannel>,
},
/// Delete channel
ChannelDelete { id: String },
/// User joins a group
ChannelGroupJoin { id: String, user: String },
/// User leaves a group
ChannelGroupLeave { id: String, user: String },
/// User started typing in a channel
ChannelStartTyping { id: String, user: String },
/// User stopped typing in a channel
ChannelStopTyping { id: String, user: String },
/// User acknowledged message in channel
ChannelAck {
id: String,
user: String,
message_id: String,
},
/// New webhook
WebhookCreate(Webhook),
/// Update existing webhook
WebhookUpdate {
id: String,
data: PartialWebhook,
remove: Vec<FieldsWebhook>,
},
/// Delete webhook
WebhookDelete { id: String },
/// Auth events
Auth(AuthifierEvent),
}
impl EventV1 {
/// Publish helper wrapper
pub async fn p(self, channel: String) {
#[cfg(not(debug_assertions))]
redis_kiss::p(channel, self).await;
#[cfg(debug_assertions)]
info!("Publishing event to {channel}: {self:?}");
#[cfg(debug_assertions)]
redis_kiss::publish(channel, self).await.unwrap();
}
/// Publish user event
pub async fn p_user(self, id: String, db: &Database) {
self.clone().p(id.clone()).await;
// TODO: this should be captured by member list in the future and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members {
self.clone().p(member.id.server).await;
}
}
}
/// Publish private event
pub async fn private(self, id: String) {
self.p(format!("{id}!")).await;
}
/// Publish internal global event
pub async fn global(self) {
self.p("global".to_string()).await;
}
}
+1
View File
@@ -0,0 +1 @@
pub mod client;
+4 -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]
@@ -80,6 +80,9 @@ mod models;
pub mod util; pub mod util;
pub use models::*; pub use models::*;
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 {
!t !t
@@ -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();
}); });
} }
} }
@@ -44,6 +44,10 @@ pub async fn create_database(db: &MongoDb) {
.await .await
.expect("Failed to create channel_unreads collection."); .expect("Failed to create channel_unreads collection.");
db.create_collection("channel_webhooks", None)
.await
.expect("Failed to create channel_webhooks collection.");
db.create_collection("migrations", None) db.create_collection("migrations", None)
.await .await
.expect("Failed to create migrations collection."); .expect("Failed to create migrations collection.");
@@ -72,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()
@@ -91,6 +99,18 @@ pub async fn create_database(db: &MongoDb) {
"username": 1_i32 "username": 1_i32
}, },
"name": "username", "name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true, "unique": true,
"collation": { "collation": {
"locale": "en", "locale": "en",
@@ -193,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.");
} }
@@ -1,14 +1,17 @@
use std::{ops::BitXor, time::Duration}; use std::{collections::HashSet, ops::BitXor, time::Duration};
use crate::{ use crate::{
mongodb::{ mongodb::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document}, bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions, options::FindOptions,
}, },
MongoDb, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
}; };
use futures::StreamExt; use futures::StreamExt;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct MigrationInfo { struct MigrationInfo {
@@ -16,7 +19,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 22; 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");
@@ -694,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)
@@ -705,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 {
@@ -751,8 +743,248 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.unwrap(); .unwrap();
} }
if revision <= 22 {
info!("Running migration [revision 22 / 31-05-2023]: Add moderator_id to account strikes.");
db.col::<Document>("safety_strikes")
.update_many(
doc! {},
doc! {
"$set": {
"moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4"
}
},
None,
)
.await
.expect("Failed to update server members.");
}
if revision <= 23 {
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
db.db()
.run_command(
doc! {
"dropIndexes": "users",
"index": "username"
},
None,
)
.await
.expect("Failed to drop existing username index.");
#[derive(Serialize, Deserialize)]
struct UserInformation {
#[serde(rename = "_id")]
id: String,
username: String,
}
let re_username = regex::Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap();
let users: Vec<UserInformation> = db
.col::<UserInformation>("users")
.find(doc! {}, None)
.await
.unwrap()
.map(|doc| doc.expect("id and username"))
.collect()
.await;
let search_space: Vec<String> = DISCRIMINATOR_SEARCH_SPACE.iter().cloned().collect();
let mut claimed: HashSet<String> = HashSet::new();
for i in 0..users.len() {
let info = &users[i];
let mut discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
if re_username.is_match(&info.username) {
while claimed.contains(&format!("{}#{}", info.username, discriminator)) {
let new_discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
info!(
"Re-rolled {} to {new_discriminator} from {discriminator}",
info.username
);
discriminator = new_discriminator;
}
claimed.insert(format!("{}#{}", info.username, discriminator));
info!(
"({}/{}) Migrating user \"{}\" to #{} - compliant",
i + 1,
users.len(),
info.username,
discriminator
);
db.col::<UserInformation>("users")
.update_one(
doc! {
"_id": &info.id
},
doc! {
"$set": {
"discriminator": discriminator
}
},
None,
)
.await
.unwrap();
} else {
let mut sanitised = info
.username
.graphemes(true)
.filter(|s| re_username.is_match(s))
.collect::<String>();
while sanitised.len() < 2 {
sanitised += "_";
}
while claimed.contains(&format!("{}#{}", sanitised, discriminator)) {
let new_discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
info!("Re-rolled {sanitised} to {new_discriminator} from {discriminator}");
discriminator = new_discriminator;
}
claimed.insert(format!("{}#{}", sanitised, discriminator));
info!(
"({}/{}) Migrating user \"{}\" to #{} - sanitised: \"{}\"",
i + 1,
users.len(),
info.username,
discriminator,
sanitised
);
db.col::<UserInformation>("users")
.update_one(
doc! {
"_id": &info.id
},
doc! {
"$set": {
"username": sanitised,
"discriminator": discriminator,
"display_name": &info.username
}
},
None,
)
.await
.unwrap();
}
}
}
if revision <= 24 {
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
db.db()
.create_collection("channel_webhooks", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
},
None,
)
.await
.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: 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))
}); });
} }
} }
@@ -0,0 +1,101 @@
use revolt_result::{create_error, Result};
use crate::Database;
/* static ALPHABET: [char; 54] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
]; */
auto_derived!(
/// Invite
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 {
/// Get the invite code for this invite
pub fn code(&'_ self) -> &'_ str {
match self {
Invite::Server { code, .. } | Invite::Group { code, .. } => code,
}
}
/// Get the ID of the user who created this invite
pub fn creator(&'_ self) -> &'_ str {
match self {
Invite::Server { creator, .. } | Invite::Group { creator, .. } => creator,
}
}
/// Create a new invite from given information
/*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> {
let code = nanoid::nanoid!(8, &ALPHABET);
let invite = match &target {
Channel::Group { id, .. } => Ok(Invite::Group {
code,
creator: creator.id.clone(),
channel: id.clone(),
}),
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
Ok(Invite::Server {
code,
creator: creator.id.clone(),
server: server.clone(),
channel: id.clone(),
})
}
_ => Err(Error::InvalidOperation),
}?;
db.insert_invite(&invite).await?;
Ok(invite)
}*/
/// Resolve an invite by its ID or by a public server ID
pub async fn find(db: &Database, code: &str) -> Result<Invite> {
if let Ok(invite) = db.fetch_invite(code).await {
return Ok(invite);
} else if let Ok(server) = db.fetch_server(code).await {
if server.discoverable {
if let Some(channel) = server.channels.into_iter().next() {
return Ok(Invite::Server {
code: code.to_string(),
server: server.id,
creator: server.owner,
channel,
});
}
}
}
Err(create_error!(NotFound))
}
}
@@ -0,0 +1,21 @@
use revolt_result::Result;
use crate::Invite;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannelInvites: Sync + Send {
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> 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_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>>;
}
@@ -0,0 +1,119 @@
use bson::Document;
use mongodb::options::UpdateOptions;
use revolt_result::Result;
use ulid::Ulid;
use crate::ChannelUnread;
use crate::MongoDb;
use super::AbstractChannelUnreads;
static COL: &str = "channel_unreads";
#[async_trait]
impl AbstractChannelUnreads for MongoDb {
/// Acknowledge a message.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$unset": {
"mentions": 1_i32
},
"$set": {
"last_id": message_id
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Acknowledge many channels.
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL)
.delete_many(
doc! {
"_id.channel": {
"$in": channel_ids
},
"_id.user": user_id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", COL))?;
self.col::<Document>(COL)
.insert_many(
channel_ids
.iter()
.map(|channel_id| {
doc! {
"_id": {
"channel": channel_id,
"user": user_id
},
"last_id": &current_time
}
})
.collect::<Vec<Document>>(),
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
}
/// Add a mention.
async fn add_mention_to_unread<'a>(
&self,
channel_id: &str,
user_id: &str,
message_ids: &[String],
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$push": {
"mentions": {
"$each": message_ids
}
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query!(
self,
find,
COL,
doc! {
"_id.user": user_id
}
)
}
}
@@ -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())
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,175 @@
use revolt_result::Result;
use crate::events::client::EventV1;
use crate::{Database, File};
auto_derived_partial!(
/// Webhook
pub struct Webhook {
/// Webhook Id
#[serde(rename = "_id")]
pub id: String,
/// The name of the webhook
pub name: String,
/// The avatar of the webhook
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
/// The channel this webhook belongs to
pub channel_id: String,
/// The permissions of the webhook
pub permissions: u64,
/// The private token for the webhook
pub token: Option<String>,
},
"PartialWebhook"
);
auto_derived!(
/// Optional fields on webhook object
pub enum FieldsWebhook {
Avatar,
}
);
#[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)]
impl Webhook {
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_webhook(self).await?;
// Avoid leaking the token to people who receive the event
let mut webhook = self.clone();
webhook.token = None;
EventV1::WebhookCreate(webhook.into())
.p(self.channel_id.clone())
.await;
Ok(())
}
pub fn assert_token(&self, token: &str) -> Result<()> {
if self.token.as_deref() == Some(token) {
Ok(())
} else {
Err(create_error!(InvalidCredentials))
}
}
pub async fn update(
&mut self,
db: &Database,
mut partial: PartialWebhook,
remove: Vec<FieldsWebhook>,
) -> Result<()> {
for field in &remove {
self.remove_field(field)
}
self.apply_options(partial.clone());
db.update_webhook(&self.id, &partial, &remove).await?;
partial.token = None; // Avoid leaking the token to people who receive the event
EventV1::WebhookUpdate {
id: self.id.clone(),
data: partial.into(),
remove: remove.into_iter().map(|v| v.into()).collect(),
}
.p(self.channel_id.clone())
.await;
Ok(())
}
pub fn remove_field(&mut self, field: &FieldsWebhook) {
match field {
FieldsWebhook::Avatar => self.avatar = None,
}
}
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_webhook(&self.id).await?;
EventV1::WebhookDelete {
id: self.id.clone(),
}
.p(self.channel_id.clone())
.await;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{FieldsWebhook, PartialWebhook, Webhook};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let webhook_id = "webhook";
let channel_id = "channel";
let webhook = Webhook {
id: webhook_id.to_string(),
name: "Webhook Name".to_string(),
channel_id: channel_id.to_string(),
avatar: None,
..Default::default()
};
db.insert_webhook(&webhook).await.unwrap();
let mut updated_webhook = webhook.clone();
updated_webhook
.update(
&db,
PartialWebhook {
name: Some("New Name".to_string()),
..Default::default()
},
vec![FieldsWebhook::Avatar],
)
.await
.unwrap();
let fetched_webhook = db.fetch_webhook(webhook_id).await.unwrap();
let fetched_webhooks = db.fetch_webhooks_for_channel(channel_id).await.unwrap();
assert_eq!(updated_webhook, fetched_webhook);
assert_ne!(webhook, fetched_webhook);
assert_eq!(1, fetched_webhooks.len());
assert_eq!(fetched_webhook, fetched_webhooks[0]);
webhook.delete(&db).await.unwrap();
assert!(db.fetch_webhook(webhook_id).await.is_err());
assert_eq!(
0,
db.fetch_webhooks_for_channel(channel_id)
.await
.unwrap()
.len()
)
});
}
}
@@ -0,0 +1,29 @@
use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractWebhooks: Sync + Send {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>;
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook>;
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>>;
/// Update webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()>;
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()>;
}
@@ -0,0 +1,77 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractWebhooks;
static COL: &str = "channel_webhooks";
#[async_trait]
impl AbstractWebhooks for MongoDb {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
query!(self, insert_one, COL, &webhook).map(|_| ())
}
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
query!(self, find_one_by_id, COL, webhook_id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
Ok(self
.col::<Webhook>(COL)
.find(
doc! {
"channel_id": channel_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 webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
webhook_id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, webhook_id).map(|_| ())
}
}
impl IntoDocumentPath for FieldsWebhook {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsWebhook::Avatar => "avatar",
})
}
}
@@ -0,0 +1,70 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
use super::AbstractWebhooks;
#[async_trait]
impl AbstractWebhooks for ReferenceDb {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if webhooks.contains_key(&webhook.id) {
Err(create_database_error!("insert", "webhook"))
} else {
webhooks.insert(webhook.id.to_string(), webhook.clone());
Ok(())
}
}
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
let webhooks = self.channel_webhooks.lock().await;
webhooks
.get(webhook_id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
let webhooks = self.channel_webhooks.lock().await;
Ok(webhooks
.values()
.filter(|webhook| webhook.channel_id == channel_id)
.cloned()
.collect())
}
/// Update webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if let Some(webhook) = webhooks.get_mut(webhook_id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
webhook.remove_field(field);
}
webhook.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if webhooks.remove(webhook_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,723 @@
use std::collections::HashMap;
use revolt_config::config;
use revolt_models::v0::{self, MessageAuthor};
use revolt_permissions::OverrideField;
use revolt_result::Result;
use serde::{Deserialize, Serialize};
use crate::{
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server,
SystemMessage, User,
};
auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages
SavedMessages {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the user this channel belongs to
user: String,
},
/// Direct message channel between two users
DirectMessage {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Whether this direct message channel is currently open on both sides
active: bool,
/// 2-tuple of user ids participating in direct message
recipients: Vec<String>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
},
/// Group channel between 1 or more participants
Group {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Display name of the channel
name: String,
/// User id of the owner of the group
owner: String,
/// Channel description
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Array of user ids participating in channel
recipients: Vec<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
/// Permissions assigned to members of this group
/// (does not apply to the owner of the group)
#[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<i64>,
/// Whether this group is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
/// Text channel belonging to a server
TextChannel {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Display name of the channel
name: String,
/// Channel description
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
/// Default permissions assigned to users in this channel
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
/// Voice channel belonging to a server
VoiceChannel {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Display name of the channel
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
/// Channel description
description: Option<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Default permissions assigned to users in this channel
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
}
);
auto_derived!(
#[derive(Default)]
pub struct PartialChannel {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role_permissions: Option<HashMap<String, OverrideField>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_permissions: Option<OverrideField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_message_id: Option<String>,
}
/// Optional fields on channel object
pub enum FieldsChannel {
Description,
Icon,
DefaultPermissions,
}
);
#[allow(clippy::disallowed_methods)]
impl Channel {
/* /// Create a channel
pub async fn create(&self, db: &Database) -> Result<()> {
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(())
}*/
/// 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)
}
/// Add user to a group
pub async fn add_user_to_group(
&mut self,
db: &Database,
user: &User,
by_id: &str,
) -> Result<()> {
if let Channel::Group { recipients, .. } = self {
if recipients.contains(&String::from(&user.id)) {
return Err(create_error!(AlreadyInGroup));
}
recipients.push(String::from(&user.id));
}
match &self {
Channel::Group { id, .. } => {
db.add_user_to_group(id, &user.id).await?;
EventV1::ChannelGroupJoin {
id: id.to_string(),
user: user.id.to_string(),
}
.p(id.to_string())
.await;
SystemMessage::UserAdded {
id: user.id.to_string(),
by: by_id.to_string(),
}
.into_message(id.to_string())
.send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await
.ok();
EventV1::ChannelCreate(self.clone().into())
.private(user.id.to_string())
.await;
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Map out whether it is a direct DM
pub fn is_direct_dm(&self) -> bool {
matches!(self, Channel::DirectMessage { .. })
}
/// Check whether has a user as a recipient
pub fn contains_user(&self, user_id: &str) -> bool {
match self {
Channel::Group { recipients, .. } => recipients.contains(&String::from(user_id)),
_ => false,
}
}
/// Get list of recipients
pub fn users(&self) -> Result<Vec<String>> {
match self {
Channel::Group { recipients, .. } => Ok(recipients.to_owned()),
_ => Err(create_error!(NotFound)),
}
}
/// Clone this channel's id
pub fn id(&self) -> String {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id.clone(),
}
}
/// Set role permission on a channel
pub async fn set_role_permission(
&mut self,
db: &Database,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
match self {
Channel::TextChannel {
id,
server,
role_permissions,
..
}
| Channel::VoiceChannel {
id,
server,
role_permissions,
..
} => {
db.set_channel_role_permission(id, role_id, permissions)
.await?;
role_permissions.insert(role_id.to_string(), permissions);
EventV1::ChannelUpdate {
id: id.clone(),
data: PartialChannel {
role_permissions: Some(role_permissions.clone()),
..Default::default()
}
.into(),
clear: vec![],
}
.p(server.clone())
.await;
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Update channel data
pub async fn update(
&mut self,
db: &Database,
partial: PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
let id = self.id().to_string();
db.update_channel(&id, &partial, remove.clone()).await?;
EventV1::ChannelUpdate {
id: id.clone(),
data: partial.into(),
clear: remove.into_iter().map(|v| v.into()).collect(),
}
.p(match self {
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
_ => id,
})
.await;
Ok(())
}
/// Remove a field from Channel object
pub fn remove_field(&mut self, field: &FieldsChannel) {
match field {
FieldsChannel::Description => match self {
Self::Group { description, .. }
| Self::TextChannel { description, .. }
| Self::VoiceChannel { description, .. } => {
description.take();
}
_ => {}
},
FieldsChannel::Icon => match self {
Self::Group { icon, .. }
| Self::TextChannel { icon, .. }
| Self::VoiceChannel { icon, .. } => {
icon.take();
}
_ => {}
},
FieldsChannel::DefaultPermissions => match self {
Self::TextChannel {
default_permissions,
..
}
| Self::VoiceChannel {
default_permissions,
..
} => {
default_permissions.take();
}
_ => {}
},
}
}
/// Remove multiple fields from Channel object
pub fn remove_fields(&mut self, partial: Vec<FieldsChannel>) {
for field in partial {
self.remove_field(&field)
}
}
/// Apply partial channel to channel
pub fn apply_options(&mut self, partial: PartialChannel) {
match self {
Self::SavedMessages { .. } => {}
Self::DirectMessage { active, .. } => {
if let Some(v) = partial.active {
*active = v;
}
}
Self::Group {
name,
owner,
description,
icon,
nsfw,
permissions,
..
} => {
if let Some(v) = partial.name {
*name = v;
}
if let Some(v) = partial.owner {
*owner = v;
}
if let Some(v) = partial.description {
description.replace(v);
}
if let Some(v) = partial.icon {
icon.replace(v);
}
if let Some(v) = partial.nsfw {
*nsfw = v;
}
if let Some(v) = partial.permissions {
permissions.replace(v);
}
}
Self::TextChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
..
}
| Self::VoiceChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
..
} => {
if let Some(v) = partial.name {
*name = v;
}
if let Some(v) = partial.description {
description.replace(v);
}
if let Some(v) = partial.icon {
icon.replace(v);
}
if let Some(v) = partial.nsfw {
*nsfw = v;
}
if let Some(v) = partial.role_permissions {
*role_permissions = v;
}
if let Some(v) = partial.default_permissions {
default_permissions.replace(v);
}
}
}
}
/// Remove user from a group
pub async fn remove_user_from_group(
&self,
db: &Database,
user: &User,
by_id: Option<&str>,
silent: bool,
) -> Result<()> {
match &self {
Channel::Group {
id,
name,
owner,
recipients,
..
} => {
if &user.id == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
db.update_channel(
id,
&PartialChannel {
owner: Some(new_owner.into()),
..Default::default()
},
vec![],
)
.await?;
SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(),
to: new_owner.to_string(),
}
.into_message(id.to_string())
.send(
db,
MessageAuthor::System {
username: name,
avatar: None,
},
self,
false,
)
.await
.ok();
} else {
db.delete_channel(self).await?;
return Ok(());
}
}
EventV1::ChannelGroupLeave {
id: id.to_string(),
user: user.id.to_string(),
}
.p(id.to_string())
.await;
if !silent {
if let Some(by) = by_id {
SystemMessage::UserRemove {
id: user.id.to_string(),
by: by.to_string(),
}
} else {
SystemMessage::UserLeft {
id: user.id.to_string(),
}
}
.into_message(id.to_string())
.send(
db,
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
self,
false,
)
.await
.ok();
}
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Delete a channel
pub async fn delete(&self, db: &Database) -> Result<()> {
let id = self.id().to_string();
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
db.delete_channel(self).await
}
}
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsChannel::Description => "description",
FieldsChannel::Icon => "icon",
FieldsChannel::DefaultPermissions => "default_permissions",
})
}
}
#[cfg(test)]
mod tests {
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use crate::{fixture, util::permissions::DatabasePermissionQuery};
#[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));
});
}
}
@@ -0,0 +1,50 @@
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannels: Sync + Send {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()>;
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel>;
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>>;
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>>;
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel>;
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel>;
/// Insert a user to a group
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel_id: &str,
role_id: &str,
permissions: OverrideField,
) -> Result<()>;
// Update channel
async fn update_channel(
&self,
id: &str,
channel_id: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()>;
// Remove a user from a group
async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
// Delete a channel
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
}
@@ -0,0 +1,193 @@
use super::AbstractChannels;
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::Document;
use futures::StreamExt;
use revolt_permissions::OverrideField;
use revolt_result::Result;
static COL: &str = "channels";
#[async_trait]
impl AbstractChannels for MongoDb {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
query!(self, insert_one, COL, &channel).map(|_| ())
}
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
query!(self, find_one_by_id, COL, channel_id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
Ok(self
.col::<Channel>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("fetch", "channels"))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
query!(
self,
find,
COL,
doc! {
"$or": [
{
"$or": [
{
"channel_type": "DirectMessage"
},
{
"channel_type": "Group"
}
],
"recipients": user_id
},
{
"channel_type": "SavedMessages",
"user": user_id
}
]
}
)
}
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
query!(
self,
find_one,
COL,
doc! {
"channel_type": "SavedMessages",
"user": user_id
}
)?
.ok_or_else(|| create_error!(InternalError))
}
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
let doc = match (user_a, user_b) {
self_user if self_user.0 == self_user.1 => {
doc! {
"channel_type": "SavedMessages",
"user": self_user.0
}
}
users => {
doc! {
"channel_type": "DirectMessage",
"recipients": {
"$all": [ users.0, users.1 ]
}
}
}
};
query!(self, find_one, COL, doc)?.ok_or_else(|| create_error!(NotFound))
}
/// Insert a user to a group
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$push": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channel"))
}
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel: &str,
role: &str,
permissions: OverrideField,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! { "_id": channel },
doc! {
"$set": {
"role_permissions.".to_owned() + role: permissions
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channel"))
}
// Update channel
async fn update_channel(
&self,
id: &str,
channel: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
channel,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
// Remove a user from a group
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$pull": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channels"))
}
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
}
}
@@ -0,0 +1,157 @@
use std::collections::hash_map::Entry;
use super::AbstractChannels;
use crate::ReferenceDb;
use crate::{Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
use revolt_result::Result;
#[async_trait]
impl AbstractChannels for ReferenceDb {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
entry.insert(channel.clone());
Ok(())
} else {
Err(create_database_error!("insert", "channel"))
}
}
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
channels
.get(channel_id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
let channels = self.channels.lock().await;
ids.iter()
.map(|id| {
channels
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
}
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
let channels = self.channels.lock().await;
Ok(channels
.values()
.filter(|channel| channel.contains_user(user_id))
.cloned()
.collect())
}
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
channels
.get(user_id)
.cloned()
.ok_or_else(|| create_database_error!("fetch", "channel"))
}
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
for (_, data) in channels.iter() {
if data.contains_user(user_a) && data.contains_user(user_b) {
return Ok(data.to_owned());
}
}
Err(create_error!(NotFound))
}
/// Insert a user to a group
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(channel_id) {
recipients.push(String::from(user_id));
Ok(())
} else {
Err(create_error!(InvalidOperation))
}
}
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel_id: &str,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(mut channel) = channels.get_mut(channel_id) {
match &mut channel {
Channel::TextChannel {
role_permissions, ..
}
| Channel::VoiceChannel {
role_permissions, ..
} => {
if role_permissions.get(role_id).is_some() {
role_permissions.remove(role_id);
role_permissions.insert(String::from(role_id), permissions);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
_ => Err(create_error!(NotFound)),
}
} else {
Err(create_error!(NotFound))
}
}
// Update channel
async fn update_channel(
&self,
id: &str,
channel: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(channel_data) = channels.get_mut(id) {
channel_data.apply_options(channel.to_owned());
channel_data.remove_fields(remove);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
// Remove a user from a group
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(channel_data) = channels.get_mut(channel) {
if channel_data.users()?.contains(&String::from(user)) {
channel_data.users()?.retain(|x| x != user);
return Ok(());
} else {
return Err(create_error!(NotFound));
}
}
Err(create_error!(NotFound))
}
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if channels.remove(&channel.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,87 @@
use std::collections::HashSet;
use std::str::FromStr;
use once_cell::sync::Lazy;
use revolt_result::Result;
use ulid::Ulid;
use crate::events::client::EventV1;
use crate::Database;
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
include_str!("unicode_emoji.txt")
.split('\n')
.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 {
/// Get parent id
fn parent(&self) -> &str {
match &self.parent {
EmojiParent::Server { id } => id,
EmojiParent::Detached => "",
}
}
/// Create an emoji
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_emoji(self).await?;
EventV1::EmojiCreate(self.clone().into())
.p(self.parent().to_string())
.await;
Ok(())
}
/// Delete an emoji
pub async fn delete(self, db: &Database) -> Result<()> {
EventV1::EmojiDelete {
id: self.id.to_string(),
}
.p(self.parent().to_string())
.await;
db.detach_emoji(&self).await
}
/// Check whether we can use a given emoji
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
if Ulid::from_str(emoji).is_ok() {
db.fetch_emoji(emoji).await?;
Ok(true)
} else {
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
}
}
}
@@ -0,0 +1,24 @@
use revolt_result::Result;
use crate::Emoji;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractEmojis: Sync + Send {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji>;
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>>;
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
}
@@ -0,0 +1,69 @@
use bson::Document;
use revolt_result::Result;
use crate::Emoji;
use crate::MongoDb;
use super::AbstractEmojis;
static COL: &str = "emojis";
#[async_trait]
impl AbstractEmojis for MongoDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
query!(self, insert_one, COL, &emoji).map(|_| ())
}
/// 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>> {
query!(
self,
find,
COL,
doc! {
"parent.id": parent_id
}
)
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
query!(
self,
find,
COL,
doc! {
"parent.id": {
"$in": parent_ids
}
}
)
}
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": &emoji.id
},
doc! {
"$set": {
"parent": {
"type": "Detached"
}
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
}
@@ -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))
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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 {
@@ -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,575 @@
use std::collections::HashSet;
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use revolt_config::config;
use revolt_models::v0::{
self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification,
ReplyIntent, SendableEmbed, RE_MENTION,
};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::Result;
use ulid::Ulid;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::idempotency::IdempotencyKey,
Channel, Database, Emoji, File,
};
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
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(())
}
/// 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(())
}
/// 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))
}
}
}
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()
}
}
@@ -0,0 +1,39 @@
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
mod mongodb;
mod reference;
#[async_trait]
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
async fn fetch_message(&self, id: &str) -> Result<Message>;
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
/// Append information to a given message
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove reaction from a message
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<()>;
}
@@ -0,0 +1,280 @@
use bson::{to_bson, Document};
use futures::try_join;
use mongodb::options::FindOptions;
use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage};
use super::AbstractMessages;
static COL: &str = "messages";
#[async_trait]
impl AbstractMessages for MongoDb {
/// Insert a new message into the database
async fn insert_message(&self, message: &Message) -> Result<()> {
query!(self, insert_one, COL, &message).map(|_| ())
}
/// Fetch a message by its id
async fn fetch_message(&self, id: &str) -> Result<Message> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch multiple messages by given query
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
let mut filter = doc! {};
// 1. Apply message filters
if let Some(channel) = query.filter.channel {
filter.insert("channel", channel);
}
if let Some(author) = query.filter.author {
filter.insert("author", author);
}
let is_search_query = if let Some(query) = query.filter.query {
filter.insert(
"$text",
doc! {
"$search": query
},
);
true
} else {
false
};
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
// 3. Apply message time period
match query.time_period {
MessageTimePeriod::Relative { nearby } => {
// 3.1. Prepare filters
let mut older_message_filter = filter.clone();
let mut newer_message_filter = filter;
older_message_filter.insert(
"_id",
doc! {
"$lt": &nearby
},
);
newer_message_filter.insert(
"_id",
doc! {
"$gte": &nearby
},
);
// 3.2. Execute in both directions
let (a, b) = try_join!(
self.find_with_options::<_, Message>(
COL,
newer_message_filter,
FindOptions::builder()
.limit(limit / 2 + 1)
.sort(doc! {
"_id": 1_i32
})
.build(),
),
self.find_with_options::<_, Message>(
COL,
older_message_filter,
FindOptions::builder()
.limit(limit / 2)
.sort(doc! {
"_id": -1_i32
})
.build(),
)
)
.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))
}
}
}
/// 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
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$addToSet": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$pull": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$unset": {
format!("reactions.{emoji}"): 1
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// 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))
}
}
@@ -0,0 +1,272 @@
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))
}
}*/
}
/// 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,7 +1,14 @@
mod admin_migrations; mod admin_migrations;
mod bots; mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks;
mod channels;
mod emojis;
mod files; mod files;
mod safety_strikes; mod messages;
mod ratelimit_events;
mod server_bans;
mod server_members; mod server_members;
mod servers; mod servers;
mod user_settings; mod user_settings;
@@ -9,8 +16,15 @@ 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 channels::*;
pub use emojis::*;
pub use files::*; pub use files::*;
pub use safety_strikes::*; pub use messages::*;
pub use ratelimit_events::*;
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::*;
@@ -23,8 +37,15 @@ pub trait AbstractDatabase:
+ Send + Send
+ admin_migrations::AbstractMigrations + admin_migrations::AbstractMigrations
+ bots::AbstractBots + bots::AbstractBots
+ channels::AbstractChannels
+ channel_invites::AbstractChannelInvites
+ channel_unreads::AbstractChannelUnreads
+ channel_webhooks::AbstractWebhooks
+ emojis::AbstractEmojis
+ files::AbstractAttachments + files::AbstractAttachments
+ safety_strikes::AbstractAccountStrikes + messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents
+ 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)
}
}
@@ -1,105 +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,
/// User Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
},
"PartialAccountStrike"
);
#[allow(clippy::disallowed_methods)]
impl AccountStrike {
/// 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_a = "a";
let strike_b = "b";
let strike = AccountStrike {
id: strike_a.to_string(),
user_id: user_id.to_string(),
reason: "reason 1".to_string(),
};
db.insert_account_strike(&strike).await.unwrap();
let mut updated_strike = strike.clone();
updated_strike
.update(
&db,
PartialAccountStrike {
reason: Some("new reason".to_string()),
..Default::default()
},
)
.await
.unwrap();
db.insert_account_strike(&AccountStrike {
id: strike_b.to_string(),
user_id: user_id.to_string(),
reason: "reason 2".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_a));
assert!(ids.contains(strike_b));
let fetched_strike = strikes
.into_iter()
.find(|strike| strike.id == strike_a)
.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 = "bots";
#[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,12 @@
use crate::MemberCompositeKey;
auto_derived!(
/// Server Ban
pub struct ServerBan {
/// Unique member id
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Reason for ban creation
pub reason: Option<String>,
}
);
@@ -0,0 +1,21 @@
use revolt_result::Result;
use crate::{MemberCompositeKey, ServerBan};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractServerBans: Sync + Send {
/// Insert new ban into database
async fn insert_ban(&self, ban: &ServerBan) -> Result<()>;
/// Fetch a server ban by server and user id
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan>;
/// Fetch all bans in a server
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(())
} }
@@ -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
} }
@@ -208,80 +254,7 @@ impl Server {
} }
} }
/* /// 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 /// Remove a member from a server
pub async fn remove_member( pub async fn remove_member(
@@ -364,14 +337,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 +367,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 +388,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 +422,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,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::*;
+415 -15
View File
@@ -1,6 +1,12 @@
use crate::{Database, File}; use std::{collections::HashSet, time::Duration};
use revolt_result::{Error, ErrorType, Result}; use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_config::config;
use revolt_result::{create_error, Error, ErrorType, Result};
use ulid::Ulid;
auto_derived_partial!( auto_derived_partial!(
/// # User /// # User
@@ -10,6 +16,11 @@ auto_derived_partial!(
pub id: String, pub id: String,
/// Username /// Username
pub username: String, pub username: String,
/// Discriminator
pub discriminator: String,
/// Display name
#[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>,
@@ -41,6 +52,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,
@@ -98,18 +118,236 @@ auto_derived!(
/// Id of the owner of this bot /// Id of the owner of this bot
pub owner: String, pub owner: String,
} }
/// Optional fields on user object
pub enum FieldsUser {
Avatar,
StatusText,
StatusPresence,
ProfileContent,
ProfileBackground,
}
); );
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 {
/// Create a new user
pub async fn create<I, D>(
db: &Database,
username: String,
account_id: I,
data: D,
) -> Result<User>
where
I: Into<Option<String>>,
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 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
}
}
/// Check whether a username is already in use by another user /// Check whether a username is already in use by another user
#[allow(dead_code)] #[allow(dead_code)]
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> { async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
@@ -123,6 +361,167 @@ impl User {
} }
} }
/// 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)),
}
}
/// Update user data /// Update user data
pub async fn update<'a>( pub async fn update<'a>(
&mut self, &mut self,
@@ -137,13 +536,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(())
} }
@@ -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>>;
@@ -87,6 +87,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;
@@ -56,6 +56,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 +106,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
@@ -13,19 +13,19 @@ impl<'r> FromRequest<'r> for User {
.local_cache_async(async { .local_cache_async(async {
let db = request.rocket().state::<Database>().expect("`Database`"); let db = request.rocket().state::<Database>().expect("`Database`");
let _header_bot_token = request let header_bot_token = request
.headers() .headers()
.get("x-bot-token") .get("x-bot-token")
.next() .next()
.map(|x| x.to_string()); .map(|x| x.to_string());
/* if let Some(bot_token) = header_bot_token { if let Some(bot_token) = header_bot_token {
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await { if let Ok(bot) = db.fetch_bot_by_token(&bot_token).await {
return Some(user); if let Ok(user) = db.fetch_user(&bot.id).await {
return Some(user);
}
} }
} else */ } else if let Outcome::Success(session) = request.guard::<Session>().await {
if let Outcome::Success(session) = request.guard::<Session>().await {
// This uses a guard so can't really easily be refactored into from_token at this stage.
if let Ok(user) = db.fetch_user(&session.user_id).await { if let Ok(user) = db.fetch_user(&session.user_id).await {
return Some(user); return Some(user);
} }
@@ -0,0 +1,31 @@
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
};
use crate::User;
impl<'r> OpenApiFromRequest<'r> for User {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
let mut requirements = schemars::Map::new();
requirements.insert("Session Token".to_owned(), vec![]);
Ok(RequestHeaderInput::Security(
"Session Token".to_owned(),
SecurityScheme {
data: SecuritySchemeData::ApiKey {
name: "x-session-token".to_owned(),
location: "header".to_owned(),
},
description: Some("Used to authenticate as a user.".to_owned()),
extensions: schemars::Map::new(),
},
requirements,
))
}
}
+122
View File
@@ -0,0 +1,122 @@
// Queue Type: Debounced
use crate::Database;
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use std::{collections::HashMap, time::Duration};
use super::DelayedTask;
/// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)]
pub enum AckEvent {
/// Add mentions for a user in a channel
AddMention {
/// Message IDs
ids: Vec<String>,
},
/// Acknowledge message in a channel for a user
AckMessage {
/// Message ID
id: String,
},
}
/// Task information
struct Data {
/// Channel to ack
channel: String,
/// User to ack for
user: String,
/// Event
event: AckEvent,
}
#[derive(Debug)]
struct Task {
event: AckEvent,
}
static Q: Lazy<Queue<Data>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(channel: String, user: String, event: AckEvent) {
Q.try_push(Data {
channel,
user,
event,
})
.ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let mut tasks = HashMap::<(String, String), DelayedTask<Task>>::new();
let mut keys = vec![];
loop {
// Find due tasks.
for (key, task) in &tasks {
if task.should_run() {
keys.push(key.clone());
}
}
// Commit any due tasks to the database.
for key in &keys {
if let Some(task) = tasks.remove(key) {
let Task { event } = task.data;
let (user, channel) = key;
if let Err(err) = match &event {
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
AckEvent::AckMessage { id } => db.acknowledge_message(channel, user, id).await,
AckEvent::AddMention { ids } => {
db.add_mention_to_unread(channel, user, ids).await
}
} {
error!("{err:?} for {event:?}. ({user}, {channel})");
} else {
info!("User {user} ack in {channel} with {event:?}");
}
}
}
// Clear keys
keys.clear();
// Queue incoming tasks.
while let Some(Data {
channel,
user,
mut event,
}) = Q.try_pop()
{
let key = (user, channel);
if let Some(task) = tasks.get_mut(&key) {
task.delay();
match &mut event {
AckEvent::AddMention { ids } => {
if let AckEvent::AddMention { ids: existing } = &mut task.data.event {
existing.append(ids);
} else {
task.data.event = event;
}
}
AckEvent::AckMessage { .. } => {
task.data.event = event;
}
}
} else {
tasks.insert(key, DelayedTask::new(Task { event }));
}
}
// Sleep for an arbitrary amount of time.
async_std::task::sleep(Duration::from_secs(1)).await;
}
}
@@ -0,0 +1,87 @@
// Queue Type: Debounced
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use std::{collections::HashMap, time::Duration};
use crate::{Database, PartialChannel};
use super::DelayedTask;
/// Task information
struct Data {
/// Channel to update
channel: String,
/// Latest message ID
id: String,
/// Whether the channel is a DM
is_dm: bool,
}
/// Task information
#[derive(Debug)]
struct Task {
/// Latest message ID
id: String,
/// Whether the channel is a DM
is_dm: bool,
}
static Q: Lazy<Queue<Data>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(channel: String, id: String, is_dm: bool) {
Q.try_push(Data { channel, id, is_dm }).ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let mut tasks = HashMap::<String, DelayedTask<Task>>::new();
let mut keys = vec![];
loop {
// Find due tasks.
for (key, task) in &tasks {
if task.should_run() {
keys.push(key.clone());
}
}
// Commit any due tasks to the database.
for key in &keys {
if let Some(task) = tasks.remove(key) {
let Task { id, is_dm, .. } = task.data;
let mut channel = PartialChannel {
last_message_id: Some(id.to_string()),
..Default::default()
};
if is_dm {
channel.active = Some(true);
}
match db.update_channel(key, &channel, vec![]).await {
Ok(_) => info!("Updated last_message_id for {key} to {id}."),
Err(err) => error!("Failed to update last_message_id with {err:?}!"),
}
}
}
// Clear keys
keys.clear();
// Queue incoming tasks.
while let Some(Data { channel, id, is_dm }) = Q.try_pop() {
if let Some(task) = tasks.get_mut(&channel) {
task.data.id = id;
task.delay();
} else {
tasks.insert(channel, DelayedTask::new(Task { id, is_dm }));
}
}
// Sleep for an arbitrary amount of time.
async_std::task::sleep(Duration::from_secs(1)).await;
}
}
+58
View File
@@ -0,0 +1,58 @@
//! Semi-important background task management
use crate::Database;
use async_std::task;
use std::time::Instant;
const WORKER_COUNT: usize = 5;
pub mod ack;
pub mod last_message_id;
pub mod process_embeds;
pub mod web_push;
/// Spawn background workers
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
for _ in 0..WORKER_COUNT {
task::spawn(ack::worker(db.clone()));
task::spawn(last_message_id::worker(db.clone()));
task::spawn(process_embeds::worker(db.clone()));
task::spawn(web_push::worker(authifier_db.clone()));
}
}
/// Task with additional information on when it should run
pub struct DelayedTask<T> {
pub data: T,
last_updated: Instant,
first_seen: Instant,
}
/// Commit to database every 30 seconds if the task is particularly active.
static EXPIRE_CONSTANT: u64 = 30;
/// Otherwise, commit to database after 5 seconds.
static SAVE_CONSTANT: u64 = 5;
impl<T> DelayedTask<T> {
/// Create a new delayed task
pub fn new(data: T) -> Self {
DelayedTask {
data,
last_updated: Instant::now(),
first_seen: Instant::now(),
}
}
/// Push a task further back in time
pub fn delay(&mut self) {
self.last_updated = Instant::now()
}
/// Check if a task should run yet
pub fn should_run(&self) -> bool {
self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT
|| self.last_updated.elapsed().as_secs() > SAVE_CONSTANT
}
}
@@ -0,0 +1,170 @@
use crate::{models::Message, AppendMessage, Database};
use futures::future::join_all;
use linkify::{LinkFinder, LinkKind};
use regex::Regex;
use revolt_config::config;
use revolt_result::Result;
use async_lock::Semaphore;
use async_std::task::spawn;
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use revolt_models::v0::Embed;
use std::{collections::HashSet, sync::Arc};
use isahc::prelude::*;
/// Task information
#[derive(Debug)]
struct EmbedTask {
/// Channel we're processing the event in
channel: String,
/// ID of the message we're processing
id: String,
/// Content of the message
content: String,
}
static Q: Lazy<Queue<EmbedTask>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(channel: String, id: String, content: String) {
Q.try_push(EmbedTask {
channel,
id,
content,
})
.ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let semaphore = Arc::new(Semaphore::new(
config().await.api.workers.max_concurrent_connections,
));
loop {
let task = Q.pop().await;
let db = db.clone();
let semaphore = semaphore.clone();
spawn(async move {
let config = config().await;
let embeds = generate(
task.content,
&config.hosts.january,
config.features.limits.default.message_embeds,
semaphore,
)
.await;
if let Ok(embeds) = embeds {
if let Err(err) = Message::append(
&db,
task.id,
task.channel,
AppendMessage {
embeds: Some(embeds),
},
)
.await
{
error!("Encountered an error appending to message: {:?}", err);
}
}
});
}
}
static RE_CODE: Lazy<Regex> = Lazy::new(|| Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap());
static RE_IGNORED: Lazy<Regex> = Lazy::new(|| Regex::new("(<http.+>)").unwrap());
pub async fn generate(
content: String,
host: &str,
max_embeds: usize,
semaphore: Arc<Semaphore>,
) -> Result<Vec<Embed>> {
// Ignore code blocks.
let content = RE_CODE.replace_all(&content, "");
// Ignore all content between angle brackets starting with http.
let content = RE_IGNORED.replace_all(&content, "");
let content = content
// Ignore quoted lines.
.split('\n')
.map(|v| {
if let Some(c) = v.chars().next() {
if c == '>' {
return "";
}
}
v
})
.collect::<Vec<&str>>()
.join("\n");
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
// Process all links, stripping anchors and
// only taking up to `max_embeds` of links.
let links: Vec<String> = finder
.links(&content)
.map(|x| {
x.as_str()
.chars()
.take_while(|&ch| ch != '#')
.collect::<String>()
})
.collect::<HashSet<String>>()
.into_iter()
.take(max_embeds)
.collect();
// If no links, fail out.
if links.is_empty() {
return Err(create_error!(LabelMe));
}
// TODO: batch request to january
let mut tasks = Vec::new();
for link in links {
let semaphore = semaphore.clone();
let host = host.to_string();
tasks.push(spawn(async move {
let guard = semaphore.acquire().await;
if let Ok(mut response) = isahc::get_async(format!(
"{host}/embed?url={}",
url_escape::encode_component(&link)
))
.await
{
drop(guard);
response.json::<Embed>().await.ok()
} else {
None
}
}));
}
let embeds = join_all(tasks)
.await
.into_iter()
.flatten()
.collect::<Vec<Embed>>();
// Prevent database update when no embeds are found.
if !embeds.is_empty() {
Ok(embeds)
} else {
Err(create_error!(LabelMe))
}
}
+162
View File
@@ -0,0 +1,162 @@
use std::collections::HashSet;
use authifier::Database;
use base64::{
engine::{self},
Engine as _,
};
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use revolt_config::config;
use revolt_models::v0::PushNotification;
use revolt_presence::filter_online;
use serde_json::json;
use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushMessageBuilder,
};
/// Task information
#[derive(Debug)]
struct PushTask {
/// User IDs of the targets that are to receive this notification
recipients: Vec<String>,
/// Push Notification
payload: PushNotification,
}
static Q: Lazy<Queue<PushTask>> = Lazy::new(|| Queue::new(10_000));
/// Queue a new task for a worker
pub async fn queue(recipients: Vec<String>, payload: PushNotification) {
if recipients.is_empty() {
return;
}
let online_ids = filter_online(&recipients).await;
let recipients = (&recipients.into_iter().collect::<HashSet<String>>() - &online_ids)
.into_iter()
.collect::<Vec<String>>();
Q.try_push(PushTask {
recipients,
payload,
})
.ok();
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
}
/// Start a new worker
pub async fn worker(db: Database) {
let config = config().await;
let web_push_client = IsahcWebPushClient::new().unwrap();
let fcm_client = if config.api.fcm.api_key.is_empty() {
None
} else {
Some(fcm::Client::new())
};
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
.decode(config.api.vapid.private_key)
.expect("valid `VAPID_PRIVATE_KEY`");
loop {
let task = Q.pop().await;
if let Ok(sessions) = db.find_sessions_with_subscription(&task.recipients).await {
for session in sessions {
if let Some(sub) = session.subscription {
if sub.endpoint == "fcm" {
// Use Firebase Cloud Messaging
if let Some(client) = &fcm_client {
let PushNotification {
author,
icon,
image: _,
body,
tag,
timestamp: _,
url: _,
} = &task.payload;
let mut notification = fcm::NotificationBuilder::new();
notification.title(author);
notification.icon(icon);
notification.body(body);
notification.tag(tag);
// TODO: expand support for fields
let notification = notification.finalize();
let mut message_builder =
fcm::MessageBuilder::new(&config.api.fcm.api_key, &sub.auth);
message_builder.notification(notification);
if let Err(err) = client.send(message_builder.finalize()).await {
error!("Failed to send FCM notification! {:?}", err);
} else {
info!("Sent FCM notification to {:?}.", session.id);
}
} else {
info!("No FCM token was specified!");
}
} else {
// Use Web Push Standard
let subscription = SubscriptionInfo {
endpoint: sub.endpoint,
keys: SubscriptionKeys {
auth: sub.auth,
p256dh: sub.p256dh,
},
};
match VapidSignatureBuilder::from_pem(
std::io::Cursor::new(&web_push_private_key),
&subscription,
) {
Ok(sig_builder) => match sig_builder.build() {
Ok(signature) => {
let mut builder = WebPushMessageBuilder::new(&subscription);
builder.set_vapid_signature(signature);
let payload = json!(task.payload).to_string();
builder
.set_payload(ContentEncoding::AesGcm, payload.as_bytes());
match builder.build() {
Ok(msg) => match web_push_client.send(msg).await {
Ok(_) => {
info!(
"Sent Web Push notification to {:?}.",
session.id
)
}
Err(err) => {
error!("Hit error sending Web Push! {:?}", err)
}
},
Err(err) => {
error!(
"Failed to build message for {}! {:?}",
session.user_id, err
)
}
}
}
Err(err) => error!(
"Failed to build signature for {}! {:?}",
session.user_id, err
),
},
Err(err) => error!(
"Failed to create signature builder for {}! {:?}",
session.user_id, err
),
}
}
}
}
}
}
}
@@ -0,0 +1 @@
pub mod v0;
+850
View File
@@ -0,0 +1,850 @@
use revolt_models::v0::*;
use revolt_permissions::{calculate_user_permissions, UserPermission};
use crate::{util::permissions::DatabasePermissionQuery, Database};
impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot {
#[cfg(debug_assertions)]
assert_eq!(self.id, user.id);
PublicBot {
id: self.id,
username: user.username,
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
description: user
.profile
.map(|profile| profile.content)
.unwrap_or_default(),
}
}
}
impl From<crate::Bot> for Bot {
fn from(value: crate::Bot) -> Self {
Bot {
id: value.id,
owner_id: value.owner,
token: value.token,
public: value.public,
analytics: value.analytics,
discoverable: value.discoverable,
interactions_url: value.interactions_url,
terms_of_service_url: value.terms_of_service_url,
privacy_policy_url: value.privacy_policy_url,
flags: value.flags.unwrap_or_default() as u32,
}
}
}
impl From<FieldsBot> for crate::FieldsBot {
fn from(value: FieldsBot) -> Self {
match value {
FieldsBot::InteractionsURL => crate::FieldsBot::InteractionsURL,
FieldsBot::Token => crate::FieldsBot::Token,
}
}
}
impl From<crate::FieldsBot> for FieldsBot {
fn from(value: crate::FieldsBot) -> Self {
match value {
crate::FieldsBot::InteractionsURL => FieldsBot::InteractionsURL,
crate::FieldsBot::Token => FieldsBot::Token,
}
}
}
impl From<crate::Invite> for Invite {
fn from(value: crate::Invite) -> Self {
match value {
crate::Invite::Group {
code,
creator,
channel,
} => Invite::Group {
code,
creator,
channel,
},
crate::Invite::Server {
code,
server,
creator,
channel,
} => Invite::Server {
code,
server,
creator,
channel,
},
}
}
}
impl From<crate::ChannelUnread> for ChannelUnread {
fn from(value: crate::ChannelUnread) -> Self {
ChannelUnread {
id: value.id.into(),
last_id: value.last_id,
mentions: value.mentions.unwrap_or_default(),
}
}
}
impl From<crate::ChannelCompositeKey> for ChannelCompositeKey {
fn from(value: crate::ChannelCompositeKey) -> Self {
ChannelCompositeKey {
channel: value.channel,
user: value.user,
}
}
}
impl From<crate::Webhook> for Webhook {
fn from(value: crate::Webhook) -> Self {
Webhook {
id: value.id,
name: value.name,
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
permissions: value.permissions,
}
}
}
impl From<crate::PartialWebhook> for PartialWebhook {
fn from(value: crate::PartialWebhook) -> Self {
PartialWebhook {
id: value.id,
name: value.name,
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
permissions: value.permissions,
}
}
}
impl From<FieldsWebhook> for crate::FieldsWebhook {
fn from(_value: FieldsWebhook) -> Self {
Self::Avatar
}
}
impl From<crate::FieldsWebhook> for FieldsWebhook {
fn from(_value: crate::FieldsWebhook) -> Self {
Self::Avatar
}
}
impl From<crate::Channel> for Channel {
fn from(value: crate::Channel) -> Self {
match value {
crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user },
crate::Channel::DirectMessage {
id,
active,
recipients,
last_message_id,
} => Channel::DirectMessage {
id,
active,
recipients,
last_message_id,
},
crate::Channel::Group {
id,
name,
owner,
description,
recipients,
icon,
last_message_id,
permissions,
nsfw,
} => Channel::Group {
id,
name,
owner,
description,
recipients,
icon: icon.map(|file| file.into()),
last_message_id,
permissions,
nsfw,
},
crate::Channel::TextChannel {
id,
server,
name,
description,
icon,
last_message_id,
default_permissions,
role_permissions,
nsfw,
} => Channel::TextChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
last_message_id,
default_permissions,
role_permissions,
nsfw,
},
crate::Channel::VoiceChannel {
id,
server,
name,
description,
icon,
default_permissions,
role_permissions,
nsfw,
} => Channel::VoiceChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
nsfw,
},
}
}
}
impl From<crate::PartialChannel> for PartialChannel {
fn from(value: crate::PartialChannel) -> Self {
PartialChannel {
name: value.name,
owner: value.owner,
description: value.description,
icon: value.icon.map(|file| file.into()),
nsfw: value.nsfw,
active: value.active,
permissions: value.permissions,
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
}
}
}
impl From<FieldsChannel> for crate::FieldsChannel {
fn from(value: FieldsChannel) -> Self {
match value {
FieldsChannel::Description => crate::FieldsChannel::Description,
FieldsChannel::Icon => crate::FieldsChannel::Icon,
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
}
}
}
impl From<crate::FieldsChannel> for FieldsChannel {
fn from(value: crate::FieldsChannel) -> Self {
match value {
crate::FieldsChannel::Description => FieldsChannel::Description,
crate::FieldsChannel::Icon => FieldsChannel::Icon,
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
}
}
}
impl From<crate::Emoji> for Emoji {
fn from(value: crate::Emoji) -> Self {
Emoji {
id: value.id,
parent: value.parent.into(),
creator_id: value.creator_id,
name: value.name,
animated: value.animated,
nsfw: value.nsfw,
}
}
}
impl From<crate::EmojiParent> for EmojiParent {
fn from(value: crate::EmojiParent) -> Self {
match value {
crate::EmojiParent::Detached => EmojiParent::Detached,
crate::EmojiParent::Server { id } => EmojiParent::Server { id },
}
}
}
impl From<EmojiParent> for crate::EmojiParent {
fn from(value: EmojiParent) -> Self {
match value {
EmojiParent::Detached => crate::EmojiParent::Detached,
EmojiParent::Server { id } => crate::EmojiParent::Server { id },
}
}
}
impl From<crate::File> for File {
fn from(value: crate::File) -> Self {
File {
id: value.id,
tag: value.tag,
filename: value.filename,
metadata: value.metadata.into(),
content_type: value.content_type,
size: value.size,
deleted: value.deleted,
reported: value.reported,
message_id: value.message_id,
user_id: value.user_id,
server_id: value.server_id,
object_id: value.object_id,
}
}
}
impl From<crate::Metadata> for Metadata {
fn from(value: crate::Metadata) -> Self {
match value {
crate::Metadata::File => Metadata::File,
crate::Metadata::Text => Metadata::Text,
crate::Metadata::Image { width, height } => Metadata::Image {
width: width as usize,
height: height as usize,
},
crate::Metadata::Video { width, height } => Metadata::Video {
width: width as usize,
height: height as usize,
},
crate::Metadata::Audio => Metadata::Audio,
}
}
}
impl From<crate::Message> for Message {
fn from(value: crate::Message) -> Self {
Message {
id: value.id,
nonce: value.nonce,
channel: value.channel,
author: value.author,
webhook: value.webhook,
content: value.content,
system: value.system.map(|system| system.into()),
attachments: value
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value
.reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect(),
interactions: value.interactions.into(),
masquerade: value.masquerade.map(|masq| masq.into()),
}
}
}
impl From<crate::PartialMessage> for PartialMessage {
fn from(value: crate::PartialMessage) -> Self {
PartialMessage {
id: value.id,
nonce: value.nonce,
channel: value.channel,
author: value.author,
webhook: value.webhook,
content: value.content,
system: value.system.map(|system| system.into()),
attachments: value
.attachments
.map(|v| v.into_iter().map(|f| f.into()).collect()),
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
replies: value.replies,
reactions: value.reactions.map(|reactions| {
reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect()
}),
interactions: value.interactions.map(|interactions| interactions.into()),
masquerade: value.masquerade.map(|masq| masq.into()),
}
}
}
impl From<crate::SystemMessage> for SystemMessage {
fn from(value: crate::SystemMessage) -> Self {
match value {
crate::SystemMessage::ChannelDescriptionChanged { by } => {
Self::ChannelDescriptionChanged { by }
}
crate::SystemMessage::ChannelIconChanged { by } => Self::ChannelIconChanged { by },
crate::SystemMessage::ChannelOwnershipChanged { from, to } => {
Self::ChannelOwnershipChanged { from, to }
}
crate::SystemMessage::ChannelRenamed { name, by } => Self::ChannelRenamed { name, by },
crate::SystemMessage::Text { content } => Self::Text { content },
crate::SystemMessage::UserAdded { id, by } => Self::UserAdded { id, by },
crate::SystemMessage::UserBanned { id } => Self::UserBanned { id },
crate::SystemMessage::UserJoined { id } => Self::UserJoined { id },
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
}
}
}
impl From<crate::Interactions> for Interactions {
fn from(value: crate::Interactions) -> Self {
Interactions {
reactions: value
.reactions
.map(|reactions| reactions.into_iter().collect()),
restrict_reactions: value.restrict_reactions,
}
}
}
impl From<Interactions> for crate::Interactions {
fn from(value: Interactions) -> Self {
crate::Interactions {
reactions: value
.reactions
.map(|reactions| reactions.into_iter().collect()),
restrict_reactions: value.restrict_reactions,
}
}
}
impl From<crate::AppendMessage> for AppendMessage {
fn from(value: crate::AppendMessage) -> Self {
AppendMessage {
embeds: value.embeds,
}
}
}
impl From<crate::Masquerade> for Masquerade {
fn from(value: crate::Masquerade) -> Self {
Masquerade {
name: value.name,
avatar: value.avatar,
colour: value.colour,
}
}
}
impl From<Masquerade> for crate::Masquerade {
fn from(value: Masquerade) -> Self {
crate::Masquerade {
name: value.name,
avatar: value.avatar,
colour: value.colour,
}
}
}
impl From<crate::ServerBan> for ServerBan {
fn from(value: crate::ServerBan) -> Self {
ServerBan {
id: value.id.into(),
reason: value.reason,
}
}
}
impl From<crate::Member> for Member {
fn from(value: crate::Member) -> Self {
Member {
id: value.id.into(),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::PartialMember> for PartialMember {
fn from(value: crate::PartialMember) -> Self {
PartialMember {
id: value.id.map(|id| id.into()),
joined_at: value.joined_at,
nickname: value.nickname,
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
}
}
}
impl From<crate::MemberCompositeKey> for MemberCompositeKey {
fn from(value: crate::MemberCompositeKey) -> Self {
MemberCompositeKey {
server: value.server,
user: value.user,
}
}
}
impl From<crate::FieldsMember> for FieldsMember {
fn from(value: crate::FieldsMember) -> Self {
match value {
crate::FieldsMember::Avatar => FieldsMember::Avatar,
crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout,
}
}
}
impl From<crate::RemovalIntention> for RemovalIntention {
fn from(value: crate::RemovalIntention) -> Self {
match value {
crate::RemovalIntention::Ban => RemovalIntention::Ban,
crate::RemovalIntention::Kick => RemovalIntention::Kick,
crate::RemovalIntention::Leave => RemovalIntention::Leave,
}
}
}
impl From<crate::Server> for Server {
fn from(value: crate::Server) -> Self {
Server {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect(),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.unwrap_or_default() as u32,
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::PartialServer> for PartialServer {
fn from(value: crate::PartialServer) -> Self {
PartialServer {
id: value.id,
owner: value.owner,
name: value.name,
description: value.description,
channels: value.channels,
categories: value
.categories
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
system_messages: value.system_messages.map(|v| v.into()),
roles: value
.roles
.map(|roles| roles.into_iter().map(|(k, v)| (k, v.into())).collect()),
default_permissions: value.default_permissions,
icon: value.icon.map(|f| f.into()),
banner: value.banner.map(|f| f.into()),
flags: value.flags.map(|v| v as u32),
nsfw: value.nsfw,
analytics: value.analytics,
discoverable: value.discoverable,
}
}
}
impl From<crate::FieldsServer> for FieldsServer {
fn from(value: crate::FieldsServer) -> Self {
match value {
crate::FieldsServer::Banner => FieldsServer::Banner,
crate::FieldsServer::Categories => FieldsServer::Categories,
crate::FieldsServer::Description => FieldsServer::Description,
crate::FieldsServer::Icon => FieldsServer::Icon,
crate::FieldsServer::SystemMessages => FieldsServer::SystemMessages,
}
}
}
impl From<crate::Category> for Category {
fn from(value: crate::Category) -> Self {
Category {
id: value.id,
title: value.title,
channels: value.channels,
}
}
}
impl From<crate::SystemMessageChannels> for SystemMessageChannels {
fn from(value: crate::SystemMessageChannels) -> Self {
SystemMessageChannels {
user_joined: value.user_joined,
user_left: value.user_left,
user_kicked: value.user_kicked,
user_banned: value.user_banned,
}
}
}
impl From<crate::Role> for Role {
fn from(value: crate::Role) -> Self {
Role {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::PartialRole> for PartialRole {
fn from(value: crate::PartialRole) -> Self {
PartialRole {
name: value.name,
permissions: value.permissions,
colour: value.colour,
hoist: value.hoist,
rank: value.rank,
}
}
}
impl From<crate::FieldsRole> for FieldsRole {
fn from(value: crate::FieldsRole) -> Self {
match value {
crate::FieldsRole::Colour => FieldsRole::Colour,
}
}
}
impl crate::User {
pub async fn into<'a, P>(self, db: &Database, perspective: P) -> User
where
P: Into<Option<&'a crate::User>>,
{
let perspective = perspective.into();
let (relationship, can_see_profile) = if self.bot.is_some() {
(RelationshipStatus::None, true)
} else if let Some(perspective) = perspective {
let mut query = DatabasePermissionQuery::new(db, perspective).user(&self);
if perspective.id == self.id {
(RelationshipStatus::User, true)
} else {
(
perspective
.relations
.as_ref()
.map(|relations| {
relations
.iter()
.find(|relationship| relationship.id == self.id)
.map(|relationship| relationship.status.clone().into())
.unwrap_or_default()
})
.unwrap_or_default(),
calculate_user_permissions(&mut query)
.await
.has_user_permission(UserPermission::ViewProfile),
)
}
} else {
(RelationshipStatus::None, false)
};
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: if let Some(crate::User { id, .. }) = perspective {
if id == &self.id {
self.relations
.unwrap_or_default()
.into_iter()
.map(|relation| relation.into())
.collect()
} else {
vec![]
}
} else {
vec![]
},
badges: self.badges.unwrap_or_default() as u32,
status: if can_see_profile {
self.status.map(|status| status.into())
} else {
None
},
profile: if can_see_profile {
self.profile.map(|profile| profile.into())
} else {
None
},
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship,
online: can_see_profile && revolt_presence::is_online(&self.id).await,
id: self.id,
}
}
pub async fn into_self(self) -> User {
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: self
.relations
.map(|relationships| {
relationships
.into_iter()
.map(|relationship| relationship.into())
.collect()
})
.unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32,
status: self.status.map(|status| status.into()),
profile: self.profile.map(|profile| profile.into()),
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship: RelationshipStatus::User,
online: revolt_presence::is_online(&self.id).await,
id: self.id,
}
}
}
impl From<crate::PartialUser> for PartialUser {
fn from(value: crate::PartialUser) -> Self {
PartialUser {
username: value.username,
discriminator: value.discriminator,
display_name: value.display_name,
avatar: value.avatar.map(|file| file.into()),
relations: value.relations.map(|relationships| {
relationships
.into_iter()
.map(|relationship| relationship.into())
.collect()
}),
badges: value.badges.map(|badges| badges as u32),
status: value.status.map(|status| status.into()),
profile: value.profile.map(|profile| profile.into()),
flags: value.flags.map(|flags| flags as u32),
privileged: value.privileged,
bot: value.bot.map(|bot| bot.into()),
relationship: None,
online: None,
id: value.id,
}
}
}
impl From<FieldsUser> for crate::FieldsUser {
fn from(value: FieldsUser) -> Self {
match value {
FieldsUser::Avatar => crate::FieldsUser::Avatar,
FieldsUser::ProfileBackground => crate::FieldsUser::ProfileBackground,
FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent,
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
FieldsUser::StatusText => crate::FieldsUser::StatusText,
}
}
}
impl From<crate::FieldsUser> for FieldsUser {
fn from(value: crate::FieldsUser) -> Self {
match value {
crate::FieldsUser::Avatar => FieldsUser::Avatar,
crate::FieldsUser::ProfileBackground => FieldsUser::ProfileBackground,
crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent,
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
crate::FieldsUser::StatusText => FieldsUser::StatusText,
}
}
}
impl From<crate::RelationshipStatus> for RelationshipStatus {
fn from(value: crate::RelationshipStatus) -> Self {
match value {
crate::RelationshipStatus::None => RelationshipStatus::None,
crate::RelationshipStatus::User => RelationshipStatus::User,
crate::RelationshipStatus::Friend => RelationshipStatus::Friend,
crate::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
crate::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
crate::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
crate::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther,
}
}
}
impl From<crate::Relationship> for Relationship {
fn from(value: crate::Relationship) -> Self {
Self {
user_id: value.id,
status: value.status.into(),
}
}
}
impl From<crate::Presence> for Presence {
fn from(value: crate::Presence) -> Self {
match value {
crate::Presence::Online => Presence::Online,
crate::Presence::Idle => Presence::Idle,
crate::Presence::Focus => Presence::Focus,
crate::Presence::Busy => Presence::Busy,
crate::Presence::Invisible => Presence::Invisible,
}
}
}
impl From<crate::UserStatus> for UserStatus {
fn from(value: crate::UserStatus) -> Self {
UserStatus {
text: value.text,
presence: value.presence.map(|presence| presence.into()),
}
}
}
impl From<crate::UserProfile> for UserProfile {
fn from(value: crate::UserProfile) -> Self {
UserProfile {
content: value.content,
background: value.background.map(|file| file.into()),
}
}
}
impl From<crate::BotInformation> for BotInformation {
fn from(value: crate::BotInformation) -> Self {
BotInformation {
owner_id: value.owner,
}
}
}
@@ -1,23 +1,21 @@
use crate::{Error, Result}; use std::num::NonZeroUsize;
use revolt_result::{create_error, Result};
#[cfg(feature = "rocket-impl")]
use revolt_result::Error;
use async_std::sync::Mutex; use async_std::sync::Mutex;
use revolt_rocket_okapi::gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
use serde::{Deserialize, Serialize};
use validator::Validate;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
#[derive(Validate, Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct IdempotencyKey { pub struct IdempotencyKey {
#[validate(length(min = 1, max = 64))]
key: String, key: String,
} }
static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> = Lazy::new(|| Mutex::new(lru::LruCache::new(100))); static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
impl IdempotencyKey { impl IdempotencyKey {
// Backwards compatibility. // Backwards compatibility.
@@ -26,7 +24,7 @@ impl IdempotencyKey {
if let Some(v) = v { if let Some(v) = v {
let mut cache = TOKEN_CACHE.lock().await; let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&v).is_some() { if cache.get(&v).is_some() {
return Err(Error::DuplicateNonce); return Err(create_error!(DuplicateNonce));
} }
cache.put(v.clone(), ()); cache.put(v.clone(), ());
@@ -41,6 +39,17 @@ impl IdempotencyKey {
} }
} }
#[cfg(feature = "rocket-impl")]
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
revolt_okapi::openapi3::{Parameter, ParameterValue},
};
#[cfg(feature = "rocket-impl")]
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
#[cfg(feature = "rocket-impl")]
impl<'r> OpenApiFromRequest<'r> for IdempotencyKey { impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
fn from_request_input( fn from_request_input(
_gen: &mut OpenApiGenerator, _gen: &mut OpenApiGenerator,
@@ -70,6 +79,13 @@ impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
} }
} }
#[cfg(feature = "rocket-impl")]
use rocket::{
http::Status,
request::{FromRequest, Outcome},
};
#[cfg(feature = "rocket-impl")]
#[async_trait] #[async_trait]
impl<'r> FromRequest<'r> for IdempotencyKey { impl<'r> FromRequest<'r> for IdempotencyKey {
type Error = Error; type Error = Error;
@@ -81,14 +97,19 @@ impl<'r> FromRequest<'r> for IdempotencyKey {
.next() .next()
.map(|k| k.to_string()) .map(|k| k.to_string())
{ {
let idempotency = IdempotencyKey { key }; if key.len() > 64 {
if let Err(error) = idempotency.validate() { return Outcome::Failure((
return Outcome::Failure((Status::BadRequest, Error::FailedValidation { error })); Status::BadRequest,
create_error!(FailedValidation {
error: "idempotency key too long".to_string(),
}),
));
} }
let idempotency = IdempotencyKey { key };
let mut cache = TOKEN_CACHE.lock().await; let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&idempotency.key).is_some() { if cache.get(&idempotency.key).is_some() {
return Outcome::Failure((Status::Conflict, Error::DuplicateNonce)); return Outcome::Failure((Status::Conflict, create_error!(DuplicateNonce)));
} }
cache.put(idempotency.key.clone(), ()); cache.put(idempotency.key.clone(), ());
+5
View File
@@ -1,2 +1,7 @@
pub mod bridge;
pub mod idempotency;
pub mod permissions; pub mod permissions;
pub mod reference; pub mod reference;
#[cfg(test)]
pub mod test_fixtures;
+299 -33
View File
@@ -1,29 +1,32 @@
use std::borrow::Cow; use std::borrow::Cow;
use revolt_permissions::{ use revolt_permissions::{
calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus, calculate_user_permissions, ChannelType, Override, PermissionQuery, PermissionValue,
RelationshipStatus,
}; };
use crate::{Database, User}; use crate::{Channel, Database, Member, Server, User};
/// Permissions calculator /// Permissions calculator
pub struct PermissionCalculator<'a> { #[derive(Clone)]
pub struct DatabasePermissionQuery<'a> {
#[allow(dead_code)] #[allow(dead_code)]
database: &'a Database, database: &'a Database,
perspective: &'a User, perspective: &'a User,
user: Option<Cow<'a, User>>, user: Option<Cow<'a, User>>,
// pub channel: Cow<'a, Channel>, channel: Option<Cow<'a, Channel>>,
// pub server: Cow<'a, Server>, server: Option<Cow<'a, Server>>,
// pub member: Cow<'a, Member>, member: Option<Cow<'a, Member>>,
// flag_known_relationship: Option<&'a RelationshipStatus>, // flag_known_relationship: Option<&'a RelationshipStatus>,
cached_user_permission: Option<u32>, cached_user_permission: Option<PermissionValue>,
cached_mutual_connection: Option<bool>,
cached_permission: Option<u64>, cached_permission: Option<u64>,
} }
#[async_trait] #[async_trait]
impl PermissionQuery for PermissionCalculator<'_> { impl PermissionQuery for DatabasePermissionQuery<'_> {
// * For calculating user permission // * For calculating user permission
/// Is our perspective user privileged? /// Is our perspective user privileged?
@@ -48,6 +51,10 @@ impl PermissionQuery for PermissionCalculator<'_> {
/// Get the relationship with have with the currently selected user /// Get the relationship with have with the currently selected user
async fn user_relationship(&mut self) -> RelationshipStatus { async fn user_relationship(&mut self) -> RelationshipStatus {
if let Some(other_user) = &self.user { if let Some(other_user) = &self.user {
if self.perspective.id == other_user.id {
return RelationshipStatus::User;
}
if let Some(relations) = &self.perspective.relations { if let Some(relations) = &self.perspective.relations {
for entry in relations { for entry in relations {
if entry.id == other_user.id { if entry.id == other_user.id {
@@ -81,93 +88,308 @@ impl PermissionQuery for PermissionCalculator<'_> {
/// Do we have a mutual connection with the currently selected user? /// Do we have a mutual connection with the currently selected user?
async fn have_mutual_connection(&mut self) -> bool { async fn have_mutual_connection(&mut self) -> bool {
// TODO: User::has_mutual_connection if let Some(value) = self.cached_mutual_connection {
false value
} else if let Some(user) = &self.user {
let value = self
.perspective
.has_mutual_connection(self.database, &user.id)
.await
.unwrap_or_default();
self.cached_mutual_connection = Some(value);
matches!(value, true)
} else {
false
}
} }
// * For calculating server permission // * For calculating server permission
/// Is our perspective user the server's owner? /// Is our perspective user the server's owner?
async fn are_we_server_owner(&mut self) -> bool { async fn are_we_server_owner(&mut self) -> bool {
todo!() if let Some(server) = &self.server {
server.owner == self.perspective.id
} else {
false
}
} }
/// Is our perspective user a member of the server? /// Is our perspective user a member of the server?
async fn are_we_a_member(&mut self) -> bool { async fn are_we_a_member(&mut self) -> bool {
todo!() if let Some(server) = &self.server {
if self.member.is_some() {
true
} else if let Ok(member) = self
.database
.fetch_member(&server.id, &self.perspective.id)
.await
{
self.member = Some(Cow::Owned(member));
true
} else {
false
}
} else {
false
}
} }
/// Get default server permission /// Get default server permission
async fn get_default_server_permissions(&mut self) -> u64 { async fn get_default_server_permissions(&mut self) -> u64 {
todo!() if let Some(server) = &self.server {
server.default_permissions as u64
} else {
0
}
} }
/// Get the ordered role overrides (from lowest to highest) for this member in this server /// Get the ordered role overrides (from lowest to highest) for this member in this server
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> { async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
todo!() if let Some(server) = &self.server {
let member_roles = self
.member
.as_ref()
.map(|member| member.roles.clone())
.unwrap_or_default();
let mut roles = server
.roles
.iter()
.filter(|(id, _)| member_roles.contains(id))
.map(|(_, role)| {
let v: Override = role.permissions.into();
(role.rank, v)
})
.collect::<Vec<(i64, Override)>>();
roles.sort_by(|a, b| b.0.cmp(&a.0));
roles.into_iter().map(|(_, v)| v).collect()
} else {
vec![]
}
} }
/// Is our perspective user timed out on this server? /// Is our perspective user timed out on this server?
async fn are_we_timed_out(&mut self) -> bool { async fn are_we_timed_out(&mut self) -> bool {
todo!() if let Some(member) = &self.member {
member.in_timeout()
} else {
false
}
} }
// * For calculating channel permission // * For calculating channel permission
/// Get the type of the channel /// Get the type of the channel
async fn get_channel_type(&mut self) -> ChannelType { async fn get_channel_type(&mut self) -> ChannelType {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::DirectMessage { .. })
| Cow::Owned(Channel::DirectMessage { .. }) => ChannelType::DirectMessage,
Cow::Borrowed(Channel::Group { .. }) | Cow::Owned(Channel::Group { .. }) => {
ChannelType::Group
}
Cow::Borrowed(Channel::SavedMessages { .. })
| Cow::Owned(Channel::SavedMessages { .. }) => ChannelType::SavedMessages,
Cow::Borrowed(Channel::TextChannel { .. })
| Cow::Owned(Channel::TextChannel { .. })
| Cow::Borrowed(Channel::VoiceChannel { .. })
| Cow::Owned(Channel::VoiceChannel { .. }) => ChannelType::ServerChannel,
}
} else {
ChannelType::Unknown
}
} }
/// Get the default channel permissions /// Get the default channel permissions
/// Group channel defaults should be mapped to an allow-only override /// Group channel defaults should be mapped to an allow-only override
async fn get_default_channel_permissions(&mut self) -> Override { async fn get_default_channel_permissions(&mut self) -> Override {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::Group { permissions, .. })
| Cow::Owned(Channel::Group { permissions, .. }) => Override {
allow: permissions.unwrap_or_default() as u64,
deny: 0,
},
Cow::Borrowed(Channel::TextChannel {
default_permissions,
..
})
| Cow::Owned(Channel::TextChannel {
default_permissions,
..
})
| Cow::Borrowed(Channel::VoiceChannel {
default_permissions,
..
})
| Cow::Owned(Channel::VoiceChannel {
default_permissions,
..
}) => default_permissions.unwrap_or_default().into(),
_ => Default::default(),
}
} else {
Default::default()
}
} }
/// Get the ordered role overrides (from lowest to highest) for this member in this channel /// Get the ordered role overrides (from lowest to highest) for this member in this channel
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> { async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::TextChannel {
role_permissions, ..
})
| Cow::Owned(Channel::TextChannel {
role_permissions, ..
})
| Cow::Borrowed(Channel::VoiceChannel {
role_permissions, ..
})
| Cow::Owned(Channel::VoiceChannel {
role_permissions, ..
}) => {
if let Some(server) = &self.server {
let member_roles = self
.member
.as_ref()
.map(|member| member.roles.clone())
.unwrap_or_default();
let mut roles = role_permissions
.iter()
.filter(|(id, _)| member_roles.contains(id))
.filter_map(|(id, permission)| {
server.roles.get(id).map(|role| {
let v: Override = (*permission).into();
(role.rank, v)
})
})
.collect::<Vec<(i64, Override)>>();
roles.sort_by(|a, b| b.0.cmp(&a.0));
roles.into_iter().map(|(_, v)| v).collect()
} else {
vec![]
}
}
_ => vec![],
}
} else {
vec![]
}
} }
/// Do we own this group or saved messages channel if it is one of those? /// Do we own this group or saved messages channel if it is one of those?
async fn do_we_own_the_channel(&mut self) -> bool { async fn do_we_own_the_channel(&mut self) -> bool {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::Group { owner, .. })
| Cow::Owned(Channel::Group { owner, .. }) => owner == &self.perspective.id,
Cow::Borrowed(Channel::SavedMessages { user, .. })
| Cow::Owned(Channel::SavedMessages { user, .. }) => user == &self.perspective.id,
_ => false,
}
} else {
false
}
} }
/// Are we a recipient of this channel? /// Are we a recipient of this channel?
async fn are_we_part_of_the_channel(&mut self) -> bool { async fn are_we_part_of_the_channel(&mut self) -> bool {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::DirectMessage { recipients, .. })
| Cow::Owned(Channel::DirectMessage { recipients, .. })
| Cow::Borrowed(Channel::Group { recipients, .. })
| Cow::Owned(Channel::Group { recipients, .. }) => {
recipients.contains(&self.perspective.id)
}
_ => false,
}
} else {
false
}
} }
/// Set the current user as the recipient of this channel /// Set the current user as the recipient of this channel
/// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths) /// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths)
async fn set_recipient_as_user(&mut self) { async fn set_recipient_as_user(&mut self) {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::DirectMessage { recipients, .. })
| Cow::Owned(Channel::DirectMessage { recipients, .. }) => {
let recipient_id = recipients
.iter()
.find(|recipient| recipient != &&self.perspective.id)
.expect("Missing recipient for DM");
if let Ok(user) = self.database.fetch_user(recipient_id).await {
self.user.replace(Cow::Owned(user));
}
}
_ => unimplemented!(),
}
}
} }
/// Set the current server as the server owning this channel /// Set the current server as the server owning this channel
/// (this will only ever be called for server channels, use unimplemented!() for other code paths) /// (this will only ever be called for server channels, use unimplemented!() for other code paths)
async fn set_server_from_channel(&mut self) { async fn set_server_from_channel(&mut self) {
todo!() if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::TextChannel { server, .. })
| Cow::Owned(Channel::TextChannel { server, .. })
| Cow::Borrowed(Channel::VoiceChannel { server, .. })
| Cow::Owned(Channel::VoiceChannel { server, .. }) => {
if let Some(known_server) =
// I'm not sure why I can't just pattern match both at once here?
// It throws some weird error and the provided fix doesn't work :/
if let Some(Cow::Borrowed(known_server)) = self.server {
Some(known_server)
} else if let Some(Cow::Owned(ref known_server)) = self.server {
Some(known_server)
} else {
None
}
{
if server == &known_server.id {
// Already cached, return early.
return;
}
}
if let Ok(server) = self.database.fetch_server(server).await {
self.server.replace(Cow::Owned(server));
}
}
_ => unimplemented!(),
}
}
} }
} }
impl<'a> PermissionCalculator<'a> { impl<'a> DatabasePermissionQuery<'a> {
/// Create a new permission calculator /// Create a new permission calculator
pub fn new(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> { pub fn new(database: &'a Database, perspective: &'a User) -> DatabasePermissionQuery<'a> {
PermissionCalculator { DatabasePermissionQuery {
database, database,
perspective, perspective,
user: None, user: None,
channel: None,
server: None,
member: None,
cached_mutual_connection: None,
cached_user_permission: None, cached_user_permission: None,
cached_permission: None, cached_permission: None,
} }
} }
/// Calculate the user permission value /// Calculate the user permission value
pub async fn calc_user(mut self) -> PermissionCalculator<'a> { pub async fn calc_user(mut self) -> DatabasePermissionQuery<'a> {
if self.cached_user_permission.is_some() { if self.cached_user_permission.is_some() {
return self; return self;
} }
@@ -176,14 +398,14 @@ impl<'a> PermissionCalculator<'a> {
panic!("Expected `PermissionCalculator.user to exist."); panic!("Expected `PermissionCalculator.user to exist.");
} }
PermissionCalculator { DatabasePermissionQuery {
cached_user_permission: Some(calculate_user_permissions(&mut self).await), cached_user_permission: Some(calculate_user_permissions(&mut self).await),
..self ..self
} }
} }
/// Calculate the permission value /// Calculate the permission value
pub async fn calc(self) -> PermissionCalculator<'a> { pub async fn calc(self) -> DatabasePermissionQuery<'a> {
if self.cached_permission.is_some() { if self.cached_permission.is_some() {
return self; return self;
} }
@@ -192,15 +414,59 @@ impl<'a> PermissionCalculator<'a> {
} }
/// Use user /// Use user
pub fn user(self, user: Cow<'a, User>) -> PermissionCalculator { pub fn user(self, user: &'a User) -> DatabasePermissionQuery {
PermissionCalculator { DatabasePermissionQuery {
user: Some(user), user: Some(Cow::Borrowed(user)),
..self ..self
} }
} }
/// Use channel
pub fn channel(self, channel: &'a Channel) -> DatabasePermissionQuery {
DatabasePermissionQuery {
channel: Some(Cow::Borrowed(channel)),
..self
}
}
/// Use server
pub fn server(self, server: &'a Server) -> DatabasePermissionQuery {
DatabasePermissionQuery {
server: Some(Cow::Borrowed(server)),
..self
}
}
/// Use member
pub fn member(self, member: &'a Member) -> DatabasePermissionQuery {
DatabasePermissionQuery {
member: Some(Cow::Borrowed(member)),
..self
}
}
/// Access the underlying user
pub fn user_ref(&self) -> &Option<Cow<User>> {
&self.user
}
/// Access the underlying server
pub fn channel_ref(&self) -> &Option<Cow<Channel>> {
&self.channel
}
/// Access the underlying server
pub fn server_ref(&self) -> &Option<Cow<Server>> {
&self.server
}
/// Access the underlying member
pub fn member_ref(&self) -> &Option<Cow<Member>> {
&self.member
}
} }
/// Short-hand for creating a permission calculator /// Short-hand for creating a permission calculator
pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> { pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> DatabasePermissionQuery<'a> {
PermissionCalculator::new(database, perspective) DatabasePermissionQuery::new(database, perspective)
} }
+31 -1
View File
@@ -7,7 +7,7 @@ use schemars::{
JsonSchema, JsonSchema,
}; };
use crate::{Bot, Database}; use crate::{Bot, Channel, Database, Emoji, Message, Server, User, Webhook};
/// Reference to some object in the database /// Reference to some object in the database
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -26,6 +26,36 @@ impl Reference {
pub async fn as_bot(&self, db: &Database) -> Result<Bot> { pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await db.fetch_bot(&self.id).await
} }
/// Fetch emoji from Ref
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
db.fetch_emoji(&self.id).await
}
/// Fetch channel from Ref
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
db.fetch_channel(&self.id).await
}
/// Fetch message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(&self.id).await
}
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await
}
/// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> {
db.fetch_user(&self.id).await
}
/// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await
}
} }
#[cfg(feature = "rocket-impl")] #[cfg(feature = "rocket-impl")]

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