Compare commits

..

687 Commits

Author SHA1 Message Date
Paul Makles
c8d5128b0c chore: add additional sanitisation 2023-06-11 12:26:18 +01:00
Paul Makles
0578a05a05 feat: add remove "displayname" field 2023-06-11 12:05:08 +01:00
Paul Makles
9a412b3e08 fix: actually update the display name 2023-06-11 11:50:21 +01:00
Paul Makles
8deec1f80a fix: display name is optional 2023-06-11 10:56:03 +01:00
Paul Makles
26afbeed84 chore: extend discriminator block list 2023-06-11 10:49:51 +01:00
Paul Makles
9975de01bc chore: remove webhooks from production 2023-06-11 10:09:50 +01:00
Paul Makles
ac525466b8 chore: cut a new release (0.6.0) 2023-06-11 10:04:15 +01:00
Paul Makles
c7a04e4559 fix: conflict resolution in migration
fix: write new username and display name
chore: restrict discriminator search space
2023-06-11 09:53:41 +01:00
Paul Makles
5bbe30edbc feat(core/database): migrate to discriminators 2023-06-11 09:16:24 +01:00
Paul Makles
31c7dc0577 feat: add discriminator and display name fields 2023-06-09 16:34:18 +01:00
Paul Makles
aba5c7d8af fix: shouldn't prefix dep: 2023-06-04 19:45:47 +01:00
Paul Makles
2f4ea4cabb refactor(core/models): transitively apply feature flags 2023-06-04 19:45:31 +01:00
Paul Makles
7e801a31bd fix: make sure feature flags are respected 2023-06-04 19:43:56 +01:00
Paul Makles
eb4870c9d5 fix: explicitly target Rust 1.70.0 for images 2023-06-04 19:42:42 +01:00
Paul Makles
b1fdd7fc74 merge: pull request #251 from revoltchat/fix/zomatree/send-message-null 2023-06-04 19:29:07 +01:00
Paul Makles
bf3e027067 feat: channel model implementation (#250)
Co-authored-by: ToastXC <100072983+toastxc@users.noreply.github.com>
2023-06-04 18:52:49 +01:00
Zomatree
a7a7e49e18 fix: send message route not accepting null 2023-06-04 18:46:49 +01:00
Paul Makles
3c834d06d4 fix: must not restrict lowerbound for embeds 2023-06-03 17:49:31 +01:00
Paul Makles
f5e6086d2a fix(core/database): test matches incorrect entry
[skip ci]
2023-06-03 17:15:43 +01:00
Paul Makles
3365a8e482 chore: force bind to 0.0.0.0 because it's just ignoring all environment variables??? 2023-06-03 17:05:25 +01:00
Paul Makles
268c2d1355 fix: re-order crate publishing 2023-06-03 14:11:44 +01:00
Paul Makles
e9d6b963df chore: bump version to 0.6.0-rc.1 2023-06-03 14:06:04 +01:00
Paul Makles
f9d9447203 feat: add additional validation for github webhook 2023-06-03 14:02:16 +01:00
Paul Makles
a0002d0b43 feat: reintroduce permission checks for send 2023-06-03 14:00:17 +01:00
Paul Makles
23188032ca refactor(delta): port routes to core webhook models 2023-06-03 13:01:43 +01:00
Paul Makles
f9f5a30e2c refactor(quark): strip webhook code 2023-06-03 13:01:28 +01:00
Paul Makles
e393e17b59 feat(core/result): add rocket and okapi support 2023-06-03 13:01:12 +01:00
Paul Makles
a29d8f6aab refactor(core/models): swap dependency order with db
feat(core/models): implement webhook model
2023-06-03 13:01:01 +01:00
Paul Makles
1dbbc3ed8d feat(core/database): implement webhook model 2023-06-03 13:00:05 +01:00
Paul Makles
410acb5b5c merge: branch 'master' into webhooks 2023-06-02 18:55:32 +01:00
Paul Makles
53c9160cd6 feat: implement creating strikes
feat: add moderator_id to strikes
fix: use correct collection for strikes
2023-05-31 19:04:20 +01:00
Paul Makles
28b1170633 ci: switch to stable Rust releases for builds 2023-05-31 17:22:12 +01:00
Paul Makles
4abd4070f7 feat: allow editing owned bots using user edit route 2023-05-31 17:10:57 +01:00
Paul Makles
bca17b11a7 feat: filter reports on fetch 2023-05-31 17:04:33 +01:00
Paul Makles
2a6d532852 feat: allow fetching channels with server 2023-05-31 14:05:46 +01:00
Paul Makles
a9a5af8cc8 feat: account strike CRUD API 2023-05-31 13:39:13 +01:00
Paul Makles
edfa8e5256 feat(core/models): add account strikes 2023-05-31 13:06:16 +01:00
Paul Makles
2ebcfdc770 refactor: clean up hints 2023-05-31 13:02:59 +01:00
Paul Makles
fc47df786b feat(core/database): add account strikes model 2023-05-31 13:01:19 +01:00
Paul Makles
f6aa405607 fix(core/database): create user for bot CRUD test 2023-05-31 12:58:36 +01:00
Paul Makles
101e8feaa2 fix: fallback to empty string when deserialising 2023-05-25 11:25:25 +01:00
ToastXC
df8f588e15 fix: remove check for bot account under group member fetch (#245) 2023-05-21 18:34:27 +01:00
Paul Makles
3ef549537e feat: de-duplicate user update events sent to clients 2023-05-21 16:22:42 +01:00
Paul Makles
d81d08f1ce feat: add a route for searching for server members 2023-05-21 16:09:14 +01:00
Paul Makles
a77b7717b8 feat: generate and fetch multiple snapshots 2023-05-21 15:36:36 +01:00
Paul Makles
d5f903781d feat: add more specificity to report reasons 2023-05-21 15:35:16 +01:00
Paul Makles
8b12546b56 fix: allow editing flags and discoverable 2023-05-15 17:18:33 +01:00
Paul Makles
5bb91a7113 merge: pull request #242 from Rexogamer/patch-2
chore: bump max bots count to 10
2023-05-15 17:17:17 +01:00
Sophie L
a04c2512b4 chore: bump max bots count to 10 2023-04-27 16:30:24 +01:00
Paul Makles
c3f0df6567 chore: add style guide [skip ci] 2023-04-27 12:04:50 +01:00
Paul Makles
8b8aec4dc1 feat(core/database): implement server member 2023-04-26 18:58:31 +01:00
Paul Makles
1d228ff76e feat(core/database): implement server model 2023-04-26 18:32:29 +01:00
Paul Makles
b5863c0104 feat(core/database): implement user settings model 2023-04-26 17:53:46 +01:00
Paul Makles
08ccc43f8d feat(core/database): implement user operations 2023-04-26 17:36:51 +01:00
Paul Makles
e33ae17061 refactor(core/database): AbstractAttachment -> AbstractAttachments 2023-04-26 16:51:52 +01:00
Paul Makles
60ebdb64d7 feat(core/database): implement File operations 2023-04-26 16:49:39 +01:00
Paul Makles
c27d0ad5bb chore: move relations.drawio 2023-04-26 16:45:51 +01:00
Paul Makles
c1b9d941f3 refactor(core/database): crud ordering in files 2023-04-26 16:36:38 +01:00
Zomatree
175866ce00 Merge remote-tracking branch 'revoltchat/master' into webhooks 2023-04-25 20:38:09 +01:00
Zomatree
8c5de18991 restrict webhooks to text and group dm channels 2023-04-25 20:30:16 +01:00
Paul Makles
1afb27a508 ci: compile in permissions crate 2023-04-24 10:08:22 +01:00
Paul Makles
6a8481f7df refactor: is-patched transitive through models 2023-04-23 23:09:21 +01:00
Paul Makles
7f201565c0 feat: implement unoptimised filter_online if redis is not patched 2023-04-23 23:06:51 +01:00
Paul Makles
ae9474b95d fix(core/database): gate rocket-impl properly 2023-04-23 22:17:34 +01:00
Paul Makles
050f3abc89 chore: update crate definitions and bump versions 2023-04-23 22:14:38 +01:00
Paul Makles
888c22cb54 feat(core/database): start implementing permissions 2023-04-23 22:12:15 +01:00
Paul Makles
f633fccbca feat(core/permissions): initial commit 2023-04-23 22:11:59 +01:00
Paul Makles
39fec310f9 refactor(core/models): move models into v0 module 2023-04-23 18:09:16 +01:00
Paul Makles
d5d922d830 feat(core/models): rest of the conversions to User model 2023-04-23 18:07:23 +01:00
Paul Makles
f8c8407af3 feat(core/models): basic implementation of User and File models 2023-04-23 17:44:36 +01:00
Paul Makles
e6d0d44c5a feat(core/database): basic implementation of User and File models 2023-04-23 17:44:23 +01:00
Paul Makles
1933c9ea3d feat(core/database): add Reference struct 2023-04-23 14:59:44 +01:00
Paul Makles
8a695b4bb5 ci: actually make the source folders for building deps 2023-04-23 09:03:37 +01:00
Paul Makles
bf71b45fbb ci: copy core crates into Dockerfile 2023-04-23 08:51:43 +01:00
Paul Makles
9124c9f1e3 ci: include new core crates in build 2023-04-22 23:42:17 +01:00
Paul Makles
bcc2d05625 merge: branch 'refactor/split-project-into-core-crates' 2023-04-22 21:13:45 +01:00
Paul Makles
22bfd720b5 merge: remote-tracking branch 'origin/master' into refactor/split-project-into-core-crates 2023-04-22 21:09:51 +01:00
Paul Makles
63f56aec0c ci: re-order services and testing 2023-04-22 20:30:02 +01:00
Paul Makles
dd3d7e9c49 fix(core/database): use configured database name for migrations 2023-04-22 20:15:10 +01:00
Paul Makles
dbb66edd9f chore(core/database): delete useless example binary 2023-04-22 20:07:56 +01:00
Paul Makles
8bfb48dff3 test(core/database): try migrating the database 2023-04-22 20:02:23 +01:00
Paul Makles
12d963d2bd refactor: create presence crate and add tests 2023-04-22 20:01:23 +01:00
Paul Makles
c817c2dd40 fix: don't use wildcard for log dependency 2023-04-22 18:03:19 +01:00
Paul Makles
8a230ba989 chore: change publishing order 2023-04-22 18:02:01 +01:00
Paul Makles
2a9cc3190c fix: forgot to specify version for result crate 2023-04-22 18:01:44 +01:00
Paul Makles
e84d55a697 chore: update Cargo.toml for core crates 2023-04-22 18:01:06 +01:00
Paul Makles
403a94f70c feat(delta): example implementation of new core models 2023-04-22 17:56:48 +01:00
Paul Makles
633eb78630 chore: migrate to revolt_optional_struct 2023-04-22 17:56:24 +01:00
Paul Makles
bbe1f4936c chore: bridge gap between core/result and quark 2023-04-22 17:56:09 +01:00
Paul Makles
11a87263be feat(core/models): implement Bot and PublicBot 2023-04-22 17:55:30 +01:00
Paul Makles
8b30dddc06 chore(core/result): add schema feature 2023-04-22 17:55:12 +01:00
Paul Makles
b93dd90caf refactor(core/database): use empty strings instead of options 2023-04-22 17:54:46 +01:00
Paul Makles
0054019f82 ci: run test on all pushes 2023-04-22 16:05:40 +01:00
Paul Makles
736220a94e ci: run tests with and without MongoDB 2023-04-22 16:03:08 +01:00
Paul Makles
a9c82791b3 fix: force OOP bot updates to avoid inconsistencies 2023-04-22 16:01:27 +01:00
Paul Makles
eacf4decab refactor: clean up database before and after tests 2023-04-22 16:01:07 +01:00
Paul Makles
b8cda2ec74 feat: add drop_database for tests 2023-04-22 16:00:57 +01:00
Paul Makles
69ab7e031b chore: add linting rules for disallowed methods 2023-04-22 16:00:30 +01:00
Paul Makles
ace6431cb8 fix: ensure database namespace is valid 2023-04-22 15:24:46 +01:00
Paul Makles
40790de909 feat: macros for reducing error boilerplate 2023-04-22 15:23:20 +01:00
Paul Makles
750037b5d2 test: use custom database name and add clean up 2023-04-22 14:58:55 +01:00
Paul Makles
6b10385c0d feat: implement bots including tests
this starts work on #78
2023-04-22 14:25:02 +01:00
Paul Makles
56ead0f894 feat: write result crate 2023-04-22 12:48:09 +01:00
Paul Makles
d633cba630 fix: migration example 2023-04-22 12:12:23 +01:00
Paul Makles
e43833c0ea refactor: combine models and db crate together 2023-04-22 12:12:02 +01:00
Paul Makles
7f86337cb2 refactor: split SISMEMBER into two commands 2023-04-22 10:41:12 +01:00
Paul Makles
f2bb388b3b refactor: minimum implementation of revolt-models and revolt-database 2023-04-22 00:01:56 +01:00
Paul Makles
43e45aef3f fix: patch redis-rs for SMISMEMBER support 2023-04-21 19:37:33 +01:00
Paul Makles
c5880596a4 refactor: use a separate online user ID set 2023-04-21 19:36:25 +01:00
Paul Makles
59a644891d fix: collect IDs as set first to deduplicate entries 2023-04-21 18:52:41 +01:00
Paul Makles
a326cdc736 refactor: remove unused import 2023-04-21 18:40:00 +01:00
Paul Makles
03a28dbb3e fix: use debian:bullseye image for glibc 2.29 2023-04-21 18:11:06 +01:00
Paul Makles
32542a822e feat: rewrite presence system to use sets 2023-04-21 17:28:13 +01:00
Paul Makles
1df90ff53b chore: update DSN for Sentry 2023-04-21 17:27:45 +01:00
Zomatree
57f00a283b make request function names the same as file name 2023-04-20 17:52:10 +01:00
Zomatree
312e1dd41f update code to be inline with reviews 2023-04-20 17:45:48 +01:00
ToastXC
487b979f0d fix: remove check for bot account fetching invites from a server (#237) 2023-04-20 11:05:13 +01:00
ToastXC
b9d813d8f0 fix: deny access for bot creating a server (#238) 2023-04-20 11:04:51 +01:00
Paul Makles
36dd128459 merge: pull request #234 from thebearingedge/arm64-support 2023-04-20 11:04:35 +01:00
Zomatree
426d836a09 Merge remote-tracking branch 'revoltchat/master' into webhooks 2023-04-18 19:50:04 +01:00
Zomatree
2fad66d24a only send a subsect of the webhook in a message 2023-04-18 19:39:34 +01:00
Zomatree
144f0d39c6 fix: Add limits to channels and roles 2023-04-10 17:39:21 +01:00
Zomatree
83429f9f65 fix broken merge conflict 2023-04-01 23:09:29 +01:00
Angelo Kontaxis
a60ae78176 Merge branch 'master' into webhooks 2023-04-01 22:52:11 +01:00
Zomatree
7b39f4e9d9 change how webhook information is in the webhook 2023-04-01 22:49:34 +01:00
Tim Davis
32a294a64a feat: add support for arm64 (#142)
- set explicit action permissions
- make package owner dynamic so forks can test workflow
- build multi platform images using cross compilation amd64 -> arm64
- add a scope to buildx gha cache
2023-03-21 12:23:57 -07:00
Paul Makles
b7b70346b4 fix: add additional checks to group members 2023-03-18 22:32:15 +00:00
Paul Makles
59832e6ba2 fix(ratelimiter): mutate entry instead of copying 2023-03-15 21:54:58 +00:00
Zomatree
f309218573 chore: cleanup code 2023-03-15 19:47:30 +00:00
Zomatree
0321eff62b chore: move to once_cell from lazy_static 2023-03-15 13:47:32 +00:00
Zomatree
01577fd798 avoid leaking the token in events 2023-03-11 23:24:10 +00:00
Paul Makles
4c8ea31d98 feat(admin): add global message query route 2023-03-11 17:26:48 +00:00
Paul Makles
92ac86a6bd chore: bump version
fix: separate privileged check when editing server
2023-03-11 17:04:30 +00:00
Paul Makles
13ed69c82a feat: privileged user editing 2023-03-11 16:53:25 +00:00
Paul Makles
b83f6da648 feat: allow editing server flags through edit route 2023-03-11 16:40:23 +00:00
Paul Makles
89f1167239 refactor: unify message query into one method 2023-03-11 16:37:48 +00:00
Paul Makles
82d868751f chore: add privileged override for user permission 2023-03-04 20:45:49 +00:00
Paul Makles
31b9f18921 chore: bump version and publish 0.5.13 2023-03-04 11:19:53 +00:00
Paul Makles
fd80823910 fix: ensure we have send perm to edit message 2023-03-04 11:18:18 +00:00
Paul Makles
6bd8221eda feat(core): add raw db statisics endpoint for admin panel 2023-03-04 11:18:05 +00:00
Paul Makles
ce77e926a5 fix(safety): implement fetch_snapshot fully 2023-03-03 20:14:30 +00:00
Paul Makles
2710edb76b feat(safety): add api route for fetching a report 2023-03-03 08:17:55 +00:00
Paul Makles
8f1ff9e774 feat(safety): add API route for editing report 2023-03-02 12:54:55 +00:00
Paul Makles
c09244039e chore: bump and release version 2023-03-01 19:14:31 +00:00
Paul Makles
bf9108408e feat(safety): fetch additional context for snapshot 2023-03-01 15:09:17 +00:00
Zomatree
fca91e1560 update upstream code 2023-03-01 11:49:28 +00:00
Paul Makles
304336d905 feat(safety): fetch reports and fetch snapshot API 2023-03-01 11:22:22 +00:00
Paul Makles
42b4906594 chore: add database migrations for safety 2023-03-01 11:21:58 +00:00
Angelo Kontaxis
3c881a40a6 Merge branch 'master' into webhooks 2023-02-23 17:26:59 +00:00
Paul Makles
3038fb230f feat(safety): save surrounding context around messages (closes #226) 2023-02-23 18:12:45 +01:00
Paul Makles
73af105feb feat(safety): add global event for report creation (closes #225) 2023-02-23 17:58:05 +01:00
Paul Makles
2de37b23c1 merge: branch local cleanup branch 2023-02-22 12:45:39 +01:00
Paul Makles
0c75d8c8b3 refactor: remove old .github directories [skip ci] 2023-02-22 12:44:46 +01:00
Paul Makles
be793b1570 chore(ci): update commit author [skip ci] 2023-02-22 12:44:24 +01:00
Paul Makles
5a3a97df34 merge: pull request #223 from revoltchat/insert/feat/user-safety-api 2023-02-22 08:52:37 +00:00
Paul Makles
0c072b01d7 fix(safety): prevent reporting own content or self 2023-02-21 12:42:08 +01:00
Paul Makles
056c0380b2 feat(safety): implement report content API 2023-02-21 12:38:35 +01:00
Paul Makles
49598daf70 fix: port user safety API to new codebase 2023-02-17 09:45:34 +00:00
Paul Makles
0445181d5b merge: branch 'master' into insert/feat/user-safety-api 2023-02-17 09:43:12 +00:00
Paul Makles
e780178cc0 fix(ci): move token definition to correct stage 2023-01-29 16:46:19 +00:00
Paul Makles
c4b9884825 fix(ci): use correct token 2023-01-29 16:36:44 +00:00
Paul Makles
fdab997d17 chore(ci): switch testing to stable Rust 2023-01-29 14:23:56 +00:00
Paul Makles
8e0addc3e0 fix: fallback properly if no VERGEN strings can generate 2023-01-29 14:22:57 +00:00
Paul Makles
0da4baf777 chore: bump version and clean up repo 2023-01-29 14:16:18 +00:00
Paul Makles
c03fcda85b refactor: switch from "rauth" to Authifier crate 2023-01-29 14:14:35 +00:00
Zomatree
c7849a82bf Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-27 17:27:32 +00:00
Zomatree
781d51df6f add seperate non token webhook routes 2023-01-27 17:26:32 +00:00
Paul Makles
0afbcc065f refactor: clippy warning for empty list 2023-01-27 16:18:00 +00:00
Paul Makles
a406e5504d chore: add support for Streamable in media struct 2023-01-27 16:17:47 +00:00
Zomatree
e8c233f60b feat: concurrently query January for embeds (#221) 2023-01-27 16:06:27 +00:00
Zomatree
758b1e43e7 no longer make mesage_send.rs public 2023-01-24 22:54:09 +00:00
Zomatree
5cc8231c36 deduplicate message sending 2023-01-24 22:52:18 +00:00
insertish
72595f30d4 ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2023-01-24 19:43:40 +00:00
insertish
39e345af74 ci: synced local '.github/workflows/triage_issue.yml' with remote 'workflows/triage_issue.yml'
[skip ci]
2023-01-24 19:43:40 +00:00
Zomatree
7b447faefa add pull request events 2023-01-21 22:57:12 +00:00
Zomatree
a017256e95 add more events 2023-01-20 20:15:12 +00:00
Zomatree
1050891d87 Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-20 20:04:18 +00:00
Paul Makles
e0b918771d chore: ensure restrict_reactions is used in conjunction with reactions list 2023-01-20 18:06:44 +00:00
Paul Makles
7a6bd70dcd feat: add route to fetch user flags
chore: bump rauth to change disabled account behaviour
2023-01-20 17:47:26 +00:00
Paul Makles
c0ef3d295a fix: fallback if vergen cannot generate data 2023-01-20 17:14:25 +00:00
Paul Makles
f5c95f9d12 chore: bump rauth for lockout reset on pwd reset 2023-01-20 15:57:58 +00:00
Paul Makles
fed1da72fd feat: support rauth events
feat: add platform user wipe event
2023-01-20 15:43:45 +00:00
Paul Makles
cf9c0bc5e1 chore: bump rauth 2023-01-20 14:00:19 +00:00
Paul Makles
cc2054838a chore: swap okapi for revolt_okapi 2023-01-20 14:00:19 +00:00
Zomatree
a246cff7f0 Merge branch 'master' of github.com:revoltchat/backend into webhooks 2023-01-18 21:03:15 +00:00
Paul Makles
d9d2735efa fix(ratelimiter): re-calculate reset time
closes #204

Co-authored-by: TheBobBobs <84781603+TheBobBobs@users.noreply.github.com>
2023-01-18 18:26:00 +00:00
Ryan
94b0472934 feat: pass server flags with invite data (#203) 2023-01-18 18:17:59 +00:00
Sophie L
4a7917e9ea feat: enable reactions by default in servers/DMs 2023-01-18 18:17:37 +00:00
Lea
ce6b454874 fix: raise masquerade avatar URL length limit 2023-01-18 18:17:05 +00:00
ToastXC
fc32996b51 chore: remove clippy warnings (#213) 2023-01-18 18:16:27 +00:00
Paul Makles
d74104fae4 feat: initial ideas for content reporting 2023-01-18 12:35:49 +00:00
Paul Makles
32d3683013 fix(ci): re-order docker-compose arguments
chore(ci): prevent running gen on PR
2023-01-14 15:53:09 +00:00
Paul Makles
0cebb69e02 feat: automate generating OpenAPI specification 2023-01-14 15:42:30 +00:00
Paul Makles
117958d8cd feat: include build information in root api call 2023-01-14 15:42:06 +00:00
Paul Makles
c5b823ad13 chore: clippy warnings 2023-01-03 21:06:43 +00:00
Paul Makles
27b4699299 fix: unify foreign user fetch into one method 2023-01-03 21:04:04 +00:00
Zomatree
c218db5047 add push event 2022-12-30 22:44:36 +00:00
Zomatree
c7f2f3614f star event 2022-12-30 18:08:43 +00:00
Zomatree
c826d8cf07 add github webhook support 2022-11-27 06:52:44 +00:00
Zomatree
5cb2320760 inital webhook support 2022-11-26 23:07:22 +00:00
Paul Makles
ee1c4cc2d5 chore: bump quark
feat: update email templates to include recipient
2022-11-19 16:52:51 +00:00
Paul Makles
0e0daf86f7 feat: update emails to not look garbage 2022-10-23 21:23:29 +01:00
Paul Makles
572e08c733 chore: update rauth 2022-10-23 21:08:08 +01:00
Paul Makles
9011939344 chore: remove .env file 2022-10-23 20:55:45 +01:00
Paul Makles
f4dcd091cc feat: add 'Focus' presence status 2022-09-18 12:12:31 +01:00
Paul Makles
86ad72b426 fix: strip unknown channels from categories (fixes #201) 2022-09-12 21:39:29 +01:00
Paul Makles
044d82d566 fix: use correct validation for masq role colour 2022-09-03 10:06:49 +01:00
Paul Makles
deffe663cb feat: add ability to leave groups / servers silently 2022-09-02 15:32:11 +01:00
Paul Makles
e7f6433d77 fix: actually transfer group ownership 2022-09-01 09:45:17 +01:00
Paul Makles
345e4d5f13 fix: validate categories 2022-09-01 09:41:00 +01:00
Sophie L
81fc3e7760 fix: set the cap to 100 2022-09-01 09:01:43 +01:00
Paul Makles
ca8d2e6118 fix: reaction ordering by using indexmap 2022-08-14 11:47:30 +02:00
Paul Makles
c1227e8e33 fix: check React permission on interactions 2022-08-14 11:31:36 +02:00
Paul Makles
0e0c6f2c3a fix: validate number of reactions on message when adding more 2022-08-13 20:26:09 +02:00
Paul Makles
fa82be74b5 fix: adjust permission checks for member_edit 2022-07-26 14:06:53 +01:00
Paul Makles
8491ced13d fix: ensure list matches 2022-07-18 12:46:33 +01:00
Paul Makles
229d4e2e1d fix: correct ordering for creating ownership change event 2022-07-15 21:46:41 +01:00
Paul Makles
7bdd2d69d6 feat: add message reactions + interactions object 2022-07-15 21:24:49 +01:00
Paul Makles
ab2af9b5e5 feat: add group ownership transfer 2022-07-15 17:58:08 +01:00
Paul Makles
5aebda2d16 chore: restrict emojis to lowercase 2022-07-15 17:57:53 +01:00
Paul Makles
a7273d0556 feat: allow servers to set discoverable value 2022-07-15 17:57:30 +01:00
Paul Makles
72aed64e5b chore: allow banning users not in server 2022-07-15 16:47:27 +01:00
Paul Makles
00ad4d54e7 feat: add nsfw flag to emotes 2022-07-15 16:34:08 +01:00
Paul Makles
cba8c4ef3b fix: add case-insensitive flag to colour regex 2022-07-15 16:13:04 +01:00
Paul Makles
b6e395fe0d fix: use default for roles key 2022-07-15 16:02:58 +01:00
Paul Makles
8d07457c58 chore: make joined_at migration shorter 2022-07-15 16:02:46 +01:00
Paul Makles
f1171e5358 feat: restrict role colours to regex (increase length limit) 2022-07-15 15:36:00 +01:00
Paul Makles
d96c9f62c4 chore: detach emojis on delete 2022-07-15 14:01:59 +01:00
Paul Makles
4f73e43a03 feat: restrict permissions for users in timeout 2022-07-15 13:55:55 +01:00
Paul Makles
741b8ee8fd feat: add role colours to masquerades
feat: add `joined_at` property to members
feat: add `timeout` property to members (non-functional)
2022-07-15 13:42:01 +01:00
Paul Makles
692081b7f0 chore: bump rauth for verification fix 2022-07-14 14:01:10 +01:00
Paul Makles
943b1f08f0 fix: prevent short-circuit on permissions 2022-07-10 18:27:22 +01:00
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
Paul
ad06ff16c4 Messaging: Upsert mentions into unreads.
Messaging: Allow multiple attachment upload.
2021-06-22 17:42:18 +01:00
Paul
b10d4f3559 Messaging: Add mentions to unread object. 2021-06-17 14:42:48 +01:00
Paul
4727f997ed New Feature: Add server-side channel unreads. 2021-06-16 23:24:31 +01:00
Paul
d2b17b64e7 Docker: Publish to :latest tag when building. 2021-06-16 19:36:59 +01:00
Paul
598f6374b3 Messaging: Don't embed URLs in code. 2021-06-16 16:43:17 +01:00
Paul
bad9024537 Messaging: Remove embed when editing message. 2021-06-16 16:31:06 +01:00
Paul
6d67b3f767 Fix: Prevent users banning themselves. 2021-06-16 15:46:54 +01:00
Paul
9a2ccecb89 Channels: Make sure to delete last, to keep integrity. 2021-06-16 15:42:46 +01:00
Paul
c4b39c56d4 Fix: Delete channel query failing due to empty unset. 2021-06-16 14:15:53 +01:00
Paul
f33611aee6 Fix: Handle status::User on group create. 2021-06-14 16:00:29 +01:00
Paul
95d7c23d32 Badges: Add responsible disclosure badge.
Fix: Don't serialize None on Metadata. (january embed)
2021-06-13 20:19:07 +01:00
Paul
6b8158bf54 Fix: Use channel_invites collection name instead of invites.
Was conflicting with invite-only rauth registrations.
2021-06-12 10:33:31 +01:00
Paul
9be9b76035 Servers: Add new system messages. 2021-06-11 22:12:01 +01:00
Paul
03fa923ef4 Refactor: Send system messages directly from content. 2021-06-11 19:18:46 +01:00
Paul
78c890aa59 Messaging: Parse mentions. 2021-06-11 18:07:37 +01:00
Paul
0f18a6781d Channels: Include last message id on text channels.
API: Return members when fetching messages.
Misc: Remove defunct DISABLE_REGISTRATION variable.
2021-06-11 16:22:35 +01:00
Paul
f0d1ab390b Invites: Include server and channel ID.
Permissions: Small fix, consider self as User.
2021-06-10 14:56:53 +01:00
Paul
335f729c53 Notifications: Remove ServerCreate event. 2021-06-07 18:28:28 +01:00
Paul
c763a292a7 Run cargo fmt. 2021-06-07 16:16:40 +01:00
Paul
fce92382bf Servers: Fix create / leave.
Prevent removal of self / owner.
2021-06-07 16:14:48 +01:00
Paul
4e90cf1f58 Servers: Ban and unban users +
ban list.
2021-06-06 16:52:00 +01:00
Paul
782bfb7e03 Servers: Route for kicking a member. 2021-06-06 15:22:23 +01:00
Paul
1f1d9613e2 Servers: Fetch and edit members.
Permissions: Use bit representation for permissions.
2021-06-06 14:57:55 +01:00
Paul
0af4fa4be9 Servers: Add description field.
API: Allow removal of description on channels and servers.
2021-06-06 10:55:31 +01:00
Paul
380369e1c4 Servers: Route for fetching server invites. 2021-06-06 10:31:06 +01:00
Paul
dbd70abaca Servers: Add route for fetching members. 2021-06-06 10:17:33 +01:00
Paul
f9fbe54b17 Servers: Create and delete invites. Create and fetch servers. 2021-06-05 22:41:47 +01:00
Paul
681b2b8ab6 Servers: Add invites. Create, view and join. 2021-06-03 20:00:08 +01:00
Paul
812fa2a98f Permissions: Resolve guild, allows ability to delete channels, delete others messages, etc. 2021-06-02 12:35:01 +01:00
Paul
1713ad057d Servers: Add route for creating channels.
Notifications: Subscribe to guild channels.
2021-06-02 11:51:58 +01:00
Paul
f32f447233 Notifications: Subscribe users to new guild channels. 2021-06-02 10:27:51 +01:00
Paul
8627f27a50 Channels: Don't return self on msg query. 2021-06-02 10:12:53 +01:00
insert
fea68faa54 Add LICENSE 2021-06-01 19:45:42 +00:00
Paul
46bf5fcdd1 Servers: Add TextChannel. 2021-06-01 20:38:35 +01:00
Paul
6bb4501c52 Servers: Add edit route. Ability to set icon / banner. 2021-06-01 17:42:23 +01:00
Paul
bff72fa6c3 Servers: Core objects required. Includes creation. Bump version to 0.5.0. 2021-06-01 17:09:31 +01:00
Paul
a4c1fee4cc Sync: Allow custom timestamp; return correct data structure. 2021-06-01 13:25:47 +01:00
Paul
62ffa10939 rAuth: Silently fail email send for creation. 2021-05-27 15:51:24 +01:00
Paul
a39adb64c6 Hive: Don't send out sync for irrelevant users. 2021-05-26 13:19:03 +01:00
Paul
3cd3bc54af New Feature: Settings sync to server. 2021-05-26 13:02:40 +01:00
Paul
5aa9624d5c Database: Add user settings sync entity.
Run cargo fmt.
2021-05-26 11:02:16 +01:00
Paul
7aad88ad3f Messages: Allow multiple attachments. (⚠️ breaking change) 2021-05-25 17:23:47 +01:00
Paul
fa3bc56ef4 Channels: Add option to return users in message query. 2021-05-25 15:03:49 +01:00
Paul
7c7943b9f6 Websocket: Provide channel id for message edit / del. 2021-05-25 14:06:23 +01:00
Paul
67b9b8ceee Users: Serve early adopter badge from server. 2021-05-25 13:37:12 +01:00
Paul
f1462d4e4d Remove snafu. 2021-05-15 18:47:05 +01:00
Paul
fae0198343 Bump version to 0.4.1-alpha.11 2021-05-14 22:54:27 +01:00
Paul
10f116b4a3 Block voice calls in saved messages channel.
Add messages for desc / icon change for group chats.
2021-05-14 22:44:35 +01:00
Paul
6716a2d32b Join with newlines for embed generation.
Server January URL endpoint on server configuration.
2021-05-14 22:39:16 +01:00
Paul
6cc92b877e Fix block user route, send correct user struct.
Add route for fetching members.
Cargo fmt on accident.
2021-05-14 22:29:43 +01:00
Paul
cc0307f702 Fix find mutual query. 2021-05-13 17:19:43 +01:00
Paul
274d6f2b5a Update January definitions. (special embeds) 2021-05-09 19:30:55 +01:00
Paul
0f100213ba Update January definitions; add servers collection 2021-05-08 17:08:45 +01:00
Paul
7293abfc53 Add january support. 2021-05-07 18:00:21 +01:00
Paul
cf7bb832da Really make sure MongoDB driver can't screw up. 2021-05-03 21:02:12 +01:00
Paul
efc3794f3d Emergency patch; Rust MDB driver deletes doc. with empty update. 2021-05-03 20:48:37 +01:00
Paul
a319e72655 Add a way to choose message fetch sort. 2021-05-03 16:35:59 +01:00
Paul
2173b1e9f8 Fix type. 2021-05-03 15:04:05 +01:00
Paul
c086fe7ac4 Move versioning to source. 2021-05-03 15:03:30 +01:00
Paul
fa960ebc94 Send out unset in event. 2021-05-03 11:17:28 +01:00
Paul
b47067b311 Cache Cargo dependencies for Docker build. 2021-05-03 09:59:40 +01:00
Paul
2e996a487b Add event when user goes offline. 2021-05-03 08:57:49 +01:00
Paul
92bface6ae Add group icons / profile backgrounds. 2021-05-01 22:55:37 +01:00
Paul
8cfa5d7091 Run cargo fmt. 2021-05-01 17:29:31 +01:00
Paul
59b18fd376 Delete old avatar; fix database migration. 2021-05-01 17:12:51 +01:00
Paul
f135a57a9b API change: deprecate Gravatar.
Include avatar information in user object.
2021-05-01 16:38:06 +01:00
Paul
5da26cb833 Breaking: Provide new user object instead of id.
Fix rustup complaining about join macros.
2021-05-01 15:54:29 +01:00
Paul
c8981ac695 Merge branch 'master' of https://gitlab.insrt.uk/revolt/delta 2021-05-01 13:08:55 +01:00
Martin Loffler
33b0658680 Multi-thread rocket and hive 2021-04-30 13:19:31 +02:00
Paul
81002c75d2 Only push out status; mark attachments as deleted. 2021-04-28 17:12:14 +01:00
Paul
f163cb65de AAAAAAAAAAAAA
We should be setting message_id not message.
2021-04-24 18:01:08 +01:00
Paul
f7bcd3ad93 Migrate to Autumn 1.0.0 2021-04-24 16:00:13 +01:00
Paul
9e2250783c Allow people with mutual connections to see profile / status; fix username check. 2021-04-07 19:21:25 +01:00
Paul
2050a0609f Fix update_one failing on sys message. 2021-04-03 21:37:24 +01:00
Paul
a547177325 Add status presence indicator. 2021-04-03 20:50:14 +01:00
Paul
c3fa7bf74e Change route paths for user edits. 2021-04-03 15:02:29 +01:00
Paul
7f5d6f2312 Format code. 2021-04-03 14:44:01 +01:00
Paul
9492e145f9 User status / profile change route, username change route. 2021-04-03 14:41:40 +01:00
Paul
78c42fd412 Fetch profile route, push badges and status. 2021-04-02 21:55:02 +01:00
Paul
3797878d20 Fixes bug with distributed nodes. 2021-04-02 13:25:51 +01:00
Paul
d2bf477fd8 Expose voso host. 2021-04-01 15:11:19 +01:00
Martin Loffler
c9bff789de Fixed room creation failure in join_call 2021-04-01 15:59:06 +02:00
Paul
56bd763820 Prevent setting null in edit. Preflight for variables. 2021-04-01 14:01:07 +01:00
Paul
32cd9d8a13 Strict typing for system messages; add a way to rename group. 2021-03-31 20:54:47 +01:00
Paul
60731e1c70 Integrate voso, join_call route for channels. 2021-03-31 11:48:52 +01:00
449 changed files with 35066 additions and 5358 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

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

@@ -0,0 +1,124 @@
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:
permissions:
contents: read
packages: write
jobs:
base:
runs-on: ubuntu-latest
name: Build base image
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Authenticate with GHCR
- name: Login to Github Container Registry
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest
cache-from: type=gha,scope=buildx-base-multi-arch
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
publish:
needs: [base]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
strategy:
matrix:
project: [delta, bonfire]
name: Build ${{ matrix.project }} image
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v3
- 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:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v2
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": "${{ github.repository_owner }}/server"
},
"bonfire": {
"path": "crates/bonfire",
"tag": "${{ github.repository_owner }}/bonfire"
}
}
export_to: output
# Configure metadata
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
docker.io/${{ steps.export.outputs.tag }}
ghcr.io/${{ steps.export.outputs.tag }}
# Build crate image
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: ${{ steps.export.outputs.path }}/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta.outputs.labels }}

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

@@ -0,0 +1,81 @@
name: Rust build, test, and generate specification
on:
push:
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 services in background
run: |
docker-compose -f docker-compose.db.yml up -d
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
- name: Run cargo test (with MongoDB)
uses: actions-rs/cargo@v1
env:
MONGODB: mongodb://localhost
with:
command: test
- name: Copy .env.example
if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: |
cp .env.example .env
- name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: |
cargo run --bin revolt-delta &
- name: Wait for API to go up
if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: nev7n/wait_for_response@v1
with:
url: "http://localhost:8000/"
- name: Checkout API repository
if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: actions/checkout@v3
with:
repository: revoltchat/api
path: api
token: ${{ secrets.PAT }}
- name: Download OpenAPI specification
if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
- name: Commit changes
if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: EndBug/add-and-commit@v4
with:
cwd: "api"
add: "*.json"
author_name: Revolt CI
author_email: revolt-ci@users.noreply.github.com
message: "chore: generate OpenAPI specification"

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

8
.gitignore vendored
View File

@@ -1,6 +1,6 @@
Rocket.toml
/target
**/*.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

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

@@ -0,0 +1,4 @@
{
"editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy"
}

3522
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,49 +1,6 @@
[package]
name = "revolt"
version = "0.3.3-alpha.8"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
[workspace]
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = "0.13.0"
futures = "0.3.8"
impl_ops = "0.1.1"
web-push = "0.7.2"
many-to-many = "0.1.2"
ctrlc = { version = "3.0", features = ["termination"] }
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "c52758a5087cd035b0ed9c6eacc942ba5468d2ce" }
hive_pubsub = { version = "0.4.3", features = ["mongo"] }
rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" }
rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false }
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
once_cell = "1.4.1"
dotenv = "0.15.0"
ulid = "0.4.1"
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.11", features = ["derive"] }
snafu = { version = "0.6.9" }
serde_json = "1.0.57"
bitfield = "0.13.2"
reqwest = { version = "0.10.8", features = ["json"] }
urlencoding = "1.1.1"
lazy_static = "1.4.0"
num_enum = "0.5.1"
chrono = "0.4.15"
time = "0.2.16"
rand = "0.7.3"
md5 = "0.7.0"
regex = "1"
lettre = "0.10.0-alpha.1"
env_logger = "0.7.1"
log = "0.4.11"
[patch.crates-io]
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
redis = { git = "https://github.com/insertish/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }

View File

@@ -1,22 +1,32 @@
# Build Stage
FROM ekidd/rust-musl-builder:nightly-2021-01-01 AS builder
FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
USER 0:0
WORKDIR /home/rust/src
RUN USER=root cargo new --bin revolt
WORKDIR /home/rust/src/revolt
COPY Cargo.toml Cargo.lock ./
COPY assets/templates ./assets/templates
COPY src ./src
RUN cargo build --release
ARG TARGETARCH
# 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"]
# Install build requirements
RUN dpkg --add-architecture "${TARGETARCH}"
RUN apt-get update && \
apt-get install -y \
make \
pkg-config \
libssl-dev:"${TARGETARCH}"
COPY scripts/build-image-layer.sh /tmp/
RUN sh /tmp/build-image-layer.sh tools
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/quark/Cargo.toml ./crates/quark/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps
COPY crates ./crates
RUN sh /tmp/build-image-layer.sh apps

661
LICENSE Normal file
View File

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

67
STYLE_GUIDE.md Normal file
View File

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

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

15
clippy.toml Normal file
View File

@@ -0,0 +1,15 @@
disallowed-methods = [
# Shouldn't need to access these directly
"revolt_database::models::bots::model::Bot::remove_field",
# Prefer to use Object::create()
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::insert_account_strike",
# Prefer to use Object::update(&self)
"revolt_database::models::bots::ops::AbstractBots::update_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
# Prefer to use Object::delete(&self)
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
]

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

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

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

@@ -0,0 +1,31 @@
[package]
name = "revolt-bonfire"
version = "0.6.1"
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"] }
# core
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }

11
crates/bonfire/Dockerfile Normal file
View File

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

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_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.
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,252 @@
use std::net::SocketAddr;
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
use revolt_presence::{create_session, delete_session};
use revolt_quark::{
events::{
client::EventV1,
server::ClientMessage,
state::{State, SubscriptionStateChange},
},
models::{user::UserHint, User},
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) = 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 = 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:?}");
});
}

View File

@@ -0,0 +1,67 @@
[package]
name = "revolt-database"
version = "0.6.1"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
description = "Revolt Backend: Database Implementation"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
# Databases
mongodb = [ "dep:mongodb", "bson" ]
# ... Other
async-std-runtime = [ "async-std" ]
rocket-impl = [ "rocket", "schemars" ]
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
# Default Features
default = [ "mongodb", "async-std-runtime" ]
[dependencies]
# Core
revolt-result = { version = "0.6.1", path = "../result" }
revolt-models = { version = "0.6.1", path = "../models" }
revolt-presence = { version = "0.6.1", path = "../presence" }
revolt-permissions = { version = "0.6.1", path = "../permissions", features = [ "serde", "bson" ] }
# Utility
log = "0.4"
rand = "0.8.5"
ulid = "1.0.0"
nanoid = "0.4.0"
once_cell = "1.17"
# Serialisation
serde_json = "1"
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
# Events
redis-kiss = { version = "0.1.4" }
# Database
bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "2.1.0", default-features = false }
# Database Migration
unicode-segmentation = "1.10.1"
regex = "1"
# Async Language Features
futures = "0.3.19"
async-trait = "0.1.51"
async-recursion = "1.0.4"
# Async
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Rocket Impl
schemars = { version = "0.8.8", optional = true }
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"], optional = true }
# Authifier
authifier = { version = "1.0" }

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
AccountStrike, Bot, Channel, File, Member, MemberCompositeKey, Server, User, UserSettings,
Webhook,
};
database_derived!(
/// Reference implementation
#[derive(Default)]
pub struct ReferenceDb {
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub server_bans: Arc<Mutex<HashMap<String, ()>>>,
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
pub emoji: Arc<Mutex<HashMap<String, ()>>>,
pub messages: Arc<Mutex<HashMap<String, ()>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
}
);

View File

@@ -0,0 +1,269 @@
use authifier::AuthifierEvent;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{
Channel, FieldsChannel, FieldsWebhook, PartialChannel, PartialWebhook, Webhook,
};
use revolt_result::Error;
use crate::Database;
/// WebSocket Client Errors
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "error")]
pub enum WebSocketError {
LabelMe,
InternalError { at: String },
InvalidSession,
OnboardingNotFinished,
AlreadyAuthenticated,
MalformedData { msg: String },
}
/// Ping Packet
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Ping {
Binary(Vec<u8>),
Number(usize),
}
/// Untagged Error
#[derive(Serialize)]
#[serde(untagged)]
pub enum ErrorEvent {
Error(WebSocketError),
APIError(Error),
}
/// Protocol Events
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum EventV1 {
/// Multiple events
Bulk { v: Vec<EventV1> },
/// Successfully authenticated
Authenticated,
/* /// Basic data to cache
Ready {
users: Vec<User>,
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
emojis: Option<Vec<Emoji>>,
},
/// Ping response
Pong { data: Ping },
/// New message
Message(Message),
/// Update existing message
MessageUpdate {
id: String,
channel: String,
data: PartialMessage,
},
/// Append information to existing message
MessageAppend {
id: String,
channel: String,
append: AppendMessage,
},
/// Delete message
MessageDelete { id: String, channel: String },
/// New reaction to a message
MessageReact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove user's reaction from message
MessageUnreact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove a reaction from message
MessageRemoveReaction {
id: String,
channel_id: String,
emoji_id: String,
},
/// Bulk delete messages
BulkMessageDelete { channel: String, ids: Vec<String> },
/// New server
ServerCreate {
id: String,
server: Server,
channels: Vec<Channel>,
},
/// Update existing server
ServerUpdate {
id: String,
data: PartialServer,
clear: Vec<FieldsServer>,
},
/// Delete server
ServerDelete { id: String },
/// Update existing server member
ServerMemberUpdate {
id: MemberCompositeKey,
data: PartialMember,
clear: Vec<FieldsMember>,
},
/// User joins server
ServerMemberJoin { id: String, user: String },
/// User left server
ServerMemberLeave { id: String, user: String },
/// Server role created or updated
ServerRoleUpdate {
id: String,
role_id: String,
data: PartialRole,
clear: Vec<FieldsRole>,
},
/// Server role deleted
ServerRoleDelete { id: String, role_id: String },
/// Update existing user
UserUpdate {
id: String,
data: PartialUser,
clear: Vec<FieldsUser>,
event_id: Option<String>,
},
/// Relationship with another user changed
UserRelationship {
id: String,
user: User,
// ! this field can be deprecated
status: RelationshipStatus,
},
/// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings },
/// User has been platform banned or deleted their account
///
/// Clients should remove the following associated data:
/// - Messages
/// - DM Channels
/// - Relationships
/// - Server Memberships
///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 },
/// New emoji
EmojiCreate(Emoji),
/// Delete emoji
EmojiDelete { id: String },
/// New report
ReportCreate(Report), */
/// New channel
ChannelCreate(Channel),
/// Update existing channel
ChannelUpdate {
id: String,
data: PartialChannel,
clear: Vec<FieldsChannel>,
},
/// Delete channel
ChannelDelete { id: String },
/// User joins a group
ChannelGroupJoin { id: String, user: String },
/// User leaves a group
ChannelGroupLeave { id: String, user: String },
/// User started typing in a channel
ChannelStartTyping { id: String, user: String },
/// User stopped typing in a channel
ChannelStopTyping { id: String, user: String },
/// User acknowledged message in channel
ChannelAck {
id: String,
user: String,
message_id: String,
},
/// New webhook
WebhookCreate(Webhook),
/// Update existing webhook
WebhookUpdate {
id: String,
data: PartialWebhook,
remove: Vec<FieldsWebhook>,
},
/// Delete webhook
WebhookDelete { id: String },
/// Auth events
Auth(AuthifierEvent),
}
impl EventV1 {
/// Publish helper wrapper
pub async fn p(self, channel: String) {
#[cfg(not(debug_assertions))]
redis_kiss::p(channel, self).await;
#[cfg(debug_assertions)]
info!("Publishing event to {channel}: {self:?}");
#[cfg(debug_assertions)]
redis_kiss::publish(channel, self).await.unwrap();
}
/// Publish user event
pub async fn p_user(self, id: String, db: &Database) {
self.clone().p(id.clone()).await;
// ! FIXME: this should be captured by member list in the future
// ! and not immediately fanned out to users
if let Ok(members) = db.fetch_all_memberships(&id).await {
for member in members {
self.clone().p(member.id.server).await;
}
}
}
/// Publish private event
pub async fn private(self, id: String) {
self.p(format!("{id}!")).await;
}
/// Publish internal global event
pub async fn global(self) {
self.p("global".to_string()).await;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,213 @@
use super::scripts::LATEST_REVISION;
use crate::mongodb::bson::doc;
use crate::mongodb::options::CreateCollectionOptions;
use crate::MongoDb;
pub async fn create_database(db: &MongoDb) {
info!("Creating database.");
let db = db.db();
db.create_collection("accounts", None)
.await
.expect("Failed to create accounts collection.");
db.create_collection("users", None)
.await
.expect("Failed to create users collection.");
db.create_collection("channels", None)
.await
.expect("Failed to create channels collection.");
db.create_collection("messages", None)
.await
.expect("Failed to create messages collection.");
db.create_collection("servers", None)
.await
.expect("Failed to create servers collection.");
db.create_collection("server_members", None)
.await
.expect("Failed to create server_members collection.");
db.create_collection("server_bans", None)
.await
.expect("Failed to create server_bans collection.");
db.create_collection("channel_invites", None)
.await
.expect("Failed to create channel_invites collection.");
db.create_collection("channel_unreads", None)
.await
.expect("Failed to create channel_unreads collection.");
db.create_collection("channel_webhooks", None)
.await
.expect("Failed to create channel_webhooks collection.");
db.create_collection("migrations", None)
.await
.expect("Failed to create migrations collection.");
db.create_collection("attachments", None)
.await
.expect("Failed to create attachments collection.");
db.create_collection("user_settings", None)
.await
.expect("Failed to create user_settings collection.");
db.create_collection("safety_reports", None)
.await
.expect("Failed to create safety_reports collection.");
db.create_collection("safety_snapshots", None)
.await
.expect("Failed to create safety_snapshots collection.");
db.create_collection("safety_strikes", None)
.await
.expect("Failed to create safety_strikes collection.");
db.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
db.create_collection(
"pubsub",
CreateCollectionOptions::builder()
.capped(true)
.size(1_000_000)
.build(),
)
.await
.expect("Failed to create pubsub collection.");
db.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
},
None,
)
.await
.expect("Failed to create username index.");
db.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
},
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
},
{
"key": {
"author": 1_i32
},
"name": "author"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
db.run_command(
doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create channel_unreads index.");
db.run_command(
doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create server_members index.");
db.collection("migrations")
.insert_one(
doc! {
"_id": 0_i32,
"revision": LATEST_REVISION
},
None,
)
.await
.expect("Failed to save migration info.");
info!("Created database.");
}

View File

@@ -0,0 +1,954 @@
use std::{collections::HashSet, ops::BitXor, time::Duration};
use crate::{
mongodb::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
};
use futures::StreamExt;
use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Serialize, Deserialize)]
struct MigrationInfo {
_id: i32,
revision: i32,
}
pub const LATEST_REVISION: i32 = 25;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
let data = migrations
.find_one(None, None)
.await
.expect("Failed to fetch migration data.");
if let Some(doc) = data {
let info: MigrationInfo =
from_document(doc).expect("Failed to read migration information.");
let revision = run_migrations(db, info.revision).await;
migrations
.update_one(
doc! {
"_id": info._id
},
doc! {
"$set": {
"revision": revision
}
},
None,
)
.await
.expect("Failed to commit migration information.");
info!("Migration complete. Currently at revision {}.", revision);
} else {
panic!("Database was configured incorrectly, possibly because initalization failed.")
}
}
pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Starting database migration.");
if revision <= 0 {
info!("Running migration [revision 0]: Test migration system.");
}
if revision <= 1 {
info!("Running migration [revision 1 / 2021-04-24]: Migrate to Autumn v1.0.0.");
let messages = db.col::<Document>("messages");
let attachments = db.col::<Document>("attachments");
messages
.update_many(
doc! { "attachment": { "$exists": 1_i32 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0_i32 } },
None,
)
.await
.expect("Failed to update messages.");
attachments
.update_many(
doc! {},
doc! { "$set": { "tag": "attachments", "size": 0_i32 } },
None,
)
.await
.expect("Failed to update attachments.");
}
if revision <= 2 {
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
db.db()
.create_collection("servers", None)
.await
.expect("Failed to create servers collection.");
}
if revision <= 3 {
info!("Running migration [revision 3 / 2021-05-25]: Support multiple file uploads, add channel_unreads and user_settings.");
let messages = db.col::<Document>("messages");
let mut cursor = messages
.find(
doc! {
"attachment": {
"$exists": 1_i32
}
},
FindOptions::builder()
.projection(doc! {
"_id": 1_i32,
"attachments": [ "$attachment" ]
})
.build(),
)
.await
.expect("Failed to fetch messages.");
while let Some(result) = cursor.next().await {
let doc = result.unwrap();
let id = doc.get_str("_id").unwrap();
let attachments = doc.get_array("attachments").unwrap();
messages
.update_one(
doc! { "_id": id },
doc! { "$unset": { "attachment": 1_i32 }, "$set": { "attachments": attachments } },
None,
)
.await
.unwrap();
}
db.db()
.create_collection("channel_unreads", None)
.await
.expect("Failed to create channel_unreads collection.");
db.db()
.create_collection("user_settings", None)
.await
.expect("Failed to create user_settings collection.");
}
if revision <= 4 {
info!("Running migration [revision 4 / 2021-06-01]: Add more server collections.");
db.db()
.create_collection("server_members", None)
.await
.expect("Failed to create server_members collection.");
db.db()
.create_collection("server_bans", None)
.await
.expect("Failed to create server_bans collection.");
db.db()
.create_collection("channel_invites", None)
.await
.expect("Failed to create channel_invites collection.");
}
if revision <= 5 {
info!("Running migration [revision 5 / 2021-06-26]: Add permissions.");
#[derive(Serialize)]
struct Server {
pub default_permissions: (i32, i32),
}
let server = Server {
default_permissions: (0_i32, 0_i32),
};
db.col::<Document>("servers")
.update_many(
doc! {},
doc! {
"$set": to_document(&server).unwrap()
},
None,
)
.await
.expect("Failed to migrate servers.");
}
if revision <= 6 {
info!("Running migration [revision 6 / 2021-07-09]: Add message text index.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
}
if revision <= 7 {
info!("Running migration [revision 7 / 2021-08-11]: Add message text index.");
db.db()
.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
}
if revision <= 8 {
info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1.");
db.db()
.run_command(
doc! {
"dropIndexes": "accounts",
"index": ["email", "email_normalised"]
},
None,
)
.await
.expect("Failed to delete legacy account indexes.");
let col = db.col::<Document>("sessions");
let mut cursor = db
.col::<Document>("accounts")
.find(doc! {}, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(account) = doc {
let id = account.get_str("_id").unwrap();
if let Some(sessions) = account.get("sessions") {
#[derive(Deserialize)]
struct Session {
id: String,
token: String,
friendly_name: String,
subscription: Option<Document>,
}
let sessions = from_bson::<Vec<Session>>(sessions.clone()).unwrap();
for session in sessions {
info!("Converting session {} to new format.", &session.id);
let mut doc = doc! {
"_id": session.id,
"token": session.token,
"user_id": id,
"name": session.friendly_name,
};
if let Some(sub) = session.subscription {
doc.insert("subscription", sub);
}
col.insert_one(doc, None).await.ok();
}
} else {
info!("Account doesn't have any sessions!");
}
}
}
db.col::<Document>("accounts")
.update_many(
doc! {},
doc! {
"$unset": {
"sessions": 1_i32,
},
"$set": {
"mfa": {
"recovery_codes": []
}
}
},
None,
)
.await
.unwrap();
}
if revision <= 9 {
info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id.");
let mut cursor = db
.col::<Document>("channels")
.find(doc! {}, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(channel) = doc {
let channel_id = channel.get_str("_id").unwrap();
if let Some(last_message) = channel.get("last_message") {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Obj {
#[serde(rename = "_id")]
id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum LastMessage {
Obj(Obj),
Id(String),
}
let lm = from_bson::<LastMessage>(last_message.clone()).unwrap();
let id = match lm {
LastMessage::Obj(Obj { id }) => id,
LastMessage::Id(id) => id,
};
info!("Converting session {} to new format.", &channel_id);
db.col::<Document>("channels")
.update_one(
doc! {
"_id": channel_id
},
doc! {
"$set": {
"last_message_id": id
},
"$unset": {
"last_message": 1_i32,
}
},
None,
)
.await
.unwrap();
} else {
info!("{} has no last_message.", &channel_id);
}
}
}
}
if revision <= 10 {
info!("Running migration [revision 10 / 2021-11-01]: Remove nonce values on channels and servers.");
db.col::<Document>("servers")
.update_many(
doc! {},
doc! {
"$unset": {
"nonce": 1_i32,
}
},
None,
)
.await
.unwrap();
db.col::<Document>("channels")
.update_many(
doc! {},
doc! {
"$unset": {
"nonce": 1_i32,
}
},
None,
)
.await
.unwrap();
}
if revision <= 11 {
info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32
},
"name": "channel"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
db.db()
.run_command(
doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create channel_unreads index.");
db.db()
.run_command(
doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create server_members index.");
}
if revision <= 12 {
info!("Running migration [revision 12 / 2021-11-21]: Add indexes to database.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
}
if revision <= 13 {
info!("Running migration [revision 13 / 22-02-2022]: Wipe legacy permission values.");
warn!("This is a destructive operation and will wipe existing permission data (excl. defaults for SendMessage).");
warn!("Taking a backup is advised.");
warn!("Continuing in 10 seconds...");
async_std::task::sleep(Duration::from_secs(10)).await;
let servers = db.col::<Document>("servers");
let mut cursor = servers.find(doc! {}, None).await.unwrap();
while let Some(Ok(mut document)) = cursor.next().await {
let id = document.get_str("_id").unwrap().to_string();
info!("Updating server {id}");
let mut update = doc! {};
// Try to pluck channel permission SendMessage (0x2)
// Structure of default_permissions used to be [server, channel]
let has_send = document
.get_array("default_permissions")
.map(|x| {
x.get(1)
.map(|x| x.as_i32().map(|x| (x as u32 & 0x2) == 0x2))
})
.ok()
.flatten()
.flatten()
.unwrap_or_default();
update.insert(
"default_permissions",
// Remove Send Message permission if it wasn't originally granted
(4000323584).bitxor(if has_send { 0 } else { (1 << 22) as u64 }) as i64,
);
if let Some(Bson::Document(mut roles)) = document.remove("roles") {
for role in roles.keys().cloned().collect::<Vec<String>>() {
if let Some(Bson::Document(role)) = roles.get_mut(role) {
role.insert(
"permissions",
doc! {
"a": 0_i64,
"d": 0_i64,
},
);
}
}
update.insert("roles", roles);
}
servers
.update_one(doc! { "_id": id }, doc! { "$set": update }, None)
.await
.unwrap();
}
let channels = db.col::<Document>("channels");
let mut cursor = channels.find(doc! {}, None).await.unwrap();
while let Some(Ok(document)) = cursor.next().await {
let id = document.get_str("_id").unwrap().to_string();
info!("Updating channel {id}");
let mut unset = doc! {
"permissions": 1_i32,
"role_permissions": 1_i32,
};
// Try to pluck channel permission SendMessage (0x2)
let has_send = document
.get_i32("default_permissions")
.map(|x| (x as u32 & 0x2) == 0x2)
.unwrap_or(true);
if has_send {
// Let parent permissions fall through.
unset.insert("default_permissions", 1_i32);
}
let mut update = doc! {
"$unset": unset
};
if !has_send {
// Block send message permission.
update.insert(
"$set",
doc! {
"default_permissions": {
"a": 0_i64,
"d": (1 << 22) as i64
}
},
);
}
channels
.update_one(doc! { "_id": id }, update, None)
.await
.unwrap();
}
}
if revision <= 14 {
info!("Running migration [revision 14 / 21-04-2022]: Split content into content and system fields.");
db.col::<Document>("messages")
.update_many(
doc! {
"content": {
"$type": "object"
}
},
doc! {
"$rename": {
"content": "system"
}
},
None,
)
.await
.unwrap();
}
if revision <= 15 {
info!("Running migration [revision 15 / 04-06-2022]: Migrate Authifier to latest version.");
let db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
db.run_migration(authifier::Migration::M2022_06_03EnsureUpToSpec)
.await
.unwrap();
}
if revision <= 16 {
info!("Running migration [revision 16 / 07-07-2022]: Add `emojis` collection and Authifier migration.");
let authifier_db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
authifier_db
.run_migration(authifier::Migration::M2022_06_09AddIndexForDeletion)
.await
.unwrap();
db.db()
.create_collection("emojis", None)
.await
.expect("Failed to create emojis collection.");
db.db()
.run_command(
doc! {
"createIndexes": "emojis",
"indexes": [
{
"key": {
"parent.id": 1_i32,
},
"name": "parent_id"
}
]
},
None,
)
.await
.expect("Failed to create emoji parent index.");
}
if revision <= 17 {
info!("Running migration [revision 17 / 15-07-2022]: Initialise `joined_at` property on server members.");
db.col::<Document>("server_members")
.update_many(
doc! {},
doc! {
"$set": {
"joined_at": DateTime::now().to_rfc3339_string()
}
},
None,
)
.await
.expect("Failed to update server members.");
}
if revision <= 18 {
info!("Running migration [revision 18 / 27-02-2022]: Create author index on messages. Drop plain channel index if exists.");
if db
.db()
.run_command(
doc! {
"dropIndexes": "messages",
"index": ["channel"]
},
None,
)
.await
.is_err()
{
info!("Failed to drop `messages.channel` index but this is ok since that means it's probably gone.");
}
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"author": 1_i32,
},
"name": "author"
}
]
},
None,
)
.await
.expect("Failed to create messages author index.");
}
if revision <= 19 {
info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable.");
db.db()
.create_collection("safety_reports", None)
.await
.unwrap();
db.db()
.create_collection("safety_snapshots", None)
.await
.unwrap();
db.col::<Document>("safety_reports")
.update_many(
doc! {},
doc! {
"$set": {
"status": "Created"
}
},
None,
)
.await
.unwrap();
}
if revision <= 20 {
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
db.db()
.run_command(
doc! {
"createIndexes": "safety_snapshots",
"indexes": [
{
"key": {
"report_id": 1_i32
},
"name": "report_id"
}
]
},
None,
)
.await
.expect("Failed to create safety snapshot index.");
}
if revision <= 21 {
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
db.db()
.create_collection("safety_strikes", None)
.await
.unwrap();
}
if revision <= 22 {
info!("Running migration [revision 22 / 31-05-2023]: Add moderator_id to account strikes.");
db.col::<Document>("safety_strikes")
.update_many(
doc! {},
doc! {
"$set": {
"moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4"
}
},
None,
)
.await
.expect("Failed to update server members.");
}
if revision <= 23 {
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
db.db()
.run_command(
doc! {
"dropIndexes": "users",
"index": "username"
},
None,
)
.await
.expect("Failed to drop existing username index.");
#[derive(Serialize, Deserialize)]
struct UserInformation {
#[serde(rename = "_id")]
id: String,
username: String,
}
let re_username = regex::Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap();
let users: Vec<UserInformation> = db
.col::<UserInformation>("users")
.find(doc! {}, None)
.await
.unwrap()
.map(|doc| doc.expect("id and username"))
.collect()
.await;
let search_space: Vec<String> = DISCRIMINATOR_SEARCH_SPACE.iter().cloned().collect();
let mut claimed: HashSet<String> = HashSet::new();
for i in 0..users.len() {
let info = &users[i];
let mut discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
if re_username.is_match(&info.username) {
while claimed.contains(&format!("{}#{}", info.username, discriminator)) {
let new_discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
info!(
"Re-rolled {} to {new_discriminator} from {discriminator}",
info.username
);
discriminator = new_discriminator;
}
claimed.insert(format!("{}#{}", info.username, discriminator));
info!(
"({}/{}) Migrating user \"{}\" to #{} - compliant",
i + 1,
users.len(),
info.username,
discriminator
);
db.col::<UserInformation>("users")
.update_one(
doc! {
"_id": &info.id
},
doc! {
"$set": {
"discriminator": discriminator
}
},
None,
)
.await
.unwrap();
} else {
let mut sanitised = info
.username
.graphemes(true)
.filter(|s| re_username.is_match(s))
.collect::<String>();
while sanitised.len() < 2 {
sanitised += "_";
}
while claimed.contains(&format!("{}#{}", sanitised, discriminator)) {
let new_discriminator = {
let mut rng = rand::thread_rng();
search_space.choose(&mut rng).unwrap()
};
info!("Re-rolled {sanitised} to {new_discriminator} from {discriminator}");
discriminator = new_discriminator;
}
claimed.insert(format!("{}#{}", sanitised, discriminator));
info!(
"({}/{}) Migrating user \"{}\" to #{} - sanitised: \"{}\"",
i + 1,
users.len(),
info.username,
discriminator,
sanitised
);
db.col::<UserInformation>("users")
.update_one(
doc! {
"_id": &info.id
},
doc! {
"$set": {
"username": sanitised,
"discriminator": discriminator,
"display_name": &info.username
}
},
None,
)
.await
.unwrap();
}
}
}
if revision <= 24 {
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
db.db()
.create_collection("channel_webhooks", None)
.await
.ok();
db.db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
},
None,
)
.await
.expect("Failed to create username index.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,158 @@
use revolt_result::Result;
use crate::events::client::EventV1;
use crate::{Database, File};
auto_derived_partial!(
/// Webhook
pub struct Webhook {
/// Webhook Id
#[serde(rename = "_id")]
pub id: String,
/// The name of the webhook
pub name: String,
/// The avatar of the webhook
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
/// The channel this webhook belongs to
pub channel_id: String,
/// The private token for the webhook
pub token: Option<String>,
},
"PartialWebhook"
);
auto_derived!(
/// Optional fields on webhook object
pub enum FieldsWebhook {
Avatar,
}
);
#[allow(clippy::disallowed_methods)]
impl Webhook {
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_webhook(self).await?;
// Avoid leaking the token to people who receive the event
let mut webhook = self.clone();
webhook.token = None;
EventV1::WebhookCreate(webhook.into())
.p(self.channel_id.clone())
.await;
Ok(())
}
pub fn assert_token(&self, token: &str) -> Result<()> {
if self.token.as_deref() == Some(token) {
Ok(())
} else {
Err(create_error!(InvalidCredentials))
}
}
pub async fn update(
&mut self,
db: &Database,
mut partial: PartialWebhook,
remove: Vec<FieldsWebhook>,
) -> Result<()> {
for field in &remove {
self.remove_field(field)
}
self.apply_options(partial.clone());
db.update_webhook(&self.id, &partial, &remove).await?;
partial.token = None; // Avoid leaking the token to people who receive the event
EventV1::WebhookUpdate {
id: self.id.clone(),
data: partial.into(),
remove: remove.into_iter().map(|v| v.into()).collect(),
}
.p(self.channel_id.clone())
.await;
Ok(())
}
pub fn remove_field(&mut self, field: &FieldsWebhook) {
match field {
FieldsWebhook::Avatar => self.avatar = None,
}
}
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_webhook(&self.id).await?;
EventV1::WebhookDelete {
id: self.id.clone(),
}
.p(self.channel_id.clone())
.await;
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{FieldsWebhook, PartialWebhook, Webhook};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let webhook_id = "webhook";
let channel_id = "channel";
let webhook = Webhook {
id: webhook_id.to_string(),
name: "Webhook Name".to_string(),
channel_id: channel_id.to_string(),
avatar: Some(Default::default()),
..Default::default()
};
db.insert_webhook(&webhook).await.unwrap();
let mut updated_webhook = webhook.clone();
updated_webhook
.update(
&db,
PartialWebhook {
name: Some("New Name".to_string()),
..Default::default()
},
vec![FieldsWebhook::Avatar],
)
.await
.unwrap();
let fetched_webhook = db.fetch_webhook(webhook_id).await.unwrap();
let fetched_webhooks = db.fetch_webhooks_for_channel(channel_id).await.unwrap();
assert_eq!(updated_webhook, fetched_webhook);
assert_ne!(webhook, fetched_webhook);
assert_eq!(1, fetched_webhooks.len());
assert_eq!(fetched_webhook, fetched_webhooks[0]);
webhook.delete(&db).await.unwrap();
assert!(db.fetch_webhook(webhook_id).await.is_err());
assert_eq!(
0,
db.fetch_webhooks_for_channel(channel_id)
.await
.unwrap()
.len()
)
});
}
}

View File

@@ -0,0 +1,29 @@
use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractWebhooks: Sync + Send {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>;
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook>;
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>>;
/// Update webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()>;
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()>;
}

View File

@@ -0,0 +1,77 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractWebhooks;
static COL: &str = "channel_webhooks";
#[async_trait]
impl AbstractWebhooks for MongoDb {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
query!(self, insert_one, COL, &webhook).map(|_| ())
}
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
query!(self, find_one_by_id, COL, webhook_id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
Ok(self
.col::<Webhook>(COL)
.find(
doc! {
"channel_id": channel_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
webhook_id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, webhook_id).map(|_| ())
}
}
impl IntoDocumentPath for FieldsWebhook {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsWebhook::Avatar => "avatar",
})
}
}

View File

@@ -0,0 +1,70 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
use super::AbstractWebhooks;
#[async_trait]
impl AbstractWebhooks for ReferenceDb {
/// Insert new webhook into the database
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if webhooks.contains_key(&webhook.id) {
Err(create_database_error!("insert", "webhook"))
} else {
webhooks.insert(webhook.id.to_string(), webhook.clone());
Ok(())
}
}
/// Fetch webhook by id
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
let webhooks = self.channel_webhooks.lock().await;
webhooks
.get(webhook_id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch webhooks for channel
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
let webhooks = self.channel_webhooks.lock().await;
Ok(webhooks
.values()
.filter(|webhook| webhook.channel_id == channel_id)
.cloned()
.collect())
}
/// Update webhook with new information
async fn update_webhook(
&self,
webhook_id: &str,
partial: &PartialWebhook,
remove: &[FieldsWebhook],
) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if let Some(webhook) = webhooks.get_mut(webhook_id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
webhook.remove_field(field);
}
webhook.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete webhook by id
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
let mut webhooks = self.channel_webhooks.lock().await;
if webhooks.remove(webhook_id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

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

View File

@@ -0,0 +1,529 @@
use std::collections::HashMap;
use revolt_permissions::OverrideField;
use revolt_result::Result;
use serde::{Deserialize, Serialize};
use crate::{events::client::EventV1, Database, File, IntoDocumentPath};
auto_derived!(
pub enum Channel {
/// Personal "Saved Notes" channel which allows users to save messages
SavedMessages {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the user this channel belongs to
user: String,
},
/// Direct message channel between two users
DirectMessage {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Whether this direct message channel is currently open on both sides
active: bool,
/// 2-tuple of user ids participating in direct message
recipients: Vec<String>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
},
/// Group channel between 1 or more participants
Group {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Display name of the channel
name: String,
/// User id of the owner of the group
owner: String,
/// Channel description
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Array of user ids participating in channel
recipients: Vec<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
/// Permissions assigned to members of this group
/// (does not apply to the owner of the group)
#[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<i64>,
/// Whether this group is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
/// Text channel belonging to a server
TextChannel {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Display name of the channel
name: String,
/// Channel description
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Id of the last message sent in this channel
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
/// Default permissions assigned to users in this channel
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
/// Voice channel belonging to a server
VoiceChannel {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the server this channel belongs to
server: String,
/// Display name of the channel
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
/// Channel description
description: Option<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Default permissions assigned to users in this channel
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
}
);
auto_derived!(
#[derive(Default)]
pub struct PartialChannel {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub permissions: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub role_permissions: Option<HashMap<String, OverrideField>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_permissions: Option<OverrideField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_message_id: Option<String>,
}
/// Optional fields on channel object
pub enum FieldsChannel {
Description,
Icon,
DefaultPermissions,
}
);
impl Channel {
/// Create a channel
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_channel(self).await?;
Ok(())
}
/// Add user to a group
pub async fn add_user_to_group(
&mut self,
db: &Database,
user_id: &str,
_by_id: &str,
) -> Result<()> {
if let Channel::Group { recipients, .. } = self {
if recipients.contains(&String::from(user_id)) {
return Err(create_error!(AlreadyInGroup));
}
recipients.push(String::from(user_id));
}
match &self {
Channel::Group { id, .. } => {
db.add_user_to_group(id, user_id).await?;
EventV1::ChannelGroupJoin {
id: id.to_string(),
user: user_id.to_string(),
}
.p(id.to_string())
.await;
EventV1::ChannelCreate(self.clone().into())
.private(user_id.to_string())
.await;
/* TODO: SystemMessage::UserAdded {
id: user.to_string(),
by: by.to_string(),
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok(); */
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Map out whether it is a direct DM
pub fn is_direct_dm(&self) -> bool {
matches!(self, Channel::DirectMessage { .. })
}
/// Check whether has a user as a recipient
pub fn contains_user(&self, user_id: &str) -> bool {
match self {
Channel::Group { recipients, .. } => recipients.contains(&String::from(user_id)),
_ => false,
}
}
/// Get list of recipients
pub fn users(&self) -> Result<Vec<String>> {
match self {
Channel::Group { recipients, .. } => Ok(recipients.to_owned()),
_ => Err(create_error!(NotFound)),
}
}
/// Get a reference to this channel's id
pub fn id(&self) -> String {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id.clone(),
}
}
/// Set role permission on a channel
pub async fn set_role_permission(
&mut self,
db: &Database,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
match self {
Channel::TextChannel {
id,
server,
role_permissions,
..
}
| Channel::VoiceChannel {
id,
server,
role_permissions,
..
} => {
db.set_channel_role_permission(id, role_id, permissions)
.await?;
role_permissions.insert(role_id.to_string(), permissions);
EventV1::ChannelUpdate {
id: id.clone(),
data: PartialChannel {
role_permissions: Some(role_permissions.clone()),
..Default::default()
}
.into(),
clear: vec![],
}
.p(server.clone())
.await;
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Update channel data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
db.update_channel(&self.id(), &partial, remove.clone())
.await?;
Ok(())
}
/// Remove a field from Channel object
pub fn remove_field(&mut self, field: &FieldsChannel) {
match field {
FieldsChannel::Description => match self {
Self::Group { description, .. }
| Self::TextChannel { description, .. }
| Self::VoiceChannel { description, .. } => {
description.take();
}
_ => {}
},
FieldsChannel::Icon => match self {
Self::Group { icon, .. }
| Self::TextChannel { icon, .. }
| Self::VoiceChannel { icon, .. } => {
icon.take();
}
_ => {}
},
FieldsChannel::DefaultPermissions => match self {
Self::TextChannel {
default_permissions,
..
}
| Self::VoiceChannel {
default_permissions,
..
} => {
default_permissions.take();
}
_ => {}
},
}
}
/// Remove multiple fields from Channel object
pub fn remove_fields(&mut self, partial: Vec<FieldsChannel>) {
for field in partial {
self.remove_field(&field)
}
}
/// Apply partial channel to channel
pub fn apply_options(&mut self, partial: PartialChannel) {
match self {
Self::SavedMessages { .. } => {}
Self::DirectMessage { active, .. } => {
if let Some(v) = partial.active {
*active = v;
}
}
Self::Group {
name,
owner,
description,
icon,
nsfw,
permissions,
..
} => {
if let Some(v) = partial.name {
*name = v;
}
if let Some(v) = partial.owner {
*owner = v;
}
if let Some(v) = partial.description {
description.replace(v);
}
if let Some(v) = partial.icon {
icon.replace(v);
}
if let Some(v) = partial.nsfw {
*nsfw = v;
}
if let Some(v) = partial.permissions {
permissions.replace(v);
}
}
Self::TextChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
..
}
| Self::VoiceChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
..
} => {
if let Some(v) = partial.name {
*name = v;
}
if let Some(v) = partial.description {
description.replace(v);
}
if let Some(v) = partial.icon {
icon.replace(v);
}
if let Some(v) = partial.nsfw {
*nsfw = v;
}
if let Some(v) = partial.role_permissions {
*role_permissions = v;
}
if let Some(v) = partial.default_permissions {
default_permissions.replace(v);
}
}
}
}
/// Remove user from a group
pub async fn remove_user_from_group(
&self,
db: &Database,
user_id: &str,
_by_id: Option<&str>,
silent: bool,
) -> Result<()> {
match &self {
Channel::Group {
id,
owner,
recipients,
..
} => {
if user_id == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != user_id) {
db.update_channel(
id,
&PartialChannel {
owner: Some(new_owner.into()),
..Default::default()
},
vec![],
)
.await?;
/* TODO: SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(),
to: new_owner.into(),
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok(); */
} else {
db.delete_channel(self).await?;
return Ok(());
}
}
EventV1::ChannelGroupLeave {
id: id.to_string(),
user: user_id.to_string(),
}
.p(id.to_string())
.await;
if !silent {
/* TODO: if let Some(_by) = by_id {
SystemMessage::UserRemove {
id: user_id.to_string(),
by: by.to_string(),
}
} else {
SystemMessage::UserLeft {
id: user_id.to_string(),
}
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok(); */
}
Ok(())
}
_ => Err(create_error!(InvalidOperation)),
}
}
/// Delete a channel
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_channel(self).await
}
}
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsChannel::Description => "description",
FieldsChannel::Icon => "icon",
FieldsChannel::DefaultPermissions => "default_permissions",
})
}
}

View File

@@ -0,0 +1,50 @@
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractChannels: Sync + Send {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()>;
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel>;
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>>;
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>>;
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel>;
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel>;
/// Insert a user to a group
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel_id: &str,
role_id: &str,
permissions: OverrideField,
) -> Result<()>;
// Update channel
async fn update_channel(
&self,
id: &str,
channel_id: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()>;
// Remove a user from a group
async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
// Delete a channel
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
}

View File

@@ -0,0 +1,193 @@
use super::AbstractChannels;
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
use bson::Document;
use futures::StreamExt;
use revolt_permissions::OverrideField;
use revolt_result::Result;
static COL: &str = "channels";
#[async_trait]
impl AbstractChannels for MongoDb {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
query!(self, insert_one, COL, &channel).map(|_| ())
}
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
query!(self, find_one_by_id, COL, channel_id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
Ok(self
.col::<Channel>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("fetch", "channels"))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
query!(
self,
find,
COL,
doc! {
"$or": [
{
"$or": [
{
"channel_type": "DirectMessage"
},
{
"channel_type": "Group"
}
],
"recipients": user_id
},
{
"channel_type": "SavedMessages",
"user": user_id
}
]
}
)
}
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
query!(
self,
find_one,
COL,
doc! {
"channel_type": "SavedMessages",
"user": user_id
}
)?
.ok_or_else(|| create_error!(InternalError))
}
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
let doc = match (user_a, user_b) {
self_user if self_user.0 == self_user.1 => {
doc! {
"channel_type": "SavedMessages",
"user": self_user.0
}
}
users => {
doc! {
"channel_type": "DirectMessage",
"recipients": {
"$all": [ users.0, users.1 ]
}
}
}
};
query!(self, find_one, COL, doc)?.ok_or_else(|| create_error!(NotFound))
}
/// Insert a user to a group
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$push": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channel"))
}
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel: &str,
role: &str,
permissions: OverrideField,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! { "_id": channel },
doc! {
"$set": {
"role_permissions.".to_owned() + role: permissions
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channel"))
}
// Update channel
async fn update_channel(
&self,
id: &str,
channel: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
channel,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
// Remove a user from a group
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": channel
},
doc! {
"$pull": {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "channels"))
}
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
}
}

View File

@@ -0,0 +1,157 @@
use std::collections::hash_map::Entry;
use super::AbstractChannels;
use crate::ReferenceDb;
use crate::{Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
use revolt_result::Result;
#[async_trait]
impl AbstractChannels for ReferenceDb {
/// Insert a new channel in the database
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
entry.insert(channel.clone());
Ok(())
} else {
Err(create_database_error!("insert", "channel"))
}
}
/// Fetch a channel from the database
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
channels
.get(channel_id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all channels from the database
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
let channels = self.channels.lock().await;
ids.iter()
.map(|id| {
channels
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
}
/// Fetch all direct messages for a user
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
let channels = self.channels.lock().await;
Ok(channels
.values()
.filter(|channel| channel.contains_user(user_id))
.cloned()
.collect())
}
// Fetch saved messages channel
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
channels
.get(user_id)
.cloned()
.ok_or_else(|| create_database_error!("fetch", "channel"))
}
// Fetch direct message channel (DM or Saved Messages)
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
let channels = self.channels.lock().await;
for (_, data) in channels.iter() {
if data.contains_user(user_a) && data.contains_user(user_b) {
return Ok(data.to_owned());
}
}
Err(create_error!(NotFound))
}
/// Insert a user to a group
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(channel_id) {
recipients.push(String::from(user_id));
Ok(())
} else {
Err(create_error!(InvalidOperation))
}
}
/// Insert channel role permissions
async fn set_channel_role_permission(
&self,
channel_id: &str,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(mut channel) = channels.get_mut(channel_id) {
match &mut channel {
Channel::TextChannel {
role_permissions, ..
}
| Channel::VoiceChannel {
role_permissions, ..
} => {
if role_permissions.get(role_id).is_some() {
role_permissions.remove(role_id);
role_permissions.insert(String::from(role_id), permissions);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
_ => Err(create_error!(NotFound)),
}
} else {
Err(create_error!(NotFound))
}
}
// Update channel
async fn update_channel(
&self,
id: &str,
channel: &PartialChannel,
remove: Vec<FieldsChannel>,
) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(channel_data) = channels.get_mut(id) {
channel_data.apply_options(channel.to_owned());
channel_data.remove_fields(remove);
Ok(())
} else {
Err(create_error!(NotFound))
}
}
// Remove a user from a group
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
let mut channels = self.channels.lock().await;
if let Some(channel_data) = channels.get_mut(channel) {
if channel_data.users()?.contains(&String::from(user)) {
channel_data.users()?.retain(|x| x != user);
return Ok(());
} else {
return Err(create_error!(NotFound));
}
}
Err(create_error!(NotFound))
}
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if channels.remove(&channel.id()).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
mod admin_migrations;
mod bots;
mod channel_webhooks;
mod channels;
mod files;
mod safety_strikes;
mod server_members;
mod servers;
mod user_settings;
mod users;
pub use admin_migrations::*;
pub use bots::*;
pub use channel_webhooks::*;
pub use channels::*;
pub use files::*;
pub use safety_strikes::*;
pub use server_members::*;
pub use servers::*;
pub use user_settings::*;
pub use users::*;
use crate::{Database, MongoDb, ReferenceDb};
pub trait AbstractDatabase:
Sync
+ Send
+ admin_migrations::AbstractMigrations
+ bots::AbstractBots
+ channels::AbstractChannels
+ channel_webhooks::AbstractWebhooks
+ files::AbstractAttachments
+ safety_strikes::AbstractAccountStrikes
+ server_members::AbstractServerMembers
+ servers::AbstractServers
+ user_settings::AbstractUserSettings
+ users::AbstractUsers
{
}
impl AbstractDatabase for ReferenceDb {}
impl AbstractDatabase for MongoDb {}
impl std::ops::Deref for Database {
type Target = dyn AbstractDatabase;
fn deref(&self) -> &Self::Target {
match &self {
Database::Reference(dummy) => dummy,
Database::MongoDb(mongo) => mongo,
}
}
}

View File

@@ -0,0 +1,218 @@
<mxfile host="65bd71144e">
<diagram id="9BKyaxnbqnTkbfLuLd1z" name="Page-1">
<mxGraphModel dx="2466" dy="650" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="56" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="6" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="57" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="5" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="260" y="370"/>
<mxPoint x="275" y="370"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="58" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="9" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="260" y="370"/>
<mxPoint x="342" y="370"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="59" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="300" y="250" as="targetPoint"/>
<Array as="points">
<mxPoint x="260" y="370"/>
<mxPoint x="300" y="370"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="2" value="Attachment" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="220" y="490" width="80" height="30" as="geometry"/>
</mxCell>
<mxCell id="51" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="3" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="290" y="160" as="targetPoint"/>
<Array as="points">
<mxPoint x="95" y="340"/>
<mxPoint x="195" y="340"/>
<mxPoint x="195" y="260"/>
<mxPoint x="260" y="260"/>
<mxPoint x="260" y="220"/>
<mxPoint x="290" y="220"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="68" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" edge="1" parent="1" source="3" target="5">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="95" y="340"/>
<mxPoint x="245" y="340"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="3" value="Channel Invite" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="50" y="400" width="90" height="30" as="geometry"/>
</mxCell>
<mxCell id="4" value="Channel Unread" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="-70" y="400" width="100" height="30" as="geometry"/>
</mxCell>
<mxCell id="43" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;dashed=1;" parent="1" source="5" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="60" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="5" target="9" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="67" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;startArrow=diamondThin;startFill=1;" edge="1" parent="1" source="5" target="66">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="245" y="370"/>
<mxPoint x="195" y="370"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="5" value="Channel" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="230" y="280" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="45" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="6" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="46" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="6" target="5" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="310" y="355"/>
<mxPoint x="260" y="355"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="6" value="Message" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="280" y="400" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="54" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="7" target="9" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="505" y="350"/>
<mxPoint x="355" y="350"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="7" value="Server Ban" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="470" y="400" width="70" height="30" as="geometry"/>
</mxCell>
<mxCell id="47" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="8" target="9" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="405" y="350"/>
<mxPoint x="355" y="350"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="48" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="8" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="405" y="350"/>
<mxPoint x="310" y="350"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="8" value="Server Member" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="360" y="400" width="90" height="30" as="geometry"/>
</mxCell>
<mxCell id="29" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;startArrow=diamondThin;startFill=1;" parent="1" source="10" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="240" y="145"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="10" value="Bot" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="200" y="130" width="40" height="30" as="geometry"/>
</mxCell>
<mxCell id="31" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="11" target="12" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="11" value="User Settings" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="150" y="210" width="90" height="30" as="geometry"/>
</mxCell>
<mxCell id="15" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;" parent="1" source="12" target="13" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="12" value="User" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="280" y="130" width="40" height="30" as="geometry"/>
</mxCell>
<mxCell id="13" value="Account" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="310" y="40" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="17" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;" parent="1" source="14" target="13" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="14" value="Session" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="350" y="130" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="44" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="310" y="160" as="targetPoint"/>
<Array as="points">
<mxPoint x="355" y="220"/>
<mxPoint x="310" y="220"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="9" value="Server" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="330" y="280" width="50" height="30" as="geometry"/>
</mxCell>
<mxCell id="55" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="4" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="290" y="160" as="targetPoint"/>
<mxPoint x="200" y="400" as="sourcePoint"/>
<Array as="points">
<mxPoint x="-20" y="340"/>
<mxPoint x="195" y="340"/>
<mxPoint x="195" y="260"/>
<mxPoint x="260" y="260"/>
<mxPoint x="260" y="220"/>
<mxPoint x="290" y="220"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="63" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#FFFFFF;entryX=1;entryY=1;entryDx=0;entryDy=0;" parent="1" source="61" target="12" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="584.25" y="335" as="targetPoint"/>
<Array as="points">
<mxPoint x="584" y="340"/>
<mxPoint x="390" y="340"/>
<mxPoint x="390" y="210"/>
<mxPoint x="320" y="210"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="64" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="61" target="9" edge="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="584" y="340"/>
<mxPoint x="368" y="340"/>
<mxPoint x="368" y="310"/>
</Array>
</mxGeometry>
</mxCell>
<mxCell id="61" value="Emoji" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
<mxGeometry x="558.5" y="400" width="51.5" height="30" as="geometry"/>
</mxCell>
<mxCell id="65" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="61" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="270" y="500" as="sourcePoint"/>
<mxPoint x="320" y="440" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="66" value="Webhook" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="160" y="400" width="70" height="30" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

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

View File

@@ -0,0 +1,124 @@
use revolt_result::Result;
use crate::Database;
auto_derived_partial!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[serde(rename = "_id")]
pub id: String,
/// Id of reported user
pub user_id: String,
/// Id of moderator
pub moderator_id: String,
/// Attached reason
pub reason: String,
},
"PartialAccountStrike"
);
#[allow(clippy::disallowed_methods)]
impl AccountStrike {
pub async fn create(
db: &Database,
user_id: String,
reason: String,
moderator_id: String,
) -> Result<AccountStrike> {
let strike = AccountStrike {
id: ulid::Ulid::new().to_string(),
user_id,
moderator_id,
reason,
};
db.insert_account_strike(&strike).await?;
Ok(strike)
}
/// Update this strike
pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> {
db.update_account_strike(&self.id, &partial).await?;
self.apply_options(partial);
Ok(())
}
/// Delete this strike
pub async fn delete(&self, db: &Database) -> Result<()> {
db.delete_account_strike(&self.id).await
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use crate::{AccountStrike, PartialAccountStrike};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let user_id = "user";
let strike = AccountStrike::create(
&db,
user_id.to_string(),
"reason 1".to_string(),
"moderator_id".to_string(),
)
.await
.unwrap();
let mut updated_strike = strike.clone();
updated_strike
.update(
&db,
PartialAccountStrike {
reason: Some("new reason".to_string()),
..Default::default()
},
)
.await
.unwrap();
let strike2 = AccountStrike::create(
&db,
user_id.to_string(),
"reason 2".to_string(),
"moderator_id".to_string(),
)
.await
.unwrap();
let strikes = db.fetch_account_strikes_by_user(user_id).await.unwrap();
let ids = strikes
.iter()
.cloned()
.map(|strike| strike.id)
.collect::<HashSet<String>>();
assert!(ids.contains(&strike.id));
assert!(ids.contains(&strike2.id));
let fetched_strike = strikes
.into_iter()
.find(|entry| entry.id == strike.id)
.unwrap();
assert_eq!(fetched_strike, updated_strike);
assert_ne!(fetched_strike, strike);
strike.delete(&db).await.unwrap();
assert_eq!(
1,
db.fetch_account_strikes_by_user(user_id)
.await
.unwrap()
.len()
)
});
}
}

View File

@@ -0,0 +1,24 @@
use revolt_result::Result;
use crate::{AccountStrike, PartialAccountStrike};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAccountStrikes: Sync + Send {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()>;
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike>;
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>>;
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()>;
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()>;
}

View File

@@ -0,0 +1,55 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::MongoDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
static COL: &str = "safety_strikes";
#[async_trait]
impl AbstractAccountStrikes for MongoDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
query!(self, insert_one, COL, &strike).map(|_| ())
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
Ok(self
.col::<AccountStrike>(COL)
.find(
doc! {
"user_id": user_id,
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}

View File

@@ -0,0 +1,60 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{AccountStrike, PartialAccountStrike};
use super::AbstractAccountStrikes;
#[async_trait]
impl AbstractAccountStrikes for ReferenceDb {
/// Insert new strike into the database
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.contains_key(&strike.id) {
Err(create_database_error!("insert", "strike"))
} else {
strikes.insert(strike.id.to_string(), strike.clone());
Ok(())
}
}
/// Fetch strike by id
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
let strikes = self.account_strikes.lock().await;
strikes
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch strikes by user id
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
let strikes = self.account_strikes.lock().await;
Ok(strikes
.values()
.filter(|strike| strike.user_id == user_id)
.cloned()
.collect())
}
/// Update strike with new information
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if let Some(strike) = strikes.get_mut(id) {
strike.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a strike from the database
async fn delete_account_strike(&self, id: &str) -> Result<()> {
let mut strikes = self.account_strikes.lock().await;
if strikes.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

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

View File

@@ -0,0 +1,118 @@
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::{Database, File, Server};
auto_derived_partial!(
/// Server Member
pub struct Member {
/// Unique member id
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Time at which this user joined the server
#[serde(skip_serializing_if = "Option::is_none")]
pub joined_at: Option<Timestamp>,
/// Member's nickname
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
/// Avatar attachment
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
/// Member's roles
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub roles: Vec<String>,
/// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Timestamp>,
},
"PartialMember"
);
auto_derived!(
/// Composite primary key consisting of server and user id
#[derive(Hash, Default)]
pub struct MemberCompositeKey {
/// Server Id
pub server: String,
/// User Id
pub user: String,
}
/// Optional fields on server member object
pub enum FieldsMember {
Nickname,
Avatar,
Roles,
Timeout,
}
/// Member removal intention
pub enum RemovalIntention {
Leave,
Kick,
Ban,
}
);
impl Member {
/// Update member data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialMember,
remove: Vec<FieldsMember>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
db.update_member(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::ServerMemberUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
}
.p(self.id.server.clone())
.await; */
Ok(())
}
pub fn remove_field(&mut self, field: &FieldsMember) {
match field {
FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None,
}
}
/// Get this user's current ranking
pub fn get_ranking(&self, server: &Server) -> i64 {
let mut value = i64::MAX;
for role in &self.roles {
if let Some(role) = server.roles.get(role) {
if role.rank < value {
value = role.rank;
}
}
}
value
}
/// Check whether this member is in timeout
pub fn in_timeout(&self) -> bool {
if let Some(timeout) = self.timeout {
*timeout > *Timestamp::now_utc()
} else {
false
}
}
}

View File

@@ -0,0 +1,41 @@
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()>;
/// Fetch a server member by their id
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>;
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
/// Fetch server count of a user
async fn fetch_server_count(&self, user_id: &str) -> Result<usize>;
/// Update information for a server member
async fn update_member(
&self,
id: &MemberCompositeKey,
partial: &PartialMember,
remove: Vec<FieldsMember>,
) -> Result<()>;
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
}

View File

@@ -0,0 +1,178 @@
use futures::StreamExt;
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractServerMembers;
static COL: &str = "server_members";
#[async_trait]
impl AbstractServerMembers for MongoDb {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()> {
query!(self, insert_one, COL, &member).map(|_| ())
}
/// Fetch a server member by their id
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member> {
query!(
self,
find_one,
COL,
doc! {
"_id.server": server_id,
"_id.user": user_id
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.server": server_id
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.user": user_id
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.server": server_id,
"_id.user": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(
doc! {
"_id.server": server_id
},
None,
)
.await
.map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL))
}
/// Fetch server count of a user
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(
doc! {
"_id.user": user_id
},
None,
)
.await
.map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL))
}
/// Update information for a server member
async fn update_member(
&self,
id: &MemberCompositeKey,
partial: &PartialMember,
remove: Vec<FieldsMember>,
) -> Result<()> {
query!(
self,
update_one,
COL,
doc! {
"_id.server": &id.server,
"_id.user": &id.user
},
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
query!(
self,
delete_one,
COL,
doc! {
"_id.server": &id.server,
"_id.user": &id.user
}
)
.map(|_| ())
}
}
impl IntoDocumentPath for FieldsMember {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsMember::Avatar => "avatar",
FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout",
})
}
}

View File

@@ -0,0 +1,117 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use super::AbstractServerMembers;
#[async_trait]
impl AbstractServerMembers for ReferenceDb {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()> {
let mut server_members = self.server_members.lock().await;
if server_members.contains_key(&member.id) {
Err(create_database_error!("insert", "member"))
} else {
server_members.insert(member.id.clone(), member.clone());
Ok(())
}
}
/// Fetch a server member by their id
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member> {
let server_members = self.server_members.lock().await;
server_members
.get(&MemberCompositeKey {
server: server_id.to_string(),
user: user_id.to_string(),
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
.filter(|member| member.id.server == server_id)
.cloned()
.collect())
}
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
.filter(|member| member.id.user == user_id)
.cloned()
.collect())
}
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
ids.iter()
.map(|id| {
server_members
.get(&MemberCompositeKey {
server: server_id.to_string(),
user: id.to_string(),
})
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
}
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
.filter(|member| member.id.server == server_id)
.count())
}
/// Fetch server count of a user
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
.filter(|member| member.id.user == user_id)
.count())
}
/// Update information for a server member
async fn update_member(
&self,
id: &MemberCompositeKey,
partial: &PartialMember,
remove: Vec<FieldsMember>,
) -> Result<()> {
let mut server_members = self.server_members.lock().await;
if let Some(member) = server_members.get_mut(id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
member.remove_field(&field);
}
member.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await;
if server_members.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

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

View File

@@ -0,0 +1,451 @@
use std::collections::{HashMap, HashSet};
use revolt_permissions::OverrideField;
use revolt_result::Result;
use ulid::Ulid;
use crate::{Database, File};
auto_derived_partial!(
/// Server
pub struct Server {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// User id of the owner
pub owner: String,
/// Name of the server
pub name: String,
/// Description for the server
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Channels within this server
// ! FIXME: this may be redundant
pub channels: Vec<String>,
/// Categories for this server
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<Category>>,
/// Configuration for sending system event messages
#[serde(skip_serializing_if = "Option::is_none")]
pub system_messages: Option<SystemMessageChannels>,
/// Roles for this server
#[serde(
default = "HashMap::<String, Role>::new",
skip_serializing_if = "HashMap::<String, Role>::is_empty"
)]
pub roles: HashMap<String, Role>,
/// Default set of server and channel permissions
pub default_permissions: i64,
/// Icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<File>,
/// Banner attachment
#[serde(skip_serializing_if = "Option::is_none")]
pub banner: Option<File>,
/// Bitfield of server flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
/// Whether this server is flagged as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
pub nsfw: bool,
/// Whether to enable analytics
#[serde(skip_serializing_if = "crate::if_false", default)]
pub analytics: bool,
/// Whether this server should be publicly discoverable
#[serde(skip_serializing_if = "crate::if_false", default)]
pub discoverable: bool,
},
"PartialServer"
);
auto_derived_partial!(
/// Role
pub struct Role {
/// Role name
pub name: String,
/// Permissions available to this role
pub permissions: OverrideField,
/// Colour used for this role
///
/// This can be any valid CSS colour
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
/// Whether this role should be shown separately on the member sidebar
#[serde(skip_serializing_if = "crate::if_false", default)]
pub hoist: bool,
/// Ranking of this role
#[serde(default)]
pub rank: i64,
},
"PartialRole"
);
auto_derived!(
/// Channel category
pub struct Category {
/// Unique ID for this category
pub id: String,
/// Title for this category
pub title: String,
/// Channels in this category
pub channels: Vec<String>,
}
/// System message channel assignments
pub struct SystemMessageChannels {
/// ID of channel to send user join messages in
#[serde(skip_serializing_if = "Option::is_none")]
pub user_joined: Option<String>,
/// ID of channel to send user left messages in
#[serde(skip_serializing_if = "Option::is_none")]
pub user_left: Option<String>,
/// ID of channel to send user kicked messages in
#[serde(skip_serializing_if = "Option::is_none")]
pub user_kicked: Option<String>,
/// ID of channel to send user banned messages in
#[serde(skip_serializing_if = "Option::is_none")]
pub user_banned: Option<String>,
}
/// Optional fields on server object
pub enum FieldsServer {
Description,
Categories,
SystemMessages,
Icon,
Banner,
}
/// Optional fields on server object
pub enum FieldsRole {
Colour,
}
);
impl Server {
/// Create a server
pub async fn create(&self, db: &Database) -> Result<()> {
db.insert_server(self).await
}
/// Update server data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
db.update_server(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::ServerUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
}
.p(self.id.clone())
.await; */
Ok(())
}
/// Delete a server
pub async fn delete(self, db: &Database) -> Result<()> {
/* // TODO: EventV1::ServerDelete {
id: self.id.clone(),
}
.p(self.id.clone())
.await; */
db.delete_server(&self.id).await
}
/// Remove a field from Server
pub fn remove_field(&mut self, field: &FieldsServer) {
match field {
FieldsServer::Description => self.description = None,
FieldsServer::Categories => self.categories = None,
FieldsServer::SystemMessages => self.system_messages = None,
FieldsServer::Icon => self.icon = None,
FieldsServer::Banner => self.banner = None,
}
}
/// Set role permission on a server
pub async fn set_role_permission(
&mut self,
db: &Database,
role_id: &str,
permissions: OverrideField,
) -> Result<()> {
if let Some(role) = self.roles.get_mut(role_id) {
role.update(
db,
&self.id,
role_id,
PartialRole {
permissions: Some(permissions),
..Default::default()
},
vec![],
)
.await?;
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/* /// Create a new member in a server
pub async fn create_member(
&self,
db: &Database,
user: User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
return Err(Error::Banned);
}
let member = Member {
id: MemberCompositeKey {
server: self.id.clone(),
user: user.id.clone(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
};
db.insert_member(&member).await?;
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
if should_fetch {
let perm = perms(&user).server(self).member(&member);
let existing_channels = db.fetch_channels(&self.channels).await?;
for channel in existing_channels {
if perm
.clone()
.channel(&channel)
.has_permission(db, Permission::ViewChannel)
.await?
{
channels.push(channel);
}
}
}
/* // TODO: EventV1::ServerMemberJoin {
id: self.id.clone(),
user: user.id.clone(),
}
.p(self.id.clone())
.await;
EventV1::ServerCreate {
id: self.id.clone(),
server: self.clone(),
channels: channels.clone(),
}
.private(user.id.clone())
.await; */
if let Some(id) = self
.system_messages
.as_ref()
.and_then(|x| x.user_joined.as_ref())
{
SystemMessage::UserJoined {
id: user.id.clone(),
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
Ok(channels)
}
/// Remove a member from a server
pub async fn remove_member(
&self,
db: &Database,
member: Member,
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&member.id).await?;
/* // TODO: EventV1::ServerMemberLeave {
id: self.id.to_string(),
user: member.id.user.clone(),
}
.p(member.id.server)
.await; */
if !silent {
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
RemovalIntention::Leave => x.user_left.as_ref(),
RemovalIntention::Kick => x.user_kicked.as_ref(),
RemovalIntention::Ban => x.user_banned.as_ref(),
}) {
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
}
Ok(())
}
/// Create ban
pub async fn ban_user(
self,
db: &Database,
id: MemberCompositeKey,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan { id, reason };
db.insert_ban(&ban).await?;
Ok(ban)
}
/// Ban a member from a server
pub async fn ban_member(
self,
db: &Database,
member: Member,
reason: Option<String>,
) -> Result<ServerBan> {
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
.await?;
self.ban_user(db, member.id, reason).await
} */
}
impl Role {
/// Into optional struct
pub fn into_optional(self) -> PartialRole {
PartialRole {
name: Some(self.name),
permissions: Some(self.permissions),
colour: self.colour,
hoist: Some(self.hoist),
rank: Some(self.rank),
}
}
/// Create a role
pub async fn create(&self, db: &Database, server_id: &str) -> Result<String> {
let role_id = Ulid::new().to_string();
db.insert_role(server_id, &role_id, self).await?;
/* // TODO: EventV1::ServerRoleUpdate {
id: server_id.to_string(),
role_id: role_id.to_string(),
data: self.clone().into_optional(),
clear: vec![],
}
.p(server_id.to_string())
.await; */
Ok(role_id)
}
/// Update server data
pub async fn update<'a>(
&mut self,
db: &Database,
server_id: &str,
role_id: &str,
partial: PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
db.update_role(server_id, role_id, &partial, remove.clone())
.await?;
/* // TODO: EventV1::ServerRoleUpdate {
id: server_id.to_string(),
role_id: role_id.to_string(),
data: partial,
clear: vec![],
}
.p(server_id.to_string())
.await; */
Ok(())
}
/// Remove field from Role
pub fn remove_field(&mut self, field: &FieldsRole) {
match field {
FieldsRole::Colour => self.colour = None,
}
}
/// Delete a role
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
/* // TODO: EventV1::ServerRoleDelete {
id: server_id.to_string(),
role_id: role_id.to_string(),
}
.p(server_id.to_string())
.await; */
db.delete_role(server_id, role_id).await
}
}
impl SystemMessageChannels {
pub fn into_channel_ids(self) -> HashSet<String> {
let mut ids = HashSet::new();
if let Some(id) = self.user_joined {
ids.insert(id);
}
if let Some(id) = self.user_left {
ids.insert(id);
}
if let Some(id) = self.user_kicked {
ids.insert(id);
}
if let Some(id) = self.user_banned {
ids.insert(id);
}
ids
}
}

View File

@@ -0,0 +1,46 @@
use revolt_result::Result;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractServers: Sync + Send {
/// Insert a new server into database
async fn insert_server(&self, server: &Server) -> Result<()>;
/// Fetch a server by its id
async fn fetch_server(&self, id: &str) -> Result<Server>;
/// Fetch a servers by their ids
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>>;
/// Update a server with new information
async fn update_server(
&self,
id: &str,
partial: &PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()>;
/// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()>;
/// Insert a new role into server object
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()>;
/// Update an existing role on a server
async fn update_role(
&self,
server_id: &str,
role_id: &str,
partial: &PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()>;
/// Delete a role from a server
///
/// Also updates channels and members.
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()>;
}

View File

@@ -0,0 +1,183 @@
use bson::{to_document, Document};
use futures::StreamExt;
use revolt_result::Result;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractServers;
static COL: &str = "servers";
#[async_trait]
impl AbstractServers for MongoDb {
/// Insert a new server into database
async fn insert_server(&self, server: &Server) -> Result<()> {
query!(self, insert_one, COL, &server).map(|_| ())
}
/// Fetch a server by its id
async fn fetch_server(&self, id: &str) -> Result<Server> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a servers by their ids
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
Ok(self
.col::<Server>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("find", "servers"))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Update a server with new information
async fn update_server(
&self,
id: &str,
partial: &PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
/// Insert a new role into server object
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": server_id
},
doc! {
"$set": {
"roles.".to_owned() + role_id: to_document(role)
.map_err(|_| create_database_error!("to_document", "role"))?
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "server"))
}
/// Update an existing role on a server
async fn update_role(
&self,
server_id: &str,
role_id: &str,
partial: &PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
server_id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
"roles.".to_owned() + role_id + "."
)
.map(|_| ())
}
/// Delete a role from a server
///
/// Also updates channels and members.
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
self.col::<Document>("server_members")
.update_many(
doc! {
"_id.server": server_id
},
doc! {
"$pull": {
"roles": &role_id
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "server_members"))?;
self.col::<Document>("channels")
.update_one(
doc! {
"server": server_id
},
doc! {
"$unset": {
"role_permissions.".to_owned() + role_id: 1_i32
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_one", "channels"))?;
self.col::<Document>("servers")
.update_one(
doc! {
"_id": server_id
},
doc! {
"$unset": {
"roles.".to_owned() + role_id: 1_i32
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "servers"))
}
}
impl IntoDocumentPath for FieldsServer {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsServer::Banner => "banner",
FieldsServer::Categories => "categories",
FieldsServer::Description => "description",
FieldsServer::Icon => "icon",
FieldsServer::SystemMessages => "system_messages",
})
}
}
impl IntoDocumentPath for FieldsRole {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsRole::Colour => "colour",
})
}
}

View File

@@ -0,0 +1,126 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
use super::AbstractServers;
#[async_trait]
impl AbstractServers for ReferenceDb {
/// Insert a new server into database
async fn insert_server(&self, server: &Server) -> Result<()> {
let mut servers = self.servers.lock().await;
if servers.contains_key(&server.id) {
Err(create_database_error!("insert", "server"))
} else {
servers.insert(server.id.to_string(), server.clone());
Ok(())
}
}
/// Fetch a server by its id
async fn fetch_server(&self, id: &str) -> Result<Server> {
let servers = self.servers.lock().await;
servers
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a servers by their ids
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
let servers = self.servers.lock().await;
ids.iter()
.map(|id| {
servers
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
}
/// Update a server with new information
async fn update_server(
&self,
id: &str,
partial: &PartialServer,
remove: Vec<FieldsServer>,
) -> Result<()> {
let mut servers = self.servers.lock().await;
if let Some(server) = servers.get_mut(id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
server.remove_field(&field);
}
server.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Delete a server by its id
async fn delete_server(&self, id: &str) -> Result<()> {
let mut servers = self.servers.lock().await;
if servers.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Insert a new role into server object
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
let mut servers = self.servers.lock().await;
if let Some(server) = servers.get_mut(server_id) {
server.roles.insert(role_id.to_string(), role.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Update an existing role on a server
async fn update_role(
&self,
server_id: &str,
role_id: &str,
partial: &PartialRole,
remove: Vec<FieldsRole>,
) -> Result<()> {
let mut servers = self.servers.lock().await;
if let Some(server) = servers.get_mut(server_id) {
if let Some(role) = server.roles.get_mut(role_id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
role.remove_field(&field);
}
role.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
/// Delete a role from a server
///
/// Also updates channels and members.
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
let mut servers = self.servers.lock().await;
if let Some(server) = servers.get_mut(server_id) {
if server.roles.remove(role_id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
}

View File

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

View File

@@ -0,0 +1,28 @@
use std::collections::HashMap;
use crate::Database;
use revolt_result::Result;
pub type UserSettings = HashMap<String, (i64, String)>;
#[async_trait]
pub trait UserSettingsImpl {
async fn set(self, db: &Database, user: &str) -> Result<()>;
}
#[async_trait]
impl UserSettingsImpl for UserSettings {
async fn set(self, db: &Database, user: &str) -> Result<()> {
db.set_user_settings(user, &self).await?;
/* // TODO: EventV1::UserSettingsUpdate {
id: user.to_string(),
update: self,
}
.private(user.to_string())
.await; */
Ok(())
}
}

View File

@@ -0,0 +1,18 @@
use revolt_result::Result;
use crate::UserSettings;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractUserSettings: Sync + Send {
/// Fetch a subset of user settings
async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result<UserSettings>;
/// Update a subset of user settings
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()>;
/// Delete all user settings
async fn delete_user_settings(&self, id: &str) -> Result<()>;
}

View File

@@ -0,0 +1,67 @@
use ::mongodb::options::FindOneOptions;
use bson::to_bson;
use bson::Document;
use mongodb::options::UpdateOptions;
use revolt_result::Result;
use crate::MongoDb;
use crate::UserSettings;
use super::AbstractUserSettings;
static COL: &str = "user_settings";
#[async_trait]
impl AbstractUserSettings for MongoDb {
/// Fetch a subset of user settings
async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result<UserSettings> {
let mut projection = doc! {
"_id": 0,
};
for key in filter {
projection.insert(key, 1);
}
Ok(query!(
self,
find_one_with_options,
COL,
doc! {
"_id": id
},
FindOneOptions::builder().projection(projection).build()
)?
.unwrap_or_default())
}
/// Update a subset of user settings
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()> {
let mut set = doc! {};
for (key, data) in settings {
set.insert(
key,
vec![to_bson(&data.0).unwrap(), to_bson(&data.1).unwrap()],
);
}
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$set": set
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "user_settings"))
}
/// Delete all user settings
async fn delete_user_settings(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}

View File

@@ -0,0 +1,46 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::UserSettings;
use super::AbstractUserSettings;
#[async_trait]
impl AbstractUserSettings for ReferenceDb {
/// Fetch a subset of user settings
async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result<UserSettings> {
let user_settings = self.user_settings.lock().await;
user_settings
.get(id)
.cloned()
.map(|settings| {
settings
.into_iter()
.filter(|(key, _)| filter.contains(key))
.collect()
})
.ok_or_else(|| create_error!(NotFound))
}
/// Update a subset of user settings
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()> {
let mut user_settings = self.user_settings.lock().await;
if let Some(settings) = user_settings.get_mut(id) {
settings.extend(settings.clone());
} else {
user_settings.insert(id.to_string(), settings.clone());
}
Ok(())
}
/// Delete all user settings
async fn delete_user_settings(&self, id: &str) -> Result<()> {
let mut user_settings = self.user_settings.lock().await;
if user_settings.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

@@ -0,0 +1,9 @@
mod model;
mod ops;
#[cfg(feature = "rocket-impl")]
mod rocket;
#[cfg(feature = "rocket-impl")]
pub use self::rocket::*;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,219 @@
use std::collections::HashSet;
use crate::{Database, File};
use once_cell::sync::Lazy;
use revolt_result::{Error, ErrorType, Result};
auto_derived_partial!(
/// # User
pub struct User {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Username
pub username: String,
/// Discriminator
pub discriminator: String,
/// Display name
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
/// Avatar attachment
pub avatar: Option<File>,
/// Relationships with other users
#[serde(skip_serializing_if = "Option::is_none")]
pub relations: Option<Vec<Relationship>>,
/// Bitfield of user badges
#[serde(skip_serializing_if = "Option::is_none")]
pub badges: Option<i32>,
/// User's current status
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<UserStatus>,
/// User's profile page
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<UserProfile>,
/// Enum of user flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
/// Whether this user is privileged
#[serde(skip_serializing_if = "crate::if_false", default)]
pub privileged: bool,
/// Bot information
#[serde(skip_serializing_if = "Option::is_none")]
pub bot: Option<BotInformation>,
},
"PartialUser"
);
auto_derived!(
/// User's relationship with another user (or themselves)
pub enum RelationshipStatus {
None,
User,
Friend,
Outgoing,
Incoming,
Blocked,
BlockedOther,
}
/// Relationship entry indicating current status with other user
pub struct Relationship {
#[serde(rename = "_id")]
pub id: String,
pub status: RelationshipStatus,
}
/// Presence status
pub enum Presence {
/// User is online
Online,
/// User is not currently available
Idle,
/// User is focusing / will only receive mentions
Focus,
/// User is busy / will not receive any notifications
Busy,
/// User appears to be offline
Invisible,
}
/// User's active status
pub struct UserStatus {
/// Custom status text
#[serde(skip_serializing_if = "String::is_empty", default)]
pub text: String,
/// Current presence option
#[serde(skip_serializing_if = "Option::is_none")]
pub presence: Option<Presence>,
}
/// User's profile
pub struct UserProfile {
/// Text content on user's profile
#[serde(skip_serializing_if = "String::is_empty", default)]
pub content: String,
/// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<File>,
}
/// Bot information for if the user is a bot
pub struct BotInformation {
/// Id of the owner of this bot
pub owner: String,
}
/// Optional fields on user object
pub enum FieldsUser {
Avatar,
StatusText,
StatusPresence,
ProfileContent,
ProfileBackground,
}
);
impl User {
/// Check whether a username is already in use by another user
#[allow(dead_code)]
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
match db.fetch_user_by_username(username).await {
Ok(_) => Ok(true),
Err(Error {
error_type: ErrorType::NotFound,
..
}) => Ok(false),
Err(error) => Err(error),
}
}
/// Update user data
pub async fn update<'a>(
&mut self,
db: &Database,
partial: PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
for field in &remove {
self.remove_field(field);
}
self.apply_options(partial.clone());
db.update_user(&self.id, &partial, remove.clone()).await?;
/* // TODO: EventV1::UserUpdate {
id: self.id.clone(),
data: partial,
clear: remove,
}
.p_user(self.id.clone(), db)
.await; */
Ok(())
}
/// Remove a field from User object
pub fn remove_field(&mut self, field: &FieldsUser) {
match field {
FieldsUser::Avatar => self.avatar = None,
FieldsUser::StatusText => {
if let Some(x) = self.status.as_mut() {
x.text = String::new();
}
}
FieldsUser::StatusPresence => {
if let Some(x) = self.status.as_mut() {
x.presence = None;
}
}
FieldsUser::ProfileContent => {
if let Some(x) = self.profile.as_mut() {
x.content = String::new();
}
}
FieldsUser::ProfileBackground => {
if let Some(x) = self.profile.as_mut() {
x.background = None;
}
}
}
}
/// Mark as deleted
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
self.update(
db,
PartialUser {
username: Some(format!("Deleted User {}", self.id)),
flags: Some(2),
..Default::default()
},
vec![
FieldsUser::Avatar,
FieldsUser::StatusText,
FieldsUser::StatusPresence,
FieldsUser::ProfileContent,
FieldsUser::ProfileBackground,
],
)
.await
}
}
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
let mut set = (2..9999)
.map(|v| format!("{:0>4}", v))
.collect::<HashSet<String>>();
for discrim in [
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
] {
set.remove(&format!("{:0>4}", discrim));
}
set.into_iter().collect()
});

View File

@@ -0,0 +1,57 @@
use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractUsers: Sync + Send {
/// Insert a new user into the database
async fn insert_user(&self, user: &User) -> Result<()>;
/// Fetch a user from the database
async fn fetch_user(&self, id: &str) -> Result<User>;
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User>;
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
/// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
/// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
/// Fetch ids of channels that both users are in
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
/// Fetch ids of servers that both users share
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
/// Update a user by their id given some data
async fn update_user(
&self,
id: &str,
user: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()>;
/// Set relationship with another user
///
/// This should use pull_relationship if relationship is None.
async fn set_relationship(
&self,
user_id: &str,
target_id: &str,
relationship: &RelationshipStatus,
) -> Result<()>;
/// Remove relationship with another user
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()>;
/// Delete a user by their id
async fn delete_user(&self, id: &str) -> Result<()>;
}

View File

@@ -0,0 +1,300 @@
use ::mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions};
use authifier::models::Session;
use futures::StreamExt;
use revolt_result::Result;
use crate::DocumentId;
use crate::IntoDocumentPath;
use crate::MongoDb;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
use super::AbstractUsers;
static COL: &str = "users";
#[async_trait]
impl AbstractUsers for MongoDb {
/// Insert a new user into the database
async fn insert_user(&self, user: &User) -> Result<()> {
query!(self, insert_one, COL, &user).map(|_| ())
}
/// Fetch a user from the database
async fn fetch_user(&self, id: &str) -> Result<User> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
query!(
self,
find_one_with_options,
COL,
doc! {
"username": username
},
FindOneOptions::builder()
.collation(
Collation::builder()
.locale("en")
.strength(CollationStrength::Secondary)
.build(),
)
.build()
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
let session = self
.col::<Session>("sessions")
.find_one(
doc! {
"token": token
},
None,
)
.await
.map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession))?;
self.fetch_user(&session.id).await
}
/// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
Ok(self
.col::<User>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
/// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>(COL)
.find(
doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { s.ok() })
.map(|user| user.id)
.collect()
.await)
}
/// Fetch ids of channels that both users are in
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>("channels")
.find(
doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ user_a, user_b ]
}
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async { s.ok() })
.map(|user| user.id)
.collect()
.await)
}
/// Fetch ids of servers that both users share
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>("server_members")
.aggregate(
vec![
doc! {
"$match": {
"_id.user": user_a
}
},
doc! {
"$lookup": {
"from": "server_members",
"as": "members",
"let": {
"server": "$_id.server"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{ "$eq": [ "$_id.user", user_b ] },
{ "$eq": [ "$_id.server", "$$server" ] }
]
}
}
}
]
}
},
doc! {
"$match": {
"members": {
"$size": 1_i32
}
}
},
doc! {
"$project": {
"_id": "$_id.server"
}
},
],
None,
)
.await
.map_err(|_| create_database_error!("aggregate", "server_members"))?
.filter_map(|s| async { s.ok() })
.filter_map(|doc| async move { doc.get_str("_id").map(|id| id.to_string()).ok() })
.collect()
.await)
}
/// Update a user by their id given some data
async fn update_user(
&self,
id: &str,
partial: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
query!(
self,
update_one_by_id,
COL,
id,
partial,
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ())
}
/// Set relationship with another user
///
/// This should use pull_relationship if relationship is None.
async fn set_relationship(
&self,
user_id: &str,
target_id: &str,
relationship: &RelationshipStatus,
) -> Result<()> {
if let RelationshipStatus::None = relationship {
return self.pull_relationship(user_id, target_id).await;
}
self.col::<User>(COL)
.update_one(
doc! {
"_id": user_id
},
vec![doc! {
"$set": {
"relations": {
"$concatArrays": [
{
"$ifNull": [
{
"$filter": {
"input": "$relations",
"cond": {
"$ne": [
"$$this._id",
target_id
]
}
}
},
[]
]
},
[
{
"_id": target_id,
"status": format!("{relationship:?}")
}
]
]
}
}
}],
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "user"))
}
/// Remove relationship with another user
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
self.col::<User>(COL)
.update_one(
doc! {
"_id": user_id
},
doc! {
"$pull": {
"relations": {
"_id": target_id
}
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Delete a user by their id
async fn delete_user(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
impl IntoDocumentPath for FieldsUser {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsUser::Avatar => "avatar",
FieldsUser::ProfileBackground => "profile.background",
FieldsUser::ProfileContent => "profile.content",
FieldsUser::StatusPresence => "status.presence",
FieldsUser::StatusText => "status.text",
})
}
}

View File

@@ -0,0 +1,121 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
use super::AbstractUsers;
#[async_trait]
impl AbstractUsers for ReferenceDb {
/// Insert a new user into the database
async fn insert_user(&self, user: &User) -> Result<()> {
let mut users = self.users.lock().await;
if users.contains_key(&user.id) {
Err(create_database_error!("insert", "user"))
} else {
users.insert(user.id.to_string(), user.clone());
Ok(())
}
}
/// Fetch a user from the database
async fn fetch_user(&self, id: &str) -> Result<User> {
let users = self.users.lock().await;
users
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their username
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
let users = self.users.lock().await;
let lowercase = username.to_lowercase();
users
.values()
.find(|user| user.username.to_lowercase() == lowercase)
.cloned()
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch a user from the database by their session token
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> {
todo!()
}
/// Fetch multiple users by their ids
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
let users = self.users.lock().await;
ids.iter()
.map(|id| {
users
.get(id)
.cloned()
.ok_or_else(|| create_error!(NotFound))
})
.collect()
}
/// Fetch ids of users that both users are friends with
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
todo!()
}
/// Fetch ids of channels that both users are in
async fn fetch_mutual_channel_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
todo!()
}
/// Fetch ids of servers that both users share
async fn fetch_mutual_server_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
todo!()
}
/// Update a user by their id given some data
async fn update_user(
&self,
id: &str,
partial: &PartialUser,
remove: Vec<FieldsUser>,
) -> Result<()> {
let mut users = self.users.lock().await;
if let Some(user) = users.get_mut(id) {
for field in remove {
#[allow(clippy::disallowed_methods)]
user.remove_field(&field);
}
user.apply_options(partial.clone());
Ok(())
} else {
Err(create_error!(NotFound))
}
}
/// Set relationship with another user
///
/// This should use pull_relationship if relationship is None.
async fn set_relationship(
&self,
_user_id: &str,
_target_id: &str,
_relationship: &RelationshipStatus,
) -> Result<()> {
todo!()
}
/// Remove relationship with another user
async fn pull_relationship(&self, _user_id: &str, _target_id: &str) -> Result<()> {
todo!()
}
/// Delete a user by their id
async fn delete_user(&self, id: &str) -> Result<()> {
let mut users = self.users.lock().await;
if users.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

@@ -0,0 +1,44 @@
use authifier::models::Session;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
use crate::{Database, User};
#[rocket::async_trait]
impl<'r> FromRequest<'r> for User {
type Error = authifier::Error;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let user: &Option<User> = request
.local_cache_async(async {
let db = request.rocket().state::<Database>().expect("`Database`");
let _header_bot_token = request
.headers()
.get("x-bot-token")
.next()
.map(|x| x.to_string());
/* if let Some(bot_token) = header_bot_token {
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await {
return Some(user);
}
} else */
if let Outcome::Success(session) = request.guard::<Session>().await {
// This uses a guard so can't really easily be refactored into from_token at this stage.
if let Ok(user) = db.fetch_user(&session.user_id).await {
return Some(user);
}
}
None
})
.await;
if let Some(user) = user {
Outcome::Success(user.clone())
} else {
Outcome::Failure((Status::Unauthorized, authifier::Error::InvalidSession))
}
}
}

View File

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

View File

@@ -0,0 +1,336 @@
use revolt_models::v0::*;
impl From<crate::AccountStrike> for AccountStrike {
fn from(value: crate::AccountStrike) -> Self {
AccountStrike {
id: value.id,
user_id: value.user_id,
reason: value.reason,
}
}
}
impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot {
#[cfg(debug_assertions)]
assert_eq!(self.id, user.id);
PublicBot {
id: self.id,
username: user.username,
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
description: user
.profile
.map(|profile| profile.content)
.unwrap_or_default(),
}
}
}
impl From<crate::Bot> for Bot {
fn from(value: crate::Bot) -> Self {
Bot {
id: value.id,
owner_id: value.owner,
token: value.token,
public: value.public,
analytics: value.analytics,
discoverable: value.discoverable,
interactions_url: value.interactions_url,
terms_of_service_url: value.terms_of_service_url,
privacy_policy_url: value.privacy_policy_url,
flags: value.flags.unwrap_or_default() as u32,
}
}
}
impl From<crate::Webhook> for Webhook {
fn from(value: crate::Webhook) -> Self {
Webhook {
id: value.id,
name: value.name,
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
}
}
}
impl From<crate::PartialWebhook> for PartialWebhook {
fn from(value: crate::PartialWebhook) -> Self {
PartialWebhook {
id: value.id,
name: value.name,
avatar: value.avatar.map(|file| file.into()),
channel_id: value.channel_id,
token: value.token,
}
}
}
impl From<FieldsWebhook> for crate::FieldsWebhook {
fn from(_value: FieldsWebhook) -> Self {
Self::Avatar
}
}
impl From<crate::FieldsWebhook> for FieldsWebhook {
fn from(_value: crate::FieldsWebhook) -> Self {
Self::Avatar
}
}
impl From<crate::Channel> for Channel {
fn from(value: crate::Channel) -> Self {
match value {
crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user },
crate::Channel::DirectMessage {
id,
active,
recipients,
last_message_id,
} => Channel::DirectMessage {
id,
active,
recipients,
last_message_id,
},
crate::Channel::Group {
id,
name,
owner,
description,
recipients,
icon,
last_message_id,
permissions,
nsfw,
} => Channel::Group {
id,
name,
owner,
description,
recipients,
icon: icon.map(|file| file.into()),
last_message_id,
permissions,
nsfw,
},
crate::Channel::TextChannel {
id,
server,
name,
description,
icon,
last_message_id,
default_permissions,
role_permissions,
nsfw,
} => Channel::TextChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
last_message_id,
default_permissions,
role_permissions,
nsfw,
},
crate::Channel::VoiceChannel {
id,
server,
name,
description,
icon,
default_permissions,
role_permissions,
nsfw,
} => Channel::VoiceChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
nsfw,
},
}
}
}
impl From<crate::PartialChannel> for PartialChannel {
fn from(value: crate::PartialChannel) -> Self {
PartialChannel {
name: value.name,
owner: value.owner,
description: value.description,
icon: value.icon.map(|file| file.into()),
nsfw: value.nsfw,
active: value.active,
permissions: value.permissions,
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
}
}
}
impl From<FieldsChannel> for crate::FieldsChannel {
fn from(value: FieldsChannel) -> Self {
match value {
FieldsChannel::Description => crate::FieldsChannel::Description,
FieldsChannel::Icon => crate::FieldsChannel::Icon,
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
}
}
}
impl From<crate::FieldsChannel> for FieldsChannel {
fn from(value: crate::FieldsChannel) -> Self {
match value {
crate::FieldsChannel::Description => FieldsChannel::Description,
crate::FieldsChannel::Icon => FieldsChannel::Icon,
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
}
}
}
impl From<crate::File> for File {
fn from(value: crate::File) -> Self {
File {
id: value.id,
tag: value.tag,
filename: value.filename,
metadata: value.metadata.into(),
content_type: value.content_type,
size: value.size,
deleted: value.deleted,
reported: value.reported,
message_id: value.message_id,
user_id: value.user_id,
server_id: value.server_id,
object_id: value.object_id,
}
}
}
impl From<crate::Metadata> for Metadata {
fn from(value: crate::Metadata) -> Self {
match value {
crate::Metadata::File => Metadata::File,
crate::Metadata::Text => Metadata::Text,
crate::Metadata::Image { width, height } => Metadata::Image {
width: width as usize,
height: height as usize,
},
crate::Metadata::Video { width, height } => Metadata::Video {
width: width as usize,
height: height as usize,
},
crate::Metadata::Audio => Metadata::Audio,
}
}
}
impl crate::User {
pub async fn into<P>(self, perspective: P) -> User
where
P: Into<Option<crate::User>>,
{
let relationship = if let Some(perspective) = perspective.into() {
perspective
.relations
.unwrap_or_default()
.into_iter()
.find(|relationship| relationship.id == self.id)
.map(|relationship| relationship.status.into())
.unwrap_or_default()
} else {
RelationshipStatus::None
};
// do permission stuff here
// TODO: implement permissions =)
let can_see_profile = false;
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
status: None,
profile: None,
flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
relationship,
online: can_see_profile && revolt_presence::is_online(&self.id).await,
id: self.id,
}
}
}
impl From<crate::RelationshipStatus> for RelationshipStatus {
fn from(value: crate::RelationshipStatus) -> Self {
match value {
crate::RelationshipStatus::None => RelationshipStatus::None,
crate::RelationshipStatus::User => RelationshipStatus::User,
crate::RelationshipStatus::Friend => RelationshipStatus::Friend,
crate::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
crate::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
crate::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
crate::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther,
}
}
}
impl From<crate::Relationship> for Relationship {
fn from(value: crate::Relationship) -> Self {
Self {
user_id: value.id,
status: value.status.into(),
}
}
}
impl From<crate::Presence> for Presence {
fn from(value: crate::Presence) -> Self {
match value {
crate::Presence::Online => Presence::Online,
crate::Presence::Idle => Presence::Idle,
crate::Presence::Focus => Presence::Focus,
crate::Presence::Busy => Presence::Busy,
crate::Presence::Invisible => Presence::Invisible,
}
}
}
impl From<crate::UserStatus> for UserStatus {
fn from(value: crate::UserStatus) -> Self {
UserStatus {
text: value.text,
presence: value.presence.map(|presence| presence.into()),
}
}
}
impl From<crate::UserProfile> for UserProfile {
fn from(value: crate::UserProfile) -> Self {
UserProfile {
content: value.content,
background: value.background.map(|file| file.into()),
}
}
}
impl From<crate::BotInformation> for BotInformation {
fn from(value: crate::BotInformation) -> Self {
BotInformation {
owner_id: value.owner,
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod bridge;
pub mod permissions;
pub mod reference;

View File

@@ -0,0 +1,206 @@
use std::borrow::Cow;
use revolt_permissions::{
calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus,
};
use crate::{Database, User};
/// Permissions calculator
pub struct PermissionCalculator<'a> {
#[allow(dead_code)]
database: &'a Database,
perspective: &'a User,
user: Option<Cow<'a, User>>,
// pub channel: Cow<'a, Channel>,
// pub server: Cow<'a, Server>,
// pub member: Cow<'a, Member>,
// flag_known_relationship: Option<&'a RelationshipStatus>,
cached_user_permission: Option<u32>,
cached_permission: Option<u64>,
}
#[async_trait]
impl PermissionQuery for PermissionCalculator<'_> {
// * For calculating user permission
/// Is our perspective user privileged?
async fn are_we_privileged(&mut self) -> bool {
self.perspective.privileged
}
/// Is our perspective user a bot?
async fn are_we_a_bot(&mut self) -> bool {
self.perspective.bot.is_some()
}
/// Is our perspective user and the currently selected user the same?
async fn are_the_users_same(&mut self) -> bool {
if let Some(other_user) = &self.user {
self.perspective.id == other_user.id
} else {
false
}
}
/// Get the relationship with have with the currently selected user
async fn user_relationship(&mut self) -> RelationshipStatus {
if let Some(other_user) = &self.user {
if let Some(relations) = &self.perspective.relations {
for entry in relations {
if entry.id == other_user.id {
return match entry.status {
crate::RelationshipStatus::None => RelationshipStatus::None,
crate::RelationshipStatus::User => RelationshipStatus::User,
crate::RelationshipStatus::Friend => RelationshipStatus::Friend,
crate::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
crate::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
crate::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
crate::RelationshipStatus::BlockedOther => {
RelationshipStatus::BlockedOther
}
};
}
}
}
}
RelationshipStatus::None
}
/// Whether the currently selected user is a bot
async fn user_is_bot(&mut self) -> bool {
if let Some(other_user) = &self.user {
other_user.bot.is_some()
} else {
false
}
}
/// Do we have a mutual connection with the currently selected user?
async fn have_mutual_connection(&mut self) -> bool {
// TODO: User::has_mutual_connection
false
}
// * For calculating server permission
/// Is our perspective user the server's owner?
async fn are_we_server_owner(&mut self) -> bool {
todo!()
}
/// Is our perspective user a member of the server?
async fn are_we_a_member(&mut self) -> bool {
todo!()
}
/// Get default server permission
async fn get_default_server_permissions(&mut self) -> u64 {
todo!()
}
/// Get the ordered role overrides (from lowest to highest) for this member in this server
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
todo!()
}
/// Is our perspective user timed out on this server?
async fn are_we_timed_out(&mut self) -> bool {
todo!()
}
// * For calculating channel permission
/// Get the type of the channel
async fn get_channel_type(&mut self) -> ChannelType {
todo!()
}
/// Get the default channel permissions
/// Group channel defaults should be mapped to an allow-only override
async fn get_default_channel_permissions(&mut self) -> Override {
todo!()
}
/// Get the ordered role overrides (from lowest to highest) for this member in this channel
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
todo!()
}
/// Do we own this group or saved messages channel if it is one of those?
async fn do_we_own_the_channel(&mut self) -> bool {
todo!()
}
/// Are we a recipient of this channel?
async fn are_we_part_of_the_channel(&mut self) -> bool {
todo!()
}
/// Set the current user as the recipient of this channel
/// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths)
async fn set_recipient_as_user(&mut self) {
todo!()
}
/// Set the current server as the server owning this channel
/// (this will only ever be called for server channels, use unimplemented!() for other code paths)
async fn set_server_from_channel(&mut self) {
todo!()
}
}
impl<'a> PermissionCalculator<'a> {
/// Create a new permission calculator
pub fn new(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> {
PermissionCalculator {
database,
perspective,
user: None,
cached_user_permission: None,
cached_permission: None,
}
}
/// Calculate the user permission value
pub async fn calc_user(mut self) -> PermissionCalculator<'a> {
if self.cached_user_permission.is_some() {
return self;
}
if self.user.is_none() {
panic!("Expected `PermissionCalculator.user to exist.");
}
PermissionCalculator {
cached_user_permission: Some(calculate_user_permissions(&mut self).await),
..self
}
}
/// Calculate the permission value
pub async fn calc(self) -> PermissionCalculator<'a> {
if self.cached_permission.is_some() {
return self;
}
self
}
/// Use user
pub fn user(self, user: Cow<'a, User>) -> PermissionCalculator {
PermissionCalculator {
user: Some(user),
..self
}
}
}
/// Short-hand for creating a permission calculator
pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> {
PermissionCalculator::new(database, perspective)
}

View File

@@ -0,0 +1,57 @@
use revolt_result::Result;
#[cfg(feature = "rocket-impl")]
use rocket::request::FromParam;
#[cfg(feature = "rocket-impl")]
use schemars::{
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
JsonSchema,
};
use crate::{Bot, Database, Webhook};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
pub struct Reference {
/// Id of object
pub id: String,
}
impl Reference {
/// Create a Ref from an unchecked string
pub fn from_unchecked(id: String) -> Reference {
Reference { id }
}
/// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await
}
/// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await
}
}
#[cfg(feature = "rocket-impl")]
impl<'r> FromParam<'r> for Reference {
type Error = &'r str;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
Ok(Reference::from_unchecked(param.into()))
}
}
#[cfg(feature = "rocket-impl")]
impl JsonSchema for Reference {
fn schema_name() -> String {
"Id".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
..Default::default()
})
}
}

View File

@@ -0,0 +1,31 @@
[package]
name = "revolt-models"
version = "0.6.1"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
description = "Revolt Backend: API Models"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
serde = [ "dep:serde", "revolt-permissions/serde" ]
schemas = [ "dep:schemars", "revolt-permissions/schemas" ]
validator = [ "dep:validator" ]
partials = [ "dep:revolt_optional_struct", "serde", "schemas" ]
default = [ "serde", "partials" ]
[dependencies]
# Core
revolt-permissions = { version = "0.6.1", path = "../permissions" }
# Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
# Spec Generation
schemars = { version = "0.8.8", optional = true }
# Validation
validator = { version = "0.16.0", optional = true, features = ["derive"] }

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