Compare commits

...

348 Commits

Author SHA1 Message Date
Paul Makles
cf82972a80 chore: bump version 2024-02-10 23:51:04 +00:00
Bob Bobs
ea00f0fec1 fix(delta): set ratelimit headers on 429 responses 2024-02-10 23:49:15 +00:00
Bob Bobs
226dbca6e0 fix(core/models): use IndexMap for reactions 2024-02-10 23:45:34 +00:00
Bob Bobs
e4b5a23f71 fix(delta): MemberResponse only include roles member has 2024-02-10 23:44:20 +00:00
Bob Bobs
9663caa1ff fix(delta): use untagged enum for MemberResponse 2024-02-10 23:44:20 +00:00
Paul Makles
f9f0f409a8 chore: bump version to 0.6.11 2024-02-10 01:05:08 +00:00
Paul Makles
8f4d6c864b feat: block mentions from low trust users (account created < 12 hours) 2024-02-10 01:04:52 +00:00
Paul Makles
513461c99a chore: bump version 2024-02-06 13:43:00 +00:00
ToastXC
41ef76ff3e feat: member fetching with roles (#299) 2024-02-06 13:20:35 +00:00
Paul Makles
c451e431cd merge: pull request #301 from TheBobBobs/bonfire-redis
fix(bonfire): random disconnects from redis
2024-02-06 13:18:55 +00:00
Amy
09a848f5a6 fix: don't exceed the max emoji limit by one
closes #295
2024-02-06 13:17:13 +00:00
Bob Bobs
308193377d fix(bonfire): use REDIS_URI env var 2024-02-02 10:31:11 -07:00
Bob Bobs
3d3326e68a (bonfire): parse correct PayloadType 2024-02-01 20:07:12 -07:00
Bob Bobs
88c2232a6c fix(bonfire): use fred for redis
https://github.com/aembke/fred.rs
2024-02-01 19:00:45 -07:00
Bob Bobs
25016efbc2 fix(database): fetch all server emojis instead of one 2023-12-02 11:46:18 +00:00
Bob Bobs
db63ac4064 fix: add emojis to ServerCreate event 2023-12-02 11:46:18 +00:00
TheBobBobs
1f593a1708 fix: allow reacting with existing emojis when at limit 2023-12-02 11:45:23 +00:00
TheBobBobs
219c16a69c fix: set relationship for users in BulkMessageResponse 2023-12-01 20:54:59 +00:00
Paul Makles
aed8f69f34 fix(database): set channel ids for new server object 2023-11-20 10:54:48 +00:00
Paul Makles
196a856c68 refactor: move and update fixme / todo comments 2023-10-28 14:53:39 +01:00
Paul Makles
9bf60e87e9 fix: validate masquerade on new message models 2023-10-28 14:53:28 +01:00
Paul Makles
b90e7a4412 fix: prevent potential double fetching of cached server 2023-10-28 14:53:12 +01:00
Paul Makles
cb813e91f0 chore: cut new 0.6.9 release 2023-10-28 14:39:10 +01:00
Paul Makles
a6bc9fcbb3 refactor: implement Server::create 2023-10-28 14:25:29 +01:00
Paul Makles
cd0737911b refactor: use fixtures to populate db
refactor: method for creating server channels
2023-10-27 16:31:37 +01:00
Paul Makles
7705354d4d ci: use reference db for generating spec 2023-10-26 15:07:03 +01:00
Paul Makles
39ac8b2278 refactor: don't create unnecessary reference 2023-10-26 14:12:28 +01:00
arslee07
c5494aa1ca fix: respect Permission::SendEmbeds when editing a message 2023-10-26 14:12:28 +01:00
Paul Makles
1a96bf5a4b chore: remove redundant file [skip ci] 2023-10-26 14:05:01 +01:00
Paul Makles
37a1244751 test: ignore hello world test because it breaks cargo test? 2023-10-26 14:05:01 +01:00
Paul Makles
bf39f18f8d fix: always generate new test database 2023-10-26 14:05:01 +01:00
Paul Makles
95be7dc87a ci: use nextest because cargo test sig sevs 2023-10-26 14:05:01 +01:00
Paul Makles
9be171c7b6 fix: don't copy the old test file 2023-10-26 14:05:01 +01:00
Paul Makles
786fbb8ae4 chore: disable broken tests 2023-10-26 14:05:01 +01:00
Paul Makles
6c0210e8a1 feat: always use test configuration during tests 2023-10-26 14:05:01 +01:00
Paul Makles
456bf7b42a fix: check server ownership for text channel
refactor: minor cleanup of code
2023-10-26 14:05:01 +01:00
Paul Makles
21ffea0f9f fix(core/database): store member during permission query 2023-10-26 14:05:01 +01:00
Paul Makles
96d90215d2 fix(core/models): validator feature flag not properly gatekept 2023-10-26 14:05:01 +01:00
Paul Makles
29dcf714f2 chore: start work on test for permission issue 2023-10-26 14:05:01 +01:00
Paul Makles
cb97004d3f feat(core): provide user profile where appropriate 2023-10-16 09:33:36 +01:00
Paul Makles
0b6e5dd9db chore: add nix file to vscode config 2023-10-16 09:23:11 +01:00
Paul Makles
7132877201 feat(core): validation for files in reference db 2023-10-16 09:22:53 +01:00
Paul Makles
4d42fb74e7 fix(core): publish user settings event 2023-10-16 09:22:17 +01:00
Paul Makles
866ace3381 refactor: use config for max bot count 2023-10-16 09:21:48 +01:00
Paul Makles
0798e81862 feat(core): implement ratelimit events for reference db 2023-10-16 09:21:34 +01:00
Paul Makles
2fa5ac41ac refactor: tests for more group routes 2023-09-22 19:04:17 +01:00
Paul Makles
25ae1555a2 chore: add nextest and fmt to nix 2023-09-22 19:03:49 +01:00
Paul Makles
881888bd1c chore: add timeout for nextest runs 2023-09-22 19:03:29 +01:00
Paul Makles
41f20c2239 ci: use custom Revolt.toml for tests 2023-09-10 15:46:00 +01:00
Paul Makles
38f1381fea chore: switch to resolver=2 2023-09-10 15:19:23 +01:00
Paul Makles
6838b0b1f0 refactor: ensure in-memory database consistency
test: update authifier to allow for in-memory tests
ci: explicitly run in-memory and mongo tests
2023-09-10 14:49:27 +01:00
Paul Makles
055deab105 fix: allow bots to authenticate with new routes 2023-09-10 14:48:13 +01:00
Paul Makles
6d2073120d fix: carry over send embed permission when sending [skip ci] 2023-09-10 12:41:23 +01:00
Paul Makles
9789909061 feat(delta): rewrite emoji routes w/o quark
#283
2023-09-05 12:36:18 +01:00
Paul Makles
895de86f1e fix(core/config): use uppercase filename 2023-09-03 19:00:17 +01:00
Paul Makles
98b8c5403b feat(delta): rewrite channel / message fetch 2023-09-03 18:27:21 +01:00
Paul Makles
aa7c75c60e chore: ignore Revolt.toml 2023-09-03 18:12:15 +01:00
Paul Makles
4270f0c5d4 feat(core): implement create group 2023-09-03 18:12:08 +01:00
Paul Makles
eb1f45d208 fix(core/database): use attachments collection name 2023-09-03 17:43:13 +01:00
Paul Makles
9929f7499a refactor(core): use core crates for sending messages 2023-09-03 16:31:39 +01:00
Paul Makles
279d9ef1b5 refactor(core): remove quark references from webhook routes 2023-09-03 15:52:18 +01:00
Paul Makles
5a9bb9e68d fix: convert index map/set to hash map/set
fix: idempotency token should use correct crate cfg
2023-08-27 15:51:20 +01:00
Paul Makles
a8fc9e928a fix: import validator crate directly in derive 2023-08-27 15:45:23 +01:00
Paul Makles
da231ae390 fix: remove indexmap from models 2023-08-27 15:43:57 +01:00
Paul Makles
7a31da4189 fix: add indexmap/serde to model depends 2023-08-27 15:40:05 +01:00
Paul Makles
7d0b5392b5 fix: should include crate in build script too 2023-08-27 15:33:28 +01:00
Paul Makles
1e1d44c0e4 fix: add config crate to Dockerfile
chore: bump release version
2023-08-27 15:30:53 +01:00
Paul Makles
1c4588967f merge: branch 'insert/dev-branch' 2023-08-27 15:18:52 +01:00
Paul Makles
853366a297 feat: add support for Firebase Cloud Messaging 2023-08-27 15:18:19 +01:00
Paul Makles
7568f49755 test: add tests to bot routes 2023-08-27 15:01:22 +01:00
Paul Makles
0542788567 feat(delta): test harness for web server 2023-08-27 13:01:21 +01:00
Paul Makles
73f7b8f007 feat: ensure bot invites work correctly 2023-08-27 12:21:48 +01:00
Paul Makles
c2412190a8 refactor: solve clippy hints 2023-08-27 11:23:02 +01:00
Paul Makles
5372296dc0 feat(core): implement message tasks
includes:
- acknowledgements
- last_message_id
- embeds
- web push
2023-08-27 11:17:03 +01:00
Paul Makles
41a47bdf8f feat: add revolt_config crate 2023-08-27 11:15:20 +01:00
Paul Makles
51c26e324d refactor: remove quark/web 2023-08-26 16:19:00 +01:00
Paul Makles
3a55d00c6a chore: disable nix environment extension 2023-08-26 16:17:21 +01:00
Paul Makles
004e84ece9 fix: revoke all channel permissions if can't view 2023-08-26 16:17:20 +01:00
Paul Makles
3d0914ce52 chore: add nix shell configuration 2023-08-26 16:17:19 +01:00
Paul Makles
bde432cb75 chore: strip bot model from quark 2023-08-26 16:16:48 +01:00
Paul Makles
28d225952a chore: add separate debug Docker build script 2023-08-26 15:34:25 +01:00
Paul Makles
2fb9e7a802 merge: branch 'master' into insert/dev-branch 2023-08-26 13:34:44 +01:00
Paul Makles
431046cb4c chore: cut new release with redis patches and debug profile 2023-08-26 13:33:05 +01:00
Zomatree
92032cd9bc chore: Log errors from failed pub/sub messages 2023-08-26 13:14:24 +01:00
Lea
bfdd713d38 fix: allow fetching mutuals for bots 2023-08-26 13:14:15 +01:00
Paul Makles
430df03b20 chore: bump sentry crate 2023-08-10 09:42:44 +01:00
Paul Makles
f5f70287e7 feat: add SENTRY_DSN env variable 2023-08-10 09:40:41 +01:00
Paul Makles
d5ba4ebf0c feat(core/database): implement permissions backend 2023-08-05 17:42:23 +01:00
Paul Makles
3bfead4ff1 feat(core/database): system message helper 2023-08-05 17:03:16 +01:00
Paul Makles
d04621b1d6 feat(core): implement message db methods 2023-08-05 16:59:58 +01:00
Paul Makles
a681df04bd feat(core): permissions query, finish bots impl 2023-08-05 16:14:47 +01:00
Paul Makles
9f3c1036d0 chore: refine user and bot creation methods 2023-08-05 12:06:43 +01:00
Paul Makles
c9011ac692 chore: migrate bots create route 2023-08-05 11:24:02 +01:00
Paul Makles
42f977f536 feat: start work on migrating routes 2023-08-03 22:11:48 +01:00
Paul Makles
a0580c5f5a chore: remove legacy query route 2023-08-03 19:43:05 +01:00
Paul Makles
121a9cd87c feat(core): implement message model
closes #273
2023-08-03 19:32:47 +01:00
Paul Makles
d87d608d9e chore(core): flesh out implementations for channels / servers 2023-08-03 18:27:13 +01:00
Paul Makles
bd9ecc1519 fix: missing conversion 2023-08-02 00:31:19 +01:00
Paul Makles
f5a113da47 feat(core): implement server ban
closes #274
2023-08-01 21:19:07 +01:00
Paul Makles
11fdb0c1dc feat(core): implement emoji model
closes #270
2023-08-01 20:45:58 +01:00
Paul Makles
e0033ceb12 feat(core): implement channel unreads model
closes #269
2023-08-01 17:41:43 +01:00
Paul Makles
7318ec6ef6 chore(core/models): add cfg_attr where necessary 2023-08-01 15:54:56 +01:00
Paul Makles
a516c7adcf feat(core): implement channel invite model
closes #268
2023-08-01 15:53:43 +01:00
Paul Makles
c48109ca66 docs: add note for 'Id' casing 2023-08-01 15:51:57 +01:00
Paul Makles
f0ef781cd7 chore: add MSRV to README [skip ci]
closes #254
2023-08-01 15:04:45 +01:00
Paul Makles
d9dd040db4 feat(core/models): implement server members model
closes #280
2023-08-01 15:00:07 +01:00
Paul Makles
fec947b4c8 feat(core/models): implement server model
closes #279
2023-08-01 14:55:25 +01:00
Paul Makles
ec6df36c25 chore: strip legacy admin API 2023-08-01 14:35:36 +01:00
Paul Makles
76150db293 chore: add prometheus metrics 2023-07-04 17:52:40 +01:00
Paul Makles
815c3fe99e chore: cut a new release (0.6.4) 2023-07-02 08:45:39 +01:00
Zomatree
54878e8e8d feat: add webhook permissions 2023-07-02 08:41:38 +01:00
TheBobBobs
6e4798f1d4 fix(bonfire): populate cache for calculating permissions (#259) 2023-07-02 08:40:15 +01:00
TheBobBobs
55bd6fb087 fix: incorrect owner rank 2023-07-02 08:35:49 +01:00
Paul Makles
49035f4817 feat: ratelimit user edit route and discriminator changes 2023-06-15 19:24:53 +01:00
Paul Makles
c0ebaa0bd3 chore: include discriminator in ban list response 2023-06-15 19:23:47 +01:00
Paul Makles
b98b244fc3 fix: enable staging for API spec generation 2023-06-15 15:21:00 +01:00
Paul Makles
c8d5128b0c chore: add additional sanitisation 2023-06-11 12:26:18 +01:00
Paul Makles
0578a05a05 feat: add remove "displayname" field 2023-06-11 12:05:08 +01:00
Paul Makles
9a412b3e08 fix: actually update the display name 2023-06-11 11:50:21 +01:00
Paul Makles
8deec1f80a fix: display name is optional 2023-06-11 10:56:03 +01:00
Paul Makles
26afbeed84 chore: extend discriminator block list 2023-06-11 10:49:51 +01:00
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 Makles
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
Paul Makles
4abd4070f7 feat: allow editing owned bots using user edit route 2023-05-31 17:10:57 +01:00
Paul Makles
bca17b11a7 feat: filter reports on fetch 2023-05-31 17:04:33 +01:00
Paul Makles
2a6d532852 feat: allow fetching channels with server 2023-05-31 14:05:46 +01:00
Paul Makles
a9a5af8cc8 feat: account strike CRUD API 2023-05-31 13:39:13 +01:00
Paul Makles
edfa8e5256 feat(core/models): add account strikes 2023-05-31 13:06:16 +01:00
Paul Makles
2ebcfdc770 refactor: clean up hints 2023-05-31 13:02:59 +01:00
Paul Makles
fc47df786b feat(core/database): add account strikes model 2023-05-31 13:01:19 +01:00
Paul Makles
f6aa405607 fix(core/database): create user for bot CRUD test 2023-05-31 12:58:36 +01:00
Paul Makles
101e8feaa2 fix: fallback to empty string when deserialising 2023-05-25 11:25:25 +01:00
ToastXC
df8f588e15 fix: remove check for bot account under group member fetch (#245) 2023-05-21 18:34:27 +01:00
Paul Makles
3ef549537e feat: de-duplicate user update events sent to clients 2023-05-21 16:22:42 +01:00
Paul Makles
d81d08f1ce feat: add a route for searching for server members 2023-05-21 16:09:14 +01:00
Paul Makles
a77b7717b8 feat: generate and fetch multiple snapshots 2023-05-21 15:36:36 +01:00
Paul Makles
d5f903781d feat: add more specificity to report reasons 2023-05-21 15:35:16 +01:00
Paul Makles
8b12546b56 fix: allow editing flags and discoverable 2023-05-15 17:18:33 +01:00
Paul Makles
5bb91a7113 merge: pull request #242 from Rexogamer/patch-2
chore: bump max bots count to 10
2023-05-15 17:17:17 +01:00
Sophie L
a04c2512b4 chore: bump max bots count to 10 2023-04-27 16:30:24 +01:00
Paul Makles
c3f0df6567 chore: add style guide [skip ci] 2023-04-27 12:04:50 +01:00
Paul Makles
8b8aec4dc1 feat(core/database): implement server member 2023-04-26 18:58:31 +01:00
Paul Makles
1d228ff76e feat(core/database): implement server model 2023-04-26 18:32:29 +01:00
Paul Makles
b5863c0104 feat(core/database): implement user settings model 2023-04-26 17:53:46 +01:00
Paul Makles
08ccc43f8d feat(core/database): implement user operations 2023-04-26 17:36:51 +01:00
Paul Makles
e33ae17061 refactor(core/database): AbstractAttachment -> AbstractAttachments 2023-04-26 16:51:52 +01:00
Paul Makles
60ebdb64d7 feat(core/database): implement File operations 2023-04-26 16:49:39 +01:00
Paul Makles
c27d0ad5bb chore: move relations.drawio 2023-04-26 16:45:51 +01:00
Paul Makles
c1b9d941f3 refactor(core/database): crud ordering in files 2023-04-26 16:36:38 +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
Paul Makles
1afb27a508 ci: compile in permissions crate 2023-04-24 10:08:22 +01:00
Paul Makles
6a8481f7df refactor: is-patched transitive through models 2023-04-23 23:09:21 +01:00
Paul Makles
7f201565c0 feat: implement unoptimised filter_online if redis is not patched 2023-04-23 23:06:51 +01:00
Paul Makles
ae9474b95d fix(core/database): gate rocket-impl properly 2023-04-23 22:17:34 +01:00
Paul Makles
050f3abc89 chore: update crate definitions and bump versions 2023-04-23 22:14:38 +01:00
Paul Makles
888c22cb54 feat(core/database): start implementing permissions 2023-04-23 22:12:15 +01:00
Paul Makles
f633fccbca feat(core/permissions): initial commit 2023-04-23 22:11:59 +01:00
Paul Makles
39fec310f9 refactor(core/models): move models into v0 module 2023-04-23 18:09:16 +01:00
Paul Makles
d5d922d830 feat(core/models): rest of the conversions to User model 2023-04-23 18:07:23 +01:00
Paul Makles
f8c8407af3 feat(core/models): basic implementation of User and File models 2023-04-23 17:44:36 +01:00
Paul Makles
e6d0d44c5a feat(core/database): basic implementation of User and File models 2023-04-23 17:44:23 +01:00
Paul Makles
1933c9ea3d feat(core/database): add Reference struct 2023-04-23 14:59:44 +01:00
Paul Makles
8a695b4bb5 ci: actually make the source folders for building deps 2023-04-23 09:03:37 +01:00
Paul Makles
bf71b45fbb ci: copy core crates into Dockerfile 2023-04-23 08:51:43 +01:00
Paul Makles
9124c9f1e3 ci: include new core crates in build 2023-04-22 23:42:17 +01:00
Paul Makles
bcc2d05625 merge: branch 'refactor/split-project-into-core-crates' 2023-04-22 21:13:45 +01:00
Paul Makles
22bfd720b5 merge: remote-tracking branch 'origin/master' into refactor/split-project-into-core-crates 2023-04-22 21:09:51 +01:00
Paul Makles
63f56aec0c ci: re-order services and testing 2023-04-22 20:30:02 +01:00
Paul Makles
dd3d7e9c49 fix(core/database): use configured database name for migrations 2023-04-22 20:15:10 +01:00
Paul Makles
dbb66edd9f chore(core/database): delete useless example binary 2023-04-22 20:07:56 +01:00
Paul Makles
8bfb48dff3 test(core/database): try migrating the database 2023-04-22 20:02:23 +01:00
Paul Makles
12d963d2bd refactor: create presence crate and add tests 2023-04-22 20:01:23 +01:00
Paul Makles
c817c2dd40 fix: don't use wildcard for log dependency 2023-04-22 18:03:19 +01:00
Paul Makles
8a230ba989 chore: change publishing order 2023-04-22 18:02:01 +01:00
Paul Makles
2a9cc3190c fix: forgot to specify version for result crate 2023-04-22 18:01:44 +01:00
Paul Makles
e84d55a697 chore: update Cargo.toml for core crates 2023-04-22 18:01:06 +01:00
Paul Makles
403a94f70c feat(delta): example implementation of new core models 2023-04-22 17:56:48 +01:00
Paul Makles
633eb78630 chore: migrate to revolt_optional_struct 2023-04-22 17:56:24 +01:00
Paul Makles
bbe1f4936c chore: bridge gap between core/result and quark 2023-04-22 17:56:09 +01:00
Paul Makles
11a87263be feat(core/models): implement Bot and PublicBot 2023-04-22 17:55:30 +01:00
Paul Makles
8b30dddc06 chore(core/result): add schema feature 2023-04-22 17:55:12 +01:00
Paul Makles
b93dd90caf refactor(core/database): use empty strings instead of options 2023-04-22 17:54:46 +01:00
Paul Makles
0054019f82 ci: run test on all pushes 2023-04-22 16:05:40 +01:00
Paul Makles
736220a94e ci: run tests with and without MongoDB 2023-04-22 16:03:08 +01:00
Paul Makles
a9c82791b3 fix: force OOP bot updates to avoid inconsistencies 2023-04-22 16:01:27 +01:00
Paul Makles
eacf4decab refactor: clean up database before and after tests 2023-04-22 16:01:07 +01:00
Paul Makles
b8cda2ec74 feat: add drop_database for tests 2023-04-22 16:00:57 +01:00
Paul Makles
69ab7e031b chore: add linting rules for disallowed methods 2023-04-22 16:00:30 +01:00
Paul Makles
ace6431cb8 fix: ensure database namespace is valid 2023-04-22 15:24:46 +01:00
Paul Makles
40790de909 feat: macros for reducing error boilerplate 2023-04-22 15:23:20 +01:00
Paul Makles
750037b5d2 test: use custom database name and add clean up 2023-04-22 14:58:55 +01:00
Paul Makles
6b10385c0d feat: implement bots including tests
this starts work on #78
2023-04-22 14:25:02 +01:00
Paul Makles
56ead0f894 feat: write result crate 2023-04-22 12:48:09 +01:00
Paul Makles
d633cba630 fix: migration example 2023-04-22 12:12:23 +01:00
Paul Makles
e43833c0ea refactor: combine models and db crate together 2023-04-22 12:12:02 +01:00
Paul Makles
7f86337cb2 refactor: split SISMEMBER into two commands 2023-04-22 10:41:12 +01:00
Paul Makles
f2bb388b3b refactor: minimum implementation of revolt-models and revolt-database 2023-04-22 00:01:56 +01:00
Paul Makles
43e45aef3f fix: patch redis-rs for SMISMEMBER support 2023-04-21 19:37:33 +01:00
Paul Makles
c5880596a4 refactor: use a separate online user ID set 2023-04-21 19:36:25 +01:00
Paul Makles
59a644891d fix: collect IDs as set first to deduplicate entries 2023-04-21 18:52:41 +01:00
Paul Makles
a326cdc736 refactor: remove unused import 2023-04-21 18:40:00 +01:00
Paul Makles
03a28dbb3e fix: use debian:bullseye image for glibc 2.29 2023-04-21 18:11:06 +01:00
Paul Makles
32542a822e feat: rewrite presence system to use sets 2023-04-21 17:28:13 +01:00
Paul Makles
1df90ff53b chore: update DSN for Sentry 2023-04-21 17:27:45 +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
ToastXC
487b979f0d fix: remove check for bot account fetching invites from a server (#237) 2023-04-20 11:05:13 +01:00
ToastXC
b9d813d8f0 fix: deny access for bot creating a server (#238) 2023-04-20 11:04:51 +01:00
Paul Makles
36dd128459 merge: pull request #234 from thebearingedge/arm64-support 2023-04-20 11:04:35 +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
144f0d39c6 fix: Add limits to channels and roles 2023-04-10 17:39:21 +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
Tim Davis
32a294a64a feat: add support for arm64 (#142)
- set explicit action permissions
- make package owner dynamic so forks can test workflow
- build multi platform images using cross compilation amd64 -> arm64
- add a scope to buildx gha cache
2023-03-21 12:23:57 -07:00
Paul Makles
b7b70346b4 fix: add additional checks to group members 2023-03-18 22:32:15 +00:00
Paul Makles
59832e6ba2 fix(ratelimiter): mutate entry instead of copying 2023-03-15 21:54:58 +00:00
Zomatree
f309218573 chore: cleanup code 2023-03-15 19:47:30 +00:00
Zomatree
0321eff62b chore: move to once_cell from lazy_static 2023-03-15 13:47:32 +00:00
Zomatree
01577fd798 avoid leaking the token in events 2023-03-11 23:24:10 +00:00
Paul Makles
4c8ea31d98 feat(admin): add global message query route 2023-03-11 17:26:48 +00:00
Paul Makles
92ac86a6bd chore: bump version
fix: separate privileged check when editing server
2023-03-11 17:04:30 +00:00
Paul Makles
13ed69c82a feat: privileged user editing 2023-03-11 16:53:25 +00:00
Paul Makles
b83f6da648 feat: allow editing server flags through edit route 2023-03-11 16:40:23 +00:00
Paul Makles
89f1167239 refactor: unify message query into one method 2023-03-11 16:37:48 +00:00
Paul Makles
82d868751f chore: add privileged override for user permission 2023-03-04 20:45:49 +00:00
Paul Makles
31b9f18921 chore: bump version and publish 0.5.13 2023-03-04 11:19:53 +00:00
Paul Makles
fd80823910 fix: ensure we have send perm to edit message 2023-03-04 11:18:18 +00:00
Paul Makles
6bd8221eda feat(core): add raw db statisics endpoint for admin panel 2023-03-04 11:18:05 +00:00
Paul Makles
ce77e926a5 fix(safety): implement fetch_snapshot fully 2023-03-03 20:14:30 +00:00
Paul Makles
2710edb76b feat(safety): add api route for fetching a report 2023-03-03 08:17:55 +00:00
Paul Makles
8f1ff9e774 feat(safety): add API route for editing report 2023-03-02 12:54:55 +00:00
Paul Makles
c09244039e chore: bump and release version 2023-03-01 19:14:31 +00:00
Paul Makles
bf9108408e feat(safety): fetch additional context for snapshot 2023-03-01 15:09:17 +00:00
Zomatree
fca91e1560 update upstream code 2023-03-01 11:49:28 +00:00
Paul Makles
304336d905 feat(safety): fetch reports and fetch snapshot API 2023-03-01 11:22:22 +00:00
Paul Makles
42b4906594 chore: add database migrations for safety 2023-03-01 11:21:58 +00:00
Angelo Kontaxis
3c881a40a6 Merge branch 'master' into webhooks 2023-02-23 17:26:59 +00:00
Paul Makles
3038fb230f feat(safety): save surrounding context around messages (closes #226) 2023-02-23 18:12:45 +01:00
Paul Makles
73af105feb feat(safety): add global event for report creation (closes #225) 2023-02-23 17:58:05 +01:00
Paul Makles
2de37b23c1 merge: branch local cleanup branch 2023-02-22 12:45:39 +01:00
Paul Makles
0c75d8c8b3 refactor: remove old .github directories [skip ci] 2023-02-22 12:44:46 +01:00
Paul Makles
be793b1570 chore(ci): update commit author [skip ci] 2023-02-22 12:44:24 +01:00
Paul Makles
5a3a97df34 merge: pull request #223 from revoltchat/insert/feat/user-safety-api 2023-02-22 08:52:37 +00:00
Paul Makles
0c072b01d7 fix(safety): prevent reporting own content or self 2023-02-21 12:42:08 +01:00
Paul Makles
056c0380b2 feat(safety): implement report content API 2023-02-21 12:38:35 +01:00
Paul Makles
49598daf70 fix: port user safety API to new codebase 2023-02-17 09:45:34 +00:00
Paul Makles
0445181d5b merge: branch 'master' into insert/feat/user-safety-api 2023-02-17 09:43:12 +00:00
Paul Makles
e780178cc0 fix(ci): move token definition to correct stage 2023-01-29 16:46:19 +00:00
Paul Makles
c4b9884825 fix(ci): use correct token 2023-01-29 16:36:44 +00:00
Paul Makles
fdab997d17 chore(ci): switch testing to stable Rust 2023-01-29 14:23:56 +00:00
Paul Makles
8e0addc3e0 fix: fallback properly if no VERGEN strings can generate 2023-01-29 14:22:57 +00:00
Paul Makles
0da4baf777 chore: bump version and clean up repo 2023-01-29 14:16:18 +00:00
Paul Makles
c03fcda85b refactor: switch from "rauth" to Authifier crate 2023-01-29 14:14:35 +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
Paul Makles
0afbcc065f refactor: clippy warning for empty list 2023-01-27 16:18:00 +00:00
Paul Makles
a406e5504d chore: add support for Streamable in media struct 2023-01-27 16:17:47 +00:00
Zomatree
e8c233f60b feat: concurrently query January for embeds (#221) 2023-01-27 16:06:27 +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
insertish
72595f30d4 ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2023-01-24 19:43:40 +00:00
insertish
39e345af74 ci: synced local '.github/workflows/triage_issue.yml' with remote 'workflows/triage_issue.yml'
[skip ci]
2023-01-24 19:43:40 +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
Paul Makles
e0b918771d chore: ensure restrict_reactions is used in conjunction with reactions list 2023-01-20 18:06:44 +00:00
Paul Makles
7a6bd70dcd feat: add route to fetch user flags
chore: bump rauth to change disabled account behaviour
2023-01-20 17:47:26 +00:00
Paul Makles
c0ef3d295a fix: fallback if vergen cannot generate data 2023-01-20 17:14:25 +00:00
Paul Makles
f5c95f9d12 chore: bump rauth for lockout reset on pwd reset 2023-01-20 15:57:58 +00:00
Paul Makles
fed1da72fd feat: support rauth events
feat: add platform user wipe event
2023-01-20 15:43:45 +00:00
Paul Makles
cf9c0bc5e1 chore: bump rauth 2023-01-20 14:00:19 +00:00
Paul Makles
cc2054838a chore: swap okapi for revolt_okapi 2023-01-20 14:00:19 +00:00
Zomatree
a246cff7f0 Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-18 21:03:15 +00:00
Paul Makles
d9d2735efa fix(ratelimiter): re-calculate reset time
closes #204

Co-authored-by: TheBobBobs <84781603+TheBobBobs@users.noreply.github.com>
2023-01-18 18:26:00 +00:00
Ryan
94b0472934 feat: pass server flags with invite data (#203) 2023-01-18 18:17:59 +00:00
Sophie L
4a7917e9ea feat: enable reactions by default in servers/DMs 2023-01-18 18:17:37 +00:00
Lea
ce6b454874 fix: raise masquerade avatar URL length limit 2023-01-18 18:17:05 +00:00
ToastXC
fc32996b51 chore: remove clippy warnings (#213) 2023-01-18 18:16:27 +00:00
Paul Makles
d74104fae4 feat: initial ideas for content reporting 2023-01-18 12:35:49 +00:00
Paul Makles
32d3683013 fix(ci): re-order docker-compose arguments
chore(ci): prevent running gen on PR
2023-01-14 15:53:09 +00:00
Paul Makles
0cebb69e02 feat: automate generating OpenAPI specification 2023-01-14 15:42:30 +00:00
Paul Makles
117958d8cd feat: include build information in root api call 2023-01-14 15:42:06 +00:00
Paul Makles
c5b823ad13 chore: clippy warnings 2023-01-03 21:06:43 +00:00
Paul Makles
27b4699299 fix: unify foreign user fetch into one method 2023-01-03 21:04:04 +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
Paul Makles
ee1c4cc2d5 chore: bump quark
feat: update email templates to include recipient
2022-11-19 16:52:51 +00:00
Paul Makles
0e0daf86f7 feat: update emails to not look garbage 2022-10-23 21:23:29 +01:00
Paul Makles
572e08c733 chore: update rauth 2022-10-23 21:08:08 +01:00
Paul Makles
9011939344 chore: remove .env file 2022-10-23 20:55:45 +01:00
Paul Makles
f4dcd091cc feat: add 'Focus' presence status 2022-09-18 12:12:31 +01:00
Paul Makles
86ad72b426 fix: strip unknown channels from categories (fixes #201) 2022-09-12 21:39:29 +01:00
Paul Makles
044d82d566 fix: use correct validation for masq role colour 2022-09-03 10:06:49 +01:00
Paul Makles
deffe663cb feat: add ability to leave groups / servers silently 2022-09-02 15:32:11 +01:00
Paul Makles
e7f6433d77 fix: actually transfer group ownership 2022-09-01 09:45:17 +01:00
Paul Makles
345e4d5f13 fix: validate categories 2022-09-01 09:41:00 +01:00
Sophie L
81fc3e7760 fix: set the cap to 100 2022-09-01 09:01:43 +01:00
Paul Makles
ca8d2e6118 fix: reaction ordering by using indexmap 2022-08-14 11:47:30 +02:00
Paul Makles
c1227e8e33 fix: check React permission on interactions 2022-08-14 11:31:36 +02:00
Paul Makles
0e0c6f2c3a fix: validate number of reactions on message when adding more 2022-08-13 20:26:09 +02:00
Paul Makles
fa82be74b5 fix: adjust permission checks for member_edit 2022-07-26 14:06:53 +01:00
Paul Makles
8491ced13d fix: ensure list matches 2022-07-18 12:46:33 +01:00
Paul Makles
229d4e2e1d fix: correct ordering for creating ownership change event 2022-07-15 21:46:41 +01:00
Paul Makles
7bdd2d69d6 feat: add message reactions + interactions object 2022-07-15 21:24:49 +01:00
Paul Makles
ab2af9b5e5 feat: add group ownership transfer 2022-07-15 17:58:08 +01:00
Paul Makles
5aebda2d16 chore: restrict emojis to lowercase 2022-07-15 17:57:53 +01:00
Paul Makles
a7273d0556 feat: allow servers to set discoverable value 2022-07-15 17:57:30 +01:00
Paul Makles
72aed64e5b chore: allow banning users not in server 2022-07-15 16:47:27 +01:00
Paul Makles
00ad4d54e7 feat: add nsfw flag to emotes 2022-07-15 16:34:08 +01:00
Paul Makles
cba8c4ef3b fix: add case-insensitive flag to colour regex 2022-07-15 16:13:04 +01:00
Paul Makles
b6e395fe0d fix: use default for roles key 2022-07-15 16:02:58 +01:00
Paul Makles
8d07457c58 chore: make joined_at migration shorter 2022-07-15 16:02:46 +01:00
Paul Makles
f1171e5358 feat: restrict role colours to regex (increase length limit) 2022-07-15 15:36:00 +01:00
Paul Makles
d96c9f62c4 chore: detach emojis on delete 2022-07-15 14:01:59 +01:00
Paul Makles
4f73e43a03 feat: restrict permissions for users in timeout 2022-07-15 13:55:55 +01:00
Paul Makles
741b8ee8fd feat: add role colours to masquerades
feat: add `joined_at` property to members
feat: add `timeout` property to members (non-functional)
2022-07-15 13:42:01 +01:00
Paul Makles
692081b7f0 chore: bump rauth for verification fix 2022-07-14 14:01:10 +01:00
Paul Makles
943b1f08f0 fix: prevent short-circuit on permissions 2022-07-10 18:27:22 +01:00
367 changed files with 27964 additions and 3809 deletions

2
.config/nextest.toml Normal file
View File

@@ -0,0 +1,2 @@
[profile.default]
slow-timeout = { period = "3s", terminate-after = 2 }

79
.env
View File

@@ -1,79 +0,0 @@
# MongoDB URI
MONGODB=mongodb://localhost
# URL to where the Revolt app is publicly accessible
REVOLT_APP_URL=http://local.revolt.chat:5000
# URL to where the API is publicly accessible
REVOLT_PUBLIC_URL=http://local.revolt.chat:8000
VITE_API_URL=http://local.revolt.chat:8000
# URL to where the WebSocket server is publicly accessible
REVOLT_EXTERNAL_WS_URL=ws://local.revolt.chat:9000
# URL to where Autumn is publicly available
AUTUMN_PUBLIC_URL=http://local.revolt.chat:3000
# URL to where January is publicly available
JANUARY_PUBLIC_URL=http://local.revolt.chat:7000
# URL to where Vortex is publicly available
# VOSO_PUBLIC_URL=https://voso.revolt.chat
##
## hCaptcha Settings
##
# If you are sure that you don't want to use hCaptcha, set to 1.
REVOLT_UNSAFE_NO_CAPTCHA=1
# hCaptcha API key
# REVOLT_HCAPTCHA_KEY=0x0000000000000000000000000000000000000000
# hCaptcha site key
# REVOLT_HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
##
## Email Settings
##
# If you are sure that you don't want to use email verification, set to 1.
REVOLT_UNSAFE_NO_EMAIL=1
# SMTP host
# REVOLT_SMTP_HOST=smtp.example.com
# SMTP username
# REVOLT_SMTP_USERNAME=noreply@example.com
# SMTP password
# REVOLT_SMTP_PASSWORD=CHANGEME
# SMTP From header
# REVOLT_SMTP_FROM=Revolt <noreply@example.com>
##
## Application Settings
##
# Whether to only allow users to sign up if they have an invite code
REVOLT_INVITE_ONLY=0
# Maximum number of people that can be in a group chat
REVOLT_MAX_GROUP_SIZE=150
# VAPID keys for push notifications
# Generate using this guide: https://gitlab.insrt.uk/revolt/delta/-/wikis/vapid
# --> Please replace these keys before going into production! <--
REVOLT_VAPID_PRIVATE_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw=
##
## Vortex configuration
##
# VOSO_MANAGE_TOKEN=CHANGEME

View File

@@ -59,6 +59,9 @@ REVOLT_UNSAFE_NO_EMAIL=1
## 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
REVOLT_INVITE_ONLY=0

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
ko_fi: insertish
custom: https://insrt.uk/donate

View File

@@ -20,47 +20,52 @@ on:
- "Dockerfile"
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
base:
runs-on: ubuntu-latest
name: Build base image (amd64)
name: Build base image
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Authenticate with GHCR
- name: Login to Github Container Registry
uses: docker/login-action@v1
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Build all projects and cache
- name: Build Base Image
uses: docker/build-push-action@v3
- name: Build base image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/revoltchat/base:latest
cache-from: type=gha
cache-to: type=gha,mode=max
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest
cache-from: type=gha,scope=buildx-base-multi-arch
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
publish_amd64:
publish:
needs: [base]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
strategy:
matrix:
project: [delta, bonfire]
name: Build ${{ matrix.project }} image (amd64)
name: Build ${{ matrix.project }} image
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -68,10 +73,11 @@ jobs:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -86,11 +92,11 @@ jobs:
{
"delta": {
"path": "crates/delta",
"tag": "revoltchat/server"
"tag": "${{ github.repository_owner }}/server"
},
"bonfire": {
"path": "crates/bonfire",
"tag": "revoltchat/bonfire"
"tag": "${{ github.repository_owner }}/bonfire"
}
}
export_to: output
@@ -98,19 +104,21 @@ jobs:
# Configure metadata
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v4
with:
images: ${{ steps.export.outputs.tag }}, ghcr.io/${{ steps.export.outputs.tag }}
images: |
docker.io/${{ steps.export.outputs.tag }}
ghcr.io/${{ steps.export.outputs.tag }}
# Build crate image
- name: Publish
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64
platforms: linux/amd64,linux/arm64
file: ${{ steps.export.outputs.path }}/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,10 +1,9 @@
name: Rust build and test
name: Rust build, test, and generate specification
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
branches: [master]
env:
CARGO_TERM_COLOR: always
@@ -15,19 +14,75 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install latest nightly
- name: Install latest stable
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
components: rustfmt, clippy
toolchain: stable
override: true
components: rustfmt, clippy
- name: Install cargo-nextest
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-nextest
locked: true
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
- name: Run services in background
run: |
docker-compose -f docker-compose.db.yml up -d
- name: Copy .env.example
run: |
cp .env.example .env
- name: Run cargo test
uses: actions-rs/cargo@v1
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
if: github.event_name != 'pull_request' && github.ref_name == 'master'
env:
TEST_DB: REFERENCE
run: |
cargo run --bin revolt-delta &
- name: Wait for API to go up
if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: nev7n/wait_for_response@v1
with:
command: test
url: "http://localhost:8000/"
- name: Checkout API repository
if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: actions/checkout@v3
with:
repository: revoltchat/api
path: api
token: ${{ secrets.PAT }}
- name: Download OpenAPI specification
if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
- name: Commit changes
if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: EndBug/add-and-commit@v4
with:
cwd: "api"
add: "*.json"
author_name: Revolt CI
author_email: revolt-ci@users.noreply.github.com
message: "chore: generate OpenAPI specification"

View File

@@ -15,22 +15,27 @@ jobs:
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectNext(number: 3) {
projectV2(number: 3) {
id
fields(first:20) {
nodes {
id
name
settings
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project
env:
@@ -39,11 +44,11 @@ jobs:
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
projectNextItem {
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV

View File

@@ -15,22 +15,27 @@ jobs:
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectNext(number: 3) {
projectV2(number: 3) {
id
fields(first:20) {
nodes {
id
name
settings
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
@@ -39,13 +44,13 @@ jobs:
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $pr:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
projectNextItem {
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Set fields
@@ -59,14 +64,16 @@ jobs:
$status_field: ID!
$status_value: String!
) {
set_status: updateProjectNextItemField(input: {
set_status: updateProjectV2ItemFieldValue(input: {
projectId: $project
itemId: $item
fieldId: $status_field
value: $status_value
value: {
singleSelectOptionId: $status_value
}
}) {
projectNextItem {
projectV2Item {
id
}
}
}
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent

6
.gitignore vendored
View File

@@ -1,3 +1,9 @@
Rocket.toml
Revolt.toml
target
.data
.env
.vercel
.DS_Store

View File

@@ -1,4 +1,6 @@
{
"editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy"
}
"editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy",
"nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
}

2214
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,10 @@
[workspace]
members = ["crates/*"]
resolver = "2"
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
[patch.crates-io]
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
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" }

View File

@@ -1,12 +1,33 @@
# Build Stage
FROM rustlang/rust:nightly-slim AS builder
FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
USER 0:0
WORKDIR /home/rust/src
# Install build requirements
RUN apt-get update && apt-get install -y libssl-dev pkg-config
ARG TARGETARCH
# Build all crates
# Install build requirements
RUN dpkg --add-architecture "${TARGETARCH}"
RUN apt-get update && \
apt-get install -y \
make \
pkg-config \
libssl-dev:"${TARGETARCH}"
COPY scripts/build-image-layer.sh /tmp/
RUN sh /tmp/build-image-layer.sh tools
# 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 cargo build --locked --release
RUN sh /tmp/build-image-layer.sh apps

29
Dockerfile.useCurrentArch Normal file
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

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)
- [Contribution Guide](https://developers.revolt.chat/contributing)
## Minimum Supported Rust Version
Rust 1.70 or higher.
## Contributing
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).

73
STYLE_GUIDE.md Normal file
View File

@@ -0,0 +1,73 @@
# Code Style Guide
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
w.r.t. `model.rs` files
- All struct definitions must be commented.
```rust
/// Server
pub struct Server {
/// Name of the server
pub name: String,
```
- Struct definitions should not include derives unless necessary (if additional traits such as Hash are required) and instead use `auto_derived!` and `auto_derived_partial!`.
```rust
auto_derived_partial!(
/// Server
pub struct Server { .. },
"PartialServer"
);
```
- `auto_derived!` macro accepts multiple entries and should be used as such:
```rust
auto_derived!(
/// Optional fields on server object
pub enum FieldsServer { .. }
/// Optional fields on server object
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:
```rust
#[serde(skip_serializing_if = "crate::if_false", default)]
```
- `impl` blocks may be defined below the struct definitions and should be ordered in the same order of definition. Methods in the block must follow the same guidelines as traits where-in: methods are ordered in terms of CRUD, there are empty line breaks, and methods are commented.
w.r.t. `ops` module for models
- All traits must use a the name format `AbstractPlural` where Plural is the plural form of the collection. e.g. Servers
- Traits defined must follow these guidelines:
- Methods are ordered in terms of CRUD, create-read-update-delete ordering.
```rust
#[async_trait]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()>;
/// Fetch a server member by their id
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
/// Update information for a server member
async fn update_member(&self, .. ) -> Result<()>;
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
}
```
- There should be an empty line break between each method declaration.
- All methods must have an appropriate comment.
- When implementing the trait defined in `ops.rs` with each driver, the method declaration style should be the same for ease of searching: same ordering, same comments, same line breaks.

View File

@@ -1,7 +0,0 @@
#!/bin/bash
# Build base image
docker build -t revolt.chat/base:latest -f Dockerfile .
# Build crates
docker build -t revolt.chat/delta:latest -f crates/delta/Dockerfile .
docker build -t revolt.chat/bonfire:latest -f crates/bonfire/Dockerfile .

28
clippy.toml Normal file
View File

@@ -0,0 +1,28 @@
disallowed-methods = [
# Shouldn't need to access these directly
"revolt_database::models::bots::model::Bot::remove_field",
"revolt_database::models::messages::model::Message::attach_sendable_embed",
"revolt_database::models::users::model::User::set_relationship",
"revolt_database::models::users::model::User::apply_relationship",
# Prefer to use Object::create()
"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",
# Prefer to use Object::delete(&self)
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
]

View File

@@ -1,2 +0,0 @@
ko_fi: insertish
custom: https://insrt.uk/donate

View File

@@ -1,98 +0,0 @@
name: Docker
on:
push:
branches:
- "master"
tags:
- "*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
pull_request:
branches:
- "master"
paths:
- "Dockerfile"
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [linux/amd64]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/${{ matrix.architecture }}
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
- name: Build
uses: docker/build-push-action@v2
with:
context: .
platforms: ${{ matrix.architecture }}
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
publish_amd64:
needs: [test]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache amd64 Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/linux/amd64
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: ghcr.io/revoltchat/bonfire
- name: Login to Github Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and publish
uses: docker/build-push-action@v2
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

View File

@@ -1,33 +0,0 @@
name: Rust build and test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
env:
CARGO_TERM_COLOR: always
jobs:
check:
name: Rust project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install latest nightly
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
components: rustfmt, clippy
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test

View File

@@ -1,49 +0,0 @@
name: Add Issue to Board
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectNext(number: 3) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV

View File

@@ -1,72 +0,0 @@
name: Add PR to Board
on:
pull_request_target:
types: [opened, synchronize, ready_for_review, review_requested]
jobs:
track_pr:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectNext(number: 3) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
PR_ID: ${{ github.event.pull_request.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $pr:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Set fields
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
mutation (
$project: ID!
$item: ID!
$status_field: ID!
$status_value: String!
) {
set_status: updateProjectNextItemField(input: {
projectId: $project
itemId: $item
fieldId: $status_field
value: $status_value
}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "1.0.6-patch.2"
version = "0.6.12"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -18,6 +18,7 @@ querystring = "1.1.0"
revolt-quark = { path = "../quark" }
# serde
bincode = "1.3.3"
serde_json = "1.0.79"
rmp-serde = "1.0.0"
serde = "1.0.136"
@@ -25,4 +26,15 @@ serde = "1.0.136"
# async
futures = "0.3.21"
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
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
sentry = "0.31.5"
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }

View File

@@ -1,10 +1,11 @@
# Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder
RUN cargo install --locked --path crates/bonfire
# Bundle Stage
FROM debian:buster-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /usr/local/cargo/bin/revolt-bonfire ./
FROM debian:bullseye-slim
RUN apt-get update && \
apt-get install -y ca-certificates && \
apt-get clean
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
EXPOSE 9000
CMD ["./revolt-bonfire"]

View File

@@ -1,7 +1,7 @@
use std::env;
use async_std::net::TcpListener;
use revolt_quark::presence::presence_clear_region;
use revolt_presence::clear_region;
#[macro_use]
extern crate log;
@@ -18,7 +18,7 @@ async fn main() {
database::connect().await;
// Clean up the current region information.
presence_clear_region(None).await;
clear_region(None).await;
// Setup a TCP listener to accept WebSocket connections on.
// By default, we bind to port 9000 on all interfaces.
@@ -29,6 +29,10 @@ async fn main() {
// Start accepting new connections and spawn a client for each connection.
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:?}");
});
}
}

View File

@@ -1,6 +1,17 @@
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_quark::{
events::{
client::EventV1,
@@ -8,246 +19,249 @@ use revolt_quark::{
state::{State, SubscriptionStateChange},
},
models::{user::UserHint, User},
presence::{presence_create_session, presence_delete_session},
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.
pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
// Spawn a new Async task to work on.
task::spawn(async move {
info!("User connected from {addr:?}");
pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
// Upgrade the TCP connection to a WebSocket connection.
// In this process, we also parse any additional parameters given.
// 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.
// In this process, we also parse any additional parameters given.
// e.g. wss://example.com?format=json&version=1
let (sender, receiver) = oneshot::channel();
if let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
stream,
WebsocketHandshakeCallback::from(sender),
None,
)
// Split the socket for simultaneously read and write.
let (mut write, mut read) = ws.split();
// If the user has not provided authentication, request information.
if config.get_session_token().is_none() {
while let Ok(message) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) =
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
{
// Verify we've received a valid config, otherwise we should just drop the connection.
if let Ok(mut config) = receiver.await {
info!(
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
config.get_protocol_version(),
config.get_protocol_format()
);
.is_err()
{
return;
}
// Split the socket for simultaneously read and write.
let (write, mut read) = ws.split();
let write = Mutex::new(write);
// Download required data to local cache and send Ready payload.
let Ok(ready_payload) = state.generate_ready_payload(db).await else {
return;
};
if write.send(config.encode(&ready_payload)).await.is_err() {
return;
}
// If the user has not provided authentication, request information.
if config.get_session_token().is_none() {
'outer: while let Ok(message) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) =
config.decode(message.as_ref().unwrap())
{
config.set_session_token(token);
break 'outer;
}
}
// Create presence session.
let (first_session, session_id) = create_session(&user_id, 0).await;
// If this was the first session, notify other users that we just went online.
if first_session {
state.broadcast_presence_change(true).await;
}
{
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.
if let Some(token) = config.get_session_token().as_ref() {
match User::from_token(db, token, UserHint::Any).await {
Ok(user) => {
info!("User {addr:?} authenticated as @{}", user.username);
#[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}");
// Create local state.
let mut state = State::from(user);
let user_id = state.cache.user_id.clone();
subscriber.unsubscribe(id).await.unwrap();
}
// Create presence session.
let (first_session, session_id) =
presence_create_session(&user_id, 0).await;
for id in add {
#[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
// Notify socket we have authenticated.
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 = presence_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();
}
}
subscriber.subscribe(id).await.unwrap();
}
}
// * Disconnect the WebSocket if it isn't already.
/*ws.close(Some(CloseFrame {
code: CloseCode::Normal,
reason: std::borrow::Cow::from(""),
}))
.await
.unwrap();*/
SubscriptionStateChange::None => {}
}
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();
}
}
_ => {}
}
}
}

View File

@@ -0,0 +1,26 @@
[package]
name = "revolt-config"
version = "0.6.12"
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 }

View File

@@ -0,0 +1,3 @@
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"

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

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;
}
}

View File

@@ -0,0 +1,89 @@
[package]
name = "revolt-database"
version = "0.6.12"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Database Implementation"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
# Databases
mongodb = ["dep:mongodb", "bson"]
# ... Other
tasks = ["isahc", "linkify", "url-escape"]
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 = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.6.12", path = "../config" }
revolt-result = { version = "0.6.12", path = "../result" }
revolt-models = { version = "0.6.12", path = "../models" }
revolt-presence = { version = "0.6.12", path = "../presence" }
revolt-permissions = { version = "0.6.12", path = "../permissions", features = [
"serde",
"bson",
] }
# Utility
log = "0.4"
lru = "0.11.0"
rand = "0.8.5"
ulid = "1.0.0"
nanoid = "0.4.0"
base64 = "0.21.3"
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
serde_json = "1"
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
# Events
redis-kiss = { version = "0.1.4" }
# Database
bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "2.1.0", default-features = false }
# Database Migration
unicode-segmentation = "1.10.1"
regex = "1"
# Async Language Features
futures = "0.3.19"
async-lock = "2.8.0"
async-trait = "0.1.51"
async-recursion = "1.0.4"
# Async
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Rocket Impl
schemars = { version = "0.8.8", 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 = { version = "1.0.8" }

View File

@@ -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
}
]

View File

@@ -0,0 +1,89 @@
mod mongodb;
mod reference;
use rand::Rng;
use revolt_config::config;
pub use self::mongodb::*;
pub use self::reference::*;
/// Database information to use to create a client
pub enum DatabaseInfo {
/// Auto-detect the database in use
Auto,
/// Auto-detect the database in use and create an empty testing database
Test(String),
/// Use the mock database
Reference,
/// Connect to MongoDB
MongoDb { uri: String, database_name: String },
/// Use existing MongoDB connection
MongoDbFromClient(::mongodb::Client, String),
}
/// Database
#[derive(Clone)]
pub enum Database {
/// Mock database
Reference(ReferenceDb),
/// MongoDB database
MongoDb(MongoDb),
}
impl DatabaseInfo {
/// Create a database client from the given database information
#[async_recursion]
pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
Ok(match self {
DatabaseInfo::Auto => {
if std::env::var("TEST_DB").is_ok() {
DatabaseInfo::Test(format!(
"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(),
}
.connect()
.await?
} else {
DatabaseInfo::Reference.connect().await?
}
}
DatabaseInfo::Test(database_name) => {
match std::env::var("TEST_DB")
.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 => Database::Reference(Default::default()),
DatabaseInfo::MongoDb { uri, database_name } => {
let client = ::mongodb::Client::with_uri_str(uri)
.await
.map_err(|_| "Failed to init db connection.".to_string())?;
Database::MongoDb(MongoDb(client, database_name))
}
DatabaseInfo::MongoDbFromClient(client, database_name) => {
Database::MongoDb(MongoDb(client, database_name))
}
})
}
}

View File

@@ -0,0 +1,243 @@
use std::collections::HashMap;
use std::ops::Deref;
use futures::StreamExt;
use mongodb::bson::{doc, to_document, Document};
use mongodb::error::Result;
use mongodb::options::{FindOneOptions, FindOptions};
use mongodb::results::{DeleteResult, InsertOneResult, UpdateResult};
use serde::de::DeserializeOwned;
use serde::Serialize;
database_derived!(
#[cfg(feature = "mongodb")]
/// MongoDB implementation
pub struct MongoDb(pub ::mongodb::Client, pub String);
);
impl Deref for MongoDb {
type Target = mongodb::Client;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[allow(dead_code)]
impl MongoDb {
/// Get the Revolt database
pub fn db(&self) -> mongodb::Database {
self.database(&self.1)
}
/// Get a collection by its name
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
self.db().collection(collection)
}
/// Insert one document into a collection
pub async fn insert_one<T: Serialize>(
&self,
collection: &'static str,
document: T,
) -> Result<InsertOneResult> {
self.col::<T>(collection).insert_one(document, None).await
}
/// Count documents by projection
pub async fn count_documents(
&self,
collection: &'static str,
projection: Document,
) -> Result<u64> {
self.col::<Document>(collection)
.count_documents(projection, None)
.await
}
/// Find multiple documents in a collection with options
pub async fn find_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
options: O,
) -> Result<Vec<T>>
where
O: Into<Option<FindOptions>>,
{
Ok(self
.col::<T>(collection)
.find(projection, options)
.await?
.filter_map(|s| async {
if cfg!(debug_assertions) {
// Hard fail on invalid documents
Some(s.unwrap())
} else {
s.ok()
}
})
.collect::<Vec<T>>()
.await)
}
/// Find multiple documents in a collection
pub async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
) -> Result<Vec<T>> {
self.find_with_options(collection, projection, None).await
}
/// Find one document with options
pub async fn find_one_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
options: O,
) -> Result<Option<T>>
where
O: Into<Option<FindOneOptions>>,
{
self.col::<T>(collection)
.find_one(projection, options)
.await
}
/// Find one document
pub async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
) -> Result<Option<T>> {
self.find_one_with_options(collection, projection, None)
.await
}
/// Find one document by its ID
pub async fn find_one_by_id<T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
id: &str,
) -> Result<Option<T>> {
self.find_one(
collection,
doc! {
"_id": id
},
)
.await
}
/// Update one document given a projection, partial document, and list of paths to unset
pub async fn update_one<P, T: Serialize>(
&self,
collection: &'static str,
projection: Document,
partial: T,
remove: Vec<&dyn IntoDocumentPath>,
prefix: P,
) -> Result<UpdateResult>
where
P: Into<Option<String>>,
{
let prefix = prefix.into();
let mut unset = doc! {};
for field in remove {
if let Some(path) = field.as_path() {
if let Some(prefix) = &prefix {
unset.insert(prefix.to_owned() + path, 1_i32);
} else {
unset.insert(path, 1_i32);
}
}
}
let query = doc! {
"$unset": unset,
"$set": if let Some(prefix) = &prefix {
to_document(&prefix_keys(&partial, prefix))
} else {
to_document(&partial)
}?
};
self.col::<Document>(collection)
.update_one(projection, query, None)
.await
}
/// Update one document given an ID, partial document, and list of paths to unset
pub async fn update_one_by_id<P, T: Serialize>(
&self,
collection: &'static str,
id: &str,
partial: T,
remove: Vec<&dyn IntoDocumentPath>,
prefix: P,
) -> Result<UpdateResult>
where
P: Into<Option<String>>,
{
self.update_one(
collection,
doc! {
"_id": id
},
partial,
remove,
prefix,
)
.await
}
/// Delete one document by the given projection
pub async fn delete_one(
&self,
collection: &'static str,
projection: Document,
) -> Result<DeleteResult> {
self.col::<Document>(collection)
.delete_one(projection, None)
.await
}
/// Delete one document by the given ID
pub async fn delete_one_by_id(
&self,
collection: &'static str,
id: &str,
) -> Result<DeleteResult> {
self.delete_one(
collection,
doc! {
"_id": id
},
)
.await
}
}
/// Just a string ID struct
#[derive(Deserialize)]
pub struct DocumentId {
#[serde(rename = "_id")]
pub id: String,
}
pub trait IntoDocumentPath: Send + Sync {
/// Create JSON key path
fn as_path(&self) -> Option<&'static str>;
}
/// Prefix keys on an arbitrary object
pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_json::Value> {
let v: String = serde_json::to_string(t).unwrap();
let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap();
v.into_iter()
.filter(|(_k, v)| !v.is_null())
.map(|(k, v)| (format!("{}{}", prefix.to_owned(), k), v))
.collect()
}

View File

@@ -0,0 +1,31 @@
use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, User, UserSettings, Webhook,
};
database_derived!(
/// Reference implementation
#[derive(Default)]
pub struct ReferenceDb {
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 users: Arc<Mutex<HashMap<String, User>>>,
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
}
);

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;
}
}

View File

@@ -0,0 +1 @@
pub mod client;

View File

@@ -0,0 +1,89 @@
#[macro_use]
extern crate serde;
#[macro_use]
extern crate async_recursion;
#[macro_use]
extern crate async_trait;
#[macro_use]
extern crate log;
#[macro_use]
extern crate revolt_optional_struct;
#[macro_use]
extern crate revolt_result;
#[cfg(feature = "mongodb")]
pub use mongodb;
#[cfg(feature = "mongodb")]
#[macro_use]
extern crate bson;
macro_rules! database_derived {
( $( $item:item )+ ) => {
$(
#[derive(Clone)]
$item
)+
};
}
macro_rules! auto_derived {
( $( $item:item )+ ) => {
$(
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
$item
)+
};
}
macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => {
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
#[optional_name = $name]
#[opt_skip_serializing_none]
#[opt_some_priority]
$item
};
}
mod drivers;
pub use drivers::*;
#[cfg(test)]
macro_rules! database_test {
( | $db: ident | $test:expr ) => {
let db = $crate::DatabaseInfo::Test(format!(
"{}:{}",
file!().replace('/', "_").replace(".rs", ""),
line!()
))
.connect()
.await
.expect("Database connection failed.");
db.drop_database().await;
#[allow(clippy::redundant_closure_call)]
(|$db: $crate::Database| $test)(db.clone()).await;
db.drop_database().await
};
}
mod models;
pub mod util;
pub use models::*;
pub mod events;
pub mod tasks;
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,24 @@
auto_derived!(
/// Document representing migration information
pub struct MigrationInfo {
/// Unique Id
#[serde(rename = "_id")]
pub id: i32,
/// Current database revision
pub revision: i32,
}
);
#[cfg(test)]
mod tests {
#[async_std::test]
async fn migrate() {
database_test!(|db| async move {
// Initialise the database
db.migrate_database().await.unwrap();
// Migrate the existing database
db.migrate_database().await.unwrap();
});
}
}

View File

@@ -0,0 +1,12 @@
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractMigrations: Sync + Send {
#[cfg(test)]
/// Drop the database
async fn drop_database(&self);
/// Migrate the database
async fn migrate_database(&self) -> Result<(), ()>;
}

View File

@@ -1,13 +1,20 @@
use crate::{AbstractMigrations, Result};
use crate::MongoDb;
use super::super::MongoDb;
use super::AbstractMigrations;
mod init;
mod scripts;
#[async_trait]
impl AbstractMigrations for MongoDb {
async fn migrate_database(&self) -> Result<()> {
#[cfg(test)]
/// Drop the database
async fn drop_database(&self) {
self.db().drop(None).await.ok();
}
/// Migrate the database
async fn migrate_database(&self) -> Result<(), ()> {
info!("Migrating the database.");
let list = self
@@ -15,7 +22,7 @@ impl AbstractMigrations for MongoDb {
.await
.expect("Failed to fetch database names.");
if list.iter().any(|x| x == "revolt") {
if list.iter().any(|x| x == &self.1) {
scripts::migrate_database(self).await;
} else {
init::create_database(self).await;

View File

@@ -1,9 +1,8 @@
use crate::r#impl::mongo::MongoDb;
use super::scripts::LATEST_REVISION;
use mongodb::bson::doc;
use mongodb::options::CreateCollectionOptions;
use crate::mongodb::bson::doc;
use crate::mongodb::options::CreateCollectionOptions;
use crate::MongoDb;
pub async fn create_database(db: &MongoDb) {
info!("Creating database.");
@@ -45,6 +44,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.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)
.await
.expect("Failed to create migrations collection.");
@@ -57,10 +60,26 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create user_settings collection.");
db.create_collection("safety_reports", None)
.await
.expect("Failed to create safety_reports collection.");
db.create_collection("safety_snapshots", None)
.await
.expect("Failed to create safety_snapshots collection.");
db.create_collection("safety_strikes", None)
.await
.expect("Failed to create safety_strikes collection.");
db.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
db.create_collection("ratelimit_events", None)
.await
.expect("Failed to create ratelimit_events collection.");
db.create_collection(
"pubsub",
CreateCollectionOptions::builder()
@@ -80,6 +99,18 @@ pub async fn create_database(db: &MongoDb) {
"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",
@@ -103,18 +134,18 @@ pub async fn create_database(db: &MongoDb) {
},
"name": "content"
},
{
"key": {
"channel": 1_i32
},
"name": "channel"
},
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
},
{
"key": {
"author": 1_i32
},
"name": "author"
}
]
},
@@ -182,5 +213,24 @@ pub async fn create_database(db: &MongoDb) {
.await
.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.");
}

View File

@@ -1,14 +1,17 @@
use std::time::Duration;
use std::{collections::HashSet, ops::BitXor, time::Duration};
use bson::Bson;
use futures::StreamExt;
use mongodb::{
bson::{doc, from_bson, from_document, to_document, Document},
options::FindOptions,
use crate::{
mongodb::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
};
use futures::StreamExt;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use serde::{Deserialize, Serialize};
use crate::{r#impl::mongo::MongoDb, Permission, DEFAULT_PERMISSION_SERVER};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Serialize, Deserialize)]
struct MigrationInfo {
@@ -16,7 +19,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 17;
pub const LATEST_REVISION: i32 = 26;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -215,7 +218,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
if revision <= 8 {
info!("Running migration [revision 8 / 2021-09-10]: Update to rAuth version 1.");
info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1.");
db.db()
.run_command(
@@ -503,13 +506,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
update.insert(
"default_permissions",
(*DEFAULT_PERMISSION_SERVER
// Remove Send Message permission if it wasn't originally granted
^ (if has_send {
0
} else {
Permission::SendMessage as u64
})) as i64,
// Remove Send Message permission if it wasn't originally granted
(4000323584).bitxor(if has_send { 0 } else { (1 << 22) as u64 }) as i64,
);
if let Some(Bson::Document(mut roles)) = document.remove("roles") {
@@ -568,7 +566,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! {
"default_permissions": {
"a": 0_i64,
"d": Permission::SendMessage as i64
"d": (1 << 22) as i64
}
},
);
@@ -603,20 +601,20 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
if revision <= 15 {
info!("Running migration [revision 15 / 04-06-2022]: Migrate rAuth to latest version.");
info!("Running migration [revision 15 / 04-06-2022]: Migrate Authifier to latest version.");
let db = rauth::Database::MongoDb(rauth::database::MongoDb(db.db()));
db.run_migration(rauth::Migration::M2022_06_03EnsureUpToSpec)
let db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
db.run_migration(authifier::Migration::M2022_06_03EnsureUpToSpec)
.await
.unwrap();
}
if revision <= 16 {
info!("Running migration [revision 16 / 07-07-2022]: Add `emojis` collection and rAuth migration.");
info!("Running migration [revision 16 / 07-07-2022]: Add `emojis` collection and Authifier migration.");
let rauth_db = rauth::Database::MongoDb(rauth::database::MongoDb(db.db()));
rauth_db
.run_migration(rauth::Migration::M2022_06_09AddIndexForDeletion)
let authifier_db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
authifier_db
.run_migration(authifier::Migration::M2022_06_09AddIndexForDeletion)
.await
.unwrap();
@@ -644,8 +642,349 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to create emoji parent index.");
}
if revision <= 17 {
info!("Running migration [revision 17 / 15-07-2022]: Initialise `joined_at` property on server members.");
db.col::<Document>("server_members")
.update_many(
doc! {},
doc! {
"$set": {
"joined_at": DateTime::now().to_rfc3339_string()
}
},
None,
)
.await
.expect("Failed to update server members.");
}
if revision <= 18 {
info!("Running migration [revision 18 / 27-02-2022]: Create author index on messages. Drop plain channel index if exists.");
if db
.db()
.run_command(
doc! {
"dropIndexes": "messages",
"index": ["channel"]
},
None,
)
.await
.is_err()
{
info!("Failed to drop `messages.channel` index but this is ok since that means it's probably gone.");
}
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"author": 1_i32,
},
"name": "author"
}
]
},
None,
)
.await
.expect("Failed to create messages author index.");
}
if revision <= 19 {
info!(
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
);
db.db()
.create_collection("safety_reports", None)
.await
.unwrap();
db.db()
.create_collection("safety_snapshots", None)
.await
.unwrap();
}
if revision <= 20 {
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
db.db()
.run_command(
doc! {
"createIndexes": "safety_snapshots",
"indexes": [
{
"key": {
"report_id": 1_i32
},
"name": "report_id"
}
]
},
None,
)
.await
.expect("Failed to create safety snapshot index.");
}
if revision <= 21 {
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
db.db()
.create_collection("safety_strikes", None)
.await
.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`.
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION
LATEST_REVISION.max(revision)
}

View File

@@ -0,0 +1,16 @@
use crate::ReferenceDb;
use super::AbstractMigrations;
#[async_trait]
impl AbstractMigrations for ReferenceDb {
#[cfg(test)]
/// Drop the database
async fn drop_database(&self) {}
/// Migrate the database
async fn migrate_database(&self) -> Result<(), ()> {
// Here you would do your typical migrations if this was a real database.
Ok(())
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,215 @@
use revolt_config::config;
use revolt_result::Result;
use ulid::Ulid;
use crate::{BotInformation, Database, PartialUser, User};
auto_derived_partial!(
/// Bot
pub struct Bot {
/// Bot Id
///
/// This equals the associated bot user's id.
#[serde(rename = "_id")]
pub id: String,
/// User Id of the bot owner
pub owner: String,
/// Token used to authenticate requests for this bot
pub token: String,
/// Whether the bot is public
/// (may be invited by anyone)
pub public: bool,
/// Whether to enable analytics
#[serde(skip_serializing_if = "crate::if_false", default)]
pub analytics: bool,
/// Whether this bot should be publicly discoverable
#[serde(skip_serializing_if = "crate::if_false", default)]
pub discoverable: bool,
/// Reserved; URL for handling interactions
#[serde(skip_serializing_if = "String::is_empty", default)]
pub interactions_url: String,
/// URL for terms of service
#[serde(skip_serializing_if = "String::is_empty", default)]
pub terms_of_service_url: String,
/// URL for privacy policy
#[serde(skip_serializing_if = "String::is_empty", default)]
pub privacy_policy_url: String,
/// Enum of bot flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
},
"PartialBot"
);
auto_derived!(
/// Optional fields on bot object
pub enum FieldsBot {
Token,
InteractionsURL,
}
);
#[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)]
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
pub fn remove_field(&mut self, field: &FieldsBot) {
match field {
FieldsBot::Token => self.token = nanoid::nanoid!(64),
FieldsBot::InteractionsURL => {
self.interactions_url = String::new();
}
}
}
/// Update this bot
pub async fn update(
&mut self,
db: &Database,
mut partial: PartialBot,
remove: Vec<FieldsBot>,
) -> Result<()> {
if remove.contains(&FieldsBot::Token) {
partial.token = Some(nanoid::nanoid!(64));
}
for field in &remove {
self.remove_field(field);
}
db.update_bot(&self.id, &partial, remove).await?;
self.apply_options(partial);
Ok(())
}
/// Delete this bot
pub async fn delete(&self, db: &Database) -> Result<()> {
db.fetch_user(&self.id).await?.mark_deleted(db).await?;
db.delete_bot(&self.id).await
}
}
#[cfg(test)]
mod tests {
use crate::{Bot, FieldsBot, PartialBot, User};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let owner = User::create(&db, "Owner".to_string(), None, None)
.await
.unwrap();
let bot = Bot::create(
&db,
"Bot Name".to_string(),
&owner,
PartialBot {
token: Some("my token".to_string()),
interactions_url: Some("some url".to_string()),
..Default::default()
},
)
.await
.unwrap();
assert!(!bot.interactions_url.is_empty());
let mut updated_bot = bot.clone();
updated_bot
.update(
&db,
PartialBot {
public: Some(true),
..Default::default()
},
vec![FieldsBot::Token, FieldsBot::InteractionsURL],
)
.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_bots = db.fetch_bots_by_user(&owner.id).await.unwrap();
assert!(!bot.public);
assert!(fetched_bot1.public);
assert!(!bot.interactions_url.is_empty());
assert!(fetched_bot1.interactions_url.is_empty());
assert_ne!(bot.token, fetched_bot1.token);
assert_eq!(updated_bot, fetched_bot1);
assert_eq!(fetched_bot1, fetched_bot2);
assert_eq!(fetched_bot1, fetched_bots[0]);
assert_eq!(1, db.get_number_of_bots_by_user(&owner.id).await.unwrap());
bot.delete(&db).await.unwrap();
assert!(db.fetch_bot(&bot.id).await.is_err());
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))
});
}
}

View File

@@ -1,26 +1,35 @@
use crate::models::bot::{Bot, FieldsBot, PartialBot};
use crate::Result;
#[async_trait]
pub trait AbstractBot: Sync + Send {
/// Fetch a bot by its id
async fn fetch_bot(&self, id: &str) -> Result<Bot>;
/// Fetch a bot by its token
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot>;
/// Insert new bot into the database
async fn insert_bot(&self, bot: &Bot) -> Result<()>;
/// Update bot with new information
async fn update_bot(&self, id: &str, bot: &PartialBot, remove: Vec<FieldsBot>) -> Result<()>;
/// Delete a bot from the database
async fn delete_bot(&self, id: &str) -> Result<()>;
/// Fetch bots owned by a user
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>>;
/// Get the number of bots owned by a user
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize>;
}
use revolt_result::Result;
use crate::{Bot, FieldsBot, PartialBot};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractBots: Sync + Send {
/// Insert new bot into the database
async fn insert_bot(&self, bot: &Bot) -> Result<()>;
/// Fetch a bot by its id
async fn fetch_bot(&self, id: &str) -> Result<Bot>;
/// Fetch a bot by its token
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot>;
/// Fetch bots owned by a user
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>>;
/// Get the number of bots owned by a user
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize>;
/// Update bot with new information
async fn update_bot(
&self,
id: &str,
partial: &PartialBot,
remove: Vec<FieldsBot>,
) -> Result<()>;
/// Delete a bot from the database
async fn delete_bot(&self, id: &str) -> Result<()>;
}

View File

@@ -0,0 +1,92 @@
use revolt_result::Result;
use crate::{Bot, FieldsBot, PartialBot};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractBots;
static COL: &str = "bots";
#[async_trait]
impl AbstractBots for MongoDb {
/// Insert new bot into the database
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
query!(self, insert_one, COL, &bot).map(|_| ())
}
/// Fetch a bot by its id
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a bot by its token
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
query!(
self,
find_one,
COL,
doc! {
"token": token
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch bots owned by a user
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
query!(
self,
find,
COL,
doc! {
"owner": user_id
}
)
}
/// Get the number of bots owned by a user
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
query!(
self,
count_documents,
COL,
doc! {
"owner": user_id
}
)
.map(|v| v as usize)
}
/// Update bot with new information
async fn update_bot(
&self,
id: &str,
partial: &PartialBot,
remove: Vec<FieldsBot>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Delete a bot from the database
async fn delete_bot(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
impl IntoDocumentPath for FieldsBot {
fn as_path(&self) -> Option<&'static str> {
match self {
FieldsBot::InteractionsURL => Some("interactions_url"),
FieldsBot::Token => None,
}
}
}

View File

@@ -0,0 +1,82 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{Bot, FieldsBot, PartialBot};
use super::AbstractBots;
#[async_trait]
impl AbstractBots for ReferenceDb {
/// Insert new bot into the database
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
let mut bots = self.bots.lock().await;
if bots.contains_key(&bot.id) {
Err(create_database_error!("insert", "bot"))
} else {
bots.insert(bot.id.to_string(), bot.clone());
Ok(())
}
}
/// Fetch a bot by its id
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
let bots = self.bots.lock().await;
bots.get(id).cloned().ok_or_else(|| create_error!(NotFound))
}
/// Fetch a bot by its token
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
let bots = self.bots.lock().await;
bots.values()
.find(|bot| bot.token == token)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch bots owned by a user
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
let bots = self.bots.lock().await;
Ok(bots
.values()
.filter(|bot| bot.owner == user_id)
.cloned()
.collect())
}
/// Get the number of bots owned by a user
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
let bots = self.bots.lock().await;
Ok(bots.values().filter(|bot| bot.owner == user_id).count())
}
/// Update bot with new information
async fn update_bot(
&self,
id: &str,
partial: &PartialBot,
remove: Vec<FieldsBot>,
) -> Result<()> {
let mut bots = self.bots.lock().await;
if let Some(bot) = bots.get_mut(id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
bot.remove_field(&field);
}
bot.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a bot from the database
async fn delete_bot(&self, id: &str) -> Result<()> {
let mut bots = self.bots.lock().await;
if bots.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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))
}
}

View File

@@ -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<()>;
}

View File

@@ -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(|_| ())
}
}

View File

@@ -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))
}
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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,
}
);

View File

@@ -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>>;
}

View File

@@ -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
}
)
}
}

View File

@@ -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())
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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()
)
});
}
}

View File

@@ -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<()>;
}

View File

@@ -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",
})
}
}

View File

@@ -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))
}
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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));
});
}
}

View File

@@ -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<()>;
}

View File

@@ -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(|_| ())
}
}

View File

@@ -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))
}
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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))
}
}
}

View File

@@ -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<()>;
}

View File

@@ -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))
}
}

View File

@@ -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

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,107 @@
use crate::Database;
use revolt_result::Result;
auto_derived_partial!(
/// File
pub struct File {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Tag / bucket this file was uploaded to
pub tag: String,
/// Original filename
pub filename: String,
/// Parsed metadata of this file
pub metadata: Metadata,
/// Raw content type of this file
pub content_type: String,
/// Size of this file (in bytes)
pub size: isize,
/// Whether this file was deleted
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted: Option<bool>,
/// Whether this file was reported
#[serde(skip_serializing_if = "Option::is_none")]
pub reported: Option<bool>,
// TODO: migrate this mess to having:
// - author_id
// - parent: Parent { Message(id), User(id), etc }
#[serde(skip_serializing_if = "Option::is_none")]
pub message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_id: Option<String>,
/// Id of the object this file is associated with
#[serde(skip_serializing_if = "Option::is_none")]
pub object_id: Option<String>,
},
"PartialFile"
);
auto_derived!(
/// Metadata associated with a file
#[serde(tag = "type")]
#[derive(Default)]
pub enum Metadata {
/// File is just a generic uncategorised file
#[default]
File,
/// File contains textual data and should be displayed as such
Text,
/// File is an image with specific dimensions
Image { width: isize, height: isize },
/// File is a video with specific dimensions
Video { width: isize, height: isize },
/// File is audio
Audio,
}
);
impl File {
/// Use a file for a message attachment
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "attachments", "message", parent)
.await
}
/// Use a file for a user profile background
pub async fn use_background(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "backgrounds", "user", parent)
.await
}
/// Use a file for a user avatar
pub async fn use_avatar(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "avatars", "user", parent)
.await
}
/// Use a file for an icon
pub async fn use_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "icons", "object", parent)
.await
}
/// Use a file for a server icon
pub async fn use_server_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "icons", "object", parent)
.await
}
/// Use a file for a server banner
pub async fn use_banner(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "banners", "server", parent)
.await
}
/// Use a file for an emoji
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "emojis", "object", parent)
.await
}
}

View File

@@ -0,0 +1,30 @@
use revolt_result::Result;
use crate::File;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAttachments: Sync + Send {
/// Insert attachment into database.
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
) -> Result<File>;
/// Mark an attachment as having been reported.
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>;
/// Mark an attachment as having been deleted.
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>;
/// Mark multiple attachments as having been deleted.
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>;
}

View File

@@ -0,0 +1,117 @@
use bson::Document;
use revolt_result::Result;
use crate::File;
use crate::MongoDb;
use super::AbstractAttachments;
static COL: &str = "attachments";
#[async_trait]
impl AbstractAttachments for MongoDb {
/// Insert attachment into database.
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
query!(self, insert_one, COL, &attachment).map(|_| ())
}
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
) -> Result<File> {
let key = format!("{parent_type}_id");
let file = query!(
self,
find_one,
COL,
doc! {
"_id": id,
"tag": tag,
&key: {
"$exists": false
}
}
)?
.ok_or_else(|| create_error!(NotFound))?;
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$set": {
key: parent_id
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_one", COL))?;
Ok(file)
}
/// Mark an attachment as having been reported.
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$set": {
"reported": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Mark an attachment as having been deleted.
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Mark multiple attachments as having been deleted.
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": {
"$in": ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
}

View File

@@ -0,0 +1,89 @@
use revolt_result::Result;
use crate::File;
use crate::ReferenceDb;
use super::AbstractAttachments;
#[async_trait]
impl AbstractAttachments for ReferenceDb {
/// Insert attachment into database.
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
let mut attachments = self.files.lock().await;
if attachments.contains_key(&attachment.id) {
Err(create_database_error!("insert", "attachment"))
} else {
attachments.insert(attachment.id.to_string(), attachment.clone());
Ok(())
}
}
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
) -> Result<File> {
let mut files = self.files.lock().await;
if let Some(file) = files.get_mut(id) {
if file.tag == tag {
match parent_type {
"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 {
Err(create_error!(NotFound))
}
}
/// Mark an attachment as having been reported.
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
let mut files = self.files.lock().await;
if let Some(file) = files.get_mut(id) {
file.reported = Some(true);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Mark an attachment as having been deleted.
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
let mut files = self.files.lock().await;
if let Some(file) = files.get_mut(id) {
file.deleted = Some(true);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Mark multiple attachments as having been deleted.
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
let mut files = self.files.lock().await;
for id in ids {
if !files.contains_key(id) {
return Err(create_error!(NotFound));
}
}
for id in ids {
if let Some(file) = files.get_mut(id) {
file.reported = Some(true);
}
}
Ok(())
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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()
}
}

View File

@@ -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<()>;
}

View File

@@ -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))
}
}

View File

@@ -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(())
}
}

View File

@@ -0,0 +1,68 @@
mod admin_migrations;
mod bots;
mod channel_invites;
mod channel_unreads;
mod channel_webhooks;
mod channels;
mod emojis;
mod files;
mod messages;
mod ratelimit_events;
mod server_bans;
mod server_members;
mod servers;
mod user_settings;
mod users;
pub use admin_migrations::*;
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 messages::*;
pub use ratelimit_events::*;
pub use server_bans::*;
pub use server_members::*;
pub use servers::*;
pub use user_settings::*;
pub use users::*;
use crate::{Database, MongoDb, ReferenceDb};
pub trait AbstractDatabase:
Sync
+ Send
+ admin_migrations::AbstractMigrations
+ bots::AbstractBots
+ channels::AbstractChannels
+ channel_invites::AbstractChannelInvites
+ channel_unreads::AbstractChannelUnreads
+ channel_webhooks::AbstractWebhooks
+ emojis::AbstractEmojis
+ files::AbstractAttachments
+ messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents
+ server_bans::AbstractServerBans
+ server_members::AbstractServerMembers
+ servers::AbstractServers
+ user_settings::AbstractUserSettings
+ users::AbstractUsers
{
}
impl AbstractDatabase for ReferenceDb {}
impl AbstractDatabase for MongoDb {}
impl std::ops::Deref for Database {
type Target = dyn AbstractDatabase;
fn deref(&self) -> &Self::Target {
match &self {
Database::Reference(dummy) => dummy,
Database::MongoDb(mongo) => mongo,
}
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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
}
}

View File

@@ -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>;
}

View File

@@ -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))
}
}

View File

@@ -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)
}
}

View File

@@ -1,6 +1,6 @@
<mxfile host="65bd71144e">
<diagram id="9BKyaxnbqnTkbfLuLd1z" name="Page-1">
<mxGraphModel dx="1033" dy="710" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<mxGraphModel dx="2466" dy="650" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
@@ -35,18 +35,12 @@
<mxCell id="2" value="Attachment" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="220" y="490" width="80" height="30" as="geometry"/>
</mxCell>
<mxCell id="49" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="3" target="5" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="195" y="340"/>
<mxPoint x="245" y="340"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="51" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="3" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="290" y="160" as="targetPoint"/>
<Array as="points">
<mxPoint x="95" y="340"/>
<mxPoint x="195" y="340"/>
<mxPoint x="195" y="260"/>
<mxPoint x="260" y="260"/>
<mxPoint x="260" y="220"/>
@@ -54,11 +48,19 @@
</Array>
</mxGeometry>
</mxCell>
<mxCell id="68" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" edge="1" parent="1" source="3" target="5">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="95" y="340"/>
<mxPoint x="245" y="340"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="3" value="Channel Invite" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="150" y="400" width="90" height="30" as="geometry"/>
<mxGeometry x="50" y="400" width="90" height="30" as="geometry"/>
</mxCell>
<mxCell id="4" value="Channel Unread" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="40" y="400" width="100" height="30" as="geometry"/>
<mxGeometry x="-70" y="400" width="100" height="30" as="geometry"/>
</mxCell>
<mxCell id="43" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;dashed=1;" parent="1" source="5" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
@@ -66,6 +68,14 @@
<mxCell id="60" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="5" target="9" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="67" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;startArrow=diamondThin;startFill=1;" edge="1" parent="1" source="5" target="66">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="245" y="370"/>
<mxPoint x="195" y="370"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="5" value="Channel" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="230" y="280" width="60" height="30" as="geometry"/>
</mxCell>
@@ -161,8 +171,8 @@
<mxPoint x="290" y="160" as="targetPoint"/>
<mxPoint x="200" y="400" as="sourcePoint"/>
<Array as="points">
<mxPoint x="90" y="370"/>
<mxPoint x="195" y="370"/>
<mxPoint x="-20" y="340"/>
<mxPoint x="195" y="340"/>
<mxPoint x="195" y="260"/>
<mxPoint x="260" y="260"/>
<mxPoint x="260" y="220"/>
@@ -199,6 +209,9 @@
<mxPoint x="320" y="440" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="66" value="Webhook" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="160" y="400" width="70" height="30" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -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>,
}
);

View File

@@ -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<()>;
}

View File

@@ -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(|_| ())
}
}

View File

@@ -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))
}
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

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