Compare commits

...

153 Commits

Author SHA1 Message Date
Zomatree
db58818bab fix: only send UserMoveVoiceChannel to the user 2025-10-23 15:52:38 +01:00
Zomatree
b4ea75cf1a fix: dont set notification recipients when empty vec is passed 2025-10-23 15:52:16 +01:00
Zomatree
8d8e8f2cf1 fix: handle track_muted and track_unmuted 2025-10-23 15:51:39 +01:00
Zomatree
db19957de5 chore: add temp sentry logging system 2025-10-22 21:28:01 +01:00
Zomatree
20a4816235 fix: include channel ids in UserMoveVoiceChannel 2025-10-22 20:19:31 +01:00
Zomatree
6ba48a0197 fix: add Speak and Listen to default permissions 2025-10-22 18:19:02 +01:00
Zomatree
47002ec81c fix: dont break on user not still being in channel and force disconnecting 2025-10-20 00:29:56 +01:00
Zomatree
6ab264eb36 fix: return correct error if user is already in a voice channel 2025-10-19 23:25:35 +01:00
Zomatree
59af536e29 refactor: seperate out disconnect logic 2025-10-12 02:02:36 +01:00
Zomatree
5c7eec68d0 chore: add debug logging to voice ingress 2025-10-12 01:12:08 +01:00
Zomatree
94d7320e70 fix(ci): add voice-ingress to compose 2025-10-11 23:46:13 +01:00
Zomatree
af78ac0586 chore(ci): move to stoatchat 2025-10-11 23:22:41 +01:00
Zomatree
a42ddcbbfa Merge branch 'main' into feat/livekit 2025-10-11 23:09:07 +01:00
Zomatree
d65c1a1ab3 fix(ci): publish images under stoatchat and remove docker hub 2025-10-11 23:01:35 +01:00
Zomatree
530264f747 chore: add error logging to internal errors 2025-10-11 22:22:11 +01:00
Zomatree
ae1d4dd264 fix: dont presume channel has a node 2025-10-09 01:32:17 +01:00
izzy
4789dd4847 fix: include voice-ingress in build 2025-09-30 14:55:31 -05:00
izzy
f91d3bbfdd merge: branch 'main' into feat/livekit 2025-09-30 14:52:24 -05:00
izzy
4fb99e3bd0 ci: also include gifbox src 2025-09-23 12:12:33 -05:00
izzy
f0a83abcfa ci: add missing src copies for Docker build 2025-09-23 12:09:19 -05:00
izzy
6f1c715b8c chore: bump version to 0.8.9 2025-09-23 12:02:35 -05:00
Zomatree
5b50532d15 feat: track join time 2025-09-20 05:26:58 +01:00
Zomatree
938480ccec Signed-off-by: Angelo Kontaxis <me@zomatree.live> 2025-09-20 01:58:57 +01:00
Zomatree
38dd4d1079 fix: swap to using reqwest for query building 2025-09-19 02:50:00 +01:00
Zomatree
154204742d feat: add ratelimits to gifbox 2025-09-19 02:50:00 +01:00
Zomatree
db55998546 docs: document revolt-coalesced 2025-09-19 02:50:00 +01:00
Zomatree
5885e067a6 feat: trending and categories routes 2025-09-19 02:50:00 +01:00
Zomatree
a92152d86d fix: use our own result types instead of tenors types
add user auth
2025-09-19 02:50:00 +01:00
Zomatree
b5cd5e30ef feat: require auth for search 2025-09-19 02:50:00 +01:00
Zomatree
b0c977b324 feat: initial work on tenor gif searching 2025-09-19 02:50:00 +01:00
Zomatree
bfe4018e43 fix: apple music to use original url instead of metadata url
Apple Music returns the url without the query parameter in the opengraph meta tags, causing the regex to not find the track id, meaning any embed links generated would only link to the album.
2025-09-19 02:41:53 +01:00
Zomatree
cc7a7962a8 fix: use trust_cloudflare config value instead of env var 2025-09-18 21:22:31 +01:00
Zomatree
fb4011084d refactor: move ratelimits to a generic system for all web servers 2025-09-18 21:22:31 +01:00
Zomatree
3a3415915f fix: relax settings name regex 2025-09-17 15:41:54 +01:00
Zomatree
964884a5de chore: add policy changes 2025-09-17 15:41:54 +01:00
Zomatree
db57706794 feat: ready payload field customisation 2025-09-17 15:41:54 +01:00
Paul Makles
a186732ca5 merge: pull request #428 from ElfFlu/bugfix/16byte-attachment-gain 2025-09-12 12:48:28 +01:00
Zomatree
f422e8e917 Merge remote-tracking branch 'origin' into feat/livekit 2025-09-07 05:39:51 +01:00
Zomatree
1632e3dacf feat: allow users to specify who is notified when starting a call 2025-09-07 05:01:14 +01:00
Tom
e635bc23ec fix: don't remove timeouts when a member leaves a server (#409) 2025-09-06 18:41:23 -07:00
Zomatree
cc9a68b9c5 chore: cleanup 2025-09-07 02:27:50 +01:00
IAmTomahawkx
77a23239ea feat: Send push notifications for dm call start/end 2025-09-06 15:14:48 -07:00
Zomatree
3cb7da95e3 chore: add missing traits implementations to PermissionValue 2025-09-05 15:22:55 +01:00
Zomatree
dea0f675dd fix: implement Serialize to ClientMessage 2025-09-05 13:07:52 +01:00
Zomatree
480f210ce8 feat: include member payload in ServerMemberJoin event 2025-08-24 06:32:38 +01:00
Zomatree
caa8607468 feat: include groups and dms in fetch mutuals 2025-08-24 03:40:10 +01:00
ElfFlu
32e6600272 fix: remove authentication tag bytes from attachment download
Signed-off-by: ElfFlu <228066338+ElfFlu@users.noreply.github.com>
2025-08-23 19:05:32 +02:00
Zomatree
5335124306 fix: add license to revolt-parser 2025-08-20 18:55:52 +01:00
Zomatree
1561481eb4 feat: make message lexer use unowned string 2025-08-20 18:55:39 +01:00
Zomatree
d03e6be2eb fix: update migration 2025-08-19 18:56:17 +01:00
Zomatree
0a4d77ffd3 fix: remove VoiceChannel channel type
fix: calculate user overwrites correctly
fix: dont include personal info in livekit user metadata
fix: revoke video permissons on denied publish
fix: add video to default permissions
2025-08-19 17:37:09 +01:00
IAmTomahawkx
8f9015a6ff fix: apply uname fix to january and autumn 2025-08-18 16:59:08 -07:00
Zomatree
c6f121f585 Merge remote-tracking branch 'origin/main' into feat/livekit 2025-08-18 07:44:52 +01:00
Zomatree
96ddeb79d2 fix: dont set max participants in livekit 2025-08-18 07:28:09 +01:00
Zomatree
a36b138978 fix: move call system message to daemon,
check max participants when creating a token to avoid giving tokens but erroring when attempting to join,
check if the channel actually supports voice
2025-08-18 04:58:06 +01:00
Zomatree
ed22b3a5ce chore: switch Reference to using an actual reference 2025-08-12 03:44:29 +01:00
Zomatree
65fbd36624 fix: allow for disabling default features 2025-08-12 02:35:09 +01:00
Zomatree
860c47fbad feat: - track call length
- move voice and video limits to config
- seperate VoiceInformation into model and db model
- fix build scripts
2025-08-12 01:48:57 +01:00
izzy
050ba16d4a fix: correct shebang for try-tag-and-release 2025-08-07 12:58:52 +02:00
izzy
65bc6c8fc6 chore: add just tool to nix shell 2025-08-07 12:58:15 +02:00
izzy
6ad3da5f35 fix: permit empty remove array in edit requests 2025-08-07 12:57:33 +02:00
izzy
947eb15771 fix: newly created roles should be ranked the lowest 2025-08-07 12:29:42 +02:00
IAmTomahawkx
f4ee35fb09 fix: don't update the same field while trying to remove it
fixes #392
2025-08-06 14:18:40 -07:00
IAmTomahawkx
6048587d34 fix: rename openapi operation ids
fixes #406
2025-08-06 12:28:58 -07:00
Zomatree
a770d5822a chore: cleanup code 2025-08-04 03:20:08 +01:00
Zomatree
10c9682bba feat: ability to force disconnect existing voice connection 2025-08-04 03:19:53 +01:00
Builderb
80cf8fc4e8 Increase limit of embed icon url due to new cdn changes causing longer url (#426)
Signed-off-by: Builderb <builderbgamer@gmail.com>
2025-08-03 11:54:22 -07:00
higgs01
4f54227495 chore: use mc alias set instead of removed mc config (#423) 2025-07-18 09:28:31 +01:00
Zomatree
eb48975428 chore: bump livekit dep 2025-07-01 20:22:53 +01:00
Zomatree
28ed85a779 Merge branch 'main' into feat/livekit 2025-07-01 20:22:34 +01:00
izzy
aab1734615 chore: bump version to 0.8.8 2025-06-08 11:57:57 +01:00
Paul Makles
40a41ffd64 merge: pull request #418 from revoltchat/feat/role-ranks-v2 2025-06-08 11:46:00 +01:00
izzy
d30ceea373 test: complete test for editing role positions 2025-06-08 11:32:42 +01:00
izzy
3e8a401077 test: begin writing test for role editing 2025-06-08 11:18:47 +01:00
izzy
99f400bc7b fix: logic error in initial data check 2025-06-08 11:18:47 +01:00
izzy
73b576a75f fix: ensure server ID is fanned out with role ranks update
fix: ensure original order is correctly sorted on edit route
2025-06-08 11:18:47 +01:00
izzy
4e4e598daf refactor: minor formatting changes 2025-06-08 11:18:47 +01:00
Zomatree
77daf82b94 chore: refactor new role rankings code 2025-06-08 11:18:47 +01:00
Zomatree
e00603f276 fix: route ranking 2025-06-08 11:18:47 +01:00
Zomatree
1b2c7b2fa1 feat: add roles migration 2025-06-08 11:18:47 +01:00
Zomatree
c526095d4f feat: initial bulk role reorder route 2025-06-08 11:18:47 +01:00
Zomatree
95ca735b7b fix: get_voice_state has incorrect unique_key and allowed_sources should be lowercase 2025-06-08 02:33:35 +01:00
izzy
8cc4bbea4d refactor: clean up clippy warnings 2025-06-07 17:50:11 +01:00
izzy
911ffc767e merge: remote-tracking branch 'origin/feat/store-session-hello-new' 2025-06-07 17:31:15 +01:00
izzy
1690df998d fix: local tests with overrides were missing prerequisites 2025-06-07 17:30:16 +01:00
izzy
519d3c08a8 chore: correct dependency order in justfile 2025-06-07 16:51:09 +01:00
IAmTomahawkx
9846d8aac2 fix(tests): add policy change value to text fixtures 2025-06-06 21:06:18 -07:00
IAmTomahawkx
c74b6255dd don't use local authifier 2025-06-06 20:13:14 -07:00
IAmTomahawkx
df91b8c990 store last login time of session
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-06-06 20:10:37 -07:00
Zomatree
901ac56154 fix: remove node from channel voice state 2025-06-05 17:26:37 +01:00
Zomatree
3dbf3bb7a0 feat: private livekit nodes 2025-06-05 14:14:19 +01:00
Zomatree
45df5724b3 Merge remote-tracking branch 'origin/main' into livekit 2025-06-02 02:20:04 +01:00
Zomatree
ea1c165d80 Merge remote-tracking branch 'origin/main' into livekit 2025-06-02 02:11:51 +01:00
Zomatree
48e0b48ea9 fix: update branch from main 2025-05-01 16:58:35 +01:00
Zomatree
61038ddcf1 Merge remote-tracking branch 'origin/main' into livekit 2025-05-01 16:42:13 +01:00
Zomatree
b96f07e615 fix: update branch from main 2025-04-29 15:52:03 +01:00
Zomatree
d830fb0911 Merge branch 'main' into livekit 2025-04-29 15:47:16 +01:00
Zomatree
cc9ff9a632 fix: add back missing early adopter badge 2025-04-29 15:40:41 +01:00
Zomatree
02f18705f4 fix: dont leak invisible presence to others 2025-04-29 15:40:10 +01:00
Zomatree
a841df3b36 fix: show full branch name on github webhook messages 2025-04-29 15:40:10 +01:00
Tom
2a36f5912d feat: Add Mass Mentions to the backend (#394)
* feat: create base of push daemon

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

* Add outbound senders

* Make web_push send to rabbit instead (temp stuff)

* feat: stability and friend requests

* make vapid fr stuff not suck

* swap naming of queue

* move pushd into daemons folder

* fix cargo file for move into daemons folder

* feat: probably working fcm push notifs

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

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

* add pushd to docker

* mix: Remove old code, add stuff to pushd

* fix: lockfile

* feat: update rocket to 5.0.1

* fix: fix queues and ack bugs

* Move rabbit messsage processing into ack queue

* chore: update readme

* chore: optimizations for ack database hits

* pushd flowchart

* misc: update flowchart

* exit dependancy hell

* add rocket_impl flag to authifier

* make the tests file of delta actually compile

* fix: don't silence every push message

* fix: don't silence all messages

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

* validate mentions at a server membership level

* put back that import that was actually important

* minor fix to lockfile

* update delta authifier

* feat: proper permissions for push notifications

* add unit test for mention sanitization

* remove local file dependancy on authifier

* update ports to proper defaults

* fixTM the node bindings

* Theoretically configure docker releases for pushd

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

* declare exchange in pushd and delta

* fix createbuckets script

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

* fix: reference db implementation

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

* fix: remove finally redundant code

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

* fix: changes

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

* fix: other changes

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

* fix: make channel name return result

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

* Add role mention parsing

* feat: update to mongo 3.1, add member generator.

* integrate mass mentions into pushd

* patch redis-rs with updated versions

* feat: chunk role mentions

* move permission bits to 37/38 to avoid livekit conflict

* change role mention format to <%id>

* fix the lockfile from merge

* fix: PR change requests

* feat: add tests

* fix: i am a dumbass

* fix: tests, again

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-04-29 15:40:09 +01:00
izzy
a110a087be ci: downgrade lockfile 2025-04-29 15:39:12 +01:00
izzy
21b9164ee7 chore: bump version to 0.8.4
chore: enable webhooks in test builds
2025-04-29 15:39:12 +01:00
ShaksterNano
2f3d1f0c7e feat: add option to send message with missing replies
Signed-off-by: ShaksterNano <54268387+shaksternano@users.noreply.github.com>
2025-04-29 15:39:12 +01:00
Builderb
62204fe50b feat: allow bots to manage emojis (#407)
* Remove bot check on emoji create

Signed-off-by: Builderb <builderbgamer@gmail.com>

* Remove bot check on emoji delete

Signed-off-by: Builderb <builderbgamer@gmail.com>

---------

Signed-off-by: Builderb <builderbgamer@gmail.com>
2025-04-29 15:39:12 +01:00
Bradlee Barnes
773b890012 fix: remove SavedMessages and Group due to given permissions
Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>
2025-04-29 15:39:12 +01:00
Bradlee Barnes
9c3b2fb364 fix: allow message pinning and unpinning for DM-like channels
Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>
2025-04-29 15:39:11 +01:00
Nils Ponsard
5fe19d9815 feat: allow to set an icon when creating a group
Signed-off-by: Nils Ponsard <nilsponsard@gmail.com>
2025-04-29 15:39:11 +01:00
phazeschift
c1c5b0fd29 test: add delete server channel test
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-04-29 15:39:11 +01:00
phazeschift
6737100b4b test: add utilities for common setup
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-04-29 15:39:11 +01:00
phazeschift
6f0e80eef9 test: Add tests for invite/fetch
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-04-29 15:39:11 +01:00
IAmTomahawkx
bfc6f50ee5 chore: ignore python venv and dev script for user generation
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-04-29 15:39:11 +01:00
Paul Makles
7abbad700c fix: include production default value for config 2025-04-29 15:39:11 +01:00
Paul Makles
a671ba6308 fix: don't bump the lockfile version 2025-04-29 15:39:11 +01:00
Paul Makles
1d385ef2bc chore: bump version to 0.8.3 2025-04-29 15:39:11 +01:00
Paul Makles
6558e9f7c4 feat: add company information to email footers 2025-04-29 15:39:10 +01:00
Paul Makles
92f4b44cb3 chore: clarify cargo deny / tomls 2025-04-29 15:39:10 +01:00
Paul Makles
370442bf4e feat: add crond container definitions 2025-04-29 15:39:10 +01:00
Paul Makles
d54c5e80fa chore: fix version references 2025-04-29 15:39:10 +01:00
Paul Makles
a42746c55b feat: file deletion implementation 2025-04-29 15:39:10 +01:00
Paul Makles
9a98c53aff feat: init crond crate 2025-04-29 15:38:13 +01:00
Paul Makles
4ea08a189d fix: match new error type for status code 2025-04-29 15:37:42 +01:00
Paul Makles
ae15a0b4c7 fix: don't allow users to time themselves out
closes #376
2025-04-29 15:37:42 +01:00
Paul Makles
9e5fbec9b0 chore: disable LTO because it likely overloads GitHub worker 2025-04-29 15:36:56 +01:00
Paul Makles
0b02c84f04 chore: bump version to 0.8.2 2025-04-29 15:36:56 +01:00
Paul Makles
8bfe3ec6b0 fix: ensure ratelimiter adjusts for version prefix 2025-04-29 15:36:56 +01:00
Paul Makles
a7345a7b90 chore: mount API at /0.8/ as well 2025-04-29 15:36:56 +01:00
Paul Makles
12854d0f1b fix: change permission check for fetching channel webhooks 2025-04-29 15:36:56 +01:00
Paul Makles
9a2a7901e7 fix: ensure limit is always at least 1 when sent to database 2025-04-29 15:36:56 +01:00
Paul Makles
e8cb0bb88a docs: add some notes on overrides; fix defaults 2025-04-29 15:36:55 +01:00
Paul Makles
343b90b9af refactor: add error messages for Rabbit issues 2025-04-29 15:36:55 +01:00
Paul Makles
a4246f39e4 chore: add pushd to debug image script 2025-04-29 15:36:55 +01:00
Zomatree
38380db6e7 refactor: Remove VoiceChannel 2025-04-29 15:05:18 +01:00
Zomatree
091aa4705b fix: use existing node if someone is already in the voice channel 2025-03-25 20:37:06 +00:00
Zomatree
8b5e085232 feat: being moved between voice channels 2025-03-01 22:09:40 +00:00
Zomatree
9c2cc8736c fix: respond with the url when joining a voice channel 2025-02-17 02:11:32 +00:00
Zomatree
951128032b fix: expose list of nodes 2025-02-17 01:48:34 +00:00
Zomatree
367ac887f4 feat: support multiple voice nodes 2025-02-17 01:39:00 +00:00
Zomatree
060b4c43f4 feat: make Channel::server return a reference 2025-02-07 21:35:51 +00:00
Zomatree
2593a4272b chore: switch to ids for parameter 2025-02-02 13:46:45 +00:00
Zomatree
742eb3bb7d fix: remove local testing values from config 2025-01-28 16:06:58 +00:00
Zomatree
41a6de67cd fix: remove locked rocket version 2025-01-28 15:59:58 +00:00
Zomatree
c8ae34ba73 feat: implement missing permission syncs 2025-01-28 15:55:21 +00:00
Zomatree
c55b5bf75f fix: include voice states in servercreate event
feat: call started system message in dms
2025-01-27 20:46:29 +00:00
Zomatree
9de87a01ab Merge remote-tracking branch 'origin/main' into livekit 2025-01-26 20:36:40 +00:00
Zomatree
2f7294705e feat(livekit): push unfinished code 2024-10-13 20:23:00 +01:00
Zomatree
120ca449b8 feat(livekit): more permission handling 2024-07-23 04:07:34 +01:00
Zomatree
ffbc899792 split voice ops into its own library 2024-05-13 14:09:08 +01:00
Zomatree
675c7d6c0d fix redis key inconsistancy 2024-04-24 02:29:31 +01:00
Zomatree
67569f932e more stuff 2024-04-24 02:23:52 +01:00
Zomatree
7a4421089c track current state 2024-04-11 01:07:03 +01:00
Zomatree
ae87bc0b67 move voice states to global 2024-04-09 22:36:48 +01:00
Zomatree
ad0dcad497 initial livekit support
fix up code

undo changes to compose file

add back .env.example
2024-04-09 21:21:52 +01:00
248 changed files with 8393 additions and 2813 deletions

View File

@@ -49,13 +49,6 @@ jobs:
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:
@@ -72,14 +65,13 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest
# revoltchat/server
# stoatchat/api
- name: Docker meta
id: meta-delta
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/server
ghcr.io/revoltchat/server
ghcr.io/stoatchat/api
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -92,14 +84,13 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-delta.outputs.labels }}
# revoltchat/bonfire
# stoatchat/events
- name: Docker meta
id: meta-bonfire
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/bonfire
ghcr.io/revoltchat/bonfire
ghcr.io/stoatchat/events
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -112,14 +103,13 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-bonfire.outputs.labels }}
# revoltchat/autumn
# stoatchat/file-server
- name: Docker meta
id: meta-autumn
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/autumn
ghcr.io/revoltchat/autumn
ghcr.io/stoatchat/file-server
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -132,14 +122,13 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-autumn.outputs.labels }}
# revoltchat/january
# stoatchat/proxy
- name: Docker meta
id: meta-january
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/january
ghcr.io/revoltchat/january
ghcr.io/stoatchat/proxy
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -152,14 +141,32 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-january.outputs.labels }}
# revoltchat/crond
# stoatchat/gifbox
- name: Docker meta
id: meta-gifbox
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/gifbox
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/gifbox/Dockerfile
tags: ${{ steps.meta-gifbox.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-gifbox.outputs.labels }}
# stoatchat/crond
- name: Docker meta
id: meta-crond
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/crond
ghcr.io/revoltchat/crond
ghcr.io/stoatchat/crond
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -172,14 +179,13 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-crond.outputs.labels }}
# revoltchat/pushd
# stoatchat/pushd
- name: Docker meta
id: meta-pushd
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/pushd
ghcr.io/revoltchat/pushd
ghcr.io/stoatchat/pushd
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -191,3 +197,22 @@ jobs:
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-pushd.outputs.labels }}
# stoatchat/voice-ingress
- name: Docker meta
id: meta-voice-ingress
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/voice-ingress
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/daemons/voice-ingress/Dockerfile
tags: ${{ steps.meta-voice-ingress.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-voice-ingress.outputs.labels }}

View File

@@ -73,7 +73,7 @@ jobs:
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: actions/checkout@v3
with:
repository: revoltchat/api
repository: stoatchat/api
path: api
token: ${{ secrets.PAT }}

View File

@@ -14,7 +14,7 @@ jobs:
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
organization(login: "stoatchat"){
projectV2(number: 3) {
id
fields(first:20) {

View File

@@ -14,7 +14,7 @@ jobs:
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
organization(login: "stoatchat"){
projectV2(number: 5) {
id
fields(first:20) {

4
.gitignore vendored
View File

@@ -11,4 +11,6 @@ venv/
.vercel
.DS_Store
.idea
livekit.yml
.idea
start

3562
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,8 @@ members = [
[patch.crates-io]
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
#authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
#rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
# I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just

View File

@@ -27,10 +27,14 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps

View File

@@ -23,10 +23,14 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps

View File

@@ -21,9 +21,11 @@ The services and libraries that power the Revolt service.<br/>
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service | ![Crates.io Version](https://img.shields.io/crates/v/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/size/revolt-coalesced) ![Crates.io License](https://img.shields.io/crates/l/revolt-coalesced) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/gifbox` | [crates/services/gifbox](crates/services/gifbox) | Tenor proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
@@ -66,6 +68,7 @@ As a heads-up, the development environment uses the following ports:
| `crates/bonfire` | 14703 |
| `crates/services/autumn` | 14704 |
| `crates/services/january` | 14705 |
| `crates/services/gifbox` | 14706 |
Now you can clone and build the project:
@@ -114,7 +117,8 @@ If you'd like to change anything, create a `Revolt.overrides.toml` file and spec
> And corresponding Revolt configuration:
>
> ```toml
> # Revolt.overrides.toml
> # Revolt.overrides.toml
> # and Revolt.test-overrides.toml
> [database]
> mongodb = "mongodb://127.0.0.1:14017"
> redis = "redis://127.0.0.1:14079/"
@@ -140,6 +144,8 @@ cargo run --bin revolt-bonfire
cargo run --bin revolt-autumn
# run the proxy server
cargo run --bin revolt-january
# run the tenor proxy
cargo run --bin revolt-gifbox
# run the push daemon (not usually needed in regular development)
cargo run --bin revolt-pushd

View File

@@ -26,6 +26,11 @@ january = "http://local.revolt.chat:14705"
voso_legacy = ""
voso_legacy_ws = ""
# Public urls for livekit nodes
# each entry here should have a corresponding entry under `api.livekit.nodes`
[hosts.livekit]
worldwide = "ws://local.revolt.chat:14706"
[api]
[api.smtp]
@@ -40,6 +45,18 @@ port = 14025
use_tls = false
use_starttls = false
[api.livekit]
# Config for livekit nodes
# Make sure to change the secret when deploying
# The key and secret should match the values livekit is using
[api.livekit.nodes.worldwide]
url = "http://livekit"
lat = 0.0
lon = 0.0
key = "worldwide_key"
secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0"
[files.s3]
# S3 protocol endpoint
endpoint = "http://127.0.0.1:14009"

View File

@@ -34,13 +34,13 @@ services:
- minio
entrypoint: >
/bin/sh -c "while ! /usr/bin/mc ready minio; do
/usr/bin/mc config host add minio http://minio:9000 minioautumn minioautumn;
/usr/bin/mc alias set minio http://minio:9000 minioautumn minioautumn;
echo 'Waiting minio...' && sleep 1;
done; /usr/bin/mc mb minio/revolt-uploads; exit 0;"
# Rabbit
rabbit:
image: rabbitmq:3-management
image: rabbitmq:4-management
environment:
RABBITMQ_DEFAULT_USER: rabbituser
RABBITMQ_DEFAULT_PASS: rabbitpass

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.8.7"
version = "0.8.9"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -19,6 +19,7 @@ async-channel = "2.3.1"
# parsing
querystring = "1.1.0"
regex = "1.11.1"
# serde
bincode = "1.3.3"
@@ -41,7 +42,7 @@ revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.7", path = "../core/permissions" }
revolt-permissions = { version = "0.8.9", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis

View File

@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder
FROM ghcr.io/stoatchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage

View File

@@ -1,9 +1,15 @@
use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender;
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_database::events::client::ReadyPayloadFields;
use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize};
/// matches either a single word ie "users" or a key and value ie "settings[notifications]"
static READY_PAYLOAD_FIELD_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap());
/// Enumeration of supported protocol formats
#[derive(Debug)]
pub enum ProtocolFormat {
@@ -17,6 +23,7 @@ pub struct ProtocolConfiguration {
protocol_version: i32,
format: ProtocolFormat,
session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
}
impl ProtocolConfiguration {
@@ -25,11 +32,13 @@ impl ProtocolConfiguration {
protocol_version: i32,
format: ProtocolFormat,
session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
) -> Self {
Self {
protocol_version,
format,
session_token,
ready_payload_fields,
}
}
@@ -86,14 +95,8 @@ impl ProtocolConfiguration {
}
/// Get ready payload fields
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
vec![
ReadyPayloadFields::Users,
ReadyPayloadFields::Servers,
ReadyPayloadFields::Channels,
ReadyPayloadFields::Members,
ReadyPayloadFields::Emoji,
]
pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields {
&self.ready_payload_fields
}
}
@@ -124,6 +127,23 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
let mut protocol_version = 1;
let mut format = ProtocolFormat::Json;
let mut session_token = None;
let mut ready_payload_fields = if params.iter().any(|(k, _)| *k == "ready") {
// If they pass the ready field, set all fields to false
ReadyPayloadFields {
users: false,
servers: false,
channels: false,
members: false,
emojis: false,
voice_states: false,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: false,
}
} else {
ReadyPayloadFields::default()
};
// Parse and map parameters from key-value to known variables.
for (key, value) in params {
@@ -139,6 +159,31 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
_ => {}
},
"token" => session_token = Some(value.into()),
"ready" => {
// Re-enable all the fields the client specifies
if let Some(captures) = READY_PAYLOAD_FIELD_REGEX.captures(value) {
if let Some(field) = captures.get(0) {
match field.as_str() {
"users" => ready_payload_fields.users = true,
"servers" => ready_payload_fields.servers = true,
"channels" => ready_payload_fields.channels = true,
"members" => ready_payload_fields.members = true,
"emojis" => ready_payload_fields.emojis = true,
"voice_states" => ready_payload_fields.voice_states = true,
"channel_unreads" => ready_payload_fields.channel_unreads = true,
"user_settings" => {
if let Some(subkey) = captures.get(1) {
ready_payload_fields
.user_settings
.push(subkey.as_str().to_string());
}
}
"policy_changes" => ready_payload_fields.policy_changes = true,
_ => {}
}
}
}
}
_ => {}
}
}
@@ -151,6 +196,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
protocol_version,
format,
session_token,
ready_payload_fields,
})
.is_ok()
{

View File

@@ -4,6 +4,7 @@ use futures::future::join_all;
use revolt_database::{
events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery,
voice::get_channel_voice_state,
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
};
use revolt_models::v0;
@@ -17,8 +18,9 @@ use super::state::{Cache, State};
impl Cache {
/// Check whether the current user can view a channel
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
#[allow(deprecated)]
match &channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Channel::TextChannel { server, .. } => {
let member = self.members.get(server);
let server = self.servers.get(server);
let mut query =
@@ -95,21 +97,23 @@ impl State {
pub async fn generate_ready_payload(
&mut self,
db: &Database,
fields: Vec<ReadyPayloadFields>,
fields: &ReadyPayloadFields,
) -> Result<EventV1> {
let user = self.clone_user();
self.cache.is_bot = user.bot.is_some();
// Fetch pending policy changes.
let policy_changes = if user.bot.is_some() {
vec![]
let policy_changes = if user.bot.is_some() || !fields.policy_changes {
None
} else {
db.fetch_policy_changes()
.await?
.into_iter()
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.map(Into::into)
.collect()
Some(
db.fetch_policy_changes()
.await?
.into_iter()
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.map(Into::into)
.collect(),
)
};
// Find all relationships to the user.
@@ -168,7 +172,7 @@ impl State {
.await?;
// Fetch customisations.
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
let emojis = if fields.emojis {
Some(
db.fetch_emoji_by_parent_ids(
&servers
@@ -176,25 +180,56 @@ impl State {
.map(|x| x.id.to_string())
.collect::<Vec<String>>(),
)
.await?,
.await?
.into_iter()
.map(|emoji| emoji.into())
.collect(),
)
} else {
None
};
// Fetch user settings
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
.iter()
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
{
Some(db.fetch_user_settings(&user.id, keys).await?)
let user_settings = if !fields.user_settings.is_empty() {
Some(
db.fetch_user_settings(&user.id, &fields.user_settings)
.await?,
)
} else {
None
};
// Fetch channel unreads
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
Some(db.fetch_unreads(&user.id).await?)
let channel_unreads = if fields.channel_unreads {
Some(
db.fetch_unreads(&user.id)
.await?
.into_iter()
.map(|unread| unread.into())
.collect(),
)
} else {
None
};
let voice_states = if fields.voice_states {
// fetch voice states for all the channels we can see
let mut voice_states = Vec::new();
for channel in channels.iter().filter(|c| {
matches!(
c,
Channel::DirectMessage { .. }
| Channel::Group { .. }
| Channel::TextChannel { voice: Some(_), .. }
)
}) {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
voice_states.push(voice_state)
}
}
Some(voice_states)
} else {
None
};
@@ -241,30 +276,27 @@ impl State {
}
Ok(EventV1::Ready {
users: if fields.contains(&ReadyPayloadFields::Users) {
Some(users)
} else {
None
},
servers: if fields.contains(&ReadyPayloadFields::Servers) {
users: if fields.users { Some(users) } else { None },
servers: if fields.servers {
Some(servers.into_iter().map(Into::into).collect())
} else {
None
},
channels: if fields.contains(&ReadyPayloadFields::Channels) {
channels: if fields.channels {
Some(channels.into_iter().map(Into::into).collect())
} else {
None
},
members: if fields.contains(&ReadyPayloadFields::Members) {
members: if fields.members {
Some(members.into_iter().map(Into::into).collect())
} else {
None
},
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
voice_states,
emojis,
user_settings,
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
channel_unreads,
policy_changes,
})
@@ -279,19 +311,14 @@ impl State {
let id = &id.to_string();
for (channel_id, channel) in &self.cache.channels {
match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
if server == id {
channel_ids.insert(channel_id.clone());
if channel.server() == Some(id) {
channel_ids.insert(channel_id.clone());
if self.cache.can_view_channel(db, channel).await {
added_channels.push(channel_id.clone());
} else {
removed_channels.push(channel_id.clone());
}
}
if self.cache.can_view_channel(db, channel).await {
added_channels.push(channel_id.clone());
} else {
removed_channels.push(channel_id.clone());
}
_ => {}
}
}
@@ -459,6 +486,7 @@ impl State {
server,
channels,
emojis: _,
voice_states: _,
} => {
self.insert_subscription(id.clone()).await;

View File

@@ -1,7 +1,9 @@
use std::env;
use std::{env, sync::Arc};
use async_std::net::TcpListener;
use revolt_presence::clear_region;
use once_cell::sync::OnceCell;
use revolt_database::voice::VoiceClient;
#[macro_use]
extern crate log;
@@ -12,6 +14,15 @@ pub mod events;
mod database;
mod websocket;
pub static VOICE_CLIENT: OnceCell<Arc<VoiceClient>> = OnceCell::new();
pub fn get_voice_client() -> Arc<VoiceClient> {
VOICE_CLIENT
.get()
.expect("get_voice_client called before set")
.clone()
}
#[async_std::main]
async fn main() {
// Configure requirements for Bonfire.
@@ -24,6 +35,8 @@ async fn main() {
clear_region(None).await;
}
VOICE_CLIENT.set(Arc::new(VoiceClient::from_revolt_config().await)).unwrap();
// Setup a TCP listener to accept WebSocket connections on.
// By default, we bind to port 14703 on all interfaces.
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into());

View File

@@ -17,6 +17,7 @@ use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
use revolt_config::report_internal_error;
use revolt_database::{
events::{client::EventV1, server::ClientMessage},
iso8601_timestamp::Timestamp,
Database, User, UserHint,
};
use revolt_presence::{create_session, delete_session};
@@ -100,6 +101,10 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
info!("User {addr:?} authenticated as @{}", user.username);
db.update_session_last_seen(&session_id, Timestamp::now_utc())
.await
.ok();
// Create local state.
let mut state = State::from(user, session_id);
let user_id = state.cache.user_id.clone();

View File

@@ -0,0 +1,22 @@
[package]
name = "revolt-coalesced"
version = "0.8.9"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
description = "Revolt Backend: Coalescion service"
[features]
tokio = ["dep:tokio"]
queue = ["dep:indexmap"]
cache = ["dep:lru"]
default = ["tokio"]
[dependencies]
tokio = { version = "1.47.0", features = ["sync"], optional = true }
indexmap = { version = "*", optional = true }
lru = { version = "*", optional = true }
[dev-dependencies]
tokio = { version = "1.47.0", features = ["rt", "rt-multi-thread", "macros", "time"] }

View File

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

View File

@@ -0,0 +1,24 @@
#[derive(Clone, PartialEq, Eq, Debug)]
/// Config values for [`CoalescionService`].
pub struct CoalescionServiceConfig {
/// How many tasks are running at once
pub max_concurrent: Option<usize>,
/// Whether to queue tasks once `max_concurrent` is reached
#[cfg(feature = "queue")]
pub queue_requests: bool,
/// Max amount of tasks in the buffer queue
#[cfg(feature = "queue")]
pub max_queue: Option<usize>,
}
impl Default for CoalescionServiceConfig {
fn default() -> Self {
Self {
max_concurrent: Some(100),
#[cfg(feature = "queue")]
queue_requests: true,
#[cfg(feature = "queue")]
max_queue: Some(100)
}
}
}

View File

@@ -0,0 +1,27 @@
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
/// Coalescion service error.
pub enum Error {
/// Failed to receive the actions return from the channel for unknown reason
RecvError,
/// Reached the `max_concurrent` amount of actions running at once and could not queue the action
MaxConcurrent,
/// Reached the `max_queue` amount of actions in the queue
MaxQueue,
/// Failed to downcast the type to the current type being returned, this will be most likely an ID collision
DowncastError,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::RecvError => write!(f, "Unable to receive data from the channel"),
Error::MaxConcurrent => write!(f, "Max number of tasks running at once"),
Error::MaxQueue => write!(f, "Max number of tasks in queue"),
Error::DowncastError => write!(f, "Failed to downcast type, possible key collision with different types")
}
}
}
impl std::error::Error for Error {}

View File

@@ -0,0 +1,39 @@
//! # Coalesced
//!
//! Coalescion service to group, caching and queue duplicate actions.
//! useful for deduplicating web requests, database lookups and other similar resource
//! intensive or rate-limited actions.
//!
//! ## Features
//! - `tokio`: Uses tokio for the async backend, this is currently the only backend.
//! - `queue`: Whether to support queueing requests to only allow X amount of actions running at once.
//! - `cache`: Whether to cache the actions results for future actions with the same id, uses an LRU cache internally.
//!
//! [`CoalescionService`] uses both [`Arc`] and [`RwLock`] internally and can be cheaply cloned to
//! use in your codebase.
//!
//! It is common practice to wrap the service and in your own which delegates the executions to ensure all ids are tracked in one location across your codebase.
//!
//! All values are stored using [`Any`] and must be [`'static`] + [`Send`] + [`Sync`], if there is an id mismatch
//! and a type is wrong the library will return an error, values returned from the service are also
//! wrapped in an [`Arc`] as they are shared to each duplicate action.
//!
//! ## Example:
//! ```rs
//! use revolt_coalesced::CoalescionService;
//!
//! let service = CoalescionService::new();
//!
//! let user_id = "my_user_id";
//! let user = service.execute(user_id, || async move {
//! database.fetch_user(user_id).await.unwrap()
//! }).await;
//! ```
mod config;
mod error;
mod service;
pub use config::CoalescionServiceConfig;
pub use error::Error;
pub use service::CoalescionService;

View File

@@ -0,0 +1,208 @@
use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc};
use tokio::sync::{
watch::{channel as watch_channel, Receiver},
RwLock,
};
#[cfg(feature = "cache")]
use lru::LruCache;
#[cfg(feature = "queue")]
use indexmap::IndexMap;
use crate::{CoalescionServiceConfig, Error};
#[derive(Debug, Clone)]
#[allow(clippy::type_complexity)]
/// # Coalescion service
///
/// See module description for example usage.
pub struct CoalescionService<Id: Hash + Clone + Eq> {
config: Arc<CoalescionServiceConfig>,
watchers: Arc<RwLock<HashMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "queue")]
queue: Arc<RwLock<IndexMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "cache")]
cache: Option<Arc<tokio::sync::Mutex<LruCache<Id, Arc<dyn Any + Send + Sync>>>>>,
}
impl<Id: Hash + Clone + Eq> CoalescionService<Id> {
pub fn new() -> Self {
Default::default()
}
pub fn from_config(config: CoalescionServiceConfig) -> Self {
Self {
config: Arc::new(config),
watchers: Arc::new(RwLock::new(HashMap::new())),
#[cfg(feature = "queue")]
queue: Arc::new(RwLock::new(IndexMap::new())),
#[cfg(feature = "cache")]
cache: None,
}
}
#[cfg(feature = "cache")]
pub fn from_cache(
config: CoalescionServiceConfig,
cache: LruCache<Id, Arc<dyn Any + Send + Sync>>,
) -> Self {
Self {
cache: Some(Arc::new(Mutex::new(cache))),
..Self::from_config(config)
}
}
async fn wait_for<Value: Any + Send + Sync>(
&self,
mut receiver: Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>,
) -> Result<Arc<Value>, Error> {
receiver
.wait_for(|v| v.is_some())
.await
.map_err(|_| Error::RecvError)
.and_then(|r| r.clone().unwrap())
.and_then(|arc| Arc::downcast(arc).map_err(|_| Error::DowncastError))
}
async fn insert_and_execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
let (send, recv) = watch_channel(None);
self.watchers.write().await.insert(id.clone(), recv);
let value = Ok(Arc::new(func().await));
send.send_modify(|opt| {
opt.replace(value.clone().map(|v| v as Arc<dyn Any + Send + Sync>));
});
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Ok(value) = &value {
cache.lock().await.push(id.clone(), value.clone());
}
};
self.watchers.write().await.remove(&id);
value
}
/// Coalesces an function, the actual function may not run if one with the same id is already running,
/// queued to be ran, or cached, the id should be globally unique for this specific action.
pub async fn execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Some(value) = cache.lock().await.get(&id) {
return Arc::downcast::<Value>(value.clone()).map_err(|_| Error::DowncastError);
}
};
let (receiver, length) = {
let watchers = self.watchers.read().await;
let length = watchers.len();
(watchers.get(&id).cloned(), length)
};
if let Some(receiver) = receiver {
self.wait_for(receiver).await
} else {
match self.config.max_concurrent {
Some(max_concurrent) if length >= max_concurrent => {
#[cfg(feature = "queue")]
if self.config.queue_requests {
let (receiver, length) = {
let queue = self.queue.read().await;
(queue.get(&id).cloned(), queue.len())
};
if let Some(receiver) = receiver {
return self.wait_for(receiver).await;
} else {
if self
.config
.max_queue
.is_some_and(|max_queue| max_queue >= length)
{
return Err(Error::MaxQueue);
};
let (send, recv) = watch_channel(None);
self.queue.write().await.insert(id.clone(), recv);
loop {
let length = self.watchers.read().await.len();
if length < max_concurrent {
let first_key = {
let queue = self.queue.read().await;
queue.first().map(|v| v.0).cloned()
};
if first_key == Some(id.clone()) {
self.queue.write().await.shift_remove(&id);
let response = self.insert_and_execute(id, func).await;
send.send_modify(|opt| {
opt.replace(
response
.clone()
.map(|v| v as Arc<dyn Any + Send + Sync>),
);
});
return response;
}
}
}
}
} else {
Err(Error::MaxConcurrent)
}
#[cfg(not(feature = "queue"))]
Err(Error::MaxConcurrent)
}
_ => self.insert_and_execute(id, func).await,
}
}
}
/// Fetches the amount of currently running tasks
pub async fn current_task_count(&self) -> usize {
self.watchers.read().await.len()
}
#[cfg(feature = "queue")]
/// Fetches the current length of the queue
pub async fn current_queue_len(&self) -> usize {
self.queue.read().await.len()
}
}
impl<Id: Hash + Clone + Eq> Default for CoalescionService<Id> {
fn default() -> Self {
Self::from_config(CoalescionServiceConfig::default())
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.8.7"
version = "0.8.9"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -11,8 +11,9 @@ description = "Revolt Backend: Configuration"
[features]
anyhow = ["dep:sentry-anyhow"]
report-macros = ["revolt-result"]
sentry = ["dep:sentry"]
test = ["async-std"]
default = ["test", "anyhow"]
default = ["test", "sentry"]
[dependencies]
# Utility
@@ -32,8 +33,8 @@ log = "0.4.14"
pretty_env_logger = "0.4.0"
# Sentry
sentry = "0.31.5"
sentry = { version = "0.31.5", optional = true }
sentry-anyhow = { version = "0.38.1", optional = true }
# Core
revolt-result = { version = "0.8.7", path = "../result", optional = true }
revolt-result = { version = "0.8.9", path = "../result", optional = true }

View File

@@ -22,6 +22,8 @@ january = "http://local.revolt.chat/january"
voso_legacy = ""
voso_legacy_ws = ""
[hosts.livekit]
[rabbit]
host = "rabbit"
port = 5672
@@ -56,6 +58,8 @@ voso_legacy_token = ""
trust_cloudflare = false
# easypwned endpoint
easypwned = ""
# Tenor API Key
tenor_key = ""
[api.security.captcha]
# hCaptcha configuration
@@ -66,8 +70,13 @@ hcaptcha_sitekey = ""
# Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50
[api.users]
[api.livekit]
# How long to ring devices for when calling in dms/groups, in seconds
call_ring_duration = 30
[api.livekit.nodes]
[api.users]
[pushd]
# this changes the names of the queues to not overlap
@@ -85,6 +94,7 @@ message_queue = "notifications.origin.message"
mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received
dm_call_queue = "notifications.ingest.dm_call" # friend request received
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices
@@ -228,6 +238,18 @@ message_attachments = 5
# Maximum number of servers the user can create/join
servers = 50
# Maximum audio frequency (Hz) in voice calls
voice_quality = 16000
# Whether the user can use video streams in voice calls
video = true
# Mamimum resolution (width, height) of video streams in voice calls
video_resolution = [1080, 720]
# Minimum and maximum aspect ratio of video streams in voice calls
video_aspect_ratio = [0.3, 2.5]
[features.limits.new_user.file_upload_size_limit]
# Maximum file size limits (in bytes)
attachments = 20_000_000
@@ -255,6 +277,18 @@ message_attachments = 5
# Maximum number of servers the user can create/join
servers = 100
# Maximum audio frequency (Hz) in voice calls
voice_quality = 16000
# Whether the user can use video streams in voice calls
video = true
# Mamimum resolution (width, height) of video streams in voice calls
video_resolution = [1080, 720]
# Minimum and maximum aspect ratio of video streams in voice calls
video_aspect_ratio = [0.3, 2.5]
[features.limits.default.file_upload_size_limit]
# Maximum file size limits (in bytes)
attachments = 20_000_000
@@ -273,7 +307,9 @@ process_message_delay_limit = 5
# Configuration for Sentry error reporting
api = ""
events = ""
voice_ingress = ""
files = ""
proxy = ""
pushd = ""
crond = ""
gifbox = ""

View File

@@ -6,10 +6,12 @@ use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
#[cfg(feature = "sentry")]
pub use sentry::{capture_error, capture_message, Level};
#[cfg(feature = "anyhow")]
pub use sentry_anyhow::capture_anyhow;
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! report_error {
( $expr: expr, $error: ident $( $tt:tt )? ) => {
@@ -24,7 +26,7 @@ macro_rules! report_error {
};
}
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! capture_internal_error {
( $expr: expr ) => {
@@ -35,7 +37,7 @@ macro_rules! capture_internal_error {
};
}
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! report_internal_error {
( $expr: expr ) => {
@@ -60,6 +62,9 @@ static CONFIG_SEARCH_PATHS: [&str; 3] = [
"/Revolt.toml",
];
/// Path to search for test overrides
static TEST_OVERRIDE_PATH: &str = "Revolt.test-overrides.toml";
/// Configuration builder
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
RwLock::new({
@@ -73,6 +78,20 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
include_str!("../Revolt.test.toml"),
FileFormat::Toml,
));
// recursively search upwards for an overrides file (if there is one)
if let Ok(cwd) = std::env::current_dir() {
let mut path = Some(cwd.as_path());
while let Some(current_path) = path {
let target_path = current_path.join(TEST_OVERRIDE_PATH);
if target_path.exists() {
builder = builder
.add_source(File::new(target_path.to_str().unwrap(), FileFormat::Toml));
}
path = current_path.parent();
}
}
}
for path in CONFIG_SEARCH_PATHS {
@@ -106,8 +125,7 @@ pub struct Hosts {
pub events: String,
pub autumn: String,
pub january: String,
pub voso_legacy: String,
pub voso_legacy_ws: String,
pub livekit: HashMap<String, String>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -171,6 +189,7 @@ pub struct ApiSecurity {
pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool,
pub easypwned: String,
pub tenor_key: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -178,6 +197,25 @@ pub struct ApiWorkers {
pub max_concurrent_connections: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiLiveKit {
pub call_ring_duration: usize,
pub nodes: HashMap<String, LiveKitNode>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct LiveKitNode {
pub url: String,
pub lat: f64,
pub lon: f64,
pub key: String,
pub secret: String,
// whether to hide the node in the nodes list
#[serde(default)]
pub private: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiUsers {
pub early_adopter_cutoff: Option<u64>,
@@ -189,6 +227,7 @@ pub struct Api {
pub smtp: ApiSmtp,
pub security: ApiSecurity,
pub workers: ApiWorkers,
pub livekit: ApiLiveKit,
pub users: ApiUsers,
}
@@ -201,6 +240,7 @@ pub struct Pushd {
// Queues
pub message_queue: String,
pub mass_mention_queue: String,
pub dm_call_queue: String,
pub fr_accepted_queue: String,
pub fr_received_queue: String,
pub generic_queue: String,
@@ -231,6 +271,10 @@ impl Pushd {
self.get_routing_key(self.mass_mention_queue.clone())
}
pub fn get_dm_call_routing_key(&self) -> String {
self.get_routing_key(self.dm_call_queue.clone())
}
pub fn get_fr_accepted_routing_key(&self) -> String {
self.get_routing_key(self.fr_accepted_queue.clone())
}
@@ -298,6 +342,10 @@ pub struct FeaturesLimits {
pub message_length: usize,
pub message_attachments: usize,
pub servers: usize,
pub voice_quality: u32,
pub video: bool,
pub video_resolution: [u32; 2],
pub video_aspect_ratio: [f32; 2],
pub file_upload_size_limit: HashMap<String, usize>,
}
@@ -342,10 +390,12 @@ pub struct Features {
pub struct Sentry {
pub api: String,
pub events: String,
pub voice_ingress: String,
pub files: String,
pub proxy: String,
pub pushd: String,
pub crond: String,
pub gifbox: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -388,6 +438,11 @@ pub async fn read() -> Config {
pub async fn config() -> Settings {
let mut config = read().await.try_deserialize::<Settings>().unwrap();
// inject REDIS_URI for redis-kiss library
if std::env::var("REDIS_URL").is_err() {
std::env::set_var("REDIS_URI", config.database.redis.clone());
}
// auto-detect production nodes
if config.hosts.api.contains("https") && config.hosts.api.contains("revolt.chat") {
config.production = true;
@@ -397,6 +452,7 @@ pub async fn config() -> Settings {
}
/// Configure logging and common Rust variables
#[cfg(feature = "sentry")]
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
@@ -406,12 +462,6 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
}
if std::env::var("REDIS_URL").is_err() {
// Configure redis-kiss library
let config = config().await;
std::env::set_var("REDIS_URI", config.database.redis);
}
pretty_env_logger::init();
log::info!("Starting {release}");
@@ -428,6 +478,7 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
}
}
#[cfg(feature = "sentry")]
#[macro_export]
macro_rules! configure {
($application: ident) => {

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.8.7"
version = "0.8.9"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -10,12 +10,12 @@ description = "Revolt Backend: Database Implementation"
[features]
# Databases
mongodb = ["dep:mongodb", "bson"]
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
# ... Other
tasks = ["isahc", "linkify", "url-escape"]
async-std-runtime = ["async-std"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
axum-impl = ["axum"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
@@ -24,19 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.8.7", path = "../config", features = [
revolt-config = { version = "0.8.9", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.7", path = "../result" }
revolt-models = { version = "0.8.7", path = "../models", features = [
revolt-result = { version = "0.8.9", path = "../result" }
revolt-models = { version = "0.8.9", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.8.7", path = "../presence" }
revolt-permissions = { version = "0.8.7", path = "../permissions", features = [
revolt-presence = { version = "0.8.9", path = "../presence" }
revolt-permissions = { version = "0.8.9", path = "../permissions", features = [
"serde",
"bson",
] }
revolt-parser = { version = "0.8.7", path = "../parser" }
revolt-parser = { version = "0.8.9", path = "../parser" }
# Utility
log = "0.4"
@@ -91,13 +91,13 @@ rocket = { version = "0.5.1", default-features = false, features = [
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
# Notifications
fcm_v1 = "0.3.0"
web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.15", features = ["rocket_impl"] }
authifier = { version = "1.0.15" }
# RabbitMQ
amqprs = { version = "1.7.0" }
# Voice
livekit-api = "0.4.4"
livekit-protocol = "0.4.0"
livekit-runtime = { version = "0.3.1", features = ["tokio"] }

View File

@@ -3,18 +3,21 @@
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Member",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "Member",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0002"
},
{
@@ -23,6 +26,9 @@
"channel_type": "Group",
"name": "My Group",
"owner": "__ID:0__",
"recipients": ["__ID:0__", "__ID:1__"]
"recipients": [
"__ID:0__",
"__ID:1__"
]
}
]
]

View File

@@ -3,18 +3,21 @@
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Moderator",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "User",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
@@ -39,7 +42,9 @@
"_id": "__ID:4__",
"owner": "__ID:0__",
"name": "Server",
"channels": ["__ID:3__"],
"channels": [
"__ID:3__"
],
"roles": {
"__ID:5__": {
"name": "Moderator",
@@ -47,7 +52,7 @@
"a": 545270208,
"d": 0
},
"rank": 3
"rank": 1
},
"__ID:6__": {
"name": "Owner",
@@ -66,7 +71,9 @@
"user": "__ID:0__",
"server": "__ID:4__"
},
"roles": ["__ID:6__"],
"roles": [
"__ID:6__"
],
"joined_at": 1698318340195
},
{
@@ -75,7 +82,9 @@
"user": "__ID:1__",
"server": "__ID:4__"
},
"roles": ["__ID:5__"],
"roles": [
"__ID:5__"
],
"joined_at": 1698318340195
},
{
@@ -86,4 +95,4 @@
},
"joined_at": 1698318340195
}
]
]

View File

@@ -2,7 +2,8 @@ use std::collections::HashSet;
use crate::events::rabbit::*;
use crate::User;
use amqprs::channel::BasicPublishArguments;
use amqprs::channel::{BasicPublishArguments, ExchangeDeclareArguments};
use amqprs::connection::OpenConnectionArguments;
use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError};
use amqprs::{BasicProperties, FieldTable};
use revolt_models::v0::PushNotification;
@@ -25,6 +26,35 @@ impl AMQP {
}
}
pub async fn new_auto() -> AMQP {
let config = revolt_config::config().await;
let connection = Connection::open(&OpenConnectionArguments::new(
&config.rabbit.host,
config.rabbit.port,
&config.rabbit.username,
&config.rabbit.password,
))
.await
.expect("Failed to connect to RabbitMQ");
let channel = connection
.open_channel(None)
.await
.expect("Failed to open RabbitMQ channel");
channel
.exchange_declare(
ExchangeDeclareArguments::new(&config.pushd.exchange, "direct")
.durable(true)
.finish(),
)
.await
.expect("Failed to declare exchange");
AMQP::new(connection, channel)
}
pub async fn friend_request_accepted(
&self,
accepted_request_user: &User,
@@ -240,4 +270,50 @@ impl AMQP {
)
.await
}
/// # DM Call Update
/// Used to send an update about a DM call, eg. start or end of a call.
/// Recipients can be used to narrow the scope of recipients, otherwise all recipients will be notified.
/// `ended` refers to the ringing period, not necessarily the call itself.
pub async fn dm_call_updated(
&self,
initiator_id: &str,
channel_id: &str,
started_at: Option<&str>,
ended: bool,
recipients: Option<Vec<String>>,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = InternalDmCallPayload {
payload: DmCallPayload {
initiator_id: initiator_id.to_string(),
channel_id: channel_id.to_string(),
started_at: started_at.map(|f| f.to_string()),
ended,
},
recipients,
};
let payload = to_string(&payload).unwrap();
debug!(
"Sending dm call update payload on channel {}: {}",
config.pushd.get_dm_call_routing_key(),
payload
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
.finish(),
payload.into(),
BasicPublishArguments::new(
&config.pushd.exchange,
&config.pushd.get_dm_call_routing_key(),
),
)
.await
}
}

View File

@@ -1,3 +1,4 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -13,6 +14,7 @@ use authifier::Authifier;
use rand::Rng;
use revolt_config::config;
#[cfg(feature = "mongodb")]
pub use self::mongodb::*;
pub use self::reference::*;
@@ -25,17 +27,20 @@ pub enum DatabaseInfo {
/// Use the mock database
Reference,
/// Connect to MongoDB
#[cfg(feature = "mongodb")]
MongoDb { uri: String, database_name: String },
/// Use existing MongoDB connection
#[cfg(feature = "mongodb")]
MongoDbFromClient(::mongodb::Client, String),
}
/// Database
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum Database {
/// Mock database
Reference(ReferenceDb),
/// MongoDB database
#[cfg(feature = "mongodb")]
MongoDb(MongoDb),
}
@@ -45,7 +50,7 @@ impl DatabaseInfo {
pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
Ok(match self {
match self {
DatabaseInfo::Auto => {
if std::env::var("TEST_DB").is_ok() {
DatabaseInfo::Test(format!(
@@ -53,16 +58,20 @@ impl DatabaseInfo {
rand::thread_rng().gen_range(1_000_000..10_000_000)
))
.connect()
.await?
.await
} else if !config.database.mongodb.is_empty() {
DatabaseInfo::MongoDb {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name: "revolt".to_string(),
}
.connect()
.await?
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string())
} else {
DatabaseInfo::Reference.connect().await?
DatabaseInfo::Reference.connect().await
}
}
DatabaseInfo::Test(database_name) => {
@@ -70,30 +79,36 @@ impl DatabaseInfo {
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
.as_str()
{
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
"REFERENCE" => DatabaseInfo::Reference.connect().await,
"MONGODB" => {
DatabaseInfo::MongoDb {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name,
}
.connect()
.await?
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string())
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
}
}
DatabaseInfo::Reference => Database::Reference(Default::default()),
DatabaseInfo::Reference => Ok(Database::Reference(Default::default())),
#[cfg(feature = "mongodb")]
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))
Ok(Database::MongoDb(MongoDb(client, database_name)))
}
#[cfg(feature = "mongodb")]
DatabaseInfo::MongoDbFromClient(client, database_name) => {
Database::MongoDb(MongoDb(client, database_name))
Ok(Database::MongoDb(MongoDb(client, database_name)))
}
})
}
}
}
@@ -219,12 +234,16 @@ impl Database {
Authifier {
database: match self {
Database::Reference(_) => Default::default(),
#[cfg(feature = "mongodb")]
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: auth_config,
#[cfg(feature = "tasks")]
event_channel: Some(crate::tasks::authifier_relay::sender()),
#[cfg(not(feature = "tasks"))]
event_channel: None,
}
}
}

View File

@@ -10,8 +10,8 @@ use serde::de::DeserializeOwned;
use serde::Serialize;
database_derived!(
#[cfg(feature = "mongodb")]
/// MongoDB implementation
#[derive(Debug)]
pub struct MongoDb(pub ::mongodb::Client, pub String);
);

View File

@@ -10,7 +10,7 @@ use crate::{
database_derived!(
/// Reference implementation
#[derive(Default)]
#[derive(Default, Debug)]
pub struct ReferenceDb {
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,

View File

@@ -3,10 +3,7 @@ use revolt_result::Error;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage,
FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message,
PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser,
PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, Webhook,
AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook
};
use crate::Database;
@@ -20,16 +17,33 @@ pub enum Ping {
}
/// Fields provided in Ready payload
#[derive(PartialEq)]
pub enum ReadyPayloadFields {
Users,
Servers,
Channels,
Members,
Emoji,
#[derive(PartialEq, Debug, Clone, Deserialize)]
pub struct ReadyPayloadFields {
pub users: bool,
pub servers: bool,
pub channels: bool,
pub members: bool,
pub emojis: bool,
pub voice_states: bool,
pub user_settings: Vec<String>,
pub channel_unreads: bool,
pub policy_changes: bool,
}
UserSettings(Vec<String>),
ChannelUnreads,
impl Default for ReadyPayloadFields {
fn default() -> Self {
Self {
users: true,
servers: true,
channels: true,
members: true,
emojis: true,
voice_states: true,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: true,
}
}
}
/// Protocol Events
@@ -57,13 +71,16 @@ pub enum EventV1 {
members: Option<Vec<Member>>,
#[serde(skip_serializing_if = "Option::is_none")]
emojis: Option<Vec<Emoji>>,
#[serde(skip_serializing_if = "Option::is_none")]
voice_states: Option<Vec<ChannelVoiceState>>,
#[serde(skip_serializing_if = "Option::is_none")]
user_settings: Option<UserSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
channel_unreads: Option<Vec<ChannelUnread>>,
policy_changes: Vec<PolicyChange>,
#[serde(skip_serializing_if = "Option::is_none")]
policy_changes: Option<Vec<PolicyChange>>,
},
/// Ping response
@@ -122,6 +139,7 @@ pub enum EventV1 {
server: Server,
channels: Vec<Channel>,
emojis: Vec<Emoji>,
voice_states: Vec<ChannelVoiceState>
},
/// Update existing server
@@ -144,7 +162,13 @@ pub enum EventV1 {
},
/// User joins server
ServerMemberJoin { id: String, user: String },
ServerMemberJoin {
id: String,
// Deprecated: use member.id.user
#[deprecated = "Use member.id.user instead"]
user: String,
member: Member,
},
/// User left server
ServerMemberLeave {
@@ -165,6 +189,9 @@ pub enum EventV1 {
/// Server role deleted
ServerRoleDelete { id: String, role_id: String },
/// Server roles ranks updated
ServerRoleRanksUpdate { id: String, ranks: Vec<String> },
/// Update existing user
UserUpdate {
id: String,
@@ -245,6 +272,33 @@ pub enum EventV1 {
/// Auth events
Auth(AuthifierEvent),
/// Voice events
VoiceChannelJoin {
id: String,
state: UserVoiceState,
},
VoiceChannelLeave {
id: String,
user: String,
},
VoiceChannelMove {
user: String,
from: String,
to: String,
state: UserVoiceState
},
UserVoiceStateUpdate {
id: String,
channel_id: String,
data: PartialUserVoiceState,
},
UserMoveVoiceChannel {
node: String,
from: String,
to: String,
token: String,
}
}
impl EventV1 {

View File

@@ -37,6 +37,20 @@ pub struct GenericPayload {
pub user: User,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct DmCallPayload {
pub initiator_id: String,
pub channel_id: String,
pub started_at: Option<String>,
pub ended: bool,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct InternalDmCallPayload {
pub payload: DmCallPayload,
pub recipients: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
#[allow(clippy::large_enum_variant)]
@@ -46,6 +60,7 @@ pub enum PayloadKind {
FRReceived(FRReceivedPayload),
BadgeUpdate(usize),
Generic(GenericPayload),
DmCallStartEnd(DmCallPayload),
}
#[derive(Serialize, Deserialize)]

View File

@@ -1,8 +1,8 @@
use serde::Deserialize;
use serde::{Serialize, Deserialize};
use super::client::Ping;
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ClientMessage {
Authenticate { token: String },

View File

@@ -25,6 +25,9 @@ pub use mongodb;
#[macro_use]
extern crate bson;
#[cfg(not(feature = "async-std-runtime"))]
compile_error!("async-std-runtime feature must be enabled.");
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! query {
@@ -103,11 +106,15 @@ pub mod util;
pub use models::*;
pub mod events;
#[cfg(feature = "tasks")]
pub mod tasks;
mod amqp;
pub use amqp::amqp::AMQP;
pub mod voice;
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t

View File

@@ -1,3 +1,4 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,18 +1,21 @@
use std::{collections::HashSet, ops::BitXor, time::Duration};
use std::{
collections::{HashMap, HashSet},
ops::BitXor,
time::Duration,
};
use crate::{
mongodb::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, User, DISCRIMINATOR_SEARCH_SPACE,
AbstractServers, Invite, MongoDb, User, DISCRIMINATOR_SEARCH_SPACE,
};
use bson::{oid::ObjectId, to_bson};
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use revolt_result::{Error, ErrorType};
use revolt_permissions::{ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS};
use serde::{Deserialize, Serialize};
use unicode_segmentation::UnicodeSegmentation;
@@ -22,7 +25,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 41; // MUST BE +1 to last migration
pub const LATEST_REVISION: i32 = 49; // MUST BE +1 to last migration
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -910,6 +913,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
if revision <= 26 {
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging.");
auto_derived!(
@@ -1076,6 +1080,14 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
channel_id: String,
}
#[allow(clippy::enum_variant_names)]
#[derive(serde::Serialize, serde::Deserialize)]
enum Channel {
Group { owner: String },
TextChannel { server: String },
VoiceChannel { server: String }
}
let webhooks = db
.db()
.collection::<WebhookShell>("channel_webhooks")
@@ -1087,8 +1099,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.await;
for webhook in webhooks {
match db.fetch_channel(&webhook.channel_id).await {
Ok(channel) => {
match db.col::<Channel>("channels").find_one(doc! { "_id": &webhook.channel_id }).await.unwrap() {
Some(channel) => {
let creator_id = match channel {
Channel::Group { owner, .. } => owner,
Channel::TextChannel { server, .. }
@@ -1096,7 +1108,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let server = db.fetch_server(&server).await.expect("server");
server.owner
}
_ => unreachable!("not server or group channel!"),
};
db.db()
@@ -1114,17 +1125,13 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.await
.expect("update webhook");
}
Err(Error {
error_type: ErrorType::NotFound,
..
}) => {
None => {
db.db()
.collection::<WebhookShell>("channel_webhooks")
.delete_one(doc! { "_id": webhook._id })
.await
.expect("failed to delete invalid webhook");
}
Err(err) => panic!("{err:?}"),
}
}
}
@@ -1165,6 +1172,98 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("failed to update users");
}
if revision <= 43 {
info!(
"Running migration [revision 43 / 05-06-2025]: convert role ranks to uniform numbers."
);
#[derive(Serialize, Deserialize, Clone)]
struct Role {
pub rank: i64,
}
#[derive(Serialize, Deserialize, Clone)]
struct Server {
#[serde(rename = "_id")]
pub id: String,
#[serde(default = "HashMap::<String, Role>::new")]
pub roles: HashMap<String, Role>,
}
let mut servers = db
.db()
.collection::<Server>("servers")
.find(doc! {
"roles": {
"$exists": true,
"$ne": []
}
})
.await
.unwrap()
.filter_map(|s| async { s.ok() })
.boxed();
while let Some(server) = servers.next().await {
let mut ordered_roles = server.roles.clone().into_iter().collect::<Vec<_>>();
ordered_roles.sort_by(|(_, role_a), (_, role_b)| role_a.rank.cmp(&role_b.rank));
let ordered_roles = ordered_roles
.into_iter()
.map(|(id, _)| id)
.collect::<Vec<_>>();
let mut doc = doc! {};
for id in server.roles.keys() {
doc.insert(
format!("roles.{id}.rank"),
ordered_roles.iter().position(|x| id == x).unwrap() as i64,
);
}
db.db()
.collection::<Server>("servers")
.update_one(doc! { "_id": &server.id }, doc! { "$set": doc })
.await
.unwrap();
}
}
if revision <= 46 {
info!("Running migration [revision 46 / 29-04-2025]: Convert all `VoiceChannel`'s into `TextChannel`");
db.col::<Document>("channels")
.update_many(
doc! { "channel_type": "VoiceChannel" },
doc! {
"$set": {
"channel_type": "TextChannel",
"voice": {}
}
}
)
.await
.expect("Failed to update voice channels");
};
if revision <= 48 {
info!("Running migration [revision 48 / 22-10-2025]: Add Video + Listen to default permissions");
db.col::<Document>("servers")
.update_many(
doc! { },
doc! {
"$bit": {
"default_permissions": {
"or": (ChannelPermission::Video + ChannelPermission::Speak + ChannelPermission::Listen) as i64
},
}
}
)
.await
.expect("Failed to update default_permissions");
};
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision)
}

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{Bot, FieldsBot, PartialBot};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -69,7 +69,7 @@ impl Invite {
creator: creator.id.clone(),
channel: id.clone(),
}),
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
Channel::TextChannel { id, server, .. } => {
Ok(Invite::Server {
code,
creator: creator.id.clone(),

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Invite;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::ChannelUnread;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{FieldsWebhook, PartialWebhook, Webhook};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,4 +1,5 @@
use std::collections::HashMap;
#![allow(deprecated)]
use std::{borrow::Cow, collections::HashMap};
use revolt_config::config;
use revolt_models::v0::{self, MessageAuthor};
@@ -8,10 +9,12 @@ use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::{
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
Server, SystemMessage, User, AMQP,
events::client::EventV1, Database, File, PartialServer, Server, SystemMessage, User, AMQP,
};
#[cfg(feature = "mongodb")]
use crate::IntoDocumentPath;
auto_derived!(
#[serde(tag = "channel_type")]
pub enum Channel {
@@ -103,38 +106,18 @@ auto_derived!(
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
/// Voice Information for when this channel is also a voice channel
#[serde(skip_serializing_if = "Option::is_none")]
voice: Option<VoiceInformation>,
},
/// Voice channel belonging to a server
VoiceChannel {
/// Unique Id
#[serde(rename = "_id")]
id: String,
/// Id of the server this channel belongs to
server: String,
}
/// Display name of the channel
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
/// Channel description
description: Option<String>,
/// Custom icon attachment
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
/// Default permissions assigned to users in this channel
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
},
#[derive(Default)]
pub struct VoiceInformation {
/// Maximium amount of users allowed in the voice channel at once
#[serde(skip_serializing_if = "Option::is_none")]
pub max_users: Option<usize>,
}
);
@@ -161,6 +144,8 @@ auto_derived!(
pub default_permissions: Option<OverrideField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub voice: Option<VoiceInformation>,
}
/// Optional fields on channel object
@@ -168,6 +153,7 @@ auto_derived!(
Description,
Icon,
DefaultPermissions,
Voice,
}
);
@@ -219,16 +205,19 @@ impl Channel {
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
voice: data.voice.map(|voice| voice.into()),
},
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
v0::LegacyServerChannelType::Voice => Channel::TextChannel {
id: id.clone(),
server: server.id.to_owned(),
name: data.name,
description: data.description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
voice: Some(data.voice.unwrap_or_default().into()),
},
};
@@ -429,8 +418,28 @@ impl Channel {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
| Channel::TextChannel { id, .. } => id,
}
}
/// Clone this channel's server id
pub fn server(&self) -> Option<&str> {
match self {
Channel::TextChannel { server, .. } => Some(server),
_ => None,
}
}
/// Gets this channel's voice information
pub fn voice(&self) -> Option<Cow<VoiceInformation>> {
match self {
Self::DirectMessage { .. } | Self::Group { .. } => {
Some(Cow::Owned(VoiceInformation::default()))
}
Self::TextChannel {
voice: Some(voice), ..
} => Some(Cow::Borrowed(voice)),
_ => None,
}
}
@@ -447,12 +456,6 @@ impl Channel {
server,
role_permissions,
..
}
| Channel::VoiceChannel {
id,
server,
role_permissions,
..
} => {
db.set_channel_role_permission(id, role_id, permissions)
.await?;
@@ -499,7 +502,7 @@ impl Channel {
clear: remove.into_iter().map(|v| v.into()).collect(),
}
.p(match self {
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
Self::TextChannel { server, .. } => server.clone(),
_ => id,
})
.await;
@@ -511,17 +514,13 @@ impl Channel {
pub fn remove_field(&mut self, field: &FieldsChannel) {
match field {
FieldsChannel::Description => match self {
Self::Group { description, .. }
| Self::TextChannel { description, .. }
| Self::VoiceChannel { description, .. } => {
Self::Group { description, .. } | Self::TextChannel { description, .. } => {
description.take();
}
_ => {}
},
FieldsChannel::Icon => match self {
Self::Group { icon, .. }
| Self::TextChannel { icon, .. }
| Self::VoiceChannel { icon, .. } => {
Self::Group { icon, .. } | Self::TextChannel { icon, .. } => {
icon.take();
}
_ => {}
@@ -530,15 +529,17 @@ impl Channel {
Self::TextChannel {
default_permissions,
..
}
| Self::VoiceChannel {
default_permissions,
..
} => {
default_permissions.take();
}
_ => {}
},
FieldsChannel::Voice => match self {
Self::TextChannel { voice, .. } => {
voice.take();
}
_ => {}
},
}
}
@@ -550,6 +551,7 @@ impl Channel {
}
/// Apply partial channel to channel
#[allow(deprecated)]
pub fn apply_options(&mut self, partial: PartialChannel) {
match self {
Self::SavedMessages { .. } => {}
@@ -598,15 +600,7 @@ impl Channel {
nsfw,
default_permissions,
role_permissions,
..
}
| Self::VoiceChannel {
name,
description,
icon,
nsfw,
default_permissions,
role_permissions,
voice,
..
} => {
if let Some(v) = partial.name {
@@ -632,6 +626,10 @@ impl Channel {
if let Some(v) = partial.default_permissions {
default_permissions.replace(v);
}
if let Some(v) = partial.voice {
voice.replace(v);
}
}
}
}
@@ -646,10 +644,11 @@ impl Channel {
.private(user.to_string())
.await;
#[cfg(feature = "tasks")]
crate::tasks::ack::queue_ack(
self.id().to_string(),
user.to_string(),
AckEvent::AckMessage {
crate::tasks::ack::AckEvent::AckMessage {
id: message.to_string(),
},
)
@@ -766,12 +765,14 @@ impl Channel {
}
}
#[cfg(feature = "mongodb")]
impl IntoDocumentPath for FieldsChannel {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsChannel::Description => "description",
FieldsChannel::Icon => "icon",
FieldsChannel::DefaultPermissions => "default_permissions",
FieldsChannel::Voice => "voice",
})
}
}

View File

@@ -1,5 +1,7 @@
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
use revolt_permissions::OverrideField;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -184,7 +184,7 @@ impl AbstractChannels for MongoDb {
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let id = channel.id().to_string();
let server_id = match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Channel::TextChannel { server, .. } => {
Some(server)
}
_ => None,

View File

@@ -94,9 +94,6 @@ impl AbstractChannels for ReferenceDb {
match &mut channel {
Channel::TextChannel {
role_permissions, ..
}
| Channel::VoiceChannel {
role_permissions, ..
} => {
if role_permissions.get(role_id).is_some() {
role_permissions.remove(role_id);

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Emoji;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::FileHash;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -4,6 +4,7 @@ use crate::File;
use super::FileUsedFor;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -14,7 +14,6 @@ use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::{
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
permissions::DatabasePermissionQuery,
@@ -22,6 +21,9 @@ use crate::{
Channel, Database, Emoji, File, User, AMQP,
};
#[cfg(feature = "tasks")]
use crate::tasks::{self, ack::AckEvent};
auto_derived_partial!(
/// Message
pub struct Message {
@@ -112,6 +114,11 @@ auto_derived!(
MessagePinned { id: String, by: String },
#[serde(rename = "message_unpinned")]
MessageUnpinned { id: String, by: String },
#[serde(rename = "call_started")]
CallStarted {
by: String,
finished_at: Option<Timestamp>,
},
}
/// Name and / or avatar override information
@@ -324,9 +331,7 @@ impl Message {
}
let server_id = match channel {
Channel::TextChannel { ref server, .. } | Channel::VoiceChannel { ref server, .. } => {
Some(server.clone())
}
Channel::TextChannel { ref server, .. } => Some(server.clone()),
_ => None,
};
@@ -476,6 +481,7 @@ impl Message {
// Validate the mentions go to users in the channel/server
if !user_mentions.is_empty() {
#[allow(deprecated)]
match channel {
Channel::DirectMessage { ref recipients, .. }
| Channel::Group { ref recipients, .. } => {
@@ -483,8 +489,7 @@ impl Message {
user_mentions.retain(|m| recipients_hash.contains(m));
role_mentions.clear();
}
Channel::TextChannel { ref server, .. }
| Channel::VoiceChannel { ref server, .. } => {
Channel::TextChannel { ref server, .. }=> {
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
@@ -616,9 +621,11 @@ impl Message {
.await;
// Update last_message_id
#[cfg(feature = "tasks")]
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
// Add mentions for affected users
#[cfg(feature = "tasks")]
if !mentions_elsewhere {
if let Some(mentions) = &self.mentions {
tasks::ack::queue_message(
@@ -637,6 +644,7 @@ impl Message {
}
// Generate embeds
#[cfg(feature = "tasks")]
if generate_embeds {
if let Some(content) = &self.content {
tasks::process_embeds::queue(
@@ -677,6 +685,7 @@ impl Message {
&& (self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications
#[cfg(feature = "tasks")]
tasks::ack::queue_message(
self.channel.to_string(),
AckEvent::ProcessMessage {
@@ -787,7 +796,7 @@ impl Message {
query: MessageQuery,
perspective: &User,
include_users: Option<bool>,
server_id: Option<String>,
server_id: Option<&str>,
) -> Result<BulkMessageResponse> {
let messages: Vec<v0::Message> = db
.fetch_messages(query)
@@ -830,6 +839,7 @@ impl Message {
v0::SystemMessage::MessageUnpinned { by, .. } => {
users.push(by.clone());
}
v0::SystemMessage::CallStarted { by, .. } => users.push(by.clone()),
}
}
users
@@ -844,7 +854,7 @@ impl Message {
users,
members: if let Some(server_id) = server_id {
Some(
db.fetch_members(&server_id, &user_ids)
db.fetch_members(server_id, &user_ids)
.await?
.into_iter()
.map(Into::into)

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -38,7 +38,10 @@ pub use servers::*;
pub use user_settings::*;
pub use users::*;
use crate::{Database, MongoDb, ReferenceDb};
use crate::{Database, ReferenceDb};
#[cfg(feature = "mongodb")]
use crate::MongoDb;
pub trait AbstractDatabase:
Sync
@@ -66,6 +69,8 @@ pub trait AbstractDatabase:
}
impl AbstractDatabase for ReferenceDb {}
#[cfg(feature = "mongodb")]
impl AbstractDatabase for MongoDb {}
impl std::ops::Deref for Database {
@@ -74,6 +79,7 @@ impl std::ops::Deref for Database {
fn deref(&self) -> &Self::Target {
match &self {
Database::Reference(dummy) => dummy,
#[cfg(feature = "mongodb")]
Database::MongoDb(mongo) => mongo,
}
}

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::PolicyChange;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,6 +1,8 @@
use std::time::Duration;
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Report;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::Snapshot;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{MemberCompositeKey, ServerBan};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,12 +1,21 @@
use iso8601_timestamp::Timestamp;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use crate::voice::get_channel_voice_state;
use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
Server, SystemMessage, User,
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel,
Database, File, Server, SystemMessage, User,
};
fn default_true() -> bool {
true
}
fn is_true(x: &bool) -> bool {
*x
}
auto_derived_partial!(
/// Server Member
pub struct Member {
@@ -30,6 +39,17 @@ auto_derived_partial!(
/// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Timestamp>,
/// Whether the member is server-wide voice muted
#[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_publish: bool,
/// Whether the member is server-wide voice deafened
#[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_receive: bool,
// This value only exists in the database, not the models.
// If it is not-None, the database layer should return None to member fetching queries.
// pub pending_deletion_at: Option<Timestamp>
},
"PartialMember"
);
@@ -50,6 +70,9 @@ auto_derived!(
Avatar,
Roles,
Timeout,
CanReceive,
CanPublish,
JoinedAt,
}
/// Member removal intention
@@ -69,6 +92,8 @@ impl Default for Member {
avatar: None,
roles: vec![],
timeout: None,
can_publish: true,
can_receive: true,
}
}
}
@@ -90,7 +115,7 @@ impl Member {
return Err(create_error!(AlreadyInServer));
}
let member = Member {
let mut member = Member {
id: MemberCompositeKey {
server: server.id.to_string(),
user: user.id.to_string(),
@@ -98,7 +123,9 @@ impl Member {
..Default::default()
};
db.insert_member(&member).await?;
if let Some(updated) = db.insert_or_merge_member(&member).await? {
member = updated;
}
let should_fetch = channels.is_none();
let mut channels = channels.unwrap_or_default();
@@ -121,9 +148,18 @@ impl Member {
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
let mut voice_states = Vec::new();
for channel in &channels {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
voice_states.push(voice_state)
}
}
EventV1::ServerMemberJoin {
id: server.id.clone(),
user: user.id.clone(),
member: member.clone().into(),
}
.p(server.id.clone())
.await;
@@ -137,6 +173,7 @@ impl Member {
.map(|channel| channel.into())
.collect(),
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
voice_states
}
.private(user.id.clone())
.await;
@@ -186,10 +223,13 @@ impl Member {
pub fn remove_field(&mut self, field: &FieldsMember) {
match field {
FieldsMember::JoinedAt => (),
FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None,
FieldsMember::CanReceive => self.can_receive = true,
FieldsMember::CanPublish => self.can_publish = true,
}
}
@@ -224,7 +264,7 @@ impl Member {
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&self.id).await?;
db.soft_delete_member(&self.id).await?;
EventV1::ServerMemberLeave {
id: self.id.server.to_string(),
@@ -260,3 +300,74 @@ impl Member {
Ok(())
}
}
#[cfg(test)]
mod tests {
use iso8601_timestamp::{Duration, Timestamp};
use revolt_models::v0::DataCreateServer;
use crate::{Member, PartialMember, RemovalIntention, Server, User};
#[async_std::test]
async fn muted_member_rejoin() {
database_test!(|db| async move {
match db {
crate::Database::Reference(_) => return,
crate::Database::MongoDb(_) => (),
}
let owner = User::create(&db, "Server Owner".to_string(), None, None)
.await
.unwrap();
let kickable_user = User::create(&db, "Member".to_string(), None, None)
.await
.unwrap();
let server = Server::create(
&db,
DataCreateServer {
name: "Server".to_string(),
description: None,
nsfw: None,
},
&owner,
false,
)
.await
.unwrap()
.0;
Member::create(&db, &server, &owner, None).await.unwrap();
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
kickable_member
.update(
&db,
PartialMember {
timeout: Some(Timestamp::now_utc() + Duration::minutes(5)),
..Default::default()
},
vec![],
)
.await
.unwrap();
assert!(kickable_member.in_timeout());
kickable_member
.remove(&db, &server, RemovalIntention::Kick, false)
.await
.unwrap();
let kickable_member = Member::create(&db, &server, &kickable_user, None)
.await
.unwrap()
.0;
assert!(kickable_member.in_timeout())
});
}
}

View File

@@ -1,16 +1,20 @@
use ::mongodb::SessionCursor;
#[cfg(feature = "mongodb")]
use ::mongodb::{ClientSession, SessionCursor};
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
MongoDb {
session: ::mongodb::ClientSession,
session: ClientSession,
cursor: Option<SessionCursor<Member>>,
},
@@ -22,7 +26,7 @@ pub enum ChunkedServerMembersGenerator {
impl ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
pub fn new_mongo(session: ClientSession, cursor: SessionCursor<Member>) -> Self {
ChunkedServerMembersGenerator::MongoDb {
session,
cursor: Some(cursor),
@@ -69,13 +73,13 @@ impl ChunkedServerMembersGenerator {
#[async_trait]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()>;
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>>;
/// 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>>;
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
@@ -96,10 +100,10 @@ pub trait AbstractServerMembers: Sync + Send {
) -> Result<ChunkedServerMembersGenerator>;
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
async fn fetch_all_memberships(&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>>;
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>>;
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
@@ -115,6 +119,14 @@ pub trait AbstractServerMembers: Sync + Send {
remove: Vec<FieldsMember>,
) -> Result<()>;
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Marks a user as no longer a member of a server, while retaining the database value.
/// This is used to keep information such as timeouts in place, but will remove information such as join date and applied roles.
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Forcibly delete a server member by their id.
/// This will cancel any pending timeouts or other longer term actions, and they will not be reapplied on rejoin.
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
/// Fetch all members who have been marked for deletion.
async fn remove_dangling_members(&self) -> Result<()>;
}

View File

@@ -1,4 +1,6 @@
use bson::Document;
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use mongodb::options::ReadConcern;
use revolt_result::Result;
@@ -11,9 +13,42 @@ 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(|_| ())
/// Insert a new server member (or use the existing member if one is found)
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
let existing: Result<Option<Document>> = query!(
self,
find_one,
COL,
doc! {
"_id.server": &member.id.server,
"_id.user": &member.id.user,
"pending_deletion_at": {"$exists": true}
}
);
// Update the existing record if it exist, otherwise make a new record
if existing.is_ok_and(|x| x.is_some()) {
self.col::<Member>(COL)
.find_one_and_update(
doc! {
"_id.server": &member.id.server,
"_id.user": &member.id.user,
},
doc! {
"$set": {
"joined_at": member.joined_at.duration_since(Timestamp::UNIX_EPOCH).whole_seconds(),
},
"$unset": {
"pending_deletion_at": ""
}
},
)
.return_document(mongodb::options::ReturnDocument::After)
.await
.map_err(|_| create_database_error!("update_one", COL))
} else {
query!(self, insert_one, COL, &member).map(|_| ())?;
Ok(None)
}
}
/// Fetch a server member by their id
@@ -24,18 +59,20 @@ impl AbstractServerMembers for MongoDb {
COL,
doc! {
"_id.server": server_id,
"_id.user": user_id
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
}
)?
.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>> {
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id
"_id.server": server_id,
"pending_deletion_at": {"$exists": false}
})
.await
.map_err(|_| create_database_error!("find", COL))?
@@ -139,11 +176,12 @@ impl AbstractServerMembers for MongoDb {
}
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.user": user_id
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
})
.await
.map_err(|_| create_database_error!("find", COL))?
@@ -159,11 +197,12 @@ impl AbstractServerMembers for MongoDb {
}
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"pending_deletion_at": {"$exists": false},
"_id.user": {
"$in": ids
}
@@ -185,7 +224,8 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(doc! {
"_id.server": server_id
"_id.server": server_id,
"pending_deletion_at": {"$exists": false}
})
.await
.map(|c| c as usize)
@@ -196,7 +236,8 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(doc! {
"_id.user": user_id
"_id.user": user_id,
"pending_deletion_at": {"$exists": false}
})
.await
.map(|c| c as usize)
@@ -225,8 +266,42 @@ impl AbstractServerMembers for MongoDb {
.map(|_| ())
}
/// Marks a member for deletion.
/// This will remove the record if the user has no pending actions (eg. timeout),
/// otherwise will slate the record for deletion by revolt_crond once the actions expire.
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let member = self.fetch_member(&id.server, &id.user).await;
if let Ok(member) = member {
if member.in_timeout() {
self.col::<Document>(COL)
.update_many(
doc! {
"_id.server": &id.server,
"_id.user": &id.user,
},
doc! {
"$set": {"pending_deletion_at": format!("{}", member.timeout.unwrap().format())},
"$unset": {
"joined_at": "",
"avatar": "",
"nickname": "",
"roles": ""
}
},
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
} else {
self.force_delete_member(id).await
}
} else {
Err(create_database_error!("fetch_member", COL))
}
}
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
query!(
self,
delete_one,
@@ -238,15 +313,31 @@ impl AbstractServerMembers for MongoDb {
)
.map(|_| ())
}
async fn remove_dangling_members(&self) -> Result<()> {
let now = Timestamp::now_utc();
let date = bson::to_bson(&now).expect("Failed to serialize timestamp");
self.col::<Document>(COL)
.delete_many(doc! {
"pending_deletion_at": {"$lt": date}
})
.await
.map(|_| ())
.map_err(|_| create_database_error!("count_documents", COL))
}
}
impl IntoDocumentPath for FieldsMember {
fn as_path(&self) -> Option<&'static str> {
Some(match self {
FieldsMember::JoinedAt => "joined_at",
FieldsMember::Avatar => "avatar",
FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout",
FieldsMember::CanPublish => "is_publishing",
FieldsMember::CanReceive => "is_receiving",
})
}
}

View File

@@ -8,13 +8,13 @@ use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
#[async_trait]
impl AbstractServerMembers for ReferenceDb {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()> {
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
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(())
Ok(None)
}
}
@@ -31,7 +31,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
@@ -105,7 +105,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.values()
@@ -115,7 +115,7 @@ impl AbstractServerMembers for ReferenceDb {
}
/// Fetch multiple members by their ids
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(ids
.iter()
@@ -169,8 +169,26 @@ impl AbstractServerMembers for ReferenceDb {
}
}
/// Soft delete a member
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await;
let member = server_members.get_mut(id);
if let Some(member) = member {
if member.in_timeout() {
panic!("Soft deletion is not implemented.")
} else if server_members.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
let mut server_members = self.server_members.lock().await;
if server_members.remove(id).is_some() {
Ok(())
@@ -178,4 +196,8 @@ impl AbstractServerMembers for ReferenceDb {
Err(create_error!(NotFound))
}
}
async fn remove_dangling_members(&self) -> Result<()> {
todo!()
}
}

View File

@@ -181,7 +181,7 @@ impl Server {
}
/// Update server data
pub async fn update<'a>(
pub async fn update(
&mut self,
db: &Database,
partial: PartialServer,
@@ -228,6 +228,13 @@ impl Server {
}
}
/// Ordered roles list
pub fn ordered_roles(&self) -> Vec<(String, Role)> {
let mut ordered_roles = self.roles.clone().into_iter().collect::<Vec<_>>();
ordered_roles.sort_by(|(_, role_a), (_, role_b)| role_a.rank.cmp(&role_b.rank));
ordered_roles
}
/// Set role permission on a server
pub async fn set_role_permission(
&mut self,
@@ -253,6 +260,37 @@ impl Server {
Err(create_error!(NotFound))
}
}
/// Reorders the server's roles rankings
pub async fn set_role_ordering(&mut self, db: &Database, new_order: Vec<String>) -> Result<()> {
// New order must always contain every role
debug_assert_eq!(self.roles.len(), new_order.len());
// Set the role's ranks to the positions in the vec
for (rank, id) in new_order.iter().enumerate() {
self.roles.get_mut(id).unwrap().rank = rank as i64;
}
db.update_server(
&self.id,
&PartialServer {
roles: Some(self.roles.clone()),
..Default::default()
},
Vec::new(),
)
.await?;
// Publish bulk update event
EventV1::ServerRoleRanksUpdate {
id: self.id.clone(),
ranks: new_order,
}
.p(self.id.clone())
.await;
Ok(())
}
}
impl Role {

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -2,6 +2,7 @@ use revolt_result::Result;
use crate::UserSettings;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -1,14 +1,20 @@
use axum::{extract::FromRequestParts, http::request::Parts};
use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts};
use revolt_result::{create_error, Error, Result};
use crate::{Database, User};
#[async_trait::async_trait]
impl FromRequestParts<Database> for User {
impl<S> FromRequestParts<S> for User
where
Database: FromRef<S>,
S: Send + Sync
{
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> {
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> {
let db = Database::from_ref(state);
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
let bot = db.fetch_bot_by_token(bot_token).await?;
db.fetch_user(&bot.id).await

View File

@@ -7,11 +7,5 @@ mod rocket;
#[cfg(feature = "rocket-impl")]
mod schema;
#[cfg(feature = "axum-impl")]
pub use self::axum::*;
#[cfg(feature = "rocket-impl")]
pub use self::rocket::*;
#[cfg(feature = "rocket-impl")]
pub use self::schema::*;
pub use model::*;
pub use ops::*;

View File

@@ -1,8 +1,10 @@
use authifier::models::Session;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
@@ -61,4 +63,6 @@ pub trait AbstractUsers: Sync + Send {
/// Remove push subscription for a session by session id (TODO: remove)
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()>;
async fn update_session_last_seen(&self, session_id: &str, when: Timestamp) -> Result<()>;
}

View File

@@ -1,6 +1,7 @@
use ::mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions};
use authifier::models::Session;
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::DocumentId;
@@ -211,16 +212,34 @@ impl AbstractUsers for MongoDb {
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(|_| ())
if remove.contains(&FieldsUser::StatusText) && partial.status.is_some() {
// stupid-ass workaround to fix mongo conflicting the same item
let _: Result<()> = query!(
self,
update_one_by_id,
COL,
id,
PartialUser {
..Default::default()
},
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
None
)
.map(|_| ());
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
} else {
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
@@ -317,7 +336,26 @@ impl AbstractUsers for MongoDb {
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
.map_err(|_| create_database_error!("update_one", "sessions"))
}
async fn update_session_last_seen(&self, session_id: &str, when: Timestamp) -> Result<()> {
let formatted: &str = &when.format();
self.col::<Session>("sessions")
.update_one(
doc! {
"_id": session_id
},
doc! {
"$set": {
"last_seen": formatted
}
},
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "sessions"))
}
}

View File

@@ -1,4 +1,5 @@
use authifier::models::Session;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
@@ -168,4 +169,8 @@ impl AbstractUsers for ReferenceDb {
async fn remove_push_subscription_by_session_id(&self, _session_id: &str) -> Result<()> {
todo!()
}
async fn update_session_last_seen(&self, _session_id: &str, _when: Timestamp) -> Result<()> {
todo!()
}
}

View File

@@ -14,7 +14,7 @@ use validator::HasLen;
use revolt_result::Result;
use super::DelayedTask;
use crate::Channel::{TextChannel, VoiceChannel};
use crate::Channel::TextChannel;
/// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)]
@@ -191,17 +191,14 @@ pub async fn handle_ack_event(
.await
.expect("Failed to fetch channel from db");
match channel {
TextChannel { server, .. } | VoiceChannel { server, .. } => {
if let Err(err) =
amqp.mass_mention_message_sent(server, mass_mentions).await
{
revolt_config::capture_error(&err);
}
}
_ => {
panic!("Unknown channel type when sending mass mention event");
if let TextChannel { server, .. } = channel {
if let Err(err) =
amqp.mass_mention_message_sent(server, mass_mentions).await
{
revolt_config::capture_error(&err);
}
} else {
panic!("Unknown channel type when sending mass mention event");
}
}
}

View File

@@ -4,7 +4,7 @@ use once_cell::sync::Lazy;
use crate::events::client::EventV1;
static Q: Lazy<(Sender<AuthifierEvent>, Receiver<AuthifierEvent>)> = Lazy::new(|| unbounded());
static Q: Lazy<(Sender<AuthifierEvent>, Receiver<AuthifierEvent>)> = Lazy::new(unbounded);
/// Get sender
pub fn sender() -> Sender<AuthifierEvent> {

View File

@@ -15,8 +15,7 @@ impl crate::Bot {
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
description: user
.profile
.map(|profile| profile.content)
.flatten()
.and_then(|profile| profile.content)
.unwrap_or_default(),
}
}
@@ -144,6 +143,7 @@ impl From<crate::FieldsWebhook> for FieldsWebhook {
}
impl From<crate::Channel> for Channel {
#[allow(deprecated)]
fn from(value: crate::Channel) -> Self {
match value {
crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user },
@@ -189,6 +189,7 @@ impl From<crate::Channel> for Channel {
default_permissions,
role_permissions,
nsfw,
voice,
} => Channel::TextChannel {
id,
server,
@@ -199,31 +200,14 @@ impl From<crate::Channel> for Channel {
default_permissions,
role_permissions,
nsfw,
},
crate::Channel::VoiceChannel {
id,
server,
name,
description,
icon,
default_permissions,
role_permissions,
nsfw,
} => Channel::VoiceChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
nsfw,
voice: voice.map(|voice| voice.into()),
},
}
}
}
impl From<Channel> for crate::Channel {
#[allow(deprecated)]
fn from(value: Channel) -> crate::Channel {
match value {
Channel::SavedMessages { id, user } => crate::Channel::SavedMessages { id, user },
@@ -269,6 +253,7 @@ impl From<Channel> for crate::Channel {
default_permissions,
role_permissions,
nsfw,
voice,
} => crate::Channel::TextChannel {
id,
server,
@@ -279,25 +264,7 @@ impl From<Channel> for crate::Channel {
default_permissions,
role_permissions,
nsfw,
},
Channel::VoiceChannel {
id,
server,
name,
description,
icon,
default_permissions,
role_permissions,
nsfw,
} => crate::Channel::VoiceChannel {
id,
server,
name,
description,
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
nsfw,
voice: voice.map(|voice| voice.into()),
},
}
}
@@ -316,6 +283,7 @@ impl From<crate::PartialChannel> for PartialChannel {
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
voice: value.voice.map(|voice| voice.into())
}
}
}
@@ -333,6 +301,7 @@ impl From<PartialChannel> for crate::PartialChannel {
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
voice: value.voice.map(|voice| voice.into())
}
}
}
@@ -343,6 +312,7 @@ impl From<FieldsChannel> for crate::FieldsChannel {
FieldsChannel::Description => crate::FieldsChannel::Description,
FieldsChannel::Icon => crate::FieldsChannel::Icon,
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
FieldsChannel::Voice => crate::FieldsChannel::Voice,
}
}
}
@@ -353,6 +323,7 @@ impl From<crate::FieldsChannel> for FieldsChannel {
crate::FieldsChannel::Description => FieldsChannel::Description,
crate::FieldsChannel::Icon => FieldsChannel::Icon,
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
crate::FieldsChannel::Voice => FieldsChannel::Voice,
}
}
}
@@ -544,6 +515,7 @@ impl From<crate::SystemMessage> for SystemMessage {
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by },
crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by },
crate::SystemMessage::CallStarted { by, finished_at } => Self::CallStarted { by, finished_at }
}
}
}
@@ -640,6 +612,8 @@ impl From<crate::Member> for Member {
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
can_publish: value.can_publish,
can_receive: value.can_receive,
}
}
}
@@ -653,6 +627,8 @@ impl From<Member> for crate::Member {
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
can_publish: value.can_publish,
can_receive: value.can_receive,
}
}
}
@@ -666,6 +642,8 @@ impl From<crate::PartialMember> for PartialMember {
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
can_publish: value.can_publish,
can_receive: value.can_receive,
}
}
}
@@ -679,6 +657,8 @@ impl From<PartialMember> for crate::PartialMember {
avatar: value.avatar.map(|f| f.into()),
roles: value.roles,
timeout: value.timeout,
can_publish: value.can_publish,
can_receive: value.can_receive,
}
}
}
@@ -708,6 +688,9 @@ impl From<crate::FieldsMember> for FieldsMember {
crate::FieldsMember::Nickname => FieldsMember::Nickname,
crate::FieldsMember::Roles => FieldsMember::Roles,
crate::FieldsMember::Timeout => FieldsMember::Timeout,
crate::FieldsMember::CanReceive => FieldsMember::CanReceive,
crate::FieldsMember::CanPublish => FieldsMember::CanPublish,
crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt,
}
}
}
@@ -719,6 +702,9 @@ impl From<FieldsMember> for crate::FieldsMember {
FieldsMember::Nickname => crate::FieldsMember::Nickname,
FieldsMember::Roles => crate::FieldsMember::Roles,
FieldsMember::Timeout => crate::FieldsMember::Timeout,
FieldsMember::CanReceive => crate::FieldsMember::CanReceive,
FieldsMember::CanPublish => crate::FieldsMember::CanPublish,
FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt,
}
}
}
@@ -1114,7 +1100,7 @@ impl crate::User {
}
/// Convert user object into user model without presence information
pub async fn into_known_static<'a>(self, is_online: bool) -> User {
pub async fn into_known_static(self, is_online: bool) -> User {
let badges = self.get_badges().await;
User {
@@ -1386,3 +1372,19 @@ impl From<FieldsMessage> for crate::FieldsMessage {
}
}
}
impl From<VoiceInformation> for crate::VoiceInformation {
fn from(value: VoiceInformation) -> Self {
crate::VoiceInformation {
max_users: value.max_users
}
}
}
impl From<crate::VoiceInformation> for VoiceInformation {
fn from(value: crate::VoiceInformation) -> Self {
VoiceInformation {
max_users: value.max_users
}
}
}

View File

@@ -144,10 +144,6 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
Channel::TextChannel {
default_permissions,
..
}
| Channel::VoiceChannel {
default_permissions,
..
} => default_permissions.unwrap_or_default().into(),
_ => Default::default(),
}
@@ -156,16 +152,14 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
}
}
#[allow(dead_code)]
#[allow(dead_code, deprecated)]
fn get_channel_type(&mut self) -> ChannelType {
if let Some(channel) = &self.channel {
match channel {
Channel::DirectMessage { .. } => ChannelType::DirectMessage,
Channel::Group { .. } => ChannelType::Group,
Channel::SavedMessages { .. } => ChannelType::SavedMessages,
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
ChannelType::ServerChannel
}
Channel::TextChannel { .. } => ChannelType::ServerChannel,
}
} else {
ChannelType::Unknown
@@ -179,9 +173,6 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
match channel {
Channel::TextChannel {
role_permissions, ..
}
| Channel::VoiceChannel {
role_permissions, ..
} => role_permissions,
_ => panic!("Not supported for non-server channels"),
}
@@ -208,12 +199,6 @@ async fn calculate_members_permissions<'a>(
role_permissions,
default_permissions,
..
}
| Channel::VoiceChannel {
id,
role_permissions,
default_permissions,
..
} => (id, role_permissions, default_permissions),
_ => panic!("Calculation of member permissions must be done on a server channel"),
};

View File

@@ -0,0 +1,24 @@
use crate::Database;
use revolt_result::Result;
/// Formats a user's name depending on their optional features and location.
/// Factors in server display names and user display names before falling back to username#discriminator.
/// Passing a server in which the user is not a member will result in an Err.
pub async fn format_display_name(
db: &Database,
user_id: &str,
server_id: Option<&str>,
) -> Result<String> {
if let Some(server_id) = server_id {
let member = db.fetch_member(server_id, user_id).await?;
if let Some(nick) = member.nickname {
return Ok(nick);
}
}
let user = db.fetch_user(user_id).await?;
if let Some(display) = user.display_name {
return Ok(display);
}
Ok(format!("{}#{}", user.username, user.discriminator))
}

View File

@@ -54,7 +54,7 @@ use revolt_rocket_okapi::{
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
#[cfg(feature = "rocket-impl")]
impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
impl OpenApiFromRequest<'_> for IdempotencyKey {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,

View File

@@ -1,6 +1,9 @@
pub mod bridge;
pub mod bulk_permissions;
mod funcs;
pub mod idempotency;
pub mod permissions;
pub mod reference;
pub mod test_fixtures;
pub use funcs::*;

View File

@@ -104,7 +104,7 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
.unwrap_or_default();
self.cached_mutual_connection = Some(value);
matches!(value, true)
value
} else {
false
}
@@ -185,9 +185,26 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
}
}
async fn do_we_have_publish_overwrites(&mut self) -> bool {
if let Some(member) = &self.member {
member.can_publish
} else {
true
}
}
async fn do_we_have_receive_overwrites(&mut self) -> bool {
if let Some(member) = &self.member {
member.can_receive
} else {
true
}
}
// * For calculating channel permission
/// Get the type of the channel
#[allow(deprecated)]
async fn get_channel_type(&mut self) -> ChannelType {
if let Some(channel) = &self.channel {
match channel {
@@ -199,9 +216,7 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
Cow::Borrowed(Channel::SavedMessages { .. })
| Cow::Owned(Channel::SavedMessages { .. }) => ChannelType::SavedMessages,
Cow::Borrowed(Channel::TextChannel { .. })
| Cow::Owned(Channel::TextChannel { .. })
| Cow::Borrowed(Channel::VoiceChannel { .. })
| Cow::Owned(Channel::VoiceChannel { .. }) => ChannelType::ServerChannel,
| Cow::Owned(Channel::TextChannel { .. }) => ChannelType::ServerChannel,
}
} else {
ChannelType::Unknown
@@ -225,14 +240,6 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
| Cow::Owned(Channel::TextChannel {
default_permissions,
..
})
| Cow::Borrowed(Channel::VoiceChannel {
default_permissions,
..
})
| Cow::Owned(Channel::VoiceChannel {
default_permissions,
..
}) => default_permissions.unwrap_or_default().into(),
_ => Default::default(),
}
@@ -250,12 +257,6 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
})
| Cow::Owned(Channel::TextChannel {
role_permissions, ..
})
| Cow::Borrowed(Channel::VoiceChannel {
role_permissions, ..
})
| Cow::Owned(Channel::VoiceChannel {
role_permissions, ..
}) => {
if let Some(server) = &self.server {
let member_roles = self
@@ -343,11 +344,10 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
/// (this will only ever be called for server channels, use unimplemented!() for other code paths)
async fn set_server_from_channel(&mut self) {
if let Some(channel) = &self.channel {
#[allow(deprecated)]
match channel {
Cow::Borrowed(Channel::TextChannel { server, .. })
| Cow::Owned(Channel::TextChannel { server, .. })
| Cow::Borrowed(Channel::VoiceChannel { server, .. })
| Cow::Owned(Channel::VoiceChannel { server, .. }) => {
| Cow::Owned(Channel::TextChannel { server, .. }) => {
if let Some(known_server) =
// I'm not sure why I can't just pattern match both at once here?
// It throws some weird error and the provided fix doesn't work :/

View File

@@ -14,41 +14,40 @@ use crate::{
};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
pub struct Reference {
pub struct Reference<'a> {
/// Id of object
pub id: String,
pub id: &'a str,
}
impl Reference {
impl<'a> Reference<'a> {
/// Create a Ref from an unchecked string
pub fn from_unchecked(id: String) -> Reference {
pub fn from_unchecked(id: &'a str) -> Reference<'a> {
Reference { id }
}
/// Fetch ban from Ref
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
db.fetch_ban(server, &self.id).await
db.fetch_ban(server, self.id).await
}
/// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await
db.fetch_bot(self.id).await
}
/// Fetch emoji from Ref
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
db.fetch_emoji(&self.id).await
db.fetch_emoji(self.id).await
}
/// Fetch channel from Ref
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
db.fetch_channel(&self.id).await
db.fetch_channel(self.id).await
}
/// Fetch invite from Ref or create invite to server if discoverable
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
if ulid::Ulid::from_str(&self.id).is_ok() {
if ulid::Ulid::from_str(self.id).is_ok() {
let server = self.as_server(db).await?;
if !server.discoverable {
return Err(create_error!(NotFound));
@@ -65,18 +64,18 @@ impl Reference {
.ok_or(create_error!(NotFound))?,
})
} else {
db.fetch_invite(&self.id).await
db.fetch_invite(self.id).await
}
}
/// Fetch message from Ref
pub async fn as_message(&self, db: &Database) -> Result<Message> {
db.fetch_message(&self.id).await
db.fetch_message(self.id).await
}
/// Fetch message from Ref and validate channel
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
let msg = db.fetch_message(&self.id).await?;
let msg = db.fetch_message(self.id).await?;
if msg.channel != channel {
return Err(create_error!(NotFound));
}
@@ -86,36 +85,36 @@ impl Reference {
/// Fetch member from Ref
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
db.fetch_member(server, &self.id).await
db.fetch_member(server, self.id).await
}
/// Fetch server from Ref
pub async fn as_server(&self, db: &Database) -> Result<Server> {
db.fetch_server(&self.id).await
db.fetch_server(self.id).await
}
/// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> {
db.fetch_user(&self.id).await
db.fetch_user(self.id).await
}
/// Fetch webhook from Ref
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
db.fetch_webhook(&self.id).await
db.fetch_webhook(self.id).await
}
}
#[cfg(feature = "rocket-impl")]
impl<'r> FromParam<'r> for Reference {
impl<'r> FromParam<'r> for Reference<'r> {
type Error = &'r str;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
Ok(Reference::from_unchecked(param.into()))
Ok(Reference::from_unchecked(param))
}
}
#[cfg(feature = "rocket-impl")]
impl JsonSchema for Reference {
impl<'a> JsonSchema for Reference<'a> {
fn schema_name() -> String {
"Id".to_string()
}

View File

@@ -39,7 +39,9 @@ pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String>
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
LoadedFixture::ServerMember(member) => {
db.insert_or_merge_member(&member).await.unwrap();
}
}
}

View File

@@ -0,0 +1,566 @@
use crate::{
events::client::EventV1,
models::{Channel, User},
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, Server,
};
use iso8601_timestamp::{Duration, Timestamp};
use livekit_protocol::ParticipantPermission;
use redis_kiss::{get_connection as _get_connection, redis::Pipeline, AsyncCommands, Conn};
use revolt_config::FeaturesLimits;
use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
use revolt_result::{create_error, Result, ToRevoltError};
mod voice_client;
pub use voice_client::VoiceClient;
async fn get_connection() -> Result<Conn> {
_get_connection().await.map_err(|_| create_error!(InternalError))
}
pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
let mut conn = get_connection().await?;
if user.bot.is_some()
// bots can be in as many voice channels as it wants so we just check if its already connected to the one its trying to connect to
&& conn.sismember(format!("vc:{}", &user.id), channel_id)
.await
.to_internal_error()?
{
Err(create_error!(AlreadyConnected))
} else if conn
.scard::<_, u32>(format!("vc:{}", &user.id)) // check if the current vc set is empty
.await
.to_internal_error()?
> 0
{
Err(create_error!(AlreadyConnected))
} else {
Ok(())
}
}
pub async fn set_channel_node(channel: &str, node: &str) -> Result<()> {
get_connection()
.await?
.set(format!("node:{channel}"), node)
.await
.to_internal_error()
}
pub async fn get_channel_node(channel: &str) -> Result<Option<String>> {
get_connection()
.await?
.get(format!("node:{channel}"))
.await
.to_internal_error()
}
pub async fn get_user_voice_channels(user_id: &str) -> Result<Vec<String>> {
get_connection()
.await?
.smembers(format!("vc:{user_id}"))
.await
.to_internal_error()
}
pub async fn set_user_moved_from_voice(
old_channel: &str,
new_channel: &str,
user_id: &str,
) -> Result<()> {
get_connection()
.await?
.set_ex(
format!("moved_from:{user_id}:{old_channel}"),
new_channel,
10,
)
.await
.to_internal_error()
}
pub async fn get_user_moved_from_voice(channel_id: &str, user_id: &str) -> Result<Option<String>> {
get_connection()
.await?
.get_del(format!("moved_from:{user_id}:{channel_id}"))
.await
.to_internal_error()
}
pub async fn set_user_moved_to_voice(
new_channel: &str,
old_channel: &str,
user_id: &str,
) -> Result<()> {
get_connection()
.await?
.set_ex(format!("moved_to:{user_id}:{new_channel}"), old_channel, 10)
.await
.to_internal_error()
}
pub async fn get_user_moved_to_voice(channel_id: &str, user_id: &str) -> Result<Option<String>> {
get_connection()
.await?
.get_del(format!("moved_to:{user_id}:{channel_id}"))
.await
.to_internal_error()
}
pub async fn is_in_voice_channel(user_id: &str, channel_id: &str) -> Result<bool> {
get_connection()
.await?
.sismember(format!("vc:{user_id}"), channel_id)
.await
.to_internal_error()
}
pub async fn get_user_voice_channel_in_server(
user_id: &str,
server_id: &str,
) -> Result<Option<String>> {
let mut conn = get_connection().await?;
let unique_key = format!("{user_id}:{server_id}");
conn.get(&unique_key).await.to_internal_error()
}
pub fn get_allowed_sources(
limits: &FeaturesLimits,
permissions: PermissionValue,
) -> Vec<&'static str> {
let mut allowed_sources = Vec::new();
if permissions.has(ChannelPermission::Speak as u64) {
allowed_sources.push("microphone")
};
if permissions.has(ChannelPermission::Video as u64) && limits.video {
allowed_sources.extend(["camera", "screen_share", "screen_share_audio"]);
};
allowed_sources
}
pub async fn create_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
joined_at: Timestamp,
) -> Result<UserVoiceState> {
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
let voice_state = UserVoiceState {
joined_at,
id: user_id.to_string(),
is_receiving: true,
is_publishing: false,
screensharing: false,
camera: false,
};
Pipeline::new()
.sadd(format!("vc_members:{channel_id}"), user_id)
.sadd(format!("vc:{user_id}"), channel_id)
.set(&unique_key, channel_id)
.set(
format!("joined_at:{unique_key}"),
joined_at
.duration_since(Timestamp::UNIX_EPOCH)
.whole_milliseconds() as i64,
)
.set(
format!("is_publishing:{unique_key}"),
voice_state.is_publishing,
)
.set(
format!("is_receiving:{unique_key}"),
voice_state.is_receiving,
)
.set(
format!("screensharing:{unique_key}"),
voice_state.screensharing,
)
.set(format!("camera:{unique_key}"), voice_state.camera)
.query_async::<_, ()>(&mut get_connection().await?.into_inner())
.await
.to_internal_error()?;
Ok(voice_state)
}
pub async fn delete_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
) -> Result<()> {
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
Pipeline::new()
.srem(format!("vc_members:{channel_id}"), user_id)
.srem(format!("vc:{user_id}"), channel_id)
.del(&[
format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"),
format!("is_receiving:{unique_key}"),
format!("screensharing:{unique_key}"),
format!("camera:{unique_key}"),
unique_key.clone(),
])
.query_async(&mut get_connection().await?.into_inner())
.await
.to_internal_error()
}
pub async fn delete_channel_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_ids: &[String],
) -> Result<()> {
let parent_id = server_id.unwrap_or(channel_id);
let mut pipeline = Pipeline::new();
pipeline.del(format!("vc_members:{channel_id}"));
for user_id in user_ids {
let unique_key = format!("{user_id}:{parent_id}");
pipeline.srem(format!("vc:{user_id}"), channel_id).del(&[
format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"),
format!("is_receiving:{unique_key}"),
format!("screensharing:{unique_key}"),
format!("camera:{unique_key}"),
unique_key.clone(),
]);
}
pipeline
.query_async(&mut get_connection().await?.into_inner())
.await
.to_internal_error()
}
pub async fn update_voice_state_tracks(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
added: bool,
track: i32,
) -> Result<PartialUserVoiceState> {
let partial = match track {
/* TrackSource::Unknown */ 0 => PartialUserVoiceState::default(),
/* TrackSource::Camera */
1 => PartialUserVoiceState {
camera: Some(added),
..Default::default()
},
/* TrackSource::Microphone */
2 => PartialUserVoiceState {
is_publishing: Some(added),
..Default::default()
},
/* TrackSource::ScreenShare | TrackSource::ScreenShareAudio */
3 | 4 => PartialUserVoiceState {
screensharing: Some(added),
..Default::default()
},
_ => unreachable!(),
};
update_voice_state(channel_id, server_id, user_id, &partial).await?;
Ok(partial)
}
pub async fn update_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
partial: &PartialUserVoiceState,
) -> Result<()> {
let unique_key = format!("{}:{}", &user_id, server_id.unwrap_or(channel_id));
let mut pipeline = Pipeline::new();
if let Some(camera) = &partial.camera {
pipeline.set(format!("camera:{unique_key}"), camera);
};
if let Some(is_publishing) = &partial.is_publishing {
pipeline.set(format!("is_publishing:{unique_key}"), is_publishing);
}
if let Some(is_receiving) = &partial.is_receiving {
pipeline.set(format!("is_receiving:{unique_key}"), is_receiving);
}
if let Some(screensharing) = &partial.screensharing {
pipeline.set(format!("screensharing:{unique_key}"), screensharing);
}
pipeline
.query_async(&mut get_connection().await?.into_inner())
.await
.to_internal_error()
}
pub async fn get_voice_channel_members(channel_id: &str) -> Result<Option<Vec<String>>> {
get_connection()
.await?
.smembers::<_, Option<Vec<String>>>(format!("vc_members:{channel_id}"))
.await
.to_internal_error()
.map(|opt| opt.and_then(|v| if v.is_empty() { None } else { Some(v) }))
}
pub async fn get_voice_state(
channel_id: &str,
server_id: Option<&str>,
user_id: &str,
) -> Result<Option<UserVoiceState>> {
let unique_key = format!("{}:{}", user_id, server_id.unwrap_or(channel_id));
let (joined_at, is_publishing, is_receiving, screensharing, camera) = get_connection()
.await?
.mget(&[
format!("joined_at:{unique_key}"),
format!("is_publishing:{unique_key}"),
format!("is_receiving:{unique_key}"),
format!("screensharing:{unique_key}"),
format!("camera:{unique_key}"),
])
.await
.to_internal_error()?;
match (
joined_at,
is_publishing,
is_receiving,
screensharing,
camera,
) {
(
Some(joined_at),
Some(is_publishing),
Some(is_receiving),
Some(screensharing),
Some(camera),
) => Ok(Some(v0::UserVoiceState {
joined_at: Timestamp::UNIX_EPOCH
.checked_add(Duration::milliseconds(joined_at))
.unwrap(),
id: user_id.to_string(),
is_receiving,
is_publishing,
screensharing,
camera,
})),
_ => Ok(None),
}
}
pub async fn get_channel_voice_state(channel: &Channel) -> Result<Option<v0::ChannelVoiceState>> {
let members = get_voice_channel_members(channel.id()).await?;
let server = channel.server();
if let Some(members) = members {
let mut participants = Vec::with_capacity(members.len());
for user_id in members {
if let Some(voice_state) = get_voice_state(channel.id(), server, &user_id).await? {
participants.push(voice_state);
} else {
log::info!("Voice state not found but member in voice channel members, removing.");
delete_voice_state(channel.id(), server, &user_id).await?;
}
}
// In case a user voice state failed to be fetched, the vec's capacity will be larger than the length, shrink it
participants.shrink_to_fit();
Ok(Some(v0::ChannelVoiceState {
id: channel.id().to_string(),
participants,
}))
} else {
Ok(None)
}
}
pub async fn move_user(user: &str, from: &str, to: &str) -> Result<()> {
get_connection()
.await?
.smove(
format!("vc-members-{from}"),
format!("vc-members-{to}"),
user,
)
.await
.to_internal_error()
}
pub async fn sync_voice_permissions(
db: &Database,
voice_client: &VoiceClient,
channel: &Channel,
server: Option<&Server>,
role_id: Option<&str>,
) -> Result<()> {
let Some(node) = get_channel_node(channel.id()).await? else {
return Ok(());
};
for user_id in get_voice_channel_members(channel.id())
.await?
.iter()
.flatten()
{
let user = Reference::from_unchecked(user_id).as_user(db).await?;
sync_user_voice_permissions(db, voice_client, &node, &user, channel, server, role_id)
.await?;
}
Ok(())
}
pub async fn sync_user_voice_permissions(
db: &Database,
voice_client: &VoiceClient,
node: &str,
user: &User,
channel: &Channel,
server: Option<&Server>,
role_id: Option<&str>,
) -> Result<()> {
let channel_id = channel.id();
let server_id = server.as_ref().map(|s| s.id.as_str());
let member = match server_id {
Some(server_id) => Some(
Reference::from_unchecked(&user.id)
.as_member(db, server_id)
.await?,
),
None => None,
};
if role_id.is_none_or(|role_id| {
member
.as_ref()
.is_none_or(|member| member.roles.iter().any(|r| r == role_id))
}) {
let Some(voice_state) = get_voice_state(channel_id, server_id, &user.id).await? else {
return Ok(());
};
let mut query = DatabasePermissionQuery::new(db, user)
.channel(channel)
.user(user);
if let (Some(server), Some(member)) = (server, member.as_ref()) {
query = query.member(member).server(server)
}
let permissions = calculate_channel_permissions(&mut query).await;
let limits = user.limits().await;
let mut update_event = PartialUserVoiceState {
id: Some(user.id.clone()),
..Default::default()
};
let before = update_event.clone();
let can_video =
limits.video && permissions.has_channel_permission(ChannelPermission::Video);
let can_speak = permissions.has_channel_permission(ChannelPermission::Speak);
let can_listen = permissions.has_channel_permission(ChannelPermission::Listen);
update_event.camera = voice_state.camera.then_some(can_video);
update_event.screensharing = voice_state.screensharing.then_some(can_video);
update_event.is_publishing = voice_state.is_publishing.then_some(can_speak);
update_voice_state(channel_id, server_id, &user.id, &update_event).await?;
voice_client
.update_permissions(
node,
user,
channel_id,
ParticipantPermission {
can_subscribe: can_listen,
can_publish: can_speak,
can_publish_data: can_speak,
..Default::default()
},
)
.await?;
if update_event != before {
EventV1::UserVoiceStateUpdate {
id: user.id.clone(),
channel_id: channel_id.to_string(),
data: update_event,
}
.p(channel_id.to_string())
.await;
};
};
Ok(())
}
pub async fn set_channel_call_started_system_message(
channel_id: &str,
message_id: &str,
) -> Result<()> {
get_connection()
.await?
.set(format!("call_started_message:{channel_id}"), message_id)
.await
.to_internal_error()
}
pub async fn take_channel_call_started_system_message(channel_id: &str) -> Result<Option<String>> {
get_connection()
.await?
.get_del(format!("call_started_message:{channel_id}"))
.await
.to_internal_error()
}
pub async fn set_call_notification_recipients(
channel_id: &str,
user_id: &str,
recipients: &[String],
) -> Result<()> {
get_connection()
.await?
.set_ex(
format!("call_notification_recipients:{channel_id}-{user_id}"),
recipients,
10,
)
.await
.to_internal_error()
}
pub async fn get_call_notification_recipients(
channel_id: &str,
user_id: &str,
) -> Result<Option<Vec<String>>> {
get_connection()
.await?
.get_del(format!(
"call_notification_recipients:{channel_id}-{user_id}"
))
.await
.to_internal_error()
}

View File

@@ -0,0 +1,156 @@
use crate::{
models::{Channel, User},
Database,
};
use livekit_api::{
access_token::{AccessToken, VideoGrants},
services::room::{CreateRoomOptions, RoomClient as InnerRoomClient, UpdateParticipantOptions},
};
use livekit_protocol::{ParticipantInfo, ParticipantPermission, Room};
use revolt_config::{config, LiveKitNode};
use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::{create_error, Result, ToRevoltError};
use std::{collections::HashMap, time::Duration};
use super::get_allowed_sources;
#[derive(Debug)]
pub struct RoomClient {
pub client: InnerRoomClient,
pub node: LiveKitNode,
}
#[derive(Debug)]
pub struct VoiceClient {
pub rooms: HashMap<String, RoomClient>,
}
impl VoiceClient {
pub fn new(nodes: HashMap<String, LiveKitNode>) -> Self {
Self {
rooms: nodes
.into_iter()
.map(|(name, node)| {
(
name,
RoomClient {
client: InnerRoomClient::with_api_key(
&node.url,
&node.key,
&node.secret,
),
node,
},
)
})
.collect(),
}
}
pub fn is_enabled(&self) -> bool {
!self.rooms.is_empty()
}
pub async fn from_revolt_config() -> Self {
let config = config().await;
Self::new(config.api.livekit.nodes.clone())
}
pub fn get_node(&self, name: &str) -> Result<&RoomClient> {
self.rooms
.get(name)
.ok_or_else(|| create_error!(UnknownNode))
}
pub async fn create_token(
&self,
node: &str,
db: &Database,
user: &User,
permissions: PermissionValue,
channel: &Channel,
) -> Result<String> {
let room = self.get_node(node)?;
let limits = user.limits().await;
let allowed_sources = get_allowed_sources(&limits, permissions);
AccessToken::with_api_key(&room.node.key, &room.node.secret)
.with_name(&format!("{}#{}", user.username, user.discriminator))
.with_identity(&user.id)
.with_metadata(
&serde_json::to_string(&user.clone().into(db, None).await).to_internal_error()?,
)
.with_ttl(Duration::from_secs(10))
.with_grants(VideoGrants {
room_join: true,
can_publish: true,
can_publish_data: false,
can_publish_sources: allowed_sources
.into_iter()
.map(ToString::to_string)
.collect(),
can_subscribe: permissions.has_channel_permission(ChannelPermission::Listen),
room: channel.id().to_string(),
..Default::default()
})
.to_jwt()
.to_internal_error()
}
pub async fn create_room(&self, node: &str, channel: &Channel) -> Result<Room> {
let room = self.get_node(node)?;
room.client
.create_room(
channel.id(),
CreateRoomOptions {
empty_timeout: 5 * 60, // 5 minutes,
..Default::default()
},
)
.await
.to_internal_error()
}
pub async fn update_permissions(
&self,
node: &str,
user: &User,
channel_id: &str,
new_permissions: ParticipantPermission,
) -> Result<ParticipantInfo> {
let room = self.get_node(node)?;
room.client
.update_participant(
channel_id,
&user.id,
UpdateParticipantOptions {
permission: Some(new_permissions),
..Default::default()
},
)
.await
.to_internal_error()
}
pub async fn remove_user(&self, node: &str, user_id: &str, channel_id: &str) -> Result<()> {
let room = self.get_node(node)?;
room.client
.remove_participant(channel_id, user_id)
.await
.to_internal_error()
}
pub async fn delete_room(&self, node: &str, channel_id: &str) -> Result<()> {
let room = self.get_node(node)?;
room.client
.delete_room(channel_id)
.await
.to_internal_error()
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-files"
version = "0.8.7"
version = "0.8.9"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,10 +20,10 @@ typenum = "1.17.0"
aws-config = "1.5.5"
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.8.7", path = "../config", features = [
revolt-config = { version = "0.8.9", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.7", path = "../result" }
revolt-result = { version = "0.8.9", path = "../result" }
# image processing
jxl-oxide = "0.8.1"

View File

@@ -80,6 +80,9 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
.decrypt_in_place(nonce, b"", &mut buf)
.map_err(|_| create_error!(InternalError))?;
// Remove the authentication tag bytes that were added during encryption
buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES);
Ok(buf)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.8.7"
version = "0.8.9"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,8 +20,8 @@ default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.8.7", path = "../config" }
revolt-permissions = { version = "0.8.7", path = "../permissions" }
revolt-config = { version = "0.8.9", path = "../config" }
revolt-permissions = { version = "0.8.9", path = "../permissions" }
# Utility
regex = "1.11"

View File

@@ -132,8 +132,8 @@ auto_derived!(
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
pub interactions_url: Option<String>,
/// Fields to remove from bot object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsBot>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsBot>,
}
/// Where we are inviting a bot to

View File

@@ -1,4 +1,5 @@
use super::File;
#![allow(deprecated)]
use super::{File, UserVoiceState};
use revolt_permissions::{Override, OverrideField};
use std::collections::{HashMap, HashSet};
@@ -107,44 +108,21 @@ auto_derived!(
serde(skip_serializing_if = "crate::if_false", default)
)]
nsfw: bool,
/// Voice Information for when this channel is also a voice channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
voice: Option<VoiceInformation>,
},
/// Voice channel belonging to a server
VoiceChannel {
/// Unique Id
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
id: String,
/// Id of the server this channel belongs to
server: String,
}
/// Display name of the channel
name: String,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
/// Channel description
description: Option<String>,
/// Custom icon attachment
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
icon: Option<File>,
/// Default permissions assigned to users in this channel
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
default_permissions: Option<OverrideField>,
/// Permissions assigned based on role to this channel
#[cfg_attr(
feature = "serde",
serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)
)]
role_permissions: HashMap<String, OverrideField>,
/// Whether this channel is marked as not safe for work
#[cfg_attr(
feature = "serde",
serde(skip_serializing_if = "crate::if_false", default)
)]
nsfw: bool,
},
/// Voice information for a channel
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(validator::Validate))]
pub struct VoiceInformation {
/// Maximium amount of users allowed in the voice channel at once
#[cfg_attr(feature = "validator", validate(range(min = 1)))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub max_users: Option<usize>,
}
/// Partial representation of a channel
@@ -170,6 +148,8 @@ auto_derived!(
pub default_permissions: Option<OverrideField>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub last_message_id: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub voice: Option<VoiceInformation>,
}
/// Optional fields on channel object
@@ -177,6 +157,7 @@ auto_derived!(
Description,
Icon,
DefaultPermissions,
Voice,
}
/// New webhook information
@@ -205,9 +186,12 @@ auto_derived!(
/// Whether this channel is archived
pub archived: Option<bool>,
/// Voice Information for voice channels
pub voice: Option<VoiceInformation>,
/// Fields to remove from channel
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Option<Vec<FieldsChannel>>,
pub remove: Vec<FieldsChannel>,
}
/// Create new group
@@ -260,6 +244,10 @@ auto_derived!(
/// Whether this channel is age restricted
#[serde(skip_serializing_if = "Option::is_none")]
pub nsfw: Option<bool>,
/// Voice Information for when this channel is also a voice channel
#[serde(skip_serializing_if = "Option::is_none")]
pub voice: Option<VoiceInformation>,
}
/// New default permissions
@@ -270,7 +258,7 @@ auto_derived!(
permissions: u64,
},
Field {
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
/// Allow / deny values to set for members in this server channel
permissions: Override,
},
}
@@ -289,9 +277,32 @@ auto_derived!(
}
/// Voice server token response
pub struct LegacyCreateVoiceUserResponse {
pub struct CreateVoiceUserResponse {
/// Token for authenticating with the voice server
token: String,
pub token: String,
/// Url of the livekit server to connect to
pub url: String,
}
/// Voice state for a channel
pub struct ChannelVoiceState {
pub id: String,
/// The states of the users who are connected to the channel
pub participants: Vec<UserVoiceState>,
}
/// Join a voice channel
pub struct DataJoinCall {
/// Name of the node to join
pub node: Option<String>,
/// Whether to force disconnect any other existing voice connections
///
/// Useful for disconnecting on another device and joining on a new.
pub force_disconnect: Option<bool>,
/// Users which should be notified of the call starting
///
/// Only used when the user is the first one connected.
pub recipients: Option<Vec<String>>,
}
);
@@ -302,22 +313,19 @@ impl Channel {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
| Channel::TextChannel { id, .. } => id,
}
}
/// This returns a Result because the recipient name can't be determined here without a db call,
/// which can't be done since this is models, which can't reference the database crate.
///
/// If it returns Err, you need to fetch the name from the db.
pub fn name(&self) -> Result<&str, ()> {
/// If it returns None, you need to fetch the name from the db.
pub fn name(&self) -> Option<&str> {
match self {
Channel::DirectMessage { .. } => Err(()),
Channel::SavedMessages { .. } => Ok("Saved Messages"),
Channel::TextChannel { name, .. }
| Channel::Group { name, .. }
| Channel::VoiceChannel { name, .. } => Ok(name),
Channel::DirectMessage { .. } => None,
Channel::SavedMessages { .. } => Some("Saved Messages"),
Channel::TextChannel { name, .. } | Channel::Group { name, .. } => Some(name),
}
}
}

View File

@@ -132,6 +132,8 @@ auto_derived!(
MessagePinned { id: String, by: String },
#[serde(rename = "message_unpinned")]
MessageUnpinned { id: String, by: String },
#[serde(rename = "call_started")]
CallStarted { by: String, finished_at: Option<Timestamp> },
}
/// Name and / or avatar override information
@@ -215,7 +217,7 @@ auto_derived!(
#[derive(Default)]
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct SendableEmbed {
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub icon_url: Option<String>,
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
pub url: Option<String>,
@@ -445,6 +447,7 @@ impl From<SystemMessage> for String {
}
SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(),
SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(),
SystemMessage::CallStarted { .. } => "Call started.".to_string(),
}
}
}

View File

@@ -31,6 +31,14 @@ pub static RE_COLOUR: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^(?:[a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|(repeating-)?(linear|conic|radial)-gradient\(([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|\d+deg)([ ]+(\d{1,3}%|0))?(,[ ]*([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+)([ ]+(\d{1,3}%|0))?)+\))$").unwrap()
});
fn default_true() -> bool {
true
}
fn is_true(x: &bool) -> bool {
*x
}
auto_derived_partial!(
/// Server Member
pub struct Member {
@@ -57,6 +65,13 @@ auto_derived_partial!(
/// Timestamp this member is timed out until
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub timeout: Option<Timestamp>,
/// Whether the member is server-wide voice muted
#[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_publish: bool,
/// Whether the member is server-wide voice deafened
#[serde(skip_serializing_if = "is_true", default = "default_true")]
pub can_receive: bool,
},
"PartialMember"
);
@@ -77,6 +92,9 @@ auto_derived!(
Avatar,
Roles,
Timeout,
CanReceive,
CanPublish,
JoinedAt,
}
/// Member removal intention
@@ -123,8 +141,14 @@ auto_derived!(
pub roles: Option<Vec<String>>,
/// Timestamp this member is timed out until
pub timeout: Option<Timestamp>,
/// server-wide voice muted
pub can_publish: Option<bool>,
/// server-wide voice deafened
pub can_receive: Option<bool>,
/// voice channel to move to if already in a voice channel
pub voice_channel: Option<String>,
/// Fields to remove from channel object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsMember>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsMember>,
}
);

View File

@@ -175,6 +175,8 @@ auto_derived!(
/// Ranking position
///
/// Smaller values take priority.
///
/// **Removed** - no effect, use the edit server role positions route
pub rank: Option<i64>,
}
@@ -247,8 +249,8 @@ auto_derived!(
pub analytics: Option<bool>,
/// Fields to remove from server object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsServer>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsServer>,
}
/// New role information
@@ -267,11 +269,11 @@ auto_derived!(
pub hoist: Option<bool>,
/// Ranking position
///
/// Smaller values take priority.
/// **Removed** - no effect, use the edit server role positions route
pub rank: Option<i64>,
/// Fields to remove from role object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsRole>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsRole>,
}
/// New role permissions
@@ -286,4 +288,9 @@ auto_derived!(
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
}
/// New role positions
pub struct DataEditRoleRanks {
pub ranks: Vec<String>,
}
);

View File

@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use regex::Regex;
@@ -245,8 +246,8 @@ auto_derived!(
pub flags: Option<i32>,
/// Fields to remove from user object
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
pub remove: Option<Vec<FieldsUser>>,
#[cfg_attr(feature = "serde", serde(default))]
pub remove: Vec<FieldsUser>,
}
/// User flag reponse
@@ -255,12 +256,14 @@ auto_derived!(
pub flags: i32,
}
/// Mutual friends and servers response
/// Mutual friends, servers, groups and DMs response
pub struct MutualResponse {
/// Array of mutual user IDs that both users are friends with
pub users: Vec<String>,
/// Array of mutual server IDs that both users are in
pub servers: Vec<String>,
/// Array of mutual group and dm IDs that both users are in
pub channels: Vec<String>,
}
/// Bot information for if the user is a bot
@@ -277,6 +280,19 @@ auto_derived!(
}
);
auto_derived_partial!(
/// Voice State information for a user
pub struct UserVoiceState {
pub id: String,
pub joined_at: Timestamp,
pub is_receiving: bool,
pub is_publishing: bool,
pub screensharing: bool,
pub camera: bool,
},
"PartialUserVoiceState"
);
pub trait CheckRelationship {
fn with(&self, user: &str) -> RelationshipStatus;
}

View File

@@ -1,8 +1,9 @@
[package]
name = "revolt-parser"
version = "0.8.7"
version = "0.8.9"
edition = "2021"
license = "AGPL-3.0-or-later"
license = "MIT"
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Message Parser"
[dependencies]

View File

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

View File

@@ -4,15 +4,18 @@ use logos::Logos;
#[derive(Debug, Clone, Logos, PartialEq)]
#[logos(skip "\n")]
pub enum MessageToken {
#[logos(subpattern id="[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")]
pub enum MessageToken<'a> {
#[token("\\")]
Escape,
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())]
#[regex("```[^`\n]*", |_| 3)]
#[regex("``", |_| 2)]
#[regex("`", |_| 1)]
CodeblockMarker(usize),
#[regex("<@[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
UserMention(String),
#[regex("<%[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
RoleMention(String),
#[regex("<@(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1])]
UserMention(&'a str),
#[regex("<%(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1],)]
RoleMention(&'a str),
#[token("@everyone")]
MentionEveryone,
#[token("@online")]
@@ -27,13 +30,13 @@ pub struct MessageResults {
pub mentions_online: bool
}
struct MessageParserIterator<I> {
struct MessageParserIterator<'a, I> {
inner: I,
temp: VecDeque<MessageToken>
temp: VecDeque<MessageToken<'a>>
}
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> {
type Item = MessageToken;
impl<'a, I: Iterator<Item = MessageToken<'a>>> Iterator for MessageParserIterator<'a, I> {
type Item = MessageToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
if !self.temp.is_empty() {
@@ -80,8 +83,8 @@ pub fn parse_message(text: &str) -> MessageResults {
match token {
MessageToken::Escape => {}
MessageToken::CodeblockMarker(_) => {},
MessageToken::UserMention(id) => { results.user_mentions.insert(id); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); },
MessageToken::UserMention(id) => { results.user_mentions.insert(id.to_string()); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id.to_string()); },
MessageToken::MentionEveryone => results.mentions_everyone = true,
MessageToken::MentionOnline => results.mentions_online = true,
};
@@ -106,7 +109,7 @@ mod tests {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
}
#[test]
@@ -114,7 +117,7 @@ mod tests {
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
}
#[test]
@@ -138,8 +141,8 @@ mod tests {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>();
assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
@@ -149,8 +152,8 @@ mod tests {
let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
@@ -170,15 +173,14 @@ mod tests {
assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_inline_codeblock_no_mentions() {
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
@@ -192,12 +194,21 @@ mod tests {
assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_codeblock_with_language_no_mentions() {
let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online```").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
}
#[test]
fn test_double_inline_codeblock() {
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.8.7"
version = "0.8.9"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Core
revolt-result = { version = "0.8.7", path = "../result" }
revolt-result = { version = "0.8.9", path = "../result" }
# Utility
auto_ops = "0.3.0"

View File

@@ -61,6 +61,15 @@ pub async fn calculate_server_permissions<P: PermissionQuery>(query: &mut P) ->
permissions.apply(role_override);
}
if !query.do_we_have_publish_overwrites().await {
permissions.revoke(ChannelPermission::Speak as u64);
permissions.revoke(ChannelPermission::Video as u64);
}
if !query.do_we_have_receive_overwrites().await {
permissions.revoke(ChannelPermission::Listen as u64);
}
if query.are_we_timed_out().await {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}

View File

@@ -89,6 +89,8 @@ pub enum ChannelPermission {
DeafenMembers = 1 << 34,
/// Move members between voice channels
MoveMembers = 1 << 35,
/// Listen to other users
Listen = 1 << 36,
// * Channel permissions two electric boogaloo
/// Mention everyone and online members
@@ -130,7 +132,9 @@ pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| {
+ ChannelPermission::SendEmbeds
+ ChannelPermission::UploadFiles
+ ChannelPermission::Connect
+ ChannelPermission::Speak,
+ ChannelPermission::Speak
+ ChannelPermission::Listen
+ ChannelPermission::Video
)
});

View File

@@ -8,10 +8,18 @@ pub use server::*;
pub use user::*;
/// Holds a permission value to manipulate.
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PermissionValue(u64);
impl PermissionValue {
pub fn from_raw(value: u64) -> Self {
Self(value)
}
pub fn into_raw(self) -> u64 {
self.0
}
/// Apply a given override to this value
pub fn apply(&mut self, v: Override) {
self.allow(v.allow);

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