Compare commits

...

310 Commits

Author SHA1 Message Date
Paul
1303dbc844 chore: bump version to 0.5.3-alpha.9 2021-11-20 13:20:21 +00:00
Paul
7981748b7b fix: include associated user ids (system messages)
fixes #3
2021-11-20 13:18:13 +00:00
Paul
53277cb235 feat(push): include URL for each notification 2021-11-20 12:49:57 +00:00
Paul
92c7f023b1 fix(push): use correct query to fetch sessions 2021-11-20 12:49:28 +00:00
Paul
839d7d0d9a feat(db): add indexes to various collections 2021-11-20 12:46:42 +00:00
Paul
ea8784215d feat: add new badges [skip ci] 2021-11-13 19:43:08 +00:00
insertish
084c635e9c ci: Synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml' 2021-11-11 20:08:22 +00:00
insertish
924398df1d ci: Created local '.github/workflows/triage_pr.yml' from remote 'workflows/triage_pr.yml' 2021-11-11 19:08:47 +00:00
insertish
c204288b76 ci: Synced local '.github/workflows/triage_issue.yml' with remote 'workflows/triage_issue.yml' 2021-11-11 19:01:42 +00:00
Paul Makles
dcee48c69f Merge pull request #118 from revoltchat/bot/update-global-workflow-651726897879937c474b292841037fc969ff31b9 2021-11-11 18:34:39 +00:00
web-flow
c6efe3b190 ci: update global workflows 2021-11-11 18:33:27 +00:00
Paul Makles
13ed849657 feat: get rid of arm64 because problems
todo: bring it back later
2021-11-09 23:59:24 +00:00
Paul
08c1aa6f5d feat(messaging): add masquerade to messages 2021-11-04 20:50:30 +00:00
Paul
84ba038350 chore: bump version to 0.5.3-alpha.7 2021-11-02 13:41:52 +00:00
Paul
8eb6f5a8af chore(task_queue): add logging 2021-11-02 13:41:08 +00:00
Paul
012a30ce61 feat(task_queue): ack task
closes #111 and closes #86
2021-11-02 13:30:36 +00:00
Paul
0025bea23d feat(task_queue): web push task
closes #113
2021-11-02 12:50:35 +00:00
Paul
997d1fffc0 feat(task_queue): process embeds task
closes #112
2021-11-01 22:04:11 +00:00
Paul
6366e0ac24 feat(task_queue): last_message_id task
closes #110
2021-11-01 20:54:48 +00:00
Paul
8d25dd1d65 feat: implement idempotency tokens
closes #108 and closes #106
2021-11-01 18:32:45 +00:00
Paul
49c7bc0ffe fix: prevent server owner being kicked
Fixes #96.
2021-11-01 13:51:38 +00:00
Paul Makles
fe5ba39e21 chore(docker): split up amd64 / arm64 builds
attempts to help with #103
2021-11-01 13:21:34 +00:00
Paul
2a4e290489 chore: bump version to 0.5.3-alpha.6 2021-10-31 18:28:31 +00:00
Paul Makles
193846e289 chore(ci): update Docker workflow
Adjusts current workflow to build images when any tag is created.
2021-10-31 18:25:28 +00:00
Paul Makles
7421ffc8a2 Merge pull request #90 from Mar0xy/patch-1 2021-09-23 18:00:24 +01:00
Paul
2ef6b28029 Switch to Redis from MongoDB for event broker.
Closes #1.
2021-09-19 22:13:25 +01:00
Marie
39a759396f Merge branch 'revoltchat:master' into patch-1 2021-09-18 21:15:49 +02:00
Paul
e5e0031cad Fix rate limiter for bots.
Use local-request state to cache User object.
2021-09-18 20:06:00 +01:00
Marie
211d74bb47 Merge branch 'revoltchat:master' into patch-1 2021-09-18 20:42:22 +02:00
Paul
0dcf8cb281 Bump rauth release and publish changes. 2021-09-18 19:06:59 +01:00
Marie
24a20a8719 Add NSFW single editing 2021-09-17 23:15:43 +02:00
Paul
37d45889ec Add reported flag and bump version. 2021-09-17 13:45:05 +01:00
Paul Makles
8e9991a2de Merge pull request #84 from rmeritz/change-username 2021-09-17 13:26:40 +01:00
Paul Makles
a6d49d388f Update src/routes/users/change_username.rs
Co-authored-by: Splatterxl <splatterxl@outlook.ie>
2021-09-17 13:26:32 +01:00
Paul Makles
6eee1bc669 Merge pull request #79 from Zomatree/ratelimits 2021-09-17 13:26:03 +01:00
Paul
f8e9924f18 Drop support for armv7 Docker builds. 2021-09-16 10:30:59 +01:00
Paul
6e1f3a79cf Update Docker image to use buster-slim + glibc. 2021-09-15 22:43:23 +01:00
Paul Makles
b1f5ba32ff Merge pull request #73 from Zomatree/msgpack_fix
fix msgpack encoding
2021-09-15 22:42:16 +01:00
Zomatree
6e70a39fce fix rauth 2021-09-15 21:37:42 +01:00
Paul Makles
53aa2818b4 Merge pull request #88 from meguminloli/re_username 2021-09-15 21:30:58 +01:00
Zomatree
00a7bcb958 Merge branch 'msgpack_fix' of github.com:Zomatree/delta into msgpack_fix 2021-09-15 21:25:41 +01:00
Zomatree
edcb7c50ce remove clone 2021-09-15 21:25:37 +01:00
Paul Makles
2e40927457 Merge branch 'master' into msgpack_fix 2021-09-15 21:12:27 +01:00
Zomatree
e636879065 Merge remote-tracking branch 'upstream/master' into ratelimits 2021-09-15 21:06:08 +01:00
Zomatree
46c4de8c68 cleanup 2021-09-15 20:55:02 +01:00
Megumin
6e46362d13 add a newline at the end of regex.rs 2021-09-15 22:22:55 +03:00
meguminloli
a2bfaa5791 moved all RE_USERNAME occurrences to regex.rs
switch to normal Cargo.toml

switch to normal Cargo.toml

switch to normal Cargo.toml

solved bug
2021-09-15 21:07:01 +03:00
Paul Makles
e2da00e6b0 Merge pull request #83 from Jan0660/master
Add route for fetching self for bots and users
2021-09-14 21:22:34 +01:00
Paul
839b66109c Only run Docker test if Dockerfile changes in PR. 2021-09-14 21:19:57 +01:00
Paul Makles
7f3141eec4 Merge pull request #81 from goaaats/feat/embed_improvements 2021-09-14 21:15:06 +01:00
Paul Makles
536e335b25 Merge branch 'master' into feat/embed_improvements 2021-09-14 20:54:21 +01:00
Paul Makles
4b8205b75b Merge pull request #76 from goaaats/feature/server_limit
Add basic server limit
2021-09-14 20:52:16 +01:00
Paul
3cbc548dd8 Don't run Docker test on PR. 2021-09-14 20:50:39 +01:00
Paul Makles
6a44a00eeb Merge pull request #77 from yannickfunk/unit-tests 2021-09-14 20:38:08 +01:00
meguminloli
2660b96250 push dependencies 2021-09-14 18:26:02 +03:00
Paul
b4db321d0e Fix missing definition of flags on creation. 2021-09-14 16:16:18 +01:00
Paul Makles
0eb52d32ef Merge pull request #68 from ryanalexander/server-flags
Add server flags value
2021-09-14 16:05:08 +01:00
Paul
9b6893ca48 Write database migration for last_message_id. #67 2021-09-14 15:07:41 +01:00
Paul Makles
8aa92564fc Merge pull request #67 from yannickfunk/unify-last-message 2021-09-14 13:39:27 +01:00
Yannick Funk
ddfc0a5f86 Merge branch 'master' into unify-last-message 2021-09-13 20:19:54 +02:00
Paul
ad364320c8 Fix route definitions for change pass / email. 2021-09-12 21:37:42 +01:00
Rebecca Meritz
4f1d841312 Allow users to change case of own username
Usernames are case-insensitive so now one have the name `foo` and
someone else have the name `FOO`, but user `foo` should be able to
change their own name to `FOO` if they want to for display purposes.
2021-09-11 18:59:23 -07:00
Jan0660
405f45d0f7 Add GET /users/@me 2021-09-12 00:01:30 +00:00
Paul
0b0249f544 Fix migration again. 2021-09-11 21:11:15 +01:00
Paul
d0cb73106d Fix session migration. 2021-09-11 18:41:06 +01:00
Paul Makles
5d3fef3bfa Merge pull request #82 from revoltchat/rauth-v1 2021-09-11 17:53:21 +01:00
Paul
8bb5fb25dd Fix bot logon, should do nested match.
Add comment to migrations.
Fix attachment deletes on messages.
2021-09-11 17:46:20 +01:00
Paul
fa8eb874d0 Handle both numerical and binary pings. 2021-09-11 12:43:26 +01:00
Paul
06f968022d Update to rAuth v1, closes #63.
Add Pong packet for normal frames.
2021-09-11 11:23:15 +01:00
Paul Makles
65165b1f8e Merge pull request #80 from goaaats/fix/youtube_timestamp 2021-09-09 20:50:31 +01:00
goat
6a29c7cf03 feat(january): add MAX_EMBED_COUNT variable 2021-09-08 18:16:56 +02:00
goat
8443cfab00 feat(january): fetch embeds for multiple links 2021-09-08 17:59:29 +02:00
goat
99a6e19cfc feat(january): ignore links between angle brackets 2021-09-08 16:56:17 +02:00
goat
77422f023e fix(january): add youtube timestamp field 2021-09-07 17:10:44 +02:00
Zomatree
b99807cf2a use unwrap 2021-09-07 15:57:38 +01:00
Zomatree
fc5d14d4aa remove line 2021-09-07 15:52:33 +01:00
Zomatree
fcb59b2493 remove comments 2021-09-07 15:50:56 +01:00
Zomatree
2f452b15b1 add ratelimits 2021-09-07 15:48:06 +01:00
Yannick Funk
602a65f809 Change branch to master 2021-09-07 14:48:46 +02:00
goat
88a1d46bcc feat: add basic server limit via env var 2021-09-07 05:16:27 +02:00
Yannick Funk
d8d0d93a21 Remove release build 2021-09-06 17:13:07 +02:00
Yannick Funk
fe085ef100 Change to nightly 2021-09-06 16:40:30 +02:00
Yannick Funk
8813515786 Change branch 2021-09-06 16:30:40 +02:00
Yannick Funk
6813fdd513 Create rust.yaml 2021-09-06 16:28:33 +02:00
Zomatree
175d257e34 fix msgpack encoding 2021-09-05 22:01:58 +01:00
Paul
5328c66cbb Fix image name. 2021-09-03 16:27:53 +01:00
Paul
862f57235c Fallback to unknown user.
Add GitHub CI.
2021-09-03 16:26:34 +01:00
Paul
835a32932f Merge branch 'master' of https://github.com/revoltchat/delta 2021-09-03 16:15:27 +01:00
Paul Makles
03cb48ed00 Merge pull request #72 from yannickfunk/push-notifications 2021-09-03 16:13:24 +01:00
Paul
dbfed52f17 Prevent access to bot routes as bot. 2021-09-03 14:37:40 +01:00
Yannick Funk
43b75a2310 Add author and tag to push notification 2021-09-02 23:22:23 +02:00
Yannick Funk
5a17038012 Remove whitespace 2021-09-02 20:34:26 +02:00
Yannick Funk
f6b55574b1 Only build notification when there are subscriptions 2021-09-02 20:32:36 +02:00
Yannick Funk
b5aa0ee971 Remove print 2021-09-02 18:24:22 +02:00
Yannick Funk
697132082a First draft for push notifications 2021-09-01 21:52:24 +02:00
Ryan Alexander
1a847d0084 Add server flags value 2021-08-29 09:14:10 +10:00
Paul Makles
17a378494c Merge pull request #65 from yannickfunk/mention-regex 2021-08-26 17:57:30 +01:00
Yannick Funk
6c6a1508dc Unify last_message_id 2021-08-26 16:56:13 +02:00
Yannick Funk
b15e07c707 Initial commit 2021-08-25 21:48:43 +02:00
Yannick Funk
ffa721117e Shorten regex further 2021-08-25 16:34:42 +02:00
Yannick Funk
c29f6d830c Ignore I L O and u 2021-08-25 16:32:11 +02:00
Yannick Funk
effe3763da Shorten regex and adapt capture evaluation 2021-08-25 00:10:20 +02:00
Paul Makles
b0d2e99588 Merge pull request #64 from yannickfunk/impl-ping-pong 2021-08-24 15:18:34 +01:00
Yannick Funk
1ecd74eb6c Remove whitespace 2021-08-23 14:25:09 +02:00
Yannick Funk
ceb35e58ce Parse ws spec ping messages 2021-08-23 14:22:23 +02:00
Zomatree
31ebf19cf9 color -> colour 2021-08-23 12:44:59 +01:00
Paul Makles
40a4cf244d Merge pull request #37 from infi/master
Update README to consistent format
2021-08-22 21:59:36 +01:00
Infi
6d2d3003bd Implement requested changes 2021-08-22 22:58:39 +02:00
Paul Makles
302c3e0d4b Merge pull request #42 from Zomatree/msgpack 2021-08-17 19:07:23 +01:00
Zomatree
867962cc3c reword comment 2021-08-17 17:35:05 +01:00
Zomatree
bcfde79745 add msgpack support 2021-08-17 17:23:36 +01:00
Paul
326e539ad3 Fix issues with incoming PRs. 2021-08-16 14:07:42 +01:00
Paul Makles
c632341eb7 Merge pull request #36 from Zomatree/master 2021-08-16 13:57:09 +01:00
Paul Makles
6de1d1f98b Merge pull request #38 from heikkari/13-24 2021-08-16 13:52:42 +01:00
heikkari
721ca3a4f5 Enhancement: Routes with no body should return 204 No Content. 2021-08-16 15:34:44 +03:00
heikkari
48d2fbe5ae Add a responder for empty responses 2021-08-16 14:05:59 +03:00
heikkari
c1d03960ed Respond to malformed WS packets 2021-08-16 13:40:52 +03:00
Infi
7c7573d3cf Fix Naming for Revolt, replace mention of Revolt with Delta 2021-08-16 10:48:09 +02:00
Infi
8ca3e50d91 Remove Unused Banner 2021-08-16 10:45:40 +02:00
Infi
906dd3fb04 Update README to consistent format, closes #28 2021-08-16 10:41:28 +02:00
Zomatree
08fe01a7f2 actually remove old func 2021-08-16 01:06:13 +01:00
Zomatree
d972395e43 use existing if_false function 2021-08-16 00:35:53 +01:00
Zomatree
5179504ad9 remove whitespace 2021-08-16 00:24:55 +01:00
Zomatree
d4f7c4a77d add nsfw to servers and channels 2021-08-16 00:18:14 +01:00
Paul Makles
28c4312880 Merge pull request #35 from heikkari/34 2021-08-15 23:45:12 +01:00
heikkari
7e84d2671a Move code to database/entities/user.rs 2021-08-16 01:26:05 +03:00
Paul
bffac8c4c0 Update CORS options.
Disallow bots to be added as friends.
2021-08-15 23:11:33 +01:00
heikkari
3b5bebab78 Fix: User's invisibility status is exposed 2021-08-16 01:05:54 +03:00
heikkari
e35a6b12e1 Fix: User's invisibility status is exposed 2021-08-16 00:40:33 +03:00
Paul
f04137d50f Include servers in mutual route. 2021-08-12 16:16:13 +01:00
Paul
b3bdf403cf Add delete route for bots. Closes #8. 2021-08-12 14:28:46 +01:00
Paul
bd3606176a Add bot edit route for #8. 2021-08-12 14:05:48 +01:00
Paul
a71bfa908c Add fetch owned, public and specific bots for #8. 2021-08-12 13:40:03 +01:00
Paul
9f749cfdf8 Invite bot to server / group for #8. 2021-08-12 09:24:06 +01:00
Paul
93610ebbc3 Restrict bots from creating servers, groups, acking messages, and making friends. 2021-08-11 20:16:11 +01:00
Paul
084d71f050 Add role hoisting / ranking.
Add bot creation for #8.
2021-08-11 20:04:22 +01:00
Paul
e70f848f21 Patch password length limit on change username. 2021-08-09 14:53:26 +01:00
Paul
e7065cbf1d Bump reqwest version.
Add mark server as read route.
Mark servers as read on join.
2021-08-07 12:26:25 +01:00
Paul
fdba2d9e15 Bump musl-builder. 2021-08-06 17:56:44 +01:00
Paul
8890e3bab0 Fixes #2: Include own beta badge in payload object 2021-08-06 17:34:48 +01:00
Paul
f09fde616a Add rate limiting to message sending. 2021-08-06 17:24:16 +01:00
Paul
6fe0725799 Organise Cargo.toml 2021-08-06 17:24:16 +01:00
Paul
382972ea22 Migrate the rest of the code. 2021-08-06 17:24:16 +01:00
Paul
9fec4e48c0 Start moving to Rocket 0.5.0 2021-08-06 17:24:16 +01:00
Paul
33262cc703 Add cache control header for default avatars. 2021-07-31 14:48:08 +01:00
Paul
e655f53641 Fix embeds not re-generating properly on edit.
Include own memberships in Ready payload.
2021-07-31 12:09:03 +01:00
Paul
74b4c6bf2a Re-write emails, remove HTML body.
SpamAssassin is a bitch.
2021-07-28 14:59:16 +01:00
Paul
3c0d736f1e Use plain reset / verify emails. 2021-07-28 14:08:55 +01:00
Paul
8f6e5be1d4 Events: Distribute user updates to servers.
Fix: Deleting a server now correctly deletes all associated objects.
2021-07-28 13:40:03 +01:00
Paul
7d3ce0c96a Remove broken HTML attribute. 2021-07-27 17:24:47 +01:00
Paul
c6de871ad3 Add extra snippet. 2021-07-27 17:14:36 +01:00
insert
f32eaa98d4 Revert "Strip all <a /> tags, fuck SpamAssassin."
This reverts commit d4743b1453
2021-07-27 16:11:59 +00:00
Paul
d4743b1453 Strip all <a /> tags, fuck SpamAssassin. 2021-07-27 16:32:55 +01:00
Paul
dd65b9f358 Try make content match href. 2021-07-27 16:25:32 +01:00
Paul
ec59d0e9a9 Try to make email not go into spam. 2021-07-27 16:16:17 +01:00
Paul
2407d4da3a Deprecate UserPresence event. 2021-07-27 14:22:41 +01:00
Paul
a5f7314f97 Fix description not being updated on servers. 2021-07-27 12:29:55 +01:00
Paul
deb2344428 Merge Jan's changes and bump version. 2021-07-25 21:53:35 +01:00
insert
37bb0f397f Merge branch 'master' into 'master'
Add member_count to /invites/:id

See merge request revolt/delta!3
2021-07-25 20:46:55 +00:00
jan0660
ca04f4d510 Add member_count to /invites/:id 2021-07-25 16:49:27 +02:00
Paul
682b6cbd9f Return banned user objects.
Fix category edit.
2021-07-24 19:42:57 +01:00
Paul
1a8006b3e4 Add before, after and sort support to search. 2021-07-21 18:26:35 +01:00
Paul
27a9fc3e21 Fix username block list. 2021-07-14 11:21:53 +01:00
Paul
1872432725 0.5.1-alpha.8
Add Edit Role route.
2021-07-13 13:43:42 +01:00
Paul
6f8802ab5f Feature: W.I.P. search route 2021-07-09 14:34:01 +01:00
Paul
fe821b8283 Temporary fix for compile.
Add badge ID.
2021-07-08 22:54:46 +01:00
Paul
a4c227ce41 Add "nearby" message query. 2021-07-07 16:21:21 +01:00
Paul
edd8ef8a1d Patch max password length on rauth. 2021-07-06 12:21:40 +01:00
Paul
96c9190fa8 Fix: Allow users with server overlap to access each other. 2021-07-05 17:49:30 +01:00
Paul
e452d6a362 Fix: Permission PUT route priority. 2021-07-01 17:49:48 +01:00
Paul
57f81e018c Servers: Create or delete roles.
Members: Assign / un-assign multiple roles.
Servers: Routes to set permissions.
2021-06-30 20:09:42 +01:00
Paul Makles
31c93b9954 Channels: Add role / default permission set routes.
Server: Add role create / delete route.
2021-06-30 12:56:18 +01:00
Paul
51dc25c94c Refactor: Use category_route.rs naming convention for channels. 2021-06-27 09:58:08 +01:00
Paul
a017872c99 Servers: Add categories to server model. 2021-06-27 00:13:35 +01:00
Paul
f35a1049da Fix: Use UploadFiles instead of EmbedLinks perm. 2021-06-26 22:35:21 +01:00
Paul
07e74e1074 Servers: New permissions implementation.
Feature: Roles and group permissions. (server-side)
2021-06-26 22:16:37 +01:00
Paul
35b0c69cdb Permissions: Add EmbedLinks / UploadFiles to channels
Web Push: Send events if mentioned
2021-06-25 13:33:05 +01:00
Paul
b45ea266d8 Fix various issues relating to voice channels. 2021-06-24 12:33:16 +01:00
Paul
9a417aa6ad Messaging: Add message replies.
Servers: Add VoiceChannel.
2021-06-23 13:50:18 +01:00
Paul
ad06ff16c4 Messaging: Upsert mentions into unreads.
Messaging: Allow multiple attachment upload.
2021-06-22 17:42:18 +01:00
Paul
b10d4f3559 Messaging: Add mentions to unread object. 2021-06-17 14:42:48 +01:00
Paul
4727f997ed New Feature: Add server-side channel unreads. 2021-06-16 23:24:31 +01:00
Paul
d2b17b64e7 Docker: Publish to :latest tag when building. 2021-06-16 19:36:59 +01:00
Paul
598f6374b3 Messaging: Don't embed URLs in code. 2021-06-16 16:43:17 +01:00
Paul
bad9024537 Messaging: Remove embed when editing message. 2021-06-16 16:31:06 +01:00
Paul
6d67b3f767 Fix: Prevent users banning themselves. 2021-06-16 15:46:54 +01:00
Paul
9a2ccecb89 Channels: Make sure to delete last, to keep integrity. 2021-06-16 15:42:46 +01:00
Paul
c4b39c56d4 Fix: Delete channel query failing due to empty unset. 2021-06-16 14:15:53 +01:00
Paul
f33611aee6 Fix: Handle status::User on group create. 2021-06-14 16:00:29 +01:00
Paul
95d7c23d32 Badges: Add responsible disclosure badge.
Fix: Don't serialize None on Metadata. (january embed)
2021-06-13 20:19:07 +01:00
Paul
6b8158bf54 Fix: Use channel_invites collection name instead of invites.
Was conflicting with invite-only rauth registrations.
2021-06-12 10:33:31 +01:00
Paul
9be9b76035 Servers: Add new system messages. 2021-06-11 22:12:01 +01:00
Paul
03fa923ef4 Refactor: Send system messages directly from content. 2021-06-11 19:18:46 +01:00
Paul
78c890aa59 Messaging: Parse mentions. 2021-06-11 18:07:37 +01:00
Paul
0f18a6781d Channels: Include last message id on text channels.
API: Return members when fetching messages.
Misc: Remove defunct DISABLE_REGISTRATION variable.
2021-06-11 16:22:35 +01:00
Paul
f0d1ab390b Invites: Include server and channel ID.
Permissions: Small fix, consider self as User.
2021-06-10 14:56:53 +01:00
Paul
335f729c53 Notifications: Remove ServerCreate event. 2021-06-07 18:28:28 +01:00
Paul
c763a292a7 Run cargo fmt. 2021-06-07 16:16:40 +01:00
Paul
fce92382bf Servers: Fix create / leave.
Prevent removal of self / owner.
2021-06-07 16:14:48 +01:00
Paul
4e90cf1f58 Servers: Ban and unban users +
ban list.
2021-06-06 16:52:00 +01:00
Paul
782bfb7e03 Servers: Route for kicking a member. 2021-06-06 15:22:23 +01:00
Paul
1f1d9613e2 Servers: Fetch and edit members.
Permissions: Use bit representation for permissions.
2021-06-06 14:57:55 +01:00
Paul
0af4fa4be9 Servers: Add description field.
API: Allow removal of description on channels and servers.
2021-06-06 10:55:31 +01:00
Paul
380369e1c4 Servers: Route for fetching server invites. 2021-06-06 10:31:06 +01:00
Paul
dbd70abaca Servers: Add route for fetching members. 2021-06-06 10:17:33 +01:00
Paul
f9fbe54b17 Servers: Create and delete invites. Create and fetch servers. 2021-06-05 22:41:47 +01:00
Paul
681b2b8ab6 Servers: Add invites. Create, view and join. 2021-06-03 20:00:08 +01:00
Paul
812fa2a98f Permissions: Resolve guild, allows ability to delete channels, delete others messages, etc. 2021-06-02 12:35:01 +01:00
Paul
1713ad057d Servers: Add route for creating channels.
Notifications: Subscribe to guild channels.
2021-06-02 11:51:58 +01:00
Paul
f32f447233 Notifications: Subscribe users to new guild channels. 2021-06-02 10:27:51 +01:00
Paul
8627f27a50 Channels: Don't return self on msg query. 2021-06-02 10:12:53 +01:00
insert
fea68faa54 Add LICENSE 2021-06-01 19:45:42 +00:00
Paul
46bf5fcdd1 Servers: Add TextChannel. 2021-06-01 20:38:35 +01:00
Paul
6bb4501c52 Servers: Add edit route. Ability to set icon / banner. 2021-06-01 17:42:23 +01:00
Paul
bff72fa6c3 Servers: Core objects required. Includes creation. Bump version to 0.5.0. 2021-06-01 17:09:31 +01:00
Paul
a4c1fee4cc Sync: Allow custom timestamp; return correct data structure. 2021-06-01 13:25:47 +01:00
Paul
62ffa10939 rAuth: Silently fail email send for creation. 2021-05-27 15:51:24 +01:00
Paul
a39adb64c6 Hive: Don't send out sync for irrelevant users. 2021-05-26 13:19:03 +01:00
Paul
3cd3bc54af New Feature: Settings sync to server. 2021-05-26 13:02:40 +01:00
Paul
5aa9624d5c Database: Add user settings sync entity.
Run cargo fmt.
2021-05-26 11:02:16 +01:00
Paul
7aad88ad3f Messages: Allow multiple attachments. (⚠️ breaking change) 2021-05-25 17:23:47 +01:00
Paul
fa3bc56ef4 Channels: Add option to return users in message query. 2021-05-25 15:03:49 +01:00
Paul
7c7943b9f6 Websocket: Provide channel id for message edit / del. 2021-05-25 14:06:23 +01:00
Paul
67b9b8ceee Users: Serve early adopter badge from server. 2021-05-25 13:37:12 +01:00
Paul
f1462d4e4d Remove snafu. 2021-05-15 18:47:05 +01:00
Paul
fae0198343 Bump version to 0.4.1-alpha.11 2021-05-14 22:54:27 +01:00
Paul
10f116b4a3 Block voice calls in saved messages channel.
Add messages for desc / icon change for group chats.
2021-05-14 22:44:35 +01:00
Paul
6716a2d32b Join with newlines for embed generation.
Server January URL endpoint on server configuration.
2021-05-14 22:39:16 +01:00
Paul
6cc92b877e Fix block user route, send correct user struct.
Add route for fetching members.
Cargo fmt on accident.
2021-05-14 22:29:43 +01:00
Paul
cc0307f702 Fix find mutual query. 2021-05-13 17:19:43 +01:00
Paul
274d6f2b5a Update January definitions. (special embeds) 2021-05-09 19:30:55 +01:00
Paul
0f100213ba Update January definitions; add servers collection 2021-05-08 17:08:45 +01:00
Paul
7293abfc53 Add january support. 2021-05-07 18:00:21 +01:00
Paul
cf7bb832da Really make sure MongoDB driver can't screw up. 2021-05-03 21:02:12 +01:00
Paul
efc3794f3d Emergency patch; Rust MDB driver deletes doc. with empty update. 2021-05-03 20:48:37 +01:00
Paul
a319e72655 Add a way to choose message fetch sort. 2021-05-03 16:35:59 +01:00
Paul
2173b1e9f8 Fix type. 2021-05-03 15:04:05 +01:00
Paul
c086fe7ac4 Move versioning to source. 2021-05-03 15:03:30 +01:00
Paul
fa960ebc94 Send out unset in event. 2021-05-03 11:17:28 +01:00
Paul
b47067b311 Cache Cargo dependencies for Docker build. 2021-05-03 09:59:40 +01:00
Paul
2e996a487b Add event when user goes offline. 2021-05-03 08:57:49 +01:00
Paul
92bface6ae Add group icons / profile backgrounds. 2021-05-01 22:55:37 +01:00
Paul
8cfa5d7091 Run cargo fmt. 2021-05-01 17:29:31 +01:00
Paul
59b18fd376 Delete old avatar; fix database migration. 2021-05-01 17:12:51 +01:00
Paul
f135a57a9b API change: deprecate Gravatar.
Include avatar information in user object.
2021-05-01 16:38:06 +01:00
Paul
5da26cb833 Breaking: Provide new user object instead of id.
Fix rustup complaining about join macros.
2021-05-01 15:54:29 +01:00
Paul
c8981ac695 Merge branch 'master' of https://gitlab.insrt.uk/revolt/delta 2021-05-01 13:08:55 +01:00
Martin Loffler
33b0658680 Multi-thread rocket and hive 2021-04-30 13:19:31 +02:00
Paul
81002c75d2 Only push out status; mark attachments as deleted. 2021-04-28 17:12:14 +01:00
Paul
f163cb65de AAAAAAAAAAAAA
We should be setting message_id not message.
2021-04-24 18:01:08 +01:00
Paul
f7bcd3ad93 Migrate to Autumn 1.0.0 2021-04-24 16:00:13 +01:00
Paul
9e2250783c Allow people with mutual connections to see profile / status; fix username check. 2021-04-07 19:21:25 +01:00
Paul
2050a0609f Fix update_one failing on sys message. 2021-04-03 21:37:24 +01:00
Paul
a547177325 Add status presence indicator. 2021-04-03 20:50:14 +01:00
Paul
c3fa7bf74e Change route paths for user edits. 2021-04-03 15:02:29 +01:00
Paul
7f5d6f2312 Format code. 2021-04-03 14:44:01 +01:00
Paul
9492e145f9 User status / profile change route, username change route. 2021-04-03 14:41:40 +01:00
Paul
78c42fd412 Fetch profile route, push badges and status. 2021-04-02 21:55:02 +01:00
Paul
3797878d20 Fixes bug with distributed nodes. 2021-04-02 13:25:51 +01:00
Paul
d2bf477fd8 Expose voso host. 2021-04-01 15:11:19 +01:00
Martin Loffler
c9bff789de Fixed room creation failure in join_call 2021-04-01 15:59:06 +02:00
Paul
56bd763820 Prevent setting null in edit. Preflight for variables. 2021-04-01 14:01:07 +01:00
Paul
32cd9d8a13 Strict typing for system messages; add a way to rename group. 2021-03-31 20:54:47 +01:00
Paul
60731e1c70 Integrate voso, join_call route for channels. 2021-03-31 11:48:52 +01:00
Paul
37df18dbf7 Fixes mutual connections not being seen and hence requests failing. 2021-03-30 22:09:36 +01:00
Paul
0a9d5de369 Don't unwrap on subscription errors. 2021-03-29 10:14:25 +01:00
Paul
d2864ac025 Make sure to check that they are friends. 2021-02-19 15:49:55 +00:00
Paul
4eb76fd154 Fix mutex issues. 2021-02-19 15:24:19 +00:00
Paul
3c7852271a Add temporary typing indicator impl. 2021-02-19 14:50:23 +00:00
Paul
dadad271b4 Mark attachments as deleted when deleting messages. 2021-02-19 14:12:21 +00:00
Paul
db6047f2d3 Allow empty messages with attachment. 2021-02-19 14:09:19 +00:00
Paul
5baf85a8e9 JSON system messages. 2021-02-19 14:03:41 +00:00
Paul
10cac358a9 Label permission errors, and too many ids for /stale. 2021-02-19 13:26:04 +00:00
Paul
64d2707366 Find mutual for users; allow dots in usernames. 2021-02-19 13:11:33 +00:00
Paul
78cfbf9d21 Clean up subscription code; handle error properly. 2021-02-19 12:48:52 +00:00
Paul
02077a4024 Check if online. 2021-02-18 16:47:59 +00:00
Paul
52e070c21c Basic Web Push support. 2021-02-18 16:21:34 +00:00
Paul Makles
a2f14d2d37 Bump version. 2021-02-16 15:26:31 +00:00
Paul Makles
a7ea29d821 Add a way to send messages with attachments. 2021-02-16 15:25:33 +00:00
Paul Makles
b6b51bca26 Route which lets clients determine messages that have updated / deleted. 2021-02-16 09:34:24 +00:00
Paul Makles
2bafffbbc1 Case-insensitive search for emails. 2021-02-14 16:28:23 +00:00
Paul Makles
bdf5fb95d0 Add rAuth captcha support. 2021-02-13 20:18:38 +00:00
Paul Makles
ea8de2400a Add support for rAuth invite-only mode. 2021-02-12 20:37:24 +00:00
Paul Makles
f5e0b49bbb Update rAuth, add templates; add APP_URL variable. 2021-02-12 15:09:42 +00:00
Paul Makles
b0d07ec58f Include DM users in payload; fix permission query. + cargo fmt 2021-02-09 16:41:08 +00:00
Paul Makles
4c99d079e0 Remove ViewAll; check for mutual DMs. 2021-02-09 15:51:35 +00:00
Paul Makles
38bedbaaea Add Gravatar support. 2021-02-02 19:09:13 +00:00
Paul Makles
ea276c3517 Fix assets fr this time 2021-01-30 18:40:58 +00:00
Paul Makles
648e33a13e Include assets in docker image. 2021-01-30 18:23:23 +00:00
Paul Makles
a76ec207b3 Add Prometheus metrics. 2021-01-29 21:56:37 +00:00
Paul Makles
687b300bfc Include assets in Docker image. 2021-01-28 18:51:46 +00:00
Paul Makles
6586d8f02c Fix SSL certificates in container. 2021-01-27 19:37:21 +00:00
Paul Makles
a8a8c08c29 Update Docker image and ports. 2021-01-27 18:31:27 +00:00
Paul Makles
1806ea824b Bump rAuth revision. 2021-01-27 11:08:37 +00:00
Paul Makles
222a417fff Add utility function for including relationship on User. 2021-01-26 22:33:14 +00:00
Paul Makles
3b85dcce14 Include group members in payload. 2021-01-26 22:05:32 +00:00
Paul Makles
f42480886b Re-structure Permissions; add perm to view users from mutual groups. 2021-01-26 21:12:23 +00:00
Paul Makles
23ec2d61f1 Push env variables to rAuth + cargo fmt. 2021-01-26 17:29:03 +00:00
Paul Makles
abd8b1821d Reset migrations; [point of no return for old databases] 2021-01-26 09:30:29 +00:00
Paul Makles
75a35831da Update rauth, pin git repositories in Cargo.toml. 2021-01-25 21:35:47 +00:00
Paul Makles
99e2f874a1 $set instead of overwriting object 2021-01-24 10:28:24 +00:00
Paul Makles
11f7092fcd Add last_message to channels, mark DMs as active. 2021-01-24 10:24:44 +00:00
Paul Makles
cb882ce1b2 Block @revolt from being registered. 2021-01-24 09:29:42 +00:00
Paul Makles
5486a68bfa Move group leave into posthandle hook. 2021-01-24 09:20:43 +00:00
Paul Makles
4aaf328435 Include all channels in payload. 2021-01-24 09:20:07 +00:00
Paul Makles
9d251f794b Update tag in enums. 2021-01-19 21:27:18 +00:00
Paul Makles
1d390d483d Add a database migration for channel tag enum. 2021-01-19 21:11:38 +00:00
Paul Makles
81de29e723 Backtracking again, and adding the channel_type tag. 2021-01-19 20:55:25 +00:00
Paul Makles
984017eb61 Revert tag change, update partials to include ids. 2021-01-19 20:18:29 +00:00
Paul Makles
5ab329dfdd Prevent fetching messages from other channels. Change channel tag to channel_type. 2021-01-19 19:54:37 +00:00
148 changed files with 11018 additions and 1999 deletions

View File

@@ -1,3 +1,3 @@
assets
target
.mongo
.env

2
.github/FUNDING.yml vendored Normal file
View File

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

103
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
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: revoltchat/server, ghcr.io/revoltchat/server
- name: Login to DockerHub
uses: docker/login-action@v1
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 }}
- 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

33
.github/workflows/rust.yaml vendored Normal file
View File

@@ -0,0 +1,33 @@
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

49
.github/workflows/triage_issue.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
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

72
.github/workflows/triage_pr.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Add PR to Board
on:
pull_request_target:
types: [opened]
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

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
Rocket.toml
/target
/target_backup
**/*.rs.bk
.mongo
.env

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"rust-analyzer.diagnostics.disabled": [
"unresolved-macro-call"
]
}

2243
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,65 @@
[package]
name = "revolt"
version = "0.3.1"
# To help optimise CI and Docker builds.
# Version here is left as 0.0.0, please
# adjust and run ./set_version.sh instead.
version = "0.0.0"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures = "0.3.8"
many-to-many = "0.1.2"
impl_ops = "0.1.1"
ctrlc = { version = "3.0", features = ["termination"] }
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rauth = { git = "https://gitlab.insrt.uk/insert/rauth" }
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
hive_pubsub = { version = "0.4.3", features = ["mongo"] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", default-features = false }
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
once_cell = "1.4.1"
# Utility
lru = "0.7.0"
url = "2.2.2"
log = "0.4.11"
dotenv = "0.15.0"
ulid = "0.4.1"
linkify = "0.6.0"
once_cell = "1.4.1"
env_logger = "0.7.1"
lazy_static = "1.4.0"
ctrlc = { version = "3.0", features = ["termination"] }
# Lang. Utilities
regex = "1"
num_enum = "0.5.1"
impl_ops = "0.1.1"
bitfield = "0.13.2"
phf = { version = "0.9.0", features = ["macros"] }
# ID / key generation
ulid = "0.4.1"
nanoid = "0.4.0"
base64 = "0.13.0"
# serde
serde_json = "1.0.57"
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.11", features = ["derive"] }
snafu = { version = "0.6.9" }
serde_json = "1.0.57"
bitfield = "0.13.2"
rmp-serde = { git = "https://github.com/3Hren/msgpack-rust", rev = "5bf2c24203ad422233cf35b7b7bfad9f7e811814" }
reqwest = { version = "0.10.8", features = ["json"] }
lazy_static = "1.4.0"
num_enum = "0.5.1"
# async
futures = "0.3.8"
chrono = "0.4.15"
time = "0.2.16"
rand = "0.7.3"
regex = "1"
async-channel = "1.6.1"
reqwest = { version = "0.11.4", features = ["json"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
lettre = "0.10.0-alpha.1"
env_logger = "0.7.1"
log = "0.4.11"
# internal util
web-push = "0.7.2"
many-to-many = "0.1.2"
lettre = "0.10.0-alpha.4"
rauth = { git = "https://github.com/insertish/rauth", rev = "775a6f3ffb9ffc0e382af32fac2924e9a707e890" }
hive_pubsub = { git = "https://gitlab.insrt.uk/insert/hive", rev = "b0f3db9d33990530d7640d4bbb309c8d9eb5c0cf", features = ["redis-backend"] }
# redis
redis = { version = "0.21.2", features = ["async-std-comp"] }
mobc = { version = "0.7.3" }
mobc-redis = { version = "0.7.0", default-features = false, features = ["async-std-comp"] }
# web
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] }
mongodb = { version = "1.2.2", features = ["tokio-runtime"], default-features = false }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }

View File

@@ -1,16 +1,25 @@
# Build Stage
FROM ekidd/rust-musl-builder:nightly-2020-11-19 AS builder
FROM rustlang/rust:nightly-slim AS builder
USER 0:0
WORKDIR /home/rust/src
RUN USER=root cargo new --bin revolt
WORKDIR ./revolt
WORKDIR /home/rust/src/revolt
COPY Cargo.toml Cargo.lock ./
COPY src/bin/dummy.rs ./src/bin/dummy.rs
RUN apt-get update && apt-get install -y libssl-dev pkg-config && cargo build --release --bin dummy
COPY assets/templates ./assets/templates
COPY src ./src
RUN cargo build --release
RUN cargo install --locked --path .
# Bundle Stage
FROM scratch
COPY --from=builder /home/rust/src/revolt/target/x86_64-unknown-linux-musl/release/revolt ./
FROM debian:buster-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /usr/local/cargo/bin/revolt ./
COPY assets ./assets
EXPOSE 8000
EXPOSE 9000
ENV ROCKET_ADDRESS 0.0.0.0
ENV ROCKET_PORT 8000
CMD ["./revolt"]

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Server
Copyright (C) 2021 REVOLT
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@@ -1,20 +1,45 @@
![](assets/banner.png)
# Delta
Delta is a **blazing fast API server** built with Rust for the REVOLT platform.
## Description
Delta is a blazing fast API server built with Rust for Revolt.
**Features:**
Features:
- Robust and efficient API routes for running a chat platform.
- Distributed notification system, allowing any node to be seamlessly connected.
- Simple deployment, based mostly on pure Rust code and libraries.
- Hooks up to a MongoDB deployment, provide URI and no extra work needed.
## Docker Helper Scripts
## Stack
If you have Docker installed, you can use the helper scripts to deploy Revolt in your development environment.
- [Rocket](https://rocket.rs/) (REST)
- [Async Tungstenite](https://github.com/sdroege/async-tungstenite) (WebSockets)
- [MongoDB](https://mongodb.com/)
```bash
./build.sh # build Docker image
./run.sh # run Docker container
./monitor.sh # view container logs
./remove.sh # kill and remove container
```
## Resources
### Revolt
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing)
## CLI Commands
| Command | Description |
| ------------------ | ----------------------------------------------------------------------------------------- |
| `./publish.sh` | Publish a Docker Image. |
| `./set_version.sh` | Update the version. **Not intended for PR use.** |
| `cargo build` | Build/compile Delta. |
| `cargo run` | Run Delta. |
| `cargo fmt` | Format Delta. Not intended for PR use to avoid accidentally formatting unformatted files. |
## Contributing
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.
## License
Delta is licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/delta/blob/master/LICENSE).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-GB">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Reset your password.</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style type="text/css">
a[x-apple-data-detectors] {color: inherit !important;}
</style>
</head>
<body style="margin: 0; padding: 0;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 20px 0 30px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
<tr>
<td align="center" bgcolor="#ff4654">
<img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
</td>
</tr>
<tr>
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #153643; font-family: Arial, sans-serif;">
<h1 style="font-size: 24px; margin: 0;">Reset your password!</h1>
</td>
</tr>
<tr>
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
<p>
You requested a password reset, if you didn't perform this action you can safely ignore this email.
</p>
<p style="margin: 0;">
Reset your password by navigating to <a href="{{url}}">{{url}}</a>.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ff4654" style="padding: 30px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
<p style="margin: 0;">Sent by Revolt. &middot; Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
<p>Revolt is a user-first chat platform built with modern web technologies.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,5 @@
You requested a password reset, if you did not perform this action you can safely ignore this email.
Reset your password here: {{url}}
Sent by Revolt.

View File

@@ -0,0 +1,14 @@
<h2>Reset your password.</h2>
<p>
You requested a password reset, if you did not perform this action you can safely ignore this email.
</p>
<p>
Reset your password here: <a href="{{url}}">{{url}}</a>
</p>
<br/>
<p>
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
</p>
<p>
Revolt is a user-first chat platform built with modern web technologies.
</p>

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-GB">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Verify your account.</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style type="text/css">
a[x-apple-data-detectors] {color: inherit !important;}
</style>
</head>
<body style="margin: 0; padding: 0;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 20px 0 30px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
<tr>
<td align="center" bgcolor="#ff4654">
<img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
</td>
</tr>
<tr>
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #153643; font-family: Arial, sans-serif;">
<h1 style="font-size: 24px; margin: 0;">You're almost there!</h1>
</td>
</tr>
<tr>
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
<p>
Verify your account to be able to log into the platform. If you didn't perform this action you can safely ignore this email.
</p>
<p style="margin: 0;">
Please verify your account by navigating to <a href="{{url}}">{{url}}</a>.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ff4654" style="padding: 30px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
<p style="margin: 0;">Sent by Revolt. &middot; Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
<p>Revolt is a user-first chat platform built with modern web technologies.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,6 @@
You're almost there!
If you did not perform this action you can safely ignore this email.
Please verify your account here: {{url}}
Sent by Revolt.

View File

@@ -0,0 +1,15 @@
<h2>You're almost there!</h2>
<p>
Verify your account to be able to log into the platform.<br/>
If you did not perform this action you can safely ignore this email.
</p>
<p>
Please verify your account here: <a href="{{url}}">{{url}}</a>
</p>
<br/>
<p>
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
</p>
<p>
Revolt is a user-first chat platform built with modern web technologies.
</p>

View File

@@ -14,5 +14,5 @@ services:
- REVOLT_UNSAFE_NO_CAPTCHA=1
ports:
- "8000:8000"
- "9999:9999"
- "9000:9000"
restart: unless-stopped

7
publish.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/bash
source set_version.sh
docker build -t revoltchat/server:${version} . &&
docker tag revoltchat/server:${version} revoltchat/server:latest &&
docker push revoltchat/server:${version} &&
docker push revoltchat/server:latest

3
set_version.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
export version=0.5.3-alpha.9
echo "pub const VERSION: &str = \"${version}\";" > src/version.rs

1
src/bin/dummy.rs Normal file
View File

@@ -0,0 +1 @@
fn main() {}

View File

@@ -0,0 +1,12 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Bot {
#[serde(rename = "_id")]
pub id: String,
pub owner: String,
pub token: String,
pub public: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub interactions_url: Option<String>,
}

View File

@@ -1,12 +1,20 @@
use std::collections::HashMap;
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, to_document};
use rocket_contrib::json::JsonValue;
use crate::util::variables::MAX_GROUP_SIZE;
use futures::StreamExt;
use mongodb::bson::Bson;
use mongodb::{
bson::{doc, to_document, Document},
options::FindOptions,
};
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
#[serde(tag = "channel_type")]
pub enum Channel {
SavedMessages {
#[serde(rename = "_id")]
@@ -16,27 +24,93 @@ pub enum Channel {
DirectMessage {
#[serde(rename = "_id")]
id: String,
active: bool,
recipients: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
},
Group {
#[serde(rename = "_id")]
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<String>,
name: String,
owner: String,
description: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
recipients: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<i32>,
#[serde(skip_serializing_if = "entities::server::if_false", default)]
nsfw: bool
},
TextChannel {
#[serde(rename = "_id")]
id: String,
server: String,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<i32>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
role_permissions: HashMap<String, i32>,
#[serde(skip_serializing_if = "entities::server::if_false", default)]
nsfw: bool
},
VoiceChannel {
#[serde(rename = "_id")]
id: String,
server: String,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<i32>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
role_permissions: HashMap<String, i32>,
#[serde(skip_serializing_if = "entities::server::if_false", default)]
nsfw: bool
},
}
impl Channel {
pub fn id(&self) -> &str {
match self {
Channel::SavedMessages { id, .. } => id,
Channel::DirectMessage { id, .. } => id,
Channel::Group { id, .. } => id,
Channel::SavedMessages { id, .. }
| Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
}
}
pub fn has_messaging(&self) -> Result<()> {
match self {
Channel::SavedMessages { .. }
| Channel::DirectMessage { .. }
| Channel::Group { .. }
| Channel::TextChannel { .. } => Ok(()),
Channel::VoiceChannel { .. } => Err(Error::InvalidOperation)
}
}
@@ -56,27 +130,25 @@ impl Channel {
})?;
let channel_id = self.id().to_string();
ClientboundNotification::ChannelCreate(self)
.publish(channel_id)
.await
.ok();
ClientboundNotification::ChannelCreate(self).publish(channel_id);
Ok(())
}
pub async fn publish_update(&self, partial: JsonValue) -> Result<()> {
pub async fn publish_update(&self, data: Value) -> Result<()> {
let id = self.id().to_string();
ClientboundNotification::ChannelUpdate(partial)
.publish(id)
.await
.ok();
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data,
clear: None,
}
.publish(id);
Ok(())
}
pub async fn delete(&self) -> Result<()> {
let id = self.id();
get_collection("messages")
pub async fn delete_associated_objects(id: Bson) -> Result<()> {
get_collection("channel_invites")
.delete_many(
doc! {
"channel": id
@@ -84,11 +156,167 @@ impl Channel {
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_invites",
})
}
pub async fn delete_messages(id: Bson) -> Result<()> {
let messages = get_collection("messages");
// Delete any unreads.
get_collection("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_unreads",
})?;
// Check if there are any attachments we need to delete.
let message_ids = messages
.find(
doc! {
"channel": &id,
"attachment": {
"$exists": 1
}
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "fetch_many",
with: "messages",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
// If we found any, mark them as deleted.
if message_ids.len() > 0 {
get_collection("attachments")
.update_many(
doc! {
"message_id": {
"$in": message_ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "attachments",
})?;
}
// And then delete said messages.
messages
.delete_many(
doc! {
"channel": id
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "messages",
})?;
})
}
pub async fn delete(&self) -> Result<()> {
let id = self.id();
// Delete any invites.
Channel::delete_associated_objects(Bson::String(id.to_string())).await?;
// Delete messages.
match &self {
Channel::VoiceChannel { .. } => {},
_ => {
Channel::delete_messages(Bson::String(id.to_string())).await?;
}
}
// Remove from server object.
match &self {
Channel::TextChannel { server, .. }
| Channel::VoiceChannel { server, .. } => {
let server = Ref::from_unchecked(server.clone()).fetch_server().await?;
let mut update = doc! {
"$pull": {
"channels": id
}
};
if let Some(sys) = &server.system_messages {
let mut unset = doc! {};
if let Some(cid) = &sys.user_joined {
if id == cid {
unset.insert("system_messages.user_joined", 1);
}
}
if let Some(cid) = &sys.user_left {
if id == cid {
unset.insert("system_messages.user_left", 1);
}
}
if let Some(cid) = &sys.user_kicked {
if id == cid {
unset.insert("system_messages.user_kicked", 1);
}
}
if let Some(cid) = &sys.user_banned {
if id == cid {
unset.insert("system_messages.user_banned", 1);
}
}
if unset.len() > 0 {
update.insert("$unset", unset);
}
}
get_collection("servers")
.update_one(
doc! {
"_id": server.id
},
update,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "servers",
})?;
},
_ => {}
}
// Finally, delete the channel object.
get_collection("channels")
.delete_one(
doc! {
@@ -102,11 +330,63 @@ impl Channel {
with: "channel",
})?;
ClientboundNotification::ChannelDelete { id: id.to_string() }
.publish(id.to_string())
.await
.ok();
ClientboundNotification::ChannelDelete { id: id.to_string() }.publish(id.to_string());
if let Channel::Group { icon, .. } = self {
if let Some(attachment) = icon {
attachment.delete().await?;
}
}
Ok(())
}
pub async fn add_to_group(&self, member: String, by_user: String) -> Result<()> {
if let Channel::Group { id, recipients, .. } = &self {
if recipients.len() >= *MAX_GROUP_SIZE {
Err(Error::GroupTooLarge {
max: *MAX_GROUP_SIZE,
})?
}
if recipients.iter().find(|x| *x == &member).is_some() {
Err(Error::AlreadyInGroup)?
}
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$push": {
"recipients": &member
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
ClientboundNotification::ChannelGroupJoin {
id: id.clone(),
user: member.clone(),
}
.publish(id.clone());
Content::SystemMessage(SystemMessage::UserAdded {
id: member,
by: by_user,
})
.send_as_system(&self)
.await
.ok();
Ok(())
} else {
Err(Error::InvalidOperation)
}
}
}

View File

@@ -1,43 +0,0 @@
// use serde::{Deserialize, Serialize};
/*#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MemberCompositeKey {
pub guild: String,
pub user: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Member {
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
pub nickname: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Invite {
pub code: String,
pub creator: String,
pub channel: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Ban {
pub id: String,
pub reason: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Guild {
#[serde(rename = "_id")]
pub id: String,
// pub nonce: String, used internally
pub name: String,
pub description: String,
pub owner: String,
pub channels: Vec<String>,
pub invites: Vec<Invite>,
pub bans: Vec<Ban>,
pub default_permissions: u32,
}*/

View File

@@ -0,0 +1,96 @@
use mongodb::bson::doc;
use mongodb::bson::from_document;
use mongodb::bson::to_document;
use serde::{Deserialize, Serialize};
use crate::database::get_collection;
use crate::util::result::Error;
use crate::util::result::Result;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Invite {
Server {
#[serde(rename = "_id")]
code: String,
server: String,
creator: String,
channel: String,
},
Group {
#[serde(rename = "_id")]
code: String,
creator: String,
channel: String,
}, /* User {
code: String,
user: String
} */
}
impl Invite {
pub fn code(&self) -> &String {
match &self {
Invite::Server { code, .. } => code,
Invite::Group { code, .. } => code,
}
}
pub fn creator(&self) -> &String {
match &self {
Invite::Server { creator, .. } => creator,
Invite::Group { creator, .. } => creator,
}
}
pub async fn get(code: &str) -> Result<Invite> {
let doc = get_collection("channel_invites")
.find_one(doc! { "_id": code }, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "invite",
})?
.ok_or_else(|| Error::UnknownServer)?;
from_document::<Invite>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "invite",
})
}
pub async fn save(self) -> Result<()> {
get_collection("channel_invites")
.insert_one(
to_document(&self).map_err(|_| Error::DatabaseError {
operation: "to_bson",
with: "invite",
})?,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "invite",
})?;
Ok(())
}
pub async fn delete(&self) -> Result<()> {
get_collection("channel_invites")
.delete_one(
doc! {
"_id": self.code()
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "invite",
})?;
Ok(())
}
}

View File

@@ -1,12 +1,149 @@
use crate::util::variables::{USE_JANUARY, PUBLIC_URL, APP_URL};
use crate::{
database::*,
notifications::events::ClientboundNotification,
notifications::{events::ClientboundNotification, websocket::is_online},
util::result::{Error, Result},
};
use mongodb::bson::{doc, to_bson, DateTime};
use rocket_contrib::json::JsonValue;
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
use std::collections::HashSet;
use std::time::SystemTime;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PushNotification {
pub author: String,
pub icon: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
pub body: String,
pub tag: String,
pub timestamp: u64,
pub url: String,
}
impl PushNotification {
pub async fn new(msg: Message, channel: &Channel) -> Self {
let author = Ref::from_unchecked(msg.author.clone())
.fetch_user()
.await;
let (author, avatar) = if let Ok(author) = author {
(Some(author.username), author.avatar)
} else {
(None, None)
};
let icon = if let Some(avatar) = avatar {
avatar.get_autumn_url()
} else {
format!("{}/users/{}/default_avatar", PUBLIC_URL.as_str(), msg.author)
};
let image = msg.attachments.map_or(None, |attachments| {
attachments
.first()
.map_or(None, |v| Some(v.get_autumn_url()))
});
let body = match msg.content {
Content::Text(body) => body,
Content::SystemMessage(sys_msg) => sys_msg.into()
};
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time should be valid")
.as_secs();
Self {
author: author.unwrap_or_else(|| "Unknown".into()),
icon,
image,
body,
tag: channel.id().to_string(),
timestamp,
url: format!("{}/channel/{}/{}", *APP_URL, channel.id(), msg.id),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum SystemMessage {
#[serde(rename = "text")]
Text { content: String },
#[serde(rename = "user_added")]
UserAdded { id: String, by: String },
#[serde(rename = "user_remove")]
UserRemove { id: String, by: String },
#[serde(rename = "user_joined")]
UserJoined { id: String },
#[serde(rename = "user_left")]
UserLeft { id: String },
#[serde(rename = "user_kicked")]
UserKicked { id: String },
#[serde(rename = "user_banned")]
UserBanned { id: String },
#[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String },
#[serde(rename = "channel_description_changed")]
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
}
impl Into<String> for SystemMessage {
fn into(self) -> String {
match self {
SystemMessage::Text { content } => content,
SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(),
SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(),
SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(),
SystemMessage::UserLeft { .. } => "User left the channel.".to_string(),
SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(),
SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(),
SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(),
SystemMessage::ChannelDescriptionChanged { .. } => "Channel description changed.".to_string(),
SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string()
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Content {
Text(String),
SystemMessage(SystemMessage),
}
impl Content {
pub async fn send_as_system(self, target: &Channel) -> Result<()> {
Message::create(
"00000000000000000000000000".to_string(),
target.id().to_string(),
self,
None,
None,
None
)
.publish(&target, false)
.await
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Validate)]
pub struct Masquerade {
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 32))]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))]
avatar: Option<String>
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
@@ -17,53 +154,147 @@ pub struct Message {
pub channel: String,
pub author: String,
pub content: String,
pub content: Content,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<File>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<DateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>
}
impl Message {
pub fn create(author: String, channel: String, content: String) -> Message {
pub fn create(
author: String,
channel: String,
content: Content,
mentions: Option<Vec<String>>,
replies: Option<Vec<String>>,
masquerade: Option<Masquerade>,
) -> Message {
Message {
id: Ulid::new().to_string(),
nonce: None,
channel,
author,
content,
attachments: None,
edited: None,
embeds: None,
mentions,
replies,
masquerade
}
}
pub async fn publish(self) -> Result<()> {
pub async fn publish(self, channel: &Channel, process_embeds: bool) -> Result<()> {
// Publish message event
ClientboundNotification::Message(self.clone())
.publish(channel.id().to_string());
// Commit message to database
get_collection("messages")
.insert_one(to_bson(&self).unwrap().as_document().unwrap().clone(), None)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "messages",
with: "message",
})?;
let channel = self.channel.clone();
ClientboundNotification::Message(self)
.publish(channel)
.await
.ok();
// spawn task_queue ( process embeds )
if process_embeds {
self.process_embed().await;
}
// spawn task_queue ( update last_message_id )
match channel {
Channel::DirectMessage { id, .. } =>
crate::task_queue::task_last_message_id::queue(id.clone(), self.id.clone(), true).await,
Channel::Group { id, .. } | Channel::TextChannel { id, .. } =>
crate::task_queue::task_last_message_id::queue(id.clone(), self.id.clone(), false).await,
_ => {}
}
// if mentions {
// spawn task_queue ( update channel_unreads )
// }
if let Some(mentions) = &self.mentions {
for user in mentions {
crate::task_queue::task_ack::queue(
channel.id().into(),
user.clone(),
crate::task_queue::task_ack::AckEvent::AddMention {
ids: vec![ self.id.clone() ]
}
).await;
}
}
// if (channel => DM | Group) | mentions {
// spawn task_queue ( web push )
// }
let mut target_ids = vec![];
match &channel {
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
for recipient in recipients {
if !is_online(recipient) {
target_ids.push(recipient.clone());
}
}
}
Channel::TextChannel { .. } => {
if let Some(mentions) = &self.mentions {
target_ids.append(&mut mentions.clone());
}
}
_ => {}
}
if target_ids.len() > 0 {
if let Ok(payload) = serde_json::to_string(&PushNotification::new(self, &channel).await) {
crate::task_queue::task_web_push::queue(target_ids, payload).await;
}
}
Ok(())
}
pub async fn publish_update(&self, partial: JsonValue) -> Result<()> {
pub async fn publish_update(self, data: Value) -> Result<()> {
let channel = self.channel.clone();
ClientboundNotification::MessageUpdate(partial)
.publish(channel)
.await
.ok();
ClientboundNotification::MessageUpdate {
id: self.id.clone(),
channel: self.channel.clone(),
data,
}
.publish(channel);
self.process_embed().await;
Ok(())
}
pub async fn process_embed(&self) {
if !*USE_JANUARY {
return;
}
if let Content::Text(text) = &self.content {
crate::task_queue::task_process_embeds::queue(self.channel.clone(), self.id.clone(), text.clone()).await;
}
}
pub async fn delete(&self) -> Result<()> {
if let Some(attachments) = &self.attachments {
for attachment in attachments {
attachment.delete().await?;
}
}
get_collection("messages")
.delete_one(
doc! {
@@ -80,11 +311,53 @@ impl Message {
let channel = self.channel.clone();
ClientboundNotification::MessageDelete {
id: self.id.clone(),
channel: self.channel.clone(),
}
.publish(channel);
if let Some(attachments) = &self.attachments {
let attachment_ids: Vec<String> =
attachments.iter().map(|f| f.id.to_string()).collect();
get_collection("attachments")
.update_many(
doc! {
"_id": {
"$in": attachment_ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "attachment",
})?;
}
.publish(channel)
.await
.ok();
Ok(())
}
pub fn add_associated_user_ids(&self, ids: &mut HashSet<String>) {
ids.insert(self.author.clone());
if let Content::SystemMessage(sys) = &self.content {
match sys {
SystemMessage::Text { .. } => {},
SystemMessage::UserAdded { id, by } => { ids.insert(id.clone()); ids.insert(by.clone()); },
SystemMessage::UserRemove { id, by } => { ids.insert(id.clone()); ids.insert(by.clone()); },
SystemMessage::UserJoined { id } => { ids.insert(id.clone()); },
SystemMessage::UserLeft { id } => { ids.insert(id.clone()); },
SystemMessage::UserKicked { id } => { ids.insert(id.clone()); },
SystemMessage::UserBanned { id } => { ids.insert(id.clone()); },
SystemMessage::ChannelRenamed { by, .. } => { ids.insert(by.clone()); },
SystemMessage::ChannelDescriptionChanged { by } => { ids.insert(by.clone()); },
SystemMessage::ChannelIconChanged { by } => { ids.insert(by.clone()); }
}
}
}
}

View File

@@ -0,0 +1,122 @@
use mongodb::bson::{doc, from_document};
use serde::{Deserialize, Serialize};
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::AUTUMN_URL;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
enum Metadata {
File,
Text,
Image { width: isize, height: isize },
Video { width: isize, height: isize },
Audio,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct File {
#[serde(rename = "_id")]
pub id: String,
tag: String,
filename: String,
metadata: Metadata,
content_type: String,
size: isize,
#[serde(skip_serializing_if = "Option::is_none")]
deleted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
reported: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
object_id: Option<String>,
}
impl File {
pub async fn find_and_use(
attachment_id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
) -> Result<File> {
let attachments = get_collection("attachments");
let key = format!("{}_id", parent_type);
if let Some(doc) = attachments
.find_one(
doc! {
"_id": attachment_id,
"tag": &tag,
key.clone(): {
"$exists": false
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "attachment",
})?
{
let attachment = from_document::<File>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "attachment",
})?;
attachments
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
key: &parent_id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
Ok(attachment)
} else {
Err(Error::UnknownAttachment)
}
}
pub async fn delete(&self) -> Result<()> {
get_collection("attachments")
.update_one(
doc! {
"_id": &self.id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})
}
pub fn get_autumn_url(&self) -> String {
format!("{}/{}/{}", AUTUMN_URL.as_str(), self.tag, self.id)
}
}

View File

@@ -0,0 +1,182 @@
use crate::util::{
result::{Error, Result},
variables::JANUARY_URL,
variables::MAX_EMBED_COUNT,
};
use linkify::{LinkFinder, LinkKind};
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ImageSize {
Large,
Preview,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Image {
pub url: String,
pub width: isize,
pub height: isize,
pub size: ImageSize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Video {
pub url: String,
pub width: isize,
pub height: isize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum TwitchType {
Channel,
Video,
Clip,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum BandcampType {
Album,
Track,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Special {
None,
YouTube {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
timestamp: Option<String>,
},
Twitch {
content_type: TwitchType,
id: String,
},
Spotify {
content_type: String,
id: String,
},
Soundcloud,
Bandcamp {
content_type: BandcampType,
id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
special: Option<Special>,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<Image>,
#[serde(skip_serializing_if = "Option::is_none")]
video: Option<Video>,
// #[serde(skip_serializing_if = "Option::is_none")]
// opengraph_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
site_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
colour: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Embed {
Website(Metadata),
Image(Image),
None,
}
impl Embed {
pub async fn generate(content: String) -> Result<Vec<Embed>> {
lazy_static! {
static ref RE_CODE: Regex = Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap();
static ref RE_IGNORED: Regex = Regex::new("(<http.+>)").unwrap();
}
// Ignore code blocks.
let content = RE_CODE.replace_all(&content, "");
// Ignore all content between angle brackets starting with http.
let content = RE_IGNORED.replace_all(&content, "");
let content = content
// Ignore quoted lines.
.split("\n")
.map(|v| {
if let Some(c) = v.chars().next() {
if c == '>' {
return "";
}
}
v
})
.collect::<Vec<&str>>()
.join("\n");
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
let links: Vec<_> = finder.links(&content).take(*MAX_EMBED_COUNT).collect();
if links.len() == 0 {
return Err(Error::LabelMe);
}
let mut embeds: Vec<Embed> = Vec::new();
let mut link_index = 0;
// ! FIXME: batch request to january?
while link_index < links.len() {
let link = &links[link_index];
// Check if we already processed this link.
if link_index != 0 && links.iter().take(link_index).any(|x| x.as_str() == link.as_str()) {
link_index = link_index + 1;
continue;
}
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/embed", *JANUARY_URL))
.query(&[("url", link.as_str())])
.send()
.await;
if result.is_err() {
link_index = link_index + 1;
continue;
}
let response = result.unwrap();
if response.status().is_success() {
let res: Embed = response.json().await.map_err(|_| Error::InvalidOperation)?;
embeds.push(res);
}
link_index = link_index + 1;
}
// Prevent database update when no embeds are found.
if embeds.len() > 0 {
Ok(embeds)
} else {
Err(Error::LabelMe)
}
}
}

View File

@@ -0,0 +1,2 @@
pub mod autumn;
pub mod january;

View File

@@ -1,9 +1,20 @@
mod channel;
mod guild;
mod invites;
mod message;
mod microservice;
mod server;
mod sync;
mod user;
mod bots;
use microservice::*;
pub use autumn::*;
pub use channel::*;
pub use guild::*;
pub use invites::*;
pub use january::*;
pub use message::*;
pub use server::*;
pub use sync::*;
pub use user::*;
pub use bots::*;

View File

@@ -0,0 +1,442 @@
use std::collections::HashMap;
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{Bson, doc};
use mongodb::bson::from_document;
use mongodb::bson::to_document;
use mongodb::bson::Document;
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MemberCompositeKey {
pub server: String,
pub user: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Member {
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>
}
pub type PermissionTuple = (
i32, // server permission
i32 // channel permission
);
pub fn if_false(t: &bool) -> bool {
!t
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Role {
pub name: String,
pub permissions: PermissionTuple,
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
#[serde(skip_serializing_if = "if_false", default)]
pub hoist: bool,
#[serde(default)]
pub rank: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Category {
pub id: String,
pub title: String,
pub channels: Vec<String>
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Ban {
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
pub reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SystemMessageChannels {
pub user_joined: Option<String>,
pub user_left: Option<String>,
pub user_kicked: Option<String>,
pub user_banned: Option<String>,
}
pub enum RemoveMember {
Leave,
Kick,
Ban,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Server {
#[serde(rename = "_id")]
pub id: String,
pub owner: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub channels: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<Category>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_messages: Option<SystemMessageChannels>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
pub roles: HashMap<String, Role>,
pub default_permissions: PermissionTuple,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub banner: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
#[serde(skip_serializing_if = "if_false", default)]
pub nsfw: bool
}
impl Server {
pub async fn create(self) -> Result<()> {
get_collection("servers")
.insert_one(
to_document(&self).map_err(|_| Error::DatabaseError {
operation: "to_bson",
with: "channel",
})?,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "server",
})?;
Ok(())
}
pub async fn publish_update(&self, data: Value) -> Result<()> {
ClientboundNotification::ServerUpdate {
id: self.id.clone(),
data,
clear: None,
}
.publish(self.id.clone());
Ok(())
}
pub async fn delete(&self) -> Result<()> {
// Check if there are any attachments we need to delete.
Channel::delete_messages(Bson::Document(doc! { "$in": &self.channels })).await?;
// Delete all channels.
get_collection("channels")
.delete_many(
doc! {
"server": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channels",
})?;
// Delete any associated objects, e.g. unreads and invites.
Channel::delete_associated_objects(Bson::Document(doc! { "$in": &self.channels })).await?;
// Delete members and bans.
for with in &["server_members", "server_bans"] {
get_collection(with)
.delete_many(
doc! {
"_id.server": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with,
})?;
}
// Delete server icon / banner.
if let Some(attachment) = &self.icon {
attachment.delete().await?;
}
if let Some(attachment) = &self.banner {
attachment.delete().await?;
}
// Delete the server
get_collection("servers")
.delete_one(
doc! {
"_id": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "server",
})?;
ClientboundNotification::ServerDelete {
id: self.id.clone(),
}
.publish(self.id.clone());
Ok(())
}
pub async fn fetch_members(id: &str) -> Result<Vec<Member>> {
Ok(get_collection("server_members")
.find(
doc! {
"_id.server": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_members",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| from_document(x).ok())
.collect::<Vec<Member>>())
}
pub async fn fetch_member_ids(id: &str) -> Result<Vec<String>> {
Ok(get_collection("server_members")
.find(
doc! {
"_id.server": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_members",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| {
x.get_document("_id")
.ok()
.map(|i| i.get_str("user").ok().map(|x| x.to_string()))
})
.flatten()
.collect::<Vec<String>>())
}
pub async fn mark_as_read(&self, id: &str) -> Result<()> {
let current_time = Ulid::new().to_string();
let unreads = get_collection("channel_unreads");
unreads.delete_many(
doc! {
"_id.channel": {
"$in": &self.channels
},
"_id.user": &id
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_unreads",
})?;
unreads.insert_many(
self.channels
.iter()
.map(|channel| doc! {
"_id": {
"channel": channel,
"user": &id
},
"last_id": &current_time
})
.collect::<Vec<Document>>(),
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "channel_unreads",
})
.map(|_| ())
}
pub async fn join_member(&self, id: &str) -> Result<()> {
// Check if user is banned.
if get_collection("server_bans")
.find_one(
doc! {
"_id.server": &self.id,
"_id.user": &id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server_bans",
})?
.is_some()
{
return Err(Error::Banned);
}
// Add user to server.
get_collection("server_members")
.insert_one(
doc! {
"_id": {
"server": &self.id,
"user": &id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "server_members",
})?;
// Announce that user joined server.
ClientboundNotification::ServerMemberJoin {
id: self.id.clone(),
user: id.to_string(),
}
.publish(self.id.clone());
// Broadcast join message.
if let Some(channels) = &self.system_messages {
if let Some(cid) = &channels.user_joined {
let channel = Ref::from_unchecked(cid.clone()).fetch_channel().await?;
Content::SystemMessage(SystemMessage::UserJoined { id: id.to_string() })
.send_as_system(&channel)
.await?;
}
}
// Mark entire server as read.
self.mark_as_read(&id).await?;
Ok(())
}
pub async fn remove_member(&self, id: &str, removal: RemoveMember) -> Result<()> {
let result = get_collection("server_members")
.delete_one(
doc! {
"_id": {
"server": &self.id,
"user": &id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "server_members",
})?;
if result.deleted_count > 0 {
ClientboundNotification::ServerMemberLeave {
id: self.id.clone(),
user: id.to_string(),
}
.publish(self.id.clone());
if let Some(channels) = &self.system_messages {
let message = match removal {
RemoveMember::Leave => {
if let Some(cid) = &channels.user_left {
Some((cid.clone(), SystemMessage::UserLeft { id: id.to_string() }))
} else {
None
}
}
RemoveMember::Kick => {
if let Some(cid) = &channels.user_kicked {
Some((
cid.clone(),
SystemMessage::UserKicked { id: id.to_string() },
))
} else {
None
}
}
RemoveMember::Ban => {
if let Some(cid) = &channels.user_banned {
Some((
cid.clone(),
SystemMessage::UserBanned { id: id.to_string() },
))
} else {
None
}
}
};
if let Some((cid, message)) = message {
let channel = Ref::from_unchecked(cid).fetch_channel().await?;
Content::SystemMessage(message)
.send_as_system(&channel)
.await?;
}
}
}
Ok(())
}
pub async fn get_member_count(id: &str) -> Result<i64> {
Ok(get_collection("server_members")
.count_documents(
doc! {
"_id.server": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "count_documents",
with: "server_members",
})?)
}
}

View File

@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub type UserSettings = HashMap<String, (i64, String)>;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ChannelCompositeKey {
pub channel: String,
pub user: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ChannelUnread {
#[serde(rename = "_id")]
pub id: ChannelCompositeKey,
pub last_id: Option<String>,
pub mentions: Option<Vec<String>>,
}

View File

@@ -1,4 +1,22 @@
use futures::StreamExt;
use mongodb::bson::Document;
use mongodb::options::{Collation, FindOneOptions};
use mongodb::{
bson::{doc, from_document},
options::FindOptions,
};
use num_enum::TryFromPrimitive;
use serde::{Deserialize, Serialize};
use std::ops;
use ulid::Ulid;
use validator::Validate;
use crate::database::permissions::user::UserPermissions;
use crate::database::*;
use crate::notifications::websocket::is_online;
use crate::util::result::{Error, Result};
use crate::util::variables::EARLY_ADOPTER_BADGE;
use crate::util::variables::MAX_SERVER_COUNT;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus {
@@ -11,24 +29,309 @@ pub enum RelationshipStatus {
BlockedOther,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Relationship {
#[serde(rename = "_id")]
pub id: String,
pub status: RelationshipStatus,
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum Presence {
Online,
Idle,
Busy,
Invisible,
}
#[derive(Validate, Serialize, Deserialize, Debug, Clone)]
pub struct UserStatus {
#[validate(length(min = 1, max = 128))]
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence: Option<Presence>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserProfile {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<File>,
}
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(i32)]
pub enum Badges {
Developer = 1,
Translator = 2,
Supporter = 4,
ResponsibleDisclosure = 8,
Founder = 16,
PlatformModeration = 32,
ActiveSupporter = 64,
Paw = 128,
EarlyAdopter = 256,
ReservedRelevantJokeBadge1 = 512
}
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BotInformation {
owner: String
}
// When changing this struct, update notifications/payload.rs#113
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct User {
#[serde(rename = "_id")]
pub id: String,
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relations: Option<Vec<Relationship>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub badges: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<UserStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<UserProfile>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bot: Option<BotInformation>,
// ? This should never be pushed to the collection.
#[serde(skip_serializing_if = "Option::is_none")]
pub relationship: Option<RelationshipStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub online: Option<bool>,
}
impl User {
/// Mutate the user object to include relationship as seen by user.
pub fn from(mut self, user: &User) -> User {
self.relationship = Some(RelationshipStatus::None);
if self.id == user.id {
self.relationship = Some(RelationshipStatus::User);
return self;
}
self.relations = None;
if let Some(relations) = &user.relations {
if let Some(relationship) = relations.iter().find(|x| self.id == x.id) {
self.relationship = Some(relationship.status.clone());
return self;
}
}
self
}
/// Apply any relevant badges.
pub fn apply_badges(mut self) -> User {
let mut badges = self.badges.unwrap_or_else(|| 0);
if let Ok(id) = Ulid::from_string(&self.id) {
if id.datetime().timestamp_millis() < *EARLY_ADOPTER_BADGE {
badges = badges + Badges::EarlyAdopter;
}
}
self.badges = Some(badges);
self
}
/// Mutate the user object to appear as seen by user.
pub fn with(self, permissions: UserPermissions<[u32; 1]>) -> User {
let mut user = self.apply_badges();
if permissions.get_view_profile() {
user.online = Some(is_online(&user.id));
} else {
user.status = None;
}
// If the user's status is `Presence::Invisible`, return it as `Presence::Offline`
if let Some(status) = &user.status {
if let Some(presence) = &status.presence {
if presence == &Presence::Invisible {
user.status = None;
user.online = Some(false);
}
}
}
user.profile = None;
user
}
/// Mutate the user object to appear as seen by user.
/// Also overrides the relationship status.
pub async fn from_override(
mut self,
user: &User,
relationship: RelationshipStatus,
) -> Result<User> {
let permissions = PermissionCalculator::new(&user)
.with_relationship(&relationship)
.for_user(&self.id)
.await?;
self.relations = None;
self.relationship = Some(relationship);
Ok(self.with(permissions))
}
/// Utility function for checking claimed usernames.
pub async fn is_username_taken(username: &str) -> Result<bool> {
if username.to_lowercase() == "revolt" || username.to_lowercase() == "admin" || username.to_lowercase() == "system" {
return Ok(true);
}
if get_collection("users")
.find_one(
doc! {
"username": username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.is_some()
{
Ok(true)
} else {
Ok(false)
}
}
/// Utility function for fetching multiple users from the perspective of one.
/// Assumes user has a mutual connection with others.
pub async fn fetch_multiple_users(&self, user_ids: Vec<String>) -> Result<Vec<User>> {
let mut users = vec![];
let mut cursor = get_collection("users")
.find(
doc! {
"_id": {
"$in": user_ids
}
},
FindOptions::builder()
.projection(
doc! { "_id": 1, "username": 1, "avatar": 1, "badges": 1, "status": 1, "flags": 1, "bot": 1 },
)
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let other: User = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "user",
})?;
let permissions = PermissionCalculator::new(&self)
.with_mutual_connection()
.with_user(&other)
.for_user_given()
.await?;
users.push(other.from(&self).with(permissions));
}
}
Ok(users)
}
/// Utility function to get all of a user's memberships.
pub async fn fetch_memberships(id: &str) -> Result<Vec<Member>> {
Ok(get_collection("server_members")
.find(
doc! {
"_id.user": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_members",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| {
from_document(x).ok()
})
.collect::<Vec<Member>>())
}
/// Utility function to get all the server IDs the user is in.
pub async fn fetch_server_ids(id: &str) -> Result<Vec<String>> {
Ok(get_collection("server_members")
.find(
doc! {
"_id.user": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_members",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| {
x.get_document("_id")
.ok()
.map(|i| i.get_str("server").ok().map(|x| x.to_string()))
})
.flatten()
.collect::<Vec<String>>())
}
/// Utility function to fetch unread objects for user.
pub async fn fetch_unreads(id: &str) -> Result<Vec<Document>> {
Ok(get_collection("channel_unreads")
.find(
doc! {
"_id.user": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user_settings",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await)
}
/// Check if this user can acquire another server.
pub async fn can_acquire_server(id: &str) -> Result<bool> {
let server_ids = User::fetch_server_ids(&id).await?;
Ok(server_ids.len() < *MAX_SERVER_COUNT)
}
}

View File

@@ -2,23 +2,29 @@ use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, from_document};
use rocket::http::RawStr;
use rocket::request::FromParam;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Ref {
#[validate(length(min = 26, max = 26))]
#[validate(length(min = 1, max = 26))]
pub id: String,
}
impl Ref {
pub fn from(id: String) -> Result<Ref> {
Ok(Ref { id })
pub fn from_unchecked(id: String) -> Ref {
Ref { id }
}
pub async fn fetch<T: DeserializeOwned>(&self, collection: &'static str) -> Result<T> {
pub fn from(id: String) -> Result<Ref> {
let r = Ref { id };
r.validate()
.map_err(|error| Error::FailedValidation { error })?;
Ok(r)
}
async fn fetch<T: DeserializeOwned>(&self, collection: &'static str) -> Result<T> {
let doc = get_collection(&collection)
.find_one(
doc! {
@@ -31,7 +37,7 @@ impl Ref {
operation: "find_one",
with: &collection,
})?
.ok_or_else(|| Error::UnknownUser)?;
.ok_or_else(|| Error::NotFound)?;
Ok(from_document::<T>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
@@ -47,8 +53,71 @@ impl Ref {
self.fetch("channels").await
}
pub async fn fetch_message(&self) -> Result<Message> {
self.fetch("messages").await
pub async fn fetch_server(&self) -> Result<Server> {
self.fetch("servers").await
}
pub async fn fetch_invite(&self) -> Result<Invite> {
self.fetch("channel_invites").await
}
pub async fn fetch_bot(&self) -> Result<Bot> {
self.fetch("bots").await
}
pub async fn fetch_member(&self, server: &str) -> Result<Member> {
let doc = get_collection("server_members")
.find_one(
doc! {
"_id.user": &self.id,
"_id.server": server
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server_member",
})?
.ok_or_else(|| Error::NotFound)?;
Ok(
from_document::<Member>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "server_member",
})?,
)
}
pub async fn fetch_ban(&self, server: &str) -> Result<Ban> {
let doc = get_collection("server_bans")
.find_one(
doc! {
"_id.user": &self.id,
"_id.server": server
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server_ban",
})?
.ok_or_else(|| Error::NotFound)?;
Ok(from_document::<Ban>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "server_ban",
})?)
}
pub async fn fetch_message(&self, channel: &Channel) -> Result<Message> {
let message: Message = self.fetch("messages").await?;
if &message.channel != channel.id() {
Err(Error::InvalidOperation)
} else {
Ok(message)
}
}
}
@@ -61,9 +130,9 @@ impl User {
}
impl<'r> FromParam<'r> for Ref {
type Error = &'r RawStr;
type Error = &'r str;
fn from_param(param: &'r RawStr) -> Result<Self, Self::Error> {
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
if let Ok(result) = Ref::from(param.to_string()) {
if result.validate().is_ok() {
return Ok(result);

View File

@@ -1,35 +1,80 @@
use crate::database::*;
use mongodb::bson::{doc, from_document};
use rauth::auth::Session;
use rauth::entities::Session;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for User {
impl<'r> FromRequest<'r> for User {
type Error = rauth::util::Error;
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let session: Session = try_outcome!(request.guard::<Session>().await);
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let user: &Option<User> = request.local_cache_async(async {
let header_bot_token = request
.headers()
.get("x-bot-token")
.next()
.map(|x| x.to_string());
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &session.user_id
},
None,
)
.await
{
if let Some(doc) = result {
Outcome::Success(from_document(doc).unwrap())
if let Some(bot_token) = header_bot_token {
if let Ok(result) = get_collection("bots")
.find_one(
doc! {
"token": bot_token
},
None,
)
.await
{
if let Some(doc) = result {
let id = doc.get_str("_id").unwrap();
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &id
},
None,
)
.await
{
if let Some(doc) = result {
if let Ok(user) = from_document(doc) {
return Some(user)
}
}
}
}
}
} else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
if let Outcome::Success(session) = request.guard::<Session>().await {
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &session.user_id
},
None,
)
.await
{
if let Some(doc) = result {
if let Ok(user) = from_document(doc) {
return Some(user)
}
}
}
}
}
None
}).await;
if let Some(user) = user {
Outcome::Success(user.clone())
} else {
Outcome::Failure((
Status::InternalServerError,
rauth::util::Error::DatabaseError,
Status::Forbidden,
rauth::util::Error::InvalidSession,
))
}
}

View File

@@ -9,6 +9,10 @@ pub async fn create_database() {
info!("Creating database.");
let db = get_db();
db.create_collection("accounts", None)
.await
.expect("Failed to create accounts collection.");
db.create_collection("users", None)
.await
.expect("Failed to create users collection.");
@@ -17,22 +21,46 @@ pub async fn create_database() {
.await
.expect("Failed to create channels collection.");
db.create_collection("guilds", None)
.await
.expect("Failed to create guilds collection.");
db.create_collection("members", None)
.await
.expect("Failed to create members collection.");
db.create_collection("messages", None)
.await
.expect("Failed to create messages collection.");
db.create_collection("servers", None)
.await
.expect("Failed to create servers collection.");
db.create_collection("server_members", None)
.await
.expect("Failed to create server_members collection.");
db.create_collection("server_bans", None)
.await
.expect("Failed to create server_bans collection.");
db.create_collection("channel_invites", None)
.await
.expect("Failed to create channel_invites collection.");
db.create_collection("channel_unreads", None)
.await
.expect("Failed to create channel_unreads collection.");
db.create_collection("migrations", None)
.await
.expect("Failed to create migrations collection.");
db.create_collection("attachments", None)
.await
.expect("Failed to create attachments collection.");
db.create_collection("user_settings", None)
.await
.expect("Failed to create user_settings collection.");
db.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
db.create_collection(
"pubsub",
CreateCollectionOptions::builder()
@@ -65,6 +93,79 @@ pub async fn create_database() {
.await
.expect("Failed to create username index.");
db.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
},
{
"key": {
"channel": 1
},
"name": "channel"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"_id.channel": 1,
"_id.user": 1,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create channel_unreads index.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"_id.server": 1,
"_id.user": 1,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create server_members index.");
db.collection("migrations")
.insert_one(
doc! {

View File

@@ -10,10 +10,13 @@ pub async fn run_migrations() {
.list_database_names(None, None)
.await
.expect("Failed to fetch database names.");
if list.iter().position(|x| x == "revolt").is_none() {
init::create_database().await;
} else {
scripts::migrate_database().await;
}
// panic!("https://pbs.twimg.com/media/EDTpB5JWwAUvyxd.jpg");
rauth::entities::sync_models(&super::get_db()).await;
}

View File

@@ -1,9 +1,8 @@
use super::super::{get_collection, get_db};
use crate::database::{permissions, get_collection, get_db, PermissionTuple};
use crate::rocket::futures::StreamExt;
use futures::StreamExt;
use log::info;
use mongodb::bson::{doc, from_document};
use mongodb::options::FindOptions;
use mongodb::{bson::{Document, doc, from_bson, from_document, to_document}, options::FindOptions};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
@@ -12,7 +11,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 3;
pub const LATEST_REVISION: i32 = 12;
pub async fn migrate_database() {
let migrations = get_collection("migrations");
@@ -56,98 +55,397 @@ pub async fn run_migrations(revision: i32) -> i32 {
}
if revision <= 1 {
info!("Running migration [revision 1]: Add channels to guild object.");
info!("Running migration [revision 1 / 2021-04-24]: Migrate to Autumn v1.0.0.");
let col = get_collection("guilds");
let mut guilds = col
.find(
None,
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.expect("Failed to fetch guilds.");
let messages = get_collection("messages");
let attachments = get_collection("attachments");
let mut result = get_collection("channels")
.find(
doc! {
"type": 2
},
FindOptions::builder()
.projection(doc! { "_id": 1, "guild": 1 })
.build(),
)
.await
.expect("Failed to fetch channels.");
let mut channels = vec![];
while let Some(doc) = result.next().await {
let channel = doc.expect("Failed to fetch channel.");
let id = channel
.get_str("_id")
.expect("Failed to get channel id.")
.to_string();
let gid = channel
.get_str("guild")
.expect("Failed to get guild id.")
.to_string();
channels.push((id, gid));
}
while let Some(doc) = guilds.next().await {
let guild = doc.expect("Failed to fetch guild.");
let id = guild.get_str("_id").expect("Failed to get guild id.");
let list: Vec<String> = channels
.iter()
.filter(|x| x.1 == id)
.map(|x| x.0.clone())
.collect();
col.update_one(
doc! {
"_id": id
},
doc! {
"$set": {
"channels": list
}
},
messages
.update_many(
doc! { "attachment": { "$exists": 1 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0 } },
None,
)
.await
.expect("Failed to update guild.");
}
.expect("Failed to update messages.");
attachments
.update_many(
doc! {},
doc! { "$set": { "tag": "attachments", "size": 0 } },
None,
)
.await
.expect("Failed to update attachments.");
}
if revision <= 2 {
info!("Running migration [revision 2]: Add username index to users.");
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
get_db()
.create_collection("servers", None)
.await
.expect("Failed to create servers collection.");
}
if revision <= 3 {
info!("Running migration [revision 3 / 2021-05-25]: Support multiple file uploads, add channel_unreads and user_settings.");
let messages = get_collection("messages");
let mut cursor = messages
.find(
doc! {
"attachment": {
"$exists": 1
}
},
FindOptions::builder()
.projection(doc! {
"_id": 1,
"attachments": [ "$attachment" ]
})
.build(),
)
.await
.expect("Failed to fetch messages.");
while let Some(result) = cursor.next().await {
let doc = result.unwrap();
let id = doc.get_str("_id").unwrap();
let attachments = doc.get_array("attachments").unwrap();
messages
.update_one(
doc! { "_id": id },
doc! { "$unset": { "attachment": 1 }, "$set": { "attachments": attachments } },
None,
)
.await
.unwrap();
}
get_db()
.create_collection("channel_unreads", None)
.await
.expect("Failed to create channel_unreads collection.");
get_db()
.create_collection("user_settings", None)
.await
.expect("Failed to create user_settings collection.");
}
if revision <= 4 {
info!("Running migration [revision 4 / 2021-06-01]: Add more server collections.");
get_db()
.create_collection("server_members", None)
.await
.expect("Failed to create server_members collection.");
get_db()
.create_collection("server_bans", None)
.await
.expect("Failed to create server_bans collection.");
get_db()
.create_collection("channel_invites", None)
.await
.expect("Failed to create channel_invites collection.");
}
if revision <= 5 {
info!("Running migration [revision 5 / 2021-06-26]: Add permissions.");
#[derive(Serialize)]
struct Server {
pub default_permissions: PermissionTuple,
}
let server = Server {
default_permissions: (
*permissions::server::DEFAULT_PERMISSION as i32,
*permissions::channel::DEFAULT_PERMISSION_SERVER as i32
)
};
get_collection("servers")
.update_many(
doc! { },
doc! {
"$set": to_document(&server).unwrap()
},
None
)
.await
.expect("Failed to migrate servers.");
}
if revision <= 6 {
info!("Running migration [revision 6 / 2021-07-09]: Add message text index.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
}
if revision <= 7 {
info!("Running migration [revision 7 / 2021-08-11]: Add message text index.");
get_db()
.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
}
if revision <= 8 {
info!("Running migration [revision 8 / 2021-09-10]: Update to rAuth version 1.");
get_db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1
},
"name": "username",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
}
]
"dropIndexes": "accounts",
"index": ["email", "email_normalised"]
},
None,
)
.await
.expect("Failed to create username index.");
.expect("Failed to delete legacy account indexes.");
let col = get_collection("sessions");
let mut cursor = get_collection("accounts")
.find(doc! { }, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(account) = doc {
let id = account.get_str("_id").unwrap();
if let Some(sessions) = account.get("sessions") {
#[derive(Deserialize)]
struct Session {
id: String,
token: String,
friendly_name: String,
subscription: Option<Document>,
}
let sessions = from_bson::<Vec<Session>>(sessions.clone()).unwrap();
for session in sessions {
info!("Converting session {} to new format.", &session.id);
let mut doc = doc! {
"_id": session.id,
"token": session.token,
"user_id": id.clone(),
"name": session.friendly_name,
};
if let Some(sub) = session.subscription {
doc.insert("subscription", sub);
}
col.insert_one(doc, None).await.ok();
}
} else {
info!("Account doesn't have any sessions!");
}
}
}
get_collection("accounts")
.update_many(
doc! { },
doc! {
"$unset": {
"sessions": 1,
},
"$set": {
"mfa": {
"recovery_codes": []
}
}
},
None
)
.await
.unwrap();
}
if revision <= 9 {
info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id.");
let mut cursor = get_collection("channels")
.find(doc! { }, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(channel) = doc {
let channel_id = channel.get_str("_id").unwrap();
if let Some(last_message) = channel.get("last_message") {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Obj {
#[serde(rename = "_id")]
id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum LastMessage {
Obj(Obj),
Id(String)
}
let lm = from_bson::<LastMessage>(last_message.clone()).unwrap();
let id = match lm {
LastMessage::Obj(Obj { id }) => id,
LastMessage::Id(id) => id
};
info!("Converting session {} to new format.", &channel_id);
get_collection("channels")
.update_one(
doc! {
"_id": channel_id
},
doc! {
"$set": {
"last_message_id": id
},
"$unset": {
"last_message": 1,
}
},
None
)
.await
.unwrap();
} else {
info!("{} has no last_message.", &channel_id);
}
}
}
}
if revision <= 10 {
info!("Running migration [revision 10 / 2021-11-01]: Remove nonce values on channels and servers.");
get_collection("servers")
.update_many(
doc! {},
doc! {
"$unset": {
"nonce": 1,
}
},
None
)
.await
.unwrap();
get_collection("channels")
.update_many(
doc! {},
doc! {
"$unset": {
"nonce": 1,
}
},
None
)
.await
.unwrap();
}
if revision <= 11 {
info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1
},
"name": "channel"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"_id.channel": 1,
"_id.user": 1,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create channel_unreads index.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"_id.server": 1,
"_id.user": 1,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create server_members index.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION
}

View File

@@ -1,60 +1,159 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use super::PermissionCalculator;
use num_enum::TryFromPrimitive;
use std::ops;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum ChannelPermission {
View = 1,
SendMessage = 2,
ManageMessages = 4,
View = 0b00000000000000000000000000000001, // 1
SendMessage = 0b00000000000000000000000000000010, // 2
ManageMessages = 0b00000000000000000000000000000100, // 4
ManageChannel = 0b00000000000000000000000000001000, // 8
VoiceCall = 0b00000000000000000000000000010000, // 16
InviteOthers = 0b00000000000000000000000000100000, // 32
EmbedLinks = 0b00000000000000000000000001000000, // 64
UploadFiles = 0b00000000000000000000000010000000, // 128
Masquerade = 0b00000000000000000000000100000000, // 256
}
lazy_static! {
pub static ref DEFAULT_PERMISSION_DM: u32 =
ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::ManageChannel
+ ChannelPermission::VoiceCall
+ ChannelPermission::InviteOthers
+ ChannelPermission::EmbedLinks
+ ChannelPermission::UploadFiles
+ ChannelPermission::Masquerade;
pub static ref DEFAULT_PERMISSION_SERVER: u32 =
ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::VoiceCall
+ ChannelPermission::InviteOthers
+ ChannelPermission::EmbedLinks
+ ChannelPermission::UploadFiles;
}
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &ChannelPermission| -> u32 { *a | *b as u32 });
bitfield! {
pub struct ChannelPermissions(MSB0 [u32]);
u32;
pub get_view, _: 31;
pub get_send_message, _: 30;
pub get_manage_messages, _: 29;
pub get_manage_channel, _: 28;
pub get_voice_call, _: 27;
pub get_invite_others, _: 26;
pub get_embed_links, _: 25;
pub get_upload_files, _: 24;
pub get_masquerade, _: 23;
}
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &ChannelPermission| -> u32 { *a | *b as u32 });
impl<'a> PermissionCalculator<'a> {
pub async fn calculate_channel(self) -> Result<u32> {
let channel = if let Some(channel) = self.channel {
channel
} else {
unreachable!()
};
pub async fn calculate(user: &User, target: &Channel) -> ChannelPermissions<[u32; 1]> {
match target {
Channel::SavedMessages { user: owner, .. } => {
if &user.id == owner {
ChannelPermissions([ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::ManageMessages])
} else {
ChannelPermissions([0])
}
}
Channel::DirectMessage { recipients, .. } => {
if recipients.iter().find(|x| *x == &user.id).is_some() {
if let Some(recipient) = recipients.iter().find(|x| *x != &user.id) {
let perms = super::user::calculate(&user, recipient).await;
if perms.get_send_message() {
return ChannelPermissions([
ChannelPermission::View + ChannelPermission::SendMessage
]);
}
return ChannelPermissions([ChannelPermission::View as u32]);
match channel {
Channel::SavedMessages { user: owner, .. } => {
if &self.perspective.id == owner {
Ok(u32::MAX)
} else {
Ok(0)
}
}
Channel::DirectMessage { recipients, .. } => {
if recipients
.iter()
.find(|x| *x == &self.perspective.id)
.is_some()
{
if let Some(recipient) = recipients.iter().find(|x| *x != &self.perspective.id)
{
let perms = self.for_user(recipient).await?;
ChannelPermissions([0])
}
Channel::Group { recipients, .. } => {
if recipients.iter().find(|x| *x == &user.id).is_some() {
ChannelPermissions([ChannelPermission::View + ChannelPermission::SendMessage])
} else {
ChannelPermissions([0])
if perms.get_send_message() {
return Ok(*DEFAULT_PERMISSION_DM);
}
return Ok(ChannelPermission::View as u32);
}
}
Ok(0)
}
Channel::Group { recipients, permissions, owner, .. } => {
if &self.perspective.id == owner {
return Ok(*DEFAULT_PERMISSION_DM)
}
if recipients
.iter()
.find(|x| *x == &self.perspective.id)
.is_some()
{
if let Some(permissions) = permissions {
Ok(permissions.clone() as u32)
} else {
Ok(*DEFAULT_PERMISSION_DM)
}
} else {
Ok(0)
}
}
Channel::TextChannel { server, default_permissions, role_permissions, .. }
| Channel::VoiceChannel { server, default_permissions, role_permissions, .. } => {
let server = Ref::from_unchecked(server.clone()).fetch_server().await?;
if self.perspective.id == server.owner {
Ok(u32::MAX)
} else {
match Ref::from_unchecked(self.perspective.id.clone()).fetch_member(&server.id).await {
Ok(member) => {
let mut perm = if let Some(permission) = default_permissions {
*permission as u32
} else {
server.default_permissions.1 as u32
};
if let Some(roles) = member.roles {
for role in roles {
if let Some(permission) = role_permissions.get(&role) {
perm |= *permission as u32;
}
if let Some(server_role) = server.roles.get(&role) {
perm |= server_role.permissions.1 as u32;
}
}
}
Ok(perm)
}
Err(error) => {
match &error {
Error::NotFound => Ok(0),
_ => Err(error)
}
}
}
}
}
}
}
pub async fn for_channel(self) -> Result<ChannelPermissions<[u32; 1]>> {
Ok(ChannelPermissions([self.calculate_channel().await?]))
}
}

View File

@@ -1,4 +1,77 @@
pub use crate::database::*;
pub mod channel;
pub mod server;
pub mod user;
pub use user::get_relationship;
pub struct PermissionCalculator<'a> {
perspective: &'a User,
user: Option<&'a User>,
relationship: Option<&'a RelationshipStatus>,
channel: Option<&'a Channel>,
server: Option<&'a Server>,
// member: Option<&'a Member>,
has_mutual_connection: bool,
}
impl<'a> PermissionCalculator<'a> {
pub fn new(perspective: &'a User) -> PermissionCalculator {
PermissionCalculator {
perspective,
user: None,
relationship: None,
channel: None,
server: None,
// member: None,
has_mutual_connection: false,
}
}
pub fn with_user(self, user: &'a User) -> PermissionCalculator {
PermissionCalculator {
user: Some(&user),
..self
}
}
pub fn with_relationship(self, relationship: &'a RelationshipStatus) -> PermissionCalculator {
PermissionCalculator {
relationship: Some(&relationship),
..self
}
}
pub fn with_channel(self, channel: &'a Channel) -> PermissionCalculator {
PermissionCalculator {
channel: Some(&channel),
..self
}
}
pub fn with_server(self, server: &'a Server) -> PermissionCalculator {
PermissionCalculator {
server: Some(&server),
..self
}
}
/* pub fn with_member(self, member: &'a Member) -> PermissionCalculator {
PermissionCalculator {
member: Some(&member),
..self
}
} */
pub fn with_mutual_connection(self) -> PermissionCalculator<'a> {
PermissionCalculator {
has_mutual_connection: true,
..self
}
}
}

View File

@@ -0,0 +1,89 @@
use crate::util::result::{Error, Result};
use super::PermissionCalculator;
use super::Ref;
use num_enum::TryFromPrimitive;
use std::ops;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum ServerPermission {
View = 0b00000000000000000000000000000001, // 1
ManageRoles = 0b00000000000000000000000000000010, // 2
ManageChannels = 0b00000000000000000000000000000100, // 4
ManageServer = 0b00000000000000000000000000001000, // 8
KickMembers = 0b00000000000000000000000000010000, // 16
BanMembers = 0b00000000000000000000000000100000, // 32
// 6 bits of space
ChangeNickname = 0b00000000000000000001000000000000, // 4096
ManageNicknames = 0b00000000000000000010000000000000, // 8192
ChangeAvatar = 0b00000000000000000100000000000000, // 16382
RemoveAvatars = 0b00000000000000001000000000000000, // 32768
// 16 bits of space
}
lazy_static! {
pub static ref DEFAULT_PERMISSION: u32 =
ServerPermission::View
+ ServerPermission::ChangeNickname
+ ServerPermission::ChangeAvatar;
}
impl_op_ex!(+ |a: &ServerPermission, b: &ServerPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &ServerPermission| -> u32 { *a | *b as u32 });
bitfield! {
pub struct ServerPermissions(MSB0 [u32]);
u32;
pub get_view, _: 31;
pub get_manage_roles, _: 30;
pub get_manage_channels, _: 29;
pub get_manage_server, _: 28;
pub get_kick_members, _: 27;
pub get_ban_members, _: 26;
pub get_change_nickname, _: 19;
pub get_manage_nicknames, _: 18;
pub get_change_avatar, _: 17;
pub get_remove_avatars, _: 16;
}
impl<'a> PermissionCalculator<'a> {
pub async fn calculate_server(self) -> Result<u32> {
let server = if let Some(server) = self.server {
server
} else {
unreachable!()
};
if self.perspective.id == server.owner {
Ok(u32::MAX)
} else {
match Ref::from_unchecked(self.perspective.id.clone()).fetch_member(&server.id).await {
Ok(member) => {
let mut perm = server.default_permissions.0 as u32;
if let Some(roles) = member.roles {
for role in roles {
if let Some(server_role) = server.roles.get(&role) {
perm |= server_role.permissions.0 as u32;
}
}
}
Ok(perm)
}
Err(error) => {
match &error {
Error::NotFound => Ok(0),
_ => Err(error)
}
}
}
}
}
pub async fn for_server(self) -> Result<ServerPermissions<[u32; 1]>> {
Ok(ServerPermissions([self.calculate_server().await?]))
}
}

View File

@@ -1,55 +1,36 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use super::PermissionCalculator;
use mongodb::bson::doc;
use num_enum::TryFromPrimitive;
use std::ops;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum UserPermission {
Access = 1,
SendMessage = 2,
Invite = 4,
Access = 0b00000000000000000000000000000001, // 1
ViewProfile = 0b00000000000000000000000000000010, // 2
SendMessage = 0b00000000000000000000000000000100, // 4
Invite = 0b00000000000000000000000000001000, // 8
}
bitfield! {
pub struct UserPermissions(MSB0 [u32]);
u32;
pub get_access, _: 31;
pub get_send_message, _: 30;
pub get_invite, _: 29;
pub get_view_profile, _: 30;
pub get_send_message, _: 29;
pub get_invite, _: 28;
}
impl_op_ex!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 });
pub async fn calculate(user: &User, target: &str) -> UserPermissions<[u32; 1]> {
// if friends; Access + Message + Invite
// if mutually know each other:
// and has DMs from users enabled -> Access + Message
// otherwise -> Access
// otherwise; None
let mut permissions: u32 = 0;
match get_relationship(&user, &target) {
RelationshipStatus::Friend => {
return UserPermissions([UserPermission::Access
+ UserPermission::SendMessage
+ UserPermission::Invite])
}
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
return UserPermissions([UserPermission::Access as u32])
}
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
permissions = UserPermission::Access as u32;
}
_ => {}
}
UserPermissions([permissions])
}
pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus {
if a.id == b {
return RelationshipStatus::Friend;
return RelationshipStatus::User;
}
if let Some(relations) = &a.relations {
@@ -60,3 +41,91 @@ pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus {
RelationshipStatus::None
}
impl<'a> PermissionCalculator<'a> {
pub async fn calculate_user(self, target: &str) -> Result<u32> {
if &self.perspective.id == target {
return Ok(u32::MAX);
}
let mut permissions: u32 = 0;
match self
.relationship
.clone()
.map(|v| v.to_owned())
.unwrap_or_else(|| get_relationship(&self.perspective, &target))
{
RelationshipStatus::Friend | RelationshipStatus::User => return Ok(u32::MAX),
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
return Ok(UserPermission::Access as u32)
}
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
permissions = UserPermission::Access as u32;
// ! INFO: if we add boolean switch for permission to
// ! message people who have mutual, we need to get
// ! rid of this return statement.
// return Ok(permissions);
}
_ => {}
}
let check_server_overlap = async || {
let server_ids = User::fetch_server_ids(&self.perspective.id).await?;
Ok(
get_collection("server_members")
.find_one(
doc! {
"_id.user": &target,
"_id.server": {
"$in": server_ids
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server_members",
})?
.is_some()
)
};
if self.has_mutual_connection
|| check_server_overlap().await?
|| get_collection("channels")
.find_one(
doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ &self.perspective.id, target ]
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "channels",
})?
.is_some()
{
// ! FIXME: add privacy settings
return Ok(UserPermission::Access + UserPermission::ViewProfile);
}
Ok(permissions)
}
pub async fn for_user(self, target: &str) -> Result<UserPermissions<[u32; 1]>> {
Ok(UserPermissions([self.calculate_user(&target).await?]))
}
pub async fn for_user_given(self) -> Result<UserPermissions<[u32; 1]>> {
let id = &self.user.unwrap().id;
Ok(UserPermissions([self.calculate_user(&id).await?]))
}
}

View File

@@ -1,10 +1,11 @@
#![feature(proc_macro_hygiene, decl_macro)]
#![feature(async_closure)]
#![feature(const_option)]
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;
extern crate serde_json;
#[macro_use]
extern crate lazy_static;
#[macro_use]
@@ -16,23 +17,41 @@ extern crate ctrlc;
pub mod database;
pub mod notifications;
pub mod routes;
pub mod redis;
pub mod util;
pub mod version;
pub mod task_queue;
use async_std::task;
use futures::join;
use log::info;
use rauth;
use rauth::{
config::{Captcha, Config, EmailVerification, SMTPSettings, Template, Templates},
logic::Auth,
};
use std::str::FromStr;
use rocket_cors::AllowedOrigins;
use util::variables::{
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME,
USE_EMAIL, USE_HCAPTCHA,
};
use crate::util::ratelimit::RatelimitState;
#[async_std::main]
async fn main() {
dotenv::dotenv().ok();
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "info"));
info!("Starting REVOLT server.");
info!(
"Starting REVOLT server [version {}].",
crate::version::VERSION
);
util::variables::preflight_checks();
database::connect().await;
redis::connect().await;
notifications::hive::init_hive().await;
task_queue::start_queues().await;
ctrlc::set_handler(move || {
// Force ungraceful exit to avoid hang.
@@ -40,26 +59,84 @@ async fn main() {
})
.expect("Error setting Ctrl-C handler");
let web_task = task::spawn(launch_web());
let hive_task = task::spawn_local(notifications::hive::listen());
join!(
launch_web(),
notifications::websocket::launch_server(),
notifications::hive::listen(),
web_task,
hive_task,
notifications::websocket::launch_server()
);
}
async fn launch_web() {
let cors = rocket_cors::CorsOptions {
allowed_origins: AllowedOrigins::All,
allowed_methods: [
"Get", "Put", "Post", "Delete", "Options", "Head", "Trace", "Connect", "Patch",
]
.iter()
.map(|s| FromStr::from_str(s).unwrap())
.collect(),
..Default::default()
}
.to_cors()
.expect("Failed to create CORS.");
let auth = rauth::auth::Auth::new(database::get_collection("accounts"));
let mut config = Config {
email_verification: if *USE_EMAIL {
EmailVerification::Enabled {
smtp: SMTPSettings {
from: (*SMTP_FROM).to_string(),
host: (*SMTP_HOST).to_string(),
username: (*SMTP_USERNAME).to_string(),
password: (*SMTP_PASSWORD).to_string(),
reply_to: Some("support@revolt.chat".into()),
port: None,
use_tls: None,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("../assets/templates/verify.txt").into(),
url: format!("{}/login/verify/", *APP_URL),
html: None,
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../assets/templates/reset.txt").into(),
url: format!("{}/login/reset/", *APP_URL),
html: None,
},
welcome: None,
},
}
} else {
EmailVerification::Disabled
},
..Default::default()
};
routes::mount(rauth::routes::mount(rocket::ignite(), "/auth", auth))
if *INVITE_ONLY {
config.invite_only = true;
}
if *USE_HCAPTCHA {
config.captcha = Captcha::HCaptcha {
secret: HCAPTCHA_KEY.clone(),
};
}
let auth = Auth::new(database::get_db(), config);
let rocket = rocket::build();
routes::mount(rocket)
.mount("/", rocket_cors::catch_all_options_routes())
.mount("/auth/account", rauth::web::account::routes())
.mount("/auth/session", rauth::web::session::routes())
.manage(auth)
.manage(cors.clone())
.manage(RatelimitState::new())
.attach(cors)
.launch()
.await

View File

@@ -1,51 +1,114 @@
use hive_pubsub::PubSub;
use rauth::auth::Session;
use rocket_contrib::json::JsonValue;
use mongodb::bson::doc;
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use super::hive::{get_hive, subscribe_if_exists};
use crate::database::*;
use crate::{database::*, util::result::Result};
#[derive(Serialize, Deserialize, Debug, Snafu)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "error")]
pub enum WebSocketError {
#[snafu(display("This error has not been labelled."))]
LabelMe,
#[snafu(display("Internal server error."))]
InternalError { at: String },
#[snafu(display("Invalid session."))]
InvalidSession,
#[snafu(display("User hasn't completed onboarding."))]
OnboardingNotFinished,
#[snafu(display("Already authenticated with server."))]
AlreadyAuthenticated,
MalformedData { msg: String },
}
#[derive(Deserialize, Debug)]
pub struct Auth {
pub token: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Ping {
Binary(Vec<u8>),
Number(usize)
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ServerboundNotification {
Authenticate(Session),
Authenticate(Auth),
BeginTyping { channel: String },
EndTyping { channel: String },
Ping { data: Ping, responded: Option<()> },
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveUserField {
ProfileContent,
ProfileBackground,
StatusText,
Avatar,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveChannelField {
Icon,
Description,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveServerField {
Icon,
Banner,
Description,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveRoleField {
Colour,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveMemberField {
Nickname,
Avatar,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveBotField {
InteractionsURL,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum ClientboundNotification {
Error(WebSocketError),
Authenticated,
Ready {
users: Vec<User>,
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
},
Pong { data: Ping },
Message(Message),
MessageUpdate(JsonValue),
MessageUpdate {
id: String,
channel: String,
data: Value,
},
MessageDelete {
id: String,
channel: String,
},
ChannelCreate(Channel),
ChannelUpdate(JsonValue),
ChannelUpdate {
id: String,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveChannelField>,
},
ChannelDelete {
id: String,
},
ChannelGroupJoin {
id: String,
user: String,
@@ -54,29 +117,96 @@ pub enum ClientboundNotification {
id: String,
user: String,
},
ChannelDelete {
id: String,
},
UserRelationship {
ChannelStartTyping {
id: String,
user: String,
},
ChannelStopTyping {
id: String,
user: String,
},
ChannelAck {
id: String,
user: String,
message_id: String,
},
ServerUpdate {
id: String,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveServerField>,
},
ServerDelete {
id: String,
},
ServerMemberUpdate {
id: MemberCompositeKey,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveMemberField>,
},
ServerMemberJoin {
id: String,
user: String,
},
ServerMemberLeave {
id: String,
user: String,
},
ServerRoleUpdate {
id: String,
role_id: String,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveRoleField>,
},
ServerRoleDelete {
id: String,
role_id: String,
},
UserUpdate {
id: String,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveUserField>,
},
UserRelationship {
id: String,
user: User,
status: RelationshipStatus,
},
UserPresence {
UserSettingsUpdate {
id: String,
online: bool,
update: Value,
},
}
impl ClientboundNotification {
pub async fn publish(self, topic: String) -> Result<(), String> {
prehandle_hook(&self); // ! TODO: this should be moved to pubsub
hive_pubsub::backend::mongo::publish(get_hive(), &topic, self).await
pub fn publish(self, topic: String) {
async_std::task::spawn(async move {
prehandle_hook(&self).await.ok(); // ! FIXME: this should be moved to pubsub
hive_pubsub::backend::redis::publish(get_hive(), topic, self)
.await
.ok();
});
}
pub fn publish_as_user(self, user: String) {
self.clone().publish(user.clone());
async_std::task::spawn(async move {
if let Ok(server_ids) = User::fetch_server_ids(&user).await {
for server in server_ids {
self.clone().publish(server.clone());
}
}
});
}
}
pub fn prehandle_hook(notification: &ClientboundNotification) {
pub async fn prehandle_hook(notification: &ClientboundNotification) -> Result<()> {
match &notification {
ClientboundNotification::ChannelGroupJoin { id, user } => {
subscribe_if_exists(user.clone(), id.clone()).ok();
@@ -92,34 +222,58 @@ pub fn prehandle_hook(notification: &ClientboundNotification) {
subscribe_if_exists(recipient.clone(), channel_id.to_string()).ok();
}
}
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
// ! FIXME: write a better algorithm?
let members = Server::fetch_member_ids(server).await?;
for member in members {
subscribe_if_exists(member.clone(), channel_id.to_string()).ok();
}
}
}
}
ClientboundNotification::ChannelGroupLeave { id, user } => {
get_hive()
.hive
.unsubscribe(&user.to_string(), &id.to_string())
.ok();
ClientboundNotification::ServerMemberJoin { id, user } => {
let server = Ref::from_unchecked(id.clone()).fetch_server().await?;
subscribe_if_exists(user.clone(), id.clone()).ok();
for channel in server.channels {
subscribe_if_exists(user.clone(), channel).ok();
}
}
ClientboundNotification::UserRelationship { id, user, status } => {
if status != &RelationshipStatus::None {
subscribe_if_exists(id.clone(), user.clone()).ok();
subscribe_if_exists(id.clone(), user.id.clone()).ok();
}
}
_ => {}
}
Ok(())
}
pub fn posthandle_hook(notification: &ClientboundNotification) {
pub async fn posthandle_hook(notification: &ClientboundNotification) {
match &notification {
ClientboundNotification::ChannelDelete { id } => {
get_hive().hive.drop_topic(&id).ok();
}
ClientboundNotification::ChannelGroupLeave { id, user } => {
get_hive().hive.unsubscribe(user, id).ok();
}
ClientboundNotification::ServerDelete { id } => {
get_hive().hive.drop_topic(&id).ok();
}
ClientboundNotification::ServerMemberLeave { id, user } => {
get_hive().hive.unsubscribe(user, id).ok();
if let Ok(server) = Ref::from_unchecked(id.clone()).fetch_server().await {
for channel in server.channels {
get_hive().hive.unsubscribe(user, &channel).ok();
}
}
}
ClientboundNotification::UserRelationship { id, user, status } => {
if status == &RelationshipStatus::None {
get_hive()
.hive
.unsubscribe(&id.to_string(), &user.to_string())
.ok();
get_hive().hive.unsubscribe(id, &user.id).ok();
}
}
_ => {}

View File

@@ -1,20 +1,28 @@
use std::sync::{Arc, Mutex};
use super::{events::ClientboundNotification, websocket};
use crate::database::*;
use crate::redis::get_pool;
use crate::util::variables::REDIS_URI;
use futures::FutureExt;
use hive_pubsub::backend::mongo::MongodbPubSub;
use hive_pubsub::backend::redis::RedisPubSub;
use hive_pubsub::PubSub;
use log::{debug, error};
use once_cell::sync::OnceCell;
use serde_json::to_string;
type Hive = MongodbPubSub<String, String, ClientboundNotification>;
static HIVE: OnceCell<Hive> = OnceCell::new();
type Hive<'a> = RedisPubSub<'a, String, String, ClientboundNotification>;
static HIVE: OnceCell<Hive<'static>> = OnceCell::new();
pub async fn init_hive() {
let hive = MongodbPubSub::new(
|ids, notification| {
super::events::posthandle_hook(&notification);
let pubsub_con = redis::Client::open(REDIS_URI.to_string()).unwrap().get_async_connection().await.unwrap().into_pubsub();
let hive = RedisPubSub::new(
|ids, notification: ClientboundNotification| {
let notif = notification.clone();
async_std::task::spawn(async move {
super::events::posthandle_hook(&notif).await;
});
if let Ok(data) = to_string(&notification) {
debug!("Pushing out notification. {}", data);
@@ -23,7 +31,8 @@ pub async fn init_hive() {
error!("Failed to serialise notification.");
}
},
get_collection("pubsub"),
get_pool(),
Arc::new(Mutex::new(pubsub_con))
);
if HIVE.set(hive).is_err() {
@@ -34,6 +43,7 @@ pub async fn init_hive() {
pub async fn listen() {
HIVE.get()
.unwrap()
.clone()
.listen()
.fuse()
.await
@@ -58,6 +68,6 @@ pub fn subscribe_if_exists(user: String, topic: String) -> Result<(), String> {
Ok(())
}
pub fn get_hive() -> &'static Hive {
pub fn get_hive() -> &'static Hive<'static> {
HIVE.get().unwrap()
}

View File

@@ -1,62 +1,55 @@
use crate::notifications::events::ClientboundNotification;
use std::collections::HashSet;
use crate::{database::*, notifications::events::ClientboundNotification};
use crate::{
database::{entities::User, get_collection},
util::result::{Error, Result},
};
use futures::StreamExt;
use mongodb::{
bson::{doc, from_document},
options::FindOptions,
};
use super::websocket::is_online;
use mongodb::bson::{doc, from_document};
pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
let mut users = vec![];
let mut user_ids: HashSet<String> = HashSet::new();
if let Some(relationships) = &user.relations {
let user_ids: Vec<String> = relationships
.iter()
.map(|relationship| relationship.id.clone())
.collect();
user_ids.extend(
relationships
.iter()
.map(|relationship| relationship.id.clone()),
);
}
let mut cursor = get_collection("users")
.find(
doc! {
"_id": {
"$in": user_ids
}
},
FindOptions::builder()
.projection(doc! { "_id": 1, "username": 1 })
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
let members = User::fetch_memberships(&user.id).await?;
let server_ids: Vec<String> = members.iter()
.map(|x| x.id.server.clone())
.collect();
let mut cursor = get_collection("servers")
.find(
doc! {
"_id": {
"$in": server_ids
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "servers",
})?;
let mut servers = vec![];
let mut channel_ids = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let server: Server = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "server",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let mut user: User = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "user",
})?;
user.relationship = Some(
relationships
.iter()
.find(|x| user.id == x.id)
.ok_or_else(|| Error::InternalError)?
.status
.clone(),
);
user.online = Some(is_online(&user.id));
users.push(user);
}
channel_ids.extend(server.channels.iter().cloned());
servers.push(server);
}
}
@@ -65,16 +58,20 @@ pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
doc! {
"$or": [
{
"type": "SavedMessages",
"_id": {
"$in": channel_ids
}
},
{
"channel_type": "SavedMessages",
"user": &user.id
},
{
"type": "DirectMessage",
"recipients": &user.id,
"active": true
"channel_type": "DirectMessage",
"recipients": &user.id
},
{
"type": "Group",
"channel_type": "Group",
"recipients": &user.id
}
]
@@ -90,15 +87,38 @@ pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
let mut channels = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
channels.push(from_document(doc).map_err(|_| Error::DatabaseError {
let channel = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "channel",
})?);
})?;
if let Channel::Group { recipients, .. } = &channel {
user_ids.extend(recipients.iter().cloned());
} else if let Channel::DirectMessage { recipients, .. } = &channel {
user_ids.extend(recipients.iter().cloned());
}
channels.push(channel);
}
}
user.online = Some(true);
users.push(user);
user_ids.remove(&user.id);
let mut users = if user_ids.len() > 0 {
user.fetch_multiple_users(user_ids.into_iter().collect::<Vec<String>>())
.await?
} else {
vec![]
};
Ok(ClientboundNotification::Ready { users, channels })
user.relationship = Some(RelationshipStatus::User);
user.online = Some(true);
users.push(user.apply_badges());
Ok(ClientboundNotification::Ready {
users,
servers,
channels,
members
})
}

View File

@@ -4,6 +4,7 @@ use super::hive::get_hive;
use futures::StreamExt;
use hive_pubsub::PubSub;
use mongodb::bson::doc;
use mongodb::bson::Document;
use mongodb::options::FindOptions;
pub async fn generate_subscriptions(user: &User) -> Result<(), String> {
@@ -16,21 +17,57 @@ pub async fn generate_subscriptions(user: &User) -> Result<(), String> {
}
}
let server_ids = User::fetch_server_ids(&user.id)
.await
.map_err(|_| "Failed to fetch memberships.".to_string())?;
let channel_ids = get_collection("servers")
.find(
doc! {
"_id": {
"$in": &server_ids
}
},
None,
)
.await
.map_err(|_| "Failed to fetch servers.".to_string())?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| {
x.get_array("channels").ok().map(|v| {
v.into_iter()
.filter_map(|x| x.as_str().map(|x| x.to_string()))
.collect::<Vec<String>>()
})
})
.flatten()
.collect::<Vec<String>>();
for id in server_ids {
hive.subscribe(user.id.clone(), id)?;
}
for id in channel_ids {
hive.subscribe(user.id.clone(), id)?;
}
let mut cursor = get_collection("channels")
.find(
doc! {
"$or": [
{
"type": "SavedMessages",
"channel_type": "SavedMessages",
"user": &user.id
},
{
"type": "DirectMessage",
"recipients": &user.id,
"active": true
"channel_type": "DirectMessage",
"recipients": &user.id
},
{
"type": "Group",
"channel_type": "Group",
"recipients": &user.id
}
]

View File

@@ -1,21 +1,28 @@
use crate::database::*;
use crate::notifications::events::Ping;
use crate::util::variables::WS_HOST;
use super::subscriptions;
use async_std::net::{TcpListener, TcpStream};
use async_std::task;
use async_tungstenite::tungstenite::Message;
use futures::channel::mpsc::{unbounded, UnboundedSender};
use async_tungstenite::tungstenite::{handshake::server, Message};
use futures::channel::{
mpsc::{unbounded, UnboundedSender},
oneshot,
};
use futures::stream::TryStreamExt;
use futures::{pin_mut, prelude::*};
use hive_pubsub::PubSub;
use log::{debug, info};
use many_to_many::ManyToMany;
use rauth::auth::{Auth, Session};
use mongodb::bson::doc;
use rauth::entities::{Model, Session};
use rmp_serde;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex, RwLock};
use url::Url;
use super::{
events::{ClientboundNotification, ServerboundNotification, WebSocketError},
@@ -23,7 +30,7 @@ use super::{
};
type Tx = UnboundedSender<Message>;
type PeerMap = Arc<Mutex<HashMap<SocketAddr, Tx>>>;
type PeerMap = Arc<Mutex<HashMap<SocketAddr, (Tx, MSGFormat)>>>;
lazy_static! {
static ref CONNECTIONS: PeerMap = Arc::new(Mutex::new(HashMap::new()));
@@ -41,126 +48,271 @@ pub async fn launch_server() {
}
}
#[derive(Debug, Clone)]
enum MSGFormat {
JSON,
MSGPACK,
}
struct HeaderCallback {
sender: oneshot::Sender<MSGFormat>,
}
impl server::Callback for HeaderCallback {
fn on_request(
self,
request: &server::Request,
response: server::Response,
) -> Result<server::Response, server::ErrorResponse> {
// we dont get some of the data sometimes so im generating a fake url with the only data we actually need
let url = format!(
"ws://example.com?{}",
request.uri().query().unwrap_or("?format=json")
);
let mut query: HashMap<_, _> = url
.parse::<Url>()
.unwrap()
.query_pairs()
.into_owned()
.collect(); // should be safe to use unwrap here as we just made the url ourself
let format_query: Option<String> = query.remove("format");
let format = match format_query.as_deref().unwrap_or("json") {
"msgpack" => MSGFormat::MSGPACK,
"json" => MSGFormat::JSON,
_ => MSGFormat::JSON, // Fallback to JSON.
};
if self.sender.send(format).is_ok() {
Ok(response)
} else {
Err(server::ErrorResponse::new(None))
}
}
}
async fn accept(stream: TcpStream) {
let addr = stream
.peer_addr()
.expect("Connected streams should have a peer address.");
let ws_stream = async_tungstenite::accept_async(stream)
.await
.expect("Error during websocket handshake.");
let (sender, receiver) = oneshot::channel::<MSGFormat>();
let ws_stream =
async_tungstenite::accept_hdr_async_with_config(stream, HeaderCallback { sender }, None)
.await
.expect("Error during websocket handshake.");
let msg_format = receiver.await.unwrap(); // TODO: not use unwrap
info!("User established WebSocket connection from {}.", &addr);
let (write, read) = ws_stream.split();
let (tx, rx) = unbounded();
CONNECTIONS.lock().unwrap().insert(addr, tx.clone());
CONNECTIONS.lock().unwrap().insert(addr, (tx.clone(), msg_format.clone()));
let send = |notification: ClientboundNotification| {
if let Ok(response) = serde_json::to_string(&notification) {
if let Err(_) = tx.unbounded_send(Message::Text(response)) {
debug!("Failed unbounded_send to websocket stream.");
let res = match msg_format {
MSGFormat::JSON => match serde_json::to_string(&notification) {
Ok(s) => Message::Text(s),
Err(_) => return
}
MSGFormat::MSGPACK => match rmp_serde::to_vec_named(&notification) {
Ok(v) => Message::Binary(v),
Err(_) => return,
},
};
if let Err(_) = tx.unbounded_send(res) {
debug!("Failed unbounded_send to websocket stream.");
}
};
let session: Arc<Mutex<Option<Session>>> = Arc::new(Mutex::new(None));
let mutex_generator = || session.clone();
let user_id: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let mutex_generator = || user_id.clone();
let fwd = rx.map(Ok).forward(write);
let incoming = read.try_for_each(async move |msg| {
let mutex = mutex_generator();
//dbg!(&mutex.lock().unwrap());
if let Message::Text(text) = msg {
if let Ok(notification) = serde_json::from_str::<ServerboundNotification>(&text) {
match notification {
ServerboundNotification::Authenticate(new_session) => {
{
if mutex.lock().unwrap().is_some() {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
let maybe_decoded = match msg {
Message::Text(text) => {
serde_json::from_str::<ServerboundNotification>(&text).map_err(|e| e.to_string())
}
Message::Binary(vec) => {
rmp_serde::decode::from_read::<&[u8], ServerboundNotification>(vec.as_slice())
.map_err(|e| e.to_string())
}
Message::Ping(vec) => Ok(ServerboundNotification::Ping { data: Ping::Binary(vec), responded: Some(()) }),
_ => return Ok(()),
};
return Ok(());
let notification = match maybe_decoded {
Err(why) => {
send(ClientboundNotification::Error(
WebSocketError::MalformedData {
msg: why.to_string(),
},
));
return Ok(());
}
Ok(n) => n,
};
match notification {
ServerboundNotification::Authenticate(auth) => {
{
if mutex.lock().unwrap().is_some() {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
let id = if let Ok(Some(session)) =
Session::find_one(&get_db(), doc! { "token": &auth.token }, None).await
{
Some(session.user_id)
} else if let Ok(Some(bot)) = get_collection("bots")
.find_one(doc! { "token": auth.token }, None)
.await
{
Some(bot.get_str("_id").unwrap().to_string())
} else {
None
};
if let Some(id) = id {
if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await {
let is_invisible = if let Some(status) = &user.status {
if let Some(presence) = &status.presence {
presence == &Presence::Invisible
} else {
false
}
}
} else {
false
};
let was_online = is_online(&id);
if let Ok(validated_session) = Auth::new(get_collection("accounts"))
.verify_session(new_session)
.await
{
let id = validated_session.user_id.clone();
if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await {
let was_online = is_online(&id);
{
match USERS.write() {
Ok(mut map) => {
map.insert(id.clone(), addr);
}
Err(_) => {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Writing users map.".to_string(),
},
));
return Ok(());
}
}
match USERS.write() {
Ok(mut map) => {
map.insert(id.clone(), addr);
}
*mutex.lock().unwrap() = Some(validated_session);
if let Err(_) = subscriptions::generate_subscriptions(&user).await {
Err(_) => {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Generating subscriptions.".to_string(),
at: "Writing users map.".to_string(),
},
));
return Ok(());
}
send(ClientboundNotification::Authenticated);
match super::payload::generate_ready(user).await {
Ok(payload) => {
send(payload);
if !was_online {
ClientboundNotification::UserPresence {
id: id.clone(),
online: true,
}
.publish(id)
.await
.ok();
}
}
Err(_) => {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Generating payload.".to_string(),
},
));
return Ok(());
}
}
} else {
send(ClientboundNotification::Error(
WebSocketError::OnboardingNotFinished,
));
}
} else {
send(ClientboundNotification::Error(
WebSocketError::InvalidSession,
));
}
*mutex.lock().unwrap() = Some(id.clone());
if let Err(_) = subscriptions::generate_subscriptions(&user).await {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Generating subscriptions.".to_string(),
},
));
return Ok(());
}
send(ClientboundNotification::Authenticated);
match super::payload::generate_ready(user).await {
Ok(payload) => {
send(payload);
if !was_online && !is_invisible {
ClientboundNotification::UserUpdate {
id: id.clone(),
data: json!({
"online": true
}),
clear: None,
}
.publish_as_user(id);
}
}
Err(_) => {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Generating payload.".to_string(),
},
));
return Ok(());
}
}
} else {
send(ClientboundNotification::Error(
WebSocketError::OnboardingNotFinished,
));
}
} else {
send(ClientboundNotification::Error(
WebSocketError::InvalidSession,
));
}
}
// ! TEMP: verify user part of channel
// ! Could just run permission check here.
ServerboundNotification::BeginTyping { channel } => {
if mutex.lock().unwrap().is_some() {
let user = {
let mutex = mutex.lock().unwrap();
mutex.as_ref().unwrap().clone()
};
ClientboundNotification::ChannelStartTyping {
id: channel.clone(),
user,
}
.publish(channel);
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
ServerboundNotification::EndTyping { channel } => {
if mutex.lock().unwrap().is_some() {
let user = {
let mutex = mutex.lock().unwrap();
mutex.as_ref().unwrap().clone()
};
ClientboundNotification::ChannelStopTyping {
id: channel.clone(),
user,
}
.publish(channel);
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
ServerboundNotification::Ping { data, responded } => {
debug!("Ping received from connection {}. Payload: {:?}", &addr, data);
if responded.is_none() {
send(ClientboundNotification::Pong { data });
}
}
}
Ok(())
});
@@ -170,14 +322,29 @@ async fn accept(stream: TcpStream) {
info!("User {} disconnected.", &addr);
CONNECTIONS.lock().unwrap().remove(&addr);
let session = session.lock().unwrap();
if let Some(session) = session.as_ref() {
let mut users = USERS.write().unwrap();
users.remove(&session.user_id, &addr);
if users.get_left(&session.user_id).is_none() {
get_hive().drop_client(&session.user_id).unwrap();
let mut offline = None;
{
let user_id = user_id.lock().unwrap();
if let Some(user_id) = user_id.as_ref() {
let mut users = USERS.write().unwrap();
users.remove(&user_id, &addr);
if users.get_left(&user_id).is_none() {
get_hive().drop_client(&user_id).unwrap();
offline = Some(user_id.clone());
}
}
}
if let Some(id) = offline {
ClientboundNotification::UserUpdate {
id: id.clone(),
data: json!({
"online": false
}),
clear: None,
}
.publish_as_user(id);
}
}
pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
@@ -186,10 +353,15 @@ pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
let users = USERS.read().unwrap();
for id in ids {
// Block certain notifications from reaching users that aren't meant to see them.
if let ClientboundNotification::UserRelationship { id: user_id, .. } = &notification {
if &id != user_id {
continue;
match &notification {
ClientboundNotification::UserRelationship { id: user_id, .. }
| ClientboundNotification::UserSettingsUpdate { id: user_id, .. }
| ClientboundNotification::ChannelAck { user: user_id, .. } => {
if &id != user_id {
continue;
}
}
_ => {}
}
if let Some(mut arr) = users.get_left(&id) {
@@ -198,12 +370,18 @@ pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
}
}
let msg = Message::Text(serde_json::to_string(&notification).unwrap());
let json_msg = Message::Text(serde_json::to_string(&notification).unwrap());
let msgpack_msg = Message::Binary(rmp_serde::to_vec_named(&notification).unwrap());
let connections = CONNECTIONS.lock().unwrap();
for target in targets {
if let Some(conn) = connections.get(&target) {
if let Err(_) = conn.unbounded_send(msg.clone()) {
if let Some((conn, msg_format)) = connections.get(&target) {
let msg = match msg_format {
MSGFormat::JSON => json_msg.clone(),
MSGFormat::MSGPACK => msgpack_msg.clone()
};
if let Err(_) = conn.unbounded_send(msg) {
debug!("Failed unbounded_send.");
}
}

18
src/redis/mod.rs Normal file
View File

@@ -0,0 +1,18 @@
use crate::util::variables::REDIS_URI;
use mobc::Pool;
use mobc_redis::RedisConnectionManager;
use once_cell::sync::OnceCell;
static REDISPOOL: OnceCell<Pool<RedisConnectionManager>> = OnceCell::new();
pub async fn connect() {
let client = mobc_redis::redis::Client::open(REDIS_URI.to_string()).unwrap();
let manager = mobc_redis::RedisConnectionManager::new(client);
let pool = mobc::Pool::builder().max_open(100).build(manager);
REDISPOOL.set(pool).ok().unwrap();
}
pub fn get_pool() -> &'static Pool<RedisConnectionManager> {
REDISPOOL.get().unwrap()
}

87
src/routes/bots/create.rs Normal file
View File

@@ -0,0 +1,87 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_BOT_COUNT;
use crate::util::regex::RE_USERNAME;
use mongodb::bson::{doc, to_document};
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use nanoid::nanoid;
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
name: String,
}
#[post("/create", data = "<info>")]
pub async fn create_bot(user: User, info: Json<Data>) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
if get_collection("bots")
.count_documents(
doc! {
"owner": &user.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "count_documents",
with: "bots",
})? as usize >= *MAX_BOT_COUNT {
return Err(Error::ReachedMaximumBots)
}
let id = Ulid::new().to_string();
let token = nanoid!(64);
let bot = Bot {
id: id.clone(),
owner: user.id.clone(),
token,
public: false,
interactions_url: None
};
if User::is_username_taken(&info.name).await? {
return Err(Error::UsernameTaken);
}
get_collection("users")
.insert_one(
doc! {
"_id": &id,
"username": &info.name,
"bot": {
"owner": &user.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "user",
})?;
get_collection("bots")
.insert_one(
to_document(&bot).map_err(|_| Error::DatabaseError { with: "bot", operation: "to_document" })?,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "user",
})?;
Ok(json!(bot))
}

67
src/routes/bots/delete.rs Normal file
View File

@@ -0,0 +1,67 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, EmptyResponse, Result};
use mongodb::bson::doc;
#[delete("/<target>")]
pub async fn delete_bot(user: User, target: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bot = target.fetch_bot().await?;
if bot.owner != user.id {
return Err(Error::MissingPermission);
}
let username = format!("Deleted User {}", &bot.id);
get_collection("users")
.update_one(
doc! {
"_id": &bot.id
},
doc! {
"$set": {
"username": &username,
"flags": 2
},
"$unset": {
"avatar": 1,
"status": 1,
"profile": 1
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
with: "user",
operation: "update_one"
})?;
ClientboundNotification::UserUpdate {
id: target.id.clone(),
data: json!({
"username": username,
"flags": 2
}),
clear: None,
}
.publish_as_user(target.id.clone());
get_collection("bots")
.delete_one(
doc! {
"_id": &bot.id
},
None
)
.await
.map_err(|_| Error::DatabaseError {
with: "bot",
operation: "delete_one"
})?;
Ok(EmptyResponse {})
}

114
src/routes/bots/edit.rs Normal file
View File

@@ -0,0 +1,114 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveBotField};
use crate::util::regex::RE_USERNAME;
use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
public: Option<bool>,
interactions_url: Option<String>,
remove: Option<RemoveBotField>,
}
#[patch("/<target>", data = "<data>")]
pub async fn edit_bot(user: User, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.name.is_none()
&& data.public.is_none()
&& data.interactions_url.is_none()
&& data.remove.is_none()
{
return Ok(EmptyResponse {});
}
let bot = target.fetch_bot().await?;
if bot.owner != user.id {
return Err(Error::MissingPermission);
}
if let Some(name) = &data.name {
if User::is_username_taken(&name).await? {
return Err(Error::UsernameTaken);
}
get_collection("users")
.update_one(
doc! { "_id": &target.id },
doc! {
"$set": {
"username": name
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
ClientboundNotification::UserUpdate {
id: target.id.clone(),
data: json!({
"username": name
}),
clear: None,
}
.publish_as_user(target.id.clone());
}
let mut set = doc! {};
let mut unset = doc! {};
if let Some(remove) = &data.remove {
match remove {
RemoveBotField::InteractionsURL => {
unset.insert("interactions_url", 1);
}
}
}
if let Some(public) = &data.public {
set.insert("public", public);
}
if let Some(interactions_url) = &data.interactions_url {
set.insert("interactions_url", interactions_url);
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("bots")
.update_one(doc! { "_id": &target.id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "bot",
})?;
}
Ok(EmptyResponse {})
}

26
src/routes/bots/fetch.rs Normal file
View File

@@ -0,0 +1,26 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use serde_json::Value;
#[get("/<target>")]
pub async fn fetch_bot(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bot = target.fetch_bot().await?;
if !bot.public {
if bot.owner != user.id {
return Err(Error::BotIsPrivate);
}
}
let user = Ref::from_unchecked(bot.id.clone()).fetch_user().await?;
Ok(json!({
"bot": bot,
"user": user
}))
}

View File

@@ -0,0 +1,56 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{Document, doc, from_document};
use serde_json::Value;
#[get("/@me")]
pub async fn fetch_owned_bots(user: User) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bots = get_collection("bots")
.find(
doc! {
"owner": &user.id
},
None
)
.await
.map_err(|_| Error::DatabaseError {
with: "bots",
operation: "find"
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| from_document(x).ok())
.collect::<Vec<Bot>>();
let users = get_collection("users")
.find(
doc! {
"bot.owner": &user.id
},
None
)
.await
.map_err(|_| Error::DatabaseError {
with: "users",
operation: "find"
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| from_document(x).ok())
.collect::<Vec<User>>();
Ok(json!({
"bots": bots,
"users": users
}))
}

View File

@@ -0,0 +1,28 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use serde_json::Value;
#[get("/<target>/invite")]
pub async fn fetch_public_bot(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bot = target.fetch_bot().await?;
if !bot.public {
if bot.owner != user.id {
return Err(Error::BotIsPrivate);
}
}
let user = Ref::from_unchecked(bot.id.clone()).fetch_user().await?;
Ok(json!({
"_id": bot.id,
"username": user.username,
"avatar": user.avatar,
"description": user.profile.map(|p| p.content)
}))
}

70
src/routes/bots/invite.rs Normal file
View File

@@ -0,0 +1,70 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use rocket::serde::json::Json;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct ServerId {
server: String
}
#[derive(Deserialize)]
pub struct GroupId {
group: String
}
#[derive(Deserialize)]
#[serde(untagged)]
pub enum Destination {
Server(ServerId),
Group(GroupId)
}
#[post("/<target>/invite", data = "<dest>")]
pub async fn invite_bot(user: User, target: Ref, dest: Json<Destination>) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bot = target.fetch_bot().await?;
if !bot.public {
if bot.owner != user.id {
return Err(Error::BotIsPrivate);
}
}
match dest.into_inner() {
Destination::Server(ServerId { server }) => {
let server = Ref::from(server)?.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if !perm.get_manage_server() {
Err(Error::MissingPermission)?
}
server.join_member(&bot.id).await?;
Ok(EmptyResponse {})
}
Destination::Group(GroupId { group }) => {
let channel = Ref::from(group)?.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_invite_others() {
Err(Error::MissingPermission)?
}
channel.add_to_group(bot.id, user.id).await?;
Ok(EmptyResponse {})
}
}
}

21
src/routes/bots/mod.rs Normal file
View File

@@ -0,0 +1,21 @@
use rocket::Route;
mod create;
mod invite;
mod fetch_public;
mod fetch;
mod fetch_owned;
mod edit;
mod delete;
pub fn routes() -> Vec<Route> {
routes![
create::create_bot,
invite::invite_bot,
fetch_public::fetch_public_bot,
fetch::fetch_bot,
fetch_owned::fetch_owned_bots,
edit::edit_bot,
delete::delete_bot,
]
}

View File

@@ -0,0 +1,38 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
#[put("/<target>/ack/<message>")]
pub async fn req(user: User, target: Ref, message: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
crate::task_queue::task_ack::queue(
target.id().into(),
user.id.clone(),
crate::task_queue::task_ack::AckEvent::AckMessage {
id: message.id.clone()
}
).await;
ClientboundNotification::ChannelAck {
id: target.id().into(),
user: user.id.clone(),
message_id: message.id,
}
.publish(user.id);
Ok(EmptyResponse {})
}

View File

@@ -1,15 +1,19 @@
use crate::util::result::{Error, Result};
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::ClientboundNotification};
use mongodb::bson::doc;
#[delete("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<()> {
pub async fn req(user: User, target: Ref) -> Result<EmptyResponse> {
let target = target.fetch_channel().await?;
let perm = permissions::channel::calculate(&user, &target).await;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
match &target {
@@ -33,7 +37,7 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
with: "channel",
})?;
Ok(())
Ok(EmptyResponse {})
}
Channel::Group {
id,
@@ -66,7 +70,8 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
target.publish_update(json!({ "owner": new_owner })).await?;
} else {
return target.delete().await;
target.delete().await?;
return Ok(EmptyResponse {});
}
} else {
get_collection("channels")
@@ -92,20 +97,23 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
id: id.clone(),
user: user.id.clone(),
}
.publish(id.clone())
.await
.ok();
.publish(id.clone());
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
format!("<@{}> left the group.", user.id),
)
.publish()
.await
.ok();
Content::SystemMessage(SystemMessage::UserLeft { id: user.id })
.send_as_system(&target)
.await
.ok();
Ok(())
Ok(EmptyResponse {})
}
Channel::TextChannel { .. } |
Channel::VoiceChannel { .. } => {
if perm.get_manage_channel() {
target.delete().await?;
Ok(EmptyResponse {})
} else {
Err(Error::MissingPermission)
}
}
}
}

View File

@@ -0,0 +1,160 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveChannelField};
use mongodb::bson::{doc, to_document};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 32))]
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[validate(length(min = 0, max = 1024))]
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
remove: Option<RemoveChannelField>,
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>
}
#[patch("/<target>", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.name.is_none()
&& data.description.is_none()
&& data.icon.is_none()
&& data.remove.is_none()
&& data.nsfw.is_none()
{
return Ok(EmptyResponse {});
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_manage_channel() {
Err(Error::MissingPermission)?
}
match &target {
Channel::Group { id, icon, .. }
| Channel::TextChannel { id, icon, .. }
| Channel::VoiceChannel { id, icon, .. } => {
let mut set = doc! {};
let mut unset = doc! {};
let mut remove_icon = false;
if let Some(remove) = &data.remove {
match remove {
RemoveChannelField::Icon => {
unset.insert("icon", 1);
remove_icon = true;
}
RemoveChannelField::Description => {
unset.insert("description", 1);
}
}
}
if let Some(name) = &data.name {
set.insert("name", name);
}
if let Some(description) = &data.description {
set.insert("description", description);
}
if let Some(attachment_id) = &data.icon {
let attachment =
File::find_and_use(&attachment_id, "icons", "object", target.id()).await?;
set.insert(
"icon",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_icon = true;
}
if let Some(nsfw) = &data.nsfw {
set.insert("nsfw", nsfw);
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("channels")
.update_one(doc! { "_id": &id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!(set),
clear: data.remove,
}
.publish(id.clone());
if let Channel::Group { .. } = &target {
if let Some(name) = data.name {
Content::SystemMessage(SystemMessage::ChannelRenamed {
name,
by: user.id.clone(),
})
.send_as_system(&target)
.await
.ok();
}
if let Some(_) = data.description {
Content::SystemMessage(SystemMessage::ChannelDescriptionChanged {
by: user.id.clone(),
})
.send_as_system(&target)
.await
.ok();
}
if let Some(_) = data.icon {
Content::SystemMessage(SystemMessage::ChannelIconChanged { by: user.id })
.send_as_system(&target)
.await
.ok();
}
}
if remove_icon {
if let Some(old_icon) = icon {
old_icon.delete().await?;
}
}
Ok(EmptyResponse {})
}
_ => Err(Error::InvalidOperation),
}
}

View File

@@ -0,0 +1,20 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket::serde::json::Value;
#[get("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
Ok(json!(target))
}

View File

@@ -1,16 +0,0 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue;
#[get("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let target = target.fetch_channel().await?;
let perm = permissions::channel::calculate(&user, &target).await;
if !perm.get_view() {
Err(Error::LabelMe)?
}
Ok(json!(target))
}

View File

@@ -1,70 +1,24 @@
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_GROUP_SIZE;
use crate::{database::*, notifications::events::ClientboundNotification};
use crate::util::result::{Error, Result, EmptyResponse};
use crate::database::*;
use mongodb::bson::doc;
#[put("/<target>/recipients/<member>")]
pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
pub async fn req(user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
if get_relationship(&user, &member.id) != RelationshipStatus::Friend {
Err(Error::NotFriends)?
}
let channel = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
let perm = permissions::channel::calculate(&user, &channel).await;
if !perm.get_view() {
Err(Error::LabelMe)?
if !perm.get_invite_others() {
Err(Error::MissingPermission)?
}
if let Channel::Group { id, recipients, .. } = channel {
if recipients.len() >= *MAX_GROUP_SIZE {
Err(Error::GroupTooLarge {
max: *MAX_GROUP_SIZE,
})?
}
if recipients.iter().find(|x| *x == &member.id).is_some() {
Err(Error::AlreadyInGroup)?
}
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$push": {
"recipients": &member.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
ClientboundNotification::ChannelGroupJoin {
id: id.clone(),
user: member.id.clone(),
}
.publish(id.clone())
.await
.ok();
Message::create(
"00000000000000000000000000".to_string(),
id,
format!("<@{}> added <@{}> to the group.", user.id, member.id),
)
.publish()
.await
.ok();
Ok(())
} else {
Err(Error::InvalidOperation)
}
channel.add_to_group(member.id, user.id).await?;
Ok(EmptyResponse {})
}

View File

@@ -1,9 +1,10 @@
use crate::database::*;
use crate::util::idempotency::IdempotencyKey;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_GROUP_SIZE;
use mongodb::bson::doc;
use rocket_contrib::json::{Json, JsonValue};
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::iter::FromIterator;
@@ -16,14 +17,18 @@ pub struct Data {
name: String,
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
// Maximum length of 36 allows both ULIDs and UUIDs.
#[validate(length(min = 1, max = 36))]
nonce: String,
users: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>
}
#[post("/create", data = "<info>")]
pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> {
pub async fn req(_idempotency: IdempotencyKey, user: User, info: Json<Data>) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
@@ -37,39 +42,25 @@ pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> {
}
for target in &set {
if get_relationship(&user, target) != RelationshipStatus::Friend {
Err(Error::NotFriends)?
match get_relationship(&user, target) {
RelationshipStatus::Friend | RelationshipStatus::User => {}
_ => {
return Err(Error::NotFriends);
}
}
}
if get_collection("channels")
.find_one(
doc! {
"nonce": &info.nonce
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "channel",
})?
.is_some()
{
Err(Error::DuplicateNonce)?
}
let id = Ulid::new().to_string();
let channel = Channel::Group {
id,
nonce: Some(info.nonce.clone()),
name: info.name.clone(),
description: info
.description
.clone()
.unwrap_or_else(|| "A group.".to_string()),
name: info.name,
description: info.description,
owner: user.id,
recipients: set.into_iter().collect::<Vec<String>>(),
icon: None,
last_message_id: None,
permissions: None,
nsfw: info.nsfw.unwrap_or_default()
};
channel.clone().publish().await?;

View File

@@ -1,10 +1,10 @@
use crate::util::result::{Error, Result};
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::ClientboundNotification};
use mongodb::bson::doc;
#[delete("/<target>/recipients/<member>")]
pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
pub async fn req(user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
if &user.id == &member.id {
Err(Error::CannotRemoveYourself)?
}
@@ -16,11 +16,11 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
owner,
recipients,
..
} = channel
} = &channel
{
if &user.id != &owner {
if &user.id != owner {
// figure out if we want to use perm system here
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
if recipients.iter().find(|x| *x == &member.id).is_none() {
@@ -49,20 +49,17 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
id: id.clone(),
user: member.id.clone(),
}
.publish(id.clone())
.publish(id.clone());
Content::SystemMessage(SystemMessage::UserRemove {
id: member.id,
by: user.id,
})
.send_as_system(&channel)
.await
.ok();
Message::create(
"00000000000000000000000000".to_string(),
id,
format!("<@{}> removed <@{}> from the group.", user.id, member.id),
)
.publish()
.await
.ok();
Ok(())
Ok(EmptyResponse {})
} else {
Err(Error::InvalidOperation)
}

View File

@@ -0,0 +1,48 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use nanoid::nanoid;
use rocket::serde::json::Value;
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'
];
}
#[post("/<target>/invites")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_invite_others() {
return Err(Error::MissingPermission);
}
let code = nanoid!(8, &*ALPHABET);
match &target {
Channel::Group { .. } => {
unimplemented!()
}
Channel::TextChannel { id, server, .. }
| Channel::VoiceChannel { id, server, .. } => {
Invite::Server {
code: code.clone(),
creator: user.id,
server: server.clone(),
channel: id.clone(),
}
.save()
.await?;
Ok(json!({ "code": code }))
}
_ => Err(Error::InvalidOperation),
}
}

View File

@@ -0,0 +1,23 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket::serde::json::Value;
#[get("/<target>/members")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
if let Channel::Group { recipients, .. } = target {
Ok(json!(user.fetch_multiple_users(recipients).await?))
} else {
Err(Error::InvalidOperation)
}
}

View File

@@ -1,18 +1,22 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::result::{Error, Result, EmptyResponse};
use mongodb::bson::doc;
#[delete("/<target>/messages/<msg>")]
pub async fn req(user: User, target: Ref, msg: Ref) -> Result<()> {
pub async fn req(user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let channel = target.fetch_channel().await?;
channel.has_messaging()?;
let perm = permissions::channel::calculate(&user, &channel).await;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let message = msg.fetch_message().await?;
let message = msg.fetch_message(&channel).await?;
if message.author != user.id && !perm.get_manage_messages() {
match channel {
Channel::SavedMessages { .. } => unreachable!(),
@@ -20,5 +24,6 @@ pub async fn req(user: User, target: Ref, msg: Ref) -> Result<()> {
}
}
message.delete().await
message.delete().await?;
Ok(EmptyResponse {})
}

View File

@@ -1,9 +1,9 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::result::{Error, Result, EmptyResponse};
use chrono::Utc;
use mongodb::bson::{doc, Bson, DateTime};
use rocket_contrib::json::Json;
use mongodb::bson::{doc, Bson, DateTime, Document};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -14,33 +14,56 @@ pub struct Data {
}
#[patch("/<target>/messages/<msg>", data = "<edit>")]
pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<()> {
pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<EmptyResponse> {
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
let channel = target.fetch_channel().await?;
channel.has_messaging()?;
let perm = permissions::channel::calculate(&user, &channel).await;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let message = msg.fetch_message().await?;
let mut message = msg.fetch_message(&channel).await?;
if message.author != user.id {
Err(Error::CannotEditMessage)?
}
let edited = Utc::now();
let mut set = doc! {
"content": &edit.content,
"edited": Bson::DateTime(edited)
};
message.content = Content::Text(edit.content.clone());
let mut update = json!({ "content": edit.content, "edited": DateTime(edited) });
if let Some(embeds) = &message.embeds {
let new_embeds: Vec<Document> = vec![];
for embed in embeds {
match embed {
Embed::Website(_) | Embed::Image(_) | Embed::None => {} // Otherwise push to new_embeds.
}
}
let obj = update.as_object_mut().unwrap();
obj.insert("embeds".to_string(), json!(new_embeds));
set.insert("embeds", new_embeds);
}
get_collection("messages")
.update_one(
doc! {
"_id": &message.id
},
doc! {
"$set": {
"content": &edit.content,
"edited": Bson::DateTime(edited)
}
"$set": set
},
None,
)
@@ -50,7 +73,6 @@ pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<
with: "message",
})?;
message
.publish_update(json!({ "content": edit.content, "edited": DateTime(edited) }))
.await
message.publish_update(update).await?;
Ok(EmptyResponse {})
}

View File

@@ -1,17 +1,21 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue;
use rocket::serde::json::Value;
#[get("/<target>/messages/<msg>")]
pub async fn req(user: User, target: Ref, msg: Ref) -> Result<JsonValue> {
pub async fn req(user: User, target: Ref, msg: Ref) -> Result<Value> {
let channel = target.fetch_channel().await?;
channel.has_messaging()?;
let perm = permissions::channel::calculate(&user, &channel).await;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let message = msg.fetch_message().await?;
let message = msg.fetch_message(&channel).await?;
Ok(json!(message))
}

View File

@@ -1,16 +1,23 @@
use std::collections::HashSet;
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use futures::{StreamExt, try_join};
use mongodb::{
bson::{doc, from_document},
options::FindOptions,
};
use rocket::request::Form;
use rocket_contrib::json::JsonValue;
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Serialize, Deserialize, FromFormField)]
pub enum Sort {
Latest,
Oldest,
}
#[derive(Validate, Serialize, Deserialize, FromForm)]
pub struct Options {
#[validate(range(min = 1, max = 100))]
@@ -19,58 +26,161 @@ pub struct Options {
before: Option<String>,
#[validate(length(min = 26, max = 26))]
after: Option<String>,
sort: Option<Sort>,
// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
// It will also take half of limit rounded as the limits to each side.
// It also fetches the message ID specified.
#[validate(length(min = 26, max = 26))]
nearby: Option<String>,
include_users: Option<bool>,
}
#[get("/<target>/messages?<options..>")]
pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<JsonValue> {
pub async fn req(user: User, target: Ref, options: Options) -> Result<Value> {
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_channel().await?;
target.has_messaging()?;
let perm = permissions::channel::calculate(&user, &target).await;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let mut query = doc! { "channel": target.id() };
let mut messages = vec![];
if let Some(before) = &options.before {
query.insert("_id", doc! { "$lt": before });
}
if let Some(after) = &options.after {
query.insert("_id", doc! { "$gt": after });
}
let mut cursor = get_collection("messages")
.find(
query,
FindOptions::builder()
.limit(options.limit.unwrap_or(50))
.sort(doc! {
"_id": -1
})
.build(),
let collection = get_collection("messages");
let limit = options.limit.unwrap_or(50);
let channel = target.id();
if let Some(nearby) = &options.nearby {
let mut cursors = try_join!(
collection.find(
doc! {
"channel": channel,
"_id": {
"$gte": &nearby
}
},
FindOptions::builder()
.limit(limit / 2 + 1)
.sort(doc! {
"_id": 1
})
.build(),
),
collection.find(
doc! {
"channel": channel,
"_id": {
"$lt": &nearby
}
},
FindOptions::builder()
.limit(limit / 2)
.sort(doc! {
"_id": -1
})
.build(),
)
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
let mut messages = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
while let Some(result) = cursors.0.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
while let Some(result) = cursors.1.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
} else {
let mut query = doc! { "channel": target.id() };
if let Some(before) = &options.before {
query.insert("_id", doc! { "$lt": before });
}
if let Some(after) = &options.after {
query.insert("_id", doc! { "$gt": after });
}
let sort: i32 = if let Sort::Latest = options.sort.as_ref().unwrap_or_else(|| &Sort::Latest) {
-1
} else {
1
};
let mut cursor = collection
.find(
query,
FindOptions::builder()
.limit(limit)
.sort(doc! {
"_id": sort
})
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
}
Ok(json!(messages))
if options.include_users.unwrap_or_else(|| false) {
let mut ids = HashSet::new();
for message in &messages {
message.add_associated_user_ids(&mut ids);
}
ids.remove(&user.id);
let user_ids = ids.into_iter().collect();
let users = user.fetch_multiple_users(user_ids).await?;
if let Channel::TextChannel { server, .. } = target {
Ok(json!({
"messages": messages,
"users": users,
"members": Server::fetch_members(&server).await?
}))
} else {
Ok(json!({
"messages": messages,
"users": users,
}))
}
} else {
Ok(json!(messages))
}
}

View File

@@ -0,0 +1,74 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, from_document};
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Options {
ids: Vec<String>,
}
#[post("/<target>/messages/stale", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<Value> {
if data.ids.len() > 150 {
return Err(Error::TooManyIds);
}
let target = target.fetch_channel().await?;
target.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let mut cursor = get_collection("messages")
.find(
doc! {
"_id": {
"$in": &data.ids
},
"channel": target.id()
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
let mut updated = vec![];
let mut found_ids = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let msg = from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?;
found_ids.push(msg.id.clone());
if msg.edited.is_some() {
updated.push(msg);
}
}
}
let mut deleted = vec![];
for id in &data.ids {
if found_ids.iter().find(|x| *x == id).is_none() {
deleted.push(id);
}
}
Ok(json!({
"updated": updated,
"deleted": deleted
}))
}

View File

@@ -0,0 +1,162 @@
use std::collections::HashSet;
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::{
bson::{doc, from_document},
options::FindOptions,
};
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Serialize, Deserialize, FromFormField)]
pub enum Sort {
Relevance,
Latest,
Oldest,
}
impl Default for Sort {
fn default() -> Sort {
Sort::Relevance
}
}
#[derive(Validate, Serialize, Deserialize, FromForm)]
pub struct Options {
#[validate(length(min = 1, max = 64))]
query: String,
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
#[validate(length(min = 26, max = 26))]
before: Option<String>,
#[validate(length(min = 26, max = 26))]
after: Option<String>,
#[serde(default = "Sort::default")]
sort: Sort,
include_users: Option<bool>,
}
#[post("/<target>/search", data = "<options>")]
pub async fn req(user: User, target: Ref, options: Json<Options>) -> Result<Value> {
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_channel().await?;
target.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let mut messages = vec![];
let limit = options.limit.unwrap_or(50);
let mut filter = doc! {
"channel": target.id(),
"$text": {
"$search": &options.query
}
};
if let Some(doc) = match (&options.before, &options.after) {
(Some(before), Some(after)) => Some(doc! {
"lt": before,
"gt": after
}),
(Some(before), _) => Some(doc! {
"lt": before
}),
(_, Some(after)) => Some(doc! {
"gt": after
}),
_ => None
} {
filter.insert("_id", doc);
}
let mut cursor = get_collection("messages")
.find(
filter,
FindOptions::builder()
.projection(
if let Sort::Relevance = &options.sort {
doc! {
"score": {
"$meta": "textScore"
}
}
} else {
doc! {}
}
)
.limit(limit)
.sort(
match &options.sort {
Sort::Relevance => doc! {
"score": {
"$meta": "textScore"
}
},
Sort::Latest => doc! {
"_id": -1
},
Sort::Oldest => doc! {
"_id": 1
}
}
)
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
if options.include_users.unwrap_or_else(|| false) {
let mut ids = HashSet::new();
for message in &messages {
message.add_associated_user_ids(&mut ids);
}
ids.remove(&user.id);
let user_ids = ids.into_iter().collect();
let users = user.fetch_multiple_users(user_ids).await?;
if let Channel::TextChannel { server, .. } = target {
Ok(json!({
"messages": messages,
"users": users,
"members": Server::fetch_members(&server).await?
}))
} else {
Ok(json!({
"messages": messages,
"users": users,
}))
}
} else {
Ok(json!(messages))
}
}

View File

@@ -1,62 +1,143 @@
use std::collections::HashSet;
use crate::database::*;
use crate::util::idempotency::IdempotencyKey;
use crate::util::ratelimit::{Ratelimiter, RatelimitResponse};
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket_contrib::json::{Json, JsonValue};
use regex::Regex;
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
#[derive(Serialize, Deserialize)]
pub struct Reply {
id: String,
mention: bool
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 2000))]
#[validate(length(min = 0, max = 2000))]
content: String,
// Maximum length of 36 allows both ULIDs and UUIDs.
#[validate(length(min = 1, max = 36))]
nonce: String,
#[validate(length(min = 1, max = 128))]
attachments: Option<Vec<String>>,
nonce: Option<String>,
replies: Option<Vec<Reply>>,
#[validate]
masquerade: Option<Masquerade>
}
lazy_static! {
// ignoring I L O and U is intentional
static ref RE_MENTION: Regex = Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap();
}
#[post("/<target>/messages", data = "<message>")]
pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonValue> {
pub async fn message_send(user: User, _r: Ratelimiter, mut idempotency: IdempotencyKey, target: Ref, message: Json<Data>) -> Result<RatelimitResponse<Value>> {
let message = message.into_inner();
idempotency.consume_nonce(message.nonce.clone());
message
.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_channel().await?;
let perm = permissions::channel::calculate(&user, &target).await;
if !perm.get_send_message() {
Err(Error::LabelMe)?
if message.content.len() == 0
&& (message.attachments.is_none() || message.attachments.as_ref().unwrap().len() == 0)
{
return Err(Error::EmptyMessage);
}
if get_collection("messages")
.find_one(
doc! {
"nonce": &message.nonce
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "message",
})?
.is_some()
{
Err(Error::DuplicateNonce)?
let target = target.fetch_channel().await?;
target.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_send_message() {
return Err(Error::MissingPermission)
}
let mut mentions = HashSet::new();
for capture in RE_MENTION.captures_iter(&message.content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
}
if let Some(_) = &message.masquerade {
if !perm.get_masquerade() {
return Err(Error::MissingPermission)
}
}
let mut replies = HashSet::new();
if let Some(entries) = message.replies {
// ! FIXME: move this to app config
if entries.len() > 5 {
return Err(Error::TooManyReplies)
}
for Reply { id, mention } in entries {
let message = Ref::from_unchecked(id)
.fetch_message(&target)
.await?;
replies.insert(message.id);
if mention {
mentions.insert(message.author);
}
}
}
let id = Ulid::new().to_string();
let mut attachments = vec![];
if let Some(ids) = &message.attachments {
if ids.len() > 0 && !perm.get_upload_files() {
return Err(Error::MissingPermission)
}
// ! FIXME: move this to app config
if ids.len() > 5 {
return Err(Error::TooManyAttachments)
}
for attachment_id in ids {
attachments
.push(File::find_and_use(attachment_id, "attachments", "message", &id).await?);
}
}
let msg = Message {
id: Ulid::new().to_string(),
id,
channel: target.id().to_string(),
author: user.id,
content: message.content.clone(),
nonce: Some(message.nonce.clone()),
content: Content::Text(message.content.clone()),
nonce: Some(idempotency.key),
edited: None,
embeds: None,
attachments: if attachments.len() > 0 { Some(attachments) } else { None },
mentions: if mentions.len() > 0 {
Some(mentions.into_iter().collect::<Vec<String>>())
} else {
None
},
replies: if replies.len() > 0 {
Some(replies.into_iter().collect::<Vec<String>>())
} else {
None
},
masquerade: message.masquerade
};
msg.clone().publish().await?;
Ok(json!(msg))
msg.clone().publish(&target, perm.get_embed_links()).await?;
Ok(RatelimitResponse(json!(msg)))
}

View File

@@ -1,27 +1,45 @@
use rocket::Route;
mod delete_channel;
mod fetch_channel;
mod channel_ack;
mod channel_delete;
mod channel_edit;
mod channel_fetch;
mod members_fetch;
mod group_add_member;
mod group_create;
mod group_remove_member;
mod invite_create;
mod voice_join;
mod message_delete;
mod message_edit;
mod message_fetch;
mod message_query;
mod message_search;
mod message_query_stale;
mod message_send;
mod permissions_set;
mod permissions_set_default;
pub fn routes() -> Vec<Route> {
routes![
fetch_channel::req,
delete_channel::req,
message_send::req,
channel_ack::req,
channel_fetch::req,
members_fetch::req,
channel_delete::req,
channel_edit::req,
invite_create::req,
message_send::message_send,
message_query::req,
message_search::req,
message_query_stale::req,
message_fetch::req,
message_edit::req,
message_delete::req,
group_create::req,
group_add_member::req,
group_remove_member::req
group_remove_member::req,
voice_join::req,
permissions_set::req,
permissions_set_default::req,
]
}

View File

@@ -0,0 +1,69 @@
use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use validator::Contains;
use crate::database::*;
use crate::database::permissions::channel::ChannelPermission;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
#[derive(Serialize, Deserialize)]
pub struct Data {
permissions: u32
}
#[put("/<target>/permissions/<role>", data = "<data>", rank = 2)]
pub async fn req(user: User, target: Ref, role: String, data: Json<Data>) -> Result<EmptyResponse> {
let target = target.fetch_channel().await?;
match target {
Channel::TextChannel { id, server, mut role_permissions, .. }
| Channel::VoiceChannel { id, server, mut role_permissions, .. } => {
let target = Ref::from_unchecked(server).fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
return Err(Error::MissingPermission);
}
if !target.roles.has_element(&role) {
return Err(Error::NotFound);
}
let permissions: u32 = ChannelPermission::View as u32 | data.permissions;
get_collection("channels")
.update_one(
doc! { "_id": &id },
doc! {
"$set": {
"role_permissions.".to_owned() + &role: permissions as i32
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel"
})?;
role_permissions.insert(role, permissions as i32);
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!({
"role_permissions": role_permissions
}),
clear: None
}
.publish(id);
Ok(EmptyResponse {})
}
_ => Err(Error::InvalidOperation)
}
}

View File

@@ -0,0 +1,97 @@
use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use crate::database::*;
use crate::database::permissions::channel::{ ChannelPermission, DEFAULT_PERMISSION_DM };
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
#[derive(Serialize, Deserialize)]
pub struct Data {
permissions: u32
}
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
let target = target.fetch_channel().await?;
match target {
Channel::Group { id, owner, .. } => {
if user.id == owner {
let permissions: u32 = ChannelPermission::View as u32 | (data.permissions & *DEFAULT_PERMISSION_DM);
get_collection("channels")
.update_one(
doc! { "_id": &id },
doc! {
"$set": {
"permissions": permissions as i32
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel"
})?;
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!({
"permissions": permissions as i32
}),
clear: None
}
.publish(id);
Ok(EmptyResponse {})
} else {
Err(Error::MissingPermission)
}
}
Channel::TextChannel { id, server, .. }
| Channel::VoiceChannel { id, server, .. } => {
let target = Ref::from_unchecked(server).fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
return Err(Error::MissingPermission);
}
let permissions: u32 = ChannelPermission::View as u32 | data.permissions;
get_collection("channels")
.update_one(
doc! { "_id": &id },
doc! {
"$set": {
"default_permissions": permissions as i32
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel"
})?;
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!({
"default_permissions": permissions as i32
}),
clear: None
}
.publish(id);
Ok(EmptyResponse {})
}
_ => Err(Error::InvalidOperation)
}
}

View File

@@ -0,0 +1,91 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL};
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct CreateUserResponse {
token: String,
}
#[post("/<target>/join_call")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
if !*USE_VOSO {
return Err(Error::VosoUnavailable);
}
let target = target.fetch_channel().await?;
match target {
Channel::SavedMessages { .. } | Channel::TextChannel { .. } => {
return Err(Error::CannotJoinCall)
}
_ => {}
}
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_voice_call() {
return Err(Error::MissingPermission);
}
// To join a call:
// - Check if the room exists.
// - If not, create it.
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/room/{}", *VOSO_URL, target.id()))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await;
match result {
Err(_) => return Err(Error::VosoUnavailable),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => (),
reqwest::StatusCode::NOT_FOUND => {
if let Err(_) = client
.post(&format!("{}/room/{}", *VOSO_URL, target.id()))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await
{
return Err(Error::VosoUnavailable);
}
}
_ => return Err(Error::VosoUnavailable),
},
}
// Then create a user for the room.
if let Ok(response) = client
.post(&format!(
"{}/room/{}/user/{}",
*VOSO_URL,
target.id(),
user.id
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await
{
let res: CreateUserResponse = response.json().await.map_err(|_| Error::InvalidOperation)?;
Ok(json!(res))
} else {
Err(Error::VosoUnavailable)
}
}

View File

@@ -1,5 +0,0 @@
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![]
}

View File

@@ -0,0 +1,30 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
#[delete("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<EmptyResponse> {
let target = target.fetch_invite().await?;
if target.creator() == &user.id {
target.delete().await?;
} else {
match &target {
Invite::Server { server, .. } => {
let server = Ref::from_unchecked(server.clone()).fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if !perm.get_manage_server() {
return Err(Error::MissingPermission);
}
target.delete().await?;
}
_ => unreachable!(),
}
}
Ok(EmptyResponse {})
}

View File

@@ -0,0 +1,62 @@
use crate::database::*;
use crate::util::result::Result;
use rocket::serde::json::Value;
use serde::Serialize;
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum InviteResponse {
Server {
server_id: String,
server_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
server_icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
server_banner: Option<File>,
channel_id: String,
channel_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
user_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
member_count: i64,
},
}
#[get("/<target>")]
pub async fn req(target: Ref) -> Result<Value> {
let target = target.fetch_invite().await?;
match target {
Invite::Server {
channel, creator, ..
} => {
let channel = Ref::from_unchecked(channel).fetch_channel().await?;
let creator = Ref::from_unchecked(creator).fetch_user().await?;
match channel {
Channel::TextChannel { id, server, name, description, .. }
| Channel::VoiceChannel { id, server, name, description, .. } => {
let server = Ref::from_unchecked(server).fetch_server().await?;
Ok(json!(InviteResponse::Server {
member_count: Server::get_member_count(&server.id).await?,
server_id: server.id,
server_name: server.name,
server_icon: server.icon,
server_banner: server.banner,
channel_id: id,
channel_name: name,
channel_description: description,
user_name: creator.username,
user_avatar: creator.avatar
}))
}
_ => unreachable!()
}
}
_ => unreachable!(),
}
}

View File

@@ -0,0 +1,42 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_SERVER_COUNT;
use rocket::serde::json::Value;
#[post("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
if !User::can_acquire_server(&user.id).await? {
Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
})?
}
let target = target.fetch_invite().await?;
match target {
Invite::Server { channel, .. } => {
let channel = Ref::from_unchecked(channel).fetch_channel().await?;
let server = match &channel {
Channel::TextChannel { server, .. }
| Channel::VoiceChannel { server, .. } => {
Ref::from_unchecked(server.clone()).fetch_server().await?
}
_ => unreachable!()
};
server.join_member(&user.id).await?;
Ok(json!({
"type": "Server",
"channel": channel,
"server": server
}))
}
_ => unreachable!(),
}
}

View File

@@ -0,0 +1,9 @@
use rocket::Route;
mod invite_delete;
mod invite_fetch;
mod invite_join;
pub fn routes() -> Vec<Route> {
routes![invite_fetch::req, invite_join::req, invite_delete::req]
}

View File

@@ -1,18 +1,26 @@
pub use rocket::http::Status;
pub use rocket::response::Redirect;
use rocket::Rocket;
use rocket::{Build, Rocket};
mod bots;
mod channels;
mod guild;
mod invites;
mod onboard;
mod push;
mod root;
mod servers;
mod sync;
mod users;
pub fn mount(rocket: Rocket) -> Rocket {
pub fn mount(rocket: Rocket<Build>) -> Rocket<Build> {
rocket
.mount("/", routes![root::root])
.mount("/", routes![root::root, root::ping])
.mount("/onboard", onboard::routes())
.mount("/users", users::routes())
.mount("/channels", channels::routes())
.mount("/guild", guild::routes())
.mount("/servers", servers::routes())
.mount("/bots", bots::routes())
.mount("/invites", invites::routes())
.mount("/push", push::routes())
.mount("/sync", sync::routes())
}

View File

@@ -1,18 +1,13 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::result::{Error, Result, EmptyResponse};
use crate::util::regex::RE_USERNAME;
use mongodb::bson::doc;
use mongodb::options::{Collation, FindOneOptions};
use rauth::auth::Session;
use regex::Regex;
use rocket_contrib::json::Json;
use rauth::entities::Session;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_]+$").unwrap();
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
@@ -20,7 +15,7 @@ pub struct Data {
}
#[post("/complete", data = "<data>")]
pub async fn req(session: Session, user: Option<User>, data: Json<Data>) -> Result<()> {
pub async fn req(session: Session, user: Option<User>, data: Json<Data>) -> Result<EmptyResponse> {
if user.is_some() {
Err(Error::AlreadyOnboarded)?
}
@@ -28,38 +23,23 @@ pub async fn req(session: Session, user: Option<User>, data: Json<Data>) -> Resu
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let col = get_collection("users");
if col
.find_one(
if User::is_username_taken(&data.username).await? {
return Err(Error::UsernameTaken);
}
get_collection("users")
.insert_one(
doc! {
"_id": session.user_id,
"username": &data.username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
operation: "insert_one",
with: "user",
})?
.is_some()
{
Err(Error::UsernameTaken)?
}
})?;
col.insert_one(
doc! {
"_id": session.user_id,
"username": &data.username
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "user",
})?;
Ok(())
Ok(EmptyResponse {})
}

View File

@@ -1,10 +1,10 @@
use crate::database::*;
use rauth::auth::Session;
use rocket_contrib::json::JsonValue;
use rauth::entities::Session;
use rocket::serde::json::Value;
#[get("/hello")]
pub async fn req(_session: Session, user: Option<User>) -> JsonValue {
pub async fn req(_session: Session, user: Option<User>) -> Value {
json!({
"onboarding": user.is_none()
})

8
src/routes/push/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
use rocket::Route;
mod subscribe;
mod unsubscribe;
pub fn routes() -> Vec<Route> {
routes![subscribe::req, unsubscribe::req]
}

View File

@@ -0,0 +1,19 @@
use crate::database::*;
use crate::util::result::{EmptyResponse, Error, Result};
use mongodb::bson::doc;
use rauth::entities::{Model, Session, WebPushSubscription};
use rocket::serde::json::Json;
#[post("/subscribe", data = "<data>")]
pub async fn req(mut session: Session, data: Json<WebPushSubscription>) -> Result<EmptyResponse> {
session.subscription = Some(data.into_inner());
session
.save(&get_db(), None)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
operation: "save",
with: "session",
})
}

View File

@@ -0,0 +1,18 @@
use crate::database::*;
use crate::util::result::{EmptyResponse, Error, Result};
use mongodb::bson::doc;
use rauth::entities::{Model, Session};
#[post("/unsubscribe")]
pub async fn req(mut session: Session) -> Result<EmptyResponse> {
session.subscription = None;
session
.save(&get_db(), None)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
operation: "save",
with: "session",
})
}

View File

@@ -1,22 +1,43 @@
use crate::util::variables::{
DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, USE_EMAIL, USE_HCAPTCHA,
};
use crate::util::{ratelimit::Ratelimiter, variables::{
APP_URL, AUTUMN_URL, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, JANUARY_URL, USE_AUTUMN,
USE_EMAIL, USE_HCAPTCHA, USE_JANUARY, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL, VOSO_WS_HOST,
}};
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
use rocket::{http::Status, serde::json::Value};
#[get("/")]
pub async fn root() -> JsonValue {
pub async fn root() -> Value {
json!({
"revolt": "0.3.1",
"revolt": crate::version::VERSION,
"features": {
"registration": !*DISABLE_REGISTRATION,
"captcha": {
"enabled": *USE_HCAPTCHA,
"key": HCAPTCHA_SITEKEY.to_string()
},
"email": *USE_EMAIL,
"invite_only": *INVITE_ONLY,
"autumn": {
"enabled": *USE_AUTUMN,
"url": *AUTUMN_URL
},
"january": {
"enabled": *USE_JANUARY,
"url": *JANUARY_URL
},
"voso": {
"enabled": *USE_VOSO,
"url": *VOSO_URL,
"ws": *VOSO_WS_HOST
}
},
"ws": *EXTERNAL_WS_URL,
"app": *APP_URL,
"vapid": *VAPID_PUBLIC_KEY
})
}
#[get("/ping")]
pub async fn ping(_limitguard: Ratelimiter) -> Status {
Status::Ok
}

View File

@@ -0,0 +1,62 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 1024))]
reason: Option<String>,
}
#[put("/<server>/bans/<target>", data = "<data>")]
pub async fn req(user: User, server: Ref, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let server = server.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if !perm.get_ban_members() {
Err(Error::MissingPermission)?
}
let target = target.fetch_user().await?;
if target.id == user.id {
return Err(Error::InvalidOperation);
}
if target.id == server.owner {
return Err(Error::MissingPermission);
}
let mut document = doc! {
"_id": {
"server": &server.id,
"user": &target.id
}
};
if let Some(reason) = data.reason {
document.insert("reason", reason);
}
get_collection("server_bans")
.insert_one(document, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "server_ban",
})?;
server.remove_member(&target.id, RemoveMember::Ban).await?;
Ok(EmptyResponse {})
}

View File

@@ -0,0 +1,87 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::options::FindOptions;
use serde::{Serialize, Deserialize};
use rocket::serde::json::Value;
use mongodb::bson::{doc, from_document};
#[derive(Serialize, Deserialize)]
struct BannedUser {
_id: String,
username: String,
avatar: Option<File>
}
#[get("/<target>/bans")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_ban_members() {
return Err(Error::MissingPermission);
}
let mut cursor = get_collection("server_bans")
.find(
doc! {
"_id.server": target.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_bans",
})?;
let mut bans = vec![];
let mut user_ids = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(ban) = from_document::<Ban>(doc) {
user_ids.push(ban.id.user.clone());
bans.push(ban);
}
}
}
let mut cursor = get_collection("users")
.find(
doc! {
"_id": {
"$in": user_ids
}
},
FindOptions::builder()
.projection(doc! {
"username": 1,
"avatar": 1
})
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?;
let mut users = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(user) = from_document::<BannedUser>(doc) {
users.push(user);
}
}
}
Ok(json!({
"users": users,
"bans": bans
}))
}

View File

@@ -0,0 +1,43 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use mongodb::bson::doc;
#[delete("/<server>/bans/<target>")]
pub async fn req(user: User, server: Ref, target: Ref) -> Result<EmptyResponse> {
let server = server.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if !perm.get_ban_members() {
Err(Error::MissingPermission)?
}
if target.id == user.id {
return Err(Error::InvalidOperation);
}
if target.id == server.owner {
return Err(Error::MissingPermission);
}
let target = target.fetch_ban(&server.id).await?;
get_collection("server_bans")
.delete_one(
doc! {
"_id.server": &server.id,
"_id.user": &target.id.user
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "server_ban",
})?;
Ok(EmptyResponse {})
}

View File

@@ -0,0 +1,104 @@
use std::collections::HashMap;
use crate::database::*;
use crate::util::idempotency::IdempotencyKey;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
#[derive(Serialize, Deserialize)]
enum ChannelType {
Text,
Voice
}
impl Default for ChannelType {
fn default() -> Self {
ChannelType::Text
}
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[serde(rename = "type", default = "ChannelType::default")]
channel_type: ChannelType,
#[validate(length(min = 1, max = 32))]
name: String,
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
#[post("/<target>/channels", data = "<info>")]
pub async fn req(_idempotency: IdempotencyKey, user: User, target: Ref, info: Json<Data>) -> Result<Value> {
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_channels() {
Err(Error::MissingPermission)?
}
let id = Ulid::new().to_string();
let channel = match info.channel_type {
ChannelType::Text => Channel::TextChannel {
id: id.clone(),
server: target.id.clone(),
name: info.name,
description: info.description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: info.nsfw.unwrap_or_default(),
},
ChannelType::Voice => Channel::VoiceChannel {
id: id.clone(),
server: target.id.clone(),
name: info.name,
description: info.description,
icon: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: info.nsfw.unwrap_or_default()
}
};
channel.clone().publish().await?;
get_collection("servers")
.update_one(
doc! {
"_id": target.id
},
doc! {
"$addToSet": {
"channels": id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "server",
})?;
Ok(json!(channel))
}

View File

@@ -0,0 +1,53 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, from_document};
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ServerInvite {
#[serde(rename = "_id")]
code: String,
creator: String,
channel: String,
}
#[get("/<target>/invites")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_server() {
Err(Error::MissingPermission)?
}
let mut cursor = get_collection("channel_invites")
.find(
doc! {
"server": target.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "channel_invites",
})?;
let mut invites = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(invite) = from_document::<Invite>(doc) {
invites.push(invite);
}
}
}
Ok(json!(invites))
}

View File

@@ -0,0 +1,158 @@
use std::collections::HashSet;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveMemberField};
use mongodb::bson::{doc, to_document};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 32))]
nickname: Option<String>,
avatar: Option<String>,
roles: Option<Vec<String>>,
remove: Option<RemoveMemberField>,
}
#[patch("/<server>/members/<target>", data = "<data>")]
pub async fn req(user: User, server: Ref, target: String, data: Json<Data>) -> Result<EmptyResponse> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.nickname.is_none() && data.avatar.is_none() && data.roles.is_none() && data.remove.is_none() {
return Ok(EmptyResponse {});
}
let server = server.fetch_server().await?;
let target = Ref::from(target)?.fetch_member(&server.id).await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if data.roles.is_some() && !perm.get_manage_roles() {
return Err(Error::MissingPermission);
}
if target.id.user == user.id {
if (data.nickname.is_some() && !perm.get_change_nickname())
|| (data.avatar.is_some() && !perm.get_change_avatar())
{
return Err(Error::MissingPermission);
}
if let Some(remove) = &data.remove {
if match remove {
RemoveMemberField::Avatar => !perm.get_change_avatar(),
RemoveMemberField::Nickname => !perm.get_change_nickname(),
} {
return Err(Error::MissingPermission);
}
}
} else {
if data.avatar.is_some() || (data.nickname.is_some() && !perm.get_manage_nicknames()) {
return Err(Error::MissingPermission);
}
if let Some(remove) = &data.remove {
if match remove {
RemoveMemberField::Avatar => !perm.get_remove_avatars(),
RemoveMemberField::Nickname => !perm.get_manage_nicknames(),
} {
return Err(Error::MissingPermission);
}
}
}
let mut set = doc! {};
let mut unset = doc! {};
let mut remove_avatar = false;
if let Some(remove) = &data.remove {
match remove {
RemoveMemberField::Avatar => {
unset.insert("avatar", 1);
remove_avatar = true;
}
RemoveMemberField::Nickname => {
unset.insert("nickname", 1);
}
}
}
if let Some(name) = &data.nickname {
set.insert("nickname", name);
}
if let Some(attachment_id) = &data.avatar {
let attachment =
File::find_and_use(&attachment_id, "avatars", "user", &target.id.user).await?;
set.insert(
"avatar",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_avatar = true;
}
if let Some(role_ids) = &data.roles {
let mut ids = HashSet::new();
for role in role_ids {
if server.roles.contains_key(role) {
ids.insert(role.clone());
}
}
set.insert("roles", ids.into_iter().collect::<Vec<String>>());
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("server_members")
.update_one(
doc! { "_id.server": &server.id, "_id.user": &target.id.user },
operations,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "server_member",
})?;
}
ClientboundNotification::ServerMemberUpdate {
id: target.id.clone(),
data: json!(set),
clear: data.remove,
}
.publish(server.id.clone());
let Member { avatar, .. } = target;
if remove_avatar {
if let Some(old_avatar) = avatar {
old_avatar.delete().await?;
}
}
Ok(EmptyResponse {})
}

View File

@@ -0,0 +1,21 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket::serde::json::Value;
#[get("/<target>/members/<member>")]
pub async fn req(user: User, target: Ref, member: String) -> Result<Value> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
Ok(json!(Ref::from(member)?.fetch_member(&target.id).await?))
}

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