Compare commits

...

563 Commits

Author SHA1 Message Date
jmug
720cd46879 chore(pushd/ci): Build image pushd image in-fork.
Some checks failed
Docker / Build pushd image (push) Has been cancelled
Docker / base (push) Has been cancelled
Rust build, test, and generate specification / Rust project (push) Has been cancelled
Docker / base (release) Successful in 21m35s
Docker / Build pushd image (release) Successful in 1m32s
2026-06-27 00:34:53 -07:00
jmug
7d099d5ae7 feat(pushd): Guard against malformed vapid web pushes. 2026-06-27 00:12:11 -07:00
jmug
8847b3c5ba feat(pushd): Fix typo in inbound notification handling. 2026-06-27 00:06:05 -07:00
Paul Makles
eb5f5f91cd feat: add crond container definitions 2025-02-10 20:28:48 +00:00
Paul Makles
52e1f0ddde merge: branch 'insert/feat/crond' 2025-02-10 19:54:40 +00:00
Paul Makles
defc9ec79b fix: match new error type for status code 2025-02-10 19:05:21 +00:00
Paul Makles
7026961df4 chore: fix version references 2025-02-10 19:03:48 +00:00
Paul Makles
078380d305 merge: branch 'main' into insert/feat/crond 2025-02-10 19:02:19 +00:00
Paul Makles
add3f40b23 fix: don't allow users to time themselves out
closes #376
2025-02-10 18:54:15 +00:00
Paul Makles
7216e9909b chore: disable LTO because it likely overloads GitHub worker 2025-02-10 18:50:19 +00:00
Paul Makles
a1e6a19210 chore: bump version to 0.8.2 2025-02-10 18:37:21 +00:00
Paul Makles
5f39403ce7 fix: ensure ratelimiter adjusts for version prefix 2025-02-10 18:34:37 +00:00
Paul Makles
e525ffe5e4 chore: mount API at /0.8/ as well 2025-02-10 18:19:34 +00:00
Paul Makles
e3723d647e fix: change permission check for fetching channel webhooks 2025-02-10 18:04:46 +00:00
Paul Makles
5f84daa9db fix: ensure limit is always at least 1 when sent to database 2025-02-10 17:51:22 +00:00
Paul Makles
e957af4ca3 docs: add some notes on overrides; fix defaults 2025-02-10 17:21:23 +00:00
Paul Makles
439bacf067 refactor: add error messages for Rabbit issues 2025-02-10 17:20:29 +00:00
Paul Makles
fa55e88dd9 chore: add pushd to debug image script 2025-02-10 17:19:54 +00:00
IAmTomahawkx
71e7fe3086 chore: bump release for patch 2024-12-27 16:35:02 -08:00
IAmTomahawkx
03f2e3b1bf fix: last message in a channel could not be acked, resulting in ghost dms and channel messages 2024-12-27 16:29:56 -08:00
Paul Makles
8bbb579d23 ci: revert back to GH runner 2024-12-26 14:59:03 +00:00
Paul Makles
6448af071b ci: remove purge command 2024-12-22 16:10:09 +00:00
Paul Makles
c2a0ab71df ci: bring our own runner 2024-12-22 16:03:49 +00:00
Paul Makles
7a17165c24 ci: try pin to 20.04 2024-12-22 15:24:36 +00:00
Paul Makles
d7213fa409 fix(ci): try to work-around runner being killed 2024-12-22 14:13:30 +00:00
Paul Makles
479f0402ca ci: switch to ubuntu 24.04 pre-emptively
trying to fix build cancellations
2024-12-22 13:30:27 +00:00
Paul Makles
b62eeef80c chore(autumn): conditional animation stripping
feat(autumn): add /original redirect for files
fix(autumn): block non-images for non-attachment tags
fix(ci): use correct port for Rust test
2024-12-22 12:35:06 +00:00
Paul Makles
7b15006a50 fix: don't create "previews" of GIFs
chore: change hardcoded cache limit (should be configed!)
2024-12-20 16:42:32 +00:00
Paul Makles
ac731e547d chore: log database errors to Sentry 2024-12-17 21:09:44 +00:00
TheBobBobs
443f374f23 fix(core): fix _id typo (#384) 2024-12-17 17:52:59 +00:00
Kirill Mironov
42367f477c perf(lto): enable link-time optimization (#371)
Co-authored-by: Paul Makles <me@insrt.uk>
2024-12-17 17:23:15 +00:00
IAmTomahawkx
3cb91151ca fix exposed port in delta dockerfile 2024-12-15 19:39:57 -08:00
IAmTomahawkx
4c46054bff add uname to other docker images 2024-12-13 15:40:04 -08:00
IAmTomahawkx
f01794af93 fix: bonfire needs uname
fixes revoltchat/self-hosted#104
2024-12-12 01:58:55 -08:00
IAmTomahawkx
49f7f9549c feat: env variable to not clear presences on bonfire boot 2024-12-12 01:47:06 -08:00
IAmTomahawkx
72e3129557 fix: make docker the default connection for config 2024-11-28 15:44:00 -08:00
IAmTomahawkx
75a07a84c0 feat: version bump everything 2024-11-28 13:52:28 -08:00
Tom
b55765d7c7 Feat: Push notification server (#387)
* feat: create base of push daemon

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* Add outbound senders

* Make web_push send to rabbit instead (temp stuff)

* feat: stability and friend requests

* make vapid fr stuff not suck

* swap naming of queue

* move pushd into daemons folder

* fix cargo file for move into daemons folder

* feat: probably working fcm push notifs

* comment out fcm webpush stuff since the config keys dont exist

* fix fcm, name queues according to their prod status and configure routing keys

* add pushd to docker

* mix: Remove old code, add stuff to pushd

* fix: lockfile

* feat: update rocket to 5.0.1

* fix: fix queues and ack bugs

* Move rabbit messsage processing into ack queue

* chore: update readme

* chore: optimizations for ack database hits

* pushd flowchart

* misc: update flowchart

* exit dependancy hell

* add rocket_impl flag to authifier

* make the tests file of delta actually compile

* fix: don't silence every push message

* fix: don't silence all messages

* add debug logging for sending data to rabbit from message events

* validate mentions at a server membership level

* put back that import that was actually important

* minor fix to lockfile

* update delta authifier

* feat: proper permissions for push notifications

* add unit test for mention sanitization

* remove local file dependancy on authifier

* update ports to proper defaults

* fixTM the node bindings

* Theoretically configure docker releases for pushd

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* declare exchange in pushd and delta

* fix createbuckets script

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: reference db implementation

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: remove finally redundant code

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: changes

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: other changes

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: make channel name return result

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-11-28 13:34:17 -08:00
Paul Makles
ed5ded5e45 feat(autumn): use real memory size for s3 cache eviction
feat(january): use real memory size for proxy cache eviction
2024-11-28 11:58:36 +00:00
Paul Makles
249749e14d feat: file deletion implementation 2024-11-27 21:54:39 +00:00
Paul Makles
4c00a7dfb7 chore: strip dotenv (unmaintained) from project
It is no longer needed as configuration is loaded via. TOML or direct env if appropriate.
2024-11-27 17:05:11 +00:00
Paul Makles
acc4317246 feat: init crond crate 2024-11-27 16:56:30 +00:00
Paul Makles
b9ae333b02 fix(bonfire): make error handling consistent 2024-11-11 12:31:34 +00:00
Paul Makles
bf0fc504a9 chore(vscode): select nix env 2024-11-11 12:31:19 +00:00
Paul Makles
705e517871 fix: db migration for webhooks does not consider missing channels
fixes #378
2024-10-31 20:31:09 +00:00
Paul Makles
6f99ac2160 chore: bump little_exif debug code 2024-10-28 15:38:07 +00:00
Paul Makles
2fcc714546 chore: add helper script for running everything at once 2024-10-28 14:15:48 +00:00
Paul Makles
1e72f7bc77 chore: copy createbuckets config from self-hosted 2024-10-28 13:45:37 +00:00
Paul Makles
af0d24c7c6 fix: don't use failed file hashes
fixes #377
2024-10-28 13:40:40 +00:00
Paul Makles
ab58177dfa feat: add support for path style buckets 2024-10-27 23:28:28 +00:00
Paul Makles
df07426019 chore: bump version to 0.7.19 2024-10-24 17:48:11 +01:00
Paul Makles
c30d9a2620 chore(bonfire): fire error event with more information 2024-10-24 17:47:17 +01:00
Paul Makles
7b44317705 docs: add new features page 2024-10-24 17:45:10 +01:00
Paul Makles
876068a37e fix: populate empty vector if clear field is missing
fixes #367
2024-10-24 17:14:30 +01:00
IAmTomahawkx
397b9878e1 fix: temp fix for spam attack via notification bug abuse, but git actually commits the changes this time 2024-10-06 13:22:13 -07:00
Paul Makles
d9deadc65a fix: temp fix for spam attack via notification bug abuse 2024-10-06 02:34:47 -07:00
Paul Makles
58d3c5cc2e fix(services/january): remove image if video present and hence fix logic error
refactor(services/january): throw an error if embed fails to generate
2024-10-02 16:08:24 +01:00
Paul Makles
520fb02fb6 fix(services/january): support svg for embed generation 2024-10-02 15:20:35 +01:00
Paul Makles
2cb12a3d59 fix: do not handle error early 2024-10-02 15:05:57 +01:00
Paul Makles
18888eae5f fix: mangled symbol 2024-10-02 14:13:32 +01:00
Paul Makles
f31020fb6e refactor(core): add ImageProcessingFailed error 2024-10-02 14:12:26 +01:00
Paul Makles
bb202079e0 fix(services/january): put html parsing into block to prevent issues with futures 2024-10-02 14:09:41 +01:00
Paul Makles
bb6bcda8bd fix(services/january): reddit embeds by spoofing as discord / mapping to old reddit
closes #360
2024-10-02 12:56:32 +01:00
Paul Makles
68099bd2b7 chore: bump version & close issues
closes #366
closes #364
closes #359
closes #356
closes #354
closes #351
closes #348
closes #345
2024-10-02 12:39:32 +01:00
Paul Makles
a8db1cb40d feat(core/files): SVG rendering for thumbnails 2024-10-02 12:36:56 +01:00
Paul Makles
efa7ba78ed feat(core/files): support for jixel decoding
closes #342
2024-10-02 12:20:14 +01:00
Paul Makles
25fc692dc1 chore(core/models): truncate the rest of the embed fields 2024-10-02 11:35:37 +01:00
Paul Makles
530d68fe89 feat(services/autumn): add accept-language header
closes #358
2024-10-02 11:32:55 +01:00
Paul Makles
afd8c906ba fix(services/january): YouTube fallback 2024-10-02 11:31:55 +01:00
Paul Makles
c596dd5458 chore: bump all versions 2024-10-01 21:18:55 +01:00
Paul Makles
ed78b253ff feat(services/january): website embed generation 2024-10-01 21:11:08 +01:00
Paul Makles
66c84e0ad9 feat(services/january): image/video embeds 2024-10-01 19:13:48 +01:00
Paul Makles
21335b3297 feat(services/january): proxy images/videos 2024-10-01 19:02:39 +01:00
Paul Makles
8fc791f81a ci: remove .env file entry 2024-09-29 17:18:41 +01:00
Paul Makles
1689ee5ddc ci: use compose.yml file instead of deleted file 2024-09-29 17:08:42 +01:00
Paul Makles
7a061bb3c6 ci: add january to build list 2024-09-29 17:05:20 +01:00
Paul Makles
6c3b8eaa92 chore(core/files): do not decrypt file if nonce unavailable 2024-09-29 17:03:02 +01:00
Paul Makles
5e1b2e165f ci: add autumn to build list 2024-09-29 15:10:23 +01:00
Paul Makles
e270b5df6a chore: correct ports in Dockerfile 2024-09-29 15:10:12 +01:00
Paul Makles
cbf9e81256 fix: delete attachments by used_for.id and add corresponding index 2024-09-29 15:05:56 +01:00
Paul Makles
3080ec1f5a refactor: capture errors with line numbers
refactor: update file api
2024-09-29 14:37:08 +01:00
Paul Makles
4fd66b2719 refactor(core/result): add NotAuthenticated error which is distinct from InvalidCredentials 2024-09-29 12:02:10 +01:00
Paul Makles
916f47e2f5 feat(services/autumn): cors support 2024-09-29 12:01:32 +01:00
Paul Makles
a3c5b1bf87 chore: releasing beta build of autumn 2024-09-11 14:42:00 +01:00
Paul Makles
ace6c30ba5 feat(services/autumn): file uploads
feat(services/autumn): deduplicate uploads
feat(services/autumn): ClamAV support
2024-09-11 14:29:52 +01:00
Paul Makles
f4104612b2 chore: strip all the duplicate stuff from dev Revolt.toml 2024-09-10 16:54:07 +01:00
Paul Makles
6209bc7152 feat(services/autumn): authenticate the user on upload
feat(services/autumn): file size limits on upload
chore: add database migrations for new autumn things
2024-09-10 16:53:58 +01:00
Paul Makles
1a6a8a809b feat(core/database): axum implementation for User 2024-09-10 16:53:02 +01:00
Paul Makles
5485781e7c docs: update README with more badges 2024-09-10 15:15:53 +01:00
Paul Makles
8b21825ffe docs: update README 2024-09-09 14:24:22 +01:00
Paul Makles
530ff4f9c7 docs: this will be the developer experience ever (deprecate .env file )
fix: port is wrong for maildev
2024-09-09 14:19:31 +01:00
Paul Makles
2eb596fe99 chore: pin crate versions to "MaxSRV" 2024-09-09 13:05:43 +01:00
Paul Makles
0bbb9e7072 docs: start work on default development configuration 2024-09-09 12:49:38 +01:00
Paul Makles
2433fa661a chore: lock Rust version to 1.77.2 (related to #341) 2024-09-09 12:49:09 +01:00
Paul Makles
70bdeecf77 feat(services/autumn): strip exif data for images/videos
feat(services/autumn): hash files and log information
2024-09-06 15:10:36 +01:00
Paul Makles
24dc96f80f feat(services/autumn): work towards upload API 2024-09-01 16:42:28 +01:00
Paul Makles
ebbbb5e174 feat(services/autumn): download and preview files 2024-09-01 13:58:11 +01:00
Paul Makles
78757ac7f1 fix: spelling issue 2024-08-31 17:16:23 +01:00
Paul Makles
01368960f3 feat(core/database, bindings/node): suspend user 2024-08-31 17:10:47 +01:00
Paul Makles
ae1d5d07e3 docs: Brexit 2024-08-31 16:04:59 +01:00
Paul Makles
5ad72abca7 refactor: move authifier config to database package 2024-08-31 16:04:40 +01:00
Paul Makles
c1b92ef56e feat: scaffold services/january 2024-08-30 19:48:58 +01:00
Paul Makles
acbc1b8956 feat(core/database): clear expired FCM tokens 2024-08-30 15:51:20 +01:00
Paul Makles
bce24c8b1b chore(scripts): add push to publish-debug-image.sh [skip ci] 2024-08-30 15:34:53 +01:00
Paul Makles
31262315e7 chore(fcm): just send the whole payload as a json string 2024-08-30 15:32:39 +01:00
Paul Makles
d8acaa1107 chore: strip information from fcm notification for testing 2024-08-30 12:46:37 +01:00
Paul Makles
730039eda6 chore: switch to distroless Docker runtime images
fix: match Debian releases for build

closes #297

Co-authored-by: MaticPoh <85814976+MaticPoh@users.noreply.github.com>
2024-08-30 12:06:05 +01:00
Paul Makles
79e1388000 fix: use MSRV for dockerfile 2024-08-29 20:27:47 +01:00
Paul Makles
5c8ece0727 fix: add new crates to docker build 2024-08-29 20:26:16 +01:00
Paul Makles
ab6b92aa2a chore(core/files): update toml 2024-08-29 20:22:09 +01:00
Paul Makles
239710951a chore: add revolt-files to justfile publish cmd 2024-08-29 20:20:47 +01:00
Paul Makles
28960979ec chore: bump version to 0.7.16 2024-08-29 20:18:34 +01:00
Paul Makles
1938ebc3fa feat(services/autumn): scaffold implementation of api 2024-08-29 20:18:17 +01:00
Paul Makles
1fb4032d9e refactor: switch from fcm to fcm_v1 crate 2024-08-29 20:17:52 +01:00
Paul Makles
ae2194cce8 chore: (wip) suspend bindings 2024-08-29 20:17:35 +01:00
Paul Makles
949a0cf649 chore(core/config): add configuration options 2024-08-29 20:17:10 +01:00
Paul Makles
f6c57b23b4 feat: bindings for node.js 2024-08-26 20:01:10 +01:00
Paul Makles
12ae781621 fix: detach rather than delete emoji during server deletion 2024-08-20 13:19:23 +01:00
Paul Makles
a52d610e6c ci: use docker compose instead of docker-compose
Signed-off-by: Paul Makles <me@insrt.uk>
2024-08-05 16:32:21 +01:00
Paul Makles
f513a9cb5e chore: bump version to 0.7.15 2024-08-05 16:22:18 +01:00
Paul Makles
535f01604b fix(core/database): include users from pin events 2024-08-05 16:21:00 +01:00
Paul Makles
f8ec6ba5ff fix(delta): add check to roles_fetch route 2024-08-05 16:20:21 +01:00
Paul Makles
b8fdc07508 refactor: remove inspect_err debug code 2024-08-05 16:20:21 +01:00
Paul Makles
5ce7530206 chore: bump nixpkgs and MSRV 2024-08-05 16:20:21 +01:00
Paul Makles
b36bac3225 merge: pull request #333 from revoltchat/feat/apns
APNS fixes
2024-08-05 17:17:29 +02:00
infi
beef06ebe3 chore: applemusic option for special embed
See https://github.com/revoltchat/january/pull/41

Signed-off-by: Infi <infi@infi.sh>
2024-08-05 17:16:24 +02:00
Zomatree
4fc46f765b refactor: change Channel::id to return a reference 2024-07-31 00:05:40 +01:00
Zomatree
b45ae2cd4d fix: include user for pinned system messages 2024-07-30 18:09:35 +01:00
Zomatree
0a8089787b chore: remove debugging logging 2024-07-30 18:06:54 +01:00
Paul Makles
475fa6f2e6 chore: add optional fields for CI as template [skip ci]
Signed-off-by: Paul Makles <me@insrt.uk>
2024-07-28 10:43:01 +01:00
Zomatree
96bf73cb57 fix: send pin system messages 2024-07-22 00:09:27 +01:00
Zomatree
87a9fb79f2 chore: Write tests for pinning 2024-07-21 22:22:46 +01:00
Zomatree
c50435d499 fix: Switch to remove for unpinning 2024-07-21 22:16:23 +01:00
Zomatree
d2f4823ceb chore: Add index and migration 2024-07-21 22:12:55 +01:00
Zomatree
39230c559a fix: Bump revolt_a2 dep 2024-07-20 22:43:12 +01:00
Zomatree
32d1d5df2e feat: Send badge updates from message acks 2024-07-20 20:49:22 +01:00
Zomatree
7547fbe245 Merge branch 'IAmTomahawkx-main' 2024-07-20 19:51:41 +01:00
IAmTomahawkx
9f13cb6f47 add Badge update capabilities
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-19 13:13:13 -07:00
Tom
d94571fbf9 Merge branch 'main' into main
Signed-off-by: Tom <iamtomahawkx@gmail.com>
2024-07-19 09:20:42 -07:00
Zomatree
389ecc0e5c feat: Message pinning 2024-07-18 18:37:41 +01:00
Paul Makles
412f4a99d7 feat(core/events): add user settings / unreads to Ready payload
refactor(core/events): make all Ready payload items optional
2024-07-14 17:03:40 +01:00
Paul Makles
caa0795d07 fix(core/database); never include relations on known users 2024-07-14 16:27:11 +01:00
Paul Makles
5c82343bb3 chore: bump version to 0.7.14
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
Paul Makles
e17af1c064 feat: add reason to ServerMemberLeave event
closes #314

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
Paul Makles
a9e309395e fix: execute query to remove users from groups
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
IAmTomahawkx
8468ce19cb fix apple push notifications
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
IAmTomahawkx
27f15f7b02 change apns to use sandbox, and (somewhat) provide a custom payload
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
Paul Makles
78b9fa168b chore: bump version to 0.7.14 2024-07-10 14:17:31 +01:00
Paul Makles
e290d168ac feat: add reason to ServerMemberLeave event
closes #314
2024-07-10 14:12:53 +01:00
Paul Makles
78cd89ec32 fix: execute query to remove users from groups 2024-07-10 14:04:17 +01:00
Paul Makles
f9d9059e73 fix: bot model test did not destruct tuple 2024-06-29 18:20:01 +01:00
Paul Makles
0954d71f51 chore: bump version to 0.7.13 2024-06-29 17:36:54 +01:00
Paul Makles
9ea2bd9f2f feat: implement apple push notifications 2024-06-29 17:35:47 +01:00
Paul Makles
d6bcb844db feat: include user in responses when creating and editing bots 2024-06-26 19:49:26 +01:00
Paul Makles
ce20e689cc refactor: fix spelling of "suppress" 2024-06-26 19:48:42 +01:00
Paul Makles
d9cfc9d0e1 chore: bump version to 0.7.12 2024-06-26 18:47:40 +01:00
Paul Makles
64a07d09f4 feat: silent messages and message flags 2024-06-26 18:46:05 +01:00
Paul Makles
00e881799f fix: replace user with target user when editing someone else 2024-06-26 18:24:38 +01:00
Paul Makles
fa598dd6f8 docs: update README.md
Signed-off-by: Paul Makles <me@insrt.uk>
2024-06-26 17:18:10 +01:00
Paul Makles
ac4e2cb10b ci: disable image builds if they're not tagged [skip ci]
(seems like a waste of CPU)
2024-06-26 15:08:12 +01:00
Paul Makles
ec578ab0ef chore: lessen restrictions on interacting with bot users as owner 2024-06-26 15:07:19 +01:00
Paul Makles
ea6ba59841 fix: add DisplayName back to user fields (prevented removing name) 2024-06-26 14:54:06 +01:00
Paul Makles
f903f716e3 chore: bump version to 0.7.11 2024-06-25 21:03:00 +01:00
Paul Makles
c5f4b94aa5 fix(bonfire): Ready event should include user with online status true (if applicable) 2024-06-25 21:02:27 +01:00
Paul Makles
1d5dae4751 feat(core): include user and member on Message events 2024-06-25 21:01:45 +01:00
Paul Makles
ac20b6bc99 chore: bump version to 0.7.10 2024-06-25 19:24:28 +01:00
Paul Makles
6ec8007e4e feat(core): separate limits for new user accounts 2024-06-25 19:20:34 +01:00
Paul Makles
e5eea267cf refactor(core/config): move limits to 'global' key 2024-06-25 18:10:23 +01:00
Paul Makles
de5add09d0 fix(core/database): only run outgoing friend checks if we are creating new request
closes #327
2024-06-25 18:07:16 +01:00
Paul Makles
1ec8f46c1d chore: bump version to 0.7.9 2024-06-23 19:39:32 +01:00
Paul Makles
80666848cc feat(core): add a limit to no. of outgoing pending friend requests 2024-06-23 19:39:18 +01:00
Paul Makles
e8e9613169 chore: bump version to 0.7.8 2024-06-21 16:13:16 +01:00
Paul Makles
eda36436a8 fix: specify configuration 2024-06-21 16:13:02 +01:00
Paul Makles
93e05e9f18 fix(bonfire): ignore all Redis errors but Canceled 2024-06-20 22:12:00 +01:00
Paul Makles
090d8cba71 chore: bump version to 0.7.7 2024-06-20 21:41:12 +01:00
Paul Makles
f16e72e329 fix(bonfire): drop connections if Redis disconnects 2024-06-20 21:40:44 +01:00
Paul Makles
8248a4a5b8 fix(bonfire): clean up Redis connection after disconnect 2024-06-20 21:30:47 +01:00
Paul Makles
cf0b4274c8 chore: bump version to 0.7.6 2024-06-19 18:25:11 +01:00
Paul Makles
36ecb48c7b feat(bonfire): don't fan out UserUpdate (server) by default 2024-06-19 18:24:51 +01:00
Paul Makles
ba411fef43 chore: bump version to 0.7.5 2024-06-15 11:02:30 +01:00
Paul Makles
4055363cff fix(core/database): fetch users mentioned in system messages 2024-06-15 11:02:09 +01:00
Paul Makles
d179d1e695 fix(delta): don't specify the other member in permission check
supersedes / closes #323
2024-06-15 11:01:03 +01:00
Paul Makles
fc9ceb7540 refactor(bonfire): add additional error handling 2024-06-15 10:51:00 +01:00
Paul Makles
a31a3e2e65 refactor: remove profile from v0 user, added unintentionally 2024-06-15 10:50:43 +01:00
Paul Makles
b12e728514 feat(core/result): implement std::error::Error for Error 2024-06-15 10:50:22 +01:00
Paul Makles
962c7d62c7 fix: allow fetching discoverable servers as invites 2024-06-11 18:46:07 +01:00
Paul Makles
d677716f93 docs: update development guide 2024-06-11 14:16:44 +01:00
Paul Makles
8099310f89 fix: wait on unwaited futures 2024-06-11 14:16:35 +01:00
Paul Makles
5c40f66010 fix: wrong match keyword causing disconnects on typing event 2024-06-11 14:01:59 +01:00
Paul Makles
8e7dd21bce docs: add note about creating GitHub release [skip ci] 2024-06-11 13:08:24 +01:00
Paul Makles
4868205df4 chore: bump version to 0.7.4 2024-06-11 12:52:45 +01:00
Paul Makles
96fb0eecca fix: don't allow sending typing notification to unknown channel
closes #177
2024-06-11 12:51:52 +01:00
Paul Makles
2cb20618da feat(bonfire): add disconnection mechanism
feat(bonfire): handle session deletion and logout events
feat(core): trigger logout on bot token reset
2024-06-11 12:38:32 +01:00
Paul Makles
92e948aabc chore: update README [skip ci] 2024-06-09 14:11:30 +01:00
Paul Makles
cb4435d700 chore: bump version to 0.7.3 2024-06-09 13:49:38 +01:00
Paul Makles
4c6e78e1a5 chore: relicense some core crates to MIT
chore: update/create LICENSE files
closes #278
closes #277
2024-06-09 13:47:22 +01:00
Paul Makles
da9a91e05f fix(bonfire): send InvalidSession error if token is not sent successfully
closes #289
2024-06-09 13:42:12 +01:00
Paul Makles
506968634e fix(core/database): set do not set online status if presence is Invisible
closes #34
2024-06-09 13:41:51 +01:00
Paul Makles
a5d0cdf0dd fix(core/database): skip insertion / deletion if no invites need to be corrected 2024-06-04 18:46:11 +01:00
Paul Makles
c59aad43c8 chore: bump version to 0.7.2 2024-05-16 15:44:09 +01:00
Paul Makles
19e72babc9 fix: must not use replace_one as it expects no change in _id 2024-05-16 15:43:33 +01:00
Paul Makles
bfcdb13d6d chore: remove cla workflow 2024-05-16 15:33:07 +01:00
Zomatree
878c1a83ad fix: continue fix 2024-05-15 22:57:34 +01:00
Zomatree
471e0f55e4 fix: Use correct enum tagging for invites 2024-05-15 22:48:20 +01:00
Paul Makles
08127218ce ci: fix path to pages artifact
ci: ignore changes to documentation for docker/rust builds
2024-05-11 17:50:52 +01:00
Paul Makles
b69202c222 ci: add deployment action for documentation 2024-05-11 17:42:50 +01:00
Paul Makles
b3494b12fc docs: create mdbook 2024-05-11 17:42:08 +01:00
Paul Makles
7703475868 chore: split sentry dsn into separate options per-app 2024-04-08 23:25:53 +01:00
Paul Makles
4be3bdc4c3 fix: return more appropriate error on token fetch fail 2024-04-08 23:24:49 +01:00
Paul Makles
3e26e7e89d fix: use correct id to lookup user from session 2024-04-08 23:24:33 +01:00
Paul Makles
13b95d383e fix: import std::env in config 2024-04-08 22:46:35 +01:00
Paul Makles
3c76005c33 ci: strip references to quark 2024-04-08 22:00:33 +01:00
Paul Makles
3065e153d5 refactor: delete quark crate (closes #239) 2024-04-08 21:56:35 +01:00
Paul Makles
a1eeedabb9 refactor(bonfire): remove quark dependency 2024-04-08 21:49:19 +01:00
Paul Makles
0fc59b8e2a fix: add validator dependency to database 2024-04-07 23:49:09 +01:00
Paul Makles
265d3ba8ef chore: bump version to 0.7.0 2024-04-07 23:45:29 +01:00
Paul Makles
20d398d02c chore: remove quark dependency from delta 🎉
closes #283

fix: allow setting port and use_tls from config
closes #143
2024-04-07 23:41:31 +01:00
Paul Makles
6bc74749d2 refactor(quark): port message_query, message_search
#283
2024-04-07 23:13:45 +01:00
Paul Makles
301676fb54 refactor(quark): port report_content & code clean-up
#283
2024-04-07 22:57:55 +01:00
Paul Makles
6b488f347e feat(core/database): implement reports & snapshots 2024-04-07 22:42:46 +01:00
Paul Makles
1a8e43d280 ci: also update the first job 2024-04-07 22:29:24 +01:00
Paul Makles
acb4190f70 fix: delete associated server objects 2024-04-07 22:26:56 +01:00
Paul Makles
9e42567050 refactor(quark): port roles_create, roles_delete, roles_edit, roles_fetch, server_ack, server_delete, server_edit, server_fetch
#283
2024-04-07 22:26:56 +01:00
Sophie L
78fb6e1982 fix(ci): remnants from branch rename 2024-04-07 22:26:31 +01:00
Paul Makles
9de963fb56 refactor(quark): port member_edit, member_experimental_query, member_fetch_all, member_remove, permissions_set_default, permissions_set
#283
2024-04-07 21:35:45 +01:00
Paul Makles
03340a2b5a refactor(quark): port ban_create, ban_list, ban_remove, emoji_list, invites_fetch
#283
2024-04-07 20:48:36 +01:00
Paul Makles
b2d3344ddd refactor(quark): port edit_user, fetch_dms, fetch_profile, fetch_self, fetch_user_flags, fetch_user, find_mutual, open_dm
#283
2024-04-07 19:38:08 +01:00
Paul Makles
49bb235938 refactor(quark): port add_friend, block_user, change_username, remove_friend, send_friend_request, unblock_user
#283
2024-04-07 18:30:59 +01:00
Paul Makles
4e7049d51e fix: fully qualify create_error in create_database_error macro 2024-04-07 18:11:37 +01:00
Paul Makles
2de7598f8d refactor(quark): port get/set_settings, get_unreads
#283
2024-04-07 18:10:11 +01:00
Paul Makles
f6a565385e refactor(quark): port invite_delete, invite_fetch, invite_join 2024-04-07 17:47:59 +01:00
Paul Makles
569bd1d5e1 refactor(quark): port message_react, message_send, message_unreact, permissions_set_default, permissions_set
#283
2024-04-07 17:12:59 +01:00
Paul Makles
aca1fe6dff refactor(quark): port message_edit, message_fetch, voice_join 2024-04-07 16:47:46 +01:00
Paul Makles
aded2e3239 fix: add elevation check to role edit route 2024-04-07 15:52:14 +01:00
Paul Makles
54a4eff623 refactor(quark): port channel_ack, channel_delete, channel_ edit, group_remove_member, invite_create, members_fetch, message_bulk_delete, message_clear_reactions, message_delete
#283
2024-04-07 15:41:43 +01:00
Paul Makles
50c36dcefd docs: update environment variables [skip ci] 2024-03-29 11:06:09 +00:00
insertish
f16c275bcb ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2024-03-28 13:37:54 +00:00
Paul Makles
74ff072199 docs: deployment lifecycle added to README 2024-03-13 22:43:49 +00:00
Paul Makles
52431bd1dd docs: update README [skip ci] 2024-03-13 22:08:31 +00:00
Paul Makles
fa67e87467 chore: switch branch name to main [skip ci] 2024-03-13 22:08:23 +00:00
Bob Bobs
8f9b18444a chore(bonfire): log errors from websocket stream 2024-03-13 21:47:34 +00:00
Paul Makles
7b18e983c9 chore: faster tag and release procedure 2024-02-12 21:40:51 +00:00
Paul Makles
c0e64c9052 chore: bump version 2024-02-12 21:34:49 +00:00
Paul Makles
0d54ccd342 chore: update justfile for faster releases 2024-02-12 21:33:01 +00:00
Paul Makles
d4d5b23c7d fix: use a valid default permission set for groups 2024-02-12 21:23:38 +00:00
Paul Makles
a641034895 test: write a unit test for group chat permissions 2024-02-12 21:19:36 +00:00
Paul Makles
9dd24526ef chore: clippy recommendations 2024-02-12 21:13:31 +00:00
Bob Bobs
6d4f772d78 fix(ci): only start run command in background 2024-02-12 21:12:24 +00:00
Bob Bobs
4c4dada3da fix(ci): build API then run in background to prevent timeout 2024-02-11 14:16:31 +00:00
Paul Makles
cf82972a80 chore: bump version 2024-02-10 23:51:04 +00:00
Bob Bobs
ea00f0fec1 fix(delta): set ratelimit headers on 429 responses 2024-02-10 23:49:15 +00:00
Bob Bobs
226dbca6e0 fix(core/models): use IndexMap for reactions 2024-02-10 23:45:34 +00:00
Bob Bobs
e4b5a23f71 fix(delta): MemberResponse only include roles member has 2024-02-10 23:44:20 +00:00
Bob Bobs
9663caa1ff fix(delta): use untagged enum for MemberResponse 2024-02-10 23:44:20 +00:00
Paul Makles
f9f0f409a8 chore: bump version to 0.6.11 2024-02-10 01:05:08 +00:00
Paul Makles
8f4d6c864b feat: block mentions from low trust users (account created < 12 hours) 2024-02-10 01:04:52 +00:00
Paul Makles
513461c99a chore: bump version 2024-02-06 13:43:00 +00:00
ToastXC
41ef76ff3e feat: member fetching with roles (#299) 2024-02-06 13:20:35 +00:00
Paul Makles
c451e431cd merge: pull request #301 from TheBobBobs/bonfire-redis
fix(bonfire): random disconnects from redis
2024-02-06 13:18:55 +00:00
Amy
09a848f5a6 fix: don't exceed the max emoji limit by one
closes #295
2024-02-06 13:17:13 +00:00
Bob Bobs
308193377d fix(bonfire): use REDIS_URI env var 2024-02-02 10:31:11 -07:00
Bob Bobs
3d3326e68a (bonfire): parse correct PayloadType 2024-02-01 20:07:12 -07:00
Bob Bobs
88c2232a6c fix(bonfire): use fred for redis
https://github.com/aembke/fred.rs
2024-02-01 19:00:45 -07:00
Bob Bobs
25016efbc2 fix(database): fetch all server emojis instead of one 2023-12-02 11:46:18 +00:00
Bob Bobs
db63ac4064 fix: add emojis to ServerCreate event 2023-12-02 11:46:18 +00:00
TheBobBobs
1f593a1708 fix: allow reacting with existing emojis when at limit 2023-12-02 11:45:23 +00:00
TheBobBobs
219c16a69c fix: set relationship for users in BulkMessageResponse 2023-12-01 20:54:59 +00:00
Paul Makles
aed8f69f34 fix(database): set channel ids for new server object 2023-11-20 10:54:48 +00:00
Paul Makles
196a856c68 refactor: move and update fixme / todo comments 2023-10-28 14:53:39 +01:00
Paul Makles
9bf60e87e9 fix: validate masquerade on new message models 2023-10-28 14:53:28 +01:00
Paul Makles
b90e7a4412 fix: prevent potential double fetching of cached server 2023-10-28 14:53:12 +01:00
Paul Makles
cb813e91f0 chore: cut new 0.6.9 release 2023-10-28 14:39:10 +01:00
Paul Makles
a6bc9fcbb3 refactor: implement Server::create 2023-10-28 14:25:29 +01:00
Paul Makles
cd0737911b refactor: use fixtures to populate db
refactor: method for creating server channels
2023-10-27 16:31:37 +01:00
Paul Makles
7705354d4d ci: use reference db for generating spec 2023-10-26 15:07:03 +01:00
Paul Makles
39ac8b2278 refactor: don't create unnecessary reference 2023-10-26 14:12:28 +01:00
arslee07
c5494aa1ca fix: respect Permission::SendEmbeds when editing a message 2023-10-26 14:12:28 +01:00
Paul Makles
1a96bf5a4b chore: remove redundant file [skip ci] 2023-10-26 14:05:01 +01:00
Paul Makles
37a1244751 test: ignore hello world test because it breaks cargo test? 2023-10-26 14:05:01 +01:00
Paul Makles
bf39f18f8d fix: always generate new test database 2023-10-26 14:05:01 +01:00
Paul Makles
95be7dc87a ci: use nextest because cargo test sig sevs 2023-10-26 14:05:01 +01:00
Paul Makles
9be171c7b6 fix: don't copy the old test file 2023-10-26 14:05:01 +01:00
Paul Makles
786fbb8ae4 chore: disable broken tests 2023-10-26 14:05:01 +01:00
Paul Makles
6c0210e8a1 feat: always use test configuration during tests 2023-10-26 14:05:01 +01:00
Paul Makles
456bf7b42a fix: check server ownership for text channel
refactor: minor cleanup of code
2023-10-26 14:05:01 +01:00
Paul Makles
21ffea0f9f fix(core/database): store member during permission query 2023-10-26 14:05:01 +01:00
Paul Makles
96d90215d2 fix(core/models): validator feature flag not properly gatekept 2023-10-26 14:05:01 +01:00
Paul Makles
29dcf714f2 chore: start work on test for permission issue 2023-10-26 14:05:01 +01:00
Paul Makles
cb97004d3f feat(core): provide user profile where appropriate 2023-10-16 09:33:36 +01:00
Paul Makles
0b6e5dd9db chore: add nix file to vscode config 2023-10-16 09:23:11 +01:00
Paul Makles
7132877201 feat(core): validation for files in reference db 2023-10-16 09:22:53 +01:00
Paul Makles
4d42fb74e7 fix(core): publish user settings event 2023-10-16 09:22:17 +01:00
Paul Makles
866ace3381 refactor: use config for max bot count 2023-10-16 09:21:48 +01:00
Paul Makles
0798e81862 feat(core): implement ratelimit events for reference db 2023-10-16 09:21:34 +01:00
Paul Makles
2fa5ac41ac refactor: tests for more group routes 2023-09-22 19:04:17 +01:00
Paul Makles
25ae1555a2 chore: add nextest and fmt to nix 2023-09-22 19:03:49 +01:00
Paul Makles
881888bd1c chore: add timeout for nextest runs 2023-09-22 19:03:29 +01:00
Paul Makles
41f20c2239 ci: use custom Revolt.toml for tests 2023-09-10 15:46:00 +01:00
Paul Makles
38f1381fea chore: switch to resolver=2 2023-09-10 15:19:23 +01:00
Paul Makles
6838b0b1f0 refactor: ensure in-memory database consistency
test: update authifier to allow for in-memory tests
ci: explicitly run in-memory and mongo tests
2023-09-10 14:49:27 +01:00
Paul Makles
055deab105 fix: allow bots to authenticate with new routes 2023-09-10 14:48:13 +01:00
Paul Makles
6d2073120d fix: carry over send embed permission when sending [skip ci] 2023-09-10 12:41:23 +01:00
Paul Makles
9789909061 feat(delta): rewrite emoji routes w/o quark
#283
2023-09-05 12:36:18 +01:00
Paul Makles
895de86f1e fix(core/config): use uppercase filename 2023-09-03 19:00:17 +01:00
Paul Makles
98b8c5403b feat(delta): rewrite channel / message fetch 2023-09-03 18:27:21 +01:00
Paul Makles
aa7c75c60e chore: ignore Revolt.toml 2023-09-03 18:12:15 +01:00
Paul Makles
4270f0c5d4 feat(core): implement create group 2023-09-03 18:12:08 +01:00
Paul Makles
eb1f45d208 fix(core/database): use attachments collection name 2023-09-03 17:43:13 +01:00
Paul Makles
9929f7499a refactor(core): use core crates for sending messages 2023-09-03 16:31:39 +01:00
Paul Makles
279d9ef1b5 refactor(core): remove quark references from webhook routes 2023-09-03 15:52:18 +01:00
Paul Makles
5a9bb9e68d fix: convert index map/set to hash map/set
fix: idempotency token should use correct crate cfg
2023-08-27 15:51:20 +01:00
Paul Makles
a8fc9e928a fix: import validator crate directly in derive 2023-08-27 15:45:23 +01:00
Paul Makles
da231ae390 fix: remove indexmap from models 2023-08-27 15:43:57 +01:00
Paul Makles
7a31da4189 fix: add indexmap/serde to model depends 2023-08-27 15:40:05 +01:00
Paul Makles
7d0b5392b5 fix: should include crate in build script too 2023-08-27 15:33:28 +01:00
Paul Makles
1e1d44c0e4 fix: add config crate to Dockerfile
chore: bump release version
2023-08-27 15:30:53 +01:00
Paul Makles
1c4588967f merge: branch 'insert/dev-branch' 2023-08-27 15:18:52 +01:00
Paul Makles
853366a297 feat: add support for Firebase Cloud Messaging 2023-08-27 15:18:19 +01:00
Paul Makles
7568f49755 test: add tests to bot routes 2023-08-27 15:01:22 +01:00
Paul Makles
0542788567 feat(delta): test harness for web server 2023-08-27 13:01:21 +01:00
Paul Makles
73f7b8f007 feat: ensure bot invites work correctly 2023-08-27 12:21:48 +01:00
Paul Makles
c2412190a8 refactor: solve clippy hints 2023-08-27 11:23:02 +01:00
Paul Makles
5372296dc0 feat(core): implement message tasks
includes:
- acknowledgements
- last_message_id
- embeds
- web push
2023-08-27 11:17:03 +01:00
Paul Makles
41a47bdf8f feat: add revolt_config crate 2023-08-27 11:15:20 +01:00
Paul Makles
51c26e324d refactor: remove quark/web 2023-08-26 16:19:00 +01:00
Paul Makles
3a55d00c6a chore: disable nix environment extension 2023-08-26 16:17:21 +01:00
Paul Makles
004e84ece9 fix: revoke all channel permissions if can't view 2023-08-26 16:17:20 +01:00
Paul Makles
3d0914ce52 chore: add nix shell configuration 2023-08-26 16:17:19 +01:00
Paul Makles
bde432cb75 chore: strip bot model from quark 2023-08-26 16:16:48 +01:00
Paul Makles
28d225952a chore: add separate debug Docker build script 2023-08-26 15:34:25 +01:00
Paul Makles
2fb9e7a802 merge: branch 'master' into insert/dev-branch 2023-08-26 13:34:44 +01:00
Paul Makles
431046cb4c chore: cut new release with redis patches and debug profile 2023-08-26 13:33:05 +01:00
Zomatree
92032cd9bc chore: Log errors from failed pub/sub messages 2023-08-26 13:14:24 +01:00
Lea
bfdd713d38 fix: allow fetching mutuals for bots 2023-08-26 13:14:15 +01:00
Paul Makles
430df03b20 chore: bump sentry crate 2023-08-10 09:42:44 +01:00
Paul Makles
f5f70287e7 feat: add SENTRY_DSN env variable 2023-08-10 09:40:41 +01:00
Paul Makles
d5ba4ebf0c feat(core/database): implement permissions backend 2023-08-05 17:42:23 +01:00
Paul Makles
3bfead4ff1 feat(core/database): system message helper 2023-08-05 17:03:16 +01:00
Paul Makles
d04621b1d6 feat(core): implement message db methods 2023-08-05 16:59:58 +01:00
Paul Makles
a681df04bd feat(core): permissions query, finish bots impl 2023-08-05 16:14:47 +01:00
Paul Makles
9f3c1036d0 chore: refine user and bot creation methods 2023-08-05 12:06:43 +01:00
Paul Makles
c9011ac692 chore: migrate bots create route 2023-08-05 11:24:02 +01:00
Paul Makles
42f977f536 feat: start work on migrating routes 2023-08-03 22:11:48 +01:00
Paul Makles
a0580c5f5a chore: remove legacy query route 2023-08-03 19:43:05 +01:00
Paul Makles
121a9cd87c feat(core): implement message model
closes #273
2023-08-03 19:32:47 +01:00
Paul Makles
d87d608d9e chore(core): flesh out implementations for channels / servers 2023-08-03 18:27:13 +01:00
Paul Makles
bd9ecc1519 fix: missing conversion 2023-08-02 00:31:19 +01:00
Paul Makles
f5a113da47 feat(core): implement server ban
closes #274
2023-08-01 21:19:07 +01:00
Paul Makles
11fdb0c1dc feat(core): implement emoji model
closes #270
2023-08-01 20:45:58 +01:00
Paul Makles
e0033ceb12 feat(core): implement channel unreads model
closes #269
2023-08-01 17:41:43 +01:00
Paul Makles
7318ec6ef6 chore(core/models): add cfg_attr where necessary 2023-08-01 15:54:56 +01:00
Paul Makles
a516c7adcf feat(core): implement channel invite model
closes #268
2023-08-01 15:53:43 +01:00
Paul Makles
c48109ca66 docs: add note for 'Id' casing 2023-08-01 15:51:57 +01:00
Paul Makles
f0ef781cd7 chore: add MSRV to README [skip ci]
closes #254
2023-08-01 15:04:45 +01:00
Paul Makles
d9dd040db4 feat(core/models): implement server members model
closes #280
2023-08-01 15:00:07 +01:00
Paul Makles
fec947b4c8 feat(core/models): implement server model
closes #279
2023-08-01 14:55:25 +01:00
Paul Makles
ec6df36c25 chore: strip legacy admin API 2023-08-01 14:35:36 +01:00
Paul Makles
76150db293 chore: add prometheus metrics 2023-07-04 17:52:40 +01:00
Paul Makles
815c3fe99e chore: cut a new release (0.6.4) 2023-07-02 08:45:39 +01:00
Zomatree
54878e8e8d feat: add webhook permissions 2023-07-02 08:41:38 +01:00
TheBobBobs
6e4798f1d4 fix(bonfire): populate cache for calculating permissions (#259) 2023-07-02 08:40:15 +01:00
TheBobBobs
55bd6fb087 fix: incorrect owner rank 2023-07-02 08:35:49 +01:00
Paul Makles
49035f4817 feat: ratelimit user edit route and discriminator changes 2023-06-15 19:24:53 +01:00
Paul Makles
c0ebaa0bd3 chore: include discriminator in ban list response 2023-06-15 19:23:47 +01:00
Paul Makles
b98b244fc3 fix: enable staging for API spec generation 2023-06-15 15:21:00 +01:00
Paul Makles
c8d5128b0c chore: add additional sanitisation 2023-06-11 12:26:18 +01:00
Paul Makles
0578a05a05 feat: add remove "displayname" field 2023-06-11 12:05:08 +01:00
Paul Makles
9a412b3e08 fix: actually update the display name 2023-06-11 11:50:21 +01:00
Paul Makles
8deec1f80a fix: display name is optional 2023-06-11 10:56:03 +01:00
Paul Makles
26afbeed84 chore: extend discriminator block list 2023-06-11 10:49:51 +01:00
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
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
494 changed files with 38731 additions and 13629 deletions

2
.config/nextest.toml Normal file
View File

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

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

2
.github/FUNDING.yml vendored
View File

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

37
.github/workflows/book.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Build documentation
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write # To push a branch
pages: write # To push to a GitHub Pages site
id-token: write # To update the deployment status
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install latest mdbook
run: |
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
mkdir mdbook
curl -sSL $url | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
- name: Build Book
run: |
cd doc
mdbook build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "doc/book"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,36 +0,0 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
jobs:
CLAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: cla-assistant/github-action@v2.1.3-beta
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN : ${{ secrets.PAT }}
with:
path-to-signatures: 'signatures/version1/cla.json'
path-to-document: 'https://github.com/revoltchat/cla/blob/master/CLA.md' # e.g. a CLA or a DCO document
# branch should not be protected
branch: 'master'
allowlist: insertish,bot*
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
remote-organization-name: revoltchat
remote-repository-name: cla
create-file-commit-message: 'cla(create): creating file for storing CLA Signatures'
signed-commit-message: 'cla(sign): $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
#use-dco-flag: true - If you are using DCO instead of CLA

View File

@@ -1,116 +1,115 @@
name: Docker Test & Publish
name: Docker
on:
push:
branches:
- "master"
tags:
- "*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- ".vscode/**"
- ".gitignore"
- "LICENSE"
- "README"
- "handmade"
pull_request:
branches:
- "master"
paths:
- "Dockerfile"
workflow_dispatch:
- "handmade"
release:
types:
- published
permissions:
contents: read
jobs:
base:
runs-on: ubuntu-latest
name: Build base image (amd64)
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Builds the shared base image: the whole Rust workspace compiled once.
# Every service image (currently only pushd) is derived from this so the
# workspace is built a single time per release. On non-release events this
# still runs as a compile check, but nothing is pushed.
base:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Resolve registry host and owner
id: registry
# Owner is lowercased: Docker repository names must be lowercase.
run: |
echo "host=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_OUTPUT"
echo "owner=$(echo "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Base image ref
id: base
run: echo "image=${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/handmade-revolt-base" >> "$GITHUB_OUTPUT"
- name: Login to registry
uses: docker/login-action@v3
if: github.event_name == 'release'
with:
registry: ${{ steps.registry.outputs.host }}
username: ${{ secrets.PACKAGE_PUBLISH_USERNAME }}
password: ${{ secrets.PACKAGE_PUBLISH_TOKEN }}
- name: Build base image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: ${{ github.event_name == 'release' }}
platforms: linux/amd64
# Pinned to the commit so the publish job below consumes this
# exact base.
tags: |
${{ steps.base.outputs.image }}:latest
${{ steps.base.outputs.image }}:${{ github.sha }}
# Authenticate with GHCR
- name: Login to Github Container Registry
uses: docker/login-action@v1
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
with:
context: .
push: true
tags: ghcr.io/revoltchat/base:latest
cache-from: type=gha
cache-to: type=gha,mode=max
publish_amd64:
needs: [base]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
strategy:
matrix:
project: [delta, bonfire]
name: Build ${{ matrix.project }} image (amd64)
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Authenticate with Docker Hub and GHCR
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Resolve the correct project
- uses: kanga333/variable-mapper@master
id: export
with:
key: "${{ matrix.project }}"
map: |
{
"delta": {
"path": "crates/delta",
"tag": "revoltchat/server"
},
"bonfire": {
"path": "crates/bonfire",
"tag": "revoltchat/bonfire"
}
}
export_to: output
# Configure metadata
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ steps.export.outputs.tag }}, ghcr.io/${{ steps.export.outputs.tag }}
# Build crate image
- name: Publish
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: linux/amd64
file: ${{ steps.export.outputs.path }}/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Builds individual service images from the base. Only runs on release.
publish:
needs: [base]
runs-on: ubuntu-latest
if: github.event_name == 'release'
strategy:
matrix:
# Only pushd is built. To add more services later, add
# entries here (paths/images mirror the upstream revolt layout):
# - { project: delta, path: crates/delta, image: handmade-revolt-server }
# - { project: bonfire, path: crates/bonfire, image: handmade-revolt-bonfire }
# - { project: autumn, path: crates/services/autumn, image: handmade-revolt-autumn }
# - { project: january, path: crates/services/january, image: handmade-revolt-january }
# - { project: crond, path: crates/daemons/crond, image: handmade-revolt-crond }
include:
- { project: pushd, path: crates/daemons/pushd, image: handmade-revolt-pushd }
name: Build ${{ matrix.project }} image
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Resolve registry host and owner
id: registry
# Owner is lowercased: Docker repository names must be lowercase,
# but the org (e.g. HMC) may be uppercase.
run: |
echo "host=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_OUTPUT"
echo "owner=$(echo "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/${{ matrix.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
env:
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ steps.registry.outputs.host }}
username: ${{ secrets.PACKAGE_PUBLISH_USERNAME }}
password: ${{ secrets.PACKAGE_PUBLISH_TOKEN }}
- name: Build and publish
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.path }}/Dockerfile
push: true
platforms: linux/amd64
build-args: |
BASE_IMAGE=${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/handmade-revolt-base:${{ github.sha }}
tags: ${{ steps.meta.outputs.tags }}
annotations: ${{ steps.meta.outputs.annotations }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,10 +1,17 @@
name: Rust build and test
name: Rust build, test, and generate specification
on:
push:
branches: [ master ]
paths-ignore:
- ".github/**"
- "!.github/workflows/rust.yaml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request:
branches: [ master ]
branches: [main]
env:
CARGO_TERM_COLOR: always
@@ -15,19 +22,71 @@ 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 compose.yml up -d
- 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 == 'main'
env:
TEST_DB: REFERENCE
run: |
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
- name: Wait for API to go up
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: nev7n/wait_for_response@v1
with:
command: test
url: "http://localhost:14702/"
- name: Checkout API repository
if: github.event_name != 'pull_request' && github.ref_name == 'main'
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 == 'main'
run: curl http://localhost:14702/openapi.json -o api/OpenAPI.json
- name: Commit changes
if: github.event_name != 'pull_request' && github.ref_name == 'main'
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: 5) {
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=="🆕 Untriaged") |.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,6 +1,12 @@
Rocket.toml
Revolt.*.toml
compose.override.yml
target
.data
.env
.vercel
.DS_Store
.idea

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": "${workspaceFolder}/default.nix"
}

6223
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,24 @@
[workspace]
members = ["crates/*"]
resolver = "2"
members = [
"crates/delta",
"crates/bonfire",
"crates/core/*",
"crates/services/*",
"crates/bindings/*",
"crates/daemons/*",
]
[patch.crates-io]
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" }
# I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just
# disable it for now. In the future, we could use this
# if we were rolling our own CI.
# [profile.release]
# lto = true

View File

@@ -1,12 +1,38 @@
# Build Stage
FROM rustlang/rust:nightly-slim AS builder
FROM --platform="${BUILDPLATFORM}" rust:1.77.2-slim-bookworm
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/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/files/Cargo.toml ./crates/core/files/
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/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
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

34
Dockerfile.useCurrentArch Normal file
View File

@@ -0,0 +1,34 @@
# Build Stage
FROM rust:1.77.2-slim-bookworm
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/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/files/Cargo.toml ./crates/core/files/
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/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps
COPY crates ./crates
RUN sh /tmp/build-image-layer.sh apps

View File

@@ -1,4 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE
With the exception of crates that specify their own LICENSE file,
the following license applies to the source code of this project.
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>

229
README.md
View File

@@ -1,31 +1,220 @@
# Revolt Backend
<div align="center">
<h1>
Revolt Backend
[![Stars](https://img.shields.io/github/stars/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/stargazers)
[![Forks](https://img.shields.io/github/forks/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/network/members)
[![Pull Requests](https://img.shields.io/github/issues-pr/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/pulls)
[![Issues](https://img.shields.io/github/issues/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/issues)
[![Contributors](https://img.shields.io/github/contributors/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/graphs/contributors)
[![License](https://img.shields.io/github/license/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/blob/main/LICENSE)
</h1>
The services and libraries that power the Revolt service.<br/>
<br/>
This is a monorepo for the Revolt backend.
| Crate | Path | Description | |
| ------------------ | -------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration | ![Crates.io Version](https://img.shields.io/crates/v/revolt-config) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-config) ![Crates.io Version](https://img.shields.io/crates/size/revolt-config) ![Crates.io License](https://img.shields.io/crates/l/revolt-config) |
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation | ![Crates.io Version](https://img.shields.io/crates/v/revolt-database) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-database) ![Crates.io Version](https://img.shields.io/crates/size/revolt-database) ![Crates.io License](https://img.shields.io/crates/l/revolt-database) |
| `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines | ![Crates.io Version](https://img.shields.io/crates/v/revolt-files) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-files) ![Crates.io Version](https://img.shields.io/crates/size/revolt-files) ![Crates.io License](https://img.shields.io/crates/l/revolt-files) |
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models | ![Crates.io Version](https://img.shields.io/crates/v/revolt-models) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-models) ![Crates.io Version](https://img.shields.io/crates/size/revolt-models) ![Crates.io License](https://img.shields.io/crates/l/revolt-models) |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bindings/node` | [crates/bindings/node](crates/bindings/node) | Node.js bindings for the Revolt software | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| Crate | Path | Description |
| ---------------- | ---------------------------------------------- | ------------------------------------ |
| `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
| `quark` | [crates/quark](crates/quark) | Models and logic |
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer |
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer |
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |-->
</div>
<br/>
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
## Minimum Supported Rust Version
## Resources
Rust 1.76 or higher.
### Revolt
> [!CAUTION]
> The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341).
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing)
## Development Guide
## Contributing
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
Please note that a pull request should only take care of one issue so that we can review it quickly.
Before getting started, you'll want to install:
- Rust toolchain (rustup recommended)
- Docker
- Git
- mold (optional, faster compilation)
> A **default.nix** is available for Nix users!
> Just run `nix-shell` and continue.
As a heads-up, the development environment uses the following ports:
| Service | Port |
| ------------------------- | :------------: |
| MongoDB | 27017 |
| Redis | 6379 |
| MinIO | 14009 |
| Maildev | 14025<br>14080 |
| Revolt Web App | 14701 |
| RabbitMQ | 5672<br>15672 |
| `crates/delta` | 14702 |
| `crates/bonfire` | 14703 |
| `crates/services/autumn` | 14704 |
| `crates/services/january` | 14705 |
Now you can clone and build the project:
```bash
git clone https://github.com/revoltchat/backend revolt-backend
cd revolt-backend
cargo build
```
A default configuration `Revolt.toml` is present in this project that is suited for development.
If you'd like to change anything, create a `Revolt.overrides.toml` file and specify relevant variables.
> [!TIP]
> Use Sentry to catch unexpected service errors:
>
> ```toml
> # Revolt.overrides.toml
> [sentry]
> api = "https://abc@your.sentry/1"
> events = "https://abc@your.sentry/1"
> files = "https://abc@your.sentry/1"
> proxy = "https://abc@your.sentry/1"
> ```
> [!TIP]
> If you have port conflicts on common services, you can try the following:
>
> ```yaml
> # compose.override.yml
> services:
> redis:
> ports: !override
> - "14079:6379"
>
> database:
> ports: !override
> - "14017:27017"
>
> rabbit:
> ports: !override
> - "14072:5672"
> - "14672:15672"
> ```
>
> And corresponding Revolt configuration:
>
> ```toml
> # Revolt.overrides.toml
> [database]
> mongodb = "mongodb://127.0.0.1:14017"
> redis = "redis://127.0.0.1:14079/"
>
> [rabbit]
> port = 14072
> ```
Then continue:
```bash
# start other necessary services
docker compose up -d
# run everything together
./scripts/start.sh
# .. or individually
# run the API server
cargo run --bin revolt-delta
# run the events server
cargo run --bin revolt-bonfire
# run the file server
cargo run --bin revolt-autumn
# run the proxy server
cargo run --bin revolt-january
# run the push daemon (not usually needed in regular development)
cargo run --bin revolt-pushd
# hint:
# mold -run <cargo build, cargo run, etc...>
# mold -run ./scripts/start.sh
```
You can start a web client by doing the following:
```bash
# if you do not have yarn yet and have a modern Node.js:
corepack enable
# clone the web client and run it:
git clone --recursive https://github.com/revoltchat/revite
cd revite
yarn
yarn build:deps
echo "VITE_API_URL=http://local.revolt.chat:14702" > .env.local
yarn dev --port 14701
```
Then go to http://local.revolt.chat:14701 to create an account/login.
When signing up, go to http://localhost:14080 to find confirmation/password reset emails.
## Deployment Guide
### Cutting new crate releases
Begin by bumping crate versions:
```bash
just patch # 0.0.X
just minor # 0.X.0
just major # X.0.0
```
Then commit the changes to package files.
Proceed to publish all the new crates:
```bash
just publish
```
### Cutting new binary releases
Tag and push a new release by running:
```bash
just release
```
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/revoltchat/backend/releases/new) to create a changelog.
## Testing
First, start the required services:
```sh
docker compose -f docker-compose.db.yml up -d
```
Now run tests for whichever database:
```sh
TEST_DB=REFERENCE cargo nextest run
TEST_DB=MONGODB cargo nextest run
```
## License
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). Please check individual crates for further license information.
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
**Individual crates may supply their own licenses!**

52
Revolt.toml Normal file
View File

@@ -0,0 +1,52 @@
# ⚠️ This configuration is intended for development environment.
# If you'd like to override anything, create a Revolt.override.toml
[database]
# MongoDB connection URL
# Defaults to the container name specified in self-hosted
mongodb = "mongodb://127.0.0.1:27017"
# Redis connection URL
# Defaults to the container name specified in self-hosted
redis = "redis://127.0.0.1:6379/"
[rabbit]
host = "127.0.0.1"
[hosts]
# Web locations of various services
# Defaults assume all services are reverse-proxied
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
#
# Remember to change these to https/wss where appropriate in production!
app = "http://local.revolt.chat:14701"
api = "http://local.revolt.chat:14702"
events = "ws://local.revolt.chat:14703"
autumn = "http://local.revolt.chat:14704"
january = "http://local.revolt.chat:14705"
voso_legacy = ""
voso_legacy_ws = ""
[api]
[api.smtp]
# Email server configuration for verification
# Defaults to no email verification (host field is empty)
host = "localhost"
username = "smtp"
password = "smtp"
from_address = "development@revolt.chat"
reply_to = "support@revolt.chat"
port = 14025
use_tls = false
[files.s3]
# S3 protocol endpoint
endpoint = "http://127.0.0.1:14009"
# S3 region name
region = "minio"
# S3 protocol key ID
access_key_id = "minioautumn"
# S3 protocol access key
secret_access_key = "minioautumn"
# Bucket to upload to by default
default_bucket = "revolt-uploads"

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",
]

66
compose.yml Normal file
View File

@@ -0,0 +1,66 @@
services:
# Redis
redis:
image: eqalpha/keydb
ports:
- "6379:6379"
# MongoDB
database:
image: mongo
ports:
- "27017:27017"
volumes:
- ./.data/db:/data/db
# MinIO
minio:
image: minio/minio
command: server /data
environment:
MINIO_ROOT_USER: minioautumn
MINIO_ROOT_PASSWORD: minioautumn
volumes:
- ./.data/minio:/data
ports:
- "14009:9000"
- "14010:9001"
restart: always
# Create buckets for minio.
createbuckets:
image: minio/mc
depends_on:
- minio
entrypoint: >
/bin/sh -c "while ! /usr/bin/mc ready minio; do
/usr/bin/mc config host add minio http://minio:9000 minioautumn minioautumn;
echo 'Waiting minio...' && sleep 1;
done; /usr/bin/mc mb minio/revolt-uploads; exit 0;"
# Rabbit
rabbit:
image: rabbitmq:3-management
environment:
RABBITMQ_DEFAULT_USER: rabbituser
RABBITMQ_DEFAULT_PASS: rabbitpass
volumes:
- ./.data/rabbit:/var/lib/rabbitmq
#- ./rabbit_plugins:/opt/rabbitmq/plugins/
#- ./rabbit_enabled_plugins:/etc/rabbitmq/enabled_plugins
# uncomment this if you need to enable other plugins
ports:
- "5672:5672"
- "15672:15672" # management UI, for development
# Mock SMTP server
maildev:
image: soulteary/maildev
ports:
- "14025:25"
- "14080:8080"
environment:
MAILDEV_SMTP_PORT: 25
MAILDEV_WEB_PORT: 8080
MAILDEV_INCOMING_USER: smtp
MAILDEV_INCOMING_PASS: smtp

7
crates/bindings/node/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
target
index.node
**/node_modules
**/.DS_Store
npm-debug.log*
cargo.log
cross.log

View File

@@ -0,0 +1,25 @@
[package]
name = "revolt-nodejs-bindings"
version = "0.8.2"
description = "Node.js bindings for the Revolt software"
authors = ["Paul Makles <me@insrt.uk>"]
license = "MIT"
edition = "2021"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
neon = "1.0.0"
neon-serde4 = "1.0.0"
serde = { version = "1", features = ["derive"] }
async-std = "1.12.0"
revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-result = { version = "0.8.2", path = "../../core/result" }
revolt-database = { version = "0.8.2", path = "../../core/database" }

View File

@@ -0,0 +1,92 @@
# revolt.node
**revolt.node:** Node.js bindings for the Revolt software
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
## Building revolt.node
Building revolt.node requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
To run the build, run:
```sh
$ npm run build
```
This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`.
## Exploring revolt.node
After building revolt.node, you can explore its exports at the Node console:
```sh
$ npm i
$ npm run build
$ node
> require('.').hello()
'hello node'
```
## Available Scripts
In the project directory, you can run:
#### `npm install`
Installs the project, including running `npm run build`.
#### `npm run build`
Builds the Node addon (`index.node`) from source, generating a release build with `cargo --release`.
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm run build` and similar commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
```
npm run build -- --feature=beetle
```
#### `npm run debug`
Similar to `npm run build` but generates a debug build with `cargo`.
#### `npm run cross`
Similar to `npm run build` but uses [cross-rs](https://github.com/cross-rs/cross) to cross-compile for another platform. Use the [`CARGO_BUILD_TARGET`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget) environment variable to select the build target.
#### `npm test`
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
## Project Layout
The directory structure of this project is:
```
revolt.node/
├── Cargo.toml
├── README.md
├── src/
| └── lib.rs
├── index.node
├── package.json
└── target/
```
| Entry | Purpose |
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
| `README.md` | This file. |
| `src/` | The directory tree containing the Rust source code for the project. |
| `lib.rs` | Entry point for the Rust source code. |
| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. |
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
| `target/` | Binary artifacts generated by the Rust build. |
## Learn More
Learn more about:
- [Neon](https://neon-bindings.com).
- [Rust](https://www.rust-lang.org).
- [Node](https://nodejs.org).

84
crates/bindings/node/index.d.ts vendored Normal file
View File

@@ -0,0 +1,84 @@
import { Channel, User } from "revolt-api";
/**
* Opaque type for Revolt database
*/
export declare interface Database {}
/**
* Opaque type for Revolt database
*/
export declare interface OpaqueUser {}
/**
* Error type from Revolt backend
*/
export declare interface Err {
type: string;
location: string;
}
/**
* Initialises background tasks and logging, must be called before anything else!
* Can be called multiple times!
*/
export declare function init();
/**
* Gets a new handle to the Revolt database
* @returns {Database} Handle
*/
export declare function database(): Database;
/**
* Fetch user from database
* @param {string} userId User's ID
* @this {Database}
*/
export declare function database_fetch_user(userId: string): OpaqueUser;
/**
* Fetch user from database
* @param {string} username Username
* @param {string} discriminator Discriminator
* @this {Database}
*/
export declare function database_fetch_user_by_username(
username: string,
discriminator: string
): OpaqueUser;
/**
* Gets model data as JSON
* @this {OpaqueUser}
*/
export declare function model_data(): User;
/**
* Gets error if the model failed to fetch
* @this {OpaqueUser}
*/
export declare function model_error(): Err;
/**
* Open a direct message channel between two users
* @param {string} userA User A ID
* @param {string} userB User B ID
* @returns Existing or newly created channel
*/
export declare function proc_channels_create_dm(
userA: string,
userB: string
): Promise<Channel & { error: Err }>;
/**
* Suspend a user
* @param {string} user User
* @param {number} duration Duration (in days), set to 0 for indefinite
* @param {string} reason Pipe-separated list of reasons (e.g. reason1|reason2|reason3)
*/
export declare function proc_users_suspend(
user: OpaqueUser,
duration: number,
reason: string
): Promise<{ error: Err }>;

View File

@@ -0,0 +1,36 @@
{
"name": "revolt-nodejs-bindings",
"version": "0.7.15-rev0.0.3",
"description": "Node.js bindings for the Revolt software",
"main": "index.node",
"scripts": {
"test": "cargo test",
"cargo-build": "cargo build --message-format=json > cargo.log",
"cross-build": "cross build --message-format=json > cross.log",
"postcargo-build": "neon dist < cargo.log",
"postcross-build": "neon dist -m /target < cross.log",
"debug": "npm run cargo-build --",
"build": "npm run cargo-build -- --release",
"cross": "npm run cross-build -- --release"
},
"author": "Paul Makles",
"license": "AGPL-3.0",
"devDependencies": {
"@neon-rs/cli": "0.1.73"
},
"repository": {
"type": "git",
"url": "git+https://github.com/revoltchat/backend"
},
"keywords": [
"revolt",
"chat"
],
"bugs": {
"url": "https://github.com/revoltchat/backend/issues"
},
"homepage": "https://github.com/revoltchat/backend#readme",
"dependencies": {
"revolt-api": "^0.7.15"
}
}

117
crates/bindings/node/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,117 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
revolt-api:
specifier: ^0.7.15
version: 0.7.15
devDependencies:
'@neon-rs/cli':
specifier: 0.1.73
version: 0.1.73
packages:
/@cargo-messages/android-arm-eabi@0.1.72:
resolution: {integrity: sha512-gGZxIM1mj+Y5x+ULND6ZCNr7f70OJi9wDlycSK8hGONy9wrChN6JAIHryddC5cqcwlYAoQ6IDcDFElnhAYbybA==}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/darwin-arm64@0.1.72:
resolution: {integrity: sha512-EAzN5MLaXPljZKZDO5qR+aBs44eSq2ZbEnS7AI/FziE3MzeXbrGOS3fLba5+7yWPFXJyZolXzePm8N1EBv8ovg==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/darwin-x64@0.1.72:
resolution: {integrity: sha512-RLo6j8s3nYbjdd1LDct4wamfChyRit7zokUuxtIYCu9XOlltkN5vnj1vwnrPvoqCMZ/7CbbuHFwSTn9A71de/w==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/linux-arm-gnueabihf@0.1.72:
resolution: {integrity: sha512-tHsRshuzfjrX6SDW3jg6al8vMNLTMgczGnVYl5RuBZf/yrAUuwe30KxA9ge6w6mW6Ox797DyBchzAc9OLgTgmQ==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/linux-x64-gnu@0.1.72:
resolution: {integrity: sha512-VGtL6CCnUbhsP4aYuBNT5kfrAL7o0qjrxw97a+ax13t+nJd26tVEEIKHMu5drvvS/Nm/hn7sLT8zMnnCv0pvHg==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/win32-arm64-msvc@0.1.72:
resolution: {integrity: sha512-V93Cgz39K+yqa3MveNbhh29pYCp8izK5uEavjPoxlNxAbsMCWH+s0verGDdUcfGxjR1H2V7oZ4FszPqR2SqMRQ==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/win32-x64-msvc@0.1.72:
resolution: {integrity: sha512-knz3uSrO0OSbq3U5VWfCY8FB4NsM43BOWLZ7x4sfaMOC1XWv+IyvDdkLe6DhJx8KUw46KIAimYs9YROrp6l46Q==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@neon-rs/cli@0.1.73:
resolution: {integrity: sha512-1kv8S/feB6UQWQQwsnGfkSkEBOtlFDNExnioL81E2BwvUWgjQPaseHgpi2EpWVgsPUgur5eBm4QowmlpWkD4/w==}
hasBin: true
optionalDependencies:
'@cargo-messages/android-arm-eabi': 0.1.72
'@cargo-messages/darwin-arm64': 0.1.72
'@cargo-messages/darwin-x64': 0.1.72
'@cargo-messages/linux-arm-gnueabihf': 0.1.72
'@cargo-messages/linux-x64-gnu': 0.1.72
'@cargo-messages/win32-arm64-msvc': 0.1.72
'@cargo-messages/win32-x64-msvc': 0.1.72
dev: true
/axios@0.26.1:
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
dependencies:
follow-redirects: 1.15.6
transitivePeerDependencies:
- debug
dev: false
/follow-redirects@1.15.6:
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/lodash.defaultsdeep@4.6.1:
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
dev: false
/revolt-api@0.7.15:
resolution: {integrity: sha512-rWRnjBFvoXt/RHRxHg9KuP+x1Jk61KdsOpKOczVJg5tIqGDmqp/6QZHw5rJTtG+wCH3Ah3YKwY+MM5NHfg1W3Q==}
dependencies:
axios: 0.26.1
lodash.defaultsdeep: 4.6.1
transitivePeerDependencies:
- debug
dev: false

View File

@@ -0,0 +1,221 @@
#[macro_use]
extern crate serde;
use std::sync::OnceLock;
use neon::prelude::*;
use revolt_database::{Database, DatabaseInfo};
fn js_init(mut cx: FunctionContext) -> JsResult<JsUndefined> {
// static INIT: OnceLock<()> = OnceLock::new();
// if INIT.get().is_none() {
// INIT.get_or_init(|| {
// async_std::task::block_on(async {
// revolt_config::configure!(api);
// match DatabaseInfo::Auto.connect().await {
// Ok(db) => {
// let authifier_db = db.clone().to_authifier().await.database;
// revolt_database::tasks::start_workers(db, authifier_db);
// Ok(())
// }
// Err(err) => Err(err),
// }
// })
// .or_else(|err| cx.throw_error(err))
// .unwrap();
// });
// }
Ok(cx.undefined())
}
struct DatabaseBinding(Database, Channel);
impl Finalize for DatabaseBinding {}
impl DatabaseBinding {
fn take(&self) -> (Database, Channel) {
(self.0.clone(), self.1.clone())
}
}
fn js_database(mut cx: FunctionContext) -> JsResult<JsBox<DatabaseBinding>> {
let db = async_std::task::block_on(DatabaseInfo::Auto.connect())
.or_else(|err| cx.throw_error(err))?;
let channel = cx.channel();
Ok(cx.boxed(DatabaseBinding(db, channel)))
}
// Implementations for models
#[derive(Clone)]
#[allow(clippy::large_enum_variant)]
enum Model {
User(revolt_database::User),
Error(revolt_result::Error),
}
impl Model {
fn give(&self) -> Model {
self.clone()
}
}
impl Finalize for Model {}
macro_rules! shim_boxed {
($cx: ident, $name: ident, $model: ident, $( $variable: ident $type: ident $id: expr )+, $cmd: ident, $( $arg: expr, )+) => {
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
$(
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
)+
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
let (deferred, promise) = cx.promise();
async_std::task::spawn(async move {
let result = db.$cmd($($arg,)+).await;
deferred.settle_with(&channel, move |mut cx| {
Ok(cx.boxed(match result {
Ok(value) => Model::$model(value),
Err(error) => Model::Error(error)
}))
})
});
Ok(promise)
}
$cx.export_function(stringify!($name), $name)?;
};
}
fn js_data(mut cx: FunctionContext) -> JsResult<JsValue> {
match cx.this::<JsBox<Model>>()?.give() {
Model::Error(_) => neon_serde4::to_value(&mut cx, &None::<()>),
Model::User(user) => neon_serde4::to_value(&mut cx, &user),
}
.or_else(|e| cx.throw_error(e.to_string()))
}
fn js_error(mut cx: FunctionContext) -> JsResult<JsValue> {
let value = match cx.this::<JsBox<Model>>()?.give() {
Model::Error(err) => Some(err),
_ => None,
};
neon_serde4::to_value(&mut cx, &value).or_else(|e| cx.throw_error(e.to_string()))
}
// Basic data implementation
#[derive(Serialize, Deserialize)]
struct ResultBinding<T> {
#[serde(flatten)]
value: Option<T>,
error: Option<revolt_result::Error>,
}
macro_rules! shim {
($cx: ident, $name: ident, $( $variable: ident $type: ident $id: expr )*, $( $model: ident $modelType: ident $modelId: expr )*, | $db: ident | $closure: expr, $( $arg: expr, )+) => {
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
$(
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
)*
$(
let mut $model = if let Model::$modelType(value) = cx.argument::<JsBox<Model>>($modelId)?.give() {
value
} else {
unreachable!()
};
)*
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
let (deferred, promise) = cx.promise();
async_std::task::spawn(async move {
#[allow(clippy::redundant_closure_call)]
let result = (|$db: $crate::Database| $closure)(db.clone()).await;
deferred.settle_with(&channel, move |mut cx| {
neon_serde4::to_value(
&mut cx,
&match result {
Ok(value) => ResultBinding {
value: Some(value),
error: None,
},
Err(error) => ResultBinding {
value: None,
error: Some(error),
},
},
)
.or_else(|e| cx.throw_error(e.to_string()))
})
});
Ok(promise)
}
$cx.export_function(stringify!($name), $name)?;
};
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
// initialise required background stuff
cx.export_function("init", js_init)?;
// database & model stuff
cx.export_function("database", js_database)?;
cx.export_function("model_data", js_data)?;
cx.export_function("model_error", js_error)?;
shim_boxed!(
cx,
database_fetch_user,
User,
user_id JsString 0,
fetch_user,
&user_id,
);
shim_boxed!(
cx,
database_fetch_user_by_username,
User,
username JsString 0
discriminator JsString 1,
fetch_user_by_username,
&username, &discriminator,
);
// procedure calls
shim!(
cx,
proc_channels_create_dm,
user_a JsString 0
user_b JsString 1,
,
|db| async move {
let user_a = db.fetch_user(&user_a).await?;
let user_b = db.fetch_user(&user_b).await?;
revolt_database::Channel::create_dm(&db, &user_a, &user_b).await
},
&userA, &userB,
);
shim!(
cx,
proc_users_suspend,
duration JsNumber 1
reason JsString 2,
user User 0,
|db| async move {
let duration = duration as usize;
user.suspend(&db, if duration == 0 { None } else { Some(duration) }, Some(reason.split('|').map(|x| x.to_owned()).collect())).await
},
&user,
);
Ok(())
}

View File

@@ -0,0 +1,53 @@
const Internal = require(".");
// playing around with class wrapper, not practical
class Model {
constructor(model) {
this.model = model;
}
data() {
return Internal.model_data.bind(this.model)();
}
error() {
return Internal.model_error.bind(this.model)();
}
}
class User extends Model {
constructor(db, user) {
super(user);
this.db = db;
}
}
class Database {
constructor() {
this.db = Internal.database();
}
async fetchUser(userId) {
return new User(
this,
await Internal.database_fetch_user.bind(this.db)(userId)
);
}
async fetchUserByUsername(username, discriminator) {
return new User(
this,
await Internal.database_fetch_user_by_username.bind(this.db)(
username,
discriminator
)
);
}
}
const db = new Database();
db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data()));
db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data()));
db.fetchUserByUsername("dos", "1111").then((user) =>
console.info(user.error())
);

6
crates/bindings/package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "bindings",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

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 = "0.5.5"
version = "0.8.2"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -9,15 +9,19 @@ edition = "2021"
[dependencies]
# util
log = "*"
sentry = "0.31.5"
lru = "0.7.6"
ulid = "0.5.0"
once_cell = "1.9.0"
redis-kiss = "0.1.4"
lru_time_cache = "0.11.11"
async-channel = "2.3.1"
# parsing
querystring = "1.1.0"
# quark
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 +29,20 @@ 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
authifier = { version = "1.0.9" }
revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.2", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }

View File

@@ -1,10 +1,12 @@
# Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder
RUN cargo install --locked --path crates/bonfire
FROM debian:12 AS debian
# 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 ./
EXPOSE 9000
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
COPY --from=debian /usr/bin/uname /usr/bin/uname
EXPOSE 14703
USER nonroot
CMD ["./revolt-bonfire"]

View File

@@ -1,6 +1,7 @@
use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender;
use revolt_quark::{Error, Result};
use revolt_database::events::client::ReadyPayloadFields;
use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize};
/// Enumeration of supported protocol formats
@@ -37,16 +38,16 @@ impl ProtocolConfiguration {
match self.format {
ProtocolFormat::Json => {
if let Message::Text(text) = msg {
serde_json::from_str(text).map_err(|_| Error::InternalError)
serde_json::from_str(text).map_err(|_| create_error!(InternalError))
} else {
Err(Error::InternalError)
Err(create_error!(InternalError))
}
}
ProtocolFormat::Msgpack => {
if let Message::Binary(buf) = msg {
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError)
rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
} else {
Err(Error::InternalError)
Err(create_error!(InternalError))
}
}
}
@@ -83,6 +84,17 @@ impl ProtocolConfiguration {
pub fn get_protocol_format(&self) -> &ProtocolFormat {
&self.format
}
/// Get ready payload fields
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
vec![
ReadyPayloadFields::Users,
ReadyPayloadFields::Servers,
ReadyPayloadFields::Channels,
ReadyPayloadFields::Members,
ReadyPayloadFields::Emoji,
]
}
}
/// Object holding one side of a channel for receiving the parsed information

View File

@@ -1,5 +1,5 @@
use once_cell::sync::OnceCell;
use revolt_quark::{Database, DatabaseInfo};
use revolt_database::{Database, DatabaseInfo};
static DBCONN: OnceCell<Database> = OnceCell::new();
@@ -10,7 +10,9 @@ pub async fn connect() {
.await
.expect("Failed to connect to the database.");
DBCONN.set(database).expect("Setting `Database`");
if DBCONN.set(database).is_err() {
panic!("couldn't set database")
}
}
/// Get a reference to the current database.

View File

@@ -1,21 +1,16 @@
use std::collections::HashSet;
use crate::{
get_relationship,
models::{
server_member::FieldsMember,
user::{PartialUser, Presence, RelationshipStatus},
Channel, Member, User,
},
perms,
presence::presence_filter_online,
Database, Permission, Result,
use revolt_database::{
events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery,
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_presence::filter_online;
use revolt_result::Result;
use super::{
client::EventV1,
state::{Cache, State},
};
use super::state::{Cache, State};
/// Cache Manager
impl Cache {
@@ -23,27 +18,24 @@ impl Cache {
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
match &channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
let member = self
.members
.iter()
.map(|(_, x)| x)
.find(|x| &x.id.server == server);
let member = self.members.get(server);
let server = self.servers.get(server);
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
let mut query =
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
.channel(channel);
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
if let Some(member) = member {
perms.member.set_ref(member);
query = query.member(member);
}
if let Some(server) = server {
perms.server.set_ref(server);
query = query.server(server);
}
perms
.has_permission(db, Permission::ViewChannel)
calculate_channel_permissions(&mut query)
.await
.unwrap_or_default()
.has_channel_permission(ChannelPermission::ViewChannel)
}
_ => true,
}
@@ -68,7 +60,7 @@ impl Cache {
/// Check whether we can subscribe to another user
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
if let Some(user) = self.users.get(&self.user_id) {
match get_relationship(user, user_id) {
match user.relationship_with(user_id) {
RelationshipStatus::Friend
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing
@@ -99,11 +91,16 @@ impl Cache {
/// State Manager
impl State {
/// Generate a Ready packet for the current user
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
let mut user = self.clone_user();
pub async fn generate_ready_payload(
&mut self,
db: &Database,
fields: Vec<ReadyPayloadFields>,
) -> Result<EventV1> {
let user = self.clone_user();
self.cache.is_bot = user.bot.is_some();
// Find all relationships to the user.
let mut user_ids: Vec<String> = user
let mut user_ids: HashSet<String> = user
.relations
.as_ref()
.map(|arr| arr.iter().map(|x| x.id.to_string()).collect())
@@ -111,9 +108,15 @@ impl State {
// Fetch all memberships with their corresponding servers.
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
let servers = db.fetch_servers(&server_ids).await?;
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
// Collect channel ids from servers.
let mut channel_ids = vec![];
@@ -132,15 +135,14 @@ impl State {
for channel in &channels {
match channel {
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
user_ids.append(&mut recipients.clone());
user_ids.extend(&mut recipients.clone().into_iter());
}
_ => {}
}
}
// Fetch presence data for known users.
let online_ids = presence_filter_online(&user_ids).await;
user.online = Some(true);
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
// Fetch user data.
let users = db
@@ -153,68 +155,104 @@ impl State {
.await?;
// Fetch customisations.
let emojis = Some(
db.fetch_emoji_by_parent_ids(
&servers
.iter()
.map(|x| x.id.to_string())
.collect::<Vec<String>>(),
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
Some(
db.fetch_emoji_by_parent_ids(
&servers
.iter()
.map(|x| x.id.to_string())
.collect::<Vec<String>>(),
)
.await?,
)
.await?,
);
} else {
None
};
// Fetch user settings
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
.iter()
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
{
Some(db.fetch_user_settings(&user.id, &keys).await?)
} else {
None
};
// Fetch channel unreads
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
Some(db.fetch_unreads(&user.id).await?)
} else {
None
};
// Copy data into local state cache.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache
.users
.insert(self.cache.user_id.clone(), user.clone());
self.cache.servers = servers.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache.channels = channels
.iter()
.cloned()
.map(|x| (x.id().to_string(), x))
.collect();
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
// Make all users appear from our perspective.
let mut users: Vec<User> = users
let mut users: Vec<v0::User> = users
.into_iter()
.map(|mut x| {
x.online = Some(online_ids.contains(&x.id));
x.with_relationship(&user)
.map(|other_user| {
let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online)
})
.collect();
// Make sure we see our own user correctly.
user.relationship = Some(RelationshipStatus::User);
users.push(user.foreign());
users.push(user.into_self(true).await);
// Set subscription state internally.
self.reset_state();
self.insert_subscription(self.private_topic.clone());
self.reset_state().await;
self.insert_subscription(self.private_topic.clone()).await;
for user in &users {
self.insert_subscription(user.id.clone());
self.insert_subscription(user.id.clone()).await;
}
for server in &servers {
self.insert_subscription(server.id.clone());
self.insert_subscription(server.id.clone()).await;
if self.cache.is_bot {
self.insert_subscription(format!("{}u", server.id)).await;
}
}
for channel in &channels {
self.insert_subscription(channel.id().to_string());
self.insert_subscription(channel.id().to_string()).await;
}
Ok(EventV1::Ready {
users,
servers,
channels,
members,
emojis,
users: if fields.contains(&ReadyPayloadFields::Users) {
Some(users)
} else {
None
},
servers: if fields.contains(&ReadyPayloadFields::Servers) {
Some(servers.into_iter().map(Into::into).collect())
} else {
None
},
channels: if fields.contains(&ReadyPayloadFields::Channels) {
Some(channels.into_iter().map(Into::into).collect())
} else {
None
},
members: if fields.contains(&ReadyPayloadFields::Members) {
Some(members.into_iter().map(Into::into).collect())
} else {
None
},
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
user_settings,
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
})
}
@@ -248,11 +286,11 @@ impl State {
let mut bulk_events = vec![];
for id in added_channels {
self.insert_subscription(id);
self.insert_subscription(id).await;
}
for id in removed_channels {
self.remove_subscription(&id);
self.remove_subscription(&id).await;
self.cache.channels.remove(&id);
bulk_events.push(EventV1::ChannelDelete { id });
@@ -275,8 +313,8 @@ impl State {
.channels
.insert(channel.id().to_string(), channel.clone());
self.insert_subscription(channel.id().to_string());
bulk_events.push(EventV1::ChannelCreate(channel));
self.insert_subscription(channel.id().to_string()).await;
bulk_events.push(EventV1::ChannelCreate(channel.into()));
}
}
}
@@ -301,11 +339,12 @@ impl State {
} {
let event = EventV1::UserUpdate {
id: self.cache.user_id.clone(),
data: PartialUser {
data: v0::PartialUser {
online: Some(target),
..Default::default()
},
clear: vec![],
event_id: Some(ulid::Ulid::new().to_string()),
};
for server in self.cache.servers.keys() {
@@ -347,8 +386,8 @@ impl State {
match event {
EventV1::ChannelCreate(channel) => {
let id = channel.id().to_string();
self.insert_subscription(id.clone());
self.cache.channels.insert(id, channel.clone());
self.insert_subscription(id.clone()).await;
self.cache.channels.insert(id, channel.clone().into());
}
EventV1::ChannelUpdate {
id, data, clear, ..
@@ -356,15 +395,21 @@ impl State {
let could_view: bool = if let Some(channel) = self.cache.channels.get(id) {
self.cache.can_view_channel(db, channel).await
} else {
true
false
};
if let Some(channel) = self.cache.channels.get_mut(id) {
for field in clear {
channel.remove(field);
channel.remove_field(&field.clone().into());
}
channel.apply_options(data.clone());
channel.apply_options(data.clone().into());
}
if !self.cache.channels.contains_key(id) {
if let Ok(channel) = db.fetch_channel(id).await {
self.cache.channels.insert(id.clone(), channel);
}
}
if let Some(channel) = self.cache.channels.get(id) {
@@ -372,7 +417,7 @@ impl State {
if could_view != can_view {
if can_view {
queue_add = Some(id.clone());
*event = EventV1::ChannelCreate(channel.clone());
*event = EventV1::ChannelCreate(channel.clone().into());
} else {
queue_remove = Some(id.clone());
*event = EventV1::ChannelDelete { id: id.clone() };
@@ -381,17 +426,17 @@ impl State {
}
}
EventV1::ChannelDelete { id } => {
self.remove_subscription(id);
self.remove_subscription(id).await;
self.cache.channels.remove(id);
}
EventV1::ChannelGroupJoin { user, .. } => {
self.insert_subscription(user.clone());
self.insert_subscription(user.clone()).await;
}
EventV1::ChannelGroupLeave { id, user, .. } => {
if user == &self.cache.user_id {
self.remove_subscription(id);
self.remove_subscription(id).await;
} else if !self.cache.can_subscribe_to_user(user) {
self.remove_subscription(user);
self.remove_subscription(user).await;
}
}
@@ -399,14 +444,28 @@ impl State {
id,
server,
channels,
emojis: _,
} => {
self.insert_subscription(id.clone());
self.cache.servers.insert(id.to_string(), server.clone());
self.insert_subscription(id.clone()).await;
if self.cache.is_bot {
self.insert_subscription(format!("{}u", id)).await;
}
self.cache.servers.insert(id.clone(), server.clone().into());
let member = Member {
id: MemberCompositeKey {
server: server.id.clone(),
user: self.cache.user_id.clone(),
},
..Default::default()
};
self.cache.members.insert(id.clone(), member);
for channel in channels {
self.cache
.channels
.insert(channel.id().to_string(), channel.clone());
.insert(channel.id().to_string(), channel.clone().into());
}
queue_server = Some(id.clone());
@@ -416,10 +475,10 @@ impl State {
} => {
if let Some(server) = self.cache.servers.get_mut(id) {
for field in clear {
server.remove(field);
server.remove_field(&field.clone().into());
}
server.apply_options(data.clone());
server.apply_options(data.clone().into());
}
if data.default_permissions.is_some() {
@@ -429,39 +488,41 @@ impl State {
EventV1::ServerMemberJoin { .. } => {
// We will always receive ServerCreate when joining a new server.
}
EventV1::ServerMemberLeave { id, user } => {
EventV1::ServerMemberLeave { id, user, .. } => {
if user == &self.cache.user_id {
self.remove_subscription(id);
self.remove_subscription(id).await;
if let Some(server) = self.cache.servers.remove(id) {
for channel in &server.channels {
self.remove_subscription(channel);
self.remove_subscription(channel).await;
self.cache.channels.remove(channel);
}
}
self.cache.members.remove(id);
}
}
EventV1::ServerDelete { id } => {
self.remove_subscription(id);
self.remove_subscription(id).await;
if let Some(server) = self.cache.servers.remove(id) {
for channel in &server.channels {
self.remove_subscription(channel);
self.remove_subscription(channel).await;
self.cache.channels.remove(channel);
}
}
self.cache.members.remove(id);
}
EventV1::ServerMemberUpdate { id, data, clear } => {
if id.user == self.cache.user_id {
if let Some(member) = self.cache.members.get_mut(&id.server) {
for field in &clear.clone() {
member.remove(field);
member.remove_field(&field.clone().into());
}
member.apply_options(data.clone());
member.apply_options(data.clone().into());
}
if data.roles.is_some() || clear.contains(&FieldsMember::Roles) {
if data.roles.is_some() || clear.contains(&v0::FieldsMember::Roles) {
queue_server = Some(id.server.clone());
}
}
@@ -476,10 +537,10 @@ impl State {
if let Some(server) = self.cache.servers.get_mut(id) {
if let Some(role) = server.roles.get_mut(role_id) {
for field in &clear.clone() {
role.remove(field);
role.remove_field(&field.clone().into());
}
role.apply_options(data.clone());
role.apply_options(data.clone().into());
}
}
@@ -503,15 +564,41 @@ impl State {
}
}
EventV1::UserUpdate { event_id, .. } => {
if let Some(id) = event_id {
if self.cache.seen_events.contains(id) {
return false;
}
self.cache.seen_events.put(id.to_string(), ());
}
*event_id = None;
}
EventV1::UserRelationship { id, user, .. } => {
self.cache.users.insert(id.clone(), user.clone());
self.cache.users.insert(id.clone(), user.clone().into());
if self.cache.can_subscribe_to_user(id) {
self.insert_subscription(id.clone());
self.insert_subscription(id.clone()).await;
} else {
self.remove_subscription(id);
self.remove_subscription(id).await;
}
}
EventV1::Message(message) => {
// Since Message events are fanned out to many clients,
// we must reconstruct the relationship value at this end.
if let Some(user) = &mut message.user {
user.relationship = self
.cache
.users
.get(&self.cache.user_id)
.expect("missing self?")
.relationship_with(&message.author)
.into();
}
}
_ => {}
}
@@ -522,45 +609,13 @@ impl State {
// Sub / unsub accordingly.
if let Some(id) = queue_add {
self.insert_subscription(id);
self.insert_subscription(id).await;
}
if let Some(id) = queue_remove {
self.remove_subscription(&id);
self.remove_subscription(&id).await;
}
true
}
}
impl EventV1 {
/// Publish helper wrapper
pub async fn p(self, channel: String) {
#[cfg(not(debug_assertions))]
redis_kiss::p(channel, self).await;
#[cfg(debug_assertions)]
info!("Publishing event to {channel}: {self:?}");
#[cfg(debug_assertions)]
redis_kiss::publish(channel, self).await.unwrap();
}
/// Publish user event
pub async fn p_user(self, id: String, db: &Database) {
self.clone().p(id.clone()).await;
// ! FIXME: this should be captured by member list in the future
// ! and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members {
self.clone().p(member.id.server).await;
}
}
}
/// Publish private event
pub async fn private(self, id: String) {
self.p(format!("{}!", id)).await;
}
}

View File

@@ -0,0 +1,2 @@
pub mod r#impl;
pub mod state;

View File

@@ -0,0 +1,211 @@
use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::Duration,
};
use async_std::sync::{Mutex, RwLock};
use lru::LruCache;
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
use revolt_database::{Channel, Member, Server, User};
/// Enumeration representing some change in subscriptions
pub enum SubscriptionStateChange {
/// No change
None,
/// Clear all subscriptions
Reset,
/// Append or remove subscriptions
Change {
add: Vec<String>,
remove: Vec<String>,
},
}
/// Dumb per-state cache implementation
///
/// Ideally this would use a global cache that
/// allows for mutations and could use Rc<> to
/// track usage. If Rc<> == 1, then it only
/// remains in global cache, hence should be
/// dropped.
///
/// ------------------------------------------------
/// We can strip these objects to core information!!
/// ------------------------------------------------
#[derive(Debug)]
pub struct Cache {
pub user_id: String,
pub is_bot: bool,
pub users: HashMap<String, User>,
pub channels: HashMap<String, Channel>,
pub members: HashMap<String, Member>,
pub servers: HashMap<String, Server>,
pub seen_events: LruCache<String, ()>,
}
impl Default for Cache {
fn default() -> Self {
Cache {
user_id: Default::default(),
is_bot: false,
users: Default::default(),
channels: Default::default(),
members: Default::default(),
servers: Default::default(),
seen_events: LruCache::new(20),
}
}
}
/// Client state
pub struct State {
pub cache: Cache,
pub session_id: String,
pub private_topic: String,
pub state: SubscriptionStateChange,
pub subscribed: Arc<RwLock<HashSet<String>>>,
pub active_servers: Arc<Mutex<LruTimeCache<String, ()>>>,
}
impl State {
/// Create state from User
pub fn from(user: User, session_id: String) -> State {
let mut subscribed = HashSet::new();
let private_topic = format!("{}!", user.id);
subscribed.insert(private_topic.clone());
subscribed.insert(user.id.clone());
let mut cache: Cache = Cache {
user_id: user.id.clone(),
..Default::default()
};
cache.users.insert(user.id.clone(), user);
State {
cache,
subscribed: Arc::new(RwLock::new(subscribed)),
active_servers: Arc::new(Mutex::new(LruTimeCache::with_expiry_duration_and_capacity(
Duration::from_secs(900),
5,
))),
session_id,
private_topic,
state: SubscriptionStateChange::Reset,
}
}
/// Apply currently queued state
pub async fn apply_state(&mut self) -> SubscriptionStateChange {
// Check if we need to change subscriptions to member event topics
if !self.cache.is_bot {
enum Server {
Subscribe(String),
Unsubscribe(String),
}
let active_server_changes: Vec<Server> = {
let mut active_servers = self.active_servers.lock().await;
active_servers
.notify_iter()
.map(|e| match e {
TimedEntry::Valid(k, _) => Server::Subscribe(format!("{}u", k)),
TimedEntry::Expired(k, _) => Server::Unsubscribe(format!("{}u", k)),
})
.collect()
// It is bad practice to open more than one Mutex at once and could
// lead to a deadlock, so instead we choose to collect the changes.
};
for entry in active_server_changes {
match entry {
Server::Subscribe(k) => {
self.insert_subscription(k).await;
}
Server::Unsubscribe(k) => {
self.remove_subscription(&k).await;
}
}
}
}
// Flush changes to subscriptions
let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None);
let mut subscribed = self.subscribed.write().await;
if let SubscriptionStateChange::Change { add, remove } = &state {
for id in add {
subscribed.insert(id.clone());
}
for id in remove {
subscribed.remove(id);
}
}
state
}
/// Clone the active user
pub fn clone_user(&self) -> User {
self.cache.users.get(&self.cache.user_id).unwrap().clone()
}
/// Reset the current state
pub async fn reset_state(&mut self) {
self.state = SubscriptionStateChange::Reset;
self.subscribed.write().await.clear();
}
/// Add a new subscription
pub async fn insert_subscription(&mut self, subscription: String) {
let mut subscribed = self.subscribed.write().await;
if subscribed.contains(&subscription) {
return;
}
match &mut self.state {
SubscriptionStateChange::None => {
self.state = SubscriptionStateChange::Change {
add: vec![subscription.clone()],
remove: vec![],
};
}
SubscriptionStateChange::Change { add, .. } => {
add.push(subscription.clone());
}
SubscriptionStateChange::Reset => {}
}
subscribed.insert(subscription);
}
/// Remove existing subscription
pub async fn remove_subscription(&mut self, subscription: &str) {
let mut subscribed = self.subscribed.write().await;
if !subscribed.contains(&subscription.to_string()) {
return;
}
match &mut self.state {
SubscriptionStateChange::None => {
self.state = SubscriptionStateChange::Change {
add: vec![],
remove: vec![subscription.to_string()],
};
}
SubscriptionStateChange::Change { remove, .. } => {
remove.push(subscription.to_string());
}
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
}
subscribed.remove(subscription);
}
}

View File

@@ -1,12 +1,13 @@
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;
pub mod config;
pub mod events;
mod database;
mod websocket;
@@ -14,21 +15,28 @@ mod websocket;
#[async_std::main]
async fn main() {
// Configure requirements for Bonfire.
revolt_quark::configure!();
revolt_config::configure!(events);
database::connect().await;
// Clean up the current region information.
presence_clear_region(None).await;
let no_clear_region = env::var("NO_CLEAR_PRESENCE").unwrap_or_else(|_| "0".into()) == "1";
if !no_clear_region {
clear_region(None).await;
}
// Setup a TCP listener to accept WebSocket connections on.
// By default, we bind to port 9000 on all interfaces.
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:9000".into());
// By default, we bind to port 14703 on all interfaces.
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into());
info!("Listening on host {bind}");
let try_socket = TcpListener::bind(bind).await;
let listener = try_socket.expect("Failed to bind");
// 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,253 +1,504 @@
use std::net::SocketAddr;
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
use revolt_quark::{
events::{
client::EventV1,
server::ClientMessage,
state::{State, SubscriptionStateChange},
},
models::{user::UserHint, User},
presence::{presence_create_session, presence_delete_session},
redis_kiss, Database,
use async_tungstenite::WebSocketStream;
use authifier::AuthifierEvent;
use fred::{
error::{RedisError, RedisErrorKind},
interfaces::{ClientLike, EventInterface, PubsubInterface},
types::RedisConfig,
};
use futures::{
channel::oneshot,
join, pin_mut, select,
stream::{SplitSink, SplitStream},
FutureExt, SinkExt, StreamExt, TryStreamExt,
};
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_config::report_internal_error;
use revolt_database::{
events::{client::EventV1, server::ClientMessage},
Database, User, UserHint,
};
use revolt_presence::{create_session, delete_session};
use async_std::{net::TcpStream, sync::Mutex, task};
use async_std::{
net::TcpStream,
sync::{Mutex, RwLock},
task::spawn,
};
use revolt_result::create_error;
use sentry::Level;
use crate::config::WebsocketHandshakeCallback;
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
use crate::events::state::{State, SubscriptionStateChange};
/// Spawn a new WebSocket client worker given access to the database,
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
/// Start a new WebSocket client worker given access to the database,
/// the relevant TCP stream and the remote address of the client.
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;
};
// 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,
)
.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()
);
// Verify we've received a valid config, otherwise we should just drop the connection.
let Ok(mut config) = receiver.await else {
return;
};
// Split the socket for simultaneously read and write.
let (write, mut read) = ws.split();
let write = Mutex::new(write);
info!(
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
config.get_protocol_version(),
config.get_protocol_format()
);
// 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;
}
// 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(Some(message)) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
config.set_session_token(token);
break;
}
}
}
// Try to authenticate the user.
let Some(token) = config.get_session_token().as_ref() else {
write
.send(config.encode(&EventV1::Error {
data: create_error!(InvalidSession),
}))
.await
.ok();
return;
};
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
Ok(user) => user,
Err(err) => {
write
.send(config.encode(&EventV1::Error { data: err }))
.await
.ok();
return;
}
};
info!("User {addr:?} authenticated as @{}", user.username);
// Create local state.
let mut state = State::from(user, session_id);
let user_id = state.cache.user_id.clone();
// Notify socket we have authenticated.
if report_internal_error!(write.send(config.encode(&EventV1::Authenticated)).await).is_err() {
return;
}
// Download required data to local cache and send Ready payload.
let ready_payload = match report_internal_error!(
state
.generate_ready_payload(db, config.get_ready_payload_fields())
.await
) {
Ok(ready_payload) => ready_payload,
Err(_) => return,
};
if report_internal_error!(write.send(config.encode(&ready_payload)).await).is_err() {
return;
}
// 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;
}
{
// Setup channels and mutexes
let write = Mutex::new(write);
let subscribed = state.subscribed.clone();
let active_servers = state.active_servers.clone();
let (topic_signal_s, topic_signal_r) = async_channel::unbounded();
// TODO: this needs to be rewritten
// Create channels through which the tasks can signal to each other they need to clean up
let (kill_signal_1_s, kill_signal_1_r) = async_channel::bounded(1);
let (kill_signal_2_s, kill_signal_2_r) = async_channel::bounded(1);
// Create a PubSub connection to poll on.
let listener = listener_with_kill_signal(
db,
&mut state,
addr,
&config,
topic_signal_r,
kill_signal_1_r,
&write,
kill_signal_2_s,
);
// Read from WebSocket stream.
let worker = worker_with_kill_signal(
addr,
subscribed,
active_servers,
user_id.clone(),
&config,
topic_signal_s,
kill_signal_2_r,
read,
&write,
kill_signal_1_s,
);
join!(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;
}
}
#[allow(clippy::too_many_arguments)]
async fn listener_with_kill_signal(
db: &'static Database,
state: &mut State,
addr: SocketAddr,
config: &ProtocolConfiguration,
topic_signal_r: async_channel::Receiver<()>,
kill_signal_r: async_channel::Receiver<()>,
write: &Mutex<WsWriter>,
kill_signal_s: async_channel::Sender<()>,
) {
listener(
db,
state,
addr,
config,
topic_signal_r,
kill_signal_r,
write,
)
.await;
kill_signal_s.send(()).await.ok();
}
async fn listener(
db: &'static Database,
state: &mut State,
addr: SocketAddr,
config: &ProtocolConfiguration,
topic_signal_r: async_channel::Receiver<()>,
kill_signal_r: async_channel::Receiver<()>,
write: &Mutex<WsWriter>,
) {
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
let subscriber = match report_internal_error!(
fred::types::Builder::from_config(redis_config).build_subscriber_client()
) {
Ok(subscriber) => subscriber,
Err(_) => return,
};
if report_internal_error!(subscriber.init().await).is_err() {
return;
}
// Handle Redis connection dropping
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
subscriber.on_error(move |err| {
if let RedisErrorKind::Canceled = err.kind() {
let clean_up_s = clean_up_s.clone();
spawn(async move {
clean_up_s.lock().await.send(()).await.ok();
});
}
Ok(())
});
let mut message_rx = subscriber.message_rx();
'out: loop {
// Check for state changes for subscriptions.
match state.apply_state().await {
SubscriptionStateChange::Reset => {
if report_internal_error!(subscriber.unsubscribe_all().await).is_err() {
break 'out;
}
let subscribed = state.subscribed.read().await;
for id in subscribed.iter() {
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
break 'out;
}
}
// 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();
if report_internal_error!(subscriber.unsubscribe(id).await).is_err() {
break 'out;
}
}
// 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();
}
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
break 'out;
}
}
}
// * 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:?}");
});
let t1 = message_rx.recv().fuse();
let t2 = topic_signal_r.recv().fuse();
let t3 = kill_signal_r.recv().fuse();
let t4 = clean_up_r.recv().fuse();
pin_mut!(t1, t2, t3, t4);
select! {
_ = t4 => {
break 'out;
},
_ = t3 => {
break 'out;
},
_ = t2 => {},
message = t1 => {
// Handle incoming events.
let message = match report_internal_error!(message) {
Ok(message) => message,
Err(_) => break 'out
};
let event = match *REDIS_PAYLOAD_TYPE {
PayloadType::Json => message
.value
.as_str()
.and_then(|s| report_internal_error!(serde_json::from_str::<EventV1>(s.as_ref())).ok()),
PayloadType::Msgpack => message
.value
.as_bytes()
.and_then(|b| report_internal_error!(rmp_serde::from_slice::<EventV1>(b)).ok()),
PayloadType::Bincode => message
.value
.as_bytes()
.and_then(|b| report_internal_error!(bincode::deserialize::<EventV1>(b)).ok()),
};
let Some(mut event) = event else {
let err = format!(
"Failed to deserialise event for {}: `{:?}`",
message.channel,
message
.value
);
error!("{}", err);
sentry::capture_message(&err, Level::Error);
break 'out;
};
if let EventV1::Auth(auth) = &event {
if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
if &state.session_id == session_id {
event = EventV1::Logout;
}
} else if let AuthifierEvent::DeleteAllSessions {
exclude_session_id, ..
} = auth
{
if let Some(excluded) = exclude_session_id {
if &state.session_id != excluded {
event = EventV1::Logout;
}
} else {
event = EventV1::Logout;
}
}
} else {
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
if !should_send {
continue;
}
}
let result = write.lock().await.send(config.encode(&event)).await;
if let Err(e) = result {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
let err = format!("Error while sending an event to {addr:?}: {e:?}");
warn!("{}", err);
sentry::capture_message(&err, Level::Warning);
}
break 'out;
}
if let EventV1::Logout = event {
info!("User {addr:?} received log out event!");
break 'out;
}
}
}
}
report_internal_error!(subscriber.quit().await).ok();
}
#[allow(clippy::too_many_arguments)]
async fn worker_with_kill_signal(
addr: SocketAddr,
subscribed: Arc<RwLock<HashSet<String>>>,
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
user_id: String,
config: &ProtocolConfiguration,
topic_signal_s: async_channel::Sender<()>,
kill_signal_r: async_channel::Receiver<()>,
read: WsReader,
write: &Mutex<WsWriter>,
kill_signal_s: async_channel::Sender<()>,
) {
worker(
addr,
subscribed,
active_servers,
user_id,
config,
topic_signal_s,
kill_signal_r,
read,
write,
)
.await;
kill_signal_s.send(()).await.ok();
}
#[allow(clippy::too_many_arguments)]
async fn worker(
addr: SocketAddr,
subscribed: Arc<RwLock<HashSet<String>>>,
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
user_id: String,
config: &ProtocolConfiguration,
topic_signal_s: async_channel::Sender<()>,
kill_signal_r: async_channel::Receiver<()>,
mut read: WsReader,
write: &Mutex<WsWriter>,
) {
loop {
let t1 = read.try_next().fuse();
let t2 = kill_signal_r.recv().fuse();
pin_mut!(t1, t2);
select! {
_ = t2 => {
return;
},
result = t1 => {
let msg = match result {
Ok(Some(msg)) => msg,
Ok(None) => {
warn!("Received a None message!");
sentry::capture_message("Received a None message!", Level::Warning);
return;
}
Err(e) => {
use async_tungstenite::tungstenite::Error;
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
let err = format!("Error while reading an event from {addr:?}: {e:?}");
warn!("{}", err);
sentry::capture_message(&err, Level::Warning);
}
return;
}
};
let Ok(payload) = config.decode(&msg) else {
continue;
};
match payload {
ClientMessage::BeginTyping { channel } => {
if !subscribed.read().await.contains(&channel) {
continue;
}
EventV1::ChannelStartTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::EndTyping { channel } => {
if !subscribed.read().await.contains(&channel) {
continue;
}
EventV1::ChannelStopTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::Subscribe { server_id } => {
let mut servers = active_servers.lock().await;
let has_item = servers.contains_key(&server_id);
servers.insert(server_id, ());
if !has_item {
// Poke the listener to adjust subscriptions
topic_signal_s.send(()).await.ok();
}
}
ClientMessage::Ping { data, responded } => {
if responded.is_none() {
write
.lock()
.await
.send(config.encode(&EventV1::Pong { data }))
.await
.ok();
}
}
_ => {}
}
}
}
}
}

View File

@@ -0,0 +1,37 @@
[package]
name = "revolt-config"
version = "0.8.2"
edition = "2021"
license = "MIT"
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]
report-macros = ["revolt-result"]
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 }
# Logging
log = "0.4.14"
pretty_env_logger = "0.4.0"
# Sentry
sentry = "0.31.5"
# Core
revolt-result = { version = "0.8.2", path = "../result", optional = true }

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,9 @@
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"
[rabbit]
host = "127.0.0.1"
port = 5672
username = "rabbituser"
password = "rabbitpass"

View File

@@ -0,0 +1,261 @@
[database]
# MongoDB connection URL
# Defaults to the container name specified in self-hosted
mongodb = "mongodb://database"
# Redis connection URL
# Defaults to the container name specified in self-hosted
redis = "redis://redis/"
[hosts]
# Web locations of various services
# Defaults assume all services are reverse-proxied
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
#
# Remember to change these to https/wss where appropriate in production!
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 = ""
[rabbit]
host = "rabbit"
port = 5672
username = "rabbituser"
password = "rabbitpass"
[api]
[api.registration]
# Whether an invite should be required for registration
# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
invite_only = false
[api.smtp]
# Email server configuration for verification
# Defaults to no email verification (host field is empty)
host = ""
username = ""
password = ""
from_address = "noreply@example.com"
# reply_to = "noreply@example.com"
# port = 587
# use_tls = true
[api.security]
# Authifier Shield API key
authifier_shield_key = ""
# Legacy voice server management token
voso_legacy_token = ""
# Whether services are behind the Cloudflare network
trust_cloudflare = false
[api.security.captcha]
# hCaptcha configuration
hcaptcha_key = ""
hcaptcha_sitekey = ""
[api.workers]
# Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50
[pushd]
# this changes the names of the queues to not overlap
# prod/beta if they happen to be on the same exchange/instance.
# Usually they have to be, so that messages sent from one or the other get sent to everyone
production = true
# none of these should need changing
exchange = "revolt.notifications"
message_queue = "notifications.origin.message"
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices
[pushd.vapid]
queue = "notifications.outbound.vapid"
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
[pushd.fcm]
queue = "notifications.outbound.fcm"
key_type = ""
project_id = ""
private_key_id = ""
private_key = ""
client_email = ""
client_id = ""
auth_uri = ""
token_uri = ""
auth_provider_x509_cert_url = ""
client_x509_cert_url = ""
[pushd.apn]
sandbox = false
queue = "notifications.outbound.apn"
pkcs8 = ""
key_id = ""
team_id = ""
[files]
# Encryption key for stored files
# Generate your own key using `openssl rand -base64 32`
encryption_key = "qcuMA+ssxhMyKaNAKBGFfryfFtUH8NDlamQyDwGW6fU="
# Quality used for lossy WebP previews (set to 100 for lossless)
webp_quality = 80.0
# Mime types that cannot be uploaded or served
#
# Example for Windows executables and Android installation files:
# ["application/vnd.microsoft.portable-executable", "application/vnd.android.package-archive"]
blocked_mime_types = []
# ClamAV service
# hostname:port
clamd_host = ""
# Mime types that should be virus scanned
#
# Leave empty to scan all file types
scan_mime_types = [
"application/vnd.microsoft.portable-executable",
"application/vnd.android.package-archive",
"application/zip",
]
[files.limit]
# Minimum file size (in bytes)
min_file_size = 1
# Minimum image resolution
min_resolution = [1, 1]
# Maximum MP of images
max_mega_pixels = 40
# Maximum pixel side of an image
max_pixel_side = 10_000
[files.preview]
# Maximum image resolution
attachments = [1280, 1280]
avatars = [128, 128]
backgrounds = [1280, 720]
icons = [128, 128]
banners = [480, 480]
emojis = [128, 128]
[files.s3]
# Configuration for S3
# Defaults included for MinIO + self-hosted setup
#
# Backblaze B2:
# - endpoint is listed on the "Buckets" page
# - path_style_buckets is set to true
# - region is `eu-central-003` string from endpoint URL
# - access_key_id is keyID generated on the "Application Keys" page
# - secret_access_key is token generated on the "Application Keys" page
# - default_bucket matches the name of the bucket you've created
# S3 protocol endpoint
endpoint = "http://minio:9000"
# Whether to use path-style buckets
# Generally true, except for MinIO
path_style_buckets = false
# S3 region name
region = "minio"
# S3 protocol key ID
access_key_id = "minioautumn"
# S3 protocol access key
secret_access_key = "minioautumn"
default_bucket = "revolt-uploads"
[features]
# Bucket to upload to by default
# Feature gate options
webhooks_enabled = false
[features.limits]
[features.limits.global]
group_size = 100
message_embeds = 5
message_replies = 5
message_reactions = 20
server_emoji = 100
server_roles = 200
server_channels = 200
# How many hours since creation a user is considered new
new_user_hours = 72
# Maximum permissible body size in bytes for uploads
# (should be greater than any one file upload limit)
body_limit_size = 20_000_000
[features.limits.new_user]
# Limits imposed on new users
# Number of outgoing friend requests permitted at any time
outgoing_friend_requests = 5
# Maximum number of owned bots
bots = 2
# Message content length
message_length = 2000
# Number of attachments that can be included
message_attachments = 5
# Maximum number of servers the user can create/join
servers = 50
[features.limits.new_user.file_upload_size_limit]
# Maximum file size limits (in bytes)
attachments = 20_000_000
avatars = 4_000_000
backgrounds = 6_000_000
icons = 2_500_000
banners = 6_000_000
emojis = 500_000
[features.limits.default]
# Limits imposed on users by default
# Number of outgoing friend requests permitted at any time
outgoing_friend_requests = 10
# Maximum number of owned bots
bots = 5
# Message content length
message_length = 2000
# Number of attachments that can be included
message_attachments = 5
# Maximum number of servers the user can create/join
servers = 100
[features.limits.default.file_upload_size_limit]
# Maximum file size limits (in bytes)
attachments = 20_000_000
avatars = 4_000_000
backgrounds = 6_000_000
icons = 2_500_000
banners = 6_000_000
emojis = 500_000
[features.advanced]
# The max amount of messages the rabbitmq provider/db mention adder job will delay for before forcing handling of a channel.
# default: 5
process_message_delay_limit = 5
[sentry]
# Configuration for Sentry error reporting
api = ""
events = ""
files = ""
proxy = ""
crond = ""

View File

@@ -0,0 +1,423 @@
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;
pub use sentry::{capture_error, capture_message, Level};
#[cfg(feature = "report-macros")]
#[macro_export]
macro_rules! report_error {
( $expr: expr, $error: ident $( $tt:tt )? ) => {
$expr
.inspect_err(|err| {
$crate::capture_message(
&format!("{err:?} ({}:{}:{})", file!(), line!(), column!()),
$crate::Level::Error,
);
})
.map_err(|_| ::revolt_result::create_error!($error))
};
}
#[cfg(feature = "report-macros")]
#[macro_export]
macro_rules! capture_internal_error {
( $expr: expr ) => {
$crate::capture_message(
&format!("{:?} ({}:{}:{})", $expr, file!(), line!(), column!()),
$crate::Level::Error,
);
};
}
#[cfg(feature = "report-macros")]
#[macro_export]
macro_rules! report_internal_error {
( $expr: expr ) => {
$expr
.inspect_err(|err| {
$crate::capture_message(
&format!("{err:?} ({}:{}:{})", file!(), line!(), column!()),
$crate::Level::Error,
);
})
.map_err(|_| ::revolt_result::create_error!(InternalError))
};
}
/// Paths to search for configuration
static CONFIG_SEARCH_PATHS: [&str; 3] = [
// current working directory
"Revolt.toml",
// current working directory - overrides file
"Revolt.overrides.toml",
// root directory, for Docker containers
"/Revolt.toml",
];
/// Configuration builder
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,
));
}
for path in CONFIG_SEARCH_PATHS {
if std::path::Path::new(path).exists() {
builder = builder.add_source(File::new(path, FileFormat::Toml));
}
}
builder.build().unwrap()
})
});
#[derive(Deserialize, Debug, Clone)]
pub struct Database {
pub mongodb: String,
pub redis: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Rabbit {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Hosts {
pub app: String,
pub api: String,
pub events: String,
pub autumn: String,
pub january: String,
pub voso_legacy: String,
pub voso_legacy_ws: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiRegistration {
pub invite_only: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSmtp {
pub host: String,
pub username: String,
pub password: String,
pub from_address: String,
pub reply_to: Option<String>,
pub port: Option<i32>,
pub use_tls: Option<bool>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct PushVapid {
pub queue: String,
pub private_key: String,
pub public_key: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct PushFcm {
pub queue: String,
pub key_type: String,
pub project_id: String,
pub private_key_id: String,
pub private_key: String,
pub client_email: String,
pub client_id: String,
pub auth_uri: String,
pub token_uri: String,
pub auth_provider_x509_cert_url: String,
pub client_x509_cert_url: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct PushApn {
pub queue: String,
pub sandbox: bool,
pub pkcs8: String,
pub key_id: String,
pub team_id: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurityCaptcha {
pub hcaptcha_key: String,
pub hcaptcha_sitekey: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurity {
pub authifier_shield_key: String,
pub voso_legacy_token: String,
pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiWorkers {
pub max_concurrent_connections: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Api {
pub registration: ApiRegistration,
pub smtp: ApiSmtp,
pub security: ApiSecurity,
pub workers: ApiWorkers,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Pushd {
pub production: bool,
pub exchange: String,
pub message_queue: String,
pub fr_accepted_queue: String,
pub fr_received_queue: String,
pub generic_queue: String,
pub ack_queue: String,
pub vapid: PushVapid,
pub fcm: PushFcm,
pub apn: PushApn,
}
impl Pushd {
fn get_routing_key(&self, key: String) -> String {
match self.production {
true => key + "-prd",
false => key + "-tst",
}
}
pub fn get_ack_routing_key(&self) -> String {
self.get_routing_key(self.ack_queue.clone())
}
pub fn get_message_routing_key(&self) -> String {
self.get_routing_key(self.message_queue.clone())
}
pub fn get_fr_accepted_routing_key(&self) -> String {
self.get_routing_key(self.fr_accepted_queue.clone())
}
pub fn get_fr_received_routing_key(&self) -> String {
self.get_routing_key(self.fr_received_queue.clone())
}
pub fn get_generic_routing_key(&self) -> String {
self.get_routing_key(self.generic_queue.clone())
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct FilesLimit {
pub min_file_size: usize,
pub min_resolution: [usize; 2],
pub max_mega_pixels: usize,
pub max_pixel_side: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FilesS3 {
pub endpoint: String,
pub path_style_buckets: bool,
pub region: String,
pub access_key_id: String,
pub secret_access_key: String,
pub default_bucket: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Files {
pub encryption_key: String,
pub webp_quality: f32,
pub blocked_mime_types: Vec<String>,
pub clamd_host: String,
pub scan_mime_types: Vec<String>,
pub limit: FilesLimit,
pub preview: HashMap<String, [usize; 2]>,
pub s3: FilesS3,
}
#[derive(Deserialize, Debug, Clone)]
pub struct GlobalLimits {
pub group_size: usize,
pub message_embeds: usize,
pub message_replies: usize,
pub message_reactions: usize,
pub server_emoji: usize,
pub server_roles: usize,
pub server_channels: usize,
pub new_user_hours: usize,
pub body_limit_size: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimits {
pub outgoing_friend_requests: usize,
pub bots: usize,
pub message_length: usize,
pub message_attachments: usize,
pub servers: usize,
pub file_upload_size_limit: HashMap<String, usize>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesLimitsCollection {
pub global: GlobalLimits,
pub new_user: FeaturesLimits,
pub default: FeaturesLimits,
#[serde(flatten)]
pub roles: HashMap<String, FeaturesLimits>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FeaturesAdvanced {
#[serde(default)]
pub process_message_delay_limit: u16,
}
impl Default for FeaturesAdvanced {
fn default() -> Self {
Self {
process_message_delay_limit: 5,
}
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct Features {
pub limits: FeaturesLimitsCollection,
pub webhooks_enabled: bool,
#[serde(default)]
pub advanced: FeaturesAdvanced,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Sentry {
pub api: String,
pub events: String,
pub files: String,
pub proxy: String,
pub crond: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Settings {
pub database: Database,
pub rabbit: Rabbit,
pub hosts: Hosts,
pub api: Api,
pub pushd: Pushd,
pub files: Files,
pub features: Features,
pub sentry: Sentry,
}
impl Settings {
pub fn preflight_checks(&self) {
if self.api.smtp.host.is_empty() {
log::warn!("No SMTP settings specified! Remember to configure email.");
}
if self.api.security.captcha.hcaptcha_key.is_empty() {
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
}
}
}
pub async fn init() {
println!(
":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m",
config().await
);
}
pub async fn read() -> Config {
CONFIG_BUILDER.read().await.clone()
}
#[cached(time = 30)]
pub async fn config() -> Settings {
read().await.try_deserialize::<Settings>().unwrap()
}
/// Configure logging and common Rust variables
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
}
if std::env::var("ROCKET_ADDRESS").is_err() {
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
}
if std::env::var("REDIS_URL").is_err() {
// Configure redis-kiss library
let config = config().await;
std::env::set_var("REDIS_URI", config.database.redis);
}
pretty_env_logger::init();
log::info!("Starting {release}");
if dsn.is_empty() {
None
} else {
Some(sentry::init((
dsn,
sentry::ClientOptions {
release: Some(release.into()),
..Default::default()
},
)))
}
}
#[macro_export]
macro_rules! configure {
($application: ident) => {
let config = $crate::config().await;
let _sentry = $crate::setup_logging(
concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")),
config.sentry.$application,
)
.await;
};
}
#[cfg(feature = "test")]
#[cfg(test)]
mod tests {
use crate::init;
#[async_std::test]
async fn it_works() {
init().await;
}
}

View File

@@ -0,0 +1,102 @@
[package]
name = "revolt-database"
version = "0.8.2"
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"]
axum-impl = ["axum"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
# Default Features
default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.8.2", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.2", path = "../result" }
revolt-models = { version = "0.8.2", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.8.2", path = "../presence" }
revolt-permissions = { version = "0.8.2", 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" }
validator = { version = "0.16", features = ["derive"] }
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 }
# Axum Impl
axum = { version = "0.7.5", optional = true }
# Rocket Impl
schemars = { version = "0.8.8", optional = true }
rocket = { version = "0.5.1", default-features = false, features = [
"json",
], optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
# Notifications
fcm_v1 = "0.3.0"
web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.9", features = ["rocket_impl"] }
# RabbitMQ
amqprs = { version = "1.7.0" }

View File

@@ -0,0 +1,28 @@
[
{
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Member",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "Member",
"discriminator": "0002"
},
{
"_object_type": "Channel",
"_id": "__ID:3__",
"channel_type": "Group",
"name": "My Group",
"owner": "__ID:0__",
"recipients": ["__ID:0__", "__ID:1__"]
}
]

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,211 @@
use std::collections::HashSet;
use crate::events::rabbit::*;
use crate::User;
use amqprs::channel::BasicPublishArguments;
use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError};
use amqprs::{BasicProperties, FieldTable};
use revolt_models::v0::PushNotification;
use revolt_presence::filter_online;
use serde_json::to_string;
#[derive(Clone)]
pub struct AMQP {
#[allow(unused)]
connection: Connection,
channel: Channel,
}
impl AMQP {
pub fn new(connection: Connection, channel: Channel) -> AMQP {
AMQP {
connection,
channel,
}
}
pub async fn friend_request_accepted(
&self,
accepted_request_user: &User,
sent_request_user: &User,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = FRAcceptedPayload {
accepted_user: accepted_request_user.to_owned(),
user: sent_request_user.id.clone(),
};
let payload = to_string(&payload).unwrap();
debug!(
"Sending friend request accept payload on channel {}: {}",
config.pushd.get_fr_accepted_routing_key(),
payload
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
.finish(),
payload.into(),
BasicPublishArguments::new(
&config.pushd.exchange,
&config.pushd.get_fr_accepted_routing_key(),
),
)
.await
}
pub async fn friend_request_received(
&self,
received_request_user: &User,
sent_request_user: &User,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = FRReceivedPayload {
from_user: sent_request_user.to_owned(),
user: received_request_user.id.clone(),
};
let payload = to_string(&payload).unwrap();
debug!(
"Sending friend request received payload on channel {}: {}",
config.pushd.get_fr_received_routing_key(),
payload
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
.finish(),
payload.into(),
BasicPublishArguments::new(
&config.pushd.exchange,
&config.pushd.get_fr_received_routing_key(),
),
)
.await
}
pub async fn generic_message(
&self,
user: &User,
title: String,
body: String,
icon: Option<String>,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = GenericPayload {
title,
body,
icon,
user: user.to_owned(),
};
let payload = to_string(&payload).unwrap();
debug!(
"Sending generic payload on channel {}: {}",
config.pushd.get_generic_routing_key(),
payload
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
.finish(),
payload.into(),
BasicPublishArguments::new(
&config.pushd.exchange,
&config.pushd.get_generic_routing_key(),
),
)
.await
}
pub async fn message_sent(
&self,
recipients: Vec<String>,
payload: PushNotification,
) -> Result<(), AMQPError> {
if recipients.is_empty() {
return Ok(());
}
let config = revolt_config::config().await;
let online_ids = filter_online(&recipients).await;
let recipients = (&recipients.into_iter().collect::<HashSet<String>>() - &online_ids)
.into_iter()
.collect::<Vec<String>>();
let payload = MessageSentPayload {
notification: payload,
users: recipients,
};
let payload = to_string(&payload).unwrap();
debug!(
"Sending message payload on channel {}: {}",
config.pushd.get_message_routing_key(),
payload
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
.finish(),
payload.into(),
BasicPublishArguments::new(
&config.pushd.exchange,
&config.pushd.get_message_routing_key(),
),
)
.await
}
pub async fn ack_message(
&self,
user_id: String,
channel_id: String,
message_id: String,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = AckPayload {
user_id: user_id.clone(),
channel_id: channel_id.clone(),
message_id,
};
let payload = to_string(&payload).unwrap();
info!(
"Sending ack payload on channel {}: {}",
config.pushd.ack_queue, payload
);
let mut headers = FieldTable::new();
headers.insert(
"x-deduplication-header".try_into().unwrap(),
format!("{}-{}", &user_id, &channel_id).into(),
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
//.with_headers(headers)
.finish(),
payload.into(),
BasicPublishArguments::new(&config.pushd.exchange, &config.pushd.ack_queue),
)
.await
}
}

View File

@@ -0,0 +1,2 @@
#[allow(clippy::module_inception)]
pub mod amqp;

View File

@@ -0,0 +1,181 @@
mod mongodb;
mod reference;
use authifier::config::Captcha;
use authifier::config::EmailVerificationConfig;
use authifier::config::ResolveIp;
use authifier::config::SMTPSettings;
use authifier::config::Shield;
use authifier::config::Template;
use authifier::config::Templates;
use authifier::Authifier;
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))
}
})
}
}
impl Database {
/// Create an Authifier reference
pub async fn to_authifier(self) -> Authifier {
let config = config().await;
let mut auth_config = authifier::Config {
email_verification: if !config.api.smtp.host.is_empty() {
EmailVerificationConfig::Enabled {
smtp: SMTPSettings {
from: config.api.smtp.from_address,
host: config.api.smtp.host,
username: config.api.smtp.username,
password: config.api.smtp.password,
reply_to: Some(
config
.api
.smtp
.reply_to
.unwrap_or("support@revolt.chat".into()),
),
port: config.api.smtp.port,
use_tls: config.api.smtp.use_tls,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("../../templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("../../templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../../templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("../../templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("../../templates/deletion.html").into()),
},
welcome: None,
},
}
} else {
EmailVerificationConfig::Disabled
},
..Default::default()
};
auth_config.invite_only = config.api.registration.invite_only;
if !config.api.security.captcha.hcaptcha_key.is_empty() {
auth_config.captcha = Captcha::HCaptcha {
secret: config.api.security.captcha.hcaptcha_key,
};
}
if !config.api.security.authifier_shield_key.is_empty() {
auth_config.shield = Shield::Enabled {
api_key: config.api.security.authifier_shield_key,
strict: false,
};
}
if config.api.security.trust_cloudflare {
auth_config.resolve_ip = ResolveIp::Cloudflare;
}
Authifier {
database: match self {
Database::Reference(_) => Default::default(),
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: auth_config,
event_channel: Some(crate::tasks::authifier_relay::sender()),
}
}
}

View File

@@ -1,48 +1,19 @@
use std::collections::HashMap;
use std::ops::Deref;
use bson::{to_document, Document};
use futures::StreamExt;
use mongodb::{
options::{FindOneOptions, FindOptions},
results::{DeleteResult, InsertOneResult, UpdateResult},
};
use rocket::serde::DeserializeOwned;
use serde::{Deserialize, Serialize};
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;
use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result};
pub mod admin {
pub mod migrations;
}
pub mod media {
pub mod attachment;
pub mod emoji;
}
pub mod channels {
pub mod channel;
pub mod channel_invite;
pub mod channel_unread;
pub mod message;
}
pub mod servers {
pub mod server;
pub mod server_ban;
pub mod server_member;
}
pub mod users {
pub mod bot;
pub mod user;
pub mod user_settings;
}
#[derive(Debug, Clone)]
pub struct MongoDb(pub mongodb::Client);
impl AbstractDatabase for MongoDb {}
database_derived!(
#[cfg(feature = "mongodb")]
/// MongoDB implementation
pub struct MongoDb(pub ::mongodb::Client, pub String);
);
impl Deref for MongoDb {
type Target = mongodb::Client;
@@ -52,30 +23,40 @@ impl Deref for MongoDb {
}
}
#[allow(dead_code)]
impl MongoDb {
/// Get the Revolt database
pub fn db(&self) -> mongodb::Database {
self.database("revolt")
self.database(&self.1)
}
/// Get a collection by its name
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
self.db().collection(collection)
}
async fn insert_one<T: Serialize>(
/// 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
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: collection,
})
self.col::<T>(collection).insert_one(document, None).await
}
async fn find_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
/// 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,
@@ -87,17 +68,21 @@ impl MongoDb {
Ok(self
.col::<T>(collection)
.find(projection, options)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: collection,
})?
.filter_map(|s| async { s.ok() })
.await?
.filter_map(|s| async {
if cfg!(debug_assertions) {
// Hard fail on invalid documents
Some(s.unwrap())
} else {
s.ok()
}
})
.collect::<Vec<T>>()
.await)
}
async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
/// Find multiple documents in a collection
pub async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
@@ -105,39 +90,37 @@ impl MongoDb {
self.find_with_options(collection, projection, None).await
}
async fn find_one_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
/// 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<T>
) -> Result<Option<T>>
where
O: Into<Option<FindOneOptions>>,
{
self.col::<T>(collection)
.find_one(projection, options)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: collection,
})?
.ok_or(Error::NotFound)
}
async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
/// Find one document
pub async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
&self,
collection: &'static str,
projection: Document,
) -> Result<T> {
) -> Result<Option<T>> {
self.find_one_with_options(collection, projection, None)
.await
}
async fn find_one_by_id<T: DeserializeOwned + Unpin + Send + Sync>(
/// 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<T> {
) -> Result<Option<T>> {
self.find_one(
collection,
doc! {
@@ -147,7 +130,8 @@ impl MongoDb {
.await
}
async fn update_one<P, T: Serialize>(
/// 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,
@@ -177,22 +161,16 @@ impl MongoDb {
to_document(&prefix_keys(&partial, prefix))
} else {
to_document(&partial)
}.map_err(|_| Error::DatabaseError {
operation: "to_document",
with: collection
})?
}?
};
self.col::<Document>(collection)
.update_one(projection, query, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: collection,
})
}
async fn update_one_by_id<P, T: Serialize>(
/// 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,
@@ -215,7 +193,8 @@ impl MongoDb {
.await
}
async fn delete_one(
/// Delete one document by the given projection
pub async fn delete_one(
&self,
collection: &'static str,
projection: Document,
@@ -223,13 +202,14 @@ impl MongoDb {
self.col::<Document>(collection)
.delete_one(projection, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: collection,
})
}
async fn delete_one_by_id(&self, collection: &'static str, id: &str) -> Result<DeleteResult> {
/// 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! {
@@ -240,6 +220,7 @@ impl MongoDb {
}
}
/// Just a string ID struct
#[derive(Deserialize)]
pub struct DocumentId {
#[serde(rename = "_id")]
@@ -247,5 +228,16 @@ pub struct DocumentId {
}
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,33 @@
use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, 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 file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
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, Report>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
}
);

View File

@@ -0,0 +1,287 @@
use authifier::AuthifierEvent;
use revolt_result::Error;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage,
FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message,
PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser,
PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, Webhook,
};
use crate::Database;
/// Ping Packet
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Ping {
Binary(Vec<u8>),
Number(usize),
}
/// Fields provided in Ready payload
#[derive(PartialEq)]
pub enum ReadyPayloadFields {
Users,
Servers,
Channels,
Members,
Emoji,
UserSettings(Vec<String>),
ChannelUnreads,
}
/// Protocol Events
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum EventV1 {
/// Multiple events
Bulk { v: Vec<EventV1> },
/// Error event
Error { data: Error },
/// Successfully authenticated
Authenticated,
/// Logged out
Logout,
/// Basic data to cache
Ready {
#[serde(skip_serializing_if = "Option::is_none")]
users: Option<Vec<User>>,
#[serde(skip_serializing_if = "Option::is_none")]
servers: Option<Vec<Server>>,
#[serde(skip_serializing_if = "Option::is_none")]
channels: Option<Vec<Channel>>,
#[serde(skip_serializing_if = "Option::is_none")]
members: Option<Vec<Member>>,
#[serde(skip_serializing_if = "Option::is_none")]
emojis: Option<Vec<Emoji>>,
#[serde(skip_serializing_if = "Option::is_none")]
user_settings: Option<UserSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
channel_unreads: Option<Vec<ChannelUnread>>,
},
/// Ping response
Pong { data: Ping },
/// New message
Message(Message),
/// Update existing message
MessageUpdate {
id: String,
channel: String,
data: PartialMessage,
#[serde(default)]
clear: Vec<FieldsMessage>,
},
/// 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,
#[serde(default)]
clear: Vec<FieldsServer>,
},
/// Delete server
ServerDelete { id: String },
/// Update existing server member
ServerMemberUpdate {
id: MemberCompositeKey,
data: PartialMember,
#[serde(default)]
clear: Vec<FieldsMember>,
},
/// User joins server
ServerMemberJoin { id: String, user: String },
/// User left server
ServerMemberLeave {
id: String,
user: String,
reason: RemovalIntention,
},
/// Server role created or updated
ServerRoleUpdate {
id: String,
role_id: String,
data: PartialRole,
#[serde(default)]
clear: Vec<FieldsRole>,
},
/// Server role deleted
ServerRoleDelete { id: String, role_id: String },
/// Update existing user
UserUpdate {
id: String,
data: PartialUser,
#[serde(default)]
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,
#[serde(default)]
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().server(member.id.server).await;
}
}
}
/// Publish private event
pub async fn private(self, id: String) {
self.p(format!("{id}!")).await;
}
/// Publish server member event
pub async fn server(self, id: String) {
self.p(format!("{id}u")).await;
}
/// Publish internal global event
pub async fn global(self) {
self.p("global".to_string()).await;
}
}

View File

@@ -1,4 +1,3 @@
pub mod client;
pub mod r#impl;
pub mod rabbit;
pub mod server;
pub mod state;

View File

@@ -0,0 +1,59 @@
use std::collections::HashMap;
use revolt_models::v0::PushNotification;
use serde::{Deserialize, Serialize};
use crate::User;
#[derive(Serialize, Deserialize)]
pub struct MessageSentPayload {
pub notification: PushNotification,
pub users: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct FRAcceptedPayload {
pub accepted_user: User,
pub user: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct FRReceivedPayload {
pub from_user: User,
pub user: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct GenericPayload {
pub title: String,
pub body: String,
pub icon: Option<String>,
pub user: User,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
#[allow(clippy::large_enum_variant)]
pub enum PayloadKind {
MessageNotification(PushNotification),
FRAccepted(FRAcceptedPayload),
FRReceived(FRReceivedPayload),
BadgeUpdate(usize),
Generic(GenericPayload),
}
#[derive(Serialize, Deserialize)]
pub struct PayloadToService {
pub notification: PayloadKind,
pub user_id: String,
pub session_id: String,
pub token: String,
pub extras: HashMap<String, String>,
}
#[derive(Serialize, Deserialize)]
pub struct AckPayload {
pub user_id: String,
pub channel_id: String,
pub message_id: String,
}

View File

@@ -8,5 +8,6 @@ pub enum ClientMessage {
Authenticate { token: String },
BeginTyping { channel: String },
EndTyping { channel: String },
Subscribe { server_id: String },
Ping { data: Ping, responded: Option<()> },
}

View File

@@ -0,0 +1,119 @@
#[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;
pub use iso8601_timestamp;
#[cfg(feature = "mongodb")]
pub use mongodb;
#[cfg(feature = "mongodb")]
#[macro_use]
extern crate bson;
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! query {
( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => {
Ok($self.$type($collection, $($rest),+).await.unwrap())
};
}
#[macro_export]
#[cfg(not(debug_assertions))]
macro_rules! query {
( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => {
$self.$type($collection, $($rest),+).await
.map_err(|err| {
revolt_config::capture_internal_error!(err);
create_database_error!(stringify!($type), $collection)
})
};
}
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;
mod amqp;
pub use amqp::amqp::AMQP;
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}
/// Utility function to check if an option doesnt contain true
pub fn if_option_false(t: &Option<bool>) -> bool {
t != &Some(true)
}

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.");
@@ -53,14 +56,34 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create attachments collection.");
db.create_collection("attachment_hashes", None)
.await
.expect("Failed to create attachment_hashes collection.");
db.create_collection("user_settings", None)
.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 +103,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,19 +138,26 @@ 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"
},
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
},
]
},
None,
@@ -171,6 +213,46 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create server_members index.");
db.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
},
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
},
None,
)
.await
.expect("Failed to create attachments index.");
db.run_command(
doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
},
None,
)
.await
.expect("Failed to create attachment_hashes index.");
db.collection("migrations")
.insert_one(
doc! {
@@ -182,5 +264,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,19 @@
use std::time::Duration;
use std::{collections::HashSet, ops::BitXor, time::Duration};
use bson::{Bson, DateTime};
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,
},
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
};
use bson::oid::ObjectId;
use futures::StreamExt;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use revolt_result::{Error, ErrorType};
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 +21,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 18;
pub const LATEST_REVISION: i32 = 31;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -215,7 +220,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 +508,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 +568,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 +603,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();
@@ -661,8 +661,572 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to update server members.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
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.");
}
if revision <= 26 {
info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging.");
auto_derived!(
pub enum OldInvite {
Server {
#[serde(rename = "_id")]
code: String,
server: String,
creator: String,
channel: String,
},
Group {
#[serde(rename = "_id")]
code: String,
creator: String,
channel: String,
},
}
);
#[derive(serde::Serialize, serde::Deserialize)]
struct Outer {
_id: ObjectId,
#[serde(flatten)]
invite: OldInvite,
}
let invites = db
.db()
.collection::<Outer>("channel_invites")
.find(
doc! {
"type": { "$exists": false }
},
None,
)
.await
.expect("failed to find invites")
.filter_map(|s| async { s.ok() })
.collect::<Vec<Outer>>()
.await
.into_iter()
.map(|invite| match invite.invite {
OldInvite::Server {
code,
server,
creator,
channel,
} => Invite::Server {
code,
server,
creator,
channel,
},
OldInvite::Group {
code,
creator,
channel,
} => Invite::Group {
code,
creator,
channel,
},
})
.collect::<Vec<Invite>>();
if !invites.is_empty() {
db.db()
.collection("channel_invites")
.insert_many(invites, None)
.await
.expect("failed to insert corrected invite");
db.db()
.collection::<Outer>("channel_invites")
.delete_many(
doc! {
"type": { "$exists": false }
},
None,
)
.await
.expect("failed to find invites");
}
}
if revision <= 27 {
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
}
if revision <= 28 {
info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn.");
db.db()
.create_collection("attachment_hashes", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
}
]
},
None,
)
.await
.expect("Failed to create attachments index.");
db.db()
.run_command(
doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
},
None,
)
.await
.expect("Failed to create attachment_hashes index.");
}
// Revision 29 omitted due to bug.
if revision <= 30 {
info!("Running migration [revision 30 / 29-09-2024]: Add index for used_for.id to attachments.");
db.db()
.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
},
None,
)
.await
.expect("Failed to create attachments index.");
}
if revision <= 31 {
info!("Running migration [revision 31 / 31-10-2024]: Add creator_id to webhooks and delete those whose channels don't exist.");
#[derive(serde::Serialize, serde::Deserialize)]
struct WebhookShell {
_id: String,
channel_id: String,
}
let webhooks = db
.db()
.collection::<WebhookShell>("channel_webhooks")
.find(doc! {}, None)
.await
.expect("webhooks")
.filter_map(|s| async { s.ok() })
.collect::<Vec<WebhookShell>>()
.await;
for webhook in webhooks {
match db.fetch_channel(&webhook.channel_id).await {
Ok(channel) => {
let creator_id = match channel {
Channel::Group { owner, .. } => owner,
Channel::TextChannel { server, .. }
| Channel::VoiceChannel { server, .. } => {
let server = db.fetch_server(&server).await.expect("server");
server.owner
}
_ => unreachable!("not server or group channel!"),
};
db.db()
.collection::<Document>("channel_webhooks")
.update_one(
doc! {
"_id": webhook._id,
},
doc! {
"$set" : {
"creator_id": creator_id
}
},
None,
)
.await
.expect("update webhook");
}
Err(Error {
error_type: ErrorType::NotFound,
..
}) => {
db.db()
.collection::<WebhookShell>("channel_webhooks")
.delete_one(doc! { "_id": webhook._id }, None)
.await
.expect("failed to delete invalid webhook");
}
Err(err) => panic!("{err:?}"),
}
}
}
// 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,223 @@
use revolt_config::config;
use revolt_result::Result;
use ulid::Ulid;
use crate::{events::client::EventV1, 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, User)>
where
D: Into<Option<PartialBot>>,
{
if owner.bot.is_some() {
return Err(create_error!(IsBot));
}
if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
return Err(create_error!(ReachedMaximumBots));
}
let id = Ulid::new().to_string();
let user = User::create(
db,
username,
Some(id.to_string()),
Some(PartialUser {
bot: Some(BotInformation {
owner: owner.id.to_string(),
}),
..Default::default()
}),
)
.await?;
let mut bot = Bot {
id,
owner: owner.id.to_string(),
token: nanoid::nanoid!(64),
..Default::default()
};
if let Some(data) = data.into() {
bot.apply_options(data);
}
db.insert_bot(&bot).await?;
Ok((bot, user))
}
/// 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?;
if partial.token.is_some() {
EventV1::Logout.private(self.id.clone()).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

@@ -1,18 +1,46 @@
use nanoid::nanoid;
use revolt_result::{create_error, Result};
use crate::{
models::{Channel, Invite, User},
Database, Error, Result,
};
use crate::{Channel, Database, User};
lazy_static! {
static ref 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'
];
}
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
#[serde(tag = "type")]
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 {
@@ -29,9 +57,13 @@ impl Invite {
}
/// Create a new invite from given information
pub async fn create(db: &Database, creator: &User, target: &Channel) -> Result<Invite> {
let code = nanoid!(8, &*ALPHABET);
let invite = match &target {
pub async fn create_channel_invite(
db: &Database,
creator: &User,
channel: &Channel,
) -> Result<Invite> {
let code = nanoid::nanoid!(8, &ALPHABET);
let invite = match &channel {
Channel::Group { id, .. } => Ok(Invite::Group {
code,
creator: creator.id.clone(),
@@ -45,7 +77,7 @@ impl Invite {
channel: id.clone(),
})
}
_ => Err(Error::InvalidOperation),
_ => Err(create_error!(InvalidOperation)),
}?;
db.insert_invite(&invite).await?;
@@ -69,6 +101,6 @@ impl Invite {
}
}
Err(Error::NotFound)
Err(create_error!(NotFound))
}
}

View File

@@ -1,17 +1,21 @@
use crate::models::Invite;
use crate::Result;
#[async_trait]
pub trait AbstractChannelInvite: Sync + Send {
/// Fetch an invite by its id
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
/// Insert a new invite into the database
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
/// Delete an invite by its id
async fn delete_invite(&self, code: &str) -> Result<()>;
/// Fetch all invites for a server
async fn fetch_invites_for_server(&self, server: &str) -> Result<Vec<Invite>>;
}
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,37 @@
use revolt_result::Result;
use crate::ChannelUnread;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannelUnreads: Sync + Send {
/// Acknowledge a message, and returns updated channel unread.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<Option<ChannelUnread>>;
/// 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 unreads with mentions for a user.
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
/// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
/// Fetch unread for a specific user in a channel.
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>>;
}

View File

@@ -0,0 +1,150 @@
use bson::Document;
use mongodb::options::FindOneAndUpdateOptions;
use mongodb::options::ReturnDocument;
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, and returns updated channel unread.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<Option<ChannelUnread>> {
self.col::<ChannelUnread>(COL)
.find_one_and_update(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$pull": {
"mentions": {
"$lte": message_id
}
},
"$set": {
"last_id": message_id
}
},
FindOneAndUpdateOptions::builder()
.upsert(true)
.return_document(ReturnDocument::After)
.build(),
)
.await
.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
}
)
}
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query! {
self,
find,
COL,
doc! {
"_id.user": user_id,
"mentions": {"$ne": null}
}
}
}
/// Fetch unread for a specific user in a channel.
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
query!(
self,
find_one,
COL,
doc! {
"_id.user": user_id,
"_id.channel": channel_id
}
)
}
}

View File

@@ -0,0 +1,111 @@
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<Option<ChannelUnread>> {
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.clone(),
last_id: Some(message_id.to_string()),
mentions: None,
},
);
}
Ok(unreads.get(&key).cloned())
}
/// 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(())
}
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads
.values()
.filter(|unread| unread.id.user == user_id && unread.mentions.is_some())
.cloned()
.collect())
}
/// 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())
}
/// Fetch unread for a specific user in a channel.
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads
.get(&ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
})
.cloned())
}
}

View File

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

View File

@@ -0,0 +1,179 @@
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>,
/// User that created this webhook
pub creator_id: String,
/// 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,
creator_id: Default::default(),
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!(NotAuthenticated))
}
}
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,831 @@
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 ulid::Ulid;
use crate::{
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
Server, SystemMessage, User, AMQP,
};
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.global.server_channels {
return Err(create_error!(TooManyChannels {
max: config.features.limits.global.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.global.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.global.group_size,
}));
}
let recipients = data.users.into_iter().collect::<Vec<String>>();
let channel = Channel::Group {
id: ulid::Ulid::new().to_string(),
name: data.name,
owner: owner_id,
description: data.description,
recipients: recipients.clone(),
icon: None,
last_message_id: None,
permissions: None,
nsfw: data.nsfw.unwrap_or(false),
};
db.insert_channel(&channel).await?;
let event = EventV1::ChannelCreate(channel.clone().into());
for recipient in recipients {
event.clone().private(recipient).await;
}
Ok(channel)
}
/// Create a DM (or return the existing one / saved messages)
pub async fn create_dm(db: &Database, user_a: &User, user_b: &User) -> Result<Channel> {
// Try to find existing channel
if let Ok(channel) = db.find_direct_message_channel(&user_a.id, &user_b.id).await {
Ok(channel)
} else {
let channel = if user_a.id == user_b.id {
// Create a new saved messages channel
Channel::SavedMessages {
id: Ulid::new().to_string(),
user: user_a.id.to_string(),
}
} else {
// Create a new DM channel
Channel::DirectMessage {
id: Ulid::new().to_string(),
active: true, // show by default
recipients: vec![user_a.id.clone(), user_b.id.clone()],
last_message_id: None,
}
};
db.insert_channel(&channel).await?;
match &channel {
Channel::DirectMessage { .. } => {
let event = EventV1::ChannelCreate(channel.clone().into());
event.clone().private(user_a.id.clone()).await;
event.private(user_b.id.clone()).await;
}
_ => {}
};
Ok(channel)
}
}
/// Add user to a group
pub async fn add_user_to_group(
&mut self,
db: &Database,
amqp: &AMQP,
user: &User,
by_id: &str,
) -> Result<()> {
if let Channel::Group { recipients, .. } = self {
if recipients.contains(&String::from(&user.id)) {
return Err(create_error!(AlreadyInGroup));
}
let config = config().await;
if recipients.len() >= config.features.limits.global.group_size {
return Err(create_error!(GroupTooLarge {
max: config.features.limits.global.group_size
}));
}
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,
Some(amqp),
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
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) -> &str {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
}
}
/// 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);
}
}
}
}
/// Acknowledge a message
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
EventV1::ChannelAck {
id: self.id().to_string(),
user: user.to_string(),
message_id: message.to_string(),
}
.private(user.to_string())
.await;
crate::tasks::ack::queue_ack(
self.id().to_string(),
user.to_string(),
AckEvent::AckMessage {
id: message.to_string(),
},
)
.await;
Ok(())
}
/// Remove user from a group
pub async fn remove_user_from_group(
&self,
db: &Database,
amqp: &AMQP,
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,
Some(amqp),
MessageAuthor::System {
username: name,
avatar: None,
},
None,
None,
self,
false,
)
.await
.ok();
} else {
return self.delete(db).await;
}
}
db.remove_user_from_group(id, &user.id).await?;
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,
Some(amqp),
MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
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;
// TODO: missing functionality:
// - group invites
// - channels list / categories list on server
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_group_channel() {
database_test!(|db| async move {
fixture!(db, "group_with_members",
owner user 0
member1 user 1
member2 user 2
channel channel 3);
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &member1).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &member2).channel(&channel);
assert!(!calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
});
}
#[async_std::test]
async fn permissions_text_channel() {
database_test!(|db| async move {
fixture!(db, "server_with_roles",
owner user 0
moderator user 1
user user 2
channel channel 3);
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &moderator).channel(&channel);
assert!(calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
let mut query = DatabasePermissionQuery::new(&db, &user).channel(&channel);
assert!(!calculate_channel_permissions(&mut query)
.await
.has_channel_permission(ChannelPermission::SendMessage));
});
}
}

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

@@ -1,87 +1,192 @@
use super::AbstractChannels;
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::{Bson, Document};
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
use crate::r#impl::mongo::IntoDocumentPath;
use crate::{AbstractChannel, AbstractServer, Error, OverrideField, Result};
use super::super::MongoDb;
use futures::StreamExt;
use revolt_permissions::OverrideField;
use revolt_result::Result;
static COL: &str = "channels";
impl MongoDb {
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
// Delete all invites to these channels.
self.col::<Document>("channel_invites")
.delete_many(
doc! {
"channel": &id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_invites",
})?;
// Delete unread message objects on channels.
self.col::<Document>("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_unreads",
})
.map(|_| ())
// update many attachments with parent id
}
}
#[async_trait]
impl AbstractChannel for MongoDb {
async fn fetch_channel(&self, id: &str) -> Result<Channel> {
self.find_one_by_id(COL, id).await
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>> {
self.find(
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! {
"_id": {
"$in": ids
}
},
"$or": [
{
"$or": [
{
"channel_type": "DirectMessage"
},
{
"channel_type": "Group"
}
],
"recipients": user_id
},
{
"channel_type": "SavedMessages",
"user": user_id
}
]
}
)
.await
}
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
self.insert_one(COL, channel).await.map(|_| ())
// 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<()> {
self.update_one_by_id(
query!(
self,
update_one_by_id,
COL,
id,
channel,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None,
None
)
.await
.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<()> {
let id = channel.id().to_string();
let server_id = match channel {
@@ -151,152 +256,57 @@ impl AbstractChannel for MongoDb {
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "servers",
})?;
.map_err(|_| create_database_error!("update_one", "servers"))?;
}
// Delete associated attachments
self.delete_many_attachments(doc! {
"object_id": &id
"used_for.id": &id
})
.await?;
// Delete the channel itself
self.delete_one_by_id(COL, &id).await.map(|_| ())
}
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
self.find(
COL,
doc! {
"$or": [
{
"$or": [
{
"channel_type": "DirectMessage"
},
{
"channel_type": "Group"
}
],
"recipients": user_id
},
{
"channel_type": "SavedMessages",
"user": user_id
}
]
},
)
.await
}
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
self.find_one(
COL,
doc! {
"channel_type": "SavedMessages",
"user": user_id
},
)
.await
}
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
self.find_one(
COL,
if user_a == user_b {
doc! {
"channel_type": "SavedMessages",
"user": user_a
}
} else {
doc! {
"channel_type": "DirectMessage",
"recipients": {
"$all": [ user_a, user_b ]
}
}
},
)
.await
}
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$push": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})
}
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$pull": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})
}
async fn set_channel_role_permission(
&self,
channel: &str,
role: &str,
permissions: OverrideField,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! { "_id": channel },
doc! {
"$set": {
"role_permissions.".to_owned() + role: permissions
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
}
}
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsChannel::DefaultPermissions => "default_permissions",
FieldsChannel::Description => "description",
FieldsChannel::Icon => "icon",
})
impl MongoDb {
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
// Delete all invites to these channels.
self.col::<Document>("channel_invites")
.delete_many(
doc! {
"channel": &id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
// Delete unread message objects on channels.
self.col::<Document>("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
.map(|_| ())?;
// update many attachments with parent id
// Delete all webhooks on this channel.
self.col::<Document>("webhooks")
.delete_many(
doc! {
"channel": &id
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "webhooks"))
.map(|_| ())
}
}

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().to_string()) {
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

@@ -1,8 +1,15 @@
use crate::models::Emoji;
use crate::Result;
use revolt_result::Result;
use crate::Emoji;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractEmoji: Sync + Send {
pub trait AbstractEmojis: Sync + Send {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji>;
@@ -12,9 +19,6 @@ pub trait AbstractEmoji: Sync + Send {
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
}

View File

@@ -1,49 +1,52 @@
use bson::Document;
use revolt_result::Result;
use crate::models::Emoji;
use crate::{AbstractEmoji, Error, Result};
use crate::Emoji;
use crate::MongoDb;
use super::super::MongoDb;
use super::AbstractEmojis;
static COL: &str = "emojis";
#[async_trait]
impl AbstractEmoji for MongoDb {
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
self.find_one_by_id(COL, id).await
impl AbstractEmojis for MongoDb {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
query!(self, insert_one, COL, &emoji).map(|_| ())
}
/// Fetch emoji by their ids
/// Fetch an emoji by its id
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch emoji by their parent id
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
self.find(
query!(
self,
find,
COL,
doc! {
"parent.id": parent_id
},
}
)
.await
}
/// Fetch emoji by their parent ids
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
self.find(
query!(
self,
find,
COL,
doc! {
"parent.id": {
"$in": parent_ids
}
},
}
)
.await
}
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
self.insert_one(COL, emoji).await.map(|_| ())
}
/// Delete an emoji by its id
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
self.col::<Document>(COL)
.update_one(
@@ -61,9 +64,6 @@ impl AbstractEmoji for MongoDb {
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "emojis",
})
.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))
}
}
}

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