Compare commits

...

74 Commits

Author SHA1 Message Date
izzy
c4728c696d feat: policy changes API
chore: bump version to 0.8.7
2025-05-30 13:12:57 +01:00
IAmTomahawkx
8153f5f17a Don't panic on empty message list
... even though it shouldn't be possible.
2025-05-15 03:20:32 -07:00
izzy
99a80f723d chore: increase verbosity for ack logging 2025-05-14 11:46:56 +01:00
izzy
4f13f5899b fix: authifier should not use transactions for migrations 2025-05-14 11:40:07 +01:00
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
Paul Makles
d1236bf8f2 chore: bump version to 0.8.3 2025-02-24 22:18:10 +00:00
Paul Makles
01490f5723 feat: add company information to email footers 2025-02-24 22:17:48 +00:00
Paul Makles
3ceefb840b chore: clarify cargo deny / tomls 2025-02-18 11:53:21 +00:00
Paul Makles
eb5f5f91cd feat: add crond container definitions 2025-02-10 20:28:48 +00:00
Paul Makles
52e1f0ddde merge: branch 'insert/feat/crond' 2025-02-10 19:54:40 +00:00
Paul Makles
defc9ec79b fix: match new error type for status code 2025-02-10 19:05:21 +00:00
Paul Makles
7026961df4 chore: fix version references 2025-02-10 19:03:48 +00:00
Paul Makles
078380d305 merge: branch 'main' into insert/feat/crond 2025-02-10 19:02:19 +00:00
Paul Makles
add3f40b23 fix: don't allow users to time themselves out
closes #376
2025-02-10 18:54:15 +00:00
Paul Makles
7216e9909b chore: disable LTO because it likely overloads GitHub worker 2025-02-10 18:50:19 +00:00
Paul Makles
a1e6a19210 chore: bump version to 0.8.2 2025-02-10 18:37:21 +00:00
Paul Makles
5f39403ce7 fix: ensure ratelimiter adjusts for version prefix 2025-02-10 18:34:37 +00:00
Paul Makles
e525ffe5e4 chore: mount API at /0.8/ as well 2025-02-10 18:19:34 +00:00
Paul Makles
e3723d647e fix: change permission check for fetching channel webhooks 2025-02-10 18:04:46 +00:00
Paul Makles
5f84daa9db fix: ensure limit is always at least 1 when sent to database 2025-02-10 17:51:22 +00:00
Paul Makles
e957af4ca3 docs: add some notes on overrides; fix defaults 2025-02-10 17:21:23 +00:00
Paul Makles
439bacf067 refactor: add error messages for Rabbit issues 2025-02-10 17:20:29 +00:00
Paul Makles
fa55e88dd9 chore: add pushd to debug image script 2025-02-10 17:19:54 +00:00
IAmTomahawkx
71e7fe3086 chore: bump release for patch 2024-12-27 16:35:02 -08:00
IAmTomahawkx
03f2e3b1bf fix: last message in a channel could not be acked, resulting in ghost dms and channel messages 2024-12-27 16:29:56 -08:00
Paul Makles
8bbb579d23 ci: revert back to GH runner 2024-12-26 14:59:03 +00:00
Paul Makles
6448af071b ci: remove purge command 2024-12-22 16:10:09 +00:00
Paul Makles
249749e14d feat: file deletion implementation 2024-11-27 21:54:39 +00:00
Paul Makles
acc4317246 feat: init crond crate 2024-11-27 16:56:30 +00:00
166 changed files with 7284 additions and 5757 deletions

View File

@@ -1,2 +1,2 @@
[profile.default]
slow-timeout = { period = "3s", terminate-after = 2 }
slow-timeout = { period = "5s", terminate-after = 10 }

View File

@@ -2,24 +2,13 @@ name: Docker Test & Publish
on:
push:
# branches:
# - "main"
tags:
- "*"
paths-ignore:
- ".github/**"
- "!.github/workflows/docker.yml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request:
branches:
- "main"
paths:
- "Dockerfile"
workflow_dispatch:
permissions:
contents: read
@@ -27,50 +16,35 @@ permissions:
jobs:
base:
runs-on: self-hosted
name: Build base image
name: Test base image build
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v3
- name: Fix https://github.com/actions/runner-images/issues/9959
run: sudo apt-get autopurge -y needrestart
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Authenticate with GHCR
- name: Login to Github Container Registry
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Build the image
- name: Build base image
uses: docker/build-push-action@v4
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
push: false
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest
cache-from: type=gha,scope=buildx-base-multi-arch
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
publish:
needs: [base]
runs-on: self-hosted
if: github.event_name != 'pull_request'
strategy:
matrix:
project: [delta, bonfire, autumn, january, pushd]
name: Build ${{ matrix.project }} image
name: Publish Docker images
steps:
# Configure build environment
- name: Checkout
uses: actions/checkout@v3
- name: Fix https://github.com/actions/runner-images/issues/9959
run: sudo apt-get autopurge -y needrestart
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
@@ -81,6 +55,7 @@ jobs:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v2
with:
@@ -88,54 +63,131 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Resolve the correct project
- uses: kanga333/variable-mapper@master
id: export
# Build the image
- name: Build base image
uses: docker/build-push-action@v4
with:
key: "${{ matrix.project }}"
map: |
{
"delta": {
"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"
}
}
export_to: output
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest
# Configure metadata
# revoltchat/server
- name: Docker meta
id: meta
id: meta-delta
uses: docker/metadata-action@v4
with:
images: |
docker.io/${{ steps.export.outputs.tag }}
ghcr.io/${{ steps.export.outputs.tag }}
# Build crate image
docker.io/revoltchat/server
ghcr.io/revoltchat/server
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: ${{ steps.export.outputs.path }}/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
file: crates/delta/Dockerfile
tags: ${{ steps.meta-delta.outputs.tags }}
build-args: |
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 }}

3
.gitignore vendored
View File

@@ -1,9 +1,12 @@
Rocket.toml
Revolt.*.toml
compose.override.yml
target
.data
.env
.venv/
venv/
.vercel
.DS_Store

View File

@@ -1,6 +1,6 @@
{
"editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.check.command": "clippy",
"nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceFolder}/default.nix"
}
}

4327
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +6,15 @@ members = [
"crates/bonfire",
"crates/core/*",
"crates/services/*",
"crates/bindings/*",
"crates/daemons/pushd",
"crates/daemons/*",
]
[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.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" }
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
# I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just
# disable it for now. In the future, we could use this
# if we were rolling our own CI (that is now).
[profile.release]
lto = true

View File

@@ -1,5 +1,5 @@
# Build Stage
FROM --platform="${BUILDPLATFORM}" rust:1.77.2-slim-bookworm
FROM --platform="${BUILDPLATFORM}" rust:1.86.0-slim-bookworm
USER 0:0
WORKDIR /home/rust/src
@@ -17,18 +17,19 @@ RUN sh /tmp/build-image-layer.sh tools
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/files/Cargo.toml ./crates/core/files/
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/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps

View File

@@ -1,5 +1,5 @@
# Build Stage
FROM rust:1.77.2-slim-bookworm
FROM rust:1.86.0-slim-bookworm
USER 0:0
WORKDIR /home/rust/src
@@ -13,18 +13,19 @@ COPY scripts/build-image-layer.sh /tmp/
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
COPY crates/core/database/Cargo.toml ./crates/core/database/
COPY crates/core/files/Cargo.toml ./crates/core/files/
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/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
RUN sh /tmp/build-image-layer.sh deps

View File

@@ -12,31 +12,31 @@
The services and libraries that power the Revolt service.<br/>
<br/>
| 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/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/models` | [crates/core/models](crates/core/models) | Core: API Models | ![Crates.io Version](https://img.shields.io/crates/v/revolt-models) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-models) ![Crates.io Version](https://img.shields.io/crates/size/revolt-models) ![Crates.io License](https://img.shields.io/crates/l/revolt-models) |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/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/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| 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/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/models` | [crates/core/models](crates/core/models) | Core: API Models | ![Crates.io Version](https://img.shields.io/crates/v/revolt-models) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-models) ![Crates.io Version](https://img.shields.io/crates/size/revolt-models) ![Crates.io License](https://img.shields.io/crates/l/revolt-models) |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
</div>
<br/>
## Minimum Supported Rust Version
Rust 1.76 or higher.
Rust 1.86.0 or higher.
> [!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
@@ -61,7 +61,7 @@ As a heads-up, the development environment uses the following ports:
| MinIO | 14009 |
| Maildev | 14025<br>14080 |
| Revolt Web App | 14701 |
| RabbitMQ | 5672<br>15672 |
| RabbitMQ | 5672<br>15672 |
| `crates/delta` | 14702 |
| `crates/bonfire` | 14703 |
| `crates/services/autumn` | 14704 |
@@ -91,6 +91,38 @@ If you'd like to change anything, create a `Revolt.overrides.toml` file and spec
> proxy = "https://abc@your.sentry/1"
> ```
> [!TIP]
> If you have port conflicts on common services, you can try the following:
>
> ```yaml
> # compose.override.yml
> services:
> redis:
> ports: !override
> - "14079:6379"
>
> database:
> ports: !override
> - "14017:27017"
>
> rabbit:
> ports: !override
> - "14072:5672"
> - "14672:15672"
> ```
>
> And corresponding Revolt configuration:
>
> ```toml
> # Revolt.overrides.toml
> [database]
> mongodb = "mongodb://127.0.0.1:14017"
> redis = "redis://127.0.0.1:14079/"
>
> [rabbit]
> port = 14072
> ```
Then continue:
```bash

View File

@@ -1,13 +1,16 @@
# ⚠️ 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]
# MongoDB connection URL
# Defaults to the container name specified in self-hosted
mongodb = "mongodb://127.0.0.1:14017"
mongodb = "mongodb://127.0.0.1:27017"
# Redis connection URL
# Defaults to the container name specified in self-hosted
redis = "redis://127.0.0.1:14079/"
redis = "redis://127.0.0.1:6379/"
[rabbit]
host = "127.0.0.1"
[hosts]
# Web locations of various services
@@ -35,6 +38,7 @@ from_address = "development@revolt.chat"
reply_to = "support@revolt.chat"
port = 14025
use_tls = false
use_starttls = false
[files.s3]
# S3 protocol endpoint

View File

@@ -1,7 +0,0 @@
target
index.node
**/node_modules
**/.DS_Store
npm-debug.log*
cargo.log
cross.log

View File

@@ -1,25 +0,0 @@
[package]
name = "revolt-nodejs-bindings"
version = "0.8.0"
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.0", path = "../../core/config" }
revolt-result = { version = "0.8.0", path = "../../core/result" }
revolt-database = { version = "0.8.0", path = "../../core/database" }

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).

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 }>;

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"
}
}

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

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(())
}

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())
);

View File

@@ -1,6 +0,0 @@
{
"name": "bindings",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.8.0"
version = "0.8.7"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -36,12 +36,12 @@ async-std = { version = "1.8.0", features = [
] }
# core
authifier = { version = "1.0.9" }
authifier = { version = "1.0.15" }
revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.0", path = "../core/permissions" }
revolt-permissions = { version = "0.8.7", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis

View File

@@ -1,5 +1,6 @@
use std::collections::HashSet;
use futures::future::join_all;
use revolt_database::{
events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery,
@@ -99,6 +100,18 @@ impl State {
let user = self.clone_user();
self.cache.is_bot = user.bot.is_some();
// Fetch pending policy changes.
let policy_changes = if user.bot.is_some() {
vec![]
} else {
db.fetch_policy_changes()
.await?
.into_iter()
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.map(Into::into)
.collect()
};
// Find all relationships to the user.
let mut user_ids: HashSet<String> = user
.relations
@@ -174,7 +187,7 @@ impl State {
.iter()
.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 {
None
};
@@ -198,13 +211,11 @@ impl State {
.collect();
// Make all users appear from our perspective.
let mut users: Vec<v0::User> = users
.into_iter()
.map(|other_user| {
let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online)
})
.collect();
let mut users: Vec<v0::User> = join_all(users.into_iter().map(|other_user| async {
let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online).await
}))
.await;
// Make sure we see our own user correctly.
users.push(user.into_self(true).await);
@@ -228,6 +239,7 @@ impl State {
for channel in &channels {
self.insert_subscription(channel.id().to_string()).await;
}
Ok(EventV1::Ready {
users: if fields.contains(&ReadyPayloadFields::Users) {
Some(users)
@@ -253,6 +265,8 @@ impl State {
user_settings,
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
policy_changes,
})
}

View File

@@ -3,7 +3,7 @@ use std::{collections::HashSet, net::SocketAddr, sync::Arc};
use async_tungstenite::WebSocketStream;
use authifier::AuthifierEvent;
use fred::{
error::{RedisError, RedisErrorKind},
error::RedisErrorKind,
interfaces::{ClientLike, EventInterface, PubsubInterface},
types::RedisConfig,
};

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.8.0"
version = "0.8.7"
edition = "2021"
license = "MIT"
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
[features]
anyhow = ["dep:sentry-anyhow"]
report-macros = ["revolt-result"]
test = ["async-std"]
default = ["test"]
default = ["test", "anyhow"]
[dependencies]
# Utility
@@ -32,6 +33,7 @@ pretty_env_logger = "0.4.0"
# Sentry
sentry = "0.31.5"
sentry-anyhow = { version = "0.38.1", optional = true }
# Core
revolt-result = { version = "0.8.0", path = "../result", optional = true }
revolt-result = { version = "0.8.7", path = "../result", optional = true }

View File

@@ -7,3 +7,6 @@ host = "127.0.0.1"
port = 5672
username = "rabbituser"
password = "rabbitpass"
[features]
webhooks_enabled = true

View File

@@ -1,3 +1,5 @@
production = false
[database]
# MongoDB connection URL
# Defaults to the container name specified in self-hosted
@@ -52,6 +54,8 @@ authifier_shield_key = ""
voso_legacy_token = ""
# Whether services are behind the Cloudflare network
trust_cloudflare = false
# easypwned endpoint
easypwned = ""
[api.security.captcha]
# hCaptcha configuration
@@ -62,19 +66,28 @@ hcaptcha_sitekey = ""
# Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50
[api.users]
[pushd]
# this changes the names of the queues to not overlap
# 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
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
exchange = "revolt.notifications"
message_queue = "notifications.origin.message"
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices
mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices
[pushd.vapid]
queue = "notifications.outbound.vapid"
@@ -171,9 +184,13 @@ default_bucket = "revolt-uploads"
[features]
# Bucket to upload to by default
# Feature gate options
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]
@@ -258,3 +275,5 @@ api = ""
events = ""
files = ""
proxy = ""
pushd = ""
crond = ""

View File

@@ -7,6 +7,7 @@ use once_cell::sync::Lazy;
use serde::Deserialize;
pub use sentry::{capture_error, capture_message, Level};
pub use sentry_anyhow::capture_anyhow;
#[cfg(feature = "report-macros")]
#[macro_export]
@@ -123,6 +124,7 @@ pub struct ApiSmtp {
pub reply_to: Option<String>,
pub port: Option<i32>,
pub use_tls: Option<bool>,
pub use_starttls: Option<bool>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -168,6 +170,7 @@ pub struct ApiSecurity {
pub voso_legacy_token: String,
pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool,
pub easypwned: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -175,19 +178,29 @@ pub struct ApiWorkers {
pub max_concurrent_connections: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiUsers {
pub early_adopter_cutoff: Option<u64>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Api {
pub registration: ApiRegistration,
pub smtp: ApiSmtp,
pub security: ApiSecurity,
pub workers: ApiWorkers,
pub users: ApiUsers,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Pushd {
pub production: bool,
pub exchange: String,
pub mass_mention_chunk_size: usize,
// Queues
pub message_queue: String,
pub mass_mention_queue: String,
pub fr_accepted_queue: String,
pub fr_received_queue: String,
pub generic_queue: String,
@@ -214,6 +227,10 @@ impl Pushd {
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 {
self.get_routing_key(self.fr_accepted_queue.clone())
}
@@ -314,6 +331,9 @@ impl Default for FeaturesAdvanced {
pub struct Features {
pub limits: FeaturesLimitsCollection,
pub webhooks_enabled: bool,
pub mass_mentions_send_notifications: bool,
pub mass_mentions_enabled: bool,
#[serde(default)]
pub advanced: FeaturesAdvanced,
}
@@ -324,6 +344,8 @@ pub struct Sentry {
pub events: String,
pub files: String,
pub proxy: String,
pub pushd: String,
pub crond: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -336,6 +358,7 @@ pub struct Settings {
pub files: Files,
pub features: Features,
pub sentry: Sentry,
pub production: bool,
}
impl Settings {
@@ -363,7 +386,14 @@ pub async fn read() -> Config {
#[cached(time = 30)]
pub async fn config() -> Settings {
read().await.try_deserialize::<Settings>().unwrap()
let mut config = read().await.try_deserialize::<Settings>().unwrap();
// auto-detect production nodes
if config.hosts.api.contains("https") && config.hosts.api.contains("revolt.chat") {
config.production = true;
}
config
}
/// Configure logging and common Rust variables

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.8.0"
version = "0.8.7"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -24,18 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.8.0", path = "../config", features = [
revolt-config = { version = "0.8.7", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.0", path = "../result" }
revolt-models = { version = "0.8.0", path = "../models", features = [
revolt-result = { version = "0.8.7", path = "../result" }
revolt-models = { version = "0.8.7", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.8.0", path = "../presence" }
revolt-permissions = { version = "0.8.0", path = "../permissions", features = [
revolt-presence = { version = "0.8.7", path = "../presence" }
revolt-permissions = { version = "0.8.7", path = "../permissions", features = [
"serde",
"bson",
] }
revolt-parser = { version = "0.8.7", path = "../parser" }
# Utility
log = "0.4"
@@ -64,7 +65,7 @@ redis-kiss = { version = "0.1.4" }
# Database
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
unicode-segmentation = "1.10.1"
@@ -96,7 +97,7 @@ web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.9", features = ["rocket_impl"] }
authifier = { version = "1.0.15", features = ["rocket_impl"] }
# RabbitMQ
amqprs = { version = "1.7.0" }

View File

@@ -170,6 +170,38 @@ impl AMQP {
.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(
&self,
user_id: String,

View File

@@ -3,6 +3,7 @@ mod reference;
use authifier::config::Captcha;
use authifier::config::EmailVerificationConfig;
use authifier::config::PasswordScanning;
use authifier::config::ResolveIp;
use authifier::config::SMTPSettings;
use authifier::config::Shield;
@@ -102,6 +103,13 @@ impl Database {
let config = config().await;
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() {
EmailVerificationConfig::Enabled {
smtp: SMTPSettings {
@@ -118,28 +126,69 @@ impl Database {
),
port: config.api.smtp.port,
use_tls: config.api.smtp.use_tls,
use_starttls: config.api.smtp.use_starttls,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("../../templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("../../templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../../templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("../../templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("../../templates/deletion.html").into()),
},
welcome: None,
templates: if config.production {
Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("../../templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("../../templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../../templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
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 {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("../../templates/deletion.html").into()),
},
welcome: None,
}
} else {
Templates {
verify: Template {
title: "Verify your account.".into(),
text: include_str!("../../templates/verify.whitelabel.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: None,
},
reset: Template {
title: "Reset your password.".into(),
text: include_str!("../../templates/reset.whitelabel.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
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 {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.whitelabel.txt")
.into(),
url: format!("{}/delete/", config.hosts.app),
html: None,
},
welcome: None,
}
},
}
} else {

View File

@@ -31,17 +31,17 @@ impl MongoDb {
}
/// 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)
}
/// Insert one document into a collection
pub async fn insert_one<T: Serialize>(
pub async fn insert_one<T: Serialize + Send + Sync>(
&self,
collection: &'static str,
document: T,
) -> Result<InsertOneResult> {
self.col::<T>(collection).insert_one(document, None).await
self.col::<T>(collection).insert_one(document).await
}
/// Count documents by projection
@@ -51,7 +51,7 @@ impl MongoDb {
projection: Document,
) -> Result<u64> {
self.col::<Document>(collection)
.count_documents(projection, None)
.count_documents(projection)
.await
}
@@ -67,7 +67,8 @@ impl MongoDb {
{
Ok(self
.col::<T>(collection)
.find(projection, options)
.find(projection)
.with_options(options)
.await?
.filter_map(|s| async {
if cfg!(debug_assertions) {
@@ -101,7 +102,8 @@ impl MongoDb {
O: Into<Option<FindOneOptions>>,
{
self.col::<T>(collection)
.find_one(projection, options)
.find_one(projection)
.with_options(options)
.await
}
@@ -165,7 +167,7 @@ impl MongoDb {
};
self.col::<Document>(collection)
.update_one(projection, query, None)
.update_one(projection, query)
.await
}
@@ -200,7 +202,7 @@ impl MongoDb {
projection: Document,
) -> Result<DeleteResult> {
self.col::<Document>(collection)
.delete_one(projection, None)
.delete_one(projection)
.await
}

View File

@@ -4,8 +4,8 @@ use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
UserSettings, Webhook,
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
User, UserSettings, Webhook,
};
database_derived!(
@@ -21,6 +21,7 @@ database_derived!(
pub file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,
pub policy_changes: Arc<Mutex<HashMap<String, PolicyChange>>>,
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,

View File

@@ -6,7 +6,7 @@ use revolt_models::v0::{
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage,
FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message,
PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser,
PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, Webhook,
PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, Webhook,
};
use crate::Database;
@@ -62,6 +62,8 @@ pub enum EventV1 {
user_settings: Option<UserSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
channel_unreads: Option<Vec<ChannelUnread>>,
policy_changes: Vec<PolicyChange>,
},
/// Ping response

View File

@@ -11,6 +11,12 @@ pub struct MessageSentPayload {
pub users: Vec<String>,
}
#[derive(Serialize, Deserialize)]
pub struct MassMessageSentPayload {
pub notifications: Vec<PushNotification>,
pub server_id: String,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct FRAcceptedPayload {
pub accepted_user: User,

View File

@@ -10,7 +10,7 @@ impl AbstractMigrations for MongoDb {
#[cfg(test)]
/// Drop the database
async fn drop_database(&self) {
self.db().drop(None).await.ok();
self.db().drop().await.ok();
}
/// Migrate the database
@@ -18,7 +18,7 @@ impl AbstractMigrations for MongoDb {
info!("Migrating the database.");
let list = self
.list_database_names(None, None)
.list_database_names()
.await
.expect("Failed to fetch database names.");

View File

@@ -8,278 +8,258 @@ pub async fn create_database(db: &MongoDb) {
info!("Creating database.");
let db = db.db();
db.create_collection("accounts", None)
db.create_collection("accounts")
.await
.expect("Failed to create accounts collection.");
db.create_collection("users", None)
db.create_collection("users")
.await
.expect("Failed to create users collection.");
db.create_collection("channels", None)
db.create_collection("channels")
.await
.expect("Failed to create channels collection.");
db.create_collection("messages", None)
db.create_collection("messages")
.await
.expect("Failed to create messages collection.");
db.create_collection("servers", None)
db.create_collection("servers")
.await
.expect("Failed to create servers collection.");
db.create_collection("server_members", None)
db.create_collection("server_members")
.await
.expect("Failed to create server_members collection.");
db.create_collection("server_bans", None)
db.create_collection("server_bans")
.await
.expect("Failed to create server_bans collection.");
db.create_collection("channel_invites", None)
db.create_collection("channel_invites")
.await
.expect("Failed to create channel_invites collection.");
db.create_collection("channel_unreads", None)
db.create_collection("channel_unreads")
.await
.expect("Failed to create channel_unreads collection.");
db.create_collection("channel_webhooks", None)
db.create_collection("channel_webhooks")
.await
.expect("Failed to create channel_webhooks collection.");
db.create_collection("migrations", None)
db.create_collection("migrations")
.await
.expect("Failed to create migrations collection.");
db.create_collection("attachments", None)
db.create_collection("attachments")
.await
.expect("Failed to create attachments collection.");
db.create_collection("attachment_hashes", None)
db.create_collection("attachment_hashes")
.await
.expect("Failed to create attachment_hashes collection.");
db.create_collection("user_settings", None)
db.create_collection("user_settings")
.await
.expect("Failed to create user_settings collection.");
db.create_collection("safety_reports", None)
db.create_collection("policy_changes")
.await
.expect("Failed to create policy_changes collection.");
db.create_collection("safety_reports")
.await
.expect("Failed to create safety_reports collection.");
db.create_collection("safety_snapshots", None)
db.create_collection("safety_snapshots")
.await
.expect("Failed to create safety_snapshots collection.");
db.create_collection("safety_strikes", None)
db.create_collection("safety_strikes")
.await
.expect("Failed to create safety_strikes collection.");
db.create_collection("bots", None)
db.create_collection("bots")
.await
.expect("Failed to create bots collection.");
db.create_collection("ratelimit_events", None)
db.create_collection("ratelimit_events")
.await
.expect("Failed to create ratelimit_events collection.");
db.create_collection(
"pubsub",
CreateCollectionOptions::builder()
.capped(true)
.size(1_000_000)
.build(),
)
.await
.expect("Failed to create pubsub collection.");
db.create_collection("pubsub")
.with_options(
CreateCollectionOptions::builder()
.capped(true)
.size(1_000_000)
.build(),
)
.await
.expect("Failed to create pubsub collection.");
db.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
db.run_command(doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
]
},
None,
)
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
})
.await
.expect("Failed to create username index.");
db.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
db.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
"name": "content"
},
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
{
"key": {
"author": 1_i32
},
"name": "author"
"name": "channel_id_compound"
},
{
"key": {
"author": 1_i32
},
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
"name": "author"
},
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
]
},
None,
)
"name": "channel_pinned_compound"
},
]
})
.await
.expect("Failed to create message index.");
db.run_command(
doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
db.run_command(doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
})
.await
.expect("Failed to create channel_unreads index.");
db.run_command(
doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
db.run_command(doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
})
.await
.expect("Failed to create server_members index.");
db.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
db.run_command(doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
},
None,
)
"name": "hash"
},
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
})
.await
.expect("Failed to create attachments index.");
db.run_command(
doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
},
None,
)
db.run_command(doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
})
.await
.expect("Failed to create attachment_hashes index.");
db.collection("migrations")
.insert_one(
doc! {
"_id": 0_i32,
"revision": LATEST_REVISION
},
None,
)
.insert_one(doc! {
"_id": 0_i32,
"revision": LATEST_REVISION
})
.await
.expect("Failed to save migration info.");
db.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
db.run_command(doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
})
.await
.expect("Failed to create ratelimit_events index.");

View File

@@ -5,10 +5,11 @@ use crate::{
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
options::FindOptions,
},
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, User, DISCRIMINATOR_SEARCH_SPACE,
};
use bson::oid::ObjectId;
use bson::{oid::ObjectId, to_bson};
use futures::StreamExt;
use iso8601_timestamp::Timestamp;
use rand::seq::SliceRandom;
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
use revolt_result::{Error, ErrorType};
@@ -21,12 +22,12 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 31;
pub const LATEST_REVISION: i32 = 41; // MUST BE +1 to last migration
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
let data = migrations
.find_one(None, None)
.find_one(doc! {})
.await
.expect("Failed to fetch migration data.");
@@ -46,7 +47,6 @@ pub async fn migrate_database(db: &MongoDb) {
"revision": revision
}
},
None,
)
.await
.expect("Failed to commit migration information.");
@@ -74,7 +74,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_many(
doc! { "attachment": { "$exists": 1_i32 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0_i32 } },
None,
)
.await
.expect("Failed to update messages.");
@@ -83,7 +82,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_many(
doc! {},
doc! { "$set": { "tag": "attachments", "size": 0_i32 } },
None,
)
.await
.expect("Failed to update attachments.");
@@ -93,7 +91,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
db.db()
.create_collection("servers", None)
.create_collection("servers")
.await
.expect("Failed to create servers collection.");
}
@@ -103,12 +101,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let messages = db.col::<Document>("messages");
let mut cursor = messages
.find(
doc! {
"attachment": {
"$exists": 1_i32
}
},
.find(doc! {
"attachment": {
"$exists": 1_i32
}
})
.with_options(
FindOptions::builder()
.projection(doc! {
"_id": 1_i32,
@@ -128,19 +126,18 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_one(
doc! { "_id": id },
doc! { "$unset": { "attachment": 1_i32 }, "$set": { "attachments": attachments } },
None,
)
.await
.unwrap();
}
db.db()
.create_collection("channel_unreads", None)
.create_collection("channel_unreads")
.await
.expect("Failed to create channel_unreads collection.");
db.db()
.create_collection("user_settings", None)
.create_collection("user_settings")
.await
.expect("Failed to create user_settings collection.");
}
@@ -149,17 +146,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 4 / 2021-06-01]: Add more server collections.");
db.db()
.create_collection("server_members", None)
.create_collection("server_members")
.await
.expect("Failed to create server_members collection.");
db.db()
.create_collection("server_bans", None)
.create_collection("server_bans")
.await
.expect("Failed to create server_bans collection.");
db.db()
.create_collection("channel_invites", None)
.create_collection("channel_invites")
.await
.expect("Failed to create channel_invites collection.");
}
@@ -182,7 +179,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! {
"$set": to_document(&server).unwrap()
},
None,
)
.await
.expect("Failed to migrate servers.");
@@ -192,20 +188,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 6 / 2021-07-09]: Add message text index.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
}
]
})
.await
.expect("Failed to create message index.");
}
@@ -214,7 +207,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 7 / 2021-08-11]: Add message text index.");
db.db()
.create_collection("bots", None)
.create_collection("bots")
.await
.expect("Failed to create bots collection.");
}
@@ -223,22 +216,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1.");
db.db()
.run_command(
doc! {
"dropIndexes": "accounts",
"index": ["email", "email_normalised"]
},
None,
)
.run_command(doc! {
"dropIndexes": "accounts",
"index": ["email", "email_normalised"]
})
.await
.expect("Failed to delete legacy account indexes.");
let col = db.col::<Document>("sessions");
let mut cursor = db
.col::<Document>("accounts")
.find(doc! {}, None)
.await
.unwrap();
let mut cursor = db.col::<Document>("accounts").find(doc! {}).await.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(account) = doc {
@@ -267,7 +253,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc.insert("subscription", sub);
}
col.insert_one(doc, None).await.ok();
col.insert_one(doc).await.ok();
}
} else {
info!("Account doesn't have any sessions!");
@@ -288,7 +274,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
}
},
None,
)
.await
.unwrap();
@@ -297,11 +282,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 9 {
info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id.");
let mut cursor = db
.col::<Document>("channels")
.find(doc! {}, None)
.await
.unwrap();
let mut cursor = db.col::<Document>("channels").find(doc! {}).await.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(channel) = doc {
@@ -340,7 +321,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"last_message": 1_i32,
}
},
None,
)
.await
.unwrap();
@@ -362,7 +342,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"nonce": 1_i32,
}
},
None,
)
.await
.unwrap();
@@ -375,7 +354,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"nonce": 1_i32,
}
},
None,
)
.await
.unwrap();
@@ -385,70 +363,61 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32
},
"name": "channel"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32
},
"name": "channel"
}
]
})
.await
.expect("Failed to create message index.");
db.db()
.run_command(
doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
.run_command(doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1_i32,
"_id.user": 1_i32,
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
})
.await
.expect("Failed to create channel_unreads index.");
db.db()
.run_command(
doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
.run_command(doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1_i32,
"_id.user": 1_i32,
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
},
None,
)
"name": "compound_id"
},
{
"key": {
"_id.user": 1_i32,
},
"name": "user_id"
}
]
})
.await
.expect("Failed to create server_members index.");
}
@@ -457,21 +426,18 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 12 / 2021-11-21]: Add indexes to database.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"_id": 1_i32
},
"name": "channel_id_compound"
}
]
})
.await
.expect("Failed to create message index.");
}
@@ -485,7 +451,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
async_std::task::sleep(Duration::from_secs(10)).await;
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 {
let id = document.get_str("_id").unwrap().to_string();
@@ -529,13 +495,13 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
servers
.update_one(doc! { "_id": id }, doc! { "$set": update }, None)
.update_one(doc! { "_id": id }, doc! { "$set": update })
.await
.unwrap();
}
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 {
let id = document.get_str("_id").unwrap().to_string();
@@ -575,7 +541,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
channels
.update_one(doc! { "_id": id }, update, None)
.update_one(doc! { "_id": id }, update)
.await
.unwrap();
}
@@ -596,7 +562,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"content": "system"
}
},
None,
)
.await
.unwrap();
@@ -621,25 +586,22 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.unwrap();
db.db()
.create_collection("emojis", None)
.create_collection("emojis")
.await
.expect("Failed to create emojis collection.");
db.db()
.run_command(
doc! {
"createIndexes": "emojis",
"indexes": [
{
"key": {
"parent.id": 1_i32,
},
"name": "parent_id"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "emojis",
"indexes": [
{
"key": {
"parent.id": 1_i32,
},
"name": "parent_id"
}
]
})
.await
.expect("Failed to create emoji parent index.");
}
@@ -652,10 +614,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! {},
doc! {
"$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
.expect("Failed to update server members.");
@@ -666,13 +627,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if db
.db()
.run_command(
doc! {
"dropIndexes": "messages",
"index": ["channel"]
},
None,
)
.run_command(doc! {
"dropIndexes": "messages",
"index": ["channel"]
})
.await
.is_err()
{
@@ -680,20 +638,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"author": 1_i32,
},
"name": "author"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"author": 1_i32,
},
"name": "author"
}
]
})
.await
.expect("Failed to create messages author index.");
}
@@ -703,35 +658,26 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
);
db.db()
.create_collection("safety_reports", None)
.await
.unwrap();
db.db().create_collection("safety_reports").await.unwrap();
db.db()
.create_collection("safety_snapshots", None)
.await
.unwrap();
db.db().create_collection("safety_snapshots").await.unwrap();
}
if revision <= 20 {
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
db.db()
.run_command(
doc! {
"createIndexes": "safety_snapshots",
"indexes": [
{
"key": {
"report_id": 1_i32
},
"name": "report_id"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "safety_snapshots",
"indexes": [
{
"key": {
"report_id": 1_i32
},
"name": "report_id"
}
]
})
.await
.expect("Failed to create safety snapshot index.");
}
@@ -739,10 +685,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 21 {
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
db.db()
.create_collection("safety_strikes", None)
.await
.unwrap();
db.db().create_collection("safety_strikes").await.unwrap();
}
if revision <= 22 {
@@ -756,7 +699,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4"
}
},
None,
)
.await
.expect("Failed to update server members.");
@@ -766,13 +708,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
db.db()
.run_command(
doc! {
"dropIndexes": "users",
"index": "username"
},
None,
)
.run_command(doc! {
"dropIndexes": "users",
"index": "username"
})
.await
.expect("Failed to drop existing username index.");
@@ -787,7 +726,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let users: Vec<UserInformation> = db
.col::<UserInformation>("users")
.find(doc! {}, None)
.find(doc! {})
.await
.unwrap()
.map(|doc| doc.expect("id and username"))
@@ -839,7 +778,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"discriminator": discriminator
}
},
None,
)
.await
.unwrap();
@@ -887,7 +825,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"display_name": &info.username
}
},
None,
)
.await
.unwrap();
@@ -898,43 +835,37 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 24 {
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
db.db()
.create_collection("channel_webhooks", None)
.await
.ok();
db.db().create_collection("channel_webhooks").await.ok();
db.db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
.run_command(doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1_i32
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
]
},
None,
)
},
{
"key": {
"username": 1_i32,
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
})
.await
.expect("Failed to create username index.");
};
@@ -950,7 +881,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64
}
},
None,
)
.await
.expect("Failed to update webhooks.");
@@ -959,28 +889,22 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 25 {
info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index.");
db.db()
.create_collection("ratelimit_events", None)
.await
.ok();
db.db().create_collection("ratelimit_events").await.ok();
db.db()
.run_command(
doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "ratelimit_events",
"indexes": [
{
"key": {
"_id": 1_i32,
"target_id": 1_i32,
"event_type": 1_i32,
},
"name": "compound_key"
}
]
})
.await
.expect("Failed to create ratelimit_events index.");
}
@@ -1016,12 +940,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let invites = db
.db()
.collection::<Outer>("channel_invites")
.find(
doc! {
"type": { "$exists": false }
},
None,
)
.find(doc! {
"type": { "$exists": false }
})
.await
.expect("failed to find invites")
.filter_map(|s| async { s.ok() })
@@ -1055,18 +976,15 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if !invites.is_empty() {
db.db()
.collection("channel_invites")
.insert_many(invites, None)
.insert_many(invites)
.await
.expect("failed to insert corrected invite");
db.db()
.collection::<Outer>("channel_invites")
.delete_many(
doc! {
"type": { "$exists": false }
},
None,
)
.delete_many(doc! {
"type": { "$exists": false }
})
.await
.expect("failed to find invites");
}
@@ -1076,21 +994,18 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
db.db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
}
]
})
.await
.expect("Failed to create message index.");
}
@@ -1098,44 +1013,35 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 28 {
info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn.");
db.db()
.create_collection("attachment_hashes", None)
.await
.ok();
db.db().create_collection("attachment_hashes").await.ok();
db.db()
.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"hash": 1_i32
},
"name": "hash"
}
]
})
.await
.expect("Failed to create attachments index.");
db.db()
.run_command(
doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "attachment_hashes",
"indexes": [
{
"key": {
"processed_hash": 1_i32
},
"name": "processed_hash"
}
]
})
.await
.expect("Failed to create attachment_hashes index.");
}
@@ -1146,20 +1052,17 @@ 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.");
db.db()
.run_command(
doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
},
None,
)
.run_command(doc! {
"createIndexes": "attachments",
"indexes": [
{
"key": {
"used_for.id": 1_i32
},
"name": "used_for_id"
}
]
})
.await
.expect("Failed to create attachments index.");
}
@@ -1176,7 +1079,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let webhooks = db
.db()
.collection::<WebhookShell>("channel_webhooks")
.find(doc! {}, None)
.find(doc! {})
.await
.expect("webhooks")
.filter_map(|s| async { s.ok() })
@@ -1207,7 +1110,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"creator_id": creator_id
}
},
None,
)
.await
.expect("update webhook");
@@ -1218,7 +1120,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}) => {
db.db()
.collection::<WebhookShell>("channel_webhooks")
.delete_one(doc! { "_id": webhook._id }, None)
.delete_one(doc! { "_id": webhook._id })
.await
.expect("failed to delete invalid webhook");
}
@@ -1227,6 +1129,42 @@ 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();
}
if revision <= 40 {
info!(
"Running migration [revision |> 40 / 30-05-2025]: Set last policy acknowlegement date to now and create policy changes collection."
);
db.db()
.create_collection("policy_changes")
.await
.expect("Failed to create policy_changes collection.");
db.db()
.collection::<User>("users")
.update_many(
doc! {},
doc! {
"$set": {
"last_acknowledged_policy_change": to_bson(&Timestamp::now_utc())
.expect("failed to serialise timestamp")
}
},
)
.await
.expect("failed to update users");
}
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION.max(revision)
}

View File

@@ -1,4 +1,3 @@
use revolt_config::config;
use revolt_result::Result;
use ulid::Ulid;

View File

@@ -24,12 +24,9 @@ impl AbstractChannelInvites for MongoDb {
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
Ok(self
.col::<Invite>(COL)
.find(
doc! {
"server": server_id,
},
None,
)
.find(doc! {
"server": server_id,
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {

View File

@@ -26,6 +26,14 @@ pub trait AbstractChannelUnreads: Sync + Send {
message_ids: &[String],
) -> 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.
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;

View File

@@ -30,13 +30,15 @@ impl AbstractChannelUnreads for MongoDb {
doc! {
"$pull": {
"mentions": {
"$lt": message_id
"$lte": message_id
}
},
"$set": {
"last_id": message_id
}
},
)
.with_options(
FindOneAndUpdateOptions::builder()
.upsert(true)
.return_document(ReturnDocument::After)
@@ -51,15 +53,12 @@ impl AbstractChannelUnreads for MongoDb {
let current_time = Ulid::new().to_string();
self.col::<Document>(COL)
.delete_many(
doc! {
"_id.channel": {
"$in": channel_ids
},
"_id.user": user_id
.delete_many(doc! {
"_id.channel": {
"$in": channel_ids
},
None,
)
"_id.user": user_id
})
.await
.map_err(|_| create_database_error!("delete_many", COL))?;
@@ -77,7 +76,6 @@ impl AbstractChannelUnreads for MongoDb {
}
})
.collect::<Vec<Document>>(),
None,
)
.await
.map(|_| ())
@@ -104,13 +102,42 @@ impl AbstractChannelUnreads for MongoDb {
}
}
},
UpdateOptions::builder().upsert(true).build(),
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map(|_| ())
.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.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query!(

View File

@@ -78,6 +78,38 @@ impl AbstractChannelUnreads for ReferenceDb {
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>> {
let unreads = self.channel_unreads.lock().await;
Ok(unreads

View File

@@ -142,7 +142,7 @@ mod tests {
..Default::default()
};
db.insert_webhook(&webhook).await.unwrap();
webhook.create(&db).await.unwrap();
let mut updated_webhook = webhook.clone();
updated_webhook

View File

@@ -24,12 +24,9 @@ impl AbstractWebhooks for MongoDb {
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
Ok(self
.col::<Webhook>(COL)
.find(
doc! {
"channel_id": channel_id,
},
None,
)
.find(doc! {
"channel_id": channel_id,
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {

View File

@@ -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 channel = Channel::Group {
id: ulid::Ulid::new().to_string(),
id,
name: data.name,
owner: owner_id,
description: data.description,
recipients: recipients.clone(),
icon: None,
icon,
last_message_id: None,
permissions: None,
@@ -320,13 +328,10 @@ impl Channel {
db.insert_channel(&channel).await?;
match &channel {
Channel::DirectMessage { .. } => {
let event = EventV1::ChannelCreate(channel.clone().into());
event.clone().private(user_a.id.clone()).await;
event.private(user_b.id.clone()).await;
}
_ => {}
if let Channel::DirectMessage { .. } = &channel {
let event = EventV1::ChannelCreate(channel.clone().into());
event.clone().private(user_a.id.clone()).await;
event.private(user_b.id.clone()).await;
};
Ok(channel)

View File

@@ -23,14 +23,11 @@ impl AbstractChannels for MongoDb {
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
Ok(self
.col::<Channel>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.find(doc! {
"_id": {
"$in": ids
}
})
.await
.map_err(|_| create_database_error!("fetch", "channels"))?
.filter_map(|s| async {
@@ -119,7 +116,6 @@ impl AbstractChannels for MongoDb {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
@@ -141,7 +137,6 @@ impl AbstractChannels for MongoDb {
"role_permissions.".to_owned() + role: permissions
}
},
None,
)
.await
.map(|_| ())
@@ -179,7 +174,6 @@ impl AbstractChannels for MongoDb {
"recipients": user
}
},
None,
)
.await
.map(|_| ())
@@ -253,7 +247,6 @@ impl AbstractChannels for MongoDb {
"_id": server.id
},
update,
None,
)
.await
.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<()> {
// Delete all invites to these channels.
self.col::<Document>("channel_invites")
.delete_many(
doc! {
"channel": &id
},
None,
)
.delete_many(doc! {
"channel": &id
})
.await
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
// Delete unread message objects on channels.
self.col::<Document>("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.delete_many(doc! {
"_id.channel": &id
})
.await
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
.map(|_| ())?;
@@ -299,12 +286,9 @@ impl MongoDb {
// Delete all webhooks on this channel.
self.col::<Document>("webhooks")
.delete_many(
doc! {
"channel": &id
},
None,
)
.delete_many(doc! {
"channel": &id
})
.await
.map_err(|_| create_database_error!("delete_many", "webhooks"))
.map(|_| ())

View File

@@ -60,7 +60,6 @@ impl AbstractEmojis for MongoDb {
}
}
},
None,
)
.await
.map(|_| ())

View File

@@ -28,7 +28,7 @@ auto_derived_partial!(
/// Size of this file (in bytes)
pub size: isize,
},
"PartialFile"
"PartialFileHash"
);
auto_derived!(

View File

@@ -15,4 +15,7 @@ pub trait AbstractAttachmentHashes: Sync + Send {
/// Update an attachment hash nonce value.
async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()>;
/// Delete attachment hash by id.
async fn delete_attachment_hash(&self, id: &str) -> Result<()>;
}

View File

@@ -42,10 +42,14 @@ impl AbstractAttachmentHashes for MongoDb {
"iv": nonce
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
/// Delete attachment hash by id.
async fn delete_attachment_hash(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}

View File

@@ -23,8 +23,8 @@ impl AbstractAttachmentHashes for ReferenceDb {
let hashes = self.file_hashes.lock().await;
hashes
.values()
.find(|&hash| hash.id == hash_value || hash.processed_hash == hash_value)
.cloned()
.find(|hash| hash.id == hash_value || hash.processed_hash == hash_value)
.ok_or(create_error!(NotFound))
}
@@ -38,4 +38,14 @@ impl AbstractAttachmentHashes for ReferenceDb {
Err(create_error!(NotFound))
}
}
/// Delete attachment hash by id.
async fn delete_attachment_hash(&self, id: &str) -> Result<()> {
let mut file_hashes = self.file_hashes.lock().await;
if file_hashes.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

@@ -15,6 +15,15 @@ pub trait AbstractAttachments: Sync + Send {
/// Fetch an attachment by its id.
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File>;
/// Fetch all deleted attachments.
async fn fetch_deleted_attachments(&self) -> Result<Vec<File>>;
/// Fetch all dangling attachments.
async fn fetch_dangling_files(&self) -> Result<Vec<File>>;
/// Count references to a given hash.
async fn count_file_hash_references(&self, hash: &str) -> Result<usize>;
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
@@ -32,4 +41,7 @@ pub trait AbstractAttachments: Sync + Send {
/// Mark multiple attachments as having been deleted.
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>;
/// Delete the attachment entry.
async fn delete_attachment(&self, id: &str) -> Result<()>;
}

View File

@@ -32,6 +32,51 @@ impl AbstractAttachments for MongoDb {
.ok_or_else(|| create_error!(NotFound))
}
/// Fetch all deleted attachments.
async fn fetch_deleted_attachments(&self) -> Result<Vec<File>> {
query!(
self,
find,
COL,
doc! {
"deleted": true,
"reported": {
"$ne": true
}
}
)
}
/// Fetch all dangling attachments.
async fn fetch_dangling_files(&self) -> Result<Vec<File>> {
query!(
self,
find,
COL,
doc! {
"used_for.type": {
"$exists": 0
},
"deleted": {
"$ne": true
}
}
)
}
/// Count references to a given hash.
async fn count_file_hash_references(&self, hash: &str) -> Result<usize> {
query!(
self,
count_documents,
COL,
doc! {
"hash": hash
}
)
.map(|count| count as usize)
}
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
@@ -65,7 +110,6 @@ impl AbstractAttachments for MongoDb {
"uploader_id": uploader_id
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_one", COL))?;
@@ -85,7 +129,6 @@ impl AbstractAttachments for MongoDb {
"reported": true
}
},
None,
)
.await
.map(|_| ())
@@ -104,7 +147,6 @@ impl AbstractAttachments for MongoDb {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
@@ -114,7 +156,7 @@ impl AbstractAttachments for MongoDb {
/// Mark multiple attachments as having been deleted.
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.update_one(
.update_many(
doc! {
"_id": {
"$in": ids
@@ -125,11 +167,15 @@ impl AbstractAttachments for MongoDb {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
.map_err(|_| create_database_error!("update_many", COL))
}
/// Delete the attachment entry.
async fn delete_attachment(&self, id: &str) -> Result<()> {
query!(self, delete_one_by_id, COL, id).map(|_| ())
}
}
@@ -143,7 +189,6 @@ impl MongoDb {
"deleted": true
}
},
None,
)
.await
.map(|_| ())

View File

@@ -33,6 +33,41 @@ impl AbstractAttachments for ReferenceDb {
}
}
/// Fetch all deleted attachments.
async fn fetch_deleted_attachments(&self) -> Result<Vec<File>> {
let files = self.files.lock().await;
Ok(files
.values()
.filter(|file| {
// file has been marked as deleted
file.deleted.is_some_and(|v| v)
// and it has not been reported
&& !file.reported.is_some_and(|v| v)
})
.cloned()
.collect())
}
/// Fetch all dangling attachments.
async fn fetch_dangling_files(&self) -> Result<Vec<File>> {
let files = self.files.lock().await;
Ok(files
.values()
.filter(|file| file.used_for.is_none() && !file.deleted.is_some_and(|v| v))
.cloned()
.collect())
}
/// Count references to a given hash.
async fn count_file_hash_references(&self, hash: &str) -> Result<usize> {
let files = self.files.lock().await;
Ok(files
.values()
.filter(|file| file.hash.as_ref().is_some_and(|h| h == hash))
.cloned()
.count())
}
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
@@ -96,4 +131,14 @@ impl AbstractAttachments for ReferenceDb {
Ok(())
}
/// Delete the attachment entry.
async fn delete_attachment(&self, id: &str) -> Result<()> {
let mut files = self.files.lock().await;
if files.remove(id).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
}
}
}

View File

@@ -5,17 +5,20 @@ use iso8601_timestamp::Timestamp;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{
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_result::Result;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
use revolt_result::{ErrorType, Result};
use ulid::Ulid;
use validator::Validate;
use crate::{
events::client::EventV1,
tasks::{self, ack::AckEvent},
util::{bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey},
util::{
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
permissions::DatabasePermissionQuery,
},
Channel, Database, Emoji, File, User, AMQP,
};
@@ -53,6 +56,9 @@ auto_derived_partial!(
/// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
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
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
@@ -71,7 +77,7 @@ auto_derived_partial!(
/// Bitfield of message flags
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
pub flags: Option<u32>,
},
"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)]
impl Default for Message {
fn default() -> Self {
@@ -214,6 +244,7 @@ impl Default for Message {
edited: None,
embeds: None,
mentions: None,
role_mentions: None,
replies: None,
reactions: Default::default(),
interactions: Default::default(),
@@ -255,20 +286,50 @@ impl Message {
.map_err(|_| create_error!(InvalidOperation))?;
// Check the message is not empty
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
if (data.content.as_ref().is_none_or(|v| v.is_empty()))
&& (data.attachments.as_ref().is_none_or(|v| v.is_empty()))
&& (data.embeds.as_ref().is_none_or(|v| v.is_empty()))
{
return Err(create_error!(EmptyMessage));
}
// Ensure flags are either not set or have permissible values
if let Some(flags) = &data.flags {
if flags != &0 && flags != &1 {
let allow_mass_mentions = allow_mentions && config.features.mass_mentions_enabled;
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));
}
// 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
if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions {
@@ -302,18 +363,76 @@ impl Message {
.unwrap_or_default(),
author: author_id,
webhook: webhook.map(|w| w.into()),
flags: data.flags.map(|v| v as i32),
flags: data.flags,
..Default::default()
};
// Parse mentions in message.
let mut mentions = HashSet::new();
if allow_mentions {
if let Some(content) = &data.content {
for capture in RE_MENTION.captures_iter(content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
let mut message_mentions = if let Some(raw_content) = &data.content {
revolt_parser::parse_message(raw_content)
} else {
revolt_parser::MessageResults::default()
};
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 {
let message = db.fetch_message(&id).await?;
for ReplyIntent {
id,
mention,
fail_if_not_exists,
} in entries
{
match db.fetch_message(&id).await {
// Referenced message exists
Ok(message) => {
if mention && allow_mentions {
user_mentions.insert(message.author.to_owned());
}
if mention && allow_mentions {
mentions.insert(message.author.to_owned());
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);
}
}
}
replies.insert(message.id);
}
}
// Validate the mentions go to users in the channel/server
if !mentions.is_empty() {
if !user_mentions.is_empty() {
match channel {
Channel::DirectMessage { ref recipients, .. }
| Channel::Group { ref recipients, .. } => {
let recipients_hash: HashSet<&String, RandomState> =
HashSet::from_iter(recipients);
mentions.retain(|m| recipients_hash.contains(m));
let recipients_hash = HashSet::<&String, RandomState>::from_iter(recipients);
user_mentions.retain(|m| recipients_hash.contains(m));
role_mentions.clear();
}
Channel::TextChannel { 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;
if let Ok(valid_members) = valid_members {
let valid_mentions: HashSet<&String, RandomState> =
HashSet::from_iter(valid_members.iter().map(|m| &m.id.user));
let valid_mentions = HashSet::<&String, RandomState>::from_iter(
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
let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server)
@@ -368,7 +505,7 @@ impl Message {
.members_can_see_channel()
.await;
mentions
user_mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
}
} else {
@@ -377,13 +514,21 @@ impl Message {
}
}
Channel::SavedMessages { .. } => {
mentions.clear();
user_mentions.clear();
}
}
}
if !mentions.is_empty() {
message.mentions.replace(mentions.into_iter().collect());
if !user_mentions.is_empty() {
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() {
@@ -392,6 +537,15 @@ impl Message {
.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.
let mut attachments = vec![];
if data
@@ -470,7 +624,12 @@ impl Message {
tasks::ack::queue_message(
self.channel.to_string(),
AckEvent::ProcessMessage {
messages: vec![(None, self.clone(), mentions.clone(), true)],
messages: vec![(
None,
self.clone(),
mentions.clone(),
self.has_suppressed_notifications(),
)],
},
)
.await;
@@ -497,7 +656,7 @@ impl Message {
pub async fn send(
&mut self,
db: &Database,
amqp: Option<&AMQP>, // this is optional mostly for tests.
_amqp: Option<&AMQP>, // this is optional mostly for tests.
author: MessageAuthor<'_>,
user: Option<v0::User>,
member: Option<v0::Member>,
@@ -514,7 +673,9 @@ impl Message {
)
.await?;
if !self.has_suppressed_notifications() {
if !self.has_suppressed_notifications()
&& (self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications
tasks::ack::queue_message(
self.channel.to_string(),
@@ -537,7 +698,7 @@ impl Message {
}
_ => vec![],
},
self.has_suppressed_notifications(),
false, // branch already dictates this
)],
},
)
@@ -574,13 +735,24 @@ impl Message {
/// Whether this message has suppressed notifications
pub fn has_suppressed_notifications(&self) -> bool {
if let Some(flags) = self.flags {
flags & MessageFlags::SuppressNotifications as i32
== MessageFlags::SuppressNotifications as i32
flags & MessageFlags::SuppressNotifications as u32
== MessageFlags::SuppressNotifications as u32
} else {
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
pub async fn update(
&mut self,

View File

@@ -95,7 +95,7 @@ impl AbstractMessages for MongoDb {
COL,
older_message_filter,
FindOptions::builder()
.limit(limit / 2)
.limit(limit / 2 + 1)
.sort(doc! {
"_id": -1_i32
})
@@ -228,7 +228,6 @@ impl AbstractMessages for MongoDb {
"_id": id
},
query,
None,
)
.await
.map(|_| ())
@@ -247,7 +246,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
@@ -266,7 +264,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
@@ -285,7 +282,6 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): 1
}
},
None,
)
.await
.map(|_| ())
@@ -300,15 +296,12 @@ impl AbstractMessages for MongoDb {
/// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.col::<Document>(COL)
.delete_many(
doc! {
"channel": channel,
"_id": {
"$in": ids
}
},
None,
)
.delete_many(doc! {
"channel": channel,
"_id": {
"$in": ids
}
})
.await
.map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL))
@@ -362,7 +355,6 @@ impl MongoDb {
"deleted": true
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "attachments"))?;
@@ -370,7 +362,7 @@ impl MongoDb {
// And then delete said messages.
self.col::<Document>(COL)
.delete_many(projection, None)
.delete_many(projection)
.await
.map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL))

View File

@@ -8,6 +8,7 @@ mod emojis;
mod file_hashes;
mod files;
mod messages;
mod policy_changes;
mod ratelimit_events;
mod safety_reports;
mod safety_snapshots;
@@ -27,6 +28,7 @@ pub use emojis::*;
pub use file_hashes::*;
pub use files::*;
pub use messages::*;
pub use policy_changes::*;
pub use ratelimit_events::*;
pub use safety_reports::*;
pub use safety_snapshots::*;
@@ -51,6 +53,7 @@ pub trait AbstractDatabase:
+ file_hashes::AbstractAttachmentHashes
+ files::AbstractAttachments
+ messages::AbstractMessages
+ policy_changes::AbstractPolicyChange
+ ratelimit_events::AbstractRatelimitEvents
+ safety_reports::AbstractReport
+ safety_snapshots::AbstractSnapshot

View File

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

View File

@@ -0,0 +1,20 @@
use iso8601_timestamp::Timestamp;
auto_derived!(
/// Platform policy change
pub struct PolicyChange {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// Time at which this policy was created
pub created_time: Timestamp,
/// Time at which this policy is effective
pub effective_time: Timestamp,
/// Message shown to users
pub description: String,
/// URL with details about changes
pub url: String,
}
);

View File

@@ -0,0 +1,15 @@
use revolt_result::Result;
use crate::PolicyChange;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractPolicyChange: Sync + Send {
/// Fetch all policy changes
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>>;
/// Acknowledge policy changes
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()>;
}

View File

@@ -0,0 +1,46 @@
use bson::to_bson;
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::MongoDb;
use crate::PolicyChange;
use crate::User;
use super::AbstractPolicyChange;
static COL: &str = "policy_changes";
#[async_trait]
impl AbstractPolicyChange for MongoDb {
/// Fetch all policy changes
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>> {
query!(self, find, COL, doc! {})
}
/// Acknowledge policy changes
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()> {
let latest_policy = self
.fetch_policy_changes()
.await?
.into_iter()
.map(|policy| policy.created_time)
.max()
.unwrap_or(Timestamp::UNIX_EPOCH);
self.col::<User>("users")
.update_one(
doc! {
"_id": user_id
},
doc! {
"$set": {
"last_acknowledged_policy_change": to_bson(&latest_policy)
.map_err(|_| create_database_error!("to_bson", "timestamp"))?
}
},
)
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", COL))
}
}

View File

@@ -0,0 +1,31 @@
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
use crate::PolicyChange;
use crate::ReferenceDb;
use super::AbstractPolicyChange;
#[async_trait]
impl AbstractPolicyChange for ReferenceDb {
/// Fetch all policy changes
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>> {
let policy_changes = self.policy_changes.lock().await;
Ok(policy_changes.values().cloned().collect())
}
/// Acknowledge policy changes
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()> {
let mut users = self.users.lock().await;
let user = users.get_mut(user_id).expect("user doesn't exist");
user.last_acknowledged_policy_change = self
.fetch_policy_changes()
.await?
.into_iter()
.map(|policy| policy.created_time)
.max()
.unwrap_or(Timestamp::UNIX_EPOCH);
Ok(())
}
}

View File

@@ -23,16 +23,13 @@ impl AbstractRatelimitEvents for MongoDb {
count: usize,
) -> Result<bool> {
self.col::<RatelimitEvent>(COL)
.count_documents(
doc! {
"_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
"target_id": target_id,
"event_type": event_type.to_string()
.count_documents(doc! {
"_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
None,
)
"target_id": target_id,
"event_type": event_type.to_string()
})
.await
.map(|c| c as usize >= count)
.map_err(|_| create_database_error!("count_documents", COL))

View File

@@ -85,8 +85,8 @@ impl SnapshotContent {
Ok((
SnapshotContent::Message {
message,
prior_context: prior_context.into_iter().map(Into::into).collect(),
leading_context: leading_context.into_iter().map(Into::into).collect(),
prior_context: prior_context.into_iter().collect(),
leading_context: leading_context.into_iter().collect(),
},
files,
))

View File

@@ -13,6 +13,7 @@ auto_derived!(
}
);
#[allow(clippy::disallowed_methods)]
impl ServerBan {
/// Create ban
pub async fn create(

View File

@@ -159,7 +159,7 @@ impl Member {
}
/// Update member data
pub async fn update<'a>(
pub async fn update(
&mut self,
db: &Database,
partial: PartialMember,

View File

@@ -1,3 +1,4 @@
use ::mongodb::SessionCursor;
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
@@ -5,6 +6,66 @@ use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
mod mongodb;
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]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
@@ -16,6 +77,24 @@ pub trait AbstractServerMembers: Sync + Send {
/// Fetch all members in a server
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
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;

View File

@@ -1,10 +1,11 @@
use futures::StreamExt;
use mongodb::options::ReadConcern;
use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{IntoDocumentPath, MongoDb};
use super::AbstractServerMembers;
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
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>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.server": server_id
},
None,
)
.find(doc! {
"_id.server": server_id
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -52,16 +50,101 @@ impl AbstractServerMembers for MongoDb {
.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
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.user": user_id
},
None,
)
.find(doc! {
"_id.user": user_id
})
.await
.map_err(|_| create_database_error!("find", COL))?
.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>> {
Ok(self
.col::<Member>(COL)
.find(
doc! {
"_id.server": server_id,
"_id.user": {
"$in": ids
}
},
None,
)
.find(doc! {
"_id.server": server_id,
"_id.user": {
"$in": ids
}
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -104,12 +184,9 @@ impl AbstractServerMembers for MongoDb {
/// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(
doc! {
"_id.server": server_id
},
None,
)
.count_documents(doc! {
"_id.server": server_id
})
.await
.map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL))
@@ -118,12 +195,9 @@ impl AbstractServerMembers for MongoDb {
/// Fetch server count of a user
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL)
.count_documents(
doc! {
"_id.user": user_id
},
None,
)
.count_documents(doc! {
"_id.user": user_id
})
.await
.map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL))

View File

@@ -3,7 +3,7 @@ use revolt_result::Result;
use crate::ReferenceDb;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use super::AbstractServerMembers;
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
#[async_trait]
impl AbstractServerMembers for ReferenceDb {
@@ -40,6 +40,70 @@ impl AbstractServerMembers for ReferenceDb {
.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
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;

View File

@@ -285,7 +285,7 @@ impl Role {
}
/// Update server data
pub async fn update<'a>(
pub async fn update(
&mut self,
db: &Database,
server_id: &str,

View File

@@ -25,14 +25,11 @@ impl AbstractServers for MongoDb {
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
Ok(self
.col::<Server>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.find(doc! {
"_id": {
"$in": ids
}
})
.await
.map_err(|_| create_database_error!("find", "servers"))?
.filter_map(|s| async {
@@ -84,7 +81,6 @@ impl AbstractServers for MongoDb {
.map_err(|_| create_database_error!("to_document", "role"))?
}
},
None,
)
.await
.map(|_| ())
@@ -125,7 +121,6 @@ impl AbstractServers for MongoDb {
"roles": &role_id
}
},
None,
)
.await
.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
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_one", "channels"))?;
@@ -155,7 +149,6 @@ impl AbstractServers for MongoDb {
"roles.".to_owned() + role_id: 1_i32
}
},
None,
)
.await
.map(|_| ())
@@ -188,12 +181,9 @@ impl MongoDb {
// Find all channels
let channels: Vec<String> = self
.col::<Document>("channels")
.find(
doc! {
"server": server_id
},
None,
)
.find(doc! {
"server": server_id
})
.await
.map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async {
@@ -225,19 +215,15 @@ impl MongoDb {
}
}
},
None,
)
.await
.map_err(|_| create_database_error!("update_many", "emojis"))?;
// Delete all channels.
self.col::<Document>("channels")
.delete_many(
doc! {
"server": &server_id
},
None,
)
.delete_many(doc! {
"server": &server_id
})
.await
.map_err(|_| create_database_error!("delete_many", "channels"))?;
@@ -248,12 +234,9 @@ impl MongoDb {
// Delete members and bans.
for with in &["server_members", "server_bans"] {
self.col::<Document>(with)
.delete_many(
doc! {
"_id.server": &server_id
},
None,
)
.delete_many(doc! {
"_id.server": &server_id
})
.await
.map_err(|_| create_database_error!("delete_many", with))?;
}

View File

@@ -53,8 +53,8 @@ impl AbstractUserSettings for MongoDb {
doc! {
"$set": set
},
UpdateOptions::builder().upsert(true).build(),
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_one", "user_settings"))

View File

@@ -3,11 +3,12 @@ use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP};
use authifier::config::{EmailVerificationConfig, Template};
use futures::future::join_all;
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
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_result::{create_error, Result};
use serde_json::json;
@@ -56,6 +57,8 @@ auto_derived_partial!(
/// Time until user is unsuspended
#[serde(skip_serializing_if = "Option::is_none")]
pub suspended_until: Option<Timestamp>,
/// Last acknowledged policy change
pub last_acknowledged_policy_change: Timestamp,
},
"PartialUser"
);
@@ -152,7 +155,7 @@ pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
.collect::<HashSet<String>>();
for discrim in [
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999,
123, 1234, 1111, 2222, 3333, 4444, 5555, 6666, 7777, 8888, 9999, 1488,
] {
set.remove(&format!("{:0>4}", discrim));
}
@@ -177,6 +180,7 @@ impl Default for User {
privileged: Default::default(),
bot: Default::default(),
suspended_until: Default::default(),
last_acknowledged_policy_change: Timestamp::UNIX_EPOCH,
}
}
}
@@ -199,6 +203,7 @@ impl User {
id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()),
discriminator: User::find_discriminator(db, &username, None).await?,
username,
last_acknowledged_policy_change: Timestamp::now_utc(),
..Default::default()
};
@@ -293,7 +298,14 @@ impl User {
}
// Ensure none of the following substrings show up in the username
const BLOCKED_SUBSTRINGS: &[&str] = &["```"];
const BLOCKED_SUBSTRINGS: &[&str] = &[
"```",
"discord.gg",
"rvlt.gg",
"guilded.gg",
"https://",
"http://",
];
for substr in BLOCKED_SUBSTRINGS {
if username_lowercase.contains(substr) {
@@ -341,15 +353,13 @@ impl User {
) -> Result<Vec<v0::User>> {
let online_ids = filter_online(ids).await;
Ok(db
.fetch_users(ids)
.await?
.into_iter()
.map(|user| {
Ok(
join_all(db.fetch_users(ids).await?.into_iter().map(|user| async {
let is_online = online_ids.contains(&user.id);
user.into_known(perspective, is_online)
})
.collect())
user.into_known(perspective, is_online).await
}))
.await,
)
}
/// Find a free discriminator for a given username
@@ -630,7 +640,7 @@ impl User {
}
/// Update user data
pub async fn update<'a>(
pub async fn update(
&mut self,
db: &Database,
partial: PartialUser,
@@ -800,4 +810,18 @@ impl User {
)
.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
}
}

View File

@@ -49,12 +49,9 @@ impl AbstractUsers for MongoDb {
/// Fetch a session from the database by token
async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
self.col::<Session>("sessions")
.find_one(
doc! {
"token": token
},
None,
)
.find_one(doc! {
"token": token
})
.await
.map_err(|_| create_database_error!("find_one", "sessions"))?
.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>> {
Ok(self
.col::<User>(COL)
.find(
doc! {
"_id": {
"$in": ids
}
},
None,
)
.find(doc! {
"_id": {
"$in": ids
}
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
@@ -94,10 +88,10 @@ impl AbstractUsers for MongoDb {
Ok(self
.col::<UserDocument>(COL)
.find(
doc! {
"username": username
},
.find(doc! {
"username": username
})
.with_options(
FindOptions::builder()
.collation(
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>> {
Ok(self
.col::<DocumentId>(COL)
.find(
doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.find(doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
]
})
.with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
.await
.map_err(|_| create_database_error!("find", COL))?
.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>> {
Ok(self
.col::<DocumentId>("channels")
.find(
doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ user_a, user_b ]
}
.find(doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
"recipients": {
"$all": [ user_a, user_b ]
}
})
.with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
.await
.map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async { s.ok() })
@@ -166,49 +156,46 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self
.col::<DocumentId>("server_members")
.aggregate(
vec![
doc! {
"$match": {
"_id.user": user_a
}
},
doc! {
"$lookup": {
"from": "server_members",
"as": "members",
"let": {
"server": "$_id.server"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{ "$eq": [ "$_id.user", user_b ] },
{ "$eq": [ "$_id.server", "$$server" ] }
]
}
.aggregate(vec![
doc! {
"$match": {
"_id.user": user_a
}
},
doc! {
"$lookup": {
"from": "server_members",
"as": "members",
"let": {
"server": "$_id.server"
},
"pipeline": [
{
"$match": {
"$expr": {
"$and": [
{ "$eq": [ "$_id.user", user_b ] },
{ "$eq": [ "$_id.server", "$$server" ] }
]
}
}
]
}
},
doc! {
"$match": {
"members": {
"$size": 1_i32
}
]
}
},
doc! {
"$match": {
"members": {
"$size": 1_i32
}
},
doc! {
"$project": {
"_id": "$_id.server"
}
},
],
None,
)
}
},
doc! {
"$project": {
"_id": "$_id.server"
}
},
])
.await
.map_err(|_| create_database_error!("aggregate", "server_members"))?
.filter_map(|s| async { s.ok() })
@@ -284,7 +271,6 @@ impl AbstractUsers for MongoDb {
}
}
}],
None,
)
.await
.map(|_| ())
@@ -305,7 +291,6 @@ impl AbstractUsers for MongoDb {
}
}
},
None,
)
.await
.map(|_| ())
@@ -329,7 +314,6 @@ impl AbstractUsers for MongoDb {
"subscription": 1
}
},
None,
)
.await
.map(|_| ())

View File

@@ -6,7 +6,7 @@ use revolt_rocket_okapi::{
use crate::User;
impl<'r> OpenApiFromRequest<'r> for User {
impl OpenApiFromRequest<'_> for User {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,

View File

@@ -3,8 +3,8 @@ use crate::{Database, Message, AMQP};
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use revolt_config::capture_message;
use revolt_models::v0::PushNotification;
use rocket::form::validate::Contains;
use std::{
collections::{HashMap, HashSet},
time::Duration,
@@ -14,6 +14,7 @@ use validator::HasLen;
use revolt_result::Result;
use super::DelayedTask;
use crate::Channel::{TextChannel, VoiceChannel};
/// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)]
@@ -64,6 +65,7 @@ pub async fn queue_ack(channel: String, user: String, event: AckEvent) {
);
}
/// Do not add more than one message per event.
pub async fn queue_message(channel: String, event: AckEvent) {
Q.try_push(Data {
channel,
@@ -113,40 +115,50 @@ pub async fn handle_ack_event(
}
AckEvent::ProcessMessage { messages } => {
let mut users: HashSet<&String> = HashSet::new();
debug!(
info!(
"Processing {} messages from channel {}",
messages.len(),
messages[0].1.channel
);
// find all the users we'll be notifying
messages
.iter()
.for_each(|(_, _, recipents, _)| users.extend(recipents.iter()));
messages.iter().for_each(|(_, _, recipents, _)| {
users.extend(recipents.iter());
});
debug!("Found {} users to notify.", users.len());
info!("Found {} users to notify.", users.len());
for user in users {
let message_ids: Vec<String> = messages
.iter()
.filter(|(_, _, recipients, _)| recipients.contains(user))
.map(|(_, message, _, _)| message.id.clone())
.filter_map(|(_, message, recipients, _)| {
if recipients.contains(user) {
Some(message.id.clone())
} else {
None
}
})
.collect();
if !message_ids.is_empty() {
db.add_mention_to_unread(channel, user, &message_ids)
.await?;
}
debug!("Added {} mentions for user {}", message_ids.len(), &user);
info!("Added {} mentions for user {}", message_ids.len(), &user);
}
for (push, _, recipients, silenced) in messages {
if *silenced || recipients.is_empty() || push.is_none() {
let mut mass_mentions = vec![];
for (push, message, recipients, silenced) in messages {
if *silenced
|| push.is_none()
|| (recipients.is_empty() && !message.contains_mass_push_mention())
{
debug!(
"Rejecting push: silenced: {}, recipient count: {}, push exists: {:?}",
*silenced,
recipients.length(),
push
push.is_some()
);
continue;
}
@@ -162,6 +174,35 @@ pub async fn handle_ack_event(
{
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");
}
}
}
}
};
@@ -207,6 +248,8 @@ pub async fn worker(db: Database, amqp: AMQP) {
mut event,
}) = Q.try_pop()
{
info!("Took next ack from queue, now {} remaining", Q.len());
let key: (Option<String>, String, u8) = (
user,
channel,
@@ -221,18 +264,31 @@ pub async fn worker(db: Database, amqp: AMQP) {
if let AckEvent::ProcessMessage { messages: existing } =
&mut task.data.event
{
// add the new message to the list of messages to be processed.
existing.append(new_data);
if let Some(new_event) = new_data.pop() {
// if the message contains a mass mention, do not delay it any further.
if new_event.1.contains_mass_push_mention() {
// add the new message to the list of messages to be processed.
existing.push(new_event);
task.run_immediately();
continue;
}
// put a cap on the amount of messages that can be queued, for particularly active channels
if (existing.length() as u16)
< revolt_config::config()
.await
.features
.advanced
.process_message_delay_limit
{
task.delay();
existing.push(new_event);
// put a cap on the amount of messages that can be queued, for particularly active channels
if (existing.length() as u16)
< revolt_config::config()
.await
.features
.advanced
.process_message_delay_limit
{
task.delay();
}
} else {
let err_msg = format!("Got zero-length message event: {event:?}");
capture_message(&err_msg, revolt_config::Level::Warning);
info!("{err_msg}")
}
} else {
panic!("Somehow got an ack message in the add mention arm");

View File

@@ -26,6 +26,7 @@ pub fn start_workers(db: Database, amqp: AMQP) {
/// Task with additional information on when it should run
pub struct DelayedTask<T> {
pub data: T,
run_now: bool,
last_updated: Instant,
first_seen: Instant,
}
@@ -41,6 +42,7 @@ impl<T> DelayedTask<T> {
pub fn new(data: T) -> Self {
DelayedTask {
data,
run_now: false,
last_updated: Instant::now(),
first_seen: Instant::now(),
}
@@ -51,9 +53,15 @@ impl<T> DelayedTask<T> {
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
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
}
}

View File

@@ -1,7 +1,8 @@
use iso8601_timestamp::Timestamp;
use revolt_models::v0::*;
use revolt_permissions::{calculate_user_permissions, UserPermission};
use crate::{util::permissions::DatabasePermissionQuery, Database, FileUsedFor};
use crate::{util::permissions::DatabasePermissionQuery, Database};
impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot {
@@ -483,11 +484,12 @@ impl crate::Message {
edited: self.edited,
embeds: self.embeds,
mentions: self.mentions,
role_mentions: self.role_mentions,
replies: self.replies,
reactions: self.reactions,
interactions: self.interactions.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,
}
}
@@ -511,11 +513,12 @@ impl From<crate::PartialMessage> for PartialMessage {
edited: value.edited,
embeds: value.embeds,
mentions: value.mentions,
role_mentions: value.role_mentions,
replies: value.replies,
reactions: value.reactions,
interactions: value.interactions.map(Into::into),
masquerade: value.masquerade.map(Into::into),
flags: value.flags.map(|flags| flags as u32),
flags: value.flags,
pinned: value.pinned,
}
}
@@ -595,6 +598,17 @@ impl From<Masquerade> for crate::Masquerade {
}
}
impl From<crate::PolicyChange> for PolicyChange {
fn from(value: crate::PolicyChange) -> Self {
PolicyChange {
created_time: value.created_time,
effective_time: value.effective_time,
description: value.description,
url: value.url,
}
}
}
impl From<crate::Report> for Report {
fn from(value: crate::Report) -> Self {
Report {
@@ -992,6 +1006,8 @@ impl crate::User {
(RelationshipStatus::None, false)
};
let badges = self.get_badges().await;
User {
username: self.username,
discriminator: self.discriminator,
@@ -1010,7 +1026,7 @@ impl crate::User {
} else {
vec![]
},
badges: self.badges.unwrap_or_default() as u32,
badges,
online: can_see_profile
&& revolt_presence::is_online(&self.id).await
&& !matches!(
@@ -1021,7 +1037,7 @@ impl crate::User {
})
),
status: if can_see_profile {
self.status.map(|status| status.into())
self.status.and_then(|status| status.into(true))
} else {
None
},
@@ -1036,7 +1052,7 @@ impl crate::User {
/// Convert user object into user model assuming mutual connection
///
/// 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
P: Into<Option<&'a crate::User>>,
{
@@ -1066,13 +1082,15 @@ impl crate::User {
(RelationshipStatus::None, false)
};
let badges = self.get_badges().await;
User {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
badges,
online: can_see_profile
&& is_online
&& !matches!(
@@ -1083,7 +1101,7 @@ impl crate::User {
})
),
status: if can_see_profile {
self.status.map(|status| status.into())
self.status.and_then(|status| status.into(true))
} else {
None
},
@@ -1096,14 +1114,16 @@ impl crate::User {
}
/// 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 {
username: self.username,
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
badges,
online: is_online
&& !matches!(
self.status,
@@ -1112,7 +1132,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,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
@@ -1122,6 +1142,8 @@ impl crate::User {
}
pub async fn into_self(self, force_online: bool) -> User {
let badges = self.get_badges().await;
User {
username: self.username,
discriminator: self.discriminator,
@@ -1136,7 +1158,7 @@ impl crate::User {
.collect()
})
.unwrap_or_default(),
badges: self.badges.unwrap_or_default() as u32,
badges,
online: (force_online || revolt_presence::is_online(&self.id).await)
&& !matches!(
self.status,
@@ -1145,7 +1167,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,
privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()),
@@ -1178,6 +1200,7 @@ impl From<User> for crate::User {
privileged: value.privileged,
bot: value.bot.map(Into::into),
suspended_until: None,
last_acknowledged_policy_change: Timestamp::UNIX_EPOCH,
}
}
}
@@ -1196,7 +1219,7 @@ impl From<crate::PartialUser> for PartialUser {
.collect()
}),
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),
privileged: value.privileged,
bot: value.bot.map(|bot| bot.into()),
@@ -1285,11 +1308,23 @@ impl From<Presence> for crate::Presence {
}
}
impl From<crate::UserStatus> for UserStatus {
fn from(value: crate::UserStatus) -> Self {
UserStatus {
text: value.text,
presence: value.presence.map(|presence| presence.into()),
impl crate::UserStatus {
fn into(self, discard_invisible: bool) -> Option<UserStatus> {
let status = UserStatus {
text: self.text,
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)
}
}
}

View File

@@ -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 {
channel: Some(channel.clone()),
..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 {
members: Some(members.to_owned()),
channel: Some(channel),
..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 {
users: Some(users.to_owned()),
cached_member_perms: None,
members: None,
cached_members: None,
cached_users: None,
..self
}
}

View File

@@ -305,16 +305,14 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
/// Are we a recipient of this channel?
async fn are_we_part_of_the_channel(&mut self) -> bool {
if let Some(channel) = &self.channel {
match channel {
Cow::Borrowed(Channel::DirectMessage { recipients, .. })
| Cow::Owned(Channel::DirectMessage { recipients, .. })
| Cow::Borrowed(Channel::Group { recipients, .. })
| Cow::Owned(Channel::Group { recipients, .. }) => {
recipients.contains(&self.perspective.id)
}
_ => false,
}
if let Some(
Cow::Borrowed(Channel::DirectMessage { recipients, .. })
| Cow::Owned(Channel::DirectMessage { recipients, .. })
| Cow::Borrowed(Channel::Group { recipients, .. })
| Cow::Owned(Channel::Group { recipients, .. }),
) = &self.channel
{
recipients.contains(&self.perspective.id)
} else {
false
}
@@ -420,7 +418,7 @@ impl<'a> DatabasePermissionQuery<'a> {
}
/// Use user
pub fn user(self, user: &'a User) -> DatabasePermissionQuery {
pub fn user(self, user: &'a User) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery {
user: Some(Cow::Borrowed(user)),
..self
@@ -428,7 +426,7 @@ impl<'a> DatabasePermissionQuery<'a> {
}
/// Use channel
pub fn channel(self, channel: &'a Channel) -> DatabasePermissionQuery {
pub fn channel(self, channel: &'a Channel) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery {
channel: Some(Cow::Borrowed(channel)),
..self
@@ -436,7 +434,7 @@ impl<'a> DatabasePermissionQuery<'a> {
}
/// Use server
pub fn server(self, server: &'a Server) -> DatabasePermissionQuery {
pub fn server(self, server: &'a Server) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery {
server: Some(Cow::Borrowed(server)),
..self
@@ -444,7 +442,7 @@ impl<'a> DatabasePermissionQuery<'a> {
}
/// Use member
pub fn member(self, member: &'a Member) -> DatabasePermissionQuery {
pub fn member(self, member: &'a Member) -> DatabasePermissionQuery<'a> {
DatabasePermissionQuery {
member: Some(Cow::Borrowed(member)),
..self

View File

@@ -1,464 +1,93 @@
<!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.3.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" />
<!-- 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;
}
}
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"
>
<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%"
>
<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;
"
>
<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"
>
<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%"
>
<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"
>
<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"
>
<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
alt="Revolt Logo"
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 style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" 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%"
>
<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"
>
<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"
>
<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"
>
Account Deletion
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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">Account Deletion</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"
>
<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 requested to have your account
deleted, if you did not perform this
action please take measures to secure your
account immediately.
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">
You requested to have your account deleted, if you did not perform
this action please take measures to secure your account immediately.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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"
>
<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;
"
>
<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;
"
>Confirm</a
>
<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;">Confirm</a>
</td>
</tr>
</tbody>
@@ -467,66 +96,58 @@
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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"
>
<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 />
<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%"
>
<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"
>
<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>

View File

@@ -25,6 +25,14 @@
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>

View File

@@ -5,3 +5,10 @@ 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

View File

@@ -0,0 +1,9 @@
You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html

View File

@@ -0,0 +1,53 @@
<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
alt="Revolt Logo"
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">Notice of Terms &amp; Policy Change</h1>
<p>
This email is to notify you that we're updating all of our policy
documents.
</p>
<p>
You can read our updated Terms of Service and Privacy Policy below:<br />
Effective. 24th March 2025
</p>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Terms of Service</a
>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Privacy Policy</a
>
<p>
Our Community Guidelines have also been updated to help us better
protect and safeguard the platform. You can check them out here:
</p>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Community Guidelines</a
>
</div>
<div class="text-muted text-center my-6">
Sent by 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>

View File

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

View File

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

View File

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

View File

@@ -1,462 +1,90 @@
<!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.3.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" />
<!-- 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;
}
}
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"
>
<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%"
>
<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;
"
>
<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"
>
<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%"
>
<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"
>
<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"
>
<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
alt="Revolt Logo"
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 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%"
>
<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"
>
<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"
>
<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%"
>
<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"
>
<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 requested a password reset, click
below to continue.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">You requested a password reset, 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"
>
<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;
"
>
<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 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>
@@ -465,66 +93,58 @@
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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"
>
<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 />
<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%"
>
<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"
>
<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>

View File

@@ -21,6 +21,14 @@
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>

View File

@@ -5,3 +5,10 @@ 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

View File

@@ -0,0 +1,9 @@
You requested a password reset, if you did not perform this action you can safely ignore this email.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html

View File

@@ -1,408 +1,80 @@
<!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" />
<!-- 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-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;
}
}
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-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"
>
<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%"
>
<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;
"
>
<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"
>
<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%"
>
<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"
>
<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"
>
<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
alt="Revolt Logo"
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 style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" 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%"
>
<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"
>
<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"
>
<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"
>
Account Suspended
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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">Account Suspended</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"
>
<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"
>
Your account has been suspended, for one
or more reasons:
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">Your account has been suspended, for one or more reasons:</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"
>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
@@ -411,193 +83,98 @@
<ul class="">
{{list}}
</ul>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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"
>
<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
style="display: {{duration_display}}; line-height: 24px; font-size: 16px; width: 100%; margin: 0;"
class=""
align="left"
>
You will be able to use your account again
in {{duration}} days.
<p style="display: {{duration_display}}; line-height: 24px; font-size: 16px; width: 100%; margin: 0;" class="" align="left">
You will be able to use your account again in {{duration}} days.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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"
>
<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"
>
Further violations may result in a
permanent ban depending on severity,
please abide by the
<a
href="https://revolt.chat/aup"
style="color: #0d6efd"
>Acceptable Usage Policy</a
>.
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">
Further violations may result in a permanent ban depending on
severity, please abide by the
<a href="https://revolt.chat/aup" style="color: #0d6efd;">Acceptable Usage Policy</a>.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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"
>
<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
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Ban evasion is prohibited and will be
dealt with accordingly.
</p>
<p style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">Ban evasion is prohibited and will be dealt with accordingly.</p>
</td>
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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"
>
<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 />
<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%"
>
<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"
>
<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>

View File

@@ -33,6 +33,14 @@
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>

View File

@@ -10,3 +10,10 @@ Ban evasion is prohibited and will be dealt with accordingly.
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

View File

@@ -0,0 +1,8 @@
Your account has been suspended, for one or more reasons:
{{list}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html

View File

@@ -1,462 +1,90 @@
<!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.3.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" />
<!-- 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;
}
}
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"
>
<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%"
>
<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;
"
>
<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"
>
<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%"
>
<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"
>
<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"
>
<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
alt="Revolt Logo"
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 style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" 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%"
>
<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"
>
<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"
>
<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"
>
Almost there!
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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">Almost there!</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"
>
<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"
>
To complete your sign up, we just need to
verify your email.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">To complete your sign up, we just need to verify your 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"
>
<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;
"
>
<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;
"
>Confirm</a
>
<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;">Confirm</a>
</td>
</tr>
</tbody>
@@ -465,66 +93,58 @@
</tr>
</tbody>
</table>
<table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<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"
>
<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 />
<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%"
>
<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"
>
<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>

View File

@@ -22,6 +22,14 @@
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>

View File

@@ -6,3 +6,10 @@ 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

View File

@@ -0,0 +1,10 @@
Almost there!
To complete your sign up, we just need to verify your email.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html

View File

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

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