Compare commits

...

428 Commits

Author SHA1 Message Date
Paul Makles
fdab997d17 chore(ci): switch testing to stable Rust 2023-01-29 14:23:56 +00:00
Paul Makles
8e0addc3e0 fix: fallback properly if no VERGEN strings can generate 2023-01-29 14:22:57 +00:00
Paul Makles
0da4baf777 chore: bump version and clean up repo 2023-01-29 14:16:18 +00:00
Paul Makles
c03fcda85b refactor: switch from "rauth" to Authifier crate 2023-01-29 14:14:35 +00:00
Paul Makles
0afbcc065f refactor: clippy warning for empty list 2023-01-27 16:18:00 +00:00
Paul Makles
a406e5504d chore: add support for Streamable in media struct 2023-01-27 16:17:47 +00:00
Zomatree
e8c233f60b feat: concurrently query January for embeds (#221) 2023-01-27 16:06:27 +00:00
insertish
72595f30d4 ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2023-01-24 19:43:40 +00:00
insertish
39e345af74 ci: synced local '.github/workflows/triage_issue.yml' with remote 'workflows/triage_issue.yml'
[skip ci]
2023-01-24 19:43:40 +00:00
Paul Makles
e0b918771d chore: ensure restrict_reactions is used in conjunction with reactions list 2023-01-20 18:06:44 +00:00
Paul Makles
7a6bd70dcd feat: add route to fetch user flags
chore: bump rauth to change disabled account behaviour
2023-01-20 17:47:26 +00:00
Paul Makles
c0ef3d295a fix: fallback if vergen cannot generate data 2023-01-20 17:14:25 +00:00
Paul Makles
f5c95f9d12 chore: bump rauth for lockout reset on pwd reset 2023-01-20 15:57:58 +00:00
Paul Makles
fed1da72fd feat: support rauth events
feat: add platform user wipe event
2023-01-20 15:43:45 +00:00
Paul Makles
cf9c0bc5e1 chore: bump rauth 2023-01-20 14:00:19 +00:00
Paul Makles
cc2054838a chore: swap okapi for revolt_okapi 2023-01-20 14:00:19 +00:00
Paul Makles
d9d2735efa fix(ratelimiter): re-calculate reset time
closes #204

Co-authored-by: TheBobBobs <84781603+TheBobBobs@users.noreply.github.com>
2023-01-18 18:26:00 +00:00
Ryan
94b0472934 feat: pass server flags with invite data (#203) 2023-01-18 18:17:59 +00:00
Sophie L
4a7917e9ea feat: enable reactions by default in servers/DMs 2023-01-18 18:17:37 +00:00
Lea
ce6b454874 fix: raise masquerade avatar URL length limit 2023-01-18 18:17:05 +00:00
ToastXC
fc32996b51 chore: remove clippy warnings (#213) 2023-01-18 18:16:27 +00:00
Paul Makles
32d3683013 fix(ci): re-order docker-compose arguments
chore(ci): prevent running gen on PR
2023-01-14 15:53:09 +00:00
Paul Makles
0cebb69e02 feat: automate generating OpenAPI specification 2023-01-14 15:42:30 +00:00
Paul Makles
117958d8cd feat: include build information in root api call 2023-01-14 15:42:06 +00:00
Paul Makles
c5b823ad13 chore: clippy warnings 2023-01-03 21:06:43 +00:00
Paul Makles
27b4699299 fix: unify foreign user fetch into one method 2023-01-03 21:04:04 +00:00
Paul Makles
ee1c4cc2d5 chore: bump quark
feat: update email templates to include recipient
2022-11-19 16:52:51 +00:00
Paul Makles
0e0daf86f7 feat: update emails to not look garbage 2022-10-23 21:23:29 +01:00
Paul Makles
572e08c733 chore: update rauth 2022-10-23 21:08:08 +01:00
Paul Makles
9011939344 chore: remove .env file 2022-10-23 20:55:45 +01:00
Paul Makles
f4dcd091cc feat: add 'Focus' presence status 2022-09-18 12:12:31 +01:00
Paul Makles
86ad72b426 fix: strip unknown channels from categories (fixes #201) 2022-09-12 21:39:29 +01:00
Paul Makles
044d82d566 fix: use correct validation for masq role colour 2022-09-03 10:06:49 +01:00
Paul Makles
deffe663cb feat: add ability to leave groups / servers silently 2022-09-02 15:32:11 +01:00
Paul Makles
e7f6433d77 fix: actually transfer group ownership 2022-09-01 09:45:17 +01:00
Paul Makles
345e4d5f13 fix: validate categories 2022-09-01 09:41:00 +01:00
Sophie L
81fc3e7760 fix: set the cap to 100 2022-09-01 09:01:43 +01:00
Paul Makles
ca8d2e6118 fix: reaction ordering by using indexmap 2022-08-14 11:47:30 +02:00
Paul Makles
c1227e8e33 fix: check React permission on interactions 2022-08-14 11:31:36 +02:00
Paul Makles
0e0c6f2c3a fix: validate number of reactions on message when adding more 2022-08-13 20:26:09 +02:00
Paul Makles
fa82be74b5 fix: adjust permission checks for member_edit 2022-07-26 14:06:53 +01:00
Paul Makles
8491ced13d fix: ensure list matches 2022-07-18 12:46:33 +01:00
Paul Makles
229d4e2e1d fix: correct ordering for creating ownership change event 2022-07-15 21:46:41 +01:00
Paul Makles
7bdd2d69d6 feat: add message reactions + interactions object 2022-07-15 21:24:49 +01:00
Paul Makles
ab2af9b5e5 feat: add group ownership transfer 2022-07-15 17:58:08 +01:00
Paul Makles
5aebda2d16 chore: restrict emojis to lowercase 2022-07-15 17:57:53 +01:00
Paul Makles
a7273d0556 feat: allow servers to set discoverable value 2022-07-15 17:57:30 +01:00
Paul Makles
72aed64e5b chore: allow banning users not in server 2022-07-15 16:47:27 +01:00
Paul Makles
00ad4d54e7 feat: add nsfw flag to emotes 2022-07-15 16:34:08 +01:00
Paul Makles
cba8c4ef3b fix: add case-insensitive flag to colour regex 2022-07-15 16:13:04 +01:00
Paul Makles
b6e395fe0d fix: use default for roles key 2022-07-15 16:02:58 +01:00
Paul Makles
8d07457c58 chore: make joined_at migration shorter 2022-07-15 16:02:46 +01:00
Paul Makles
f1171e5358 feat: restrict role colours to regex (increase length limit) 2022-07-15 15:36:00 +01:00
Paul Makles
d96c9f62c4 chore: detach emojis on delete 2022-07-15 14:01:59 +01:00
Paul Makles
4f73e43a03 feat: restrict permissions for users in timeout 2022-07-15 13:55:55 +01:00
Paul Makles
741b8ee8fd feat: add role colours to masquerades
feat: add `joined_at` property to members
feat: add `timeout` property to members (non-functional)
2022-07-15 13:42:01 +01:00
Paul Makles
692081b7f0 chore: bump rauth for verification fix 2022-07-14 14:01:10 +01:00
Paul Makles
943b1f08f0 fix: prevent short-circuit on permissions 2022-07-10 18:27:22 +01:00
Paul Makles
0e5c8ed59b chore: bump version 2022-07-09 17:44:05 +01:00
Paul Makles
a7e0c42ee4 feat: add emoji 2022-07-07 13:23:31 +01:00
Paul Makles
386f027a5a chore: deprecate old add friend route 2022-07-07 13:20:03 +01:00
Paul Makles
30c7f553d8 chore: refactor autumn to media 2022-07-06 16:46:18 +01:00
Paul Makles
e291eef364 chore: create entity relationship diagram [skip ci] 2022-07-06 16:43:17 +01:00
Martin Löffler
73443ce883 revert: bump rauth for acc. verification fix
the "fix" caused emails to not send
2022-06-23 01:30:54 +02:00
Paul Makles
47297e243f fix: enforce min. password length
fixes #188
2022-06-21 11:06:02 +01:00
Paul Makles
4188b6d2f2 fix: ignore anchor links when parsing messages
fixes #183
2022-06-21 10:38:59 +01:00
Paul Makles
165380c7b4 chore: bump rauth for acc. verification fix 2022-06-21 10:28:14 +01:00
Paul Makles
49ca24ac9f chore: further restrict usernames
closes #186
2022-06-21 10:27:58 +01:00
Paul Makles
35f956ce7d fix: add separate bucket for default_avatar
closes #166
2022-06-20 11:13:15 +01:00
Paul Makles
4baab5d5d5 feat(messaging): cap total text content to 2k characters 2022-06-20 11:09:14 +01:00
Paul Makles
7fc4fb2df7 fix: rewrite attachment deletion logic 2022-06-20 10:49:09 +01:00
Paul Makles
ef757aa2fb chore: bump quark 2022-06-20 10:27:26 +01:00
Paul Makles
0585dd0c20 fix: consistent username validation across routes
fixes #187
2022-06-20 10:27:22 +01:00
Paul Makles
f96541efab fix: add additional validation on legacy nonce value 2022-06-14 17:35:44 +01:00
Paul Makles
c6414338b6 fix: marking server as read would not mark it as read
fixes #169

Porting code forwards from an older revision of the codebase; https://github.com/revoltchat/backend/blob/0.5.3-alpha.10/src/database/entities/server.rs
2022-06-14 17:32:43 +01:00
Paul Makles
4c4eb60cdb fix: don't allow members to be added more than once
fixes #182
2022-06-14 17:27:15 +01:00
Paul Makles
11d89b3bf0 feat: enable 2FA login 2022-06-12 18:50:30 +01:00
Paul Makles
64bb171cc8 fix: remove test flag from rauth 2022-06-12 18:03:49 +01:00
Paul Makles
6de5ad15c5 chore: bump rauth 2022-06-12 17:48:12 +01:00
Paul Makles
b5ab16d66f fix: bump quark to fix is_disabled check 2022-06-12 17:07:22 +01:00
Paul Makles
b9aad6d38c chore: bump rauth 2022-06-11 20:49:19 +01:00
Paul Makles
ed5c8159e9 chore: bump rauth 2022-06-10 17:47:13 +01:00
Paul Makles
2e6e1fdd44 chore: bump quark; changes deletion period 2022-06-10 17:22:34 +01:00
Paul Makles
cb2e6fb2f7 feat: separate friend request route 2022-06-09 17:02:01 +01:00
Paul Makles
d069acd5a5 chore: expose correct version in api spec 2022-06-09 16:05:04 +01:00
Paul Makles
f8b5c25c17 chore: bump rauth; adds account deletion
closes #159
2022-06-09 16:00:59 +01:00
Paul Makles
80af18761f feat: add further restrictions on usernames 2022-06-09 14:06:13 +01:00
Paul Makles
98e10c3478 fix: bump rauth with assertion fix 2022-06-08 14:28:58 +01:00
Paul Makles
2051c8ce45 chore(refactor): validate further up the code 2022-06-08 14:14:16 +01:00
Paul Makles
0abc12e5fa chore(refactor): process attachments last 2022-06-08 14:12:11 +01:00
Paul Makles
aaceb5bdee fix: sub-validate message structs 2022-06-08 14:11:24 +01:00
Paul Makles
3de7ee4c7b fix: cors needs to be attached 2022-06-08 14:08:33 +01:00
Paul Makles
d660127c14 chore: refactor generic web server code into quark 2022-06-08 14:08:33 +01:00
Paul Makles
7390b3c087 feat: update to rAuth v1 2022-06-08 14:08:33 +01:00
Paul Makles
020f2a1b1a fix: enforce role permissions on member_edit 2022-06-06 11:37:37 +01:00
Paul Makles
3e55ff3577 chore: switch to rocket_empty [skip ci] 2022-06-02 15:21:31 +01:00
Paul Makles
bb70294546 chore: delete stray Cargo.lock [skip ci] 2022-06-02 13:41:17 +01:00
Paul Makles
9c93017b8a chore(ci): upload to ghcr after base image build 2022-06-02 13:16:17 +01:00
Paul Makles
57592c0862 chore(ci): update base image [skip ci] 2022-06-02 13:14:17 +01:00
Paul Makles
f09981695e fix(ci): use matrix.project value correctly 2022-06-02 13:08:08 +01:00
Paul Makles
ad76e85c8d chore: create cla.yml 2022-06-02 12:17:30 +01:00
Paul Makles
a100b7aefa chore: try to fix docker.yaml 2022-06-02 12:17:30 +01:00
Paul Makles
a06ef2ba1a chore: add docker workflow 2022-06-02 12:17:30 +01:00
Paul Makles
6336b254c5 feat: new CI pipeline [skip ci] 2022-06-02 12:17:30 +01:00
Paul Makles
f1dd7d2c78 chore: comment out additional crates for the time being 2022-06-02 12:17:30 +01:00
Paul Makles
ae3ed7c124 chore: general clean up 2022-06-02 12:17:30 +01:00
Paul Makles
5237ecbb90 chore: update README 2022-06-02 12:17:30 +01:00
Paul Makles
2ce610e1e7 chore(monorepo): delta, january, quark 2022-06-02 12:17:30 +01:00
Paul Makles
5d8432e267 chore: remove docker workflow preparing for monorepo 2022-06-01 20:37:47 +01:00
Paul Makles
44311c87be chore: bump quark 2022-05-18 21:48:32 +01:00
Paul Makles
ee9622251a chore: bump quark 2022-05-18 21:27:54 +01:00
Paul Makles
d13573eb45 chore: bump version 2022-05-17 20:31:23 +01:00
Paul Makles
20e93b9838 chore: bump quark and rauth 2022-05-16 19:14:14 +01:00
JandereDev
dc6ce96aa2 fix: uploading server banners was broken 2022-05-15 18:10:45 +01:00
Paul Makles
de5428348c fix: actually set the correct regex 2022-05-09 23:08:17 +01:00
Paul Makles
d7527d9131 chore: loosen username restrictions 2022-05-09 23:03:08 +01:00
Paul Makles
c59c4146c4 chore: bump quark, fixes #156 2022-05-08 10:13:45 +01:00
Paul Makles
bd455b59fb fix: use i64::MAX for default role permission 2022-05-07 17:55:58 +01:00
Paul Makles
d03cb44071 chore: limit deletions to past week 2022-05-07 16:53:15 +01:00
Paul Makles
ac4cedd014 feat: bulk message delete 2022-05-07 16:42:06 +01:00
Paul Makles
09e918d8e0 feat: clean up bot restrictions 2022-05-07 15:20:42 +01:00
Paul Makles
97fef56bad fix: masquerade not being applied to mesages
fixes #153
2022-04-30 16:09:52 +01:00
Paul Makles
408743476b chore: bump quark revision 2022-04-29 17:34:01 +01:00
Paul Makles
fad53ee5f0 chore: bump quark revision 2022-04-29 17:20:04 +01:00
Paul Makles
be9a5dba5b chore: bump quark revision 2022-04-29 17:03:08 +01:00
Paul Makles
c33d387486 merge: pull request #135 from revoltchat/quark 2022-04-29 13:52:51 +01:00
Paul Makles
0651a501f6 fix: short-circuit fetch user route if self 2022-04-29 13:10:52 +01:00
Paul Makles
278d795d50 fix: don't allow mutual route if self 2022-04-29 13:07:33 +01:00
Paul Makles
6281418ecb chore: separate BannedUser mapping 2022-04-29 13:03:48 +01:00
Paul Makles
ae0e5cd7bf chore: bump quark revision 2022-04-28 20:34:00 +01:00
Paul Makles
45a3b4ba15 feat: add asset!() macro to crate 2022-04-28 20:31:40 +01:00
Paul Makles
8b72e2fa0e chore: update all dependencies 2022-04-28 20:31:28 +01:00
Paul Makles
e5af59d307 chore: remove old dependencies 2022-04-28 19:56:12 +01:00
Paul Makles
6a7e7e9501 chore: bump quark revision 2022-04-28 17:55:47 +01:00
Paul Makles
247c059f29 fix(ratelimiter): read CF-Connecting-IP for identifying user 2022-04-28 15:17:04 +01:00
Paul Makles
621dd86845 chore: bump quark 2022-04-24 19:02:50 +01:00
Paul Makles
2b016c34f7 fix: use MONGODB env variable for rauth 2022-04-24 14:01:46 +01:00
Paul Makles
e1169c5335 fix: ensure bot invite route runs correct group member checks 2022-04-24 13:59:07 +01:00
Paul Makles
fad540a87a fix: delete bots; fetch public if owner 2022-04-24 13:45:14 +01:00
Paul Makles
00cb132d29 fix: actually create the server object 2022-04-22 22:08:04 +01:00
Paul Makles
7757769f79 chore: update cargo.lock 2022-04-21 20:44:43 +01:00
Paul Makles
2d10180f08 chore: switch to latest quark revision 2022-04-21 20:44:28 +01:00
Paul Makles
180ac67056 feat: reimplement voice chat 2022-04-21 20:34:43 +01:00
Paul Makles
350bd9c0be feat: add parameter to skip offline users on fetch 2022-04-21 18:16:09 +01:00
Paul Makles
367c4e3260 chore: remove db parameter on ack 2022-04-21 18:15:59 +01:00
Paul Makles
02ba8c7978 chore: clippy 2022-04-21 17:53:16 +01:00
Paul Makles
d7839f1790 feat: allow empty message content when sending embed 2022-04-21 17:52:42 +01:00
Paul Makles
9c71f09a84 chore: split content into content, system
closes #102
2022-04-21 17:45:21 +01:00
Paul Makles
b32503d29f fix: return correct rate limit response 2022-04-21 17:43:27 +01:00
Paul Makles
535665ca1b fix: ignore blocked users when adding mentions
closes #145
2022-04-21 16:50:47 +01:00
Paul Makles
da08a7d722 feat: reimplement system messages 2022-04-21 16:36:59 +01:00
Paul Makles
5ce0c133e8 chore: refactor tasks into quark 2022-04-21 13:52:35 +01:00
Paul Makles
3adbf29060 chore: port ack task 2022-04-17 13:55:22 +01:00
Paul Makles
3d98f5dfbb feat: implement DelayedTask and last_id task 2022-04-17 13:37:07 +01:00
Paul Makles
54b6cf67ab feat(tasks): implement web push 2022-04-17 13:11:29 +01:00
Paul Makles
b4a2e8c366 fix: adjust rate limit buckets for auth_del / swagger 2022-04-17 13:11:13 +01:00
Paul Makles
3e135a1a9c feat: switch to ISO8601 timestamps 2022-04-17 10:41:12 +01:00
Paul Makles
b351ef0332 feat(user dms): open saved messages channel 2022-04-17 10:22:53 +01:00
Paul Makles
87391a49a5 feat: add link embed generation 2022-04-14 20:46:46 +01:00
Paul Makles
d2f0fb3414 feat(api): add full API rate limits 2022-04-14 17:09:16 +01:00
Paul Makles
7ea5146e58 chore: push .env.example 2022-04-12 22:58:30 +01:00
Paul Makles
3d4c40dac0 chore: use latest Quark 2022-04-12 22:56:11 +01:00
Paul Makles
c3a04d02c5 chore: update README 2022-04-12 22:55:00 +01:00
Paul Makles
1dbd66a45c chore: add LICENSE 2022-04-12 22:54:00 +01:00
Paul Makles
10645638a2 feat: use new ServerCreate events 2022-04-09 19:03:25 +01:00
Paul Makles
24def22613 chore: include "code" in invite response 2022-04-09 19:03:14 +01:00
Paul Makles
a2911e42c6 chore: bump dependencies 2022-04-09 14:38:38 +01:00
Paul Makles
558e4b86f1 chore: add debugging profile 2022-04-09 14:38:01 +01:00
Paul Makles
e245b272fa feat: update bot routes to match old spec 2022-03-20 18:47:04 +00:00
Paul Makles
b4d825b9a1 feat: strongly type and remaining routes 2022-03-20 18:14:24 +00:00
Paul Makles
aee6c751c6 fix: strongly type member fetch all route 2022-03-20 18:08:48 +00:00
Paul Makles
0232d721db chore(doc): finish up OpenAPI spec 2022-03-19 18:50:07 +00:00
Paul Makles
8dd9acdb59 chore(doc): implement remainder of documentation 2022-03-19 18:19:24 +00:00
Paul Makles
43c475eabb chore(doc): add API information to spec 2022-03-19 14:24:19 +00:00
Paul Makles
25a52cbbb1 chore(doc): comment channel routes 2022-03-19 14:14:24 +00:00
Paul Makles
14e17b10c0 feat: implement OpenAPI spec for bots and root 2022-03-18 21:22:31 +00:00
Paul Makles
a0926eea32 fix(docker): should copy all assets in 2022-03-12 23:06:03 +00:00
Paul Makles
3ae23af275 chore: update dependencies 2022-03-12 22:51:46 +00:00
Paul Makles
927556041b fix(channel/delete): send out channel update on close DM
(for https://github.com/revoltchat/revite/issues/432)
2022-03-04 16:37:26 +00:00
Paul Makles
bbb368217c feat: update avatars 2022-03-04 15:52:34 +00:00
Paul Makles
f568838ff9 fix: should be foreign 2022-03-03 22:44:55 +00:00
Paul Makles
3c5e494216 chore: return user from change username 2022-03-03 22:44:29 +00:00
Paul Makles
465d766254 chore(refactor): use strong typing for fetch public bot 2022-03-03 20:05:41 +00:00
Paul Makles
8d8bcff31f fix(server_edit): use new style throws 2022-02-24 00:07:31 +00:00
Paul Makles
3df869c21d chore: handle ban check in member creation 2022-02-23 18:08:44 +00:00
Paul Makles
a495a8dfd7 feat(permissions): handle rankings on kick / ban 2022-02-23 18:08:25 +00:00
Paul Makles
0553ce5587 feat(permissions): apply permissions on set channel routes 2022-02-23 17:51:36 +00:00
Paul Makles
fcbaab7bcb feat(server/set_default): prevent users giving perm they don't have 2022-02-23 15:59:21 +00:00
Paul Makles
437a620274 fix: also consider role ranking on setting role perm 2022-02-23 15:58:54 +00:00
Paul Makles
30af92bee4 feat(set server perm): prevent users giving out permissions they don't have 2022-02-23 15:53:58 +00:00
Paul Makles
0ecce98056 feat(roles delete): only allow members to delete roles lower ranking then themself 2022-02-23 14:42:08 +00:00
Paul Makles
04c2f8f293 feat(roles edit): ensure users can't move role above their own ranking 2022-02-23 14:39:09 +00:00
Paul Makles
d45b32bce6 feat(roles create): consider member's ranking and allow specifying custom ranking on creation 2022-02-23 14:34:29 +00:00
Paul Makles
23ec914033 feat: implement new permissions hierarchy 2022-02-23 13:30:01 +00:00
Paul Makles
71c01e0817 fix(bans): fix data format returned 2022-02-22 22:51:39 +00:00
Paul Makles
7b1a3dc7dd fix(join invite): create instead of insert member 2022-02-22 22:51:23 +00:00
Paul Makles
64d1126af9 fix(message delete): throw instead of manual check 2022-02-22 22:51:04 +00:00
Paul Makles
4c40c6c2cc feat(perms): consistently throw correct permissions 2022-02-22 21:41:12 +00:00
Paul Makles
7befd77c5b fix: switch to new permissions enum 2022-02-22 20:34:23 +00:00
Paul Makles
aab85fa143 feat: use auto db configuration 2022-02-21 14:48:36 +00:00
Paul Makles
d7be1db755 fix: make users foreign when fetching member list 2022-02-21 13:01:53 +00:00
Paul Makles
2580cec272 feat: add idempotency tokens back 2022-02-20 10:16:16 +00:00
Paul Makles
489c2bdd87 feat: rewrite requests to include events 2022-02-19 19:49:10 +00:00
Paul Makles
061af1f2c3 chore: update path 2022-02-19 12:20:01 +00:00
Paul Makles
50d9f50c8f chore: commit the things that are to be commited 2022-02-11 23:33:38 +00:00
Paul Makles
28db08ba88 fix: create membership when creating server 2022-02-11 22:27:34 +00:00
Paul Makles
9da51b2d45 fix: fix the thing (it is 10:20pm) 2022-02-11 22:21:37 +00:00
Paul Makles
6a871fa944 feat: implement fetch group invite 2022-02-11 22:16:31 +00:00
Paul Makles
4801b51cef fix: fixes for full mongo implementation 2022-02-11 21:44:59 +00:00
Paul Makles
7a9adb45f7 fix(invites): account for server bans 2022-02-11 12:52:34 +00:00
Paul Makles
f0eb228b19 fix: apply partial to bot object 2022-02-11 12:49:34 +00:00
Paul Makles
5aee381fd6 feat: enable migrations 2022-02-11 12:37:09 +00:00
Paul Makles
249b1161f9 chore: implement invite join and rauth conf 2022-02-11 11:28:06 +00:00
Paul Makles
1988a9501b chore: remove mongodb imports 2022-02-11 11:11:48 +00:00
Paul Makles
6d230e57f5 feat(web push): sub / unsub routes 2022-02-10 22:14:07 +00:00
Paul Makles
9f1eda4af0 feat(servers): role and permission routes 2022-02-10 22:14:00 +00:00
Paul Makles
734dc2bb59 feat(servers): member routes 2022-02-10 22:13:46 +00:00
Paul Makles
e22d991995 feat(servers): ban routes 2022-02-10 22:13:10 +00:00
Paul Makles
37ffde23ce feat(servers): implement create, delete, edit, fetch 2022-02-10 18:30:36 +00:00
Paul Makles
f676071641 feat(sync): implement settings and unreads fetch 2022-02-10 16:55:59 +00:00
Paul Makles
a8d5fcec9e feat(bots): implement bot routes 2022-02-10 15:54:41 +00:00
Paul Makles
d4aff072a8 feat(messages): implement message ack / delete 2022-02-10 14:23:46 +00:00
Paul Makles
57d114fee9 feat(messages): implement message editing 2022-02-10 13:33:13 +00:00
Paul Makles
a4cbbfea51 feat(messages): implement fetch, query, search 2022-02-10 12:53:26 +00:00
Paul Makles
a6cec559b1 feat(messaging): implement fetch and send 2022-02-04 23:33:58 +00:00
Paul Makles
8b4541feb0 chore: format code 2022-02-04 22:19:40 +00:00
Paul Makles
88273021a2 chore: add env variables back 2022-02-04 22:17:49 +00:00
Paul Makles
e367c42dca feat(channels): add / remove group members 2022-02-04 22:11:18 +00:00
Paul Makles
88f811761b feat: implement channel invites, perms, group create, members fetch
chore: when you accidentally run fmt
2022-02-04 21:50:30 +00:00
Paul Makles
b58755dbeb feat(channels): implement fetch, edit, delete 2022-02-04 16:48:32 +00:00
Paul Makles
780b88eb2f feat(onboard): implement onboarding routes 2022-02-04 14:57:10 +00:00
Paul Makles
1aafa51033 feat(users): implement relationships, DMs, proper perms 2022-02-04 13:38:24 +00:00
Paul Makles
63064cc9bb feat: implement edit user / username 2022-02-03 22:52:04 +00:00
Paul Makles
1d5800a3b8 feat(users): implement fetch self / user, open dm 2022-01-31 22:30:02 +00:00
Paul Makles
c1cd4bfa66 feat(routes/users): implement fetch_self 2022-01-31 20:07:36 +00:00
Paul Makles
c39a9917df chore: prototyping on user route 2022-01-31 16:59:21 +00:00
Paul Makles
0fdb749199 chore(thanos): strip down codebase to just API routes 2022-01-27 14:16:30 +00:00
Paul Makles
ab4c3f913e chore: bump and publish 2022-01-15 11:09:05 +00:00
Zomatree
8be51dc4f0 feat(messaging): add sendable embeds (#119) 2022-01-15 10:43:47 +00:00
Paul Makles
96fd18f679 chore: remove limitations on private channels 2022-01-04 20:08:04 +00:00
Paul Makles
2db54cf8de chore: merge local branch 'feat/discoverability' 2022-01-03 15:27:32 +00:00
Paul Makles
c3f092aaef feat(invite): allow direct invitation by id for discoverable servers 2022-01-03 15:26:29 +00:00
Paul Makles
ce68f7eeed fix: don't return all members on message query / search 2022-01-03 15:21:12 +00:00
Paul Makles
139e704746 feat: add discoverability and analytics flags (#130) 2022-01-03 13:12:13 +00:00
Paul Makles
345c4c627c feat: add discoverability and analytics flags 2022-01-03 12:46:10 +00:00
insertish
03ca611553 ci: Synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml' 2021-12-31 17:32:26 +00:00
Paul
0fe63e3955 chore: bump rauth revision and version to 0.5.3-alpha.11 2021-11-28 17:58:24 +00:00
Paul
4474061103 chore: bump to 0.5.3-alpha.10 2021-11-23 08:57:34 +00:00
Paul
788580a64f fix: broken index migration
feat: add new index
2021-11-23 08:57:17 +00:00
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
367 changed files with 22566 additions and 8551 deletions

View File

@@ -1,3 +1,4 @@
Dockerfile
target
.mongo
.env

79
.env.example Normal file
View File

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

36
.github/workflows/cla.yml vendored Normal file
View File

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

116
.github/workflows/docker.yaml vendored Normal file
View File

@@ -0,0 +1,116 @@
name: Docker Test & Publish
on:
push:
branches:
- "master"
tags:
- "*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- ".vscode/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request:
branches:
- "master"
paths:
- "Dockerfile"
workflow_dispatch:
jobs:
base:
runs-on: ubuntu-latest
name: Build base image (amd64)
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Authenticate with GHCR
- name: Login to Github Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Build all projects and cache
- name: Build Base Image
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ghcr.io/revoltchat/base:latest
cache-from: type=gha
cache-to: type=gha,mode=max
publish_amd64:
needs: [base]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
strategy:
matrix:
project: [delta, bonfire]
name: Build ${{ matrix.project }} image (amd64)
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Authenticate with Docker Hub and GHCR
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Resolve the correct project
- uses: kanga333/variable-mapper@master
id: export
with:
key: "${{ matrix.project }}"
map: |
{
"delta": {
"path": "crates/delta",
"tag": "revoltchat/server"
},
"bonfire": {
"path": "crates/bonfire",
"tag": "revoltchat/bonfire"
}
}
export_to: output
# Configure metadata
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: ${{ steps.export.outputs.tag }}, ghcr.io/${{ steps.export.outputs.tag }}
# Build crate image
- name: Publish
uses: docker/build-push-action@v3
with:
context: .
push: true
platforms: linux/amd64
file: ${{ steps.export.outputs.path }}/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

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

@@ -0,0 +1,75 @@
name: Rust build, test, and generate specification
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 stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
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
- name: Copy .env.example
if: github.event_name != 'pull_request'
run: |
cp .env.example .env
- name: Run services in background
if: github.event_name != 'pull_request'
run: |
docker-compose -f docker-compose.db.yml up -d
- name: Start API in background
if: github.event_name != 'pull_request'
run: |
cargo run --bin revolt-delta &
- name: Wait for API to go up
if: github.event_name != 'pull_request'
uses: nev7n/wait_for_response@v1
with:
url: "http://localhost:8000/"
- name: Checkout API repository
if: github.event_name != 'pull_request'
uses: actions/checkout@v3
with:
repository: revoltchat/api
path: api
- name: Download OpenAPI specification
if: github.event_name != 'pull_request'
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
- name: Commit changes
if: github.event_name != 'pull_request'
uses: EndBug/add-and-commit@v4
with:
cwd: "api"
add: "*.json"
message: "chore: generate OpenAPI specification"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

@@ -0,0 +1,54 @@
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"){
projectV2(number: 3) {
id
fields(first:20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV

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

@@ -0,0 +1,79 @@
name: Add PR to Board
on:
pull_request_target:
types: [opened, synchronize, ready_for_review, review_requested]
jobs:
track_pr:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectV2(number: 3) {
id
fields(first:20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
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!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.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: updateProjectV2ItemFieldValue(input: {
projectId: $project
itemId: $item
fieldId: $status_field
value: {
singleSelectOptionId: $status_value
}
}) {
projectV2Item {
id
}
}
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent

9
.gitignore vendored
View File

@@ -1,7 +1,6 @@
Rocket.toml
/target
/target_backup
**/*.rs.bk
.mongo
target
.data
.env
avatar.png
.vercel

View File

@@ -1,20 +0,0 @@
stages:
- build
# Rocket does not currently compile on stable Rust.
# Once it does, we can uncomment this, and instead
# put `allow-failure: true` on the nightly build.
#
# rust-latest:
# stage: build
# image: rust:latest
# script:
# - cargo build --verbose
# - cargo test --verbose
rust-nightly:
stage: build
image: rustlang/rust:nightly
script:
- cargo build --verbose
- cargo test --verbose

View File

@@ -1,5 +1,4 @@
{
"rust-analyzer.diagnostics.disabled": [
"unresolved-macro-call"
]
"editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy"
}

3312
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,2 @@
[package]
name = "revolt"
# 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]
regex = "1"
md5 = "0.7.0"
log = "0.4.11"
ulid = "0.4.1"
rand = "0.7.3"
time = "0.2.16"
nanoid = "0.4.0"
base64 = "0.13.0"
linkify = "0.6.0"
dotenv = "0.15.0"
futures = "0.3.8"
chrono = "0.4.15"
num_enum = "0.5.1"
impl_ops = "0.1.1"
web-push = "0.7.2"
once_cell = "1.4.1"
bitfield = "0.13.2"
env_logger = "0.7.1"
serde_json = "1.0.57"
lazy_static = "1.4.0"
urlencoding = "1.1.1"
many-to-many = "0.1.2"
lettre = "0.10.0-alpha.1"
reqwest = { version = "0.10.8", features = ["json"] }
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.11", features = ["derive"] }
ctrlc = { version = "3.0", features = ["termination"] }
hive_pubsub = { version = "0.4.4", features = ["mongo"] }
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" }
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "1db825a0059639144ea8802f6d51b5d27c5b986e" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false }
[workspace]
members = ["crates/*"]

View File

@@ -1,25 +1,12 @@
# Build Stage
FROM ekidd/rust-musl-builder:nightly-2021-01-01 AS builder
FROM rustlang/rust:nightly-slim AS builder
USER 0:0
WORKDIR /home/rust/src
RUN USER=root cargo new --bin revolt
WORKDIR /home/rust/src/revolt
# Install build requirements
RUN apt-get update && apt-get install -y libssl-dev pkg-config
# Build all crates
COPY Cargo.toml Cargo.lock ./
COPY src/bin/dummy.rs ./src/bin/dummy.rs
RUN cargo build --release --bin dummy
COPY assets/templates ./assets/templates
COPY src ./src
RUN cargo build --release
# Bundle Stage
FROM alpine:latest
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
COPY --from=builder /home/rust/src/revolt/target/x86_64-unknown-linux-musl/release/revolt ./
COPY assets ./assets
EXPOSE 8000
EXPOSE 9000
ENV ROCKET_ADDRESS 0.0.0.0
ENV ROCKET_PORT 8000
CMD ["./revolt"]
COPY crates ./crates
RUN cargo build --locked --release

10
LICENSE
View File

@@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -629,8 +629,8 @@ 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
Revolt Project
Copyright (C) 2022 Pawel Makles
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
@@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found.
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/>.
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@@ -658,4 +658,4 @@ 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/>.
<http://www.gnu.org/licenses/>.

View File

@@ -1,20 +1,31 @@
![](assets/banner.png)
# Revolt Backend
Delta is a **blazing fast API server** built with Rust for the REVOLT platform.
This is a monorepo for the Revolt backend.
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.
| Crate | Path | Description |
| ---------------- | ---------------------------------------------- | ------------------------------------ |
| `delta` | [crates/delta](crates/delta) | REST API server |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
| `quark` | [crates/quark](crates/quark) | Models and logic |
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer |
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer |
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |-->
## Docker Helper Scripts
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
If you have Docker installed, you can use the helper scripts to deploy Revolt in your development environment.
## Resources
```bash
./build.sh # build Docker image
./run.sh # run Docker container
./monitor.sh # view container logs
./remove.sh # kill and remove container
```
### 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)
## 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
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE). Please check individual crates for further license information.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,62 +0,0 @@
<!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 role="presentation" 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 style="margin: 0;">
Reset your password by <a href="{{url}}">clicking here</a>.
</p>
<p>
Or by manually navigating to the URL: {{url}}
</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; <a style="color: white;" href="https://revolt.chat">Website</a> &middot; <a style="color: white;" href="https://gitlab.insrt.uk/revolt">Source Code</a></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,62 +0,0 @@
<!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 role="presentation" 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;">Verify your account!</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 style="margin: 0;">
Please verify your account by <a href="{{url}}">clicking here</a>.
</p>
<p>
Or by manually navigating to the URL: {{url}}
</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; <a style="color: white;" href="https://revolt.chat">Website</a> &middot; <a style="color: white;" href="https://gitlab.insrt.uk/revolt">Source Code</a></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

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

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

View File

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

View File

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

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

View File

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

1
crates/bonfire/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

28
crates/bonfire/Cargo.toml Normal file
View File

@@ -0,0 +1,28 @@
[package]
name = "revolt-bonfire"
version = "0.5.7"
license = "AGPL-3.0-or-later"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# util
log = "*"
once_cell = "1.9.0"
# parsing
querystring = "1.1.0"
# quark
revolt-quark = { path = "../quark" }
# serde
serde_json = "1.0.79"
rmp-serde = "1.0.0"
serde = "1.0.136"
# async
futures = "0.3.21"
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }

10
crates/bonfire/Dockerfile Normal file
View File

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

1
crates/bonfire/LICENSE Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE

View File

@@ -0,0 +1,150 @@
use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender;
use revolt_quark::{Error, Result};
use serde::{Deserialize, Serialize};
/// Enumeration of supported protocol formats
#[derive(Debug)]
pub enum ProtocolFormat {
Json,
Msgpack,
}
/// User-provided protocol configuration
#[derive(Debug)]
pub struct ProtocolConfiguration {
protocol_version: i32,
format: ProtocolFormat,
session_token: Option<String>,
}
impl ProtocolConfiguration {
/// Create a new protocol configuration object from provided data
pub fn from(
protocol_version: i32,
format: ProtocolFormat,
session_token: Option<String>,
) -> Self {
Self {
protocol_version,
format,
session_token,
}
}
/// Decode some WebSocket message into a T: Deserialize using the client's specified protocol format
pub fn decode<'a, T: Deserialize<'a>>(&self, msg: &'a Message) -> Result<T> {
match self.format {
ProtocolFormat::Json => {
if let Message::Text(text) = msg {
serde_json::from_str(text).map_err(|_| Error::InternalError)
} else {
Err(Error::InternalError)
}
}
ProtocolFormat::Msgpack => {
if let Message::Binary(buf) = msg {
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError)
} else {
Err(Error::InternalError)
}
}
}
}
/// Encode T: Serialize into a WebSocket message using the client's specified protocol format
pub fn encode<T: Serialize>(&self, data: &T) -> Message {
match self.format {
ProtocolFormat::Json => {
Message::Text(serde_json::to_string(data).expect("Failed to serialise (as json)."))
}
ProtocolFormat::Msgpack => Message::Binary(
rmp_serde::to_vec_named(data).expect("Failed to serialise (as msgpack)."),
),
}
}
/// Set the current session token
pub fn set_session_token(&mut self, token: String) {
self.session_token.replace(token);
}
/// Get the current session token
pub fn get_session_token(&self) -> &Option<String> {
&self.session_token
}
/// Get the protocol version specified
pub fn get_protocol_version(&self) -> i32 {
self.protocol_version
}
/// Get the protocol format specified
pub fn get_protocol_format(&self) -> &ProtocolFormat {
&self.format
}
}
/// Object holding one side of a channel for receiving the parsed information
pub struct WebsocketHandshakeCallback {
sender: Sender<ProtocolConfiguration>,
}
impl WebsocketHandshakeCallback {
/// Create a callback using a given sender
pub fn from(sender: Sender<ProtocolConfiguration>) -> Self {
Self { sender }
}
}
impl handshake::server::Callback for WebsocketHandshakeCallback {
/// Handle request to create a new WebSocket connection
fn on_request(
self,
request: &handshake::server::Request,
response: handshake::server::Response,
) -> Result<handshake::server::Response, handshake::server::ErrorResponse> {
// Take and parse query parameters from the URI.
let query = request.uri().query().unwrap_or_default();
let params = querystring::querify(query);
// Set default values for the protocol.
let mut protocol_version = 1;
let mut format = ProtocolFormat::Json;
let mut session_token = None;
// Parse and map parameters from key-value to known variables.
for (key, value) in params {
match key {
"version" => {
if let Ok(version) = value.parse() {
protocol_version = version;
}
}
"format" => match value {
"json" => format = ProtocolFormat::Json,
"msgpack" => format = ProtocolFormat::Msgpack,
_ => {}
},
"token" => session_token = Some(value.into()),
_ => {}
}
}
// Send configuration information back from this callback.
// We have to use a channel as this function does not borrow mutably.
if self
.sender
.send(ProtocolConfiguration {
protocol_version,
format,
session_token,
})
.is_ok()
{
Ok(response)
} else {
Err(handshake::server::ErrorResponse::new(None))
}
}
}

View File

@@ -0,0 +1,19 @@
use once_cell::sync::OnceCell;
use revolt_quark::{Database, DatabaseInfo};
static DBCONN: OnceCell<Database> = OnceCell::new();
/// Connect Bonfire to the database.
pub async fn connect() {
let database = DatabaseInfo::Auto
.connect()
.await
.expect("Failed to connect to the database.");
DBCONN.set(database).expect("Setting `Database`");
}
/// Get a reference to the current database.
pub fn get_db() -> &'static Database {
DBCONN.get().expect("Valid `Database`")
}

View File

@@ -0,0 +1,34 @@
use std::env;
use async_std::net::TcpListener;
use revolt_quark::presence::presence_clear_region;
#[macro_use]
extern crate log;
pub mod config;
mod database;
mod websocket;
#[async_std::main]
async fn main() {
// Configure requirements for Bonfire.
revolt_quark::configure!();
database::connect().await;
// Clean up the current region information.
presence_clear_region(None).await;
// Setup a TCP listener to accept WebSocket connections on.
// By default, we bind to port 9000 on all interfaces.
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:9000".into());
info!("Listening on host {bind}");
let try_socket = TcpListener::bind(bind).await;
let listener = try_socket.expect("Failed to bind");
// Start accepting new connections and spawn a client for each connection.
while let Ok((stream, addr)) = listener.accept().await {
websocket::spawn_client(database::get_db(), stream, addr);
}
}

View File

@@ -0,0 +1,253 @@
use std::net::SocketAddr;
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
use revolt_quark::{
events::{
client::EventV1,
server::ClientMessage,
state::{State, SubscriptionStateChange},
},
models::{user::UserHint, User},
presence::{presence_create_session, presence_delete_session},
redis_kiss, Database,
};
use async_std::{net::TcpStream, sync::Mutex, task};
use crate::config::WebsocketHandshakeCallback;
/// Spawn a new WebSocket client worker given access to the database,
/// the relevant TCP stream and the remote address of the client.
pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr) {
// Spawn a new Async task to work on.
task::spawn(async move {
info!("User connected from {addr:?}");
// Upgrade the TCP connection to a WebSocket connection.
// In this process, we also parse any additional parameters given.
// e.g. wss://example.com?format=json&version=1
let (sender, receiver) = oneshot::channel();
if let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
stream,
WebsocketHandshakeCallback::from(sender),
None,
)
.await
{
// Verify we've received a valid config, otherwise we should just drop the connection.
if let Ok(mut config) = receiver.await {
info!(
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
config.get_protocol_version(),
config.get_protocol_format()
);
// Split the socket for simultaneously read and write.
let (write, mut read) = ws.split();
let write = Mutex::new(write);
// If the user has not provided authentication, request information.
if config.get_session_token().is_none() {
'outer: while let Ok(message) = read.try_next().await {
if let Ok(ClientMessage::Authenticate { token }) =
config.decode(message.as_ref().unwrap())
{
config.set_session_token(token);
break 'outer;
}
}
}
// Try to authenticate the user.
if let Some(token) = config.get_session_token().as_ref() {
match User::from_token(db, token, UserHint::Any).await {
Ok(user) => {
info!("User {addr:?} authenticated as @{}", user.username);
// Create local state.
let mut state = State::from(user);
let user_id = state.cache.user_id.clone();
// Create presence session.
let (first_session, session_id) =
presence_create_session(&user_id, 0).await;
// Notify socket we have authenticated.
write
.lock()
.await
.send(config.encode(&EventV1::Authenticated))
.await
.ok();
// Download required data to local cache and send Ready payload.
if let Ok(ready_payload) = state.generate_ready_payload(db).await {
write
.lock()
.await
.send(config.encode(&ready_payload))
.await
.ok();
// If this was the first session, notify other users that we just went online.
if first_session {
state.broadcast_presence_change(true).await;
}
// Create a PubSub connection to poll on.
let listener = async {
if let Ok(mut conn) = redis_kiss::open_pubsub_connection().await
{
loop {
// Check for state changes for subscriptions.
match state.apply_state() {
SubscriptionStateChange::Reset => {
for id in state.iter_subscriptions() {
conn.subscribe(id).await.unwrap();
}
#[cfg(debug_assertions)]
info!("{addr:?} has reset their subscriptions");
}
SubscriptionStateChange::Change { add, remove } => {
for id in remove {
#[cfg(debug_assertions)]
info!("{addr:?} unsubscribing from {id}");
conn.unsubscribe(id).await.unwrap();
}
for id in add {
#[cfg(debug_assertions)]
info!("{addr:?} subscribing to {id}");
conn.subscribe(id).await.unwrap();
}
}
SubscriptionStateChange::None => {}
}
// * Debug logging of current subscriptions.
/*#[cfg(debug_assertions)]
info!(
"User {addr:?} is subscribed to {:?}",
state
.iter_subscriptions()
.collect::<Vec<&String>>()
);*/
// Handle incoming events.
match conn.on_message().next().await.map(|item| {
(
item.get_channel_name().to_string(),
redis_kiss::decode_payload::<EventV1>(&item),
)
}) {
Some((channel, item)) => {
if let Ok(mut event) = item {
if state
.handle_incoming_event_v1(
db, &mut event,
)
.await
&& write.lock().await
.send(config.encode(&event))
.await
.is_err()
{
break;
}
} else {
warn!("Failed to deserialise an event for {channel}!");
}
}
// No more data, assume we disconnected or otherwise
// something bad occurred, so disconnect user.
None => break,
}
}
}
}
.fuse();
// Read from WebSocket stream.
let worker =
async {
while let Ok(Some(msg)) = read.try_next().await {
if let Ok(payload) = config.decode(&msg) {
match payload {
ClientMessage::BeginTyping { channel } => {
EventV1::ChannelStartTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::EndTyping { channel } => {
EventV1::ChannelStopTyping {
id: channel.clone(),
user: user_id.clone(),
}
.p(channel.clone())
.await;
}
ClientMessage::Ping { data, responded } => {
if responded.is_none() {
write
.lock()
.await
.send(config.encode(
&EventV1::Pong { data },
))
.await
.ok();
}
}
_ => {}
}
}
}
}
.fuse();
// Pin both tasks.
pin_mut!(listener, worker);
// Wait for either disconnect or for listener to die.
select!(
() = listener => {},
() = worker => {}
);
// * Combine the streams back once we are ready to disconnect.
/* ws = read.reunite(write).unwrap(); */
}
// Clean up presence session.
let last_session = presence_delete_session(&user_id, session_id).await;
// If this was the last session, notify other users that we just went offline.
if last_session {
state.broadcast_presence_change(false).await;
}
}
Err(err) => {
write.lock().await.send(config.encode(&err)).await.ok();
}
}
}
}
// * Disconnect the WebSocket if it isn't already.
/*ws.close(Some(CloseFrame {
code: CloseCode::Normal,
reason: std::borrow::Cow::from(""),
}))
.await
.unwrap();*/
}
info!("User disconnected from {addr:?}");
});
}

65
crates/delta/Cargo.toml Normal file
View File

@@ -0,0 +1,65 @@
[package]
name = "revolt-delta"
version = "0.5.7"
license = "AGPL-3.0-or-later"
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]
# Utility
lru = "0.7.0"
url = "2.2.2"
log = "0.4.11"
dotenv = "0.15.0"
dashmap = "5.2.0"
linkify = "0.6.0"
once_cell = "1.4.1"
env_logger = "0.7.1"
lazy_static = "1.4.0"
# Lang. Utilities
regex = "1"
num_enum = "0.5.1"
impl_ops = "0.1.1"
bitfield = "0.13.2"
# ID / key generation
ulid = "0.4.1"
nanoid = "0.4.0"
# serde
serde_json = "1.0.57"
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.14", features = ["derive"] }
# async
futures = "0.3.8"
chrono = "0.4.15"
async-channel = "1.6.1"
reqwest = { version = "0.11.4", features = ["json"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
# internal util
lettre = "0.10.0-alpha.4"
# 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
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_authifier = { version = "1.0.7" }
# spec generation
schemars = "0.8.8"
revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
# quark
revolt-quark = { path = "../quark" }
[build-dependencies]
vergen = "7.5.0"

13
crates/delta/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
# Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder
RUN cargo install --locked --path crates/delta
# Bundle Stage
FROM debian:buster-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /usr/local/cargo/bin/revolt-delta ./
EXPOSE 8000
ENV ROCKET_ADDRESS 0.0.0.0
ENV ROCKET_PORT 8000
CMD ["./revolt-delta"]

1
crates/delta/LICENSE Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE

16
crates/delta/build.rs Normal file
View File

@@ -0,0 +1,16 @@
use vergen::{vergen, Config};
use std::process::Command;
fn main() {
if let Ok(output) = Command::new("git")
.args(["config", "--get", "remote.origin.url"])
.output()
{
if let Ok(git_origin) = String::from_utf8(output.stdout) {
println!("cargo:rustc-env=GIT_ORIGIN_URL={}", git_origin);
}
}
vergen(Config::default()).ok();
}

73
crates/delta/src/main.rs Normal file
View File

@@ -0,0 +1,73 @@
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate revolt_rocket_okapi;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate lazy_static;
pub mod routes;
pub mod util;
use async_std::channel::unbounded;
use revolt_quark::authifier::{Authifier, AuthifierEvent};
use revolt_quark::events::client::EventV1;
use revolt_quark::DatabaseInfo;
#[launch]
async fn rocket() -> _ {
// Configure logging and environment
revolt_quark::configure!();
// Ensure environment variables are present
revolt_quark::variables::delta::preflight_checks();
// Setup database
let db = DatabaseInfo::Auto.connect().await.unwrap();
db.migrate_database().await.unwrap();
// Setup Authifier event channel
let (sender, receiver) = unbounded();
// Setup Authifier
let authifier = Authifier {
database: db.clone().into(),
config: revolt_quark::util::authifier::config(),
event_channel: Some(sender),
};
// Launch a listener for Authifier events
async_std::task::spawn(async move {
while let Ok(event) = receiver.recv().await {
match &event {
AuthifierEvent::CreateSession { .. } | AuthifierEvent::CreateAccount { .. } => {
EventV1::Auth(event).global().await
}
AuthifierEvent::DeleteSession { user_id, .. }
| AuthifierEvent::DeleteAllSessions { user_id, .. } => {
let id = user_id.to_string();
EventV1::Auth(event).private(id).await
}
}
}
});
// Launch background task workers
async_std::task::spawn(revolt_quark::tasks::start_workers(db.clone()));
// Configure CORS
let cors = revolt_quark::web::cors::new();
// Configure Rocket
let rocket = rocket::build();
routes::mount(rocket)
.mount("/", revolt_quark::web::cors::catch_all_options_routes())
.mount("/", revolt_quark::web::ratelimiter::routes())
.mount("/swagger/", revolt_quark::web::swagger::routes())
.manage(authifier)
.manage(db)
.manage(cors.clone())
.attach(revolt_quark::web::ratelimiter::RatelimitFairing)
.attach(cors)
}

View File

@@ -0,0 +1,65 @@
use crate::util::regex::RE_USERNAME;
use nanoid::nanoid;
use revolt_quark::{
models::{user::BotInformation, Bot, User},
variables::delta::MAX_BOT_COUNT,
Db, Error, Result,
};
use rocket::serde::json::Json;
use serde::Deserialize;
use ulid::Ulid;
use validator::Validate;
/// # Bot Details
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataCreateBot {
/// Bot username
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
name: String,
}
/// # Create Bot
///
/// Create a new Revolt bot.
#[openapi(tag = "Bots")]
#[post("/create", data = "<info>")]
pub async fn create_bot(db: &Db, user: User, info: Json<DataCreateBot>) -> Result<Json<Bot>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
if db.get_number_of_bots_by_user(&user.id).await? >= *MAX_BOT_COUNT {
return Err(Error::ReachedMaximumBots);
}
if db.is_username_taken(&info.name).await? {
return Err(Error::UsernameTaken);
}
let id = Ulid::new().to_string();
let bot_user = User {
id: id.clone(),
username: info.name.trim().to_string(),
bot: Some(BotInformation {
owner: user.id.clone(),
}),
..Default::default()
};
let bot = Bot {
id,
owner: user.id,
token: nanoid!(64),
..Default::default()
};
db.insert_user(&bot_user).await?;
db.insert_bot(&bot).await?;
Ok(Json(bot))
}

View File

@@ -0,0 +1,19 @@
use revolt_quark::{models::User, Db, EmptyResponse, Error, Ref, Result};
/// # Delete Bot
///
/// Delete a bot by its id.
#[openapi(tag = "Bots")]
#[delete("/<target>")]
pub async fn delete_bot(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let bot = target.as_bot(db).await?;
if bot.owner != user.id {
return Err(Error::NotFound);
}
bot.delete(db).await.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,107 @@
use crate::util::regex::RE_USERNAME;
use revolt_quark::{
models::{
bot::{FieldsBot, PartialBot},
Bot, User,
},
Db, Error, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Bot Details
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditBot {
/// Bot username
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
/// Whether the bot can be added by anyone
public: Option<bool>,
/// Whether analytics should be gathered for this bot
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
analytics: Option<bool>,
/// Interactions URL
#[validate(length(min = 1, max = 2048))]
interactions_url: Option<String>,
/// Fields to remove from bot object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsBot>>,
}
/// # Edit Bot
///
/// Edit bot details by its id.
#[openapi(tag = "Bots")]
#[patch("/<target>", data = "<data>")]
pub async fn edit_bot(
db: &Db,
user: User,
target: Ref,
data: Json<DataEditBot>,
) -> Result<Json<Bot>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let mut bot = target.as_bot(db).await?;
if bot.owner != user.id {
return Err(Error::NotFound);
}
if let Some(name) = data.name {
if db.is_username_taken(&name).await? {
return Err(Error::UsernameTaken);
}
let mut user = db.fetch_user(&bot.id).await?;
user.update_username(db, name).await?;
}
if data.public.is_none()
&& data.analytics.is_none()
&& data.interactions_url.is_none()
&& data.remove.is_none()
{
return Ok(Json(bot));
}
let DataEditBot {
public,
analytics,
interactions_url,
remove,
..
} = data;
let mut partial = PartialBot {
public,
analytics,
interactions_url,
..Default::default()
};
if let Some(remove) = &remove {
for field in remove {
bot.remove(field);
}
if remove.iter().any(|x| x == &FieldsBot::Token) {
partial.token = Some(bot.token.clone());
}
}
db.update_bot(&bot.id, &partial, remove.unwrap_or_default())
.await?;
bot.apply_options(partial);
Ok(Json(bot))
}

View File

@@ -0,0 +1,36 @@
use revolt_quark::{
models::{Bot, User},
Db, Error, Ref, Result,
};
use rocket::serde::json::Json;
use serde::Serialize;
/// # Bot Response
#[derive(Serialize, JsonSchema)]
pub struct BotResponse {
/// Bot object
bot: Bot,
/// User object
user: User,
}
/// # Fetch Bot
///
/// Fetch details of a bot you own by its id.
#[openapi(tag = "Bots")]
#[get("/<target>")]
pub async fn fetch_bot(db: &Db, user: User, target: Ref) -> Result<Json<BotResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let bot = target.as_bot(db).await?;
if bot.owner != user.id {
return Err(Error::NotFound);
}
Ok(Json(BotResponse {
user: db.fetch_user(&bot.id).await?.foreign(),
bot,
}))
}

View File

@@ -0,0 +1,42 @@
use revolt_quark::{
models::{Bot, User},
Db, Error, Result,
};
use rocket::serde::json::Json;
use serde::Serialize;
/// # Owned Bots Response
///
/// Both lists are sorted by their IDs.
#[derive(Serialize, JsonSchema)]
pub struct OwnedBotsResponse {
/// Bot objects
bots: Vec<Bot>,
/// User objects
users: Vec<User>,
}
/// # Fetch Owned Bots
///
/// Fetch all of the bots that you have control over.
#[openapi(tag = "Bots")]
#[get("/@me")]
pub async fn fetch_owned_bots(db: &Db, user: User) -> Result<Json<OwnedBotsResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let mut bots = db.fetch_bots_by_user(&user.id).await?;
let user_ids = bots
.iter()
.map(|x| x.id.to_owned())
.collect::<Vec<String>>();
let mut users = db.fetch_users(&user_ids).await?;
// Ensure the lists match up exactly.
bots.sort_by(|a, b| a.id.cmp(&b.id));
users.sort_by(|a, b| a.id.cmp(&b.id));
Ok(Json(OwnedBotsResponse { users, bots }))
}

View File

@@ -0,0 +1,44 @@
use revolt_quark::{
models::{File, User},
Db, Error, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
/// # Public Bot
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct PublicBot {
/// Bot Id
#[serde(rename = "_id")]
id: String,
/// Bot Username
username: String,
/// Profile Avatar
#[serde(skip_serializing_if = "Option::is_none")]
avatar: Option<File>,
/// Profile Description
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
/// # Fetch Public Bot
///
/// Fetch details of a public (or owned) bot by its id.
#[openapi(tag = "Bots")]
#[get("/<target>/invite")]
pub async fn fetch_public_bot(db: &Db, user: Option<User>, target: Ref) -> Result<Json<PublicBot>> {
let bot = target.as_bot(db).await?;
if !bot.public && user.map_or(true, |x| x.id != bot.owner) {
return Err(Error::NotFound);
}
let user = db.fetch_user(&bot.id).await?;
Ok(Json(PublicBot {
id: bot.id,
username: user.username,
avatar: user.avatar,
description: user.profile.and_then(|p| p.content),
}))
}

View File

@@ -0,0 +1,71 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
use rocket::serde::json::Json;
use serde::Deserialize;
/// # Invite Destination
#[derive(Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum InviteBotDestination {
/// Invite to a server
Server {
/// Server Id
server: String,
},
/// Invite to a group
Group {
/// Group Id
group: String,
},
}
/// # Invite Bot
///
/// Invite a bot to a server or group by its id.`
#[openapi(tag = "Bots")]
#[post("/<target>/invite", data = "<dest>")]
pub async fn invite_bot(
db: &Db,
user: User,
target: Ref,
dest: Json<InviteBotDestination>,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let bot = target.as_bot(db).await?;
if !bot.public && bot.owner != user.id {
return Err(Error::BotIsPrivate);
}
match dest.into_inner() {
InviteBotDestination::Server { server } => {
let server = db.fetch_server(&server).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageServer)
.await?;
let user = db.fetch_user(&bot.id).await?;
server
.create_member(db, user, None)
.await
.map(|_| EmptyResponse)
}
InviteBotDestination::Group { group } => {
let mut channel = db.fetch_channel(&group).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::InviteOthers)
.await?;
channel
.add_user_to_group(db, &bot.id, &user.id)
.await
.map(|_| EmptyResponse)
}
}
}

View File

@@ -0,0 +1,22 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod create;
mod delete;
mod edit;
mod fetch;
mod fetch_owned;
mod fetch_public;
mod invite;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
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,23 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
/// # Acknowledge Message
///
/// Lets the server and all other clients know that we've seen this message id in this channel.
#[openapi(tag = "Messaging")]
#[put("/<target>/ack/<message>")]
pub async fn req(db: &Db, user: User, target: Ref, message: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
channel
.ack(&user.id, &message.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,60 @@
use revolt_quark::{
models::{channel::PartialChannel, Channel, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
leave_silently: Option<bool>,
}
/// # Close Channel
///
/// Deletes a server channel, leaves a group or closes a group.
#[openapi(tag = "Channel Information")]
#[delete("/<target>?<options..>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
options: OptionsChannelDelete,
) -> Result<EmptyResponse> {
let mut channel = target.as_channel(db).await?;
let mut perms = perms(&user).channel(&channel);
perms.throw_permission(db, Permission::ViewChannel).await?;
match &channel {
Channel::SavedMessages { .. } => Err(Error::NoEffect),
Channel::DirectMessage { .. } => channel
.update(
db,
PartialChannel {
active: Some(false),
..Default::default()
},
vec![],
)
.await
.map(|_| EmptyResponse),
Channel::Group { .. } => channel
.remove_user_from_group(
db,
&user.id,
None,
options.leave_silently.unwrap_or_default(),
)
.await
.map(|_| EmptyResponse),
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
perms
.throw_permission(db, Permission::ManageChannel)
.await?;
channel.delete(db).await.map(|_| EmptyResponse)
}
}
}

View File

@@ -0,0 +1,210 @@
use revolt_quark::{
models::{
channel::{Channel, FieldsChannel, PartialChannel},
message::SystemMessage,
File, User,
},
perms, Database, Error, Permission, Ref, Result,
};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Channel Details
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditChannel {
/// Channel name
#[validate(length(min = 1, max = 32))]
name: Option<String>,
/// Channel description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Group owner
owner: Option<String>,
/// Icon
///
/// Provide an Autumn attachment Id.
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
/// Whether this channel is age-restricted
nsfw: Option<bool>,
/// Whether this channel is archived
archived: Option<bool>,
#[validate(length(min = 1))]
remove: Option<Vec<FieldsChannel>>,
}
/// # Edit Channel
///
/// Edit a channel object by its id.
#[openapi(tag = "Channel Information")]
#[patch("/<target>", data = "<data>")]
pub async fn req(
db: &State<Database>,
user: User,
target: Ref,
data: Json<DataEditChannel>,
) -> Result<Json<Channel>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let mut channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageChannel)
.await?;
if data.name.is_none()
&& data.description.is_none()
&& data.icon.is_none()
&& data.nsfw.is_none()
&& data.owner.is_none()
&& data.remove.is_none()
{
return Ok(Json(channel));
}
let mut partial: PartialChannel = Default::default();
// Transfer group ownership
if let Some(new_owner) = data.owner {
if let Channel::Group {
owner, recipients, ..
} = &mut channel
{
// Make sure we are the owner of this group
if owner != &user.id {
return Err(Error::NotOwner);
}
// Ensure user is part of group
if !recipients.contains(&new_owner) {
return Err(Error::NotInGroup);
}
// Transfer ownership
partial.owner = Some(new_owner.to_string());
let old_owner = std::mem::replace(owner, new_owner.to_string());
// Notify clients
SystemMessage::ChannelOwnershipChanged {
from: old_owner,
to: new_owner,
}
} else {
return Err(Error::InvalidOperation);
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
}
match &mut channel {
Channel::Group {
id,
name,
description,
icon,
nsfw,
..
}
| Channel::TextChannel {
id,
name,
description,
icon,
nsfw,
..
}
| Channel::VoiceChannel {
id,
name,
description,
icon,
nsfw,
..
} => {
if let Some(fields) = &data.remove {
if fields.contains(&FieldsChannel::Icon) {
if let Some(icon) = &icon {
db.mark_attachment_as_deleted(&icon.id).await?;
}
}
for field in fields {
match field {
FieldsChannel::Description => {
description.take();
}
FieldsChannel::Icon => {
icon.take();
}
_ => {}
}
}
}
if let Some(icon_id) = data.icon {
partial.icon = Some(File::use_icon(db, &icon_id, id).await?);
*icon = partial.icon.clone();
}
if let Some(new_name) = data.name {
*name = new_name.clone();
partial.name = Some(new_name);
}
if let Some(new_description) = data.description {
partial.description = Some(new_description);
*description = partial.description.clone();
}
if let Some(new_nsfw) = data.nsfw {
*nsfw = new_nsfw;
partial.nsfw = Some(new_nsfw);
}
// Send out mutation system messages.
if let Channel::Group { .. } = &channel {
if let Some(name) = &partial.name {
SystemMessage::ChannelRenamed {
name: name.to_string(),
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
}
if partial.description.is_some() {
SystemMessage::ChannelDescriptionChanged {
by: user.id.clone(),
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
}
if partial.icon.is_some() {
SystemMessage::ChannelIconChanged { by: user.id }
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
}
}
channel
.update(db, partial, data.remove.unwrap_or_default())
.await?;
}
_ => return Err(Error::InvalidOperation),
};
Ok(Json(channel))
}

View File

@@ -0,0 +1,21 @@
use revolt_quark::{
models::{Channel, User},
perms, Database, Permission, Ref, Result,
};
use rocket::{serde::json::Json, State};
/// # Fetch Channel
///
/// Fetch channel by its id.
#[openapi(tag = "Channel Information")]
#[get("/<target>")]
pub async fn req(db: &State<Database>, user: User, target: Ref) -> Result<Json<Channel>> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
Ok(Json(channel))
}

View File

@@ -0,0 +1,32 @@
use revolt_quark::{
models::{Channel, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
};
/// # Add Member to Group
///
/// Adds another user to the group.
#[openapi(tag = "Groups")]
#[put("/<target>/recipients/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let mut channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::InviteOthers)
.await?;
match &channel {
Channel::Group { .. } => {
let member = member.as_user(db).await?;
channel
.add_user_to_group(db, &member.id, &user.id)
.await
.map(|_| EmptyResponse)
}
_ => Err(Error::InvalidOperation),
}
}

View File

@@ -0,0 +1,84 @@
use std::{collections::HashSet, iter::FromIterator};
use revolt_quark::{
get_relationship,
models::{user::RelationshipStatus, Channel, User},
variables::delta::MAX_GROUP_SIZE,
Db, Error, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
/// # Group Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataCreateGroup {
/// Group name
#[validate(length(min = 1, max = 32))]
name: String,
/// Group description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Array of user IDs to add to the group
///
/// Must be friends with these users.
#[validate(length(min = 0, max = 49))]
users: Vec<String>,
/// Whether this group is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Group
///
/// Create a new group channel.
#[openapi(tag = "Groups")]
#[post("/create", data = "<info>")]
pub async fn req(db: &Db, user: User, info: Json<DataCreateGroup>) -> Result<Json<Channel>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let mut set: HashSet<String> = HashSet::from_iter(info.users.into_iter());
set.insert(user.id.clone());
if set.len() > *MAX_GROUP_SIZE {
return Err(Error::GroupTooLarge {
max: *MAX_GROUP_SIZE,
});
}
for target in &set {
match get_relationship(&user, target) {
RelationshipStatus::Friend | RelationshipStatus::User => {}
_ => {
return Err(Error::NotFriends);
}
}
}
let group = Channel::Group {
id: Ulid::new().to_string(),
name: info.name,
owner: user.id,
description: info.description,
recipients: set.into_iter().collect::<Vec<String>>(),
icon: None,
last_message_id: None,
permissions: None,
nsfw: info.nsfw.unwrap_or(false),
};
group.create(db).await?;
Ok(Json(group))
}

View File

@@ -0,0 +1,42 @@
use revolt_quark::{
models::{Channel, User},
Db, EmptyResponse, Error, Permission, Ref, Result,
};
/// # Remove Member from Group
///
/// Removes a user from the group.
#[openapi(tag = "Groups")]
#[delete("/<target>/recipients/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let channel = target.as_channel(db).await?;
match &channel {
Channel::Group {
owner, recipients, ..
} => {
if &user.id != owner {
return Error::from_permission(Permission::ManageChannel);
}
let member = member.as_user(db).await?;
if user.id == member.id {
return Err(Error::CannotRemoveYourself);
}
if !recipients.iter().any(|x| *x == member.id) {
return Err(Error::NotInGroup);
}
channel
.remove_user_from_group(db, &member.id, Some(&user.id), false)
.await
.map(|_| EmptyResponse)
}
_ => Err(Error::InvalidOperation),
}
}

View File

@@ -0,0 +1,27 @@
use revolt_quark::{
models::{Invite, User},
perms, Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
/// # Create Invite
///
/// Creates an invite to this channel.
///
/// Channel must be a `TextChannel`.
#[openapi(tag = "Channel Invites")]
#[post("/<target>/invites")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Invite>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::InviteOthers)
.await?;
Invite::create(db, &user, &channel).await.map(Json)
}

View File

@@ -0,0 +1,35 @@
use revolt_quark::{
models::{Channel, User},
perms, Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
/// # Fetch Group Members
///
/// Retrieves all users who are part of this group.
#[openapi(tag = "Groups")]
#[get("/<target>/members")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
if let Channel::Group { recipients, .. } = channel {
Ok(Json(
db.fetch_users(&recipients)
.await?
.into_iter()
.map(|x| x.with_relationship(&user))
.collect::<Vec<User>>(),
))
} else {
Err(Error::InvalidOperation)
}
}

View File

@@ -0,0 +1,59 @@
use chrono::Utc;
use revolt_quark::{
models::{Message, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
use serde::Deserialize;
use validator::Validate;
/// # Search Parameters
#[derive(Validate, Deserialize, JsonSchema)]
pub struct OptionsBulkDelete {
/// Message IDs
#[validate(length(min = 1, max = 100))]
ids: Vec<String>,
}
/// # Bulk Delete Messages
///
/// Delete multiple messages you've sent or one you have permission to delete.
///
/// This will always require `ManageMessages` permission regardless of whether you own the message or not.
///
/// Messages must have been sent within the past 1 week.
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
pub async fn req(
db: &Db,
user: User,
target: Ref,
options: Json<OptionsBulkDelete>,
) -> Result<EmptyResponse> {
let options = options.into_inner();
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
for id in &options.ids {
if ulid::Ulid::from_string(id)
.map_err(|_| Error::InvalidOperation)?
.datetime()
.signed_duration_since(Utc::now())
.num_days()
.abs()
> 7
{
return Err(Error::InvalidOperation);
}
}
perms(&user)
.channel(&target.as_channel(db).await?)
.throw_permission(db, Permission::ManageMessages)
.await?;
Message::bulk_delete(db, &target.id, options.ids)
.await
.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,34 @@
use revolt_quark::{
models::{message::PartialMessage, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
};
/// # Remove All Reactions from Message
///
/// Remove your own, someone else's or all of a given reaction.
///
/// Requires `ManageMessages` permission.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions")]
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageMessages)
.await?;
// Fetch relevant message
let mut message = msg.as_message_in(db, channel.id()).await?;
// Clear reactions
message
.update(
db,
PartialMessage {
reactions: Some(Default::default()),
..Default::default()
},
)
.await
.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,22 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
/// # Delete Message
///
/// Delete a message you've sent or one you have permission to delete.
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/<msg>", rank = 2)]
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let message = msg.as_message(db).await?;
if message.channel != target.id {
return Err(Error::NotFound);
}
if message.author != user.id {
perms(&user)
.channel(&target.as_channel(db).await?)
.throw_permission(db, Permission::ManageMessages)
.await?;
}
message.delete(db).await.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,95 @@
use revolt_quark::{
models::message::{PartialMessage, SendableEmbed},
models::{Message, User},
types::january::Embed,
Db, Error, Ref, Result, Timestamp,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Message Details
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataEditMessage {
/// New message content
#[validate(length(min = 1, max = 2000))]
content: Option<String>,
/// Embeds to include in the message
#[validate(length(min = 0, max = 10))]
embeds: Option<Vec<SendableEmbed>>,
}
/// # Edit Message
///
/// Edits a message that you've previously sent.
#[openapi(tag = "Messaging")]
#[patch("/<target>/messages/<msg>", data = "<edit>")]
pub async fn req(
db: &Db,
user: User,
target: String,
msg: Ref,
edit: Json<DataEditMessage>,
) -> Result<Json<Message>> {
let edit = edit.into_inner();
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
Message::validate_sum(&edit.content, &edit.embeds)?;
let mut message = msg.as_message(db).await?;
if message.channel != target {
return Err(Error::NotFound);
}
if message.author != user.id {
return Err(Error::CannotEditMessage);
}
message.edited = Some(Timestamp::now_utc());
let mut partial = PartialMessage {
edited: message.edited,
..Default::default()
};
// 1. Handle content update
if let Some(content) = &edit.content {
partial.content = Some(content.clone());
}
// 2. Clear any auto generated embeds
let mut new_embeds: Vec<Embed> = vec![];
if let Some(embeds) = &message.embeds {
for embed in embeds {
if let Embed::Text(embed) = embed {
new_embeds.push(Embed::Text(embed.clone()))
}
}
}
// 3. Replace if we are given new embeds
if let Some(embeds) = edit.embeds {
new_embeds.clear();
for embed in embeds {
new_embeds.push(embed.clone().into_embed(db, message.id.clone()).await?);
}
}
partial.embeds = Some(new_embeds);
message.update(db, partial).await?;
// Queue up a task for processing embeds
if let Some(content) = edit.content {
revolt_quark::tasks::process_embeds::queue(
message.channel.to_string(),
message.id.to_string(),
content,
)
.await;
}
Ok(Json(message))
}

View File

@@ -0,0 +1,26 @@
use revolt_quark::{
models::{Message, User},
perms, Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
/// # Fetch Message
///
/// Retrieves a message by its id.
#[openapi(tag = "Messaging")]
#[get("/<target>/messages/<msg>")]
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<Json<Message>> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission(db, Permission::ViewChannel)
.await?;
let message = msg.as_message(db).await?;
if message.channel != channel.as_id() {
return Err(Error::NotFound);
}
Ok(Json(message))
}

View File

@@ -0,0 +1,81 @@
use revolt_quark::{
models::{
message::{BulkMessageResponse, MessageSort},
User,
},
perms, Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsQueryMessages {
/// Maximum number of messages to fetch
///
/// For fetching nearby messages, this is \`(limit + 1)\`.
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
/// Message id before which messages should be fetched
#[validate(length(min = 26, max = 26))]
before: Option<String>,
/// Message id after which messages should be fetched
#[validate(length(min = 26, max = 26))]
after: Option<String>,
/// Message sort direction
sort: Option<MessageSort>,
/// Message id to search around
///
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
/// It will also take half of limit rounded as the limits to each side.
/// It also fetches the message ID specified.
#[validate(length(min = 26, max = 26))]
nearby: Option<String>,
/// Whether to include user (and member, if server channel) objects
include_users: Option<bool>,
}
/// # Fetch Messages
///
/// Fetch multiple messages.
#[openapi(tag = "Messaging")]
#[get("/<target>/messages?<options..>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
options: OptionsQueryMessages,
) -> Result<Json<BulkMessageResponse>> {
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
if let Some(MessageSort::Relevance) = options.sort {
return Err(Error::InvalidOperation);
}
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ReadMessageHistory)
.await?;
let OptionsQueryMessages {
limit,
before,
after,
sort,
nearby,
include_users,
..
} = options;
let messages = db
.fetch_messages(channel.id(), limit, before, after, sort, nearby)
.await?;
BulkMessageResponse::transform(db, &channel, messages, include_users)
.await
.map(Json)
}

View File

@@ -0,0 +1,26 @@
use revolt_quark::{models::User, Ref, Result};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct OptionsQueryStale {
/// Array of message IDs
#[validate(length(min = 0, max = 150))]
ids: Vec<String>,
}
/// # Poll Message Changes
///
/// This route returns any changed message objects and tells you if any have been deleted.
///
/// Don't actually poll this route, instead use this to update your local database.
///
/// **DEPRECATED**
#[openapi(tag = "Messaging")]
#[post("/<_target>/messages/stale", data = "<_data>")]
pub async fn req(_user: User, _target: Ref, _data: Json<OptionsQueryStale>) -> Result<()> {
Ok(())
}

View File

@@ -0,0 +1,29 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
/// # Add Reaction to Message
///
/// React to a given message.
#[openapi(tag = "Interactions")]
#[put("/<target>/messages/<msg>/reactions/<emoji>")]
pub async fn react_message(
db: &Db,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::React)
.await?;
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
// Add the reaction
message
.add_reaction(db, &user, &emoji.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,82 @@
use revolt_quark::{
models::{
message::{BulkMessageResponse, MessageSort},
User,
},
perms, Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Search Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct OptionsMessageSearch {
/// Full-text search query
///
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
#[validate(length(min = 1, max = 64))]
query: String,
/// Maximum number of messages to fetch
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
/// Message id before which messages should be fetched
#[validate(length(min = 26, max = 26))]
before: Option<String>,
/// Message id after which messages should be fetched
#[validate(length(min = 26, max = 26))]
after: Option<String>,
/// Message sort direction
///
/// By default, it will be sorted by relevance.
#[serde(default = "MessageSort::default")]
sort: MessageSort,
/// Whether to include user (and member, if server channel) objects
include_users: Option<bool>,
}
/// # Search for Messages
///
/// This route searches for messages within the given parameters.
#[openapi(tag = "Messaging")]
#[post("/<target>/search", data = "<options>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
options: Json<OptionsMessageSearch>,
) -> Result<Json<BulkMessageResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let options = options.into_inner();
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ReadMessageHistory)
.await?;
let OptionsMessageSearch {
query,
limit,
before,
after,
sort,
include_users,
} = options;
let messages = db
.search_messages(channel.id(), &query, limit, before, after, sort)
.await?;
BulkMessageResponse::transform(db, &channel, messages, include_users)
.await
.map(Json)
}

View File

@@ -0,0 +1,231 @@
use std::collections::HashSet;
use revolt_quark::{
models::{
message::{Interactions, Masquerade, Reply, SendableEmbed},
Message, User,
},
perms,
web::idempotency::IdempotencyKey,
Db, Error, Permission, Ref, Result,
};
use regex::Regex;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataMessageSend {
/// Unique token to prevent duplicate message sending
///
/// **This is deprecated and replaced by `Idempotency-Key`!**
#[validate(length(min = 1, max = 64))]
nonce: Option<String>,
/// Message content to send
#[validate(length(min = 0, max = 2000))]
content: Option<String>,
/// Attachments to include in message
#[validate(length(min = 1, max = 128))]
attachments: Option<Vec<String>>,
/// Messages to reply to
replies: Option<Vec<Reply>>,
/// Embeds to include in message
///
/// Text embed content contributes to the content length cap
#[validate(length(min = 1, max = 10))]
embeds: Option<Vec<SendableEmbed>>,
/// Masquerade to apply to this message
#[validate]
masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
interactions: Option<Interactions>,
}
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();
}
/// # Send Message
///
/// Sends a message to the given channel.
#[openapi(tag = "Messaging")]
#[post("/<target>/messages", data = "<data>")]
pub async fn message_send(
db: &Db,
user: User,
target: Ref,
data: Json<DataMessageSend>,
mut idempotency: IdempotencyKey,
) -> Result<Json<Message>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
// Validate Message is within reasonable length limits
Message::validate_sum(&data.content, &data.embeds)?;
// Ensure the request is unique
idempotency.consume_nonce(data.nonce).await?;
// Ensure we have permissions to send a message
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::SendMessage)
.await?;
// Check the message is not empty
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
{
return Err(Error::EmptyMessage);
}
// Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
let disallowed = if let Some(list) = &interactions.reactions {
list.is_empty()
} else {
true
};
if disallowed {
return Err(Error::InvalidProperty);
}
}
}
// Start constructing the message
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id.clone(),
channel: channel.id().to_string(),
author: user.id.clone(),
masquerade: data.masquerade,
interactions: data.interactions.unwrap_or_default(),
..Default::default()
};
// 1. Parse mentions in message.
let mut mentions = HashSet::new();
if let Some(content) = &data.content {
for capture in RE_MENTION.captures_iter(content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
}
}
// 2. Verify permissions for masquerade.
if let Some(masq) = &message.masquerade {
permissions
.throw_permission(db, Permission::Masquerade)
.await?;
if masq.colour.is_some() {
permissions
.throw_permission(db, Permission::ManageRole)
.await?;
}
}
// 3. Ensure interactions information is correct
message.interactions.validate(db, &mut permissions).await?;
// 4. Verify replies are valid.
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > 5 {
return Err(Error::TooManyReplies);
}
for Reply { id, mention } in entries {
let message = Ref::from_unchecked(id).as_message(db).await?;
replies.insert(message.id);
if mention {
mentions.insert(message.author);
}
}
}
if !mentions.is_empty() {
message.mentions.replace(
mentions
.into_iter()
.filter(|id| !user.has_blocked(id))
.collect::<Vec<String>>(),
);
}
if !replies.is_empty() {
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
// 5. Process included embeds.
let mut embeds = vec![];
if let Some(sendable_embeds) = data.embeds {
for sendable_embed in sendable_embeds {
embeds.push(sendable_embed.into_embed(db, message_id.clone()).await?)
}
}
if !embeds.is_empty() {
message.embeds.replace(embeds);
}
// 6. Add attachments to message.
let mut attachments = vec![];
if let Some(ids) = &data.attachments {
if !ids.is_empty() {
permissions
.throw_permission(db, Permission::UploadFiles)
.await?;
}
// ! FIXME: move this to app config
if ids.len() > 5 {
return Err(Error::TooManyAttachments);
}
for attachment_id in ids {
attachments.push(
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
.await?,
);
}
}
if !attachments.is_empty() {
message.attachments.replace(attachments);
}
// 7. Set content
message.content = data.content;
// 8. Pass-through nonce value for clients
message.nonce = Some(idempotency.into_key());
message.create(db, &channel, Some(&user)).await?;
// Queue up a task for processing embeds
if let Some(content) = &message.content {
revolt_quark::tasks::process_embeds::queue(
channel.id().to_string(),
message.id.to_string(),
content.clone(),
)
.await;
}
Ok(Json(message))
}

View File

@@ -0,0 +1,58 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
use serde::{Deserialize, Serialize};
/// # Query Parameters
#[derive(Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsUnreact {
/// Remove a specific user's reaction
user_id: Option<String>,
/// Remove all reactions
remove_all: Option<bool>,
}
/// # Remove Reaction(s) to Message
///
/// Remove your own, someone else's or all of a given reaction.
///
/// Requires `ManageMessages` if changing others' reactions.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions/<emoji>?<options..>")]
pub async fn unreact_message(
db: &Db,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
options: OptionsUnreact,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::React)
.await?;
// Check if we need to escalate permissions
let remove_all = options.remove_all.unwrap_or_default();
if options.user_id.is_some() || remove_all {
permissions
.throw_permission(db, Permission::ManageMessages)
.await?;
}
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
// Check if we should wipe all of this reaction
if remove_all {
return message
.clear_reaction(db, &emoji.id)
.await
.map(|_| EmptyResponse);
}
// Remove the reaction
message
.remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), &emoji.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,54 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod channel_ack;
mod channel_delete;
mod channel_edit;
mod channel_fetch;
mod group_add_member;
mod group_create;
mod group_remove_member;
mod invite_create;
mod members_fetch;
mod message_bulk_delete;
mod message_clear_reactions;
mod message_delete;
mod message_edit;
mod message_fetch;
mod message_query;
mod message_query_stale;
mod message_react;
mod message_search;
mod message_send;
mod message_unreact;
mod permissions_set;
mod permissions_set_default;
mod voice_join;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
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_bulk_delete::req,
message_delete::req,
group_create::req,
group_add_member::req,
group_remove_member::req,
voice_join::req,
permissions_set::req,
permissions_set_default::req,
message_react::react_message,
message_unreact::unreact_message,
message_clear_reactions::clear_reactions
]
}

View File

@@ -0,0 +1,59 @@
use rocket::serde::json::Json;
use serde::Deserialize;
use revolt_quark::{
models::{Channel, User},
perms, Db, Error, Override, Permission, Ref, Result,
};
/// # Permission Value
#[derive(Deserialize, JsonSchema)]
pub struct Data {
/// Allow / deny values to set for this role
permissions: Override,
}
/// # Set Role Permission
///
/// Sets permissions for the specified role in this channel.
///
/// Channel must be a `TextChannel` or `VoiceChannel`.
#[openapi(tag = "Channel Permissions")]
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn req(
db: &Db,
user: User,
target: Ref,
role_id: String,
data: Json<Data>,
) -> Result<Json<Channel>> {
let mut channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::ManagePermissions)
.await?;
if let Some(server) = permissions.server.get() {
if let Some(role) = server.roles.get(&role_id) {
if role.rank <= permissions.get_member_rank().unwrap_or(i64::MIN) {
return Err(Error::NotElevated);
}
let current_value: Override = role.permissions.into();
permissions
.throw_permission_override(db, current_value, data.permissions)
.await?;
channel
.set_role_permission(db, &role_id, data.permissions.into())
.await?;
Ok(Json(channel))
} else {
Err(Error::NotFound)
}
} else {
Err(Error::InvalidOperation)
}
}

View File

@@ -0,0 +1,95 @@
use rocket::serde::json::Json;
use serde::Deserialize;
use revolt_quark::{
models::{channel::PartialChannel, Channel, User},
perms, Db, Error, Override, Permission, Ref, Result,
};
/// # Permission Value
#[derive(Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum DataDefaultChannelPermissions {
Value {
/// Permission values to set for members in a `Group`
permissions: u64,
},
Field {
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
permissions: Override,
},
}
/// # Set Default Permission
///
/// Sets permissions for the default role in this channel.
///
/// Channel must be a `Group`, `TextChannel` or `VoiceChannel`.
#[openapi(tag = "Channel Permissions")]
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn req(
db: &Db,
user: User,
target: Ref,
data: Json<DataDefaultChannelPermissions>,
) -> Result<Json<Channel>> {
let data = data.into_inner();
let mut channel = target.as_channel(db).await?;
let mut perm = perms(&user).channel(&channel);
perm.throw_permission_and_view_channel(db, Permission::ManagePermissions)
.await?;
match &channel {
Channel::Group { .. } => {
if let DataDefaultChannelPermissions::Value { permissions } = data {
channel
.update(
db,
PartialChannel {
permissions: Some(permissions as i64),
..Default::default()
},
vec![],
)
.await?;
} else {
return Err(Error::InvalidOperation);
}
}
Channel::TextChannel {
default_permissions,
..
}
| Channel::VoiceChannel {
default_permissions,
..
} => {
if let DataDefaultChannelPermissions::Field { permissions } = data {
perm.throw_permission_override(
db,
default_permissions.map(|x| x.into()),
permissions,
)
.await?;
channel
.update(
db,
PartialChannel {
default_permissions: Some(permissions.into()),
..Default::default()
},
vec![],
)
.await?;
} else {
return Err(Error::InvalidOperation);
}
}
_ => return Err(Error::InvalidOperation),
}
Ok(Json(channel))
}

View File

@@ -1,44 +1,50 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL};
use revolt_quark::{
models::{Channel, User},
perms,
variables::delta::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL},
Db, Error, Permission, Ref, Result,
};
use rocket_contrib::json::JsonValue;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct CreateUserResponse {
/// # Voice Server Token Response
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct CreateVoiceUserResponse {
/// Token for authenticating with the voice server
token: String,
}
/// # Join Call
///
/// Asks the voice server for a token to join the call.
#[openapi(tag = "Voice")]
#[post("/<target>/join_call")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<CreateVoiceUserResponse>> {
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::Connect)
.await?;
if !*USE_VOSO {
return Err(Error::VosoUnavailable);
}
let target = target.fetch_channel().await?;
match target {
match channel {
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()))
.get(&format!("{}/room/{}", *VOSO_URL, channel.id()))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
@@ -51,14 +57,15 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
Ok(result) => match result.status() {
reqwest::StatusCode::OK => (),
reqwest::StatusCode::NOT_FOUND => {
if let Err(_) = client
.post(&format!("{}/room/{}", *VOSO_URL, target.id()))
if (client
.post(&format!("{}/room/{}", *VOSO_URL, channel.id()))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await
.await)
.is_err()
{
return Err(Error::VosoUnavailable);
}
@@ -72,7 +79,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.post(&format!(
"{}/room/{}/user/{}",
*VOSO_URL,
target.id(),
channel.id(),
user.id
))
.header(
@@ -82,9 +89,11 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.send()
.await
{
let res: CreateUserResponse = response.json().await.map_err(|_| Error::InvalidOperation)?;
Ok(json!(res))
response
.json()
.await
.map_err(|_| Error::InvalidOperation)
.map(Json)
} else {
Err(Error::VosoUnavailable)
}

View File

@@ -0,0 +1,81 @@
use revolt_quark::models::emoji::EmojiParent;
use revolt_quark::models::{Emoji, File, User};
use revolt_quark::{perms, Db, Error, Permission, Result};
use serde::Deserialize;
use validator::Validate;
use crate::util::regex::RE_EMOJI;
use rocket::serde::json::Json;
/// # Emoji Data
#[derive(Validate, Deserialize, JsonSchema)]
pub struct DataCreateEmoji {
/// Server name
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
name: String,
/// Parent information
parent: EmojiParent,
/// Whether the emoji is mature
#[serde(default)]
nsfw: bool,
}
/// # Create New Emoji
///
/// Create an emoji by its Autumn upload id.
#[openapi(tag = "Emojis")]
#[put("/emoji/<id>", data = "<data>")]
pub async fn create_emoji(
db: &Db,
user: User,
id: String,
data: Json<DataCreateEmoji>,
) -> Result<Json<Emoji>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
// Bots cannot manage emojis
if user.bot.is_some() {
return Err(Error::IsBot);
}
// Validate we have permission to write into parent
match &data.parent {
EmojiParent::Server { id } => {
let server = db.fetch_server(id).await?;
// Check for permission
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageCustomisation)
.await?;
// Check that there are no more than 100 emoji
// ! FIXME: hardcoded upper limit
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
if emojis.len() > 99 {
return Err(Error::TooManyEmoji);
}
}
EmojiParent::Detached => return Err(Error::InvalidOperation),
};
// Find the relevant attachment
let attachment = File::use_emoji(db, &id, &id).await?;
// Create the emoji object
let emoji = Emoji {
id,
parent: data.parent,
creator_id: user.id,
name: data.name,
animated: "image/gif" == &attachment.content_type,
nsfw: data.nsfw,
};
// Save emoji
emoji.create(db).await?;
Ok(Json(emoji))
}

View File

@@ -0,0 +1,37 @@
use revolt_quark::models::{emoji::EmojiParent, User};
use revolt_quark::{perms, Db, EmptyResponse, Error, Permission, Ref, Result};
/// # Delete Emoji
///
/// Delete an emoji by its id.
#[openapi(tag = "Emojis")]
#[delete("/emoji/<id>")]
pub async fn delete_emoji(db: &Db, user: User, id: Ref) -> Result<EmptyResponse> {
// Bots cannot manage emoji
if user.bot.is_some() {
return Err(Error::IsBot);
}
// Fetch the emoji
let emoji = id.as_emoji(db).await?;
// If we uploaded the emoji, then we have permission to delete it
if emoji.creator_id != user.id {
// Otherwise, validate we have permission to delete from parent
match &emoji.parent {
EmojiParent::Server { id } => {
let server = db.fetch_server(id).await?;
// Check for permission
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageCustomisation)
.await?;
}
EmojiParent::Detached => return Ok(EmptyResponse),
};
}
// Delete the emoji
emoji.delete(db).await.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,13 @@
use revolt_quark::models::{Emoji, User};
use revolt_quark::{Db, Ref, Result};
use rocket::serde::json::Json;
/// # Fetch Emoji
///
/// Fetch an emoji by its id.
#[openapi(tag = "Emojis")]
#[get("/emoji/<id>")]
pub async fn fetch_emoji(db: &Db, _user: User, id: Ref) -> Result<Json<Emoji>> {
id.as_emoji(db).await.map(Json)
}

View File

@@ -0,0 +1,14 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod emoji_create;
mod emoji_delete;
mod emoji_fetch;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
emoji_create::create_emoji,
emoji_delete::delete_emoji,
emoji_fetch::fetch_emoji
]
}

View File

@@ -0,0 +1,31 @@
use revolt_quark::{
models::{Invite, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
};
/// # Delete Invite
///
/// Delete an invite by its id.
#[openapi(tag = "Invites")]
#[delete("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
let invite = target.as_invite(db).await?;
if user.id == invite.creator() {
db.delete_invite(invite.code()).await
} else {
match invite {
Invite::Server { code, server, .. } => {
let server = db.fetch_server(&server).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageServer)
.await?;
db.delete_invite(&code).await
}
_ => unreachable!(),
}
}
.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,135 @@
use revolt_quark::models::{Channel, File, Invite};
use revolt_quark::{Db, Ref, Result};
use rocket::serde::json::Json;
use serde::Serialize;
/// # Invite
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Debug, Clone, JsonSchema)]
#[serde(tag = "type")]
pub enum InviteResponse {
/// Server channel invite
Server {
/// Invite code
code: String,
/// Id of the server
server_id: String,
/// Name of the server
server_name: String,
/// Attachment for server icon
#[serde(skip_serializing_if = "Option::is_none")]
server_icon: Option<File>,
/// Attachment for server banner
#[serde(skip_serializing_if = "Option::is_none")]
server_banner: Option<File>,
/// Enum of server flags
#[serde(skip_serializing_if = "Option::is_none")]
server_flags: Option<i32>,
/// Id of server channel
channel_id: String,
/// Name of server channel
channel_name: String,
/// Description of server channel
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
/// Name of user who created the invite
user_name: String,
/// Avatar of the user who created the invite
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
/// Number of members in this server
member_count: i64,
},
/// Group channel invite
Group {
/// Invite code
code: String,
/// Id of group channel
channel_id: String,
/// Name of group channel
channel_name: String,
/// Description of group channel
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
/// Name of user who created the invite
user_name: String,
/// Avatar of the user who created the invite
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
},
}
/// # Fetch Invite
///
/// Fetch an invite by its id.
#[openapi(tag = "Invites")]
#[get("/<target>")]
pub async fn req(db: &Db, target: Ref) -> Result<Json<InviteResponse>> {
Ok(Json(match target.as_invite(db).await? {
Invite::Server {
channel, creator, ..
} => {
let channel = db.fetch_channel(&channel).await?;
let user = db.fetch_user(&creator).await?;
match channel {
Channel::TextChannel {
id,
server,
name,
description,
..
}
| Channel::VoiceChannel {
id,
server,
name,
description,
..
} => {
let server = db.fetch_server(&server).await?;
InviteResponse::Server {
code: target.id,
member_count: db.fetch_member_count(&server.id).await? as i64,
server_id: server.id,
server_name: server.name,
server_icon: server.icon,
server_banner: server.banner,
server_flags: server.flags,
channel_id: id,
channel_name: name,
channel_description: description,
user_name: user.username,
user_avatar: user.avatar,
}
}
_ => unreachable!(),
}
}
Invite::Group {
channel, creator, ..
} => {
let channel = db.fetch_channel(&channel).await?;
let user = db.fetch_user(&creator).await?;
match channel {
Channel::Group {
id,
name,
description,
..
} => InviteResponse::Group {
code: target.id,
channel_id: id,
channel_name: name,
channel_description: description,
user_name: user.username,
user_avatar: user.avatar,
},
_ => unreachable!(),
}
}
}))
}

View File

@@ -0,0 +1,47 @@
use revolt_quark::{
models::{Channel, Invite, Server, User},
variables::delta::MAX_SERVER_COUNT,
Db, Error, Ref, Result,
};
use rocket::serde::json::Json;
use serde::Serialize;
/// # Join Response
#[derive(Serialize, JsonSchema)]
#[serde(tag = "type")]
pub enum InviteJoinResponse {
Server {
/// Channels in the server
channels: Vec<Channel>,
/// Server we are joining
server: Server,
},
}
/// # Join Invite
///
/// Join an invite by its ID.
#[openapi(tag = "Invites")]
#[post("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<InviteJoinResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
if !user.can_acquire_server(db).await? {
return Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
});
}
let invite = target.as_invite(db).await?;
match &invite {
Invite::Server { server, .. } => {
let server = db.fetch_server(server).await?;
let channels = server.create_member(db, user, None).await?;
Ok(Json(InviteJoinResponse::Server { channels, server }))
}
_ => unreachable!(),
}
}

View File

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

View File

@@ -0,0 +1,283 @@
use revolt_rocket_okapi::{revolt_okapi::openapi3::OpenApi, settings::OpenApiSettings};
pub use rocket::http::Status;
pub use rocket::response::Redirect;
use rocket::{Build, Rocket};
mod bots;
mod channels;
mod customisation;
mod invites;
mod onboard;
mod push;
mod root;
mod servers;
mod sync;
mod users;
pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
let settings = OpenApiSettings::default();
mount_endpoints_and_merged_docs! {
rocket, "/".to_owned(), settings,
"/" => (vec![], custom_openapi_spec()),
"" => openapi_get_routes_spec![root::root, root::ping],
"/users" => users::routes(),
"/bots" => bots::routes(),
"/channels" => channels::routes(),
"/servers" => servers::routes(),
"/invites" => invites::routes(),
"/custom" => customisation::routes(),
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes(),
};
rocket
}
fn custom_openapi_spec() -> OpenApi {
use revolt_rocket_okapi::revolt_okapi::openapi3::*;
let mut extensions = schemars::Map::new();
extensions.insert(
"x-logo".to_owned(),
json!({
"url": "https://revolt.chat/header.png",
"altText": "Revolt Header"
}),
);
extensions.insert(
"x-tagGroups".to_owned(),
json!([
{
"name": "Revolt",
"tags": [
"Core"
]
},
{
"name": "Users",
"tags": [
"User Information",
"Direct Messaging",
"Relationships"
]
},
{
"name": "Bots",
"tags": [
"Bots"
]
},
{
"name": "Channels",
"tags": [
"Channel Information",
"Channel Invites",
"Channel Permissions",
"Messaging",
"Interactions",
"Groups",
"Voice"
]
},
{
"name": "Servers",
"tags": [
"Server Information",
"Server Members",
"Server Permissions"
]
},
{
"name": "Invites",
"tags": [
"Invites"
]
},
{
"name": "Customisation",
"tags": [
"Emojis"
]
},
{
"name": "Authentication",
"tags": [
"Account",
"Session",
"Onboarding",
"MFA"
]
},
{
"name": "Miscellaneous",
"tags": [
"Sync",
"Web Push"
]
}
]),
);
OpenApi {
openapi: OpenApi::default_version(),
info: Info {
title: "Revolt API".to_owned(),
description: Some("Open source user-first chat platform.".to_owned()),
terms_of_service: Some("https://revolt.chat/terms".to_owned()),
contact: Some(Contact {
name: Some("Revolt Support".to_owned()),
url: Some("https://revolt.chat".to_owned()),
email: Some("contact@revolt.chat".to_owned()),
..Default::default()
}),
license: Some(License {
name: "AGPLv3".to_owned(),
url: Some("https://github.com/revoltchat/delta/blob/master/LICENSE".to_owned()),
..Default::default()
}),
version: env!("CARGO_PKG_VERSION").to_string(),
..Default::default()
},
servers: vec![
Server {
url: "https://api.revolt.chat".to_owned(),
description: Some("Revolt API".to_owned()),
..Default::default()
},
Server {
url: "http://local.revolt.chat:8000".to_owned(),
description: Some("Local Revolt Environment".to_owned()),
..Default::default()
},
],
external_docs: Some(ExternalDocs {
url: "https://developers.revolt.chat".to_owned(),
description: Some("Revolt Developer Documentation".to_owned()),
..Default::default()
}),
extensions,
tags: vec![
Tag {
name: "Core".to_owned(),
description: Some(
"Use in your applications to determine information about the Revolt node"
.to_owned(),
),
..Default::default()
},
Tag {
name: "User Information".to_owned(),
description: Some("Query and fetch users on Revolt".to_owned()),
..Default::default()
},
Tag {
name: "Direct Messaging".to_owned(),
description: Some("Direct message other users on Revolt".to_owned()),
..Default::default()
},
Tag {
name: "Relationships".to_owned(),
description: Some(
"Manage your friendships and block list on the platform".to_owned(),
),
..Default::default()
},
Tag {
name: "Bots".to_owned(),
description: Some("Create and edit bots".to_owned()),
..Default::default()
},
Tag {
name: "Channel Information".to_owned(),
description: Some("Query and fetch channels on Revolt".to_owned()),
..Default::default()
},
Tag {
name: "Channel Invites".to_owned(),
description: Some("Create and manage invites for channels".to_owned()),
..Default::default()
},
Tag {
name: "Channel Permissions".to_owned(),
description: Some("Manage permissions for channels".to_owned()),
..Default::default()
},
Tag {
name: "Messaging".to_owned(),
description: Some("Send and manipulate messages".to_owned()),
..Default::default()
},
Tag {
name: "Groups".to_owned(),
description: Some("Create, invite users and manipulate groups".to_owned()),
..Default::default()
},
Tag {
name: "Voice".to_owned(),
description: Some("Join and talk with other users".to_owned()),
..Default::default()
},
Tag {
name: "Server Information".to_owned(),
description: Some("Query and fetch servers on Revolt".to_owned()),
..Default::default()
},
Tag {
name: "Server Members".to_owned(),
description: Some("Find and edit server members".to_owned()),
..Default::default()
},
Tag {
name: "Server Permissions".to_owned(),
description: Some("Manage permissions for servers".to_owned()),
..Default::default()
},
Tag {
name: "Invites".to_owned(),
description: Some("View, join and delete invites".to_owned()),
..Default::default()
},
Tag {
name: "Account".to_owned(),
description: Some("Manage your account".to_owned()),
..Default::default()
},
Tag {
name: "Session".to_owned(),
description: Some("Create and manage sessions".to_owned()),
..Default::default()
},
Tag {
name: "MFA".to_owned(),
description: Some("Multi-factor Authentication".to_owned()),
..Default::default()
},
Tag {
name: "Onboarding".to_owned(),
description: Some(
"After signing up to Revolt, users must pick a unique username".to_owned(),
),
..Default::default()
},
Tag {
name: "Sync".to_owned(),
description: Some("Upload and retrieve any JSON data between clients".to_owned()),
..Default::default()
},
Tag {
name: "Web Push".to_owned(),
description: Some(
"Subscribe to and receive Revolt push notifications while offline".to_owned(),
),
..Default::default()
},
],
..Default::default()
}
}

View File

@@ -0,0 +1,45 @@
use crate::util::regex::RE_USERNAME;
use revolt_quark::{
authifier::models::Session, models::User, Database, EmptyResponse, Error, Result,
};
use rocket::{serde::json::Json, State};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # New User Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataOnboard {
/// New username which will be used to identify the user on the platform
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
username: String,
}
/// # Complete Onboarding
///
/// This sets a new username, completes onboarding and allows a user to start using Revolt.
#[openapi(tag = "Onboarding")]
#[post("/complete", data = "<data>")]
pub async fn req(
db: &State<Database>,
session: Session,
user: Option<User>,
data: Json<DataOnboard>,
) -> Result<EmptyResponse> {
if user.is_some() {
return Err(Error::AlreadyOnboarded);
}
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let username = User::validate_username(db, data.username).await?;
let user = User {
id: session.user_id,
username,
..Default::default()
};
db.insert_user(&user).await.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,21 @@
use revolt_quark::{authifier::models::Session, models::User};
use rocket::serde::json::Json;
use serde::Serialize;
/// # Onboarding Status
#[derive(Serialize, JsonSchema)]
pub struct DataHello {
/// Whether onboarding is required
onboarding: bool,
}
/// # Check Onboarding Status
///
/// This will tell you whether the current account requires onboarding or whether you can continue to send requests as usual. You may skip calling this if you're restoring an existing session.
#[openapi(tag = "Onboarding")]
#[get("/hello")]
pub async fn req(_session: Session, user: Option<User>) -> Json<DataHello> {
Json(DataHello {
onboarding: user.is_none(),
})
}

View File

@@ -0,0 +1,9 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod complete;
mod hello;
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![hello::req, complete::req]
}

View File

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

View File

@@ -0,0 +1,32 @@
use revolt_quark::{
authifier::{
models::{Session, WebPushSubscription},
Authifier,
},
EmptyResponse, Error, Result,
};
use rocket::{serde::json::Json, State};
/// # Push Subscribe
///
/// Create a new Web Push subscription.
///
/// If an existing subscription exists on this session, it will be removed.
#[openapi(tag = "Web Push")]
#[post("/subscribe", data = "<data>")]
pub async fn req(
authifier: &State<Authifier>,
mut session: Session,
data: Json<WebPushSubscription>,
) -> Result<EmptyResponse> {
session.subscription = Some(data.into_inner());
session
.save(authifier)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
operation: "save",
with: "session",
})
}

View File

@@ -0,0 +1,23 @@
use revolt_quark::{
authifier::{models::Session, Authifier},
EmptyResponse, Error, Result,
};
use rocket::State;
/// # Unsubscribe
///
/// Remove the Web Push subscription associated with the current session.
#[openapi(tag = "Web Push")]
#[post("/unsubscribe")]
pub async fn req(authifier: &State<Authifier>, mut session: Session) -> Result<EmptyResponse> {
session.subscription = None;
session
.save(authifier)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
operation: "save",
with: "session",
})
}

View File

@@ -0,0 +1,146 @@
use revolt_quark::variables::delta::{
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 revolt_quark::Result;
use rocket::http::Status;
use rocket::serde::json::Json;
use serde::Serialize;
/// # hCaptcha Configuration
#[derive(Serialize, JsonSchema, Debug)]
pub struct CaptchaFeature {
/// Whether captcha is enabled
pub enabled: bool,
/// Client key used for solving captcha
pub key: String,
}
/// # Generic Service Configuration
#[derive(Serialize, JsonSchema, Debug)]
pub struct Feature {
/// Whether the service is enabled
pub enabled: bool,
/// URL pointing to the service
pub url: String,
}
/// # Voice Server Configuration
#[derive(Serialize, JsonSchema, Debug)]
pub struct VoiceFeature {
/// Whether voice is enabled
pub enabled: bool,
/// URL pointing to the voice API
pub url: String,
/// URL pointing to the voice WebSocket server
pub ws: String,
}
/// # Feature Configuration
#[derive(Serialize, JsonSchema, Debug)]
pub struct RevoltFeatures {
/// hCaptcha configuration
pub captcha: CaptchaFeature,
/// Whether email verification is enabled
pub email: bool,
/// Whether this server is invite only
pub invite_only: bool,
/// File server service configuration
pub autumn: Feature,
/// Proxy service configuration
pub january: Feature,
/// Voice server configuration
pub voso: VoiceFeature,
}
/// # Build Information
#[derive(Serialize, JsonSchema, Debug)]
pub struct BuildInformation {
/// Commit Hash
pub commit_sha: String,
/// Commit Timestamp
pub commit_timestamp: String,
/// Git Semver
pub semver: String,
/// Git Origin URL
pub origin_url: String,
/// Build Timestamp
pub timestamp: String,
}
/// # Server Configuration
#[derive(Serialize, JsonSchema, Debug)]
pub struct RevoltConfig {
/// Revolt API Version
pub revolt: String,
/// Features enabled on this Revolt node
pub features: RevoltFeatures,
/// WebSocket URL
pub ws: String,
/// URL pointing to the client serving this node
pub app: String,
/// Web Push VAPID public key
pub vapid: String,
/// Build information
pub build: BuildInformation,
}
/// # Query Node
///
/// Fetch the server configuration for this Revolt instance.
#[openapi(tag = "Core")]
#[get("/")]
pub async fn root() -> Result<Json<RevoltConfig>> {
Ok(Json(RevoltConfig {
revolt: env!("CARGO_PKG_VERSION").to_string(),
features: RevoltFeatures {
captcha: CaptchaFeature {
enabled: *USE_HCAPTCHA,
key: HCAPTCHA_SITEKEY.to_string(),
},
email: *USE_EMAIL,
invite_only: *INVITE_ONLY,
autumn: Feature {
enabled: *USE_AUTUMN,
url: AUTUMN_URL.to_string(),
},
january: Feature {
enabled: *USE_JANUARY,
url: JANUARY_URL.to_string(),
},
voso: VoiceFeature {
enabled: *USE_VOSO,
url: VOSO_URL.to_string(),
ws: VOSO_WS_HOST.to_string(),
},
},
ws: EXTERNAL_WS_URL.to_string(),
app: APP_URL.to_string(),
vapid: VAPID_PUBLIC_KEY.to_string(),
build: BuildInformation {
commit_sha: option_env!("VERGEN_GIT_SHA")
.unwrap_or_else(|| "<failed to generate>")
.to_string(),
commit_timestamp: option_env!("VERGEN_GIT_COMMIT_TIMESTAMP")
.unwrap_or_else(|| "<failed to generate>")
.to_string(),
semver: option_env!("VERGEN_GIT_SEMVER")
.unwrap_or_else(|| "<failed to generate>")
.to_string(),
origin_url: option_env!("GIT_ORIGIN_URL")
.unwrap_or_else(|| "<failed to generate>")
.to_string(),
timestamp: option_env!("VERGEN_BUILD_TIMESTAMP")
.unwrap_or_else(|| "<failed to generate>")
.to_string(),
},
}))
}
/// Example endpoint.
#[openapi(skip)]
#[get("/ping")]
pub async fn ping(/*_limitguard: Ratelimiter*/) -> Status {
Status::Ok
}

View File

@@ -0,0 +1,73 @@
use revolt_quark::{
models::{server_member::MemberCompositeKey, ServerBan, User},
perms, Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Ban Information
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataBanCreate {
/// Ban reason
#[validate(length(min = 1, max = 1024))]
reason: Option<String>,
}
/// # Ban User
///
/// Ban a user by their id.
#[openapi(tag = "Server Members")]
#[put("/<server>/bans/<target>", data = "<data>")]
pub async fn req(
db: &Db,
user: User,
server: Ref,
target: Ref,
data: Json<DataBanCreate>,
) -> Result<Json<ServerBan>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let server = server.as_server(db).await?;
if target.id == user.id {
return Err(Error::CannotRemoveYourself);
}
if target.id == server.owner {
return Err(Error::InvalidOperation);
}
let mut permissions = perms(&user).server(&server);
permissions
.throw_permission(db, Permission::BanMembers)
.await?;
// If member exists, check privileges against them
if let Ok(member) = target.as_member(db, &server.id).await {
if member.get_ranking(permissions.server.get().unwrap())
<= permissions.get_member_rank().unwrap_or(i64::MIN)
{
return Err(Error::NotElevated);
}
server.ban_member(db, member, data.reason).await.map(Json)
} else {
let server_id = server.id.to_string();
server
.ban_user(
db,
MemberCompositeKey {
server: server_id,
user: target.id,
},
data.reason,
)
.await
.map(Json)
}
}

View File

@@ -0,0 +1,67 @@
use revolt_quark::models::{File, ServerBan, User};
use revolt_quark::{perms, Db, Permission, Ref, Result};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
/// # Banned User
///
/// Just enoguh user information to list bans.
#[derive(Serialize, Deserialize, JsonSchema)]
struct BannedUser {
/// Id of the banned user
#[serde(rename = "_id")]
pub id: String,
/// Username of the banned user
pub username: String,
/// Avatar of the banned user
pub avatar: Option<File>,
}
/// # Ban List Result
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct BanListResult {
/// Users objects
users: Vec<BannedUser>,
/// Ban objects
bans: Vec<ServerBan>,
}
impl From<User> for BannedUser {
fn from(user: User) -> Self {
BannedUser {
id: user.id,
username: user.username,
avatar: user.avatar,
}
}
}
/// # Fetch Bans
///
/// Fetch all bans on a server.
#[openapi(tag = "Server Members")]
#[get("/<target>/bans")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<BanListResult>> {
let server = target.as_server(db).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::BanMembers)
.await?;
let bans = db.fetch_bans(&server.id).await?;
let users = db
.fetch_users(
&bans
.iter()
.map(|x| &x.id.user)
.cloned()
.collect::<Vec<String>>(),
)
.await?
.into_iter()
.map(|x| x.into())
.collect();
Ok(Json(BanListResult { users, bans }))
}

View File

@@ -0,0 +1,17 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
/// # Unban user
///
/// Remove a user's ban.
#[openapi(tag = "Server Members")]
#[delete("/<server>/bans/<target>")]
pub async fn req(db: &Db, user: User, server: Ref, target: Ref) -> Result<EmptyResponse> {
let server = server.as_server(db).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::BanMembers)
.await?;
let ban = target.as_ban(db, &server.id).await?;
db.delete_ban(&ban.id).await.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,120 @@
use std::collections::HashMap;
use revolt_quark::{
models::{server::PartialServer, Channel, User},
perms, Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
/// # Channel Type
#[derive(Serialize, Deserialize, JsonSchema)]
enum ChannelType {
/// Text Channel
Text,
/// Voice Channel
Voice,
}
impl Default for ChannelType {
fn default() -> Self {
ChannelType::Text
}
}
/// # Channel Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataCreateChannel {
/// Channel type
#[serde(rename = "type", default = "ChannelType::default")]
channel_type: ChannelType,
/// Channel name
#[validate(length(min = 1, max = 32))]
name: String,
/// Channel description
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
/// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
}
/// # Create Channel
///
/// Create a new Text or Voice channel.
#[openapi(tag = "Server Information")]
#[post("/<target>/channels", data = "<info>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
info: Json<DataCreateChannel>,
) -> Result<Json<Channel>> {
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let mut server = target.as_server(db).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageChannel)
.await?;
let id = Ulid::new().to_string();
let mut channels = server.channels.clone();
channels.push(id.clone());
let DataCreateChannel {
name,
description,
nsfw,
channel_type,
} = info;
let channel = match channel_type {
ChannelType::Text => Channel::TextChannel {
id,
server: server.id.clone(),
name,
description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: nsfw.unwrap_or(false),
},
ChannelType::Voice => Channel::VoiceChannel {
id,
server: server.id.clone(),
name,
description,
icon: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: nsfw.unwrap_or(false),
},
};
channel.create(db).await?;
server
.update(
db,
PartialServer {
channels: Some(channels),
..Default::default()
},
vec![],
)
.await?;
Ok(Json(channel))
}

View File

@@ -0,0 +1,17 @@
use revolt_quark::models::{Emoji, User};
use revolt_quark::{perms, Db, Ref, Result};
use rocket::serde::json::Json;
/// # Fetch Server Emoji
///
/// Fetch all emoji on a server.
#[openapi(tag = "Server Customisation")]
#[get("/<target>/emojis")]
pub async fn list_emoji(db: &Db, user: User, target: Ref) -> Result<Json<Vec<Emoji>>> {
let server = target.as_server(db).await?;
perms(&user).server(&server).calc(db).await?;
// Fetch all emoji from server if we can view it
db.fetch_emoji_by_parent_id(&server.id).await.map(Json)
}

View File

@@ -0,0 +1,25 @@
use revolt_quark::{
models::{Invite, User},
perms, Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
/// # Fetch Invites
///
/// Fetch all server invites.
#[openapi(tag = "Server Members")]
#[get("/<target>/invites")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<Invite>>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let server = target.as_server(db).await?;
perms(&user)
.server(&server)
.throw_permission(db, Permission::ManageServer)
.await?;
db.fetch_invites_for_server(&server.id).await.map(Json)
}

View File

@@ -0,0 +1,171 @@
use std::collections::HashSet;
use revolt_quark::{
models::{
server_member::{FieldsMember, PartialMember},
File, Member, User,
},
perms, Db, Error, Permission, Ref, Result, Timestamp,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Member Data
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
pub struct DataMemberEdit {
/// Member nickname
#[validate(length(min = 1, max = 32))]
nickname: Option<String>,
/// Attachment Id to set for avatar
avatar: Option<String>,
/// Array of role ids
roles: Option<Vec<String>>,
/// Timestamp this member is timed out until
timeout: Option<Timestamp>,
/// Fields to remove from channel object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsMember>>,
}
/// # Edit Member
///
/// Edit a member by their id.
#[openapi(tag = "Server Members")]
#[patch("/<server>/members/<target>", data = "<data>")]
pub async fn req(
db: &Db,
user: User,
server: Ref,
target: Ref,
data: Json<DataMemberEdit>,
) -> Result<Json<Member>> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
// Fetch server, target member and current permissions
let mut server = server.as_server(db).await?;
let mut member = target.as_member(db, &server.id).await?;
let mut permissions = perms(&user).server(&server);
// Check permissions in server
let mut required = vec![];
if data.nickname.is_some()
|| data
.remove
.as_ref()
.map(|x| x.contains(&FieldsMember::Nickname))
.unwrap_or_default()
{
if user.id == member.id.user {
required.push(Permission::ChangeNickname);
} else {
required.push(Permission::ManageNicknames);
}
}
if data.avatar.is_some()
|| data
.remove
.as_ref()
.map(|x| x.contains(&FieldsMember::Avatar))
.unwrap_or_default()
{
if user.id == member.id.user {
required.push(Permission::ChangeAvatar);
} else {
return Err(Error::InvalidOperation);
}
}
if data.roles.is_some()
|| data
.remove
.as_ref()
.map(|x| x.contains(&FieldsMember::Roles))
.unwrap_or_default()
{
required.push(Permission::AssignRoles);
}
if data.timeout.is_some()
|| data
.remove
.as_ref()
.map(|x| x.contains(&FieldsMember::Timeout))
.unwrap_or_default()
{
required.push(Permission::TimeoutMembers);
}
for permission in required {
permissions.throw_permission(db, permission).await?;
}
// Resolve our ranking
let our_ranking = permissions.get_member_rank().unwrap_or(i64::MIN);
// Check that we have permissions to act against this member
if member.id.user != user.id
&& member.get_ranking(permissions.server.get().unwrap()) <= our_ranking
{
return Err(Error::NotElevated);
}
// Check permissions against roles in diff
if let Some(roles) = &data.roles {
let current_roles = member.roles.iter().collect::<HashSet<&String>>();
let new_roles = roles.iter().collect::<HashSet<&String>>();
let added_roles: Vec<&&String> = new_roles.difference(&current_roles).collect();
for role_id in added_roles {
if let Some(role) = server.roles.remove(*role_id) {
if role.rank <= our_ranking {
return Err(Error::NotElevated);
}
} else {
return Err(Error::InvalidRole);
}
}
}
// Apply edits to the member object
let DataMemberEdit {
nickname,
avatar,
roles,
timeout,
remove,
} = data;
let mut partial = PartialMember {
nickname,
roles,
timeout,
..Default::default()
};
// 1. Remove fields from object
if let Some(fields) = &remove {
if fields.contains(&FieldsMember::Avatar) {
if let Some(avatar) = &member.avatar {
db.mark_attachment_as_deleted(&avatar.id).await?;
}
}
}
// 2. Apply new avatar
if let Some(avatar) = avatar {
partial.avatar = Some(File::use_avatar(db, &avatar, &user.id).await?);
}
member
.update(db, partial, remove.unwrap_or_default())
.await?;
Ok(Json(member))
}

View File

@@ -0,0 +1,17 @@
use revolt_quark::{
models::{Member, User},
perms, Db, Ref, Result,
};
use rocket::serde::json::Json;
/// # Fetch Member
///
/// Retrieve a member.
#[openapi(tag = "Server Members")]
#[get("/<target>/members/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<Json<Member>> {
let server = target.as_server(db).await?;
perms(&user).server(&server).calc(db).await?;
member.as_member(db, &server.id).await.map(Json)
}

View File

@@ -0,0 +1,62 @@
use revolt_quark::{
models::{Member, User},
perms, Db, Ref, Result,
};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
/// # Query Parameters
#[derive(Deserialize, JsonSchema, FromForm)]
pub struct OptionsFetchAllMembers {
/// Whether to exclude offline users
exclude_offline: Option<bool>,
}
/// # Member List
///
/// Both lists are sorted by ID.
#[derive(Serialize, JsonSchema)]
pub struct AllMemberResponse {
/// List of members
members: Vec<Member>,
/// List of users
users: Vec<User>,
}
/// # Fetch Members
///
/// Fetch all server members.
#[openapi(tag = "Server Members")]
#[get("/<target>/members?<options..>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
options: OptionsFetchAllMembers,
) -> Result<Json<AllMemberResponse>> {
let server = target.as_server(db).await?;
perms(&user).server(&server).calc(db).await?;
let mut members = db.fetch_all_members(&server.id).await?;
let mut user_ids = vec![];
for member in &members {
user_ids.push(member.id.user.clone());
}
let mut users = User::fetch_foreign_users(db, &user_ids).await?;
// Ensure the lists match up exactly.
members.sort_by(|a, b| a.id.user.cmp(&b.id.user));
users.sort_by(|a, b| a.id.cmp(&b.id));
// Optionally, remove all offline user entries.
if let Some(true) = options.exclude_offline {
let mut iter = users.iter();
members.retain(|_| iter.next().unwrap().online.unwrap_or(false));
users.retain(|user| user.online.unwrap_or(false));
}
Ok(Json(AllMemberResponse { members, users }))
}

View File

@@ -0,0 +1,40 @@
use revolt_quark::{
models::{server_member::RemovalIntention, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
};
/// # Kick Member
///
/// Removes a member from the server.
#[openapi(tag = "Server Members")]
#[delete("/<target>/members/<member>")]
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
let server = target.as_server(db).await?;
if member.id == user.id {
return Err(Error::CannotRemoveYourself);
}
if member.id == server.owner {
return Err(Error::InvalidOperation);
}
let mut permissions = perms(&user).server(&server);
permissions
.throw_permission(db, Permission::KickMembers)
.await?;
let member = member.as_member(db, &server.id).await?;
if member.get_ranking(permissions.server.get().unwrap())
<= permissions.get_member_rank().unwrap_or(i64::MIN)
{
return Err(Error::NotElevated);
}
server
.remove_member(db, member, RemovalIntention::Kick, false)
.await
.map(|_| EmptyResponse)
}

View File

@@ -1,29 +1,34 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod ban_create;
mod ban_list;
mod ban_remove;
mod channel_create;
mod emoji_list;
mod invites_fetch;
mod member_edit;
mod member_fetch;
mod member_fetch_all;
mod member_remove;
mod permissions_set;
mod permissions_set_default;
mod roles_create;
mod roles_delete;
mod roles_edit;
mod server_ack;
mod server_create;
mod server_delete;
mod server_edit;
mod server_fetch;
mod channel_create;
mod member_edit;
mod member_fetch;
mod member_fetch_all;
mod member_remove;
mod ban_create;
mod ban_list;
mod ban_remove;
mod invites_fetch;
pub fn routes() -> Vec<Route> {
routes![
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
server_create::req,
server_delete::req,
server_fetch::req,
server_edit::req,
server_ack::req,
channel_create::req,
member_fetch_all::req,
member_remove::req,
@@ -32,6 +37,12 @@ pub fn routes() -> Vec<Route> {
ban_create::req,
ban_remove::req,
ban_list::req,
invites_fetch::req
invites_fetch::req,
roles_create::req,
roles_edit::req,
roles_delete::req,
permissions_set::req,
permissions_set_default::req,
emoji_list::list_emoji
]
}

View File

@@ -0,0 +1,56 @@
use rocket::serde::json::Json;
use serde::Deserialize;
use revolt_quark::{
models::{Server, User},
perms, Db, Error, Override, Permission, Ref, Result,
};
/// # Permission Value
#[derive(Deserialize, JsonSchema)]
pub struct DataSetServerRolePermission {
/// Allow / deny values for the role in this server.
permissions: Override,
}
/// # Set Role Permission
///
/// Sets permissions for the specified role in the server.
#[openapi(tag = "Server Permissions")]
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn req(
db: &Db,
user: User,
target: Ref,
role_id: String,
data: Json<DataSetServerRolePermission>,
) -> Result<Json<Server>> {
let data = data.into_inner();
let mut server = target.as_server(db).await?;
if let Some((current_value, rank)) = server.roles.get(&role_id).map(|x| (x.permissions, x.rank))
{
let mut permissions = perms(&user).server(&server);
permissions
.throw_permission(db, Permission::ManagePermissions)
.await?;
if rank <= permissions.get_member_rank().unwrap_or(i64::MIN) {
return Err(Error::NotElevated);
}
let current_value: Override = current_value.into();
permissions
.throw_permission_override(db, current_value, data.permissions)
.await?;
server
.set_role_permission(db, &role_id, data.permissions.into())
.await?;
Ok(Json(server))
} else {
Err(Error::NotFound)
}
}

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