Compare commits

..
Author SHA1 Message Date
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
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
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 144f0d39c6 fix: Add limits to channels and roles 2023-04-10 17:39:21 +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
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
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
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
Paul Makles 0afbcc065f refactor: clippy warning for empty list 2023-01-27 16:18:00 +00:00
Paul Makles a406e5504d chore: add support for Streamable in media struct 2023-01-27 16:17:47 +00:00
Zomatree e8c233f60b feat: concurrently query January for embeds (#221) 2023-01-27 16:06:27 +00:00
insertish 72595f30d4 ci: synced local '.github/workflows/triage_pr.yml' with remote 'workflows/triage_pr.yml'
[skip ci]
2023-01-24 19:43:40 +00:00
insertish 39e345af74 ci: synced local '.github/workflows/triage_issue.yml' with remote 'workflows/triage_issue.yml'
[skip ci]
2023-01-24 19:43:40 +00:00
Paul Makles e0b918771d chore: ensure restrict_reactions is used in conjunction with reactions list 2023-01-20 18:06:44 +00:00
Paul Makles 7a6bd70dcd feat: add route to fetch user flags
chore: bump rauth to change disabled account behaviour
2023-01-20 17:47:26 +00:00
Paul Makles c0ef3d295a fix: fallback if vergen cannot generate data 2023-01-20 17:14:25 +00:00
Paul Makles f5c95f9d12 chore: bump rauth for lockout reset on pwd reset 2023-01-20 15:57:58 +00:00
Paul Makles fed1da72fd feat: support rauth events
feat: add platform user wipe event
2023-01-20 15:43:45 +00:00
Paul Makles cf9c0bc5e1 chore: bump rauth 2023-01-20 14:00:19 +00:00
Paul Makles cc2054838a chore: swap okapi for revolt_okapi 2023-01-20 14:00:19 +00:00
Paul MaklesandTheBobBobs 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
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
482 changed files with 29135 additions and 11807 deletions
+1
View File
@@ -1,3 +1,4 @@
Dockerfile
target target
.mongo .mongo
.env .env
+79
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
-2
View File
@@ -1,2 +0,0 @@
ko_fi: insertish
custom: https://insrt.uk/donate
+36
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
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 }}
-103
View File
@@ -1,103 +0,0 @@
name: Docker
on:
push:
branches:
- "master"
tags:
- "*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- ".vscode/**"
- ".gitignore"
- ".gitlab-ci.yml"
- "LICENSE"
- "README"
pull_request:
branches:
- "master"
paths:
- "Dockerfile"
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [linux/amd64]
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/${{ matrix.architecture }}
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
- name: Build
uses: docker/build-push-action@v2
with:
context: .
platforms: ${{ matrix.architecture }}
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
publish_amd64:
needs: [test]
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- name: Checkout
uses: actions/checkout@v2
with:
submodules: "recursive"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache amd64 Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache/linux/amd64
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: revoltchat/server, ghcr.io/revoltchat/server
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and publish
uses: docker/build-push-action@v2
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+56 -8
View File
@@ -1,10 +1,9 @@
name: Rust build and test name: Rust build, test, and generate specification
on: on:
push: push:
branches: [ master ]
pull_request: pull_request:
branches: [ master ] branches: [master]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -15,19 +14,68 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install latest nightly - name: Install latest stable
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: nightly toolchain: stable
override: true override: true
components: rustfmt, clippy components: rustfmt, clippy
- name: Run cargo build - name: Run cargo build
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: build command: build
- name: Run services in background
run: |
docker-compose -f docker-compose.db.yml up -d
- name: Run cargo test - name: Run cargo test
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test 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"
+16 -11
View File
@@ -15,22 +15,27 @@ jobs:
gh api graphql -f query=' gh api graphql -f query='
query { query {
organization(login: "revoltchat"){ organization(login: "revoltchat"){
projectNext(number: 3) { projectV2(number: 3) {
id id
fields(first:20) { fields(first:20) {
nodes { nodes {
id ... on ProjectV2SingleSelectField {
name id
settings name
options {
id
name
}
}
} }
} }
} }
} }
}' > project_data.json }' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .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.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.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 - name: Add issue to project
env: env:
@@ -39,11 +44,11 @@ jobs:
run: | run: |
item_id="$( gh api graphql -f query=' item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) { mutation($project:ID!, $issue:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $issue}) { addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
projectNextItem { item {
id id
} }
} }
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')" }' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV echo 'ITEM_ID='$item_id >> $GITHUB_ENV
+22 -15
View File
@@ -15,22 +15,27 @@ jobs:
gh api graphql -f query=' gh api graphql -f query='
query { query {
organization(login: "revoltchat"){ organization(login: "revoltchat"){
projectNext(number: 3) { projectV2(number: 3) {
id id
fields(first:20) { fields(first:20) {
nodes { nodes {
id ... on ProjectV2SingleSelectField {
name id
settings name
options {
id
name
}
}
} }
} }
} }
} }
}' > project_data.json }' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .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.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.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 - name: Add PR to project
env: env:
@@ -39,13 +44,13 @@ jobs:
run: | run: |
item_id="$( gh api graphql -f query=' item_id="$( gh api graphql -f query='
mutation($project:ID!, $pr:ID!) { mutation($project:ID!, $pr:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $pr}) { addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
projectNextItem { item {
id id
} }
} }
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')" }' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Set fields - name: Set fields
@@ -59,14 +64,16 @@ jobs:
$status_field: ID! $status_field: ID!
$status_value: String! $status_value: String!
) { ) {
set_status: updateProjectNextItemField(input: { set_status: updateProjectV2ItemFieldValue(input: {
projectId: $project projectId: $project
itemId: $item itemId: $item
fieldId: $status_field fieldId: $status_field
value: $status_value value: {
singleSelectOptionId: $status_value
}
}) { }) {
projectNextItem { projectV2Item {
id id
} }
} }
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent }' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
+4 -5
View File
@@ -1,7 +1,6 @@
Rocket.toml Rocket.toml
/target target
/target_backup .data
**/*.rs.bk
.mongo
.env .env
avatar.png
.vercel
-20
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
+2 -3
View File
@@ -1,5 +1,4 @@
{ {
"rust-analyzer.diagnostics.disabled": [ "editor.formatOnSave": true,
"unresolved-macro-call" "rust-analyzer.checkOnSave.command": "clippy"
]
} }
Generated
+1734 -1316
View File
File diff suppressed because it is too large Load Diff
+5 -64
View File
@@ -1,65 +1,6 @@
[package] [workspace]
name = "revolt" members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
# To help optimise CI and Docker builds.
# Version here is left as 0.0.0, please
# adjust and run ./set_version.sh instead.
version = "0.0.0"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [patch.crates-io]
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
[dependencies] redis = { git = "https://github.com/insertish/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
# Utility
lru = "0.7.0"
url = "2.2.2"
log = "0.4.11"
dotenv = "0.15.0"
linkify = "0.6.0"
once_cell = "1.4.1"
env_logger = "0.7.1"
lazy_static = "1.4.0"
ctrlc = { version = "3.0", features = ["termination"] }
# Lang. Utilities
regex = "1"
num_enum = "0.5.1"
impl_ops = "0.1.1"
bitfield = "0.13.2"
phf = { version = "0.9.0", features = ["macros"] }
# ID / key generation
ulid = "0.4.1"
nanoid = "0.4.0"
base64 = "0.13.0"
# serde
serde_json = "1.0.57"
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.11", features = ["derive"] }
rmp-serde = { git = "https://github.com/insertish/msgpack-rust", rev = "5bf2c24203ad422233cf35b7b7bfad9f7e811814" }
# async
futures = "0.3.8"
chrono = "0.4.15"
async-channel = "1.6.1"
reqwest = { version = "0.11.4", features = ["json"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
# internal util
web-push = "0.7.2"
many-to-many = "0.1.2"
lettre = "0.10.0-alpha.4"
rauth = { git = "https://github.com/insertish/rauth", rev = "157263ffcbd6cb1073e288b215db227634c4c29a" }
hive_pubsub = { git = "https://gitlab.insrt.uk/insert/hive", rev = "b0f3db9d33990530d7640d4bbb309c8d9eb5c0cf", features = ["redis-backend"] }
# redis
redis = { version = "0.21.2", features = ["async-std-comp"] }
mobc = { version = "0.7.3" }
mobc-redis = { version = "0.7.0", default-features = false, features = ["async-std-comp"] }
# web
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rocket = { version = "0.5.0-rc.1", default-features = false, features = ["json"] }
mongodb = { version = "1.2.2", features = ["tokio-runtime"], default-features = false }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }
+26 -19
View File
@@ -1,25 +1,32 @@
# Build Stage # Build Stage
FROM rustlang/rust:nightly-slim AS builder FROM --platform="${BUILDPLATFORM}" rust:slim
USER 0:0 USER 0:0
WORKDIR /home/rust/src WORKDIR /home/rust/src
RUN USER=root cargo new --bin revolt ARG TARGETARCH
WORKDIR /home/rust/src/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 Cargo.toml Cargo.lock ./
COPY src/bin/dummy.rs ./src/bin/dummy.rs COPY crates/bonfire/Cargo.toml ./crates/bonfire/
RUN apt-get update && apt-get install -y libssl-dev pkg-config && cargo build --release --bin dummy 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
COPY assets/templates ./assets/templates # Build all apps
COPY src ./src COPY crates ./crates
RUN cargo install --locked --path . RUN sh /tmp/build-image-layer.sh apps
# Bundle Stage
FROM debian:buster-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /usr/local/cargo/bin/revolt ./
COPY assets ./assets
EXPOSE 8000
EXPOSE 9000
ENV ROCKET_ADDRESS 0.0.0.0
ENV ROCKET_PORT 8000
CMD ["./revolt"]
+5 -5
View File
@@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007 Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed. of this license document, but changing it is not allowed.
@@ -629,8 +629,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least 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. the "copyright" line and a pointer to where the full notice is found.
Server Revolt Project
Copyright (C) 2021 REVOLT Copyright (C) 2022 Pawel Makles
This program is free software: you can redistribute it and/or modify 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 it under the terms of the GNU Affero General Public License as published
@@ -643,7 +643,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail. Also add information on how to contact you by electronic and paper mail.
@@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school, 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. 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 For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.
+12 -26
View File
@@ -1,21 +1,17 @@
# Delta # Revolt Backend
## Description This is a monorepo for the Revolt backend.
Delta is a blazing fast API server built with Rust for Revolt. | 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 |-->
**Features:** Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
- 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.
## Stack
- [Rocket](https://rocket.rs/) (REST)
- [Async Tungstenite](https://github.com/sdroege/async-tungstenite) (WebSockets)
- [MongoDB](https://mongodb.com/)
## Resources ## Resources
@@ -25,16 +21,6 @@ Delta is a blazing fast API server built with Rust for Revolt.
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers) - [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
- [Contribution Guide](https://developers.revolt.chat/contributing) - [Contribution Guide](https://developers.revolt.chat/contributing)
## CLI Commands
| Command | Description |
| ------------------ | ----------------------------------------------------------------------------------------- |
| `./publish.sh` | Publish a Docker Image. |
| `./set_version.sh` | Update the version. **Not intended for PR use.** |
| `cargo build` | Build/compile Delta. |
| `cargo run` | Run Delta. |
| `cargo fmt` | Format Delta. Not intended for PR use to avoid accidentally formatting unformatted files. |
## Contributing ## Contributing
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing). The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
@@ -42,4 +28,4 @@ Please note that a pull request should only take care of one issue so that we ca
## License ## License
Delta is licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/delta/blob/master/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
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.
-63
View File
@@ -1,63 +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 border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 20px 0 30px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
<tr>
<td align="center" bgcolor="#ff4654">
<img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
</td>
</tr>
<tr>
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #153643; font-family: Arial, sans-serif;">
<h1 style="font-size: 24px; margin: 0;">Reset your password!</h1>
</td>
</tr>
<tr>
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
<p>
You requested a password reset, if you didn't perform this action you can safely ignore this email.
</p>
<p style="margin: 0;">
Reset your password by navigating to <a href="{{url}}">{{url}}</a>.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ff4654" style="padding: 30px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
<p style="margin: 0;">Sent by Revolt. &middot; Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
<p>Revolt is a user-first chat platform built with modern web technologies.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
-14
View File
@@ -1,14 +0,0 @@
<h2>Reset your password.</h2>
<p>
You requested a password reset, if you did not perform this action you can safely ignore this email.
</p>
<p>
Reset your password here: <a href="{{url}}">{{url}}</a>
</p>
<br/>
<p>
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
</p>
<p>
Revolt is a user-first chat platform built with modern web technologies.
</p>
-63
View File
@@ -1,63 +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 border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 20px 0 30px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
<tr>
<td align="center" bgcolor="#ff4654">
<img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
</td>
</tr>
<tr>
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #153643; font-family: Arial, sans-serif;">
<h1 style="font-size: 24px; margin: 0;">You're almost there!</h1>
</td>
</tr>
<tr>
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
<p>
Verify your account to be able to log into the platform. If you didn't perform this action you can safely ignore this email.
</p>
<p style="margin: 0;">
Please verify your account by navigating to <a href="{{url}}">{{url}}</a>.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ff4654" style="padding: 30px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
<p style="margin: 0;">Sent by Revolt. &middot; Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
<p>Revolt is a user-first chat platform built with modern web technologies.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
-6
View File
@@ -1,6 +0,0 @@
You're almost there!
If you did not perform this action you can safely ignore this email.
Please verify your account here: {{url}}
Sent by Revolt.
-15
View File
@@ -1,15 +0,0 @@
<h2>You're almost there!</h2>
<p>
Verify your account to be able to log into the platform.<br/>
If you did not perform this action you can safely ignore this email.
</p>
<p>
Please verify your account here: <a href="{{url}}">{{url}}</a>
</p>
<br/>
<p>
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
</p>
<p>
Revolt is a user-first chat platform built with modern web technologies.
</p>
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
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
View File
@@ -0,0 +1 @@
/target
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "revolt-bonfire"
version = "0.5.21"
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
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
View File
@@ -0,0 +1 @@
../../LICENSE
+150
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))
}
}
}
+19
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`")
}
+34
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);
}
}
+252
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:?}");
});
}
+56
View File
@@ -0,0 +1,56 @@
[package]
name = "revolt-database"
version = "0.0.2"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
description = "Revolt Backend: Database Implementation"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
# Databases
mongodb = [ "dep:mongodb", "bson" ]
# ... Other
async-std-runtime = [ "async-std" ]
rocket-impl = [ "rocket", "schemars" ]
# Default Features
default = [ "mongodb", "async-std-runtime" ]
[dependencies]
# Core
revolt-result = { version = "0.0.2", path = "../result" }
revolt-permissions = { version = "0.0.2", path = "../permissions", features = [ "serde" ] }
# Utility
log = "0.4"
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"] }
# Database
bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "2.1.0", default-features = false }
# 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" }
+67
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))
}
})
}
}
+243
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()
}
@@ -0,0 +1,28 @@
use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings};
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 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, ()>>>,
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
}
);
+86
View File
@@ -0,0 +1,86 @@
#[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::*;
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -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()
});
}
}
@@ -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<(), ()>;
}
@@ -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(())
}
}
@@ -1,13 +1,12 @@
use super::super::get_db;
use super::scripts::LATEST_REVISION; use super::scripts::LATEST_REVISION;
use log::info; use crate::mongodb::bson::doc;
use mongodb::bson::doc; use crate::mongodb::options::CreateCollectionOptions;
use mongodb::options::CreateCollectionOptions; use crate::MongoDb;
pub async fn create_database() { pub async fn create_database(db: &MongoDb) {
info!("Creating database."); info!("Creating database.");
let db = get_db(); let db = db.db();
db.create_collection("accounts", None) db.create_collection("accounts", None)
.await .await
@@ -57,6 +56,18 @@ pub async fn create_database() {
.await .await
.expect("Failed to create user_settings collection."); .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) db.create_collection("bots", None)
.await .await
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
@@ -77,13 +88,13 @@ pub async fn create_database() {
"indexes": [ "indexes": [
{ {
"key": { "key": {
"username": 1 "username": 1_i32
}, },
"name": "username", "name": "username",
"unique": true, "unique": true,
"collation": { "collation": {
"locale": "en", "locale": "en",
"strength": 2 "strength": 2_i32
} }
} }
] ]
@@ -105,16 +116,16 @@ pub async fn create_database() {
}, },
{ {
"key": { "key": {
"channel": 1 "channel": 1_i32,
"_id": 1_i32
}, },
"name": "channel" "name": "channel_id_compound"
}, },
{ {
"key": { "key": {
"channel": 1, "author": 1_i32
"_id": 1
}, },
"name": "channel_id_compound" "name": "author"
} }
] ]
}, },
@@ -123,21 +134,20 @@ pub async fn create_database() {
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
get_db() db.run_command(
.run_command(
doc! { doc! {
"createIndexes": "channel_unreads", "createIndexes": "channel_unreads",
"indexes": [ "indexes": [
{ {
"key": { "key": {
"_id.channel": 1, "_id.channel": 1_i32,
"_id.user": 1, "_id.user": 1_i32,
}, },
"name": "compound_id" "name": "compound_id"
}, },
{ {
"key": { "key": {
"_id.user": 1, "_id.user": 1_i32,
}, },
"name": "user_id" "name": "user_id"
} }
@@ -148,21 +158,20 @@ pub async fn create_database() {
.await .await
.expect("Failed to create channel_unreads index."); .expect("Failed to create channel_unreads index.");
get_db() db.run_command(
.run_command(
doc! { doc! {
"createIndexes": "server_members", "createIndexes": "server_members",
"indexes": [ "indexes": [
{ {
"key": { "key": {
"_id.server": 1, "_id.server": 1_i32,
"_id.user": 1, "_id.user": 1_i32,
}, },
"name": "compound_id" "name": "compound_id"
}, },
{ {
"key": { "key": {
"_id.user": 1, "_id.user": 1_i32,
}, },
"name": "user_id" "name": "user_id"
} }
@@ -176,7 +185,7 @@ pub async fn create_database() {
db.collection("migrations") db.collection("migrations")
.insert_one( .insert_one(
doc! { doc! {
"_id": 0, "_id": 0_i32,
"revision": LATEST_REVISION "revision": LATEST_REVISION
}, },
None, None,
@@ -0,0 +1,775 @@
use std::{ops::BitXor, time::Duration};
use crate::{
mongodb::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
MongoDb,
};
use futures::StreamExt;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct MigrationInfo {
_id: i32,
revision: i32,
}
pub const LATEST_REVISION: i32 = 23;
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.");
}
// 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
}
@@ -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(())
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -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))
});
}
}
@@ -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<()>;
}
@@ -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,
}
}
}
@@ -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))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -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
}
}
@@ -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<()>;
}
@@ -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))
}
}
@@ -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(())
}
}
+47
View File
@@ -0,0 +1,47 @@
mod admin_migrations;
mod bots;
mod files;
mod safety_strikes;
mod server_members;
mod servers;
mod user_settings;
mod users;
pub use admin_migrations::*;
pub use bots::*;
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
+ 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,
}
}
}
@@ -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>
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -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(|strike| strike.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()
)
});
}
}
@@ -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<()>;
}
@@ -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(|_| ())
}
}
@@ -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))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -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
}
}
}
@@ -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<()>;
}
@@ -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",
})
}
}
@@ -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))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -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
}
}
@@ -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<()>;
}
@@ -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",
})
}
}
@@ -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))
}
}
}
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -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(())
}
}
@@ -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<()>;
}
@@ -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(|_| ())
}
}
@@ -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))
}
}
}
@@ -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::*;
@@ -0,0 +1,197 @@
use crate::{Database, File};
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,
#[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
}
}
@@ -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<()>;
}
@@ -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",
})
}
}
@@ -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))
}
}
}
@@ -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))
}
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod permissions;
pub mod reference;
@@ -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)
}
@@ -0,0 +1,52 @@
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};
/// 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
}
}
#[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()
})
}
}
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "revolt-models"
version = "0.0.2"
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" ]
schemas = [ "dep:schemars" ]
from_database = [ "revolt-database", "revolt-presence" ]
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
default = [ "serde", "from_database" ]
[dependencies]
# Repo
revolt-database = { version = "0.0.2", path = "../database", optional = true }
revolt-presence = { version = "0.0.2", path = "../presence", optional = true }
# Serialisation
serde = { version = "1", features = ["derive"], optional = true }
# Spec Generation
schemars = { version = "0.8.8", optional = true }
+30
View File
@@ -0,0 +1,30 @@
#[cfg(feature = "serde")]
#[macro_use]
extern crate serde;
#[cfg(feature = "schemas")]
#[macro_use]
extern crate schemars;
macro_rules! auto_derived {
( $( $item:item )+ ) => {
$(
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[derive(Debug, Clone, Eq, PartialEq)]
$item
)+
};
}
pub mod v0;
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}
/// Utility function to check if an u32 is zero
pub fn if_zero_u32(t: &u32) -> bool {
t == &0
}
@@ -0,0 +1,39 @@
auto_derived!(
/// Account Strike
pub struct AccountStrike {
/// Strike Id
#[serde(rename = "_id")]
pub id: String,
/// Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// New strike information
pub struct DataCreateStrike {
/// Id of reported user
pub user_id: String,
/// Attached reason
pub reason: String,
}
/// New strike information
pub struct DataEditAccountStrike {
/// New attached reason
pub reason: String,
}
);
#[cfg(feature = "from_database")]
impl From<revolt_database::AccountStrike> for AccountStrike {
fn from(value: revolt_database::AccountStrike) -> Self {
AccountStrike {
id: value.id,
user_id: value.user_id,
reason: value.reason,
}
}
}
+124
View File
@@ -0,0 +1,124 @@
use super::User;
auto_derived!(
/// Bot
pub struct Bot {
/// Bot Id
#[serde(rename = "_id")]
pub id: String,
/// User Id of the bot owner
#[serde(rename = "owner")]
pub owner_id: 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
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub analytics: bool,
/// Whether this bot should be publicly discoverable
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
pub discoverable: bool,
/// Reserved; URL for handling interactions
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub interactions_url: String,
/// URL for terms of service
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub terms_of_service_url: String,
/// URL for privacy policy
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "String::is_empty", default)
)]
pub privacy_policy_url: String,
/// Enum of bot flags
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_zero_u32", default)
)]
pub flags: u32,
}
/// Flags that may be attributed to a bot
#[repr(u32)]
pub enum BotFlags {
Verified = 1,
Official = 2,
}
/// Public Bot
pub struct PublicBot {
/// Bot Id
#[serde(rename = "_id")]
id: String,
/// Bot Username
username: String,
/// Profile Avatar
#[serde(skip_serializing_if = "String::is_empty")]
avatar: String,
/// Profile Description
#[serde(skip_serializing_if = "String::is_empty")]
description: String,
}
/// Bot Response
pub struct FetchBotResponse {
/// Bot object
pub bot: Bot,
/// User object
pub user: User,
}
);
#[cfg(feature = "from_database")]
impl PublicBot {
pub fn from(bot: revolt_database::Bot, user: revolt_database::User) -> Self {
#[cfg(debug_assertions)]
assert_eq!(bot.id, user.id);
PublicBot {
id: bot.id,
username: user.username,
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
description: user
.profile
.map(|profile| profile.content)
.unwrap_or_default(),
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::Bot> for Bot {
fn from(value: revolt_database::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,
}
}
}
+95
View File
@@ -0,0 +1,95 @@
auto_derived!(
/// 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>,
}
/// 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: usize, height: usize },
/// File is a video with specific dimensions
Video { width: usize, height: usize },
/// File is audio
Audio,
}
);
#[cfg(feature = "from_database")]
impl From<revolt_database::File> for File {
fn from(value: revolt_database::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,
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::Metadata> for Metadata {
fn from(value: revolt_database::Metadata) -> Self {
match value {
revolt_database::Metadata::File => Metadata::File,
revolt_database::Metadata::Text => Metadata::Text,
revolt_database::Metadata::Image { width, height } => Metadata::Image {
width: width as usize,
height: height as usize,
},
revolt_database::Metadata::Video { width, height } => Metadata::Video {
width: width as usize,
height: height as usize,
},
revolt_database::Metadata::Audio => Metadata::Audio,
}
}
}
+9
View File
@@ -0,0 +1,9 @@
mod account_strikes;
mod bots;
mod files;
mod users;
pub use account_strikes::*;
pub use bots::*;
pub use files::*;
pub use users::*;
+266
View File
@@ -0,0 +1,266 @@
use super::File;
auto_derived!(
/// User
pub struct User {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Username
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
/// Avatar attachment
pub avatar: Option<File>,
/// Relationships with other users
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub relations: Vec<Relationship>,
/// Bitfield of user badges
#[serde(skip_serializing_if = "crate::if_zero_u32", default)]
pub badges: u32,
/// 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 = "crate::if_zero_u32", default)]
pub flags: u32,
/// 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>,
/// Current session user's relationship with this user
pub relationship: RelationshipStatus,
/// Whether this user is currently online
pub online: bool,
}
/// User's relationship with another user (or themselves)
#[derive(Default)]
pub enum RelationshipStatus {
#[default]
None,
User,
Friend,
Outgoing,
Incoming,
Blocked,
BlockedOther,
}
/// Relationship entry indicating current status with other user
pub struct Relationship {
#[serde(rename = "_id")]
pub user_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")]
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")]
pub content: String,
/// Background visible on user's profile
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<File>,
}
/// User badge bitfield
#[repr(u32)]
pub enum UserBadges {
/// Revolt Developer
Developer = 1,
/// Helped translate Revolt
Translator = 2,
/// Monetarily supported Revolt
Supporter = 4,
/// Responsibly disclosed a security issue
ResponsibleDisclosure = 8,
/// Revolt Founder
Founder = 16,
/// Platform moderator
PlatformModeration = 32,
/// Active monetary supporter
ActiveSupporter = 64,
/// 🦊🦝
Paw = 128,
/// Joined as one of the first 1000 users in 2021
EarlyAdopter = 256,
/// Amogus
ReservedRelevantJokeBadge1 = 512,
/// Low resolution troll face
ReservedRelevantJokeBadge2 = 1024,
}
/// User flag enum
#[repr(u32)]
pub enum UserFlags {
/// User has been suspended from the platform
Suspended = 1,
/// User has deleted their account
Deleted = 2,
/// User was banned off the platform
Banned = 4,
/// User was marked as spam and removed from platform
Spam = 8,
}
/// Bot information for if the user is a bot
pub struct BotInformation {
/// Id of the owner of this bot
#[serde(rename = "owner")]
pub owner_id: String,
}
);
pub trait CheckRelationship {
fn with(&self, user: &str) -> RelationshipStatus;
}
impl CheckRelationship for Vec<Relationship> {
fn with(&self, user: &str) -> RelationshipStatus {
for entry in self {
if entry.user_id == user {
return entry.status.clone();
}
}
RelationshipStatus::None
}
}
#[cfg(feature = "from_database")]
impl User {
pub async fn from<P>(user: revolt_database::User, perspective: P) -> Self
where
P: Into<Option<revolt_database::User>>,
{
let relationship = if let Some(perspective) = perspective.into() {
perspective
.relations
.unwrap_or_default()
.into_iter()
.find(|relationship| relationship.id == user.id)
.map(|relationship| relationship.status.into())
.unwrap_or_default()
} else {
RelationshipStatus::None
};
// do permission stuff here
// TODO: implement permissions =)
let can_see_profile = false;
Self {
username: user.username,
avatar: user.avatar.map(|file| file.into()),
relations: vec![],
badges: user.badges.unwrap_or_default() as u32,
status: None,
profile: None,
flags: user.flags.unwrap_or_default() as u32,
privileged: user.privileged,
bot: user.bot.map(|bot| bot.into()),
relationship,
online: can_see_profile && revolt_presence::is_online(&user.id).await,
id: user.id,
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::RelationshipStatus> for RelationshipStatus {
fn from(value: revolt_database::RelationshipStatus) -> Self {
match value {
revolt_database::RelationshipStatus::None => RelationshipStatus::None,
revolt_database::RelationshipStatus::User => RelationshipStatus::User,
revolt_database::RelationshipStatus::Friend => RelationshipStatus::Friend,
revolt_database::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
revolt_database::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
revolt_database::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
revolt_database::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther,
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::Relationship> for Relationship {
fn from(value: revolt_database::Relationship) -> Self {
Self {
user_id: value.id,
status: value.status.into(),
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::Presence> for Presence {
fn from(value: revolt_database::Presence) -> Self {
match value {
revolt_database::Presence::Online => Presence::Online,
revolt_database::Presence::Idle => Presence::Idle,
revolt_database::Presence::Focus => Presence::Focus,
revolt_database::Presence::Busy => Presence::Busy,
revolt_database::Presence::Invisible => Presence::Invisible,
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::UserStatus> for UserStatus {
fn from(value: revolt_database::UserStatus) -> Self {
UserStatus {
text: value.text,
presence: value.presence.map(|presence| presence.into()),
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::UserProfile> for UserProfile {
fn from(value: revolt_database::UserProfile) -> Self {
UserProfile {
content: value.content,
background: value.background.map(|file| file.into()),
}
}
}
#[cfg(feature = "from_database")]
impl From<revolt_database::BotInformation> for BotInformation {
fn from(value: revolt_database::BotInformation) -> Self {
BotInformation {
owner_id: value.owner,
}
}
}
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "revolt-permissions"
version = "0.0.2"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = [ "Paul Makles <me@insrt.uk>" ]
description = "Revolt Backend: Permission Logic"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
serde = [ "dep:serde" ]
schemas = [ "dep:schemars" ]
try-from-primitive = [ "dep:num_enum" ]
[dev-dependencies]
# Async
async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Utility
auto_ops = "0.3.0"
once_cell = "1.17"
num_enum = { version = "0.6.1", optional = true }
# Async
async-trait = "0.1.51"
# Serialisation
serde = { version = "1", features = ["derive"], optional = true }
# Spec Generation
schemars = { version = "0.8.8", optional = true }
+128
View File
@@ -0,0 +1,128 @@
use crate::{
ChannelPermission, ChannelType, PermissionQuery, PermissionValue, RelationshipStatus,
UserPermission, ALLOW_IN_TIMEOUT, DEFAULT_PERMISSION_DIRECT_MESSAGE,
DEFAULT_PERMISSION_SAVED_MESSAGES, DEFAULT_PERMISSION_VIEW_ONLY,
};
/// Calculate permissions against a user
pub async fn calculate_user_permissions<P: PermissionQuery>(query: &mut P) -> u32 {
if query.are_we_privileged().await {
return u32::MAX;
}
if query.are_the_users_same().await {
return u32::MAX;
}
let mut permissions = 0_u32;
match query.user_relationship().await {
RelationshipStatus::Friend => return u32::MAX,
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
return UserPermission::Access as u32
}
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
permissions = UserPermission::Access as u32;
}
_ => {}
}
if query.have_mutual_connection().await {
permissions = UserPermission::Access + UserPermission::ViewProfile;
if query.user_is_bot().await || query.are_we_a_bot().await {
permissions += UserPermission::SendMessage as u32;
}
permissions
} else {
permissions
}
}
/// Calculate permissions against a server
pub async fn calculate_server_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
if query.are_we_privileged().await || query.are_we_server_owner().await {
return ChannelPermission::GrantAllSafe.into();
}
if !query.are_we_a_member().await {
return 0_u64.into();
}
let mut permissions: PermissionValue = query.get_default_server_permissions().await.into();
for role_override in query.get_our_server_role_overrides().await {
permissions.apply(role_override);
}
if query.are_we_timed_out().await {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
permissions
}
/// Calculate permissions against a channel
pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
if query.are_we_privileged().await {
return ChannelPermission::GrantAllSafe.into();
}
match query.get_channel_type().await {
ChannelType::SavedMessages => {
if query.do_we_own_the_channel().await {
DEFAULT_PERMISSION_SAVED_MESSAGES.into()
} else {
0_u64.into()
}
}
ChannelType::DirectMessage => {
if query.are_we_part_of_the_channel().await {
query.set_recipient_as_user().await;
let permissions = calculate_user_permissions(query).await;
if (permissions & UserPermission::SendMessage as u32)
== UserPermission::SendMessage as u32
{
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
} else {
(*DEFAULT_PERMISSION_VIEW_ONLY).into()
}
} else {
0_u64.into()
}
}
ChannelType::Group => {
if query.do_we_own_the_channel().await {
ChannelPermission::GrantAllSafe.into()
} else if query.are_we_part_of_the_channel().await {
(*DEFAULT_PERMISSION_VIEW_ONLY
| query.get_default_channel_permissions().await.allow)
.into()
} else {
0_u64.into()
}
}
ChannelType::ServerChannel => {
query.set_server_from_channel().await;
if query.are_we_a_member().await {
let mut permissions = calculate_server_permissions(query).await;
permissions.apply(query.get_default_channel_permissions().await);
for role_override in query.get_our_channel_role_overrides().await {
permissions.apply(role_override);
}
if query.are_we_timed_out().await {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
permissions
} else {
0_u64.into()
}
}
ChannelType::Unknown => 0_u64.into(),
}
}
+16
View File
@@ -0,0 +1,16 @@
#[macro_use]
extern crate auto_ops;
#[macro_use]
extern crate async_trait;
mod r#impl;
mod models;
mod r#trait;
pub use models::*;
pub use r#impl::*;
pub use r#trait::*;
#[cfg(test)]
mod test;
@@ -0,0 +1,137 @@
use once_cell::sync::Lazy;
use std::ops::Add;
/// Abstract channel type
pub enum ChannelType {
SavedMessages,
DirectMessage,
Group,
ServerChannel,
Unknown,
}
/// Permission value on Revolt
///
/// This should be restricted to the lower 52 bits to prevent any
/// potential issues with Javascript. Also leave empty spaces for
/// future permission flags to be added.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "try-from-primitive", derive(num_enum::TryFromPrimitive))]
#[repr(u64)]
pub enum ChannelPermission {
// * Generic permissions
/// Manage the channel or channels on the server
ManageChannel = 1 << 0,
/// Manage the server
ManageServer = 1 << 1,
/// Manage permissions on servers or channels
ManagePermissions = 1 << 2,
/// Manage roles on server
ManageRole = 1 << 3,
/// Manage server customisation (includes emoji)
ManageCustomisation = 1 << 4,
// % 1 bit reserved
// * Member permissions
/// Kick other members below their ranking
KickMembers = 1 << 6,
/// Ban other members below their ranking
BanMembers = 1 << 7,
/// Timeout other members below their ranking
TimeoutMembers = 1 << 8,
/// Assign roles to members below their ranking
AssignRoles = 1 << 9,
/// Change own nickname
ChangeNickname = 1 << 10,
/// Change or remove other's nicknames below their ranking
ManageNicknames = 1 << 11,
/// Change own avatar
ChangeAvatar = 1 << 12,
/// Remove other's avatars below their ranking
RemoveAvatars = 1 << 13,
// % 7 bits reserved
// * Channel permissions
/// View a channel
ViewChannel = 1 << 20,
/// Read a channel's past message history
ReadMessageHistory = 1 << 21,
/// Send a message in a channel
SendMessage = 1 << 22,
/// Delete messages in a channel
ManageMessages = 1 << 23,
/// Manage webhook entries on a channel
ManageWebhooks = 1 << 24,
/// Create invites to this channel
InviteOthers = 1 << 25,
/// Send embedded content in this channel
SendEmbeds = 1 << 26,
/// Send attachments and media in this channel
UploadFiles = 1 << 27,
/// Masquerade messages using custom nickname and avatar
Masquerade = 1 << 28,
/// React to messages with emojis
React = 1 << 29,
// * Voice permissions
/// Connect to a voice channel
Connect = 1 << 30,
/// Speak in a voice call
Speak = 1 << 31,
/// Share video in a voice call
Video = 1 << 32,
/// Mute other members with lower ranking in a voice call
MuteMembers = 1 << 33,
/// Deafen other members with lower ranking in a voice call
DeafenMembers = 1 << 34,
/// Move members between voice channels
MoveMembers = 1 << 35,
// * Misc. permissions
// % Bits 36 to 52: free area
// % Bits 53 to 64: do not use
// * Grant all permissions
/// Safely grant all permissions
GrantAllSafe = 0x000F_FFFF_FFFF_FFFF,
/// Grant all permissions
GrantAll = u64::MAX,
}
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u64 { *a as u64 | *b as u64 });
impl_op_ex_commutative!(+ |a: &u64, b: &ChannelPermission| -> u64 { *a | *b as u64 });
pub static ALLOW_IN_TIMEOUT: Lazy<u64> =
Lazy::new(|| ChannelPermission::ViewChannel + ChannelPermission::ReadMessageHistory);
pub static DEFAULT_PERMISSION_VIEW_ONLY: Lazy<u64> =
Lazy::new(|| ChannelPermission::ViewChannel + ChannelPermission::ReadMessageHistory);
pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| {
DEFAULT_PERMISSION_VIEW_ONLY.add(
ChannelPermission::SendMessage
+ ChannelPermission::InviteOthers
+ ChannelPermission::SendEmbeds
+ ChannelPermission::UploadFiles
+ ChannelPermission::Connect
+ ChannelPermission::Speak,
)
});
pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = ChannelPermission::GrantAllSafe as u64;
pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy<u64> = Lazy::new(|| {
DEFAULT_PERMISSION.add(ChannelPermission::ManageChannel + ChannelPermission::React)
});
pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| {
DEFAULT_PERMISSION.add(
ChannelPermission::React
+ ChannelPermission::ChangeNickname
+ ChannelPermission::ChangeAvatar,
)
});

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