Compare commits

..
46 Commits
Author SHA1 Message Date
izzy 5362e84730 fix: don't try to mount env logger twice 2025-05-13 11:16:28 +01:00
izzy be89e62d54 fix: include pushd key in Revolt.toml 2025-05-13 11:04:43 +01:00
izzy a72134f4ad chore: bump version to 0.8.6
chore: publish revolt-parser
2025-05-13 10:52:44 +01:00
izzy 69ae767175 refactor: some clippy recommendations 2025-05-13 10:48:56 +01:00
izzy 2aff76c369 refactor(pushd): add non-panic error handling to all queue consumers 2025-05-13 10:41:03 +01:00
izzy 01e0f9e558 fix: add Authifier migration for last_seen 2025-05-12 12:15:23 +01:00
izzy 8ea8f50fb8 chore: bump version to 0.8.5
closes #402
2025-05-10 16:51:38 +01:00
izzy 68bcfcb626 fix: ensure easypwned api is actually set before using 2025-05-10 16:43:30 +01:00
izzy 9632a0dc1b refactor: remove crate bindings/node 2025-05-10 16:40:02 +01:00
izzy a4a0934475 feat: add use_starttls config option for SMTP 2025-05-10 16:38:26 +01:00
izzy 2061794305 docs: configure reset existing email template 2025-05-10 16:36:41 +01:00
izzy 3a7ebad883 merge: branch 'izzy/patch/easypwned'
fix: configure new authifier properties
2025-05-10 16:31:37 +01:00
izzy 6daa8223b2 ci: must login to ghcr before pushing base image 2025-05-09 23:10:35 +01:00
izzy 4a58d6eae0 chore: enable lto for production builds (#341) 2025-05-09 23:04:37 +01:00
izzy a8a2e491b0 ci: reduce threads available to build 2025-05-09 22:59:51 +01:00
izzy f98f09a5ea ci: bump Docker builds to Rust 1.86.0 2025-05-09 22:57:45 +01:00
izzy 7a4e12a732 ci: include parser toml in Dockerfile 2025-05-09 22:53:22 +01:00
izzy aa8f8575bc fix: include revolt-parser in build scripts 2025-05-09 22:51:16 +01:00
izzy 70d6a5ac08 ci: for multi-arch builds, must push instead of load 2025-05-09 22:48:50 +01:00
izzy 7f85ad5e7b ci: load the built base image into Docker 2025-05-09 22:44:58 +01:00
izzy c785ae615d ci: new attempt at using hosted runner 2025-05-09 22:39:29 +01:00
Paul Makles 9c8a362e9f merge: pull request #413 from Docteh/Docteh-patch-1 2025-05-09 20:38:33 +01:00
Kyle Kienapfel e708c612cf fix: Actually create crond container in github workflow
Just one line missing to make the container creation happen

Related to eb5f5f91cd

Signed-off-by: Kyle Kienapfel <doctor.whom@gmail.com>
2025-04-29 18:19:43 -07:00
Paul Makles f75d635c81 feat: new parser for mention everything (#411)
* feat: implement custom parser for mentions

* feat: handle backticks in codeblocks and escapes

* chore: remove fancy-regex

* fix: dont serialise empty role mentions

* chore: cleanup role mentions logic

* chore: update lockfile

* fix: actually use everyone and online mentions from content

* fix: broken merge

---------

Signed-off-by: Zomatree <me@zomatree.live>
2025-04-28 17:08:06 +01:00
ELginas 29b6ac3acf docs: correct Revolt config override filename (#410) 2025-04-25 11:27:16 +01:00
Zomatree a1b0e4767a fix: add back missing early adopter badge 2025-04-22 20:23:20 +01:00
Zomatree f4281c7050 fix: dont leak invisible presence to others 2025-04-22 19:53:55 +01:00
Zomatree d2e83c94f3 fix: show full branch name on github webhook messages 2025-04-22 16:13:03 +01:00
Tom 2540860129 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-05 10:45:04 -07:00
izzy 93a5de769d ci: downgrade lockfile 2025-04-02 12:14:31 +01:00
izzy 717d5e9a88 chore: bump version to 0.8.4
chore: enable webhooks in test builds
2025-04-02 12:11:46 +01:00
Paul Makles 18f06467bb Merge pull request #408 from shaksternano/main
feat: add option to send message with missing replies
2025-04-02 12:05:30 +01:00
Builderb 8d88ea9963 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-02 12:05:04 +01:00
Paul Makles e3e1cab02e Merge pull request #399 from StupidRepo/main
fix: allow message pinning and unpinning for DM-like channels
2025-04-02 12:04:44 +01:00
Nils Ponsard 00bd56b687 feat: allow to set an icon when creating a group
Signed-off-by: Nils Ponsard <nilsponsard@gmail.com>
2025-04-02 12:03:38 +01:00
ShaksterNano a7727bba5c feat: add option to send message with missing replies
Signed-off-by: ShaksterNano <54268387+shaksternano@users.noreply.github.com>
2025-03-31 22:26:30 +01:00
Paul Makles 6b8005f530 Merge pull request #403 from phazeschift/tests
test: Add tests for invite/fetch
2025-03-31 11:42:24 +00:00
phazeschift 714fba7ea6 test: add delete server channel test
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-03-16 14:15:45 -07:00
phazeschift ac05fb385c test: add utilities for common setup
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-03-15 19:08:23 -07:00
phazeschift 537c147c4b test: Add tests for invite/fetch
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-03-14 17:17:44 -07:00
Bradlee Barnes 7bee609b84 Merge branch 'revoltchat:main' into main 2025-03-11 02:32:47 +00:00
IAmTomahawkx 859c0214ec chore: ignore python venv and dev script for user generation
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-03-09 21:40:21 -07:00
Bradlee Barnes 5eea0bbc9a fix: remove SavedMessages and Group due to given permissions
Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>
2025-03-08 00:50:57 +00:00
Bradlee Barnes 15dec656a8 fix: allow message pinning and unpinning for DM-like channels
Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>
2025-03-07 22:39:04 +00:00
Paul Makles bcf6561c0c fix: include production default value for config 2025-02-25 14:24:05 +00:00
Paul Makles b177a3e201 fix: don't bump the lockfile version 2025-02-25 11:08:00 +00:00
120 changed files with 5965 additions and 3752 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
[profile.default] [profile.default]
slow-timeout = { period = "3s", terminate-after = 2 } slow-timeout = { period = "5s", terminate-after = 10 }
+121 -69
View File
@@ -2,24 +2,13 @@ name: Docker Test & Publish
on: on:
push: push:
# branches:
# - "main"
tags: tags:
- "*" - "*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request: pull_request:
branches: branches:
- "main" - "main"
paths: paths:
- "Dockerfile" - "Dockerfile"
workflow_dispatch:
permissions: permissions:
contents: read contents: read
@@ -27,8 +16,9 @@ permissions:
jobs: jobs:
base: base:
name: Test base image build
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Build base image if: github.event_name == 'pull_request'
steps: steps:
# Configure build environment # Configure build environment
- name: Checkout - name: Checkout
@@ -36,33 +26,21 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
# Authenticate with GHCR # Build the image
- name: Login to Github Container Registry
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image - name: Build base image
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: false
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest tags: ghcr.io/${{ github.repository_owner }}/base:latest
cache-from: type=gha,scope=buildx-base-multi-arch cache-from: type=gha,scope=buildx-base-multi-arch
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
publish: publish:
needs: [base] runs-on: self-hosted
runs-on: ubuntu-latest
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
strategy: name: Publish Docker images
matrix:
project: [delta, bonfire, autumn, january, pushd]
name: Build ${{ matrix.project }} image
steps: steps:
# Configure build environment # Configure build environment
- name: Checkout - name: Checkout
@@ -77,6 +55,7 @@ jobs:
registry: docker.io registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry - name: Login to Github Container Registry
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
@@ -84,58 +63,131 @@ jobs:
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
# Resolve the correct project # Build the image
- uses: kanga333/variable-mapper@master - name: Build base image
id: export uses: docker/build-push-action@v4
with: with:
key: "${{ matrix.project }}" context: .
map: | push: true
{ platforms: linux/amd64,linux/arm64
"delta": { tags: ghcr.io/${{ github.repository_owner }}/base:latest
"path": "crates/delta",
"tag": "${{ github.repository_owner }}/server"
},
"bonfire": {
"path": "crates/bonfire",
"tag": "${{ github.repository_owner }}/bonfire"
},
"autumn": {
"path": "crates/services/autumn",
"tag": "${{ github.repository_owner }}/autumn"
},
"january": {
"path": "crates/services/january",
"tag": "${{ github.repository_owner }}/january"
},
"pushd": {
"path": "crates/daemons/pushd",
"tag": "${{ github.repository_owner }}/pushd"
},
"crond": {
"path": "crates/daemons/crond",
"tag": "${{ github.repository_owner }}/crond"
}
}
export_to: output
# Configure metadata # revoltchat/server
- name: Docker meta - name: Docker meta
id: meta id: meta-delta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: | images: |
docker.io/${{ steps.export.outputs.tag }} docker.io/revoltchat/server
ghcr.io/${{ steps.export.outputs.tag }} ghcr.io/revoltchat/server
# Build crate image
- name: Publish - name: Publish
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
file: ${{ steps.export.outputs.path }}/Dockerfile file: crates/delta/Dockerfile
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta-delta.outputs.tags }}
build-args: | build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta-delta.outputs.labels }}
# revoltchat/bonfire
- name: Docker meta
id: meta-bonfire
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/bonfire
ghcr.io/revoltchat/bonfire
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/bonfire/Dockerfile
tags: ${{ steps.meta-bonfire.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-bonfire.outputs.labels }}
# revoltchat/autumn
- name: Docker meta
id: meta-autumn
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/autumn
ghcr.io/revoltchat/autumn
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/autumn/Dockerfile
tags: ${{ steps.meta-autumn.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-autumn.outputs.labels }}
# revoltchat/january
- name: Docker meta
id: meta-january
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/january
ghcr.io/revoltchat/january
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/january/Dockerfile
tags: ${{ steps.meta-january.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-january.outputs.labels }}
# revoltchat/crond
- name: Docker meta
id: meta-crond
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/crond
ghcr.io/revoltchat/crond
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/daemons/crond/Dockerfile
tags: ${{ steps.meta-crond.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-crond.outputs.labels }}
# revoltchat/pushd
- name: Docker meta
id: meta-pushd
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/pushd
ghcr.io/revoltchat/pushd
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/daemons/pushd/Dockerfile
tags: ${{ steps.meta-pushd.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-pushd.outputs.labels }}
+2
View File
@@ -5,6 +5,8 @@ compose.override.yml
target target
.data .data
.env .env
.venv/
venv/
.vercel .vercel
.DS_Store .DS_Store
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy", "rust-analyzer.check.command": "clippy",
"nixEnvSelector.suggestion": false, "nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceFolder}/default.nix" "nixEnvSelector.nixFile": "${workspaceFolder}/default.nix"
} }
Generated
+2453 -1863
View File
File diff suppressed because it is too large Load Diff
+4 -8
View File
@@ -6,19 +6,15 @@ members = [
"crates/bonfire", "crates/bonfire",
"crates/core/*", "crates/core/*",
"crates/services/*", "crates/services/*",
"crates/bindings/*",
"crates/daemons/*", "crates/daemons/*",
] ]
[patch.crates-io] [patch.crates-io]
redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" } redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
# I'm 99% sure this is overloading the GitHub worker # I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just # hence builds have been failing since, let's just
# disable it for now. In the future, we could use this # disable it for now. In the future, we could use this
# if we were rolling our own CI. # if we were rolling our own CI (that is now).
# [profile.release] [profile.release]
# lto = true lto = true
+2 -2
View File
@@ -1,5 +1,5 @@
# Build Stage # Build Stage
FROM --platform="${BUILDPLATFORM}" rust:1.77.2-slim-bookworm FROM --platform="${BUILDPLATFORM}" rust:1.86.0-slim-bookworm
USER 0:0 USER 0:0
WORKDIR /home/rust/src WORKDIR /home/rust/src
@@ -17,13 +17,13 @@ RUN sh /tmp/build-image-layer.sh tools
# Build all dependencies # Build all dependencies
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/ COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/ COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/ COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/ COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/files/Cargo.toml ./crates/core/files/ COPY crates/core/files/Cargo.toml ./crates/core/files/
COPY crates/core/models/Cargo.toml ./crates/core/models/ COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/ COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/ COPY crates/core/result/Cargo.toml ./crates/core/result/
+2 -2
View File
@@ -1,5 +1,5 @@
# Build Stage # Build Stage
FROM rust:1.77.2-slim-bookworm FROM rust:1.86.0-slim-bookworm
USER 0:0 USER 0:0
WORKDIR /home/rust/src WORKDIR /home/rust/src
@@ -13,13 +13,13 @@ COPY scripts/build-image-layer.sh /tmp/
# Build all dependencies # Build all dependencies
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/ COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/ COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/ COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/ COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/files/Cargo.toml ./crates/core/files/ COPY crates/core/files/Cargo.toml ./crates/core/files/
COPY crates/core/models/Cargo.toml ./crates/core/models/ COPY crates/core/models/Cargo.toml ./crates/core/models/
COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/ COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/ COPY crates/core/result/Cargo.toml ./crates/core/result/
+3 -4
View File
@@ -13,7 +13,7 @@ The services and libraries that power the Revolt service.<br/>
<br/> <br/>
| Crate | Path | Description | | | Crate | Path | Description | |
| ------------------ | -------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------ | -------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration | ![Crates.io Version](https://img.shields.io/crates/v/revolt-config) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-config) ![Crates.io Version](https://img.shields.io/crates/size/revolt-config) ![Crates.io License](https://img.shields.io/crates/l/revolt-config) | | `core/config` | [crates/core/config](crates/core/config) | Core: Configuration | ![Crates.io Version](https://img.shields.io/crates/v/revolt-config) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-config) ![Crates.io Version](https://img.shields.io/crates/size/revolt-config) ![Crates.io License](https://img.shields.io/crates/l/revolt-config) |
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation | ![Crates.io Version](https://img.shields.io/crates/v/revolt-database) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-database) ![Crates.io Version](https://img.shields.io/crates/size/revolt-database) ![Crates.io License](https://img.shields.io/crates/l/revolt-database) | | `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation | ![Crates.io Version](https://img.shields.io/crates/v/revolt-database) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-database) ![Crates.io Version](https://img.shields.io/crates/size/revolt-database) ![Crates.io License](https://img.shields.io/crates/l/revolt-database) |
| `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines | ![Crates.io Version](https://img.shields.io/crates/v/revolt-files) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-files) ![Crates.io Version](https://img.shields.io/crates/size/revolt-files) ![Crates.io License](https://img.shields.io/crates/l/revolt-files) | | `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines | ![Crates.io Version](https://img.shields.io/crates/v/revolt-files) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-files) ![Crates.io Version](https://img.shields.io/crates/size/revolt-files) ![Crates.io License](https://img.shields.io/crates/l/revolt-files) |
@@ -25,7 +25,6 @@ The services and libraries that power the Revolt service.<br/>
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events 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/january` | [crates/services/january](crates/services/january) | 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) | | `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bindings/node` | [crates/bindings/node](crates/bindings/node) | Node.js bindings for the Revolt software | ![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/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) | | `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) |
@@ -34,10 +33,10 @@ The services and libraries that power the Revolt service.<br/>
## Minimum Supported Rust Version ## Minimum Supported Rust Version
Rust 1.76 or higher. Rust 1.86.0 or higher.
> [!CAUTION] > [!CAUTION]
> The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341). > The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341). This is currently solved by build time options but we are looking for a proper fix.
## Development Guide ## Development Guide
+2 -1
View File
@@ -1,5 +1,5 @@
# ⚠️ This configuration is intended for development environment. # ⚠️ This configuration is intended for development environment.
# If you'd like to override anything, create a Revolt.override.toml # If you'd like to override anything, create a Revolt.overrides.toml
[database] [database]
# MongoDB connection URL # MongoDB connection URL
@@ -38,6 +38,7 @@ from_address = "development@revolt.chat"
reply_to = "support@revolt.chat" reply_to = "support@revolt.chat"
port = 14025 port = 14025
use_tls = false use_tls = false
use_starttls = false
[files.s3] [files.s3]
# S3 protocol endpoint # S3 protocol endpoint
-7
View File
@@ -1,7 +0,0 @@
target
index.node
**/node_modules
**/.DS_Store
npm-debug.log*
cargo.log
cross.log
-25
View File
@@ -1,25 +0,0 @@
[package]
name = "revolt-nodejs-bindings"
version = "0.8.3"
description = "Node.js bindings for the Revolt software"
authors = ["Paul Makles <me@insrt.uk>"]
license = "MIT"
edition = "2021"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
neon = "1.0.0"
neon-serde4 = "1.0.0"
serde = { version = "1", features = ["derive"] }
async-std = "1.12.0"
revolt-config = { version = "0.8.3", path = "../../core/config" }
revolt-result = { version = "0.8.3", path = "../../core/result" }
revolt-database = { version = "0.8.3", path = "../../core/database" }
-92
View File
@@ -1,92 +0,0 @@
# revolt.node
**revolt.node:** Node.js bindings for the Revolt software
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
## Building revolt.node
Building revolt.node requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
To run the build, run:
```sh
$ npm run build
```
This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`.
## Exploring revolt.node
After building revolt.node, you can explore its exports at the Node console:
```sh
$ npm i
$ npm run build
$ node
> require('.').hello()
'hello node'
```
## Available Scripts
In the project directory, you can run:
#### `npm install`
Installs the project, including running `npm run build`.
#### `npm run build`
Builds the Node addon (`index.node`) from source, generating a release build with `cargo --release`.
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm run build` and similar commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
```
npm run build -- --feature=beetle
```
#### `npm run debug`
Similar to `npm run build` but generates a debug build with `cargo`.
#### `npm run cross`
Similar to `npm run build` but uses [cross-rs](https://github.com/cross-rs/cross) to cross-compile for another platform. Use the [`CARGO_BUILD_TARGET`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget) environment variable to select the build target.
#### `npm test`
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
## Project Layout
The directory structure of this project is:
```
revolt.node/
├── Cargo.toml
├── README.md
├── src/
| └── lib.rs
├── index.node
├── package.json
└── target/
```
| Entry | Purpose |
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
| `README.md` | This file. |
| `src/` | The directory tree containing the Rust source code for the project. |
| `lib.rs` | Entry point for the Rust source code. |
| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. |
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
| `target/` | Binary artifacts generated by the Rust build. |
## Learn More
Learn more about:
- [Neon](https://neon-bindings.com).
- [Rust](https://www.rust-lang.org).
- [Node](https://nodejs.org).
-84
View File
@@ -1,84 +0,0 @@
import { Channel, User } from "revolt-api";
/**
* Opaque type for Revolt database
*/
export declare interface Database {}
/**
* Opaque type for Revolt database
*/
export declare interface OpaqueUser {}
/**
* Error type from Revolt backend
*/
export declare interface Err {
type: string;
location: string;
}
/**
* Initialises background tasks and logging, must be called before anything else!
* Can be called multiple times!
*/
export declare function init();
/**
* Gets a new handle to the Revolt database
* @returns {Database} Handle
*/
export declare function database(): Database;
/**
* Fetch user from database
* @param {string} userId User's ID
* @this {Database}
*/
export declare function database_fetch_user(userId: string): OpaqueUser;
/**
* Fetch user from database
* @param {string} username Username
* @param {string} discriminator Discriminator
* @this {Database}
*/
export declare function database_fetch_user_by_username(
username: string,
discriminator: string
): OpaqueUser;
/**
* Gets model data as JSON
* @this {OpaqueUser}
*/
export declare function model_data(): User;
/**
* Gets error if the model failed to fetch
* @this {OpaqueUser}
*/
export declare function model_error(): Err;
/**
* Open a direct message channel between two users
* @param {string} userA User A ID
* @param {string} userB User B ID
* @returns Existing or newly created channel
*/
export declare function proc_channels_create_dm(
userA: string,
userB: string
): Promise<Channel & { error: Err }>;
/**
* Suspend a user
* @param {string} user User
* @param {number} duration Duration (in days), set to 0 for indefinite
* @param {string} reason Pipe-separated list of reasons (e.g. reason1|reason2|reason3)
*/
export declare function proc_users_suspend(
user: OpaqueUser,
duration: number,
reason: string
): Promise<{ error: Err }>;
-36
View File
@@ -1,36 +0,0 @@
{
"name": "revolt-nodejs-bindings",
"version": "0.7.15-rev0.0.3",
"description": "Node.js bindings for the Revolt software",
"main": "index.node",
"scripts": {
"test": "cargo test",
"cargo-build": "cargo build --message-format=json > cargo.log",
"cross-build": "cross build --message-format=json > cross.log",
"postcargo-build": "neon dist < cargo.log",
"postcross-build": "neon dist -m /target < cross.log",
"debug": "npm run cargo-build --",
"build": "npm run cargo-build -- --release",
"cross": "npm run cross-build -- --release"
},
"author": "Paul Makles",
"license": "AGPL-3.0",
"devDependencies": {
"@neon-rs/cli": "0.1.73"
},
"repository": {
"type": "git",
"url": "git+https://github.com/revoltchat/backend"
},
"keywords": [
"revolt",
"chat"
],
"bugs": {
"url": "https://github.com/revoltchat/backend/issues"
},
"homepage": "https://github.com/revoltchat/backend#readme",
"dependencies": {
"revolt-api": "^0.7.15"
}
}
-117
View File
@@ -1,117 +0,0 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
revolt-api:
specifier: ^0.7.15
version: 0.7.15
devDependencies:
'@neon-rs/cli':
specifier: 0.1.73
version: 0.1.73
packages:
/@cargo-messages/android-arm-eabi@0.1.72:
resolution: {integrity: sha512-gGZxIM1mj+Y5x+ULND6ZCNr7f70OJi9wDlycSK8hGONy9wrChN6JAIHryddC5cqcwlYAoQ6IDcDFElnhAYbybA==}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/darwin-arm64@0.1.72:
resolution: {integrity: sha512-EAzN5MLaXPljZKZDO5qR+aBs44eSq2ZbEnS7AI/FziE3MzeXbrGOS3fLba5+7yWPFXJyZolXzePm8N1EBv8ovg==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/darwin-x64@0.1.72:
resolution: {integrity: sha512-RLo6j8s3nYbjdd1LDct4wamfChyRit7zokUuxtIYCu9XOlltkN5vnj1vwnrPvoqCMZ/7CbbuHFwSTn9A71de/w==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/linux-arm-gnueabihf@0.1.72:
resolution: {integrity: sha512-tHsRshuzfjrX6SDW3jg6al8vMNLTMgczGnVYl5RuBZf/yrAUuwe30KxA9ge6w6mW6Ox797DyBchzAc9OLgTgmQ==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/linux-x64-gnu@0.1.72:
resolution: {integrity: sha512-VGtL6CCnUbhsP4aYuBNT5kfrAL7o0qjrxw97a+ax13t+nJd26tVEEIKHMu5drvvS/Nm/hn7sLT8zMnnCv0pvHg==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/win32-arm64-msvc@0.1.72:
resolution: {integrity: sha512-V93Cgz39K+yqa3MveNbhh29pYCp8izK5uEavjPoxlNxAbsMCWH+s0verGDdUcfGxjR1H2V7oZ4FszPqR2SqMRQ==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/win32-x64-msvc@0.1.72:
resolution: {integrity: sha512-knz3uSrO0OSbq3U5VWfCY8FB4NsM43BOWLZ7x4sfaMOC1XWv+IyvDdkLe6DhJx8KUw46KIAimYs9YROrp6l46Q==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@neon-rs/cli@0.1.73:
resolution: {integrity: sha512-1kv8S/feB6UQWQQwsnGfkSkEBOtlFDNExnioL81E2BwvUWgjQPaseHgpi2EpWVgsPUgur5eBm4QowmlpWkD4/w==}
hasBin: true
optionalDependencies:
'@cargo-messages/android-arm-eabi': 0.1.72
'@cargo-messages/darwin-arm64': 0.1.72
'@cargo-messages/darwin-x64': 0.1.72
'@cargo-messages/linux-arm-gnueabihf': 0.1.72
'@cargo-messages/linux-x64-gnu': 0.1.72
'@cargo-messages/win32-arm64-msvc': 0.1.72
'@cargo-messages/win32-x64-msvc': 0.1.72
dev: true
/axios@0.26.1:
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
dependencies:
follow-redirects: 1.15.6
transitivePeerDependencies:
- debug
dev: false
/follow-redirects@1.15.6:
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/lodash.defaultsdeep@4.6.1:
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
dev: false
/revolt-api@0.7.15:
resolution: {integrity: sha512-rWRnjBFvoXt/RHRxHg9KuP+x1Jk61KdsOpKOczVJg5tIqGDmqp/6QZHw5rJTtG+wCH3Ah3YKwY+MM5NHfg1W3Q==}
dependencies:
axios: 0.26.1
lodash.defaultsdeep: 4.6.1
transitivePeerDependencies:
- debug
dev: false
-221
View File
@@ -1,221 +0,0 @@
#[macro_use]
extern crate serde;
use std::sync::OnceLock;
use neon::prelude::*;
use revolt_database::{Database, DatabaseInfo};
fn js_init(mut cx: FunctionContext) -> JsResult<JsUndefined> {
// static INIT: OnceLock<()> = OnceLock::new();
// if INIT.get().is_none() {
// INIT.get_or_init(|| {
// async_std::task::block_on(async {
// revolt_config::configure!(api);
// match DatabaseInfo::Auto.connect().await {
// Ok(db) => {
// let authifier_db = db.clone().to_authifier().await.database;
// revolt_database::tasks::start_workers(db, authifier_db);
// Ok(())
// }
// Err(err) => Err(err),
// }
// })
// .or_else(|err| cx.throw_error(err))
// .unwrap();
// });
// }
Ok(cx.undefined())
}
struct DatabaseBinding(Database, Channel);
impl Finalize for DatabaseBinding {}
impl DatabaseBinding {
fn take(&self) -> (Database, Channel) {
(self.0.clone(), self.1.clone())
}
}
fn js_database(mut cx: FunctionContext) -> JsResult<JsBox<DatabaseBinding>> {
let db = async_std::task::block_on(DatabaseInfo::Auto.connect())
.or_else(|err| cx.throw_error(err))?;
let channel = cx.channel();
Ok(cx.boxed(DatabaseBinding(db, channel)))
}
// Implementations for models
#[derive(Clone)]
#[allow(clippy::large_enum_variant)]
enum Model {
User(revolt_database::User),
Error(revolt_result::Error),
}
impl Model {
fn give(&self) -> Model {
self.clone()
}
}
impl Finalize for Model {}
macro_rules! shim_boxed {
($cx: ident, $name: ident, $model: ident, $( $variable: ident $type: ident $id: expr )+, $cmd: ident, $( $arg: expr, )+) => {
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
$(
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
)+
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
let (deferred, promise) = cx.promise();
async_std::task::spawn(async move {
let result = db.$cmd($($arg,)+).await;
deferred.settle_with(&channel, move |mut cx| {
Ok(cx.boxed(match result {
Ok(value) => Model::$model(value),
Err(error) => Model::Error(error)
}))
})
});
Ok(promise)
}
$cx.export_function(stringify!($name), $name)?;
};
}
fn js_data(mut cx: FunctionContext) -> JsResult<JsValue> {
match cx.this::<JsBox<Model>>()?.give() {
Model::Error(_) => neon_serde4::to_value(&mut cx, &None::<()>),
Model::User(user) => neon_serde4::to_value(&mut cx, &user),
}
.or_else(|e| cx.throw_error(e.to_string()))
}
fn js_error(mut cx: FunctionContext) -> JsResult<JsValue> {
let value = match cx.this::<JsBox<Model>>()?.give() {
Model::Error(err) => Some(err),
_ => None,
};
neon_serde4::to_value(&mut cx, &value).or_else(|e| cx.throw_error(e.to_string()))
}
// Basic data implementation
#[derive(Serialize, Deserialize)]
struct ResultBinding<T> {
#[serde(flatten)]
value: Option<T>,
error: Option<revolt_result::Error>,
}
macro_rules! shim {
($cx: ident, $name: ident, $( $variable: ident $type: ident $id: expr )*, $( $model: ident $modelType: ident $modelId: expr )*, | $db: ident | $closure: expr, $( $arg: expr, )+) => {
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
$(
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
)*
$(
let mut $model = if let Model::$modelType(value) = cx.argument::<JsBox<Model>>($modelId)?.give() {
value
} else {
unreachable!()
};
)*
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
let (deferred, promise) = cx.promise();
async_std::task::spawn(async move {
#[allow(clippy::redundant_closure_call)]
let result = (|$db: $crate::Database| $closure)(db.clone()).await;
deferred.settle_with(&channel, move |mut cx| {
neon_serde4::to_value(
&mut cx,
&match result {
Ok(value) => ResultBinding {
value: Some(value),
error: None,
},
Err(error) => ResultBinding {
value: None,
error: Some(error),
},
},
)
.or_else(|e| cx.throw_error(e.to_string()))
})
});
Ok(promise)
}
$cx.export_function(stringify!($name), $name)?;
};
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
// initialise required background stuff
cx.export_function("init", js_init)?;
// database & model stuff
cx.export_function("database", js_database)?;
cx.export_function("model_data", js_data)?;
cx.export_function("model_error", js_error)?;
shim_boxed!(
cx,
database_fetch_user,
User,
user_id JsString 0,
fetch_user,
&user_id,
);
shim_boxed!(
cx,
database_fetch_user_by_username,
User,
username JsString 0
discriminator JsString 1,
fetch_user_by_username,
&username, &discriminator,
);
// procedure calls
shim!(
cx,
proc_channels_create_dm,
user_a JsString 0
user_b JsString 1,
,
|db| async move {
let user_a = db.fetch_user(&user_a).await?;
let user_b = db.fetch_user(&user_b).await?;
revolt_database::Channel::create_dm(&db, &user_a, &user_b).await
},
&userA, &userB,
);
shim!(
cx,
proc_users_suspend,
duration JsNumber 1
reason JsString 2,
user User 0,
|db| async move {
let duration = duration as usize;
user.suspend(&db, if duration == 0 { None } else { Some(duration) }, Some(reason.split('|').map(|x| x.to_owned()).collect())).await
},
&user,
);
Ok(())
}
-53
View File
@@ -1,53 +0,0 @@
const Internal = require(".");
// playing around with class wrapper, not practical
class Model {
constructor(model) {
this.model = model;
}
data() {
return Internal.model_data.bind(this.model)();
}
error() {
return Internal.model_error.bind(this.model)();
}
}
class User extends Model {
constructor(db, user) {
super(user);
this.db = db;
}
}
class Database {
constructor() {
this.db = Internal.database();
}
async fetchUser(userId) {
return new User(
this,
await Internal.database_fetch_user.bind(this.db)(userId)
);
}
async fetchUserByUsername(username, discriminator) {
return new User(
this,
await Internal.database_fetch_user_by_username.bind(this.db)(
username,
discriminator
)
);
}
}
const db = new Database();
db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data()));
db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data()));
db.fetchUserByUsername("dos", "1111").then((user) =>
console.info(user.error())
);
-6
View File
@@ -1,6 +0,0 @@
{
"name": "bindings",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.8.3" version = "0.8.6"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -36,12 +36,12 @@ async-std = { version = "1.8.0", features = [
] } ] }
# core # core
authifier = { version = "1.0.9" } authifier = { version = "1.0.13" }
revolt-result = { path = "../core/result" } revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" } revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" } revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.3", path = "../core/permissions" } revolt-permissions = { version = "0.8.6", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis # redis
+6 -7
View File
@@ -1,5 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use futures::future::join_all;
use revolt_database::{ use revolt_database::{
events::client::{EventV1, ReadyPayloadFields}, events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery, util::permissions::DatabasePermissionQuery,
@@ -174,7 +175,7 @@ impl State {
.iter() .iter()
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_))) .find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
{ {
Some(db.fetch_user_settings(&user.id, &keys).await?) Some(db.fetch_user_settings(&user.id, keys).await?)
} else { } else {
None None
}; };
@@ -198,13 +199,11 @@ impl State {
.collect(); .collect();
// Make all users appear from our perspective. // Make all users appear from our perspective.
let mut users: Vec<v0::User> = users let mut users: Vec<v0::User> = join_all(users.into_iter().map(|other_user| async {
.into_iter()
.map(|other_user| {
let is_online = online_ids.contains(&other_user.id); let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online) other_user.into_known(&user, is_online).await
}) }))
.collect(); .await;
// Make sure we see our own user correctly. // Make sure we see our own user correctly.
users.push(user.into_self(true).await); users.push(user.into_self(true).await);
+1 -1
View File
@@ -3,7 +3,7 @@ use std::{collections::HashSet, net::SocketAddr, sync::Arc};
use async_tungstenite::WebSocketStream; use async_tungstenite::WebSocketStream;
use authifier::AuthifierEvent; use authifier::AuthifierEvent;
use fred::{ use fred::{
error::{RedisError, RedisErrorKind}, error::RedisErrorKind,
interfaces::{ClientLike, EventInterface, PubsubInterface}, interfaces::{ClientLike, EventInterface, PubsubInterface},
types::RedisConfig, types::RedisConfig,
}; };
+5 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.8.3" version = "0.8.6"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -9,9 +9,10 @@ description = "Revolt Backend: Configuration"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features] [features]
anyhow = ["dep:sentry-anyhow"]
report-macros = ["revolt-result"] report-macros = ["revolt-result"]
test = ["async-std"] test = ["async-std"]
default = ["test"] default = ["test", "anyhow"]
[dependencies] [dependencies]
# Utility # Utility
@@ -32,6 +33,7 @@ pretty_env_logger = "0.4.0"
# Sentry # Sentry
sentry = "0.31.5" sentry = "0.31.5"
sentry-anyhow = { version = "0.38.1", optional = true }
# Core # Core
revolt-result = { version = "0.8.3", path = "../result", optional = true } revolt-result = { version = "0.8.6", path = "../result", optional = true }
+3
View File
@@ -7,3 +7,6 @@ host = "127.0.0.1"
port = 5672 port = 5672
username = "rabbituser" username = "rabbituser"
password = "rabbitpass" password = "rabbitpass"
[features]
webhooks_enabled = true
+19 -1
View File
@@ -1,3 +1,5 @@
production = false
[database] [database]
# MongoDB connection URL # MongoDB connection URL
# Defaults to the container name specified in self-hosted # Defaults to the container name specified in self-hosted
@@ -52,6 +54,8 @@ authifier_shield_key = ""
voso_legacy_token = "" voso_legacy_token = ""
# Whether services are behind the Cloudflare network # Whether services are behind the Cloudflare network
trust_cloudflare = false trust_cloudflare = false
# easypwned endpoint
easypwned = ""
[api.security.captcha] [api.security.captcha]
# hCaptcha configuration # hCaptcha configuration
@@ -62,20 +66,29 @@ hcaptcha_sitekey = ""
# Maximum concurrent connections (to proxy server) # Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50 max_concurrent_connections = 50
[api.users]
[pushd] [pushd]
# this changes the names of the queues to not overlap # this changes the names of the queues to not overlap
# prod/beta if they happen to be on the same exchange/instance. # prod/beta if they happen to be on the same exchange/instance.
# Usually they have to be, so that messages sent from one or the other get sent to everyone # Usually they have to be, so that messages sent from one or the other get sent to everyone
production = true production = true
# Changes how many users are processed in each chunk when resolving role/everyone mentions.
# Increasing this will resolve mentions faster, but will consume more memory while resolving.
mass_mention_chunk_size = 200
# none of these should need changing # none of these should need changing
exchange = "revolt.notifications" exchange = "revolt.notifications"
message_queue = "notifications.origin.message" 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_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received fr_received_queue = "notifications.ingest.fr_received" # friend request received
generic_queue = "notifications.ingest.generic" # generic messages (title + body) generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices ack_queue = "notifications.process.ack" # updates badges for apple devices
[pushd.vapid] [pushd.vapid]
queue = "notifications.outbound.vapid" queue = "notifications.outbound.vapid"
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo" private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
@@ -171,9 +184,13 @@ default_bucket = "revolt-uploads"
[features] [features]
# Bucket to upload to by default
# Feature gate options # Feature gate options
webhooks_enabled = false webhooks_enabled = false
# Enable push notifications for mass pings (everyone, online, roles)
# When false this will still ping in-client but will not send notifications from pushd
mass_mentions_send_notifications = true
# Can role/everyone pings be used at all
mass_mentions_enabled = true
[features.limits] [features.limits]
@@ -258,4 +275,5 @@ api = ""
events = "" events = ""
files = "" files = ""
proxy = "" proxy = ""
pushd = ""
crond = "" crond = ""
+21
View File
@@ -7,6 +7,7 @@ use once_cell::sync::Lazy;
use serde::Deserialize; use serde::Deserialize;
pub use sentry::{capture_error, capture_message, Level}; pub use sentry::{capture_error, capture_message, Level};
pub use sentry_anyhow::capture_anyhow;
#[cfg(feature = "report-macros")] #[cfg(feature = "report-macros")]
#[macro_export] #[macro_export]
@@ -123,6 +124,7 @@ pub struct ApiSmtp {
pub reply_to: Option<String>, pub reply_to: Option<String>,
pub port: Option<i32>, pub port: Option<i32>,
pub use_tls: Option<bool>, pub use_tls: Option<bool>,
pub use_starttls: Option<bool>,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
@@ -168,6 +170,7 @@ pub struct ApiSecurity {
pub voso_legacy_token: String, pub voso_legacy_token: String,
pub captcha: ApiSecurityCaptcha, pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool, pub trust_cloudflare: bool,
pub easypwned: String,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
@@ -175,19 +178,29 @@ pub struct ApiWorkers {
pub max_concurrent_connections: usize, pub max_concurrent_connections: usize,
} }
#[derive(Deserialize, Debug, Clone)]
pub struct ApiUsers {
pub early_adopter_cutoff: Option<u64>,
}
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct Api { pub struct Api {
pub registration: ApiRegistration, pub registration: ApiRegistration,
pub smtp: ApiSmtp, pub smtp: ApiSmtp,
pub security: ApiSecurity, pub security: ApiSecurity,
pub workers: ApiWorkers, pub workers: ApiWorkers,
pub users: ApiUsers,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct Pushd { pub struct Pushd {
pub production: bool, pub production: bool,
pub exchange: String, pub exchange: String,
pub mass_mention_chunk_size: usize,
// Queues
pub message_queue: String, pub message_queue: String,
pub mass_mention_queue: String,
pub fr_accepted_queue: String, pub fr_accepted_queue: String,
pub fr_received_queue: String, pub fr_received_queue: String,
pub generic_queue: String, pub generic_queue: String,
@@ -214,6 +227,10 @@ impl Pushd {
self.get_routing_key(self.message_queue.clone()) self.get_routing_key(self.message_queue.clone())
} }
pub fn get_mass_mention_routing_key(&self) -> String {
self.get_routing_key(self.mass_mention_queue.clone())
}
pub fn get_fr_accepted_routing_key(&self) -> String { pub fn get_fr_accepted_routing_key(&self) -> String {
self.get_routing_key(self.fr_accepted_queue.clone()) self.get_routing_key(self.fr_accepted_queue.clone())
} }
@@ -314,6 +331,9 @@ impl Default for FeaturesAdvanced {
pub struct Features { pub struct Features {
pub limits: FeaturesLimitsCollection, pub limits: FeaturesLimitsCollection,
pub webhooks_enabled: bool, pub webhooks_enabled: bool,
pub mass_mentions_send_notifications: bool,
pub mass_mentions_enabled: bool,
#[serde(default)] #[serde(default)]
pub advanced: FeaturesAdvanced, pub advanced: FeaturesAdvanced,
} }
@@ -324,6 +344,7 @@ pub struct Sentry {
pub events: String, pub events: String,
pub files: String, pub files: String,
pub proxy: String, pub proxy: String,
pub pushd: String,
pub crond: String, pub crond: String,
} }
+9 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.8.3" version = "0.8.6"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -24,18 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.3", path = "../config", features = [ revolt-config = { version = "0.8.6", path = "../config", features = [
"report-macros", "report-macros",
] } ] }
revolt-result = { version = "0.8.3", path = "../result" } revolt-result = { version = "0.8.6", path = "../result" }
revolt-models = { version = "0.8.3", path = "../models", features = [ revolt-models = { version = "0.8.6", path = "../models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.8.3", path = "../presence" } revolt-presence = { version = "0.8.6", path = "../presence" }
revolt-permissions = { version = "0.8.3", path = "../permissions", features = [ revolt-permissions = { version = "0.8.6", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
revolt-parser = { version = "0.8.6", path = "../parser" }
# Utility # Utility
log = "0.4" log = "0.4"
@@ -64,7 +65,7 @@ redis-kiss = { version = "0.1.4" }
# Database # Database
bson = { optional = true, version = "2.1.0" } bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "2.1.0", default-features = false } mongodb = { optional = true, version = "3.1.0" }
# Database Migration # Database Migration
unicode-segmentation = "1.10.1" unicode-segmentation = "1.10.1"
@@ -96,7 +97,7 @@ web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] } revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier # Authifier
authifier = { version = "1.0.9", features = ["rocket_impl"] } authifier = { version = "1.0.13", features = ["rocket_impl"] }
# RabbitMQ # RabbitMQ
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
+32
View File
@@ -170,6 +170,38 @@ impl AMQP {
.await .await
} }
pub async fn mass_mention_message_sent(
&self,
server_id: String,
payload: Vec<PushNotification>,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = MassMessageSentPayload {
notifications: payload,
server_id,
};
let payload = to_string(&payload).unwrap();
let routing_key = config.pushd.get_mass_mention_routing_key();
debug!(
"Sending mass mention payload on channel {}: {}",
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, routing_key.as_str()),
)
.await
}
pub async fn ack_message( pub async fn ack_message(
&self, &self,
user_id: String, user_id: String,
+26 -1
View File
@@ -3,6 +3,7 @@ mod reference;
use authifier::config::Captcha; use authifier::config::Captcha;
use authifier::config::EmailVerificationConfig; use authifier::config::EmailVerificationConfig;
use authifier::config::PasswordScanning;
use authifier::config::ResolveIp; use authifier::config::ResolveIp;
use authifier::config::SMTPSettings; use authifier::config::SMTPSettings;
use authifier::config::Shield; use authifier::config::Shield;
@@ -102,6 +103,13 @@ impl Database {
let config = config().await; let config = config().await;
let mut auth_config = authifier::Config { let mut auth_config = authifier::Config {
password_scanning: if config.api.security.easypwned.is_empty() {
Default::default()
} else {
PasswordScanning::EasyPwned {
endpoint: config.api.security.easypwned,
}
},
email_verification: if !config.api.smtp.host.is_empty() { email_verification: if !config.api.smtp.host.is_empty() {
EmailVerificationConfig::Enabled { EmailVerificationConfig::Enabled {
smtp: SMTPSettings { smtp: SMTPSettings {
@@ -118,6 +126,7 @@ impl Database {
), ),
port: config.api.smtp.port, port: config.api.smtp.port,
use_tls: config.api.smtp.use_tls, use_tls: config.api.smtp.use_tls,
use_starttls: config.api.smtp.use_starttls,
}, },
expiry: Default::default(), expiry: Default::default(),
templates: if config.production { templates: if config.production {
@@ -134,6 +143,15 @@ impl Database {
url: format!("{}/login/reset/", config.hosts.app), url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("../../templates/reset.html").into()), html: Some(include_str!("../../templates/reset.html").into()),
}, },
reset_existing: Template {
title: "You already have a Revolt account, reset your password."
.into(),
text: include_str!("../../templates/reset-existing.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(
include_str!("../../templates/reset-existing.html").into(),
),
},
deletion: Template { deletion: Template {
title: "Confirm account deletion.".into(), title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.txt").into(), text: include_str!("../../templates/deletion.txt").into(),
@@ -156,9 +174,16 @@ impl Database {
url: format!("{}/login/reset/", config.hosts.app), url: format!("{}/login/reset/", config.hosts.app),
html: None, html: None,
}, },
reset_existing: Template {
title: "Reset your password.".into(),
text: include_str!("../../templates/reset.whitelabel.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: None,
},
deletion: Template { deletion: Template {
title: "Confirm account deletion.".into(), title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.whitelabel.txt").into(), text: include_str!("../../templates/deletion.whitelabel.txt")
.into(),
url: format!("{}/delete/", config.hosts.app), url: format!("{}/delete/", config.hosts.app),
html: None, html: None,
}, },
+10 -8
View File
@@ -31,17 +31,17 @@ impl MongoDb {
} }
/// Get a collection by its name /// Get a collection by its name
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> { pub fn col<T: Send + Sync>(&self, collection: &str) -> mongodb::Collection<T> {
self.db().collection(collection) self.db().collection(collection)
} }
/// Insert one document into a collection /// Insert one document into a collection
pub async fn insert_one<T: Serialize>( pub async fn insert_one<T: Serialize + Send + Sync>(
&self, &self,
collection: &'static str, collection: &'static str,
document: T, document: T,
) -> Result<InsertOneResult> { ) -> Result<InsertOneResult> {
self.col::<T>(collection).insert_one(document, None).await self.col::<T>(collection).insert_one(document).await
} }
/// Count documents by projection /// Count documents by projection
@@ -51,7 +51,7 @@ impl MongoDb {
projection: Document, projection: Document,
) -> Result<u64> { ) -> Result<u64> {
self.col::<Document>(collection) self.col::<Document>(collection)
.count_documents(projection, None) .count_documents(projection)
.await .await
} }
@@ -67,7 +67,8 @@ impl MongoDb {
{ {
Ok(self Ok(self
.col::<T>(collection) .col::<T>(collection)
.find(projection, options) .find(projection)
.with_options(options)
.await? .await?
.filter_map(|s| async { .filter_map(|s| async {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
@@ -101,7 +102,8 @@ impl MongoDb {
O: Into<Option<FindOneOptions>>, O: Into<Option<FindOneOptions>>,
{ {
self.col::<T>(collection) self.col::<T>(collection)
.find_one(projection, options) .find_one(projection)
.with_options(options)
.await .await
} }
@@ -165,7 +167,7 @@ impl MongoDb {
}; };
self.col::<Document>(collection) self.col::<Document>(collection)
.update_one(projection, query, None) .update_one(projection, query)
.await .await
} }
@@ -200,7 +202,7 @@ impl MongoDb {
projection: Document, projection: Document,
) -> Result<DeleteResult> { ) -> Result<DeleteResult> {
self.col::<Document>(collection) self.col::<Document>(collection)
.delete_one(projection, None) .delete_one(projection)
.await .await
} }
@@ -11,6 +11,12 @@ pub struct MessageSentPayload {
pub users: Vec<String>, pub users: Vec<String>,
} }
#[derive(Serialize, Deserialize)]
pub struct MassMessageSentPayload {
pub notifications: Vec<PushNotification>,
pub server_id: String,
}
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct FRAcceptedPayload { pub struct FRAcceptedPayload {
pub accepted_user: User, pub accepted_user: User,
@@ -10,7 +10,7 @@ impl AbstractMigrations for MongoDb {
#[cfg(test)] #[cfg(test)]
/// Drop the database /// Drop the database
async fn drop_database(&self) { async fn drop_database(&self) {
self.db().drop(None).await.ok(); self.db().drop().await.ok();
} }
/// Migrate the database /// Migrate the database
@@ -18,7 +18,7 @@ impl AbstractMigrations for MongoDb {
info!("Migrating the database."); info!("Migrating the database.");
let list = self let list = self
.list_database_names(None, None) .list_database_names()
.await .await
.expect("Failed to fetch database names."); .expect("Failed to fetch database names.");
@@ -8,84 +8,84 @@ pub async fn create_database(db: &MongoDb) {
info!("Creating database."); info!("Creating database.");
let db = db.db(); let db = db.db();
db.create_collection("accounts", None) db.create_collection("accounts")
.await .await
.expect("Failed to create accounts collection."); .expect("Failed to create accounts collection.");
db.create_collection("users", None) db.create_collection("users")
.await .await
.expect("Failed to create users collection."); .expect("Failed to create users collection.");
db.create_collection("channels", None) db.create_collection("channels")
.await .await
.expect("Failed to create channels collection."); .expect("Failed to create channels collection.");
db.create_collection("messages", None) db.create_collection("messages")
.await .await
.expect("Failed to create messages collection."); .expect("Failed to create messages collection.");
db.create_collection("servers", None) db.create_collection("servers")
.await .await
.expect("Failed to create servers collection."); .expect("Failed to create servers collection.");
db.create_collection("server_members", None) db.create_collection("server_members")
.await .await
.expect("Failed to create server_members collection."); .expect("Failed to create server_members collection.");
db.create_collection("server_bans", None) db.create_collection("server_bans")
.await .await
.expect("Failed to create server_bans collection."); .expect("Failed to create server_bans collection.");
db.create_collection("channel_invites", None) db.create_collection("channel_invites")
.await .await
.expect("Failed to create channel_invites collection."); .expect("Failed to create channel_invites collection.");
db.create_collection("channel_unreads", None) db.create_collection("channel_unreads")
.await .await
.expect("Failed to create channel_unreads collection."); .expect("Failed to create channel_unreads collection.");
db.create_collection("channel_webhooks", None) db.create_collection("channel_webhooks")
.await .await
.expect("Failed to create channel_webhooks collection."); .expect("Failed to create channel_webhooks collection.");
db.create_collection("migrations", None) db.create_collection("migrations")
.await .await
.expect("Failed to create migrations collection."); .expect("Failed to create migrations collection.");
db.create_collection("attachments", None) db.create_collection("attachments")
.await .await
.expect("Failed to create attachments collection."); .expect("Failed to create attachments collection.");
db.create_collection("attachment_hashes", None) db.create_collection("attachment_hashes")
.await .await
.expect("Failed to create attachment_hashes collection."); .expect("Failed to create attachment_hashes collection.");
db.create_collection("user_settings", None) db.create_collection("user_settings")
.await .await
.expect("Failed to create user_settings collection."); .expect("Failed to create user_settings collection.");
db.create_collection("safety_reports", None) db.create_collection("safety_reports")
.await .await
.expect("Failed to create safety_reports collection."); .expect("Failed to create safety_reports collection.");
db.create_collection("safety_snapshots", None) db.create_collection("safety_snapshots")
.await .await
.expect("Failed to create safety_snapshots collection."); .expect("Failed to create safety_snapshots collection.");
db.create_collection("safety_strikes", None) db.create_collection("safety_strikes")
.await .await
.expect("Failed to create safety_strikes collection."); .expect("Failed to create safety_strikes collection.");
db.create_collection("bots", None) db.create_collection("bots")
.await .await
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
db.create_collection("ratelimit_events", None) db.create_collection("ratelimit_events")
.await .await
.expect("Failed to create ratelimit_events collection."); .expect("Failed to create ratelimit_events collection.");
db.create_collection( db.create_collection("pubsub")
"pubsub", .with_options(
CreateCollectionOptions::builder() CreateCollectionOptions::builder()
.capped(true) .capped(true)
.size(1_000_000) .size(1_000_000)
@@ -94,8 +94,7 @@ pub async fn create_database(db: &MongoDb) {
.await .await
.expect("Failed to create pubsub collection."); .expect("Failed to create pubsub collection.");
db.run_command( db.run_command(doc! {
doc! {
"createIndexes": "users", "createIndexes": "users",
"indexes": [ "indexes": [
{ {
@@ -122,14 +121,11 @@ pub async fn create_database(db: &MongoDb) {
} }
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create username index."); .expect("Failed to create username index.");
db.run_command( db.run_command(doc! {
doc! {
"createIndexes": "messages", "createIndexes": "messages",
"indexes": [ "indexes": [
{ {
@@ -159,14 +155,11 @@ pub async fn create_database(db: &MongoDb) {
"name": "channel_pinned_compound" "name": "channel_pinned_compound"
}, },
] ]
}, })
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
db.run_command( db.run_command(doc! {
doc! {
"createIndexes": "channel_unreads", "createIndexes": "channel_unreads",
"indexes": [ "indexes": [
{ {
@@ -183,14 +176,11 @@ pub async fn create_database(db: &MongoDb) {
"name": "user_id" "name": "user_id"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create channel_unreads index."); .expect("Failed to create channel_unreads index.");
db.run_command( db.run_command(doc! {
doc! {
"createIndexes": "server_members", "createIndexes": "server_members",
"indexes": [ "indexes": [
{ {
@@ -207,14 +197,11 @@ pub async fn create_database(db: &MongoDb) {
"name": "user_id" "name": "user_id"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create server_members index."); .expect("Failed to create server_members index.");
db.run_command( db.run_command(doc! {
doc! {
"createIndexes": "attachments", "createIndexes": "attachments",
"indexes": [ "indexes": [
{ {
@@ -230,14 +217,11 @@ pub async fn create_database(db: &MongoDb) {
"name": "used_for_id" "name": "used_for_id"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create attachments index."); .expect("Failed to create attachments index.");
db.run_command( db.run_command(doc! {
doc! {
"createIndexes": "attachment_hashes", "createIndexes": "attachment_hashes",
"indexes": [ "indexes": [
{ {
@@ -247,25 +231,19 @@ pub async fn create_database(db: &MongoDb) {
"name": "processed_hash" "name": "processed_hash"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create attachment_hashes index."); .expect("Failed to create attachment_hashes index.");
db.collection("migrations") db.collection("migrations")
.insert_one( .insert_one(doc! {
doc! {
"_id": 0_i32, "_id": 0_i32,
"revision": LATEST_REVISION "revision": LATEST_REVISION
}, })
None,
)
.await .await
.expect("Failed to save migration info."); .expect("Failed to save migration info.");
db.run_command( db.run_command(doc! {
doc! {
"createIndexes": "ratelimit_events", "createIndexes": "ratelimit_events",
"indexes": [ "indexes": [
{ {
@@ -277,9 +255,7 @@ pub async fn create_database(db: &MongoDb) {
"name": "compound_key" "name": "compound_key"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create ratelimit_events index."); .expect("Failed to create ratelimit_events index.");
@@ -21,12 +21,12 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 31; pub const LATEST_REVISION: i32 = 32;
pub async fn migrate_database(db: &MongoDb) { pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations"); let migrations = db.col::<Document>("migrations");
let data = migrations let data = migrations
.find_one(None, None) .find_one(doc! {})
.await .await
.expect("Failed to fetch migration data."); .expect("Failed to fetch migration data.");
@@ -46,7 +46,6 @@ pub async fn migrate_database(db: &MongoDb) {
"revision": revision "revision": revision
} }
}, },
None,
) )
.await .await
.expect("Failed to commit migration information."); .expect("Failed to commit migration information.");
@@ -74,7 +73,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_many( .update_many(
doc! { "attachment": { "$exists": 1_i32 } }, doc! { "attachment": { "$exists": 1_i32 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0_i32 } }, doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0_i32 } },
None,
) )
.await .await
.expect("Failed to update messages."); .expect("Failed to update messages.");
@@ -83,7 +81,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_many( .update_many(
doc! {}, doc! {},
doc! { "$set": { "tag": "attachments", "size": 0_i32 } }, doc! { "$set": { "tag": "attachments", "size": 0_i32 } },
None,
) )
.await .await
.expect("Failed to update attachments."); .expect("Failed to update attachments.");
@@ -93,7 +90,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection."); info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
db.db() db.db()
.create_collection("servers", None) .create_collection("servers")
.await .await
.expect("Failed to create servers collection."); .expect("Failed to create servers collection.");
} }
@@ -103,12 +100,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let messages = db.col::<Document>("messages"); let messages = db.col::<Document>("messages");
let mut cursor = messages let mut cursor = messages
.find( .find(doc! {
doc! {
"attachment": { "attachment": {
"$exists": 1_i32 "$exists": 1_i32
} }
}, })
.with_options(
FindOptions::builder() FindOptions::builder()
.projection(doc! { .projection(doc! {
"_id": 1_i32, "_id": 1_i32,
@@ -128,19 +125,18 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_one( .update_one(
doc! { "_id": id }, doc! { "_id": id },
doc! { "$unset": { "attachment": 1_i32 }, "$set": { "attachments": attachments } }, doc! { "$unset": { "attachment": 1_i32 }, "$set": { "attachments": attachments } },
None,
) )
.await .await
.unwrap(); .unwrap();
} }
db.db() db.db()
.create_collection("channel_unreads", None) .create_collection("channel_unreads")
.await .await
.expect("Failed to create channel_unreads collection."); .expect("Failed to create channel_unreads collection.");
db.db() db.db()
.create_collection("user_settings", None) .create_collection("user_settings")
.await .await
.expect("Failed to create user_settings collection."); .expect("Failed to create user_settings collection.");
} }
@@ -149,17 +145,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 4 / 2021-06-01]: Add more server collections."); info!("Running migration [revision 4 / 2021-06-01]: Add more server collections.");
db.db() db.db()
.create_collection("server_members", None) .create_collection("server_members")
.await .await
.expect("Failed to create server_members collection."); .expect("Failed to create server_members collection.");
db.db() db.db()
.create_collection("server_bans", None) .create_collection("server_bans")
.await .await
.expect("Failed to create server_bans collection."); .expect("Failed to create server_bans collection.");
db.db() db.db()
.create_collection("channel_invites", None) .create_collection("channel_invites")
.await .await
.expect("Failed to create channel_invites collection."); .expect("Failed to create channel_invites collection.");
} }
@@ -182,7 +178,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! { doc! {
"$set": to_document(&server).unwrap() "$set": to_document(&server).unwrap()
}, },
None,
) )
.await .await
.expect("Failed to migrate servers."); .expect("Failed to migrate servers.");
@@ -192,8 +187,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 6 / 2021-07-09]: Add message text index."); info!("Running migration [revision 6 / 2021-07-09]: Add message text index.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "messages", "createIndexes": "messages",
"indexes": [ "indexes": [
{ {
@@ -203,9 +197,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "content" "name": "content"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
} }
@@ -214,7 +206,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 7 / 2021-08-11]: Add message text index."); info!("Running migration [revision 7 / 2021-08-11]: Add message text index.");
db.db() db.db()
.create_collection("bots", None) .create_collection("bots")
.await .await
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
} }
@@ -223,22 +215,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1."); info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"dropIndexes": "accounts", "dropIndexes": "accounts",
"index": ["email", "email_normalised"] "index": ["email", "email_normalised"]
}, })
None,
)
.await .await
.expect("Failed to delete legacy account indexes."); .expect("Failed to delete legacy account indexes.");
let col = db.col::<Document>("sessions"); let col = db.col::<Document>("sessions");
let mut cursor = db let mut cursor = db.col::<Document>("accounts").find(doc! {}).await.unwrap();
.col::<Document>("accounts")
.find(doc! {}, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await { while let Some(doc) = cursor.next().await {
if let Ok(account) = doc { if let Ok(account) = doc {
@@ -267,7 +252,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc.insert("subscription", sub); doc.insert("subscription", sub);
} }
col.insert_one(doc, None).await.ok(); col.insert_one(doc).await.ok();
} }
} else { } else {
info!("Account doesn't have any sessions!"); info!("Account doesn't have any sessions!");
@@ -288,7 +273,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -297,11 +281,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 9 { if revision <= 9 {
info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id."); info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id.");
let mut cursor = db let mut cursor = db.col::<Document>("channels").find(doc! {}).await.unwrap();
.col::<Document>("channels")
.find(doc! {}, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await { while let Some(doc) = cursor.next().await {
if let Ok(channel) = doc { if let Ok(channel) = doc {
@@ -340,7 +320,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"last_message": 1_i32, "last_message": 1_i32,
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -362,7 +341,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"nonce": 1_i32, "nonce": 1_i32,
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -375,7 +353,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"nonce": 1_i32, "nonce": 1_i32,
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -385,8 +362,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database."); info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "messages", "createIndexes": "messages",
"indexes": [ "indexes": [
{ {
@@ -396,15 +372,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "channel" "name": "channel"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "channel_unreads", "createIndexes": "channel_unreads",
"indexes": [ "indexes": [
{ {
@@ -421,15 +394,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "user_id" "name": "user_id"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create channel_unreads index."); .expect("Failed to create channel_unreads index.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "server_members", "createIndexes": "server_members",
"indexes": [ "indexes": [
{ {
@@ -446,9 +416,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "user_id" "name": "user_id"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create server_members index."); .expect("Failed to create server_members index.");
} }
@@ -457,8 +425,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 12 / 2021-11-21]: Add indexes to database."); info!("Running migration [revision 12 / 2021-11-21]: Add indexes to database.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "messages", "createIndexes": "messages",
"indexes": [ "indexes": [
{ {
@@ -469,9 +436,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "channel_id_compound" "name": "channel_id_compound"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
} }
@@ -485,7 +450,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
async_std::task::sleep(Duration::from_secs(10)).await; async_std::task::sleep(Duration::from_secs(10)).await;
let servers = db.col::<Document>("servers"); let servers = db.col::<Document>("servers");
let mut cursor = servers.find(doc! {}, None).await.unwrap(); let mut cursor = servers.find(doc! {}).await.unwrap();
while let Some(Ok(mut document)) = cursor.next().await { while let Some(Ok(mut document)) = cursor.next().await {
let id = document.get_str("_id").unwrap().to_string(); let id = document.get_str("_id").unwrap().to_string();
@@ -529,13 +494,13 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
servers servers
.update_one(doc! { "_id": id }, doc! { "$set": update }, None) .update_one(doc! { "_id": id }, doc! { "$set": update })
.await .await
.unwrap(); .unwrap();
} }
let channels = db.col::<Document>("channels"); let channels = db.col::<Document>("channels");
let mut cursor = channels.find(doc! {}, None).await.unwrap(); let mut cursor = channels.find(doc! {}).await.unwrap();
while let Some(Ok(document)) = cursor.next().await { while let Some(Ok(document)) = cursor.next().await {
let id = document.get_str("_id").unwrap().to_string(); let id = document.get_str("_id").unwrap().to_string();
@@ -575,7 +540,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
channels channels
.update_one(doc! { "_id": id }, update, None) .update_one(doc! { "_id": id }, update)
.await .await
.unwrap(); .unwrap();
} }
@@ -596,7 +561,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"content": "system" "content": "system"
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -621,13 +585,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.unwrap(); .unwrap();
db.db() db.db()
.create_collection("emojis", None) .create_collection("emojis")
.await .await
.expect("Failed to create emojis collection."); .expect("Failed to create emojis collection.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "emojis", "createIndexes": "emojis",
"indexes": [ "indexes": [
{ {
@@ -637,9 +600,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "parent_id" "name": "parent_id"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create emoji parent index."); .expect("Failed to create emoji parent index.");
} }
@@ -652,10 +613,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! {}, doc! {},
doc! { doc! {
"$set": { "$set": {
"joined_at": DateTime::now().to_rfc3339_string() "joined_at": DateTime::now().try_to_rfc3339_string().expect("Failed to convert the date to rfc3339")
} }
}, },
None,
) )
.await .await
.expect("Failed to update server members."); .expect("Failed to update server members.");
@@ -666,13 +626,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if db if db
.db() .db()
.run_command( .run_command(doc! {
doc! {
"dropIndexes": "messages", "dropIndexes": "messages",
"index": ["channel"] "index": ["channel"]
}, })
None,
)
.await .await
.is_err() .is_err()
{ {
@@ -680,8 +637,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "messages", "createIndexes": "messages",
"indexes": [ "indexes": [
{ {
@@ -691,9 +647,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "author" "name": "author"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create messages author index."); .expect("Failed to create messages author index.");
} }
@@ -703,23 +657,16 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections." "Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
); );
db.db() db.db().create_collection("safety_reports").await.unwrap();
.create_collection("safety_reports", None)
.await
.unwrap();
db.db() db.db().create_collection("safety_snapshots").await.unwrap();
.create_collection("safety_snapshots", None)
.await
.unwrap();
} }
if revision <= 20 { if revision <= 20 {
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`."); info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "safety_snapshots", "createIndexes": "safety_snapshots",
"indexes": [ "indexes": [
{ {
@@ -729,9 +676,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "report_id" "name": "report_id"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create safety snapshot index."); .expect("Failed to create safety snapshot index.");
} }
@@ -739,10 +684,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 21 { if revision <= 21 {
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`."); info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
db.db() db.db().create_collection("safety_strikes").await.unwrap();
.create_collection("safety_strikes", None)
.await
.unwrap();
} }
if revision <= 22 { if revision <= 22 {
@@ -756,7 +698,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4" "moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4"
} }
}, },
None,
) )
.await .await
.expect("Failed to update server members."); .expect("Failed to update server members.");
@@ -766,13 +707,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users."); info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"dropIndexes": "users", "dropIndexes": "users",
"index": "username" "index": "username"
}, })
None,
)
.await .await
.expect("Failed to drop existing username index."); .expect("Failed to drop existing username index.");
@@ -787,7 +725,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let users: Vec<UserInformation> = db let users: Vec<UserInformation> = db
.col::<UserInformation>("users") .col::<UserInformation>("users")
.find(doc! {}, None) .find(doc! {})
.await .await
.unwrap() .unwrap()
.map(|doc| doc.expect("id and username")) .map(|doc| doc.expect("id and username"))
@@ -839,7 +777,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"discriminator": discriminator "discriminator": discriminator
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -887,7 +824,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"display_name": &info.username "display_name": &info.username
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -898,14 +834,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 24 { if revision <= 24 {
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index."); info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
db.db() db.db().create_collection("channel_webhooks").await.ok();
.create_collection("channel_webhooks", None)
.await
.ok();
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "users", "createIndexes": "users",
"indexes": [ "indexes": [
{ {
@@ -932,9 +864,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create username index."); .expect("Failed to create username index.");
}; };
@@ -950,7 +880,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64 "permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64
} }
}, },
None,
) )
.await .await
.expect("Failed to update webhooks."); .expect("Failed to update webhooks.");
@@ -959,14 +888,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 25 { if revision <= 25 {
info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index."); info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index.");
db.db() db.db().create_collection("ratelimit_events").await.ok();
.create_collection("ratelimit_events", None)
.await
.ok();
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "ratelimit_events", "createIndexes": "ratelimit_events",
"indexes": [ "indexes": [
{ {
@@ -978,9 +903,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "compound_key" "name": "compound_key"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create ratelimit_events index."); .expect("Failed to create ratelimit_events index.");
} }
@@ -1016,12 +939,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let invites = db let invites = db
.db() .db()
.collection::<Outer>("channel_invites") .collection::<Outer>("channel_invites")
.find( .find(doc! {
doc! {
"type": { "$exists": false } "type": { "$exists": false }
}, })
None,
)
.await .await
.expect("failed to find invites") .expect("failed to find invites")
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -1055,18 +975,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if !invites.is_empty() { if !invites.is_empty() {
db.db() db.db()
.collection("channel_invites") .collection("channel_invites")
.insert_many(invites, None) .insert_many(invites)
.await .await
.expect("failed to insert corrected invite"); .expect("failed to insert corrected invite");
db.db() db.db()
.collection::<Outer>("channel_invites") .collection::<Outer>("channel_invites")
.delete_many( .delete_many(doc! {
doc! {
"type": { "$exists": false } "type": { "$exists": false }
}, })
None,
)
.await .await
.expect("failed to find invites"); .expect("failed to find invites");
} }
@@ -1076,8 +993,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index."); info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "messages", "createIndexes": "messages",
"indexes": [ "indexes": [
{ {
@@ -1088,9 +1004,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "channel_pinned_compound" "name": "channel_pinned_compound"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
} }
@@ -1098,14 +1012,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 28 { if revision <= 28 {
info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn."); info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn.");
db.db() db.db().create_collection("attachment_hashes").await.ok();
.create_collection("attachment_hashes", None)
.await
.ok();
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "attachments", "createIndexes": "attachments",
"indexes": [ "indexes": [
{ {
@@ -1115,15 +1025,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "hash" "name": "hash"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create attachments index."); .expect("Failed to create attachments index.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "attachment_hashes", "createIndexes": "attachment_hashes",
"indexes": [ "indexes": [
{ {
@@ -1133,9 +1040,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "processed_hash" "name": "processed_hash"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create attachment_hashes index."); .expect("Failed to create attachment_hashes index.");
} }
@@ -1146,8 +1051,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 30 / 29-09-2024]: Add index for used_for.id to attachments."); info!("Running migration [revision 30 / 29-09-2024]: Add index for used_for.id to attachments.");
db.db() db.db()
.run_command( .run_command(doc! {
doc! {
"createIndexes": "attachments", "createIndexes": "attachments",
"indexes": [ "indexes": [
{ {
@@ -1157,9 +1061,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"name": "used_for_id" "name": "used_for_id"
} }
] ]
}, })
None,
)
.await .await
.expect("Failed to create attachments index."); .expect("Failed to create attachments index.");
} }
@@ -1176,7 +1078,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let webhooks = db let webhooks = db
.db() .db()
.collection::<WebhookShell>("channel_webhooks") .collection::<WebhookShell>("channel_webhooks")
.find(doc! {}, None) .find(doc! {})
.await .await
.expect("webhooks") .expect("webhooks")
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -1207,7 +1109,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"creator_id": creator_id "creator_id": creator_id
} }
}, },
None,
) )
.await .await
.expect("update webhook"); .expect("update webhook");
@@ -1218,7 +1119,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}) => { }) => {
db.db() db.db()
.collection::<WebhookShell>("channel_webhooks") .collection::<WebhookShell>("channel_webhooks")
.delete_one(doc! { "_id": webhook._id }, None) .delete_one(doc! { "_id": webhook._id })
.await .await
.expect("failed to delete invalid webhook"); .expect("failed to delete invalid webhook");
} }
@@ -1227,6 +1128,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
} }
if revision <= 32 {
info!(
"Running migration [revision 32 / 12-05-2025]: (Authifier) Add last_seen to sessions."
);
let db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
db.run_migration(authifier::Migration::M2025_02_20AddLastSeenToSession)
.await
.unwrap();
}
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision) LATEST_REVISION.max(revision)
} }
@@ -1,4 +1,3 @@
use revolt_config::config;
use revolt_result::Result; use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
@@ -24,12 +24,9 @@ impl AbstractChannelInvites for MongoDb {
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> { async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
Ok(self Ok(self
.col::<Invite>(COL) .col::<Invite>(COL)
.find( .find(doc! {
doc! {
"server": server_id, "server": server_id,
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -26,6 +26,14 @@ pub trait AbstractChannelUnreads: Sync + Send {
message_ids: &[String], message_ids: &[String],
) -> Result<()>; ) -> Result<()>;
/// Add a mention.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()>;
/// Fetch all unreads with mentions for a user. /// Fetch all unreads with mentions for a user.
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>; async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
@@ -37,6 +37,8 @@ impl AbstractChannelUnreads for MongoDb {
"last_id": message_id "last_id": message_id
} }
}, },
)
.with_options(
FindOneAndUpdateOptions::builder() FindOneAndUpdateOptions::builder()
.upsert(true) .upsert(true)
.return_document(ReturnDocument::After) .return_document(ReturnDocument::After)
@@ -51,15 +53,12 @@ impl AbstractChannelUnreads for MongoDb {
let current_time = Ulid::new().to_string(); let current_time = Ulid::new().to_string();
self.col::<Document>(COL) self.col::<Document>(COL)
.delete_many( .delete_many(doc! {
doc! {
"_id.channel": { "_id.channel": {
"$in": channel_ids "$in": channel_ids
}, },
"_id.user": user_id "_id.user": user_id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", COL))?; .map_err(|_| create_database_error!("delete_many", COL))?;
@@ -77,7 +76,6 @@ impl AbstractChannelUnreads for MongoDb {
} }
}) })
.collect::<Vec<Document>>(), .collect::<Vec<Document>>(),
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -104,13 +102,42 @@ impl AbstractChannelUnreads for MongoDb {
} }
} }
}, },
UpdateOptions::builder().upsert(true).build(),
) )
.with_options(UpdateOptions::builder().upsert(true).build())
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("update_one", COL)) .map_err(|_| create_database_error!("update_one", COL))
} }
/// Add a mention to multiple users.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()> {
self.col::<Document>(COL)
.update_many(
doc! {
"_id.channel": channel_id,
"_id.user": {
"$in": user_ids
},
},
doc! {
"$push": {
"mentions": {
"$each": message_ids
}
}
},
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
}
/// Fetch all channel unreads for a user. /// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> { async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query!( query!(
@@ -78,6 +78,38 @@ impl AbstractChannelUnreads for ReferenceDb {
Ok(()) Ok(())
} }
/// Add a mention to multiple users.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
for user_id in user_ids {
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(unread) = unreads.get_mut(&key) {
unread.mentions.replace(message_ids.to_vec());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: None,
mentions: Some(message_ids.to_vec()),
},
);
}
}
Ok(())
}
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> { async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await; let unreads = self.channel_unreads.lock().await;
Ok(unreads Ok(unreads
@@ -142,7 +142,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
db.insert_webhook(&webhook).await.unwrap(); webhook.create(&db).await.unwrap();
let mut updated_webhook = webhook.clone(); let mut updated_webhook = webhook.clone();
updated_webhook updated_webhook
@@ -24,12 +24,9 @@ impl AbstractWebhooks for MongoDb {
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> { async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
Ok(self Ok(self
.col::<Webhook>(COL) .col::<Webhook>(COL)
.find( .find(doc! {
doc! {
"channel_id": channel_id, "channel_id": channel_id,
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -269,16 +269,24 @@ impl Channel {
})); }));
} }
let id = ulid::Ulid::new().to_string();
let icon = if let Some(icon_id) = data.icon {
Some(File::use_channel_icon(db, &icon_id, &id, &owner_id).await?)
} else {
None
};
let recipients = data.users.into_iter().collect::<Vec<String>>(); let recipients = data.users.into_iter().collect::<Vec<String>>();
let channel = Channel::Group { let channel = Channel::Group {
id: ulid::Ulid::new().to_string(), id,
name: data.name, name: data.name,
owner: owner_id, owner: owner_id,
description: data.description, description: data.description,
recipients: recipients.clone(), recipients: recipients.clone(),
icon: None, icon,
last_message_id: None, last_message_id: None,
permissions: None, permissions: None,
@@ -320,13 +328,10 @@ impl Channel {
db.insert_channel(&channel).await?; db.insert_channel(&channel).await?;
match &channel { if let Channel::DirectMessage { .. } = &channel {
Channel::DirectMessage { .. } => {
let event = EventV1::ChannelCreate(channel.clone().into()); let event = EventV1::ChannelCreate(channel.clone().into());
event.clone().private(user_a.id.clone()).await; event.clone().private(user_a.id.clone()).await;
event.private(user_b.id.clone()).await; event.private(user_b.id.clone()).await;
}
_ => {}
}; };
Ok(channel) Ok(channel)
@@ -23,14 +23,11 @@ impl AbstractChannels for MongoDb {
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> { async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
Ok(self Ok(self
.col::<Channel>(COL) .col::<Channel>(COL)
.find( .find(doc! {
doc! {
"_id": { "_id": {
"$in": ids "$in": ids
} }
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("fetch", "channels"))? .map_err(|_| create_database_error!("fetch", "channels"))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -119,7 +116,6 @@ impl AbstractChannels for MongoDb {
"recipients": user "recipients": user
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -141,7 +137,6 @@ impl AbstractChannels for MongoDb {
"role_permissions.".to_owned() + role: permissions "role_permissions.".to_owned() + role: permissions
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -179,7 +174,6 @@ impl AbstractChannels for MongoDb {
"recipients": user "recipients": user
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -253,7 +247,6 @@ impl AbstractChannels for MongoDb {
"_id": server.id "_id": server.id
}, },
update, update,
None,
) )
.await .await
.map_err(|_| create_database_error!("update_one", "servers"))?; .map_err(|_| create_database_error!("update_one", "servers"))?;
@@ -274,23 +267,17 @@ impl MongoDb {
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> { pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
// Delete all invites to these channels. // Delete all invites to these channels.
self.col::<Document>("channel_invites") self.col::<Document>("channel_invites")
.delete_many( .delete_many(doc! {
doc! {
"channel": &id "channel": &id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?; .map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
// Delete unread message objects on channels. // Delete unread message objects on channels.
self.col::<Document>("channel_unreads") self.col::<Document>("channel_unreads")
.delete_many( .delete_many(doc! {
doc! {
"_id.channel": &id "_id.channel": &id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", "channel_unreads")) .map_err(|_| create_database_error!("delete_many", "channel_unreads"))
.map(|_| ())?; .map(|_| ())?;
@@ -299,12 +286,9 @@ impl MongoDb {
// Delete all webhooks on this channel. // Delete all webhooks on this channel.
self.col::<Document>("webhooks") self.col::<Document>("webhooks")
.delete_many( .delete_many(doc! {
doc! {
"channel": &id "channel": &id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", "webhooks")) .map_err(|_| create_database_error!("delete_many", "webhooks"))
.map(|_| ()) .map(|_| ())
@@ -60,7 +60,6 @@ impl AbstractEmojis for MongoDb {
} }
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -28,7 +28,7 @@ auto_derived_partial!(
/// Size of this file (in bytes) /// Size of this file (in bytes)
pub size: isize, pub size: isize,
}, },
"PartialFile" "PartialFileHash"
); );
auto_derived!( auto_derived!(
@@ -42,7 +42,6 @@ impl AbstractAttachmentHashes for MongoDb {
"iv": nonce "iv": nonce
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -110,7 +110,6 @@ impl AbstractAttachments for MongoDb {
"uploader_id": uploader_id "uploader_id": uploader_id
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_one", COL))?; .map_err(|_| create_database_error!("update_one", COL))?;
@@ -130,7 +129,6 @@ impl AbstractAttachments for MongoDb {
"reported": true "reported": true
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -149,7 +147,6 @@ impl AbstractAttachments for MongoDb {
"deleted": true "deleted": true
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -170,7 +167,6 @@ impl AbstractAttachments for MongoDb {
"deleted": true "deleted": true
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -193,7 +189,6 @@ impl MongoDb {
"deleted": true "deleted": true
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
+213 -41
View File
@@ -5,17 +5,20 @@ use iso8601_timestamp::Timestamp;
use revolt_config::{config, FeaturesLimits}; use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{ use revolt_models::v0::{
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort, self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION, MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text,
}; };
use revolt_permissions::{ChannelPermission, PermissionValue}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
use revolt_result::Result; use revolt_result::{ErrorType, Result};
use ulid::Ulid; use ulid::Ulid;
use validator::Validate; use validator::Validate;
use crate::{ use crate::{
events::client::EventV1, events::client::EventV1,
tasks::{self, ack::AckEvent}, tasks::{self, ack::AckEvent},
util::{bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey}, util::{
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
permissions::DatabasePermissionQuery,
},
Channel, Database, Emoji, File, User, AMQP, Channel, Database, Emoji, File, User, AMQP,
}; };
@@ -53,6 +56,9 @@ auto_derived_partial!(
/// Array of user ids mentioned in this message /// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>, pub mentions: Option<Vec<String>>,
/// Array of role ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub role_mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to /// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>, pub replies: Option<Vec<String>>,
@@ -71,7 +77,7 @@ auto_derived_partial!(
/// Bitfield of message flags /// Bitfield of message flags
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>, pub flags: Option<u32>,
}, },
"PartialMessage" "PartialMessage"
); );
@@ -199,6 +205,30 @@ auto_derived!(
} }
); );
pub struct MessageFlagsValue(pub u32);
impl MessageFlagsValue {
pub fn has(&self, flag: MessageFlags) -> bool {
self.has_value(flag as u32)
}
pub fn has_value(&self, bit: u32) -> bool {
let mask = 1 << bit;
self.0 & mask == mask
}
pub fn set(&mut self, flag: MessageFlags, toggle: bool) -> &mut Self {
self.set_value(flag as u32, toggle)
}
pub fn set_value(&mut self, bit: u32, toggle: bool) -> &mut Self {
if toggle {
self.0 |= 1 << bit;
} else {
self.0 &= !(1 << bit);
}
self
}
}
#[allow(clippy::derivable_impls)] #[allow(clippy::derivable_impls)]
impl Default for Message { impl Default for Message {
fn default() -> Self { fn default() -> Self {
@@ -214,6 +244,7 @@ impl Default for Message {
edited: None, edited: None,
embeds: None, embeds: None,
mentions: None, mentions: None,
role_mentions: None,
replies: None, replies: None,
reactions: Default::default(), reactions: Default::default(),
interactions: Default::default(), interactions: Default::default(),
@@ -255,20 +286,50 @@ impl Message {
.map_err(|_| create_error!(InvalidOperation))?; .map_err(|_| create_error!(InvalidOperation))?;
// Check the message is not empty // Check the message is not empty
if (data.content.as_ref().map_or(true, |v| v.is_empty())) if (data.content.as_ref().is_none_or(|v| v.is_empty()))
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty())) && (data.attachments.as_ref().is_none_or(|v| v.is_empty()))
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty())) && (data.embeds.as_ref().is_none_or(|v| v.is_empty()))
{ {
return Err(create_error!(EmptyMessage)); return Err(create_error!(EmptyMessage));
} }
// Ensure flags are either not set or have permissible values let allow_mass_mentions = allow_mentions && config.features.mass_mentions_enabled;
if let Some(flags) = &data.flags {
if flags != &0 && flags != &1 { let mut mentions_everyone = false;
let mut mentions_online = false;
let mut suppress_notifications = false;
if let Some(raw_flags) = &data.flags {
if raw_flags > &7 {
// quick path to failure: bigger than all the bits combined
return Err(create_error!(InvalidProperty)); return Err(create_error!(InvalidProperty));
} }
// First step of mass mention resolution
let flags = MessageFlagsValue(*raw_flags);
suppress_notifications = flags.has(MessageFlags::SuppressNotifications);
mentions_everyone = allow_mentions && flags.has(MessageFlags::MentionsEveryone);
mentions_online = allow_mentions && flags.has(MessageFlags::MentionsOnline);
// Not a bot, and attempting to set mention flags
if user.as_ref().is_some_and(|u| u.bot.as_ref().is_none())
&& (mentions_everyone || mentions_online)
{
return Err(create_error!(IsNotBot));
} }
if mentions_everyone && mentions_online {
return Err(create_error!(InvalidFlagValue));
}
}
let server_id = match channel {
Channel::TextChannel { ref server, .. } | Channel::VoiceChannel { ref server, .. } => {
Some(server.clone())
}
_ => None,
};
// Ensure restrict_reactions is not specified without reactions list // Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions { if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions { if interactions.restrict_reactions {
@@ -302,18 +363,76 @@ impl Message {
.unwrap_or_default(), .unwrap_or_default(),
author: author_id, author: author_id,
webhook: webhook.map(|w| w.into()), webhook: webhook.map(|w| w.into()),
flags: data.flags.map(|v| v as i32), flags: data.flags,
..Default::default() ..Default::default()
}; };
// Parse mentions in message. // Parse mentions in message.
let mut mentions = HashSet::new();
if allow_mentions { let mut message_mentions = if let Some(raw_content) = &data.content {
if let Some(content) = &data.content { revolt_parser::parse_message(raw_content)
for capture in RE_MENTION.captures_iter(content) { } else {
if let Some(mention) = capture.get(1) { revolt_parser::MessageResults::default()
mentions.insert(mention.as_str().to_string()); };
message_mentions.mentions_everyone |= mentions_everyone;
message_mentions.mentions_online |= mentions_online;
let revolt_parser::MessageResults {
mut user_mentions,
mut role_mentions,
mut mentions_everyone,
mut mentions_online,
} = message_mentions;
if allow_mass_mentions && server_id.is_some() && !role_mentions.is_empty() {
let server_data = db
.fetch_server(server_id.unwrap().as_str())
.await
.expect("Failed to fetch server");
role_mentions.retain(|role_id| server_data.roles.contains_key(role_id));
} }
// Validate the user can perform a mass mention
if !config.features.mass_mentions_enabled
&& (mentions_everyone || mentions_online || !role_mentions.is_empty())
{
mentions_everyone = false;
mentions_online = false;
role_mentions.clear();
} else if mentions_everyone || mentions_online || !role_mentions.is_empty() {
debug!(
"Mentioned everyone: {}, mentioned online: {}, mentioned roles: {:?}",
mentions_everyone, mentions_online, &role_mentions
);
if let Some(user) = match author {
MessageAuthor::User(user) => Some(Ok(user)),
MessageAuthor::System { .. } => Some(Err(())), // DISALLOWED
MessageAuthor::Webhook(..) => None, // Bypass check
} {
if user.is_err() {
return Err(create_error!(InvalidProperty));
}
let owned_user: User = user.unwrap().to_owned().into();
let mut query = DatabasePermissionQuery::new(db, &owned_user).channel(&channel);
let perms = calculate_channel_permissions(&mut query).await;
if (mentions_everyone || mentions_online)
&& !perms.has_channel_permission(ChannelPermission::MentionEveryone)
{
return Err(create_error!(MissingPermission {
permission: ChannelPermission::MentionEveryone.to_string()
}));
}
if !role_mentions.is_empty()
&& !perms.has_channel_permission(ChannelPermission::MentionRoles)
{
return Err(create_error!(MissingPermission {
permission: ChannelPermission::MentionRoles.to_string()
}));
} }
} }
} }
@@ -327,38 +446,56 @@ impl Message {
})); }));
} }
for ReplyIntent { id, mention } in entries { for ReplyIntent {
let message = db.fetch_message(&id).await?; id,
mention,
fail_if_not_exists,
} in entries
{
match db.fetch_message(&id).await {
// Referenced message exists
Ok(message) => {
if mention && allow_mentions { if mention && allow_mentions {
mentions.insert(message.author.to_owned()); user_mentions.insert(message.author.to_owned());
} }
replies.insert(message.id); replies.insert(message.id);
} }
// If the referenced message doesn't exist and fail_if_not_exists
// is set to false, send the message without the reply.
Err(e) => {
if !matches!(e.error_type, ErrorType::NotFound)
|| fail_if_not_exists.unwrap_or(true)
{
return Err(e);
}
}
}
}
} }
// Validate the mentions go to users in the channel/server // Validate the mentions go to users in the channel/server
if !mentions.is_empty() { if !user_mentions.is_empty() {
match channel { match channel {
Channel::DirectMessage { ref recipients, .. } Channel::DirectMessage { ref recipients, .. }
| Channel::Group { ref recipients, .. } => { | Channel::Group { ref recipients, .. } => {
let recipients_hash: HashSet<&String, RandomState> = let recipients_hash = HashSet::<&String, RandomState>::from_iter(recipients);
HashSet::from_iter(recipients); user_mentions.retain(|m| recipients_hash.contains(m));
mentions.retain(|m| recipients_hash.contains(m)); role_mentions.clear();
} }
Channel::TextChannel { ref server, .. } Channel::TextChannel { ref server, .. }
| Channel::VoiceChannel { ref server, .. } => { | Channel::VoiceChannel { ref server, .. } => {
let mentions_vec = Vec::from_iter(mentions.iter().cloned()); let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await; let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
if let Ok(valid_members) = valid_members { if let Ok(valid_members) = valid_members {
let valid_mentions: HashSet<&String, RandomState> = let valid_mentions = HashSet::<&String, RandomState>::from_iter(
HashSet::from_iter(valid_members.iter().map(|m| &m.id.user)); valid_members.iter().map(|m| &m.id.user),
);
mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
if !mentions.is_empty() { if !user_mentions.is_empty() {
// if there are still mentions, drill down to a channel-level // if there are still mentions, drill down to a channel-level
let member_channel_view_perms = let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server) BulkDatabasePermissionQuery::from_server_id(db, server)
@@ -368,7 +505,7 @@ impl Message {
.members_can_see_channel() .members_can_see_channel()
.await; .await;
mentions user_mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false)); .retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
} }
} else { } else {
@@ -377,13 +514,21 @@ impl Message {
} }
} }
Channel::SavedMessages { .. } => { Channel::SavedMessages { .. } => {
mentions.clear(); user_mentions.clear();
} }
} }
} }
if !mentions.is_empty() { if !user_mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect()); message
.mentions
.replace(user_mentions.into_iter().collect());
}
if !role_mentions.is_empty() {
message
.role_mentions
.replace(role_mentions.into_iter().collect());
} }
if !replies.is_empty() { if !replies.is_empty() {
@@ -392,6 +537,15 @@ impl Message {
.replace(replies.into_iter().collect::<Vec<String>>()); .replace(replies.into_iter().collect::<Vec<String>>());
} }
// Calculate final message flags
let mut flag_value = MessageFlagsValue(0);
flag_value
.set(MessageFlags::SuppressNotifications, suppress_notifications)
.set(MessageFlags::MentionsEveryone, mentions_everyone)
.set(MessageFlags::MentionsOnline, mentions_online);
message.flags = Some(flag_value.0);
// Add attachments to message. // Add attachments to message.
let mut attachments = vec![]; let mut attachments = vec![];
if data if data
@@ -470,7 +624,12 @@ impl Message {
tasks::ack::queue_message( tasks::ack::queue_message(
self.channel.to_string(), self.channel.to_string(),
AckEvent::ProcessMessage { AckEvent::ProcessMessage {
messages: vec![(None, self.clone(), mentions.clone(), true)], messages: vec![(
None,
self.clone(),
mentions.clone(),
self.has_suppressed_notifications(),
)],
}, },
) )
.await; .await;
@@ -497,7 +656,7 @@ impl Message {
pub async fn send( pub async fn send(
&mut self, &mut self,
db: &Database, db: &Database,
amqp: Option<&AMQP>, // this is optional mostly for tests. _amqp: Option<&AMQP>, // this is optional mostly for tests.
author: MessageAuthor<'_>, author: MessageAuthor<'_>,
user: Option<v0::User>, user: Option<v0::User>,
member: Option<v0::Member>, member: Option<v0::Member>,
@@ -514,7 +673,9 @@ impl Message {
) )
.await?; .await?;
if !self.has_suppressed_notifications() { if !self.has_suppressed_notifications()
&& (self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications // send Push notifications
tasks::ack::queue_message( tasks::ack::queue_message(
self.channel.to_string(), self.channel.to_string(),
@@ -537,7 +698,7 @@ impl Message {
} }
_ => vec![], _ => vec![],
}, },
self.has_suppressed_notifications(), false, // branch already dictates this
)], )],
}, },
) )
@@ -574,13 +735,24 @@ impl Message {
/// Whether this message has suppressed notifications /// Whether this message has suppressed notifications
pub fn has_suppressed_notifications(&self) -> bool { pub fn has_suppressed_notifications(&self) -> bool {
if let Some(flags) = self.flags { if let Some(flags) = self.flags {
flags & MessageFlags::SuppressNotifications as i32 flags & MessageFlags::SuppressNotifications as u32
== MessageFlags::SuppressNotifications as i32 == MessageFlags::SuppressNotifications as u32
} else { } else {
false false
} }
} }
pub fn contains_mass_push_mention(&self) -> bool {
let ping = if let Some(flags) = self.flags {
let flags = MessageFlagsValue(flags);
flags.has(MessageFlags::MentionsEveryone)
} else {
false
};
ping || self.role_mentions.is_some()
}
/// Update message data /// Update message data
pub async fn update( pub async fn update(
&mut self, &mut self,
@@ -228,7 +228,6 @@ impl AbstractMessages for MongoDb {
"_id": id "_id": id
}, },
query, query,
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -247,7 +246,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): user format!("reactions.{emoji}"): user
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -266,7 +264,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): user format!("reactions.{emoji}"): user
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -285,7 +282,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): 1 format!("reactions.{emoji}"): 1
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -300,15 +296,12 @@ impl AbstractMessages for MongoDb {
/// Delete messages from a channel by their ids and corresponding channel id /// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> { async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.col::<Document>(COL) self.col::<Document>(COL)
.delete_many( .delete_many(doc! {
doc! {
"channel": channel, "channel": channel,
"_id": { "_id": {
"$in": ids "$in": ids
} }
}, })
None,
)
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL)) .map_err(|_| create_database_error!("delete_many", COL))
@@ -362,7 +355,6 @@ impl MongoDb {
"deleted": true "deleted": true
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_many", "attachments"))?; .map_err(|_| create_database_error!("update_many", "attachments"))?;
@@ -370,7 +362,7 @@ impl MongoDb {
// And then delete said messages. // And then delete said messages.
self.col::<Document>(COL) self.col::<Document>(COL)
.delete_many(projection, None) .delete_many(projection)
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL)) .map_err(|_| create_database_error!("delete_many", COL))
@@ -23,16 +23,13 @@ impl AbstractRatelimitEvents for MongoDb {
count: usize, count: usize,
) -> Result<bool> { ) -> Result<bool> {
self.col::<RatelimitEvent>(COL) self.col::<RatelimitEvent>(COL)
.count_documents( .count_documents(doc! {
doc! {
"_id": { "_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string() "$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
}, },
"target_id": target_id, "target_id": target_id,
"event_type": event_type.to_string() "event_type": event_type.to_string()
}, })
None,
)
.await .await
.map(|c| c as usize >= count) .map(|c| c as usize >= count)
.map_err(|_| create_database_error!("count_documents", COL)) .map_err(|_| create_database_error!("count_documents", COL))
@@ -85,8 +85,8 @@ impl SnapshotContent {
Ok(( Ok((
SnapshotContent::Message { SnapshotContent::Message {
message, message,
prior_context: prior_context.into_iter().map(Into::into).collect(), prior_context: prior_context.into_iter().collect(),
leading_context: leading_context.into_iter().map(Into::into).collect(), leading_context: leading_context.into_iter().collect(),
}, },
files, files,
)) ))
@@ -13,6 +13,7 @@ auto_derived!(
} }
); );
#[allow(clippy::disallowed_methods)]
impl ServerBan { impl ServerBan {
/// Create ban /// Create ban
pub async fn create( pub async fn create(
@@ -159,7 +159,7 @@ impl Member {
} }
/// Update member data /// Update member data
pub async fn update<'a>( pub async fn update(
&mut self, &mut self,
db: &Database, db: &Database,
partial: PartialMember, partial: PartialMember,
@@ -1,3 +1,4 @@
use ::mongodb::SessionCursor;
use revolt_result::Result; use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
@@ -5,6 +6,66 @@ use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
mod mongodb; mod mongodb;
mod reference; mod reference;
#[derive(Debug)]
pub enum ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
MongoDb {
session: ::mongodb::ClientSession,
cursor: Option<SessionCursor<Member>>,
},
Reference {
offset: i32,
data: Option<Vec<Member>>,
},
}
impl ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
ChunkedServerMembersGenerator::MongoDb {
session,
cursor: Some(cursor),
}
}
pub fn new_reference(data: Vec<Member>) -> Self {
ChunkedServerMembersGenerator::Reference {
offset: 0,
data: Some(data),
}
}
pub async fn next(&mut self) -> Option<Member> {
match self {
#[cfg(feature = "mongodb")]
ChunkedServerMembersGenerator::MongoDb { session, cursor } => {
if let Some(cursor) = cursor {
let value = cursor.next(session).await;
value.map(|val| val.expect("Failed to fetch the next member"))
} else {
warn!("Attempted to access a (MongoDb) server member generator without first setting a cursor");
None
}
}
ChunkedServerMembersGenerator::Reference { offset, data } => {
if let Some(data) = data {
if data.len() as i32 >= *offset {
None
} else {
let resp = &data[*offset as usize];
*offset += 1;
Some(resp.clone())
}
} else {
warn!("Attempted to access a (Reference) server member generator without first providing data");
None
}
}
}
}
}
#[async_trait] #[async_trait]
pub trait AbstractServerMembers: Sync + Send { pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database /// Insert a new server member into the database
@@ -16,6 +77,24 @@ pub trait AbstractServerMembers: Sync + Send {
/// Fetch all members in a server /// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>; async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator>;
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>>;
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator>;
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>; async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
@@ -1,10 +1,11 @@
use futures::StreamExt; use futures::StreamExt;
use mongodb::options::ReadConcern;
use revolt_result::Result; use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{IntoDocumentPath, MongoDb}; use crate::{IntoDocumentPath, MongoDb};
use super::AbstractServerMembers; use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
static COL: &str = "server_members"; static COL: &str = "server_members";
@@ -33,12 +34,9 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> { async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find( .find(doc! {
doc! {
"_id.server": server_id "_id.server": server_id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -52,16 +50,101 @@ impl AbstractServerMembers for MongoDb {
.await) .await)
} }
/// Fetch all members in a server as a generator.
/// Uses config key pushd.mass_mention_chunk_size as the batch size.
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator> {
let config = revolt_config::config().await;
let mut session = self
.start_session()
.await
.map_err(|_| create_database_error!("start_session", COL))?;
session
.start_transaction()
.read_concern(ReadConcern::snapshot())
.await
.map_err(|_| create_database_error!("start_transaction", COL))?;
let cursor = self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id
})
.session(&mut session)
.batch_size(config.pushd.mass_mention_chunk_size as u32)
.await
.map_err(|_| create_database_error!("find", COL))?;
Ok(ChunkedServerMembersGenerator::new_mongo(session, cursor))
}
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"roles": {"$in": roles}
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator> {
let config = revolt_config::config().await;
let mut session = self
.start_session()
.await
.map_err(|_| create_database_error!("start_session", COL))?;
session
.start_transaction()
.read_concern(ReadConcern::snapshot())
.await
.map_err(|_| create_database_error!("start_transaction", COL))?;
let cursor = self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"roles": {"$in": roles}
})
.session(&mut session)
.batch_size(config.pushd.mass_mention_chunk_size as u32)
.await
.map_err(|_| create_database_error!("find", COL))?;
return Ok(ChunkedServerMembersGenerator::new_mongo(session, cursor));
}
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> { async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find( .find(doc! {
doc! {
"_id.user": user_id "_id.user": user_id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -79,15 +162,12 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> { async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find( .find(doc! {
doc! {
"_id.server": server_id, "_id.server": server_id,
"_id.user": { "_id.user": {
"$in": ids "$in": ids
} }
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -104,12 +184,9 @@ impl AbstractServerMembers for MongoDb {
/// Fetch member count of a server /// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> { async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL) self.col::<Member>(COL)
.count_documents( .count_documents(doc! {
doc! {
"_id.server": server_id "_id.server": server_id
}, })
None,
)
.await .await
.map(|c| c as usize) .map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL)) .map_err(|_| create_database_error!("count_documents", COL))
@@ -118,12 +195,9 @@ impl AbstractServerMembers for MongoDb {
/// Fetch server count of a user /// Fetch server count of a user
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> { async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL) self.col::<Member>(COL)
.count_documents( .count_documents(doc! {
doc! {
"_id.user": user_id "_id.user": user_id
}, })
None,
)
.await .await
.map(|c| c as usize) .map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL)) .map_err(|_| create_database_error!("count_documents", COL))
@@ -3,7 +3,7 @@ use revolt_result::Result;
use crate::ReferenceDb; use crate::ReferenceDb;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use super::AbstractServerMembers; use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
#[async_trait] #[async_trait]
impl AbstractServerMembers for ReferenceDb { impl AbstractServerMembers for ReferenceDb {
@@ -40,6 +40,70 @@ impl AbstractServerMembers for ReferenceDb {
.collect()) .collect())
} }
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator> {
let server_members = self.server_members.lock().await;
let members = server_members
.clone()
.into_values()
.filter(move |member| member.id.server == server_id)
.collect();
// this is inefficient as shit but its the reference db so its fine
Ok(ChunkedServerMembersGenerator::new_reference(members))
}
/// Fetch all members that have any of the roles given
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.clone()
.into_values()
.filter(|member| {
member.id.server == server_id
&& !member
.roles
.iter()
.filter(|p| roles.contains(*p))
.collect::<Vec<&String>>()
.is_empty()
})
.collect())
}
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator> {
let server_members = self.server_members.lock().await;
let resp = server_members
.clone()
.into_values()
.filter(|member| {
member.id.server == server_id
&& !member
.roles
.iter()
.filter(|p| roles.contains(*p))
.collect::<Vec<&String>>()
.is_empty()
})
.collect();
return Ok(ChunkedServerMembersGenerator::new_reference(resp));
}
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> { async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await; let server_members = self.server_members.lock().await;
@@ -285,7 +285,7 @@ impl Role {
} }
/// Update server data /// Update server data
pub async fn update<'a>( pub async fn update(
&mut self, &mut self,
db: &Database, db: &Database,
server_id: &str, server_id: &str,
@@ -25,14 +25,11 @@ impl AbstractServers for MongoDb {
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> { async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
Ok(self Ok(self
.col::<Server>(COL) .col::<Server>(COL)
.find( .find(doc! {
doc! {
"_id": { "_id": {
"$in": ids "$in": ids
} }
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find", "servers"))? .map_err(|_| create_database_error!("find", "servers"))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -84,7 +81,6 @@ impl AbstractServers for MongoDb {
.map_err(|_| create_database_error!("to_document", "role"))? .map_err(|_| create_database_error!("to_document", "role"))?
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -125,7 +121,6 @@ impl AbstractServers for MongoDb {
"roles": &role_id "roles": &role_id
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_many", "server_members"))?; .map_err(|_| create_database_error!("update_many", "server_members"))?;
@@ -140,7 +135,6 @@ impl AbstractServers for MongoDb {
"role_permissions.".to_owned() + role_id: 1_i32 "role_permissions.".to_owned() + role_id: 1_i32
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_one", "channels"))?; .map_err(|_| create_database_error!("update_one", "channels"))?;
@@ -155,7 +149,6 @@ impl AbstractServers for MongoDb {
"roles.".to_owned() + role_id: 1_i32 "roles.".to_owned() + role_id: 1_i32
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -188,12 +181,9 @@ impl MongoDb {
// Find all channels // Find all channels
let channels: Vec<String> = self let channels: Vec<String> = self
.col::<Document>("channels") .col::<Document>("channels")
.find( .find(doc! {
doc! {
"server": server_id "server": server_id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find", "channels"))? .map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -225,19 +215,15 @@ impl MongoDb {
} }
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_many", "emojis"))?; .map_err(|_| create_database_error!("update_many", "emojis"))?;
// Delete all channels. // Delete all channels.
self.col::<Document>("channels") self.col::<Document>("channels")
.delete_many( .delete_many(doc! {
doc! {
"server": &server_id "server": &server_id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", "channels"))?; .map_err(|_| create_database_error!("delete_many", "channels"))?;
@@ -248,12 +234,9 @@ impl MongoDb {
// Delete members and bans. // Delete members and bans.
for with in &["server_members", "server_bans"] { for with in &["server_members", "server_bans"] {
self.col::<Document>(with) self.col::<Document>(with)
.delete_many( .delete_many(doc! {
doc! {
"_id.server": &server_id "_id.server": &server_id
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", with))?; .map_err(|_| create_database_error!("delete_many", with))?;
} }
@@ -53,8 +53,8 @@ impl AbstractUserSettings for MongoDb {
doc! { doc! {
"$set": set "$set": set
}, },
UpdateOptions::builder().upsert(true).build(),
) )
.with_options(UpdateOptions::builder().upsert(true).build())
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("update_one", "user_settings")) .map_err(|_| create_database_error!("update_one", "user_settings"))
+23 -10
View File
@@ -3,11 +3,12 @@ use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP}; use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP};
use authifier::config::{EmailVerificationConfig, Template}; use authifier::config::{EmailVerificationConfig, Template};
use futures::future::join_all;
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use revolt_config::{config, FeaturesLimits}; use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{self, UserFlags}; use revolt_models::v0::{self, UserBadges, UserFlags};
use revolt_presence::filter_online; use revolt_presence::filter_online;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use serde_json::json; use serde_json::json;
@@ -348,15 +349,13 @@ impl User {
) -> Result<Vec<v0::User>> { ) -> Result<Vec<v0::User>> {
let online_ids = filter_online(ids).await; let online_ids = filter_online(ids).await;
Ok(db Ok(
.fetch_users(ids) join_all(db.fetch_users(ids).await?.into_iter().map(|user| async {
.await?
.into_iter()
.map(|user| {
let is_online = online_ids.contains(&user.id); let is_online = online_ids.contains(&user.id);
user.into_known(perspective, is_online) user.into_known(perspective, is_online).await
}) }))
.collect()) .await,
)
} }
/// Find a free discriminator for a given username /// Find a free discriminator for a given username
@@ -637,7 +636,7 @@ impl User {
} }
/// Update user data /// Update user data
pub async fn update<'a>( pub async fn update(
&mut self, &mut self,
db: &Database, db: &Database,
partial: PartialUser, partial: PartialUser,
@@ -807,4 +806,18 @@ impl User {
) )
.await .await
} }
/// Gets the user's badges along with calculating any dynamic badges
pub async fn get_badges(&self) -> u32 {
let config = config().await;
let badges = self.badges.unwrap_or_default() as u32;
if let Some(cutoff) = config.api.users.early_adopter_cutoff {
if Ulid::from_string(&self.id).unwrap().timestamp_ms() < cutoff {
return badges + UserBadges::EarlyAdopter as u32;
};
};
badges
}
} }
@@ -49,12 +49,9 @@ impl AbstractUsers for MongoDb {
/// Fetch a session from the database by token /// Fetch a session from the database by token
async fn fetch_session_by_token(&self, token: &str) -> Result<Session> { async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
self.col::<Session>("sessions") self.col::<Session>("sessions")
.find_one( .find_one(doc! {
doc! {
"token": token "token": token
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find_one", "sessions"))? .map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession)) .ok_or_else(|| create_error!(InvalidSession))
@@ -64,14 +61,11 @@ impl AbstractUsers for MongoDb {
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> { async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
Ok(self Ok(self
.col::<User>(COL) .col::<User>(COL)
.find( .find(doc! {
doc! {
"_id": { "_id": {
"$in": ids "$in": ids
} }
}, })
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -94,10 +88,10 @@ impl AbstractUsers for MongoDb {
Ok(self Ok(self
.col::<UserDocument>(COL) .col::<UserDocument>(COL)
.find( .find(doc! {
doc! {
"username": username "username": username
}, })
.with_options(
FindOptions::builder() FindOptions::builder()
.collation( .collation(
Collation::builder() Collation::builder()
@@ -122,15 +116,13 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self Ok(self
.col::<DocumentId>(COL) .col::<DocumentId>(COL)
.find( .find(doc! {
doc! {
"$and": [ "$and": [
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } }, { "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } } { "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
] ]
}, })
FindOptions::builder().projection(doc! { "_id": 1 }).build(), .with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -143,17 +135,15 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self Ok(self
.col::<DocumentId>("channels") .col::<DocumentId>("channels")
.find( .find(doc! {
doc! {
"channel_type": { "channel_type": {
"$in": ["Group", "DirectMessage"] "$in": ["Group", "DirectMessage"]
}, },
"recipients": { "recipients": {
"$all": [ user_a, user_b ] "$all": [ user_a, user_b ]
} }
}, })
FindOptions::builder().projection(doc! { "_id": 1 }).build(), .with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
)
.await .await
.map_err(|_| create_database_error!("find", "channels"))? .map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -166,8 +156,7 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self Ok(self
.col::<DocumentId>("server_members") .col::<DocumentId>("server_members")
.aggregate( .aggregate(vec![
vec![
doc! { doc! {
"$match": { "$match": {
"_id.user": user_a "_id.user": user_a
@@ -206,9 +195,7 @@ impl AbstractUsers for MongoDb {
"_id": "$_id.server" "_id": "$_id.server"
} }
}, },
], ])
None,
)
.await .await
.map_err(|_| create_database_error!("aggregate", "server_members"))? .map_err(|_| create_database_error!("aggregate", "server_members"))?
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -284,7 +271,6 @@ impl AbstractUsers for MongoDb {
} }
} }
}], }],
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -305,7 +291,6 @@ impl AbstractUsers for MongoDb {
} }
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -329,7 +314,6 @@ impl AbstractUsers for MongoDb {
"subscription": 1 "subscription": 1
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -6,7 +6,7 @@ use revolt_rocket_okapi::{
use crate::User; use crate::User;
impl<'r> OpenApiFromRequest<'r> for User { impl OpenApiFromRequest<'_> for User {
fn from_request_input( fn from_request_input(
_gen: &mut OpenApiGenerator, _gen: &mut OpenApiGenerator,
_name: String, _name: String,
+55 -10
View File
@@ -4,7 +4,6 @@ use crate::{Database, Message, AMQP};
use deadqueue::limited::Queue; use deadqueue::limited::Queue;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use revolt_models::v0::PushNotification; use revolt_models::v0::PushNotification;
use rocket::form::validate::Contains;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
time::Duration, time::Duration,
@@ -14,6 +13,7 @@ use validator::HasLen;
use revolt_result::Result; use revolt_result::Result;
use super::DelayedTask; use super::DelayedTask;
use crate::Channel::{TextChannel, VoiceChannel};
/// Enumeration of possible events /// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
@@ -120,17 +120,22 @@ pub async fn handle_ack_event(
); );
// find all the users we'll be notifying // find all the users we'll be notifying
messages messages.iter().for_each(|(_, _, recipents, _)| {
.iter() users.extend(recipents.iter());
.for_each(|(_, _, recipents, _)| users.extend(recipents.iter())); });
debug!("Found {} users to notify.", users.len()); debug!("Found {} users to notify.", users.len());
for user in users { for user in users {
let message_ids: Vec<String> = messages let message_ids: Vec<String> = messages
.iter() .iter()
.filter(|(_, _, recipients, _)| recipients.contains(user)) .filter_map(|(_, message, recipients, _)| {
.map(|(_, message, _, _)| message.id.clone()) if recipients.contains(user) {
Some(message.id.clone())
} else {
None
}
})
.collect(); .collect();
if !message_ids.is_empty() { if !message_ids.is_empty() {
@@ -140,13 +145,18 @@ pub async fn handle_ack_event(
debug!("Added {} mentions for user {}", message_ids.len(), &user); debug!("Added {} mentions for user {}", message_ids.len(), &user);
} }
for (push, _, recipients, silenced) in messages { let mut mass_mentions = vec![];
if *silenced || recipients.is_empty() || push.is_none() {
for (push, message, recipients, silenced) in messages {
if *silenced
|| push.is_none()
|| (recipients.is_empty() && !message.contains_mass_push_mention())
{
debug!( debug!(
"Rejecting push: silenced: {}, recipient count: {}, push exists: {:?}", "Rejecting push: silenced: {}, recipient count: {}, push exists: {:?}",
*silenced, *silenced,
recipients.length(), recipients.length(),
push push.is_some()
); );
continue; continue;
} }
@@ -162,6 +172,35 @@ pub async fn handle_ack_event(
{ {
revolt_config::capture_error(&err); revolt_config::capture_error(&err);
} }
if message.contains_mass_push_mention() {
mass_mentions.push(push.clone().unwrap());
}
}
if !mass_mentions.is_empty() {
debug!(
"Sending mass mention push event to AMQP; channel {}",
&mass_mentions[0].message.channel
);
let channel = db
.fetch_channel(&mass_mentions[0].message.channel)
.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");
}
}
} }
} }
}; };
@@ -192,7 +231,7 @@ pub async fn worker(db: Database, amqp: AMQP) {
revolt_config::capture_error(&err); revolt_config::capture_error(&err);
error!("{err:?} for {event:?}. ({user:?}, {channel})"); error!("{err:?} for {event:?}. ({user:?}, {channel})");
} else { } else {
info!("User {user:?} ack in {channel} with {event:?}"); debug!("User {user:?} ack in {channel} with {event:?}");
} }
} }
} }
@@ -224,6 +263,12 @@ pub async fn worker(db: Database, amqp: AMQP) {
// add the new message to the list of messages to be processed. // add the new message to the list of messages to be processed.
existing.append(new_data); existing.append(new_data);
// if the message contains a mass mention, do not delay it any further.
if new_data[0].1.contains_mass_push_mention() {
task.run_immediately();
continue;
}
// put a cap on the amount of messages that can be queued, for particularly active channels // put a cap on the amount of messages that can be queued, for particularly active channels
if (existing.length() as u16) if (existing.length() as u16)
< revolt_config::config() < revolt_config::config()
+9 -1
View File
@@ -26,6 +26,7 @@ pub fn start_workers(db: Database, amqp: AMQP) {
/// Task with additional information on when it should run /// Task with additional information on when it should run
pub struct DelayedTask<T> { pub struct DelayedTask<T> {
pub data: T, pub data: T,
run_now: bool,
last_updated: Instant, last_updated: Instant,
first_seen: Instant, first_seen: Instant,
} }
@@ -41,6 +42,7 @@ impl<T> DelayedTask<T> {
pub fn new(data: T) -> Self { pub fn new(data: T) -> Self {
DelayedTask { DelayedTask {
data, data,
run_now: false,
last_updated: Instant::now(), last_updated: Instant::now(),
first_seen: Instant::now(), first_seen: Instant::now(),
} }
@@ -51,9 +53,15 @@ impl<T> DelayedTask<T> {
self.last_updated = Instant::now() self.last_updated = Instant::now()
} }
/// Flag the task to run right away, regardless of the time
pub fn run_immediately(&mut self) {
self.run_now = true
}
/// Check if a task should run yet /// Check if a task should run yet
pub fn should_run(&self) -> bool { pub fn should_run(&self) -> bool {
self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT self.run_now
|| self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT
|| self.last_updated.elapsed().as_secs() > SAVE_CONSTANT || self.last_updated.elapsed().as_secs() > SAVE_CONSTANT
} }
} }
+41 -19
View File
@@ -1,7 +1,7 @@
use revolt_models::v0::*; use revolt_models::v0::*;
use revolt_permissions::{calculate_user_permissions, UserPermission}; use revolt_permissions::{calculate_user_permissions, UserPermission};
use crate::{util::permissions::DatabasePermissionQuery, Database, FileUsedFor}; use crate::{util::permissions::DatabasePermissionQuery, Database};
impl crate::Bot { impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot { pub fn into_public_bot(self, user: crate::User) -> PublicBot {
@@ -483,11 +483,12 @@ impl crate::Message {
edited: self.edited, edited: self.edited,
embeds: self.embeds, embeds: self.embeds,
mentions: self.mentions, mentions: self.mentions,
role_mentions: self.role_mentions,
replies: self.replies, replies: self.replies,
reactions: self.reactions, reactions: self.reactions,
interactions: self.interactions.into(), interactions: self.interactions.into(),
masquerade: self.masquerade.map(Into::into), masquerade: self.masquerade.map(Into::into),
flags: self.flags.map(|flags| flags as u32).unwrap_or_default(), flags: self.flags.unwrap_or_default(),
pinned: self.pinned, pinned: self.pinned,
} }
} }
@@ -511,11 +512,12 @@ impl From<crate::PartialMessage> for PartialMessage {
edited: value.edited, edited: value.edited,
embeds: value.embeds, embeds: value.embeds,
mentions: value.mentions, mentions: value.mentions,
role_mentions: value.role_mentions,
replies: value.replies, replies: value.replies,
reactions: value.reactions, reactions: value.reactions,
interactions: value.interactions.map(Into::into), interactions: value.interactions.map(Into::into),
masquerade: value.masquerade.map(Into::into), masquerade: value.masquerade.map(Into::into),
flags: value.flags.map(|flags| flags as u32), flags: value.flags,
pinned: value.pinned, pinned: value.pinned,
} }
} }
@@ -992,6 +994,8 @@ impl crate::User {
(RelationshipStatus::None, false) (RelationshipStatus::None, false)
}; };
let badges = self.get_badges().await;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
@@ -1010,7 +1014,7 @@ impl crate::User {
} else { } else {
vec![] vec![]
}, },
badges: self.badges.unwrap_or_default() as u32, badges,
online: can_see_profile online: can_see_profile
&& revolt_presence::is_online(&self.id).await && revolt_presence::is_online(&self.id).await
&& !matches!( && !matches!(
@@ -1021,7 +1025,7 @@ impl crate::User {
}) })
), ),
status: if can_see_profile { status: if can_see_profile {
self.status.map(|status| status.into()) self.status.and_then(|status| status.into(true))
} else { } else {
None None
}, },
@@ -1036,7 +1040,7 @@ impl crate::User {
/// Convert user object into user model assuming mutual connection /// Convert user object into user model assuming mutual connection
/// ///
/// Relations will never be included, i.e. when we process ourselves /// Relations will never be included, i.e. when we process ourselves
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User pub async fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
where where
P: Into<Option<&'a crate::User>>, P: Into<Option<&'a crate::User>>,
{ {
@@ -1066,13 +1070,15 @@ impl crate::User {
(RelationshipStatus::None, false) (RelationshipStatus::None, false)
}; };
let badges = self.get_badges().await;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
display_name: self.display_name, display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()), avatar: self.avatar.map(|file| file.into()),
relations: vec![], relations: vec![],
badges: self.badges.unwrap_or_default() as u32, badges,
online: can_see_profile online: can_see_profile
&& is_online && is_online
&& !matches!( && !matches!(
@@ -1083,7 +1089,7 @@ impl crate::User {
}) })
), ),
status: if can_see_profile { status: if can_see_profile {
self.status.map(|status| status.into()) self.status.and_then(|status| status.into(true))
} else { } else {
None None
}, },
@@ -1096,14 +1102,16 @@ impl crate::User {
} }
/// Convert user object into user model without presence information /// Convert user object into user model without presence information
pub fn into_known_static<'a>(self, is_online: bool) -> User { pub async fn into_known_static<'a>(self, is_online: bool) -> User {
let badges = self.get_badges().await;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
display_name: self.display_name, display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()), avatar: self.avatar.map(|file| file.into()),
relations: vec![], relations: vec![],
badges: self.badges.unwrap_or_default() as u32, badges,
online: is_online online: is_online
&& !matches!( && !matches!(
self.status, self.status,
@@ -1112,7 +1120,7 @@ impl crate::User {
.. ..
}) })
), ),
status: self.status.map(|status| status.into()), status: self.status.and_then(|status| status.into(true)),
flags: self.flags.unwrap_or_default() as u32, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
@@ -1122,6 +1130,8 @@ impl crate::User {
} }
pub async fn into_self(self, force_online: bool) -> User { pub async fn into_self(self, force_online: bool) -> User {
let badges = self.get_badges().await;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
@@ -1136,7 +1146,7 @@ impl crate::User {
.collect() .collect()
}) })
.unwrap_or_default(), .unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32, badges,
online: (force_online || revolt_presence::is_online(&self.id).await) online: (force_online || revolt_presence::is_online(&self.id).await)
&& !matches!( && !matches!(
self.status, self.status,
@@ -1145,7 +1155,7 @@ impl crate::User {
.. ..
}) })
), ),
status: self.status.map(|status| status.into()), status: self.status.and_then(|status| status.into(true)),
flags: self.flags.unwrap_or_default() as u32, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
@@ -1196,7 +1206,7 @@ impl From<crate::PartialUser> for PartialUser {
.collect() .collect()
}), }),
badges: value.badges.map(|badges| badges as u32), badges: value.badges.map(|badges| badges as u32),
status: value.status.map(|status| status.into()), status: value.status.and_then(|status| status.into(false)),
flags: value.flags.map(|flags| flags as u32), flags: value.flags.map(|flags| flags as u32),
privileged: value.privileged, privileged: value.privileged,
bot: value.bot.map(|bot| bot.into()), bot: value.bot.map(|bot| bot.into()),
@@ -1285,11 +1295,23 @@ impl From<Presence> for crate::Presence {
} }
} }
impl From<crate::UserStatus> for UserStatus { impl crate::UserStatus {
fn from(value: crate::UserStatus) -> Self { fn into(self, discard_invisible: bool) -> Option<UserStatus> {
UserStatus { let status = UserStatus {
text: value.text, text: self.text,
presence: value.presence.map(|presence| presence.into()), presence: self.presence.and_then(|presence| {
if discard_invisible && presence == crate::Presence::Invisible {
None
} else {
Some(presence.into())
}
}),
};
if status.text.is_none() && status.presence.is_none() {
None
} else {
Some(status)
} }
} }
} }
@@ -87,23 +87,46 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
} }
} }
pub fn channel(self, channel: &'z Channel) -> BulkDatabasePermissionQuery { pub fn channel(self, channel: &'z Channel) -> BulkDatabasePermissionQuery<'z> {
BulkDatabasePermissionQuery { BulkDatabasePermissionQuery {
channel: Some(channel.clone()), channel: Some(channel.clone()),
..self ..self
} }
} }
pub fn members(self, members: &'z [Member]) -> BulkDatabasePermissionQuery { pub async fn from_channel_id(self, channel_id: String) -> BulkDatabasePermissionQuery<'z> {
let channel = self
.database
.fetch_channel(channel_id.as_str())
.await
.expect("Valid channel id");
drop(channel_id);
BulkDatabasePermissionQuery { BulkDatabasePermissionQuery {
members: Some(members.to_owned()), channel: Some(channel),
..self ..self
} }
} }
pub fn users(self, users: &'z [User]) -> BulkDatabasePermissionQuery { pub fn members(self, members: &'z [Member]) -> BulkDatabasePermissionQuery<'z> {
BulkDatabasePermissionQuery {
members: Some(members.to_owned()),
cached_member_perms: None,
users: None,
cached_members: None,
cached_users: None,
..self
}
}
pub fn users(self, users: &'z [User]) -> BulkDatabasePermissionQuery<'z> {
BulkDatabasePermissionQuery { BulkDatabasePermissionQuery {
users: Some(users.to_owned()), users: Some(users.to_owned()),
cached_member_perms: None,
members: None,
cached_members: None,
cached_users: None,
..self ..self
} }
} }
+8 -10
View File
@@ -305,16 +305,14 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
/// Are we a recipient of this channel? /// Are we a recipient of this channel?
async fn are_we_part_of_the_channel(&mut self) -> bool { async fn are_we_part_of_the_channel(&mut self) -> bool {
if let Some(channel) = &self.channel { if let Some(
match channel {
Cow::Borrowed(Channel::DirectMessage { recipients, .. }) Cow::Borrowed(Channel::DirectMessage { recipients, .. })
| Cow::Owned(Channel::DirectMessage { recipients, .. }) | Cow::Owned(Channel::DirectMessage { recipients, .. })
| Cow::Borrowed(Channel::Group { recipients, .. }) | Cow::Borrowed(Channel::Group { recipients, .. })
| Cow::Owned(Channel::Group { recipients, .. }) => { | Cow::Owned(Channel::Group { recipients, .. }),
) = &self.channel
{
recipients.contains(&self.perspective.id) recipients.contains(&self.perspective.id)
}
_ => false,
}
} else { } else {
false false
} }
@@ -420,7 +418,7 @@ impl<'a> DatabasePermissionQuery<'a> {
} }
/// Use user /// Use user
pub fn user(self, user: &'a User) -> DatabasePermissionQuery { pub fn user(self, user: &'a User) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery { DatabasePermissionQuery {
user: Some(Cow::Borrowed(user)), user: Some(Cow::Borrowed(user)),
..self ..self
@@ -428,7 +426,7 @@ impl<'a> DatabasePermissionQuery<'a> {
} }
/// Use channel /// Use channel
pub fn channel(self, channel: &'a Channel) -> DatabasePermissionQuery { pub fn channel(self, channel: &'a Channel) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery { DatabasePermissionQuery {
channel: Some(Cow::Borrowed(channel)), channel: Some(Cow::Borrowed(channel)),
..self ..self
@@ -436,7 +434,7 @@ impl<'a> DatabasePermissionQuery<'a> {
} }
/// Use server /// Use server
pub fn server(self, server: &'a Server) -> DatabasePermissionQuery { pub fn server(self, server: &'a Server) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery { DatabasePermissionQuery {
server: Some(Cow::Borrowed(server)), server: Some(Cow::Borrowed(server)),
..self ..self
@@ -444,7 +442,7 @@ impl<'a> DatabasePermissionQuery<'a> {
} }
/// Use member /// Use member
pub fn member(self, member: &'a Member) -> DatabasePermissionQuery { pub fn member(self, member: &'a Member) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery { DatabasePermissionQuery {
member: Some(Cow::Borrowed(member)), member: Some(Cow::Borrowed(member)),
..self ..self
@@ -0,0 +1,188 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type="text/css">
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
</style>
</head>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<tbody>
<tr>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
</table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Password Reset</h1>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">
You tried to create a new account with or try to re-verify this email
but there is already an existing verified account under this email.
</p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">
If you wish, you can now proceed to reset the password on it, click
below to continue.
</p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Reset</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in Europe
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -0,0 +1,41 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Password Reset</h1>
<p>
You tried to create a new account with or try to re-verify this email
but there is already an existing verified account under this email.
</p>
<p>
If you wish, you can now proceed to reset the password on it, click
below to continue.
</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Reset</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
@@ -0,0 +1,19 @@
You tried to create a new account with or try
to re-verify this email but there is already
an existing verified account under this email.
If you wish, you can now proceed to reset the
password on it, click below to continue.
Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-files" name = "revolt-files"
version = "0.8.3" version = "0.8.6"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,10 +20,10 @@ typenum = "1.17.0"
aws-config = "1.5.5" aws-config = "1.5.5"
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] } aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.8.3", path = "../config", features = [ revolt-config = { version = "0.8.6", path = "../config", features = [
"report-macros", "report-macros",
] } ] }
revolt-result = { version = "0.8.3", path = "../result" } revolt-result = { version = "0.8.6", path = "../result" }
# image processing # image processing
jxl-oxide = "0.8.1" jxl-oxide = "0.8.1"
+5 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.8.3" version = "0.8.6"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,13 +20,14 @@ default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.3", path = "../config" } revolt-config = { version = "0.8.6", path = "../config" }
revolt-permissions = { version = "0.8.3", path = "../permissions" } revolt-permissions = { version = "0.8.6", path = "../permissions" }
# Utility # Utility
regex = "1" regex = "1.11"
indexmap = "1.9.3" indexmap = "1.9.3"
once_cell = "1.17.1" once_cell = "1.17.1"
num_enum = "0.6.1"
# Rocket # Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
+13 -6
View File
@@ -1,8 +1,6 @@
use std::time::SystemTime; use std::time::SystemTime;
use indexmap::{IndexMap, IndexSet}; use indexmap::{IndexMap, IndexSet};
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_config::config; use revolt_config::config;
#[cfg(feature = "validator")] #[cfg(feature = "validator")]
@@ -15,9 +13,6 @@ use iso8601_timestamp::Timestamp;
use super::{Channel, Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR}; use super::{Channel, Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR};
pub static RE_MENTION: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
auto_derived_partial!( auto_derived_partial!(
/// Message /// Message
pub struct Message { pub struct Message {
@@ -58,6 +53,9 @@ auto_derived_partial!(
/// Array of user ids mentioned in this message /// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>, pub mentions: Option<Vec<String>>,
/// Array of role ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub role_mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to /// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>, pub replies: Option<Vec<String>>,
@@ -239,6 +237,10 @@ auto_derived!(
pub id: String, pub id: String,
/// Whether this reply should mention the message's author /// Whether this reply should mention the message's author
pub mention: bool, pub mention: bool,
/// Whether to error if the referenced message doesn't exist.
/// Otherwise, send a message without this reply.
/// Default is true.
pub fail_if_not_exists: Option<bool>,
} }
/// Message to send /// Message to send
@@ -367,6 +369,11 @@ auto_derived!(
pub enum MessageFlags { pub enum MessageFlags {
/// Message will not send push / desktop notifications /// Message will not send push / desktop notifications
SuppressNotifications = 1, SuppressNotifications = 1,
/// Message will mention all users who can see the channel
MentionsEveryone = 2,
/// Message will mention all users who are online and can see the channel.
/// This cannot be true if MentionsEveryone is true
MentionsOnline = 3,
} }
/// Optional fields on message /// Optional fields on message
@@ -392,7 +399,7 @@ impl Interactions {
} }
} }
impl<'a> MessageAuthor<'a> { impl MessageAuthor<'_> {
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
match self { match self {
MessageAuthor::User(user) => &user.id, MessageAuthor::User(user) => &user.id,
+9
View File
@@ -0,0 +1,9 @@
[package]
name = "revolt-parser"
version = "0.8.6"
edition = "2021"
license = "AGPL-3.0-or-later"
description = "Revolt Backend: Message Parser"
[dependencies]
logos = { version = "0.15" }
+245
View File
@@ -0,0 +1,245 @@
use std::collections::{HashSet, VecDeque};
use logos::Logos;
#[derive(Debug, Clone, Logos, PartialEq)]
#[logos(skip "\n")]
pub enum MessageToken {
#[token("\\")]
Escape,
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())]
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),
#[token("@everyone")]
MentionEveryone,
#[token("@online")]
MentionOnline
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct MessageResults {
pub user_mentions: HashSet<String>,
pub role_mentions: HashSet<String>,
pub mentions_everyone: bool,
pub mentions_online: bool
}
struct MessageParserIterator<I> {
inner: I,
temp: VecDeque<MessageToken>
}
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> {
type Item = MessageToken;
fn next(&mut self) -> Option<Self::Item> {
if !self.temp.is_empty() {
self.temp.pop_front()
} else {
let token = self.inner.next();
if token == Some(MessageToken::Escape) {
self.inner.next();
token
} else if let Some(MessageToken::CodeblockMarker(ty)) = token {
loop {
let next_token = self.inner.next();
if next_token == Some(MessageToken::CodeblockMarker(ty)) {
self.temp.clear();
self.temp.push_back(MessageToken::CodeblockMarker(ty));
break next_token
} else if let Some(token) = next_token {
self.temp.push_back(token);
} else {
break Some(MessageToken::CodeblockMarker(ty))
}
}
} else {
token
}
}
}
}
pub fn parse_message_iter(text: &str) -> impl Iterator<Item = MessageToken> + '_ {
MessageParserIterator {
inner: MessageToken::lexer(text).flatten(),
temp: VecDeque::new()
}
}
pub fn parse_message(text: &str) -> MessageResults {
let mut results = MessageResults::default();
for token in parse_message_iter(text) {
match token {
MessageToken::Escape => {}
MessageToken::CodeblockMarker(_) => {},
MessageToken::UserMention(id) => { results.user_mentions.insert(id); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); },
MessageToken::MentionEveryone => results.mentions_everyone = true,
MessageToken::MentionOnline => results.mentions_online = true,
};
};
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_nodes() {
let output = parse_message_iter("Hello everyone").collect::<Vec<_>>();
assert_eq!(output.len(), 0);
}
#[test]
fn test_simple_user_mention() {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
}
#[test]
fn test_simple_role_mention() {
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
}
#[test]
fn test_mention_everyone() {
let output = parse_message_iter("Hello @everyone.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::MentionEveryone);
}
#[test]
fn test_mention_online() {
let output = parse_message_iter("Hello @online.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::MentionOnline);
}
#[test]
fn test_everything() {
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[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
#[test]
fn test_everything_no_spaces() {
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[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
#[test]
fn test_codeblock_no_mentions() {
let output = parse_message_iter("```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online\n```").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
}
#[test]
fn test_uncontained_codeblock_should_mention() {
let output = parse_message_iter("```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
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[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);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_uncontained_inline_codeblock_should_mention() {
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
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[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_double_inline_codeblock() {
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(2));
assert_eq!(output[1], MessageToken::CodeblockMarker(2));
}
#[test]
fn test_double_inline_codeblock_with_backticks_inside() {
let output = parse_message_iter("``this `should` not `ping` @everyone``").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(2));
assert_eq!(output[1], MessageToken::CodeblockMarker(2));
}
#[test]
fn test_in_middle() {
let output = parse_message_iter("i am not pinging `@everyone`.").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[1], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_escaped_codeblock() {
let output = parse_message_iter("i am ~~not~~ pinging \\`@everyone` ok.").collect::<Vec<_>>();
assert_eq!(output.len(), 3);
assert_eq!(output[0], MessageToken::Escape);
assert_eq!(output[1], MessageToken::MentionEveryone);
assert_eq!(output[2], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_escape_mention() {
let output = parse_message_iter("i wont ping \\@everyone").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::Escape);
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.8.3" version = "0.8.6"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.8.3", path = "../result" } revolt-result = { version = "0.8.6", path = "../result" }
# Utility # Utility
auto_ops = "0.3.0" auto_ops = "0.3.0"
@@ -90,8 +90,14 @@ pub enum ChannelPermission {
/// Move members between voice channels /// Move members between voice channels
MoveMembers = 1 << 35, MoveMembers = 1 << 35,
// * Channel permissions two electric boogaloo
/// Mention everyone and online members
MentionEveryone = 1 << 37,
/// Mention roles
MentionRoles = 1 << 38,
// * Misc. permissions // * Misc. permissions
// % Bits 36 to 52: free area // % Bits 38 to 52: free area
// % Bits 53 to 64: do not use // % Bits 53 to 64: do not use
// * Grant all permissions // * Grant all permissions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.8.3" version = "0.8.6"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.8.3" version = "0.8.6"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+3 -1
View File
@@ -1,5 +1,4 @@
use axum::{http::StatusCode, response::IntoResponse, Json}; use axum::{http::StatusCode, response::IntoResponse, Json};
use rocket::http::Status;
use crate::{Error, ErrorType}; use crate::{Error, ErrorType};
@@ -52,6 +51,7 @@ impl IntoResponse for Error {
ErrorType::ReachedMaximumBots => StatusCode::BAD_REQUEST, ErrorType::ReachedMaximumBots => StatusCode::BAD_REQUEST,
ErrorType::IsBot => StatusCode::BAD_REQUEST, ErrorType::IsBot => StatusCode::BAD_REQUEST,
ErrorType::IsNotBot => StatusCode::BAD_REQUEST,
ErrorType::BotIsPrivate => StatusCode::FORBIDDEN, ErrorType::BotIsPrivate => StatusCode::FORBIDDEN,
ErrorType::CannotReportYourself => StatusCode::BAD_REQUEST, ErrorType::CannotReportYourself => StatusCode::BAD_REQUEST,
@@ -75,6 +75,8 @@ impl IntoResponse for Error {
ErrorType::NotFound => StatusCode::NOT_FOUND, ErrorType::NotFound => StatusCode::NOT_FOUND,
ErrorType::NoEffect => StatusCode::OK, ErrorType::NoEffect => StatusCode::OK,
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST, ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
ErrorType::ProxyError => StatusCode::BAD_REQUEST, ErrorType::ProxyError => StatusCode::BAD_REQUEST,
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY, ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
+7
View File
@@ -121,6 +121,7 @@ pub enum ErrorType {
// ? Bot related errors // ? Bot related errors
ReachedMaximumBots, ReachedMaximumBots,
IsBot, IsBot,
IsNotBot,
BotIsPrivate, BotIsPrivate,
// ? User safety related errors // ? User safety related errors
@@ -148,6 +149,7 @@ pub enum ErrorType {
InvalidCredentials, InvalidCredentials,
InvalidProperty, InvalidProperty,
InvalidSession, InvalidSession,
InvalidFlagValue,
NotAuthenticated, NotAuthenticated,
DuplicateNonce, DuplicateNonce,
NotFound, NotFound,
@@ -168,6 +170,11 @@ pub enum ErrorType {
// ? Legacy errors // ? Legacy errors
VosoUnavailable, VosoUnavailable,
// ? Feature flag disabled in the config
FeatureDisabled {
feature: String,
},
} }
#[macro_export] #[macro_export]
+3
View File
@@ -42,6 +42,7 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotInGroup => Status::NotFound, ErrorType::NotInGroup => Status::NotFound,
ErrorType::AlreadyPinned => Status::BadRequest, ErrorType::AlreadyPinned => Status::BadRequest,
ErrorType::NotPinned => Status::BadRequest, ErrorType::NotPinned => Status::BadRequest,
ErrorType::InvalidFlagValue => Status::BadRequest,
ErrorType::UnknownServer => Status::NotFound, ErrorType::UnknownServer => Status::NotFound,
ErrorType::InvalidRole => Status::NotFound, ErrorType::InvalidRole => Status::NotFound,
@@ -57,6 +58,7 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::ReachedMaximumBots => Status::BadRequest, ErrorType::ReachedMaximumBots => Status::BadRequest,
ErrorType::IsBot => Status::BadRequest, ErrorType::IsBot => Status::BadRequest,
ErrorType::IsNotBot => Status::BadRequest,
ErrorType::BotIsPrivate => Status::Forbidden, ErrorType::BotIsPrivate => Status::Forbidden,
ErrorType::CannotReportYourself => Status::BadRequest, ErrorType::CannotReportYourself => Status::BadRequest,
@@ -80,6 +82,7 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotFound => Status::NotFound, ErrorType::NotFound => Status::NotFound,
ErrorType::NoEffect => Status::Ok, ErrorType::NoEffect => Status::Ok,
ErrorType::FailedValidation { .. } => Status::BadRequest, ErrorType::FailedValidation { .. } => Status::BadRequest,
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest, ErrorType::ProxyError => Status::BadRequest,
ErrorType::FileTooSmall => Status::UnprocessableEntity, ErrorType::FileTooSmall => Status::UnprocessableEntity,
+5 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-crond" name = "revolt-crond"
version = "0.8.3" version = "0.8.6"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
edition = "2021" edition = "2021"
@@ -16,7 +16,7 @@ log = "0.4"
tokio = { version = "1" } tokio = { version = "1" }
# Core # Core
revolt-database = { version = "0.8.3", path = "../../core/database" } revolt-database = { version = "0.8.6", path = "../../core/database" }
revolt-result = { version = "0.8.3", path = "../../core/result" } revolt-result = { version = "0.8.6", path = "../../core/result" }
revolt-config = { version = "0.8.3", path = "../../core/config" } revolt-config = { version = "0.8.6", path = "../../core/config" }
revolt-files = { version = "0.8.3", path = "../../core/files" } revolt-files = { version = "0.8.6", path = "../../core/files" }
+14 -5
View File
@@ -1,15 +1,23 @@
[package] [package]
name = "revolt-pushd" name = "revolt-pushd"
version = "0.8.3" version = "0.8.6"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
revolt-config = { version = "0.8.3", path = "../../core/config" } revolt-result = { version = "0.8.6", path = "../../core/result" }
revolt-database = { version = "0.8.3", path = "../../core/database" } revolt-config = { version = "0.8.6", path = "../../core/config", features = [
revolt-models = { version = "0.8.3", path = "../../core/models", features = [ "report-macros",
] }
revolt-database = { version = "0.8.6", path = "../../core/database" }
revolt-models = { version = "0.8.6", path = "../../core/models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.8.6", path = "../../core/presence", features = [
"redis-is-patched",
] }
anyhow = { version = "1.0.98" }
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
fcm_v1 = "0.3.0" fcm_v1 = "0.3.0"
@@ -20,9 +28,10 @@ tokio = "1.39.2"
async-trait = "0.1.81" async-trait = "0.1.81"
ulid = "1.0.0" ulid = "1.0.0"
authifier = "1.0.8" authifier = "1.0.10"
log = "0.4.11" log = "0.4.11"
pretty_env_logger = "0.4.0"
#serialization #serialization
serde_json = "1" serde_json = "1"
Binary file not shown.
@@ -71,8 +71,14 @@ impl AsyncConsumer for AckConsumer {
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
let unreads = self.db.fetch_unread_mentions(&payload.user_id).await; let unreads = self.db.fetch_unread_mentions(&payload.user_id).await;
debug!("Processing unreads for {:}", &payload.user_id);
if let Ok(u) = &unreads { if let Ok(u) = &unreads {
if u.is_empty() { if u.is_empty() {
debug!(
"Discarding unread task (no mentions found) for {:}",
&payload.user_id
);
return; return;
} }
} else { } else {
@@ -95,6 +101,10 @@ impl AsyncConsumer for AckConsumer {
.collect(); .collect();
if apple_sessions.is_empty() { if apple_sessions.is_empty() {
debug!(
"Discarding unread task (no apn sessions found) for {:}",
&payload.user_id
);
return; return;
} }
@@ -7,6 +7,7 @@ use amqprs::{
consumer::AsyncConsumer, consumer::AsyncConsumer,
BasicProperties, Deliver, BasicProperties, Deliver,
}; };
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
use revolt_database::{events::rabbit::*, Database}; use revolt_database::{events::rabbit::*, Database};
@@ -54,21 +55,16 @@ impl FRAcceptedConsumer {
channel: None, channel: None,
} }
} }
}
#[allow(unused_variables)] async fn consume_event(
#[async_trait]
impl AsyncConsumer for FRAcceptedConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self, &mut self,
channel: &Channel, _channel: &Channel,
deliver: Deliver, _deliver: Deliver,
basic_properties: BasicProperties, _basic_properties: BasicProperties,
content: Vec<u8>, content: Vec<u8>,
) { ) -> Result<()> {
let content = String::from_utf8(content).unwrap(); let content = String::from_utf8(content)?;
let payload: FRAcceptedPayload = serde_json::from_str(content.as_str()).unwrap(); let payload: FRAcceptedPayload = serde_json::from_str(content.as_str())?;
debug!("Received FR accept event"); debug!("Received FR accept event");
@@ -111,11 +107,34 @@ impl AsyncConsumer for FRAcceptedConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone()); .insert("endpoint".to_string(), sub.endpoint.clone());
} }
let payload = serde_json::to_string(&sendable).unwrap(); let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await; publish_message(self, payload.into(), args).await;
} }
} }
} }
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FRAcceptedConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await
{
revolt_config::capture_anyhow(&err);
eprintln!("Failed to process friend request accepted event: {err:?}");
}
} }
} }
@@ -7,6 +7,7 @@ use amqprs::{
consumer::AsyncConsumer, consumer::AsyncConsumer,
BasicProperties, Deliver, BasicProperties, Deliver,
}; };
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
use revolt_database::{events::rabbit::*, Database}; use revolt_database::{events::rabbit::*, Database};
@@ -54,21 +55,16 @@ impl FRReceivedConsumer {
channel: None, channel: None,
} }
} }
}
#[allow(unused_variables)] async fn consume_event(
#[async_trait]
impl AsyncConsumer for FRReceivedConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self, &mut self,
channel: &Channel, _channel: &Channel,
deliver: Deliver, _deliver: Deliver,
basic_properties: BasicProperties, _basic_properties: BasicProperties,
content: Vec<u8>, content: Vec<u8>,
) { ) -> Result<()> {
let content = String::from_utf8(content).unwrap(); let content = String::from_utf8(content)?;
let payload: FRReceivedPayload = serde_json::from_str(content.as_str()).unwrap(); let payload: FRReceivedPayload = serde_json::from_str(content.as_str())?;
debug!("Received FR received event"); debug!("Received FR received event");
@@ -111,11 +107,34 @@ impl AsyncConsumer for FRReceivedConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone()); .insert("endpoint".to_string(), sub.endpoint.clone());
} }
let payload = serde_json::to_string(&sendable).unwrap(); let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await; publish_message(self, payload.into(), args).await;
} }
} }
} }
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FRReceivedConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await
{
revolt_config::capture_anyhow(&err);
eprintln!("Failed to process friend request received event: {err:?}");
}
} }
} }
@@ -7,6 +7,7 @@ use amqprs::{
consumer::AsyncConsumer, consumer::AsyncConsumer,
BasicProperties, Deliver, BasicProperties, Deliver,
}; };
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
use revolt_database::{events::rabbit::*, Database}; use revolt_database::{events::rabbit::*, Database};
@@ -54,21 +55,16 @@ impl GenericConsumer {
channel: None, channel: None,
} }
} }
}
#[allow(unused_variables)] async fn consume_event(
#[async_trait]
impl AsyncConsumer for GenericConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self, &mut self,
channel: &Channel, _channel: &Channel,
deliver: Deliver, _deliver: Deliver,
basic_properties: BasicProperties, _basic_properties: BasicProperties,
content: Vec<u8>, content: Vec<u8>,
) { ) -> Result<()> {
let content = String::from_utf8(content).unwrap(); let content = String::from_utf8(content)?;
let payload: MessageSentPayload = serde_json::from_str(content.as_str()).unwrap(); let payload: MessageSentPayload = serde_json::from_str(content.as_str())?;
debug!("Received message event on origin"); debug!("Received message event on origin");
@@ -117,11 +113,34 @@ impl AsyncConsumer for GenericConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone()); .insert("endpoint".to_string(), sub.endpoint.clone());
} }
let payload = serde_json::to_string(&sendable).unwrap(); let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await; publish_message(self, payload.into(), args).await;
} }
} }
} }
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for GenericConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await
{
revolt_config::capture_anyhow(&err);
eprintln!("Failed to process generic event: {err:?}");
}
} }
} }
@@ -0,0 +1,292 @@
use std::{
collections::{HashMap, HashSet},
hash::RandomState,
};
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use anyhow::Result;
use async_trait::async_trait;
use revolt_database::{
events::rabbit::*, util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member,
MessageFlagsValue,
};
use revolt_models::v0::{MessageFlags, PushNotification};
pub struct MassMessageConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
}
impl Channeled for MassMessageConsumer {
fn get_connection(&self) -> Option<&Connection> {
if self.conn.is_none() {
None
} else {
Some(self.conn.as_ref().unwrap())
}
}
fn get_channel(&self) -> Option<&Channel> {
if self.channel.is_none() {
None
} else {
Some(self.channel.as_ref().unwrap())
}
}
fn set_connection(&mut self, conn: Connection) {
self.conn = Some(conn);
}
fn set_channel(&mut self, channel: Channel) {
self.channel = Some(channel)
}
}
impl MassMessageConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> MassMessageConsumer {
MassMessageConsumer {
db,
authifier_db,
conn: None,
channel: None,
}
}
async fn fire_notification_for_users(
&mut self,
push: &PushNotification,
users: &[String],
) -> Result<()> {
if let Ok(sessions) = self
.authifier_db
.find_sessions_with_subscription(users)
.await
{
let config = revolt_config::config().await;
for session in sessions {
if let Some(sub) = session.subscription {
let mut sendable = PayloadToService {
notification: PayloadKind::MessageNotification(push.clone()),
token: sub.auth,
user_id: session.user_id,
session_id: session.id,
extras: HashMap::new(),
};
let args: BasicPublishArguments;
if sub.endpoint == "apn" {
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
} else if sub.endpoint == "fcm" {
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.fcm.queue.as_str(),
)
.finish();
} else {
// web push (vapid)
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.vapid.queue.as_str(),
)
.finish();
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await;
}
}
}
Ok(())
}
async fn consume_event(
&mut self,
_channel: &Channel,
_deliver: Deliver,
_basic_properties: BasicProperties,
content: Vec<u8>,
) -> Result<()> {
let config = revolt_config::config().await;
let content = String::from_utf8(content)?;
let payload: MassMessageSentPayload = serde_json::from_str(content.as_str())?;
debug!("Received mass message event");
// We should only ever receive clumped messages from a single channel, so it's safe to reuse this many times.
let mut query: Option<BulkDatabasePermissionQuery<'_>> = None;
let query_db = self.db.clone();
for push in payload.notifications {
if query.is_none() {
query = Some(
BulkDatabasePermissionQuery::from_server_id(&query_db, &payload.server_id)
.await
.from_channel_id(push.channel.id().to_string()) // wrong channel model, so fetch the right one
.await,
);
}
let existing_mentions: HashSet<String, RandomState> =
if let Some(ref mentions) = push.message.mentions {
HashSet::from_iter(mentions.iter().cloned())
} else {
HashSet::new()
};
// KNOWN QUIRK: if you mention @online and role(s), the offline members with the role(s) wont get pinged
if let Some(ref query) = query {
let flags = MessageFlagsValue(push.message.flags);
if flags.has(MessageFlags::MentionsEveryone) {
let mut db_query = self
.db
.fetch_all_members_chunked(&payload.server_id)
.await?;
let mut exhausted = false;
let ack_chnl = vec![push.channel.id().to_string()];
loop {
let mut chunk: Vec<Member> = vec![];
for _ in 0..config.pushd.mass_mention_chunk_size {
if let Some(member) = db_query.next().await {
chunk.push(member);
} else {
exhausted = true;
break;
}
}
let userids: Vec<String> =
chunk.iter().map(|member| member.id.user.clone()).collect();
debug!("Userids in chunk: {:?}", userids);
if let Err(err) = self
.db
.add_mention_to_many_unreads(push.channel.id(), &userids, &ack_chnl)
.await
{
revolt_config::capture_error(&err);
}
// ignore anyone in this list
let online_users = revolt_presence::filter_online(&userids).await;
let target_users: Vec<String> = userids
.iter()
.filter(|id| {
!online_users.contains(*id) && !existing_mentions.contains(*id)
})
.cloned()
.collect();
debug!(
"Userids after filter: {:?} (online: {:?}",
target_users, online_users
);
self.fire_notification_for_users(&push, &target_users)
.await?;
if exhausted {
break;
}
}
} else if let Some(roles) = &push.message.role_mentions {
// role mentions
let mut role_members = self
.db
.fetch_all_members_with_roles_chunked(&payload.server_id, roles)
.await?;
let mut chunk = vec![];
let mut exhausted = false;
while !exhausted {
chunk.clear();
for _ in 0..config.pushd.mass_mention_chunk_size {
if let Some(member) = role_members.next().await {
chunk.push(member);
} else {
exhausted = true;
break;
}
}
let mut q = query.clone().members(&chunk);
let viewing_members: Vec<String> = q
.members_can_see_channel()
.await
.iter()
.filter_map(|(uid, viewable)| {
if *viewable && !existing_mentions.contains(uid) {
Some(uid.clone())
} else {
None
}
})
.collect();
debug!("viewing members: {:?}", viewing_members);
let online = revolt_presence::filter_online(&viewing_members).await;
debug!("online: {:?}", online);
let targets: Vec<String> = viewing_members
.iter()
.filter(|m| !online.contains(*m))
.cloned()
.collect();
debug!("targets: {:?}", targets);
self.fire_notification_for_users(&push, &targets).await?;
}
}
}
}
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for MassMessageConsumer {
/// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await
{
revolt_config::capture_anyhow(&err);
eprintln!("Failed to process mass message event: {err:?}");
}
}
}
@@ -7,6 +7,7 @@ use amqprs::{
consumer::AsyncConsumer, consumer::AsyncConsumer,
BasicProperties, Deliver, BasicProperties, Deliver,
}; };
use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
use revolt_database::{events::rabbit::*, Database}; use revolt_database::{events::rabbit::*, Database};
@@ -54,21 +55,16 @@ impl MessageConsumer {
channel: None, channel: None,
} }
} }
}
#[allow(unused_variables)] async fn consume_event(
#[async_trait]
impl AsyncConsumer for MessageConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self, &mut self,
channel: &Channel, _channel: &Channel,
deliver: Deliver, _deliver: Deliver,
basic_properties: BasicProperties, _basic_properties: BasicProperties,
content: Vec<u8>, content: Vec<u8>,
) { ) -> Result<()> {
let content = String::from_utf8(content).unwrap(); let content = String::from_utf8(content)?;
let payload: MessageSentPayload = serde_json::from_str(content.as_str()).unwrap(); let payload: MessageSentPayload = serde_json::from_str(content.as_str())?;
debug!("Received message event on origin"); debug!("Received message event on origin");
@@ -117,11 +113,34 @@ impl AsyncConsumer for MessageConsumer {
.insert("endpoint".to_string(), sub.endpoint.clone()); .insert("endpoint".to_string(), sub.endpoint.clone());
} }
let payload = serde_json::to_string(&sendable).unwrap(); let payload = serde_json::to_string(&sendable)?;
publish_message(self, payload.into(), args).await; publish_message(self, payload.into(), args).await;
} }
} }
} }
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for MessageConsumer {
/// This consumer handles delegating messages into their respective platform queues.
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await
{
revolt_config::capture_anyhow(&err);
eprintln!("Failed to process message event: {err:?}");
}
} }
} }
@@ -3,4 +3,5 @@ pub mod fr_accepted;
pub mod fr_received; pub mod fr_received;
pub mod generic; pub mod generic;
mod internal; mod internal;
pub mod mass_mention;
pub mod message; pub mod message;
@@ -1,6 +1,7 @@
use std::{borrow::Cow, collections::BTreeMap, io::Cursor}; use std::{borrow::Cow, collections::BTreeMap, io::Cursor};
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use base64::{ use base64::{
engine::{self}, engine::{self},
@@ -81,7 +82,7 @@ impl ApnsOutboundConsumer {
} }
} }
println!("Got badge count for APN: {}", mention_count); debug!("Got badge count for APN: {}", mention_count);
return Some(mention_count); return Some(mention_count);
} }
@@ -122,20 +123,16 @@ impl ApnsOutboundConsumer {
Ok(ApnsOutboundConsumer { db, client }) Ok(ApnsOutboundConsumer { db, client })
} }
}
#[allow(unused_variables)] async fn consume_event(
#[async_trait]
impl AsyncConsumer for ApnsOutboundConsumer {
async fn consume(
&mut self, &mut self,
channel: &AmqpChannel, _channel: &AmqpChannel,
deliver: Deliver, _deliver: Deliver,
basic_properties: BasicProperties, _basic_properties: BasicProperties,
content: Vec<u8>, content: Vec<u8>,
) { ) -> Result<()> {
let content = String::from_utf8(content).unwrap(); let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str()).unwrap(); let payload: PayloadToService = serde_json::from_str(content.as_str())?;
let payload_options = NotificationOptions { let payload_options = NotificationOptions {
apns_id: None, apns_id: None,
@@ -159,7 +156,7 @@ impl AsyncConsumer for ApnsOutboundConsumer {
alert.from_user.username, alert.from_user.discriminator alert.from_user.username, alert.from_user.discriminator
))) )))
.clone() .clone()
.unwrap(), .ok_or_else(|| anyhow!("missing name"))?,
)]; )];
let apn_payload = Payload { let apn_payload = Payload {
@@ -188,6 +185,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(), data: BTreeMap::new(),
}; };
debug!(
"Sending friend request received for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
@@ -201,7 +202,7 @@ impl AsyncConsumer for ApnsOutboundConsumer {
alert.accepted_user.username, alert.accepted_user.discriminator alert.accepted_user.username, alert.accepted_user.discriminator
))) )))
.clone() .clone()
.unwrap(), .ok_or_else(|| anyhow!("missing name"))?,
)]; )];
let apn_payload = Payload { let apn_payload = Payload {
@@ -230,6 +231,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(), data: BTreeMap::new(),
}; };
debug!(
"Sending friend request accept for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
PayloadKind::Generic(alert) => { PayloadKind::Generic(alert) => {
@@ -259,6 +264,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(), data: BTreeMap::new(),
}; };
debug!(
"Sending generic notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
@@ -294,6 +303,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
channel_name: alert.channel.name().unwrap_or(&title), channel_name: alert.channel.name().unwrap_or(&title),
}; };
debug!(
"Sending message notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
PayloadKind::BadgeUpdate(badge) => { PayloadKind::BadgeUpdate(badge) => {
@@ -307,6 +320,7 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(), data: BTreeMap::new(),
}; };
debug!("Sending badge update for user: {:}", &payload.user_id);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
} }
@@ -321,6 +335,10 @@ impl AsyncConsumer for ApnsOutboundConsumer {
}), }),
.. ..
}) => { }) => {
info!(
"Removing APNS subscription id {:} (user: {:}) due to invalid token",
&payload.session_id, &payload.user_id
);
if let Err(err) = self if let Err(err) = self
.db .db
.remove_push_subscription_by_session_id(&payload.session_id) .remove_push_subscription_by_session_id(&payload.session_id)
@@ -334,5 +352,27 @@ impl AsyncConsumer for ApnsOutboundConsumer {
} }
} }
} }
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for ApnsOutboundConsumer {
async fn consume(
&mut self,
channel: &AmqpChannel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await
{
revolt_config::capture_anyhow(&err);
eprintln!("Failed to process APN event: {err:?}");
}
} }
} }
@@ -2,6 +2,7 @@ use std::{collections::HashMap, time::Duration};
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait; use async_trait::async_trait;
use fcm_v1::{ use fcm_v1::{
android::AndroidConfig, android::AndroidConfig,
@@ -64,22 +65,16 @@ impl FcmOutboundConsumer {
), ),
}) })
} }
}
#[allow(unused_variables)] async fn consume_event(
#[async_trait]
impl AsyncConsumer for FcmOutboundConsumer {
async fn consume(
&mut self, &mut self,
channel: &AmqpChannel, _channel: &AmqpChannel,
deliver: Deliver, _deliver: Deliver,
basic_properties: BasicProperties, _basic_properties: BasicProperties,
content: Vec<u8>, content: Vec<u8>,
) { ) -> Result<()> {
let content = String::from_utf8(content).unwrap(); let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str()).unwrap(); let payload: PayloadToService = serde_json::from_str(content.as_str())?;
let config = revolt_config::config().await;
#[allow(clippy::needless_late_init)] #[allow(clippy::needless_late_init)]
let resp: Result<Message, FcmError>; let resp: Result<Message, FcmError>;
@@ -94,7 +89,7 @@ impl AsyncConsumer for FcmOutboundConsumer {
alert.from_user.username, alert.from_user.discriminator alert.from_user.username, alert.from_user.discriminator
))) )))
.clone() .clone()
.unwrap(); .ok_or_else(|| anyhow!("missing name"))?;
let mut data = HashMap::new(); let mut data = HashMap::new();
data.insert( data.insert(
@@ -122,7 +117,7 @@ impl AsyncConsumer for FcmOutboundConsumer {
alert.accepted_user.username, alert.accepted_user.discriminator alert.accepted_user.username, alert.accepted_user.discriminator
))) )))
.clone() .clone()
.unwrap(); .ok_or_else(|| anyhow!("missing name"))?;
let mut data: HashMap<String, Value> = HashMap::new(); let mut data: HashMap<String, Value> = HashMap::new();
data.insert( data.insert(
@@ -175,7 +170,7 @@ impl AsyncConsumer for FcmOutboundConsumer {
} }
PayloadKind::BadgeUpdate(_) => { PayloadKind::BadgeUpdate(_) => {
panic!("FCM cannot handle badge updates, and they should not be sent here.") bail!("FCM cannot handle badge updates and they should not be sent here.");
} }
} }
@@ -195,5 +190,27 @@ impl AsyncConsumer for FcmOutboundConsumer {
} }
} }
} }
Ok(())
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for FcmOutboundConsumer {
async fn consume(
&mut self,
channel: &AmqpChannel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await
{
revolt_config::capture_anyhow(&err);
eprintln!("Failed to process FCM event: {err:?}");
}
} }
} }
@@ -2,13 +2,13 @@ use std::collections::HashMap;
use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver};
use anyhow::{anyhow, bail, Result};
use async_trait::async_trait; use async_trait::async_trait;
use base64::{ use base64::{
engine::{self}, engine::{self},
Engine as _, Engine as _,
}; };
use revolt_database::{events::rabbit::*, Database}; use revolt_database::{events::rabbit::*, Database};
// use revolt_models::v0::{Channel, PushNotification};
use web_push::{ use web_push::{
ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder,
WebPushClient, WebPushError, WebPushMessageBuilder, WebPushClient, WebPushError, WebPushMessageBuilder,
@@ -21,11 +21,11 @@ pub struct VapidOutboundConsumer {
} }
impl VapidOutboundConsumer { impl VapidOutboundConsumer {
pub async fn new(db: Database) -> Result<VapidOutboundConsumer, &'static str> { pub async fn new(db: Database) -> Result<VapidOutboundConsumer> {
let config = revolt_config::config().await; let config = revolt_config::config().await;
if config.pushd.vapid.private_key.is_empty() | config.pushd.vapid.public_key.is_empty() { if config.pushd.vapid.private_key.is_empty() | config.pushd.vapid.public_key.is_empty() {
return Err("No Vapid keys present"); bail!("no Vapid keys present");
} }
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
@@ -38,28 +38,30 @@ impl VapidOutboundConsumer {
pkey: web_push_private_key, pkey: web_push_private_key,
}) })
} }
}
#[allow(unused_variables)] async fn consume_event(
#[async_trait]
impl AsyncConsumer for VapidOutboundConsumer {
async fn consume(
&mut self, &mut self,
channel: &AmqpChannel, _channel: &AmqpChannel,
deliver: Deliver, _deliver: Deliver,
basic_properties: BasicProperties, _basic_properties: BasicProperties,
content: Vec<u8>, content: Vec<u8>,
) { ) -> Result<()> {
let content = String::from_utf8(content).unwrap(); let content = String::from_utf8(content)?;
let payload: PayloadToService = serde_json::from_str(content.as_str()).unwrap(); let payload: PayloadToService = serde_json::from_str(content.as_str())?;
let config = revolt_config::config().await;
let subscription = SubscriptionInfo { let subscription = SubscriptionInfo {
endpoint: payload.extras.get("endpoint").unwrap().clone(), endpoint: payload
.extras
.get("endpoint")
.ok_or_else(|| anyhow!("missing endpoint"))?
.clone(),
keys: SubscriptionKeys { keys: SubscriptionKeys {
auth: payload.token, auth: payload.token,
p256dh: payload.extras.get("p256dh").unwrap().clone(), p256dh: payload
.extras
.get("p256dh")
.ok_or_else(|| anyhow!("missing p256dh"))?
.clone(),
}, },
}; };
@@ -76,12 +78,12 @@ impl AsyncConsumer for VapidOutboundConsumer {
alert.from_user.username, alert.from_user.discriminator alert.from_user.username, alert.from_user.discriminator
))) )))
.clone() .clone()
.unwrap(); .ok_or_else(|| anyhow!("missing name"))?;
let mut body = HashMap::new(); let mut body = HashMap::new();
body.insert("body", format!("{} sent you a friend request", name)); body.insert("body", format!("{} sent you a friend request", name));
payload_body = serde_json::to_string(&body).unwrap(); payload_body = serde_json::to_string(&body)?;
} }
PayloadKind::FRAccepted(alert) => { PayloadKind::FRAccepted(alert) => {
let name = alert let name = alert
@@ -92,21 +94,21 @@ impl AsyncConsumer for VapidOutboundConsumer {
alert.accepted_user.username, alert.accepted_user.discriminator alert.accepted_user.username, alert.accepted_user.discriminator
))) )))
.clone() .clone()
.unwrap(); .ok_or_else(|| anyhow!("missing name"))?;
let mut body = HashMap::new(); let mut body = HashMap::new();
body.insert("body", format!("{} accepted your friend request", name)); body.insert("body", format!("{} accepted your friend request", name));
payload_body = serde_json::to_string(&body).unwrap(); payload_body = serde_json::to_string(&body)?;
} }
PayloadKind::Generic(alert) => { PayloadKind::Generic(alert) => {
payload_body = serde_json::to_string(&alert).unwrap(); payload_body = serde_json::to_string(&alert)?;
} }
PayloadKind::MessageNotification(alert) => { PayloadKind::MessageNotification(alert) => {
payload_body = serde_json::to_string(&alert).unwrap(); payload_body = serde_json::to_string(&alert)?;
} }
PayloadKind::BadgeUpdate(_) => { PayloadKind::BadgeUpdate(_) => {
panic!("Vapid cannot handle badge updates, and they should not be sent here.") bail!("Vapid cannot handle badge updates and they should not be sent here.");
} }
} }
@@ -122,28 +124,40 @@ impl AsyncConsumer for VapidOutboundConsumer {
Ok(msg) => { Ok(msg) => {
if let Err(err) = self.client.send(msg).await { if let Err(err) = self.client.send(msg).await {
if err == WebPushError::Unauthorized { if err == WebPushError::Unauthorized {
if let Err(err) = self self.db
.db
.remove_push_subscription_by_session_id(&payload.session_id) .remove_push_subscription_by_session_id(&payload.session_id)
.await?;
}
}
Ok(())
}
Err(err) => Err(err.into()),
}
}
Err(err) => Err(err.into()),
},
Err(err) => Err(err.into()),
}
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for VapidOutboundConsumer {
async fn consume(
&mut self,
channel: &AmqpChannel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
if let Err(err) = self
.consume_event(channel, deliver, basic_properties, content)
.await .await
{ {
revolt_config::capture_error(&err); revolt_config::capture_anyhow(&err);
} eprintln!("Failed to process Vapid event: {err:?}");
}
}
}
Err(err) => {
revolt_config::capture_error(&err);
}
}
}
Err(err) => {
revolt_config::capture_error(&err);
}
},
Err(err) => {
revolt_config::capture_error(&err);
}
} }
} }
} }
+23 -7
View File
@@ -1,3 +1,6 @@
#[macro_use]
extern crate log;
use amqprs::{ use amqprs::{
channel::{ channel::{
BasicConsumeArguments, Channel, ExchangeDeclareArguments, QueueBindArguments, BasicConsumeArguments, Channel, ExchangeDeclareArguments, QueueBindArguments,
@@ -14,14 +17,15 @@ mod consumers;
use consumers::{ use consumers::{
inbound::{ inbound::{
ack::AckConsumer, fr_accepted::FRAcceptedConsumer, fr_received::FRReceivedConsumer, ack::AckConsumer, fr_accepted::FRAcceptedConsumer, fr_received::FRReceivedConsumer,
generic::GenericConsumer, message::MessageConsumer, generic::GenericConsumer, mass_mention::MassMessageConsumer, message::MessageConsumer,
}, },
outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer}, outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer},
}; };
#[tokio::main(flavor = "multi_thread", worker_threads = 2)] #[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() { async fn main() {
let config = config().await; // Configure logging and environment
revolt_config::configure!(pushd);
// Setup database // Setup database
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap(); let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
@@ -48,6 +52,8 @@ async fn main() {
// This'll require some interesting shimming if we need to add more events once this is in prod (different payloads between prod and test), // This'll require some interesting shimming if we need to add more events once this is in prod (different payloads between prod and test),
// but that sounds like a problem for future us. // but that sounds like a problem for future us.
let config = config().await;
// inbound: generic // inbound: generic
connections.push( connections.push(
make_queue_and_consume( make_queue_and_consume(
@@ -96,6 +102,17 @@ async fn main() {
.await, .await,
); );
connections.push(
make_queue_and_consume(
&config,
&config.pushd.mass_mention_queue,
config.pushd.get_mass_mention_routing_key().as_str(),
None,
MassMessageConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
if !config.pushd.apn.pkcs8.is_empty() { if !config.pushd.apn.pkcs8.is_empty() {
connections.push( connections.push(
make_queue_and_consume( make_queue_and_consume(
@@ -223,11 +240,10 @@ where
.manual_ack(false) .manual_ack(false)
.finish(); .finish();
channel.basic_consume(consumer, args).await.unwrap(); let routing_key = channel.basic_consume(consumer, args).await.unwrap();
log::info!( info!(
"Consuming routing key {} as queue {}", "Consuming routing key {} as queue {}, tag {}",
routing_key, routing_key, queue_name, routing_key
queue_name
); );
(channel, connection) (channel, connection)
} }
+4 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.8.3" version = "0.8.6"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -52,10 +52,11 @@ async-std = { version = "1.8.0", features = [
lettre = "0.10.0-alpha.4" lettre = "0.10.0-alpha.4"
# web # web
rocket = { version = "0.5.1", default-features = false, features = ["json"] } rocket = { version = "0.5.1", default-features = false, features = ["json"] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" } rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" }
rocket_empty = { version = "0.1.1", features = ["schema"] } rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_authifier = { version = "1.0.9" } rocket_authifier = { version = "1.0.13" }
rocket_prometheus = "0.10.0-rc.3" rocket_prometheus = "0.10.0-rc.3"
# spec generation # spec generation
@@ -66,7 +67,7 @@ revolt_rocket_okapi = { version = "0.10.0", features = ["swagger"] }
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
# core # core
authifier = "1.0.9" authifier = "1.0.13"
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database", features = [ revolt-database = { path = "../core/database", features = [
"rocket-impl", "rocket-impl",
@@ -99,5 +99,26 @@ mod test {
// TEST: member leaves group (no delete) // TEST: member leaves group (no delete)
// TEST: no effect with saved messages // TEST: no effect with saved messages
// TEST: DM set to inactive // TEST: DM set to inactive
// TEST: server channel deleted
#[rocket::async_test]
async fn success_delete_channel() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, channels) = harness.new_server(&user).await;
let response = TestHarness::with_session(
session,
harness
.client
.delete(format!("/channels/{}", channels[0].id())),
)
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness
.wait_for_event(channels[0].id(), |event| match event {
EventV1::ChannelDelete { id, .. } => id == channels[0].id(),
_ => false,
})
.await;
}
} }
@@ -1,4 +1,3 @@
use futures::future::join_all;
use revolt_database::{ use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, util::{permissions::DatabasePermissionQuery, reference::Reference},
Channel, Database, User, Channel, Database, User,
@@ -8,7 +8,6 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State}; use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse; use rocket_empty::EmptyResponse;
use serde::Deserialize;
use validator::Validate; use validator::Validate;
/// # Bulk Delete Messages /// # Bulk Delete Messages

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