mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-14 13:36:59 +00:00
Compare commits
116 Commits
20240408-4
...
20240909-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eb596fe99 | ||
|
|
0bbb9e7072 | ||
|
|
2433fa661a | ||
|
|
70bdeecf77 | ||
|
|
24dc96f80f | ||
|
|
ebbbb5e174 | ||
|
|
78757ac7f1 | ||
|
|
01368960f3 | ||
|
|
ae1d5d07e3 | ||
|
|
5ad72abca7 | ||
|
|
c1b92ef56e | ||
|
|
acbc1b8956 | ||
|
|
bce24c8b1b | ||
|
|
31262315e7 | ||
|
|
d8acaa1107 | ||
|
|
730039eda6 | ||
|
|
79e1388000 | ||
|
|
5c8ece0727 | ||
|
|
ab6b92aa2a | ||
|
|
239710951a | ||
|
|
28960979ec | ||
|
|
1938ebc3fa | ||
|
|
1fb4032d9e | ||
|
|
ae2194cce8 | ||
|
|
949a0cf649 | ||
|
|
f6c57b23b4 | ||
|
|
12ae781621 | ||
|
|
a52d610e6c | ||
|
|
f513a9cb5e | ||
|
|
535f01604b | ||
|
|
f8ec6ba5ff | ||
|
|
b8fdc07508 | ||
|
|
5ce7530206 | ||
|
|
b36bac3225 | ||
|
|
beef06ebe3 | ||
|
|
4fc46f765b | ||
|
|
b45ae2cd4d | ||
|
|
0a8089787b | ||
|
|
475fa6f2e6 | ||
|
|
96bf73cb57 | ||
|
|
87a9fb79f2 | ||
|
|
c50435d499 | ||
|
|
d2f4823ceb | ||
|
|
39230c559a | ||
|
|
32d1d5df2e | ||
|
|
7547fbe245 | ||
|
|
9f13cb6f47 | ||
|
|
d94571fbf9 | ||
|
|
389ecc0e5c | ||
|
|
412f4a99d7 | ||
|
|
caa0795d07 | ||
|
|
5c82343bb3 | ||
|
|
e17af1c064 | ||
|
|
a9e309395e | ||
|
|
8468ce19cb | ||
|
|
27f15f7b02 | ||
|
|
78b9fa168b | ||
|
|
e290d168ac | ||
|
|
78cd89ec32 | ||
|
|
f9d9059e73 | ||
|
|
0954d71f51 | ||
|
|
9ea2bd9f2f | ||
|
|
d6bcb844db | ||
|
|
ce20e689cc | ||
|
|
d9cfc9d0e1 | ||
|
|
64a07d09f4 | ||
|
|
00e881799f | ||
|
|
fa598dd6f8 | ||
|
|
ac4e2cb10b | ||
|
|
ec578ab0ef | ||
|
|
ea6ba59841 | ||
|
|
f903f716e3 | ||
|
|
c5f4b94aa5 | ||
|
|
1d5dae4751 | ||
|
|
ac20b6bc99 | ||
|
|
6ec8007e4e | ||
|
|
e5eea267cf | ||
|
|
de5add09d0 | ||
|
|
1ec8f46c1d | ||
|
|
80666848cc | ||
|
|
e8e9613169 | ||
|
|
eda36436a8 | ||
|
|
93e05e9f18 | ||
|
|
090d8cba71 | ||
|
|
f16e72e329 | ||
|
|
8248a4a5b8 | ||
|
|
cf0b4274c8 | ||
|
|
36ecb48c7b | ||
|
|
ba411fef43 | ||
|
|
4055363cff | ||
|
|
d179d1e695 | ||
|
|
fc9ceb7540 | ||
|
|
a31a3e2e65 | ||
|
|
b12e728514 | ||
|
|
962c7d62c7 | ||
|
|
d677716f93 | ||
|
|
8099310f89 | ||
|
|
5c40f66010 | ||
|
|
8e7dd21bce | ||
|
|
4868205df4 | ||
|
|
96fb0eecca | ||
|
|
2cb20618da | ||
|
|
92e948aabc | ||
|
|
cb4435d700 | ||
|
|
4c6e78e1a5 | ||
|
|
da9a91e05f | ||
|
|
506968634e | ||
|
|
a5d0cdf0dd | ||
|
|
c59aad43c8 | ||
|
|
19e72babc9 | ||
|
|
bfcdb13d6d | ||
|
|
878c1a83ad | ||
|
|
471e0f55e4 | ||
|
|
08127218ce | ||
|
|
b69202c222 | ||
|
|
b3494b12fc |
37
.github/workflows/book.yml
vendored
Normal file
37
.github/workflows/book.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Build documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pages: write # To push to a GitHub Pages site
|
||||
id-token: write # To update the deployment status
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install latest mdbook
|
||||
run: |
|
||||
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
|
||||
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
mkdir mdbook
|
||||
curl -sSL $url | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Build Book
|
||||
run: |
|
||||
cd doc
|
||||
mdbook build
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "doc/book"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
36
.github/workflows/cla.yml
vendored
36
.github/workflows/cla.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: "CLA Assistant"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened,closed,synchronize]
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "CLA Assistant"
|
||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
||||
# Beta Release
|
||||
uses: cla-assistant/github-action@v2.1.3-beta
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PAT }}
|
||||
with:
|
||||
path-to-signatures: 'signatures/version1/cla.json'
|
||||
path-to-document: 'https://github.com/revoltchat/cla/blob/master/CLA.md' # e.g. a CLA or a DCO document
|
||||
# branch should not be protected
|
||||
branch: 'master'
|
||||
allowlist: insertish,bot*
|
||||
|
||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
||||
remote-organization-name: revoltchat
|
||||
remote-repository-name: cla
|
||||
create-file-commit-message: 'cla(create): creating file for storing CLA Signatures'
|
||||
signed-commit-message: 'cla(sign): $contributorName has signed the CLA in #$pullRequestNo'
|
||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
5
.github/workflows/docker.yaml
vendored
5
.github/workflows/docker.yaml
vendored
@@ -2,14 +2,15 @@ name: Docker Test & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
# branches:
|
||||
# - "main"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
|
||||
10
.github/workflows/rust.yaml
vendored
10
.github/workflows/rust.yaml
vendored
@@ -2,6 +2,14 @@ name: Rust build, test, and generate specification
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/rust.yaml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
@@ -33,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Run services in background
|
||||
run: |
|
||||
docker-compose -f docker-compose.db.yml up -d
|
||||
docker compose -f docker-compose.db.yml up -d
|
||||
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
||||
Rocket.toml
|
||||
Revolt.toml
|
||||
Revolt.*.toml
|
||||
|
||||
target
|
||||
.data
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"nixEnvSelector.suggestion": false,
|
||||
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
|
||||
"nixEnvSelector.suggestion": false
|
||||
}
|
||||
|
||||
3348
Cargo.lock
generated
3348
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,12 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
|
||||
members = [
|
||||
"crates/delta",
|
||||
"crates/bonfire",
|
||||
"crates/core/*",
|
||||
"crates/services/*",
|
||||
"crates/bindings/*",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.77.2-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
@@ -17,14 +17,18 @@ 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/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/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM rust:1.70.0-slim
|
||||
FROM rust:1.77.2-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
@@ -13,14 +13,18 @@ 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/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/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
|
||||
5
LICENSE
5
LICENSE
@@ -1,4 +1,7 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
With the exception of crates that specify their own LICENSE file,
|
||||
the following license applies to the source code of this project.
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
|
||||
59
README.md
59
README.md
@@ -17,10 +17,15 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
||||
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
Rust 1.70 or higher.
|
||||
Rust 1.76 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).
|
||||
|
||||
## Development Guide
|
||||
|
||||
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
|
||||
|
||||
Before getting started, you'll want to install:
|
||||
|
||||
- Rust toolchain (rustup recommended)
|
||||
@@ -31,6 +36,20 @@ Before getting started, you'll want to install:
|
||||
> A **default.nix** is available for Nix users!
|
||||
> Just run `nix-shell` and continue.
|
||||
|
||||
As a heads-up, the development environment uses the following ports:
|
||||
|
||||
| Service | Port |
|
||||
| ------------------------- | :------------: |
|
||||
| MongoDB | 14017 |
|
||||
| Redis | 14079 |
|
||||
| MinIO | 14009 |
|
||||
| Maildev | 14025<br>14080 |
|
||||
| Revolt Web App | 14701 |
|
||||
| `crates/delta` | 14702 |
|
||||
| `crates/bonfire` | 14703 |
|
||||
| `crates/services/autumn` | 14704 |
|
||||
| `crates/services/january` | 14705 |
|
||||
|
||||
Now you can clone and build the project:
|
||||
|
||||
```bash
|
||||
@@ -39,16 +58,19 @@ cd revolt-backend
|
||||
cargo build
|
||||
```
|
||||
|
||||
If you want to run the API and event servers:
|
||||
A default configuration `Revolt.toml` is present in this project that is suited for development.
|
||||
|
||||
If you'd like to change anything, create a `Revolt.overrides.toml` file to overwrite it.
|
||||
|
||||
You may need to configure the legacy environment options:
|
||||
|
||||
```bash
|
||||
# create environment file (will be deprecated in future)
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
# (optionally) copy the default configuration file
|
||||
cp crates/core/config/Revolt.toml Revolt.toml
|
||||
# configure as necessary...
|
||||
Then continue:
|
||||
|
||||
```bash
|
||||
# start other necessary services
|
||||
docker compose up -d
|
||||
|
||||
@@ -56,6 +78,10 @@ docker compose up -d
|
||||
cargo run --bin revolt-delta
|
||||
# run the events server
|
||||
cargo run --bin revolt-bonfire
|
||||
# run the file server
|
||||
cargo run --bin revolt-autumn
|
||||
# run th proxy server
|
||||
cargo run --bin revolt-january
|
||||
|
||||
# hint:
|
||||
# mold -run <cargo build, cargo run, etc...>
|
||||
@@ -72,10 +98,10 @@ git clone --recursive https://github.com/revoltchat/revite
|
||||
cd revite
|
||||
yarn
|
||||
yarn build:deps
|
||||
yarn dev --port 3001
|
||||
yarn dev --port 14701
|
||||
```
|
||||
|
||||
Then go to https://local.revolt.chat:3001
|
||||
Then go to https://local.revolt.chat:14701
|
||||
|
||||
## Deployment Guide
|
||||
|
||||
@@ -105,6 +131,23 @@ Tag and push a new release by running:
|
||||
just release
|
||||
```
|
||||
|
||||
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/revoltchat/backend/releases/new) to create a changelog.
|
||||
|
||||
## Testing
|
||||
|
||||
First, start the required services:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.db.yml up -d
|
||||
```
|
||||
|
||||
Now run tests for whichever database:
|
||||
|
||||
```sh
|
||||
TEST_DB=REFERENCE cargo nextest run
|
||||
TEST_DB=MONGODB cargo nextest run
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
|
||||
|
||||
199
Revolt.toml
Normal file
199
Revolt.toml
Normal file
@@ -0,0 +1,199 @@
|
||||
# ⚠️ This configuration is intended for development environment.
|
||||
# If you'd like to override anything, create a Revolt.override.toml
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://localhost:14017"
|
||||
# Redis connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
redis = "redis://localhost:14079/"
|
||||
|
||||
[hosts]
|
||||
# Web locations of various services
|
||||
# Defaults assume all services are reverse-proxied
|
||||
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
|
||||
#
|
||||
# Remember to change these to https/wss where appropriate in production!
|
||||
app = "http://local.revolt.chat:14701"
|
||||
api = "http://local.revolt.chat:14702"
|
||||
events = "ws://local.revolt.chat:14703"
|
||||
autumn = "http://local.revolt.chat:14704"
|
||||
january = "http://local.revolt.chat:14705"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
[api]
|
||||
|
||||
[api.registration]
|
||||
# Whether an invite should be required for registration
|
||||
# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
|
||||
invite_only = false
|
||||
|
||||
[api.smtp]
|
||||
# Email server configuration for verification
|
||||
# Defaults to no email verification (host field is empty)
|
||||
host = "localhost"
|
||||
username = "smtp"
|
||||
password = "smtp"
|
||||
from_address = "development@revolt.chat"
|
||||
reply_to = "support@revolt.chat"
|
||||
port = 14025
|
||||
use_tls = false
|
||||
|
||||
[api.vapid]
|
||||
# Generate your own keys:
|
||||
# 1. Run `openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem`
|
||||
# 2. Find `private_key` using `base64 vapid_private.pem`
|
||||
# 3. Find `public_key` using `openssl ec -in vapid_private.pem -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n'`
|
||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
||||
|
||||
[api.fcm]
|
||||
# Google Firebase Cloud Messaging Service Account Key
|
||||
# Obtained from the cloud messaging console
|
||||
key_type = ""
|
||||
project_id = ""
|
||||
private_key_id = ""
|
||||
private_key = ""
|
||||
client_email = ""
|
||||
client_id = ""
|
||||
auth_uri = ""
|
||||
token_uri = ""
|
||||
auth_provider_x509_cert_url = ""
|
||||
client_x509_cert_url = ""
|
||||
|
||||
[api.apn]
|
||||
# Apple Push Notifications keys for sending notifications
|
||||
sandbox = false
|
||||
pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
[api.security]
|
||||
# Authifier Shield API key
|
||||
authifier_shield_key = ""
|
||||
# Legacy voice server management token
|
||||
voso_legacy_token = ""
|
||||
# Whether services are behind the Cloudflare network
|
||||
trust_cloudflare = false
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
hcaptcha_key = ""
|
||||
hcaptcha_sitekey = ""
|
||||
|
||||
[api.workers]
|
||||
# Maximum concurrent connections (to proxy server)
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[files]
|
||||
# Encryption key for stored files
|
||||
# Generate your own key using `openssl rand -base64 32`
|
||||
encryption_key = "qcuMA+ssxhMyKaNAKBGFfryfFtUH8NDlamQyDwGW6fU="
|
||||
# Quality used for lossy WebP previews (set to 100 for lossless)
|
||||
webp_quality = 80.0
|
||||
# Mime types that cannot be uploaded or served
|
||||
#
|
||||
# Example for Windows executables and Android installation files:
|
||||
# ["application/vnd.microsoft.portable-executable", "application/vnd.android.package-archive"]
|
||||
blocked_mime_types = []
|
||||
# ClamAV service
|
||||
# hostname:port
|
||||
clamd_host = ""
|
||||
|
||||
[files.limit]
|
||||
# Minimum image resolution
|
||||
min_resolution = [1, 1]
|
||||
# Maximum MP of images
|
||||
max_mega_pixels = 40
|
||||
# Maximum pixel side of an image
|
||||
max_pixel_side = 10_000
|
||||
|
||||
[files.preview]
|
||||
# Maximum image resolution
|
||||
attachments = [1280, 1280]
|
||||
avatars = [128, 128]
|
||||
backgrounds = [1280, 720]
|
||||
icons = [128, 128]
|
||||
banners = [480, 480]
|
||||
emojis = [128, 128]
|
||||
|
||||
[files.s3]
|
||||
# Configuration for S3
|
||||
# Defaults included for MinIO + self-hosted setup
|
||||
#
|
||||
# Backblaze B2:
|
||||
# - endpoint is listed on the "Buckets" page
|
||||
# - region is `eu-central-003` string from endpoint URL
|
||||
# - access_key_id is keyID generated on the "Application Keys" page
|
||||
# - secret_access_key is token generated on the "Application Keys" page
|
||||
# - default_bucket matches the name of the bucket you've created
|
||||
|
||||
# S3 protocol endpoint
|
||||
endpoint = "http://localhost:14009"
|
||||
# S3 region name
|
||||
region = "minio"
|
||||
# S3 protocol key ID
|
||||
access_key_id = "minioautumn"
|
||||
# S3 protocol access key
|
||||
secret_access_key = "minioautumn"
|
||||
# Bucket to upload to by default
|
||||
default_bucket = "revolt-uploads"
|
||||
|
||||
[features]
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.global]
|
||||
group_size = 100
|
||||
message_embeds = 5
|
||||
message_replies = 5
|
||||
message_reactions = 20
|
||||
server_emoji = 100
|
||||
server_roles = 200
|
||||
server_channels = 200
|
||||
|
||||
# How many days since creation a user is considered new
|
||||
new_user_days = 3
|
||||
|
||||
# Maximum permissible body size in bytes for uploads
|
||||
# (should be greater than any one file upload limit)
|
||||
body_limit_size = 20_000_000
|
||||
|
||||
[features.limits.new_user]
|
||||
outgoing_friend_requests = 5
|
||||
|
||||
bots = 2
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20_000_000
|
||||
avatar_size = 4_000_000
|
||||
background_size = 6_000_000
|
||||
icon_size = 2_500_000
|
||||
banner_size = 6_000_000
|
||||
emoji_size = 500_000
|
||||
|
||||
[features.limits.default]
|
||||
outgoing_friend_requests = 10
|
||||
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20_000_000
|
||||
avatar_size = 4_000_000
|
||||
background_size = 6_000_000
|
||||
icon_size = 2_500_000
|
||||
banner_size = 6_000_000
|
||||
emoji_size = 500_000
|
||||
|
||||
[sentry]
|
||||
# Configuration for Sentry error reporting
|
||||
api = ""
|
||||
events = ""
|
||||
55
compose.yml
Normal file
55
compose.yml
Normal file
@@ -0,0 +1,55 @@
|
||||
services:
|
||||
# Redis
|
||||
redis:
|
||||
image: eqalpha/keydb
|
||||
ports:
|
||||
- "14079:6379"
|
||||
|
||||
# MongoDB
|
||||
database:
|
||||
image: mongo
|
||||
ports:
|
||||
- "14017:27017"
|
||||
volumes:
|
||||
- ./.data/db:/data/db
|
||||
|
||||
# MinIO
|
||||
minio:
|
||||
image: minio/minio
|
||||
command: server /data
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioautumn
|
||||
MINIO_ROOT_PASSWORD: minioautumn
|
||||
volumes:
|
||||
- ./.data/minio:/data
|
||||
ports:
|
||||
- "14009:9000"
|
||||
restart: always
|
||||
|
||||
# Create buckets for minio.
|
||||
createbuckets:
|
||||
image: minio/mc
|
||||
depends_on:
|
||||
- minio
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioautumn
|
||||
MINIO_ROOT_PASSWORD: minioautumn
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
/usr/bin/mc config host add minio http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD;
|
||||
while ! /usr/bin/mc ready minio; do echo 'Waiting minio...' && sleep 1; done;
|
||||
/usr/bin/mc mb minio/revolt-uploads;
|
||||
exit 0;
|
||||
"
|
||||
|
||||
# Mock SMTP server
|
||||
maildev:
|
||||
image: soulteary/maildev
|
||||
ports:
|
||||
- "14025:8080"
|
||||
- "14080:8080"
|
||||
environment:
|
||||
MAILDEV_SMTP_PORT: 25
|
||||
MAILDEV_WEB_PORT: 8080
|
||||
MAILDEV_INCOMING_USER: smtp
|
||||
MAILDEV_INCOMING_PASS: smtp
|
||||
7
crates/bindings/node/.gitignore
vendored
Normal file
7
crates/bindings/node/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
target
|
||||
index.node
|
||||
**/node_modules
|
||||
**/.DS_Store
|
||||
npm-debug.log*
|
||||
cargo.log
|
||||
cross.log
|
||||
25
crates/bindings/node/Cargo.toml
Normal file
25
crates/bindings/node/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "revolt-nodejs-bindings"
|
||||
version = "0.7.16"
|
||||
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.7.16", path = "../../core/config" }
|
||||
revolt-result = { version = "0.7.16", path = "../../core/result" }
|
||||
revolt-database = { version = "0.7.16", path = "../../core/database" }
|
||||
92
crates/bindings/node/README.md
Normal file
92
crates/bindings/node/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# revolt.node
|
||||
|
||||
**revolt.node:** Node.js bindings for the Revolt software
|
||||
|
||||
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
|
||||
|
||||
## Building revolt.node
|
||||
|
||||
Building revolt.node requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
|
||||
|
||||
To run the build, run:
|
||||
|
||||
```sh
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`.
|
||||
|
||||
## Exploring revolt.node
|
||||
|
||||
After building revolt.node, you can explore its exports at the Node console:
|
||||
|
||||
```sh
|
||||
$ npm i
|
||||
$ npm run build
|
||||
$ node
|
||||
> require('.').hello()
|
||||
'hello node'
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
#### `npm install`
|
||||
|
||||
Installs the project, including running `npm run build`.
|
||||
|
||||
#### `npm run build`
|
||||
|
||||
Builds the Node addon (`index.node`) from source, generating a release build with `cargo --release`.
|
||||
|
||||
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm run build` and similar commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
|
||||
|
||||
```
|
||||
npm run build -- --feature=beetle
|
||||
```
|
||||
|
||||
#### `npm run debug`
|
||||
|
||||
Similar to `npm run build` but generates a debug build with `cargo`.
|
||||
|
||||
#### `npm run cross`
|
||||
|
||||
Similar to `npm run build` but uses [cross-rs](https://github.com/cross-rs/cross) to cross-compile for another platform. Use the [`CARGO_BUILD_TARGET`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget) environment variable to select the build target.
|
||||
|
||||
#### `npm test`
|
||||
|
||||
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
|
||||
|
||||
## Project Layout
|
||||
|
||||
The directory structure of this project is:
|
||||
|
||||
```
|
||||
revolt.node/
|
||||
├── Cargo.toml
|
||||
├── README.md
|
||||
├── src/
|
||||
| └── lib.rs
|
||||
├── index.node
|
||||
├── package.json
|
||||
└── target/
|
||||
```
|
||||
|
||||
| Entry | Purpose |
|
||||
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
|
||||
| `README.md` | This file. |
|
||||
| `src/` | The directory tree containing the Rust source code for the project. |
|
||||
| `lib.rs` | Entry point for the Rust source code. |
|
||||
| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. |
|
||||
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
|
||||
| `target/` | Binary artifacts generated by the Rust build. |
|
||||
|
||||
## Learn More
|
||||
|
||||
Learn more about:
|
||||
|
||||
- [Neon](https://neon-bindings.com).
|
||||
- [Rust](https://www.rust-lang.org).
|
||||
- [Node](https://nodejs.org).
|
||||
84
crates/bindings/node/index.d.ts
vendored
Normal file
84
crates/bindings/node/index.d.ts
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Channel, User } from "revolt-api";
|
||||
|
||||
/**
|
||||
* Opaque type for Revolt database
|
||||
*/
|
||||
export declare interface Database {}
|
||||
|
||||
/**
|
||||
* Opaque type for Revolt database
|
||||
*/
|
||||
export declare interface OpaqueUser {}
|
||||
|
||||
/**
|
||||
* Error type from Revolt backend
|
||||
*/
|
||||
export declare interface Err {
|
||||
type: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises background tasks and logging, must be called before anything else!
|
||||
* Can be called multiple times!
|
||||
*/
|
||||
export declare function init();
|
||||
|
||||
/**
|
||||
* Gets a new handle to the Revolt database
|
||||
* @returns {Database} Handle
|
||||
*/
|
||||
export declare function database(): Database;
|
||||
|
||||
/**
|
||||
* Fetch user from database
|
||||
* @param {string} userId User's ID
|
||||
* @this {Database}
|
||||
*/
|
||||
export declare function database_fetch_user(userId: string): OpaqueUser;
|
||||
|
||||
/**
|
||||
* Fetch user from database
|
||||
* @param {string} username Username
|
||||
* @param {string} discriminator Discriminator
|
||||
* @this {Database}
|
||||
*/
|
||||
export declare function database_fetch_user_by_username(
|
||||
username: string,
|
||||
discriminator: string
|
||||
): OpaqueUser;
|
||||
|
||||
/**
|
||||
* Gets model data as JSON
|
||||
* @this {OpaqueUser}
|
||||
*/
|
||||
export declare function model_data(): User;
|
||||
|
||||
/**
|
||||
* Gets error if the model failed to fetch
|
||||
* @this {OpaqueUser}
|
||||
*/
|
||||
export declare function model_error(): Err;
|
||||
|
||||
/**
|
||||
* Open a direct message channel between two users
|
||||
* @param {string} userA User A ID
|
||||
* @param {string} userB User B ID
|
||||
* @returns Existing or newly created channel
|
||||
*/
|
||||
export declare function proc_channels_create_dm(
|
||||
userA: string,
|
||||
userB: string
|
||||
): Promise<Channel & { error: Err }>;
|
||||
|
||||
/**
|
||||
* Suspend a user
|
||||
* @param {string} user User
|
||||
* @param {number} duration Duration (in days), set to 0 for indefinite
|
||||
* @param {string} reason Pipe-separated list of reasons (e.g. reason1|reason2|reason3)
|
||||
*/
|
||||
export declare function proc_users_suspend(
|
||||
user: OpaqueUser,
|
||||
duration: number,
|
||||
reason: string
|
||||
): Promise<{ error: Err }>;
|
||||
36
crates/bindings/node/package.json
Normal file
36
crates/bindings/node/package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "revolt-nodejs-bindings",
|
||||
"version": "0.7.15-rev0.0.3",
|
||||
"description": "Node.js bindings for the Revolt software",
|
||||
"main": "index.node",
|
||||
"scripts": {
|
||||
"test": "cargo test",
|
||||
"cargo-build": "cargo build --message-format=json > cargo.log",
|
||||
"cross-build": "cross build --message-format=json > cross.log",
|
||||
"postcargo-build": "neon dist < cargo.log",
|
||||
"postcross-build": "neon dist -m /target < cross.log",
|
||||
"debug": "npm run cargo-build --",
|
||||
"build": "npm run cargo-build -- --release",
|
||||
"cross": "npm run cross-build -- --release"
|
||||
},
|
||||
"author": "Paul Makles",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@neon-rs/cli": "0.1.73"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/revoltchat/backend"
|
||||
},
|
||||
"keywords": [
|
||||
"revolt",
|
||||
"chat"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/revoltchat/backend/issues"
|
||||
},
|
||||
"homepage": "https://github.com/revoltchat/backend#readme",
|
||||
"dependencies": {
|
||||
"revolt-api": "^0.7.15"
|
||||
}
|
||||
}
|
||||
117
crates/bindings/node/pnpm-lock.yaml
generated
Normal file
117
crates/bindings/node/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,117 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
revolt-api:
|
||||
specifier: ^0.7.15
|
||||
version: 0.7.15
|
||||
|
||||
devDependencies:
|
||||
'@neon-rs/cli':
|
||||
specifier: 0.1.73
|
||||
version: 0.1.73
|
||||
|
||||
packages:
|
||||
|
||||
/@cargo-messages/android-arm-eabi@0.1.72:
|
||||
resolution: {integrity: sha512-gGZxIM1mj+Y5x+ULND6ZCNr7f70OJi9wDlycSK8hGONy9wrChN6JAIHryddC5cqcwlYAoQ6IDcDFElnhAYbybA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/darwin-arm64@0.1.72:
|
||||
resolution: {integrity: sha512-EAzN5MLaXPljZKZDO5qR+aBs44eSq2ZbEnS7AI/FziE3MzeXbrGOS3fLba5+7yWPFXJyZolXzePm8N1EBv8ovg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/darwin-x64@0.1.72:
|
||||
resolution: {integrity: sha512-RLo6j8s3nYbjdd1LDct4wamfChyRit7zokUuxtIYCu9XOlltkN5vnj1vwnrPvoqCMZ/7CbbuHFwSTn9A71de/w==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/linux-arm-gnueabihf@0.1.72:
|
||||
resolution: {integrity: sha512-tHsRshuzfjrX6SDW3jg6al8vMNLTMgczGnVYl5RuBZf/yrAUuwe30KxA9ge6w6mW6Ox797DyBchzAc9OLgTgmQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/linux-x64-gnu@0.1.72:
|
||||
resolution: {integrity: sha512-VGtL6CCnUbhsP4aYuBNT5kfrAL7o0qjrxw97a+ax13t+nJd26tVEEIKHMu5drvvS/Nm/hn7sLT8zMnnCv0pvHg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/win32-arm64-msvc@0.1.72:
|
||||
resolution: {integrity: sha512-V93Cgz39K+yqa3MveNbhh29pYCp8izK5uEavjPoxlNxAbsMCWH+s0verGDdUcfGxjR1H2V7oZ4FszPqR2SqMRQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/win32-x64-msvc@0.1.72:
|
||||
resolution: {integrity: sha512-knz3uSrO0OSbq3U5VWfCY8FB4NsM43BOWLZ7x4sfaMOC1XWv+IyvDdkLe6DhJx8KUw46KIAimYs9YROrp6l46Q==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@neon-rs/cli@0.1.73:
|
||||
resolution: {integrity: sha512-1kv8S/feB6UQWQQwsnGfkSkEBOtlFDNExnioL81E2BwvUWgjQPaseHgpi2EpWVgsPUgur5eBm4QowmlpWkD4/w==}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
'@cargo-messages/android-arm-eabi': 0.1.72
|
||||
'@cargo-messages/darwin-arm64': 0.1.72
|
||||
'@cargo-messages/darwin-x64': 0.1.72
|
||||
'@cargo-messages/linux-arm-gnueabihf': 0.1.72
|
||||
'@cargo-messages/linux-x64-gnu': 0.1.72
|
||||
'@cargo-messages/win32-arm64-msvc': 0.1.72
|
||||
'@cargo-messages/win32-x64-msvc': 0.1.72
|
||||
dev: true
|
||||
|
||||
/axios@0.26.1:
|
||||
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/follow-redirects@1.15.6:
|
||||
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dev: false
|
||||
|
||||
/lodash.defaultsdeep@4.6.1:
|
||||
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
|
||||
dev: false
|
||||
|
||||
/revolt-api@0.7.15:
|
||||
resolution: {integrity: sha512-rWRnjBFvoXt/RHRxHg9KuP+x1Jk61KdsOpKOczVJg5tIqGDmqp/6QZHw5rJTtG+wCH3Ah3YKwY+MM5NHfg1W3Q==}
|
||||
dependencies:
|
||||
axios: 0.26.1
|
||||
lodash.defaultsdeep: 4.6.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
221
crates/bindings/node/src/lib.rs
Normal file
221
crates/bindings/node/src/lib.rs
Normal file
@@ -0,0 +1,221 @@
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use neon::prelude::*;
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
fn js_init(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
||||
static INIT: OnceLock<()> = OnceLock::new();
|
||||
if INIT.get().is_none() {
|
||||
INIT.get_or_init(|| {
|
||||
async_std::task::block_on(async {
|
||||
revolt_config::configure!(api);
|
||||
|
||||
match DatabaseInfo::Auto.connect().await {
|
||||
Ok(db) => {
|
||||
let authifier_db = db.clone().to_authifier().await.database;
|
||||
revolt_database::tasks::start_workers(db, authifier_db);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
})
|
||||
.or_else(|err| cx.throw_error(err))
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(cx.undefined())
|
||||
}
|
||||
|
||||
struct DatabaseBinding(Database, Channel);
|
||||
impl Finalize for DatabaseBinding {}
|
||||
impl DatabaseBinding {
|
||||
fn take(&self) -> (Database, Channel) {
|
||||
(self.0.clone(), self.1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn js_database(mut cx: FunctionContext) -> JsResult<JsBox<DatabaseBinding>> {
|
||||
let db = async_std::task::block_on(DatabaseInfo::Auto.connect())
|
||||
.or_else(|err| cx.throw_error(err))?;
|
||||
|
||||
let channel = cx.channel();
|
||||
Ok(cx.boxed(DatabaseBinding(db, channel)))
|
||||
}
|
||||
|
||||
// Implementations for models
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Model {
|
||||
User(revolt_database::User),
|
||||
Error(revolt_result::Error),
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn give(&self) -> Model {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Finalize for Model {}
|
||||
|
||||
macro_rules! shim_boxed {
|
||||
($cx: ident, $name: ident, $model: ident, $( $variable: ident $type: ident $id: expr )+, $cmd: ident, $( $arg: expr, )+) => {
|
||||
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||
$(
|
||||
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||
)+
|
||||
|
||||
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||
let (deferred, promise) = cx.promise();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
let result = db.$cmd($($arg,)+).await;
|
||||
deferred.settle_with(&channel, move |mut cx| {
|
||||
Ok(cx.boxed(match result {
|
||||
Ok(value) => Model::$model(value),
|
||||
Err(error) => Model::Error(error)
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
$cx.export_function(stringify!($name), $name)?;
|
||||
};
|
||||
}
|
||||
|
||||
fn js_data(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
match cx.this::<JsBox<Model>>()?.give() {
|
||||
Model::Error(_) => neon_serde4::to_value(&mut cx, &None::<()>),
|
||||
Model::User(user) => neon_serde4::to_value(&mut cx, &user),
|
||||
}
|
||||
.or_else(|e| cx.throw_error(e.to_string()))
|
||||
}
|
||||
|
||||
fn js_error(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
let value = match cx.this::<JsBox<Model>>()?.give() {
|
||||
Model::Error(err) => Some(err),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
neon_serde4::to_value(&mut cx, &value).or_else(|e| cx.throw_error(e.to_string()))
|
||||
}
|
||||
|
||||
// Basic data implementation
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ResultBinding<T> {
|
||||
#[serde(flatten)]
|
||||
value: Option<T>,
|
||||
error: Option<revolt_result::Error>,
|
||||
}
|
||||
|
||||
macro_rules! shim {
|
||||
($cx: ident, $name: ident, $( $variable: ident $type: ident $id: expr )*, $( $model: ident $modelType: ident $modelId: expr )*, | $db: ident | $closure: expr, $( $arg: expr, )+) => {
|
||||
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||
$(
|
||||
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||
)*
|
||||
|
||||
$(
|
||||
let mut $model = if let Model::$modelType(value) = cx.argument::<JsBox<Model>>($modelId)?.give() {
|
||||
value
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
)*
|
||||
|
||||
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||
let (deferred, promise) = cx.promise();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
let result = (|$db: $crate::Database| $closure)(db.clone()).await;
|
||||
deferred.settle_with(&channel, move |mut cx| {
|
||||
neon_serde4::to_value(
|
||||
&mut cx,
|
||||
&match result {
|
||||
Ok(value) => ResultBinding {
|
||||
value: Some(value),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => ResultBinding {
|
||||
value: None,
|
||||
error: Some(error),
|
||||
},
|
||||
},
|
||||
)
|
||||
.or_else(|e| cx.throw_error(e.to_string()))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
$cx.export_function(stringify!($name), $name)?;
|
||||
};
|
||||
}
|
||||
|
||||
#[neon::main]
|
||||
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||
// initialise required background stuff
|
||||
cx.export_function("init", js_init)?;
|
||||
|
||||
// database & model stuff
|
||||
cx.export_function("database", js_database)?;
|
||||
cx.export_function("model_data", js_data)?;
|
||||
cx.export_function("model_error", js_error)?;
|
||||
|
||||
shim_boxed!(
|
||||
cx,
|
||||
database_fetch_user,
|
||||
User,
|
||||
user_id JsString 0,
|
||||
fetch_user,
|
||||
&user_id,
|
||||
);
|
||||
|
||||
shim_boxed!(
|
||||
cx,
|
||||
database_fetch_user_by_username,
|
||||
User,
|
||||
username JsString 0
|
||||
discriminator JsString 1,
|
||||
fetch_user_by_username,
|
||||
&username, &discriminator,
|
||||
);
|
||||
|
||||
// procedure calls
|
||||
shim!(
|
||||
cx,
|
||||
proc_channels_create_dm,
|
||||
user_a JsString 0
|
||||
user_b JsString 1,
|
||||
,
|
||||
|db| async move {
|
||||
let user_a = db.fetch_user(&user_a).await?;
|
||||
let user_b = db.fetch_user(&user_b).await?;
|
||||
revolt_database::Channel::create_dm(&db, &user_a, &user_b).await
|
||||
},
|
||||
&userA, &userB,
|
||||
);
|
||||
|
||||
shim!(
|
||||
cx,
|
||||
proc_users_suspend,
|
||||
duration JsNumber 1
|
||||
reason JsString 2,
|
||||
user User 0,
|
||||
|db| async move {
|
||||
let duration = duration as usize;
|
||||
user.suspend(&db, if duration == 0 { None } else { Some(duration) }, Some(reason.split('|').map(|x| x.to_owned()).collect())).await
|
||||
},
|
||||
&user,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
53
crates/bindings/node/test.js
Normal file
53
crates/bindings/node/test.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const Internal = require(".");
|
||||
|
||||
// playing around with class wrapper, not practical
|
||||
class Model {
|
||||
constructor(model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
data() {
|
||||
return Internal.model_data.bind(this.model)();
|
||||
}
|
||||
|
||||
error() {
|
||||
return Internal.model_error.bind(this.model)();
|
||||
}
|
||||
}
|
||||
|
||||
class User extends Model {
|
||||
constructor(db, user) {
|
||||
super(user);
|
||||
this.db = db;
|
||||
}
|
||||
}
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.db = Internal.database();
|
||||
}
|
||||
|
||||
async fetchUser(userId) {
|
||||
return new User(
|
||||
this,
|
||||
await Internal.database_fetch_user.bind(this.db)(userId)
|
||||
);
|
||||
}
|
||||
|
||||
async fetchUserByUsername(username, discriminator) {
|
||||
return new User(
|
||||
this,
|
||||
await Internal.database_fetch_user_by_username.bind(this.db)(
|
||||
username,
|
||||
discriminator
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const db = new Database();
|
||||
db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data()));
|
||||
db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data()));
|
||||
db.fetchUserByUsername("dos", "1111").then((user) =>
|
||||
console.info(user.error())
|
||||
);
|
||||
6
crates/bindings/package-lock.json
generated
Normal file
6
crates/bindings/package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "bindings",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.7.1"
|
||||
version = "0.7.16"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -14,6 +14,8 @@ lru = "0.7.6"
|
||||
ulid = "0.5.0"
|
||||
once_cell = "1.9.0"
|
||||
redis-kiss = "0.1.4"
|
||||
lru_time_cache = "0.11.11"
|
||||
async-channel = "2.3.1"
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
@@ -34,11 +36,12 @@ async-std = { version = "1.8.0", features = [
|
||||
] }
|
||||
|
||||
# core
|
||||
authifier = { version = "1.0.8" }
|
||||
revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database" }
|
||||
revolt-permissions = { version = "0.7.1", path = "../core/permissions" }
|
||||
revolt-permissions = { version = "0.7.16", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||
|
||||
# Bundle Stage
|
||||
FROM debian:bullseye-slim
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ca-certificates && \
|
||||
apt-get clean
|
||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
||||
|
||||
EXPOSE 9000
|
||||
USER nonroot
|
||||
ENV HOST=0.0.0.0:9000
|
||||
CMD ["./revolt-bonfire"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_database::events::client::ReadyPayloadFields;
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -83,6 +84,17 @@ impl ProtocolConfiguration {
|
||||
pub fn get_protocol_format(&self) -> &ProtocolFormat {
|
||||
&self.format
|
||||
}
|
||||
|
||||
/// Get ready payload fields
|
||||
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
|
||||
vec![
|
||||
ReadyPayloadFields::Users,
|
||||
ReadyPayloadFields::Servers,
|
||||
ReadyPayloadFields::Channels,
|
||||
ReadyPayloadFields::Members,
|
||||
ReadyPayloadFields::Emoji,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Object holding one side of a channel for receiving the parsed information
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use revolt_database::{
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
|
||||
MemberCompositeKey, Presence, RelationshipStatus,
|
||||
events::client::{EventV1, ReadyPayloadFields},
|
||||
util::permissions::DatabasePermissionQuery,
|
||||
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
@@ -21,11 +22,11 @@ impl Cache {
|
||||
let server = self.servers.get(server);
|
||||
let mut query =
|
||||
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
|
||||
.channel(&channel);
|
||||
.channel(channel);
|
||||
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
||||
|
||||
if let Some(member) = member {
|
||||
query = query.member(&member);
|
||||
query = query.member(member);
|
||||
}
|
||||
|
||||
if let Some(server) = server {
|
||||
@@ -90,8 +91,13 @@ impl Cache {
|
||||
/// State Manager
|
||||
impl State {
|
||||
/// Generate a Ready packet for the current user
|
||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
||||
pub async fn generate_ready_payload(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
fields: Vec<ReadyPayloadFields>,
|
||||
) -> Result<EventV1> {
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
@@ -149,14 +155,36 @@ impl State {
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = db
|
||||
.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
|
||||
Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch user settings
|
||||
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
|
||||
.iter()
|
||||
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
|
||||
{
|
||||
Some(db.fetch_user_settings(&user.id, &keys).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch channel unreads
|
||||
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
|
||||
Some(db.fetch_unreads(&user.id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Copy data into local state cache.
|
||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
@@ -179,30 +207,52 @@ impl State {
|
||||
.collect();
|
||||
|
||||
// Make sure we see our own user correctly.
|
||||
users.push(user.into_self().await);
|
||||
users.push(user.into_self(true).await);
|
||||
|
||||
// Set subscription state internally.
|
||||
self.reset_state();
|
||||
self.insert_subscription(self.private_topic.clone());
|
||||
self.reset_state().await;
|
||||
self.insert_subscription(self.private_topic.clone()).await;
|
||||
|
||||
for user in &users {
|
||||
self.insert_subscription(user.id.clone());
|
||||
self.insert_subscription(user.id.clone()).await;
|
||||
}
|
||||
|
||||
for server in &servers {
|
||||
self.insert_subscription(server.id.clone());
|
||||
self.insert_subscription(server.id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", server.id)).await;
|
||||
}
|
||||
}
|
||||
|
||||
for channel in &channels {
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
}
|
||||
|
||||
Ok(EventV1::Ready {
|
||||
users,
|
||||
servers: servers.into_iter().map(Into::into).collect(),
|
||||
channels: channels.into_iter().map(Into::into).collect(),
|
||||
members: members.into_iter().map(Into::into).collect(),
|
||||
emojis: emojis.into_iter().map(Into::into).collect(),
|
||||
users: if fields.contains(&ReadyPayloadFields::Users) {
|
||||
Some(users)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
servers: if fields.contains(&ReadyPayloadFields::Servers) {
|
||||
Some(servers.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
channels: if fields.contains(&ReadyPayloadFields::Channels) {
|
||||
Some(channels.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
members: if fields.contains(&ReadyPayloadFields::Members) {
|
||||
Some(members.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
|
||||
user_settings,
|
||||
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -236,11 +286,11 @@ impl State {
|
||||
let mut bulk_events = vec![];
|
||||
|
||||
for id in added_channels {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
for id in removed_channels {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
self.cache.channels.remove(&id);
|
||||
|
||||
bulk_events.push(EventV1::ChannelDelete { id });
|
||||
@@ -263,7 +313,7 @@ impl State {
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
bulk_events.push(EventV1::ChannelCreate(channel.into()));
|
||||
}
|
||||
}
|
||||
@@ -336,7 +386,7 @@ impl State {
|
||||
match event {
|
||||
EventV1::ChannelCreate(channel) => {
|
||||
let id = channel.id().to_string();
|
||||
self.insert_subscription(id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
self.cache.channels.insert(id, channel.clone().into());
|
||||
}
|
||||
EventV1::ChannelUpdate {
|
||||
@@ -376,17 +426,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
EventV1::ChannelDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
self.cache.channels.remove(id);
|
||||
}
|
||||
EventV1::ChannelGroupJoin { user, .. } => {
|
||||
self.insert_subscription(user.clone());
|
||||
self.insert_subscription(user.clone()).await;
|
||||
}
|
||||
EventV1::ChannelGroupLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
} else if !self.cache.can_subscribe_to_user(user) {
|
||||
self.remove_subscription(user);
|
||||
self.remove_subscription(user).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +446,12 @@ impl State {
|
||||
channels,
|
||||
emojis: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
|
||||
if self.cache.is_bot {
|
||||
self.insert_subscription(format!("{}u", id)).await;
|
||||
}
|
||||
|
||||
self.cache.servers.insert(id.clone(), server.clone().into());
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
@@ -433,13 +488,13 @@ impl State {
|
||||
EventV1::ServerMemberJoin { .. } => {
|
||||
// We will always receive ServerCreate when joining a new server.
|
||||
}
|
||||
EventV1::ServerMemberLeave { id, user } => {
|
||||
EventV1::ServerMemberLeave { id, user, .. } => {
|
||||
if user == &self.cache.user_id {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel);
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
@@ -447,11 +502,11 @@ impl State {
|
||||
}
|
||||
}
|
||||
EventV1::ServerDelete { id } => {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
|
||||
if let Some(server) = self.cache.servers.remove(id) {
|
||||
for channel in &server.channels {
|
||||
self.remove_subscription(channel);
|
||||
self.remove_subscription(channel).await;
|
||||
self.cache.channels.remove(channel);
|
||||
}
|
||||
}
|
||||
@@ -524,9 +579,23 @@ impl State {
|
||||
self.cache.users.insert(id.clone(), user.clone().into());
|
||||
|
||||
if self.cache.can_subscribe_to_user(id) {
|
||||
self.insert_subscription(id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
} else {
|
||||
self.remove_subscription(id);
|
||||
self.remove_subscription(id).await;
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::Message(message) => {
|
||||
// Since Message events are fanned out to many clients,
|
||||
// we must reconstruct the relationship value at this end.
|
||||
if let Some(user) = &mut message.user {
|
||||
user.relationship = self
|
||||
.cache
|
||||
.users
|
||||
.get(&self.cache.user_id)
|
||||
.expect("missing self?")
|
||||
.relationship_with(&message.author)
|
||||
.into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,11 +609,11 @@ impl State {
|
||||
|
||||
// Sub / unsub accordingly.
|
||||
if let Some(id) = queue_add {
|
||||
self.insert_subscription(id);
|
||||
self.insert_subscription(id).await;
|
||||
}
|
||||
|
||||
if let Some(id) = queue_remove {
|
||||
self.remove_subscription(&id);
|
||||
self.remove_subscription(&id).await;
|
||||
}
|
||||
|
||||
true
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use lru::LruCache;
|
||||
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
@@ -30,6 +36,7 @@ pub enum SubscriptionStateChange {
|
||||
#[derive(Debug)]
|
||||
pub struct Cache {
|
||||
pub user_id: String,
|
||||
pub is_bot: bool,
|
||||
|
||||
pub users: HashMap<String, User>,
|
||||
pub channels: HashMap<String, Channel>,
|
||||
@@ -43,6 +50,7 @@ impl Default for Cache {
|
||||
fn default() -> Self {
|
||||
Cache {
|
||||
user_id: Default::default(),
|
||||
is_bot: false,
|
||||
|
||||
users: Default::default(),
|
||||
channels: Default::default(),
|
||||
@@ -58,14 +66,17 @@ impl Default for Cache {
|
||||
pub struct State {
|
||||
pub cache: Cache,
|
||||
|
||||
pub session_id: String,
|
||||
pub private_topic: String,
|
||||
subscribed: HashSet<String>,
|
||||
state: SubscriptionStateChange,
|
||||
pub state: SubscriptionStateChange,
|
||||
|
||||
pub subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
pub active_servers: Arc<Mutex<LruTimeCache<String, ()>>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Create state from User
|
||||
pub fn from(user: User) -> State {
|
||||
pub fn from(user: User, session_id: String) -> State {
|
||||
let mut subscribed = HashSet::new();
|
||||
let private_topic = format!("{}!", user.id);
|
||||
subscribed.insert(private_topic.clone());
|
||||
@@ -80,22 +91,61 @@ impl State {
|
||||
|
||||
State {
|
||||
cache,
|
||||
subscribed,
|
||||
subscribed: Arc::new(RwLock::new(subscribed)),
|
||||
active_servers: Arc::new(Mutex::new(LruTimeCache::with_expiry_duration_and_capacity(
|
||||
Duration::from_secs(900),
|
||||
5,
|
||||
))),
|
||||
session_id,
|
||||
private_topic,
|
||||
state: SubscriptionStateChange::Reset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply currently queued state
|
||||
pub fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
pub async fn apply_state(&mut self) -> SubscriptionStateChange {
|
||||
// Check if we need to change subscriptions to member event topics
|
||||
if !self.cache.is_bot {
|
||||
enum Server {
|
||||
Subscribe(String),
|
||||
Unsubscribe(String),
|
||||
}
|
||||
|
||||
let active_server_changes: Vec<Server> = {
|
||||
let mut active_servers = self.active_servers.lock().await;
|
||||
active_servers
|
||||
.notify_iter()
|
||||
.map(|e| match e {
|
||||
TimedEntry::Valid(k, _) => Server::Subscribe(format!("{}u", k)),
|
||||
TimedEntry::Expired(k, _) => Server::Unsubscribe(format!("{}u", k)),
|
||||
})
|
||||
.collect()
|
||||
// It is bad practice to open more than one Mutex at once and could
|
||||
// lead to a deadlock, so instead we choose to collect the changes.
|
||||
};
|
||||
|
||||
for entry in active_server_changes {
|
||||
match entry {
|
||||
Server::Subscribe(k) => {
|
||||
self.insert_subscription(k).await;
|
||||
}
|
||||
Server::Unsubscribe(k) => {
|
||||
self.remove_subscription(&k).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush changes to subscriptions
|
||||
let state = std::mem::replace(&mut self.state, SubscriptionStateChange::None);
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if let SubscriptionStateChange::Change { add, remove } = &state {
|
||||
for id in add {
|
||||
self.subscribed.insert(id.clone());
|
||||
subscribed.insert(id.clone());
|
||||
}
|
||||
|
||||
for id in remove {
|
||||
self.subscribed.remove(id);
|
||||
subscribed.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,20 +157,16 @@ impl State {
|
||||
self.cache.users.get(&self.cache.user_id).unwrap().clone()
|
||||
}
|
||||
|
||||
/// Iterate through all subscriptions
|
||||
pub fn iter_subscriptions(&self) -> std::collections::hash_set::Iter<'_, std::string::String> {
|
||||
self.subscribed.iter()
|
||||
}
|
||||
|
||||
/// Reset the current state
|
||||
pub fn reset_state(&mut self) {
|
||||
pub async fn reset_state(&mut self) {
|
||||
self.state = SubscriptionStateChange::Reset;
|
||||
self.subscribed.clear();
|
||||
self.subscribed.write().await.clear();
|
||||
}
|
||||
|
||||
/// Add a new subscription
|
||||
pub fn insert_subscription(&mut self, subscription: String) {
|
||||
if self.subscribed.contains(&subscription) {
|
||||
pub async fn insert_subscription(&mut self, subscription: String) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if subscribed.contains(&subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -137,12 +183,13 @@ impl State {
|
||||
SubscriptionStateChange::Reset => {}
|
||||
}
|
||||
|
||||
self.subscribed.insert(subscription);
|
||||
subscribed.insert(subscription);
|
||||
}
|
||||
|
||||
/// Remove existing subscription
|
||||
pub fn remove_subscription(&mut self, subscription: &str) {
|
||||
if !self.subscribed.contains(&subscription.to_string()) {
|
||||
pub async fn remove_subscription(&mut self, subscription: &str) {
|
||||
let mut subscribed = self.subscribed.write().await;
|
||||
if !subscribed.contains(&subscription.to_string()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,6 +206,6 @@ impl State {
|
||||
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
|
||||
}
|
||||
|
||||
self.subscribed.remove(subscription);
|
||||
subscribed.remove(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ async fn main() {
|
||||
clear_region(None).await;
|
||||
|
||||
// Setup a TCP listener to accept WebSocket connections on.
|
||||
// By default, we bind to port 9000 on all interfaces.
|
||||
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:9000".into());
|
||||
// By default, we bind to port 14703 on all interfaces.
|
||||
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into());
|
||||
info!("Listening on host {bind}");
|
||||
let try_socket = TcpListener::bind(bind).await;
|
||||
let listener = try_socket.expect("Failed to bind");
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::{RedisError, RedisErrorKind},
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
pin_mut, select,
|
||||
join, pin_mut, select,
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
@@ -18,7 +20,13 @@ use revolt_database::{
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
|
||||
use async_std::{net::TcpStream, sync::Mutex};
|
||||
use async_std::{
|
||||
net::TcpStream,
|
||||
sync::{Mutex, RwLock},
|
||||
task::spawn,
|
||||
};
|
||||
use revolt_result::create_error;
|
||||
use sentry::Level;
|
||||
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
use crate::events::state::{State, SubscriptionStateChange};
|
||||
@@ -42,10 +50,12 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Verify we've received a valid config, otherwise we should just drop the connection.
|
||||
let Ok(mut config) = receiver.await else {
|
||||
return;
|
||||
};
|
||||
|
||||
info!(
|
||||
"User {addr:?} provided protocol configuration (version = {}, format = {:?})",
|
||||
config.get_protocol_version(),
|
||||
@@ -57,10 +67,8 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
|
||||
// If the user has not provided authentication, request information.
|
||||
if config.get_session_token().is_none() {
|
||||
while let Ok(message) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) =
|
||||
config.decode(message.as_ref().unwrap())
|
||||
{
|
||||
while let Ok(Some(message)) = read.try_next().await {
|
||||
if let Ok(ClientMessage::Authenticate { token }) = config.decode(&message) {
|
||||
config.set_session_token(token);
|
||||
break;
|
||||
}
|
||||
@@ -69,35 +77,49 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
|
||||
// Try to authenticate the user.
|
||||
let Some(token) = config.get_session_token().as_ref() else {
|
||||
write
|
||||
.send(config.encode(&create_error!(InvalidSession)))
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
let user = match User::from_token(db, token, UserHint::Any).await {
|
||||
|
||||
let (user, session_id) = match User::from_token(db, token, UserHint::Any).await {
|
||||
Ok(user) => user,
|
||||
Err(err) => {
|
||||
write.send(config.encode(&err)).await.ok();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user);
|
||||
let mut state = State::from(user, session_id);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
if write
|
||||
.send(config.encode(&EventV1::Authenticated))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if let Err(err) = write.send(config.encode(&EventV1::Authenticated)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Download required data to local cache and send Ready payload.
|
||||
let Ok(ready_payload) = state.generate_ready_payload(db).await else {
|
||||
return;
|
||||
let ready_payload = match state
|
||||
.generate_ready_payload(db, config.get_ready_payload_fields())
|
||||
.await
|
||||
{
|
||||
Ok(ready_payload) => ready_payload,
|
||||
Err(err) => {
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if write.send(config.encode(&ready_payload)).await.is_err() {
|
||||
|
||||
if let Err(err) = write.send(config.encode(&ready_payload)).await {
|
||||
error!("Failed to write: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -110,20 +132,44 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
}
|
||||
|
||||
{
|
||||
// Setup channels and mutexes
|
||||
let write = Mutex::new(write);
|
||||
let subscribed = state.subscribed.clone();
|
||||
let active_servers = state.active_servers.clone();
|
||||
let (topic_signal_s, topic_signal_r) = async_channel::unbounded();
|
||||
|
||||
// TODO: this needs to be rewritten
|
||||
// Create channels through which the tasks can signal to each other they need to clean up
|
||||
let (kill_signal_1_s, kill_signal_1_r) = async_channel::bounded(1);
|
||||
let (kill_signal_2_s, kill_signal_2_r) = async_channel::bounded(1);
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener(db, &mut state, addr, &config, &write).fuse();
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker(addr, user_id.clone(), &config, read, &write).fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
|
||||
// Wait for either disconnect or for listener to die.
|
||||
select!(
|
||||
() = listener => {},
|
||||
() = worker => {}
|
||||
let listener = listener_with_kill_signal(
|
||||
db,
|
||||
&mut state,
|
||||
addr,
|
||||
&config,
|
||||
topic_signal_r,
|
||||
kill_signal_1_r,
|
||||
&write,
|
||||
kill_signal_2_s,
|
||||
);
|
||||
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker_with_kill_signal(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id.clone(),
|
||||
&config,
|
||||
topic_signal_s,
|
||||
kill_signal_2_r,
|
||||
read,
|
||||
&write,
|
||||
kill_signal_1_s,
|
||||
);
|
||||
|
||||
join!(listener, worker);
|
||||
}
|
||||
// Clean up presence session.
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
@@ -134,29 +180,88 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn listener_with_kill_signal(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
listener(
|
||||
db,
|
||||
state,
|
||||
addr,
|
||||
config,
|
||||
topic_signal_r,
|
||||
kill_signal_r,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
async fn listener(
|
||||
db: &'static Database,
|
||||
state: &mut State,
|
||||
addr: SocketAddr,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_r: async_channel::Receiver<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
||||
let Ok(subscriber) = fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if subscriber.init().await.is_err() {
|
||||
let subscriber = match fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
{
|
||||
Ok(subscriber) => subscriber,
|
||||
Err(err) => {
|
||||
error!("Failed to build a subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = subscriber.init().await {
|
||||
error!("Failed to init subscriber: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
return;
|
||||
};
|
||||
|
||||
// Handle Redis connection dropping
|
||||
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
||||
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
|
||||
subscriber.on_error(move |err| {
|
||||
if let RedisErrorKind::Canceled = err.kind() {
|
||||
let clean_up_s = clean_up_s.clone();
|
||||
spawn(async move {
|
||||
clean_up_s.lock().await.send(()).await.ok();
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
loop {
|
||||
'out: loop {
|
||||
// Check for state changes for subscriptions.
|
||||
match state.apply_state() {
|
||||
match state.apply_state().await {
|
||||
SubscriptionStateChange::Reset => {
|
||||
subscriber.unsubscribe_all().await.unwrap();
|
||||
for id in state.iter_subscriptions() {
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.unsubscribe_all().await {
|
||||
error!("Unsubscribe all failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
|
||||
let subscribed = state.subscribed.read().await;
|
||||
for id in subscribed.iter() {
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -167,112 +272,255 @@ async fn listener(
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} unsubscribing from {id}");
|
||||
|
||||
subscriber.unsubscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.unsubscribe(id).await {
|
||||
error!("Unsubscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
|
||||
for id in add {
|
||||
#[cfg(debug_assertions)]
|
||||
info!("{addr:?} subscribing to {id}");
|
||||
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
if let Err(err) = subscriber.subscribe(id).await {
|
||||
error!("Subscribe failed: {err:?}");
|
||||
sentry::capture_error(&err);
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
}
|
||||
SubscriptionStateChange::None => {}
|
||||
}
|
||||
|
||||
// Handle incoming events.
|
||||
let Ok(message) = message_rx.recv().await.map_err(|e| {
|
||||
warn!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let event = match *REDIS_PAYLOAD_TYPE {
|
||||
PayloadType::Json => message
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
|
||||
PayloadType::Msgpack => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
|
||||
PayloadType::Bincode => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
|
||||
};
|
||||
let Some(mut event) = event else {
|
||||
warn!("Failed to deserialise an event for {}!", message.channel);
|
||||
return;
|
||||
};
|
||||
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
let t1 = message_rx.recv().fuse();
|
||||
let t2 = topic_signal_r.recv().fuse();
|
||||
let t3 = kill_signal_r.recv().fuse();
|
||||
let t4 = clean_up_r.recv().fuse();
|
||||
|
||||
let result = write.lock().await.send(config.encode(&event)).await;
|
||||
if let Err(e) = result {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
warn!("Error while sending an event to {addr:?}: {e:?}");
|
||||
pin_mut!(t1, t2, t3, t4);
|
||||
|
||||
select! {
|
||||
_ = t4 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t3 => {
|
||||
break 'out;
|
||||
},
|
||||
_ = t2 => {},
|
||||
message = t1 => {
|
||||
// Handle incoming events.
|
||||
let message = match message {
|
||||
Ok(message) => message,
|
||||
Err(e) => {
|
||||
error!("Error while consuming pub/sub messages: {e:?}");
|
||||
sentry::capture_error(&e);
|
||||
break 'out;
|
||||
}
|
||||
};
|
||||
|
||||
let event = match *REDIS_PAYLOAD_TYPE {
|
||||
PayloadType::Json => message
|
||||
.value
|
||||
.as_str()
|
||||
.and_then(|s| serde_json::from_str::<EventV1>(s.as_ref()).ok()),
|
||||
PayloadType::Msgpack => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| rmp_serde::from_slice::<EventV1>(b).ok()),
|
||||
PayloadType::Bincode => message
|
||||
.value
|
||||
.as_bytes()
|
||||
.and_then(|b| bincode::deserialize::<EventV1>(b).ok()),
|
||||
};
|
||||
|
||||
let Some(mut event) = event else {
|
||||
let err = format!(
|
||||
"Failed to deserialise an event for {}! Introspection: `{:?}`",
|
||||
message.channel,
|
||||
message
|
||||
.value
|
||||
.as_string()
|
||||
.map(|x| x.chars().take(32).collect::<String>())
|
||||
);
|
||||
|
||||
error!("{}", err);
|
||||
sentry::capture_message(&err, Level::Error);
|
||||
break 'out;
|
||||
};
|
||||
|
||||
if let EventV1::Auth(auth) = &event {
|
||||
if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
|
||||
if &state.session_id == session_id {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else if let AuthifierEvent::DeleteAllSessions {
|
||||
exclude_session_id, ..
|
||||
} = auth
|
||||
{
|
||||
if let Some(excluded) = exclude_session_id {
|
||||
if &state.session_id != excluded {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
|
||||
if !should_send {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let result = write.lock().await.send(config.encode(&event)).await;
|
||||
if let Err(e) = result {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while sending an event to {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
break 'out;
|
||||
}
|
||||
|
||||
if let EventV1::Logout = event {
|
||||
info!("User {addr:?} received log out event!");
|
||||
break 'out;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = subscriber.quit().await {
|
||||
error!("{}", err);
|
||||
sentry::capture_error(&err);
|
||||
}
|
||||
}
|
||||
|
||||
async fn worker(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker_with_kill_signal(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
kill_signal_s: async_channel::Sender<()>,
|
||||
) {
|
||||
worker(
|
||||
addr,
|
||||
subscribed,
|
||||
active_servers,
|
||||
user_id,
|
||||
config,
|
||||
topic_signal_s,
|
||||
kill_signal_r,
|
||||
read,
|
||||
write,
|
||||
)
|
||||
.await;
|
||||
kill_signal_s.send(()).await.ok();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn worker(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
active_servers: Arc<Mutex<lru_time_cache::LruCache<String, ()>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
topic_signal_s: async_channel::Sender<()>,
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
loop {
|
||||
let result = read.try_next().await;
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => return,
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
warn!("Error while reading an event from {addr:?}: {e:?}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
let t1 = read.try_next().fuse();
|
||||
let t2 = kill_signal_r.recv().fuse();
|
||||
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Pong { data }))
|
||||
.await
|
||||
.ok();
|
||||
pin_mut!(t1, t2);
|
||||
|
||||
select! {
|
||||
_ = t2 => {
|
||||
return;
|
||||
},
|
||||
result = t1 => {
|
||||
let msg = match result {
|
||||
Ok(Some(msg)) => msg,
|
||||
Ok(None) => {
|
||||
warn!("Received a None message!");
|
||||
sentry::capture_message("Received a None message!", Level::Warning);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
use async_tungstenite::tungstenite::Error;
|
||||
if !matches!(e, Error::AlreadyClosed | Error::ConnectionClosed) {
|
||||
let err = format!("Error while reading an event from {addr:?}: {e:?}");
|
||||
warn!("{}", err);
|
||||
sentry::capture_message(&err, Level::Warning);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(payload) = config.decode(&msg) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Subscribe { server_id } => {
|
||||
let mut servers = active_servers.lock().await;
|
||||
let has_item = servers.contains_key(&server_id);
|
||||
servers.insert(server_id, ());
|
||||
|
||||
if !has_item {
|
||||
// Poke the listener to adjust subscriptions
|
||||
topic_signal_s.send(()).await.ok();
|
||||
}
|
||||
}
|
||||
ClientMessage::Ping { data, responded } => {
|
||||
if responded.is_none() {
|
||||
write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&EventV1::Pong { data }))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.7.1"
|
||||
version = "0.7.16"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Configuration"
|
||||
|
||||
|
||||
9
crates/core/config/LICENSE
Normal file
9
crates/core/config/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,8 +1,17 @@
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
mongodb = "mongodb://database"
|
||||
# Redis connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
redis = "redis://redis/"
|
||||
|
||||
[hosts]
|
||||
# Web locations of various services
|
||||
# Defaults assume all services are reverse-proxied
|
||||
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
|
||||
#
|
||||
# Remember to change these to https/wss where appropriate in production!
|
||||
app = "http://local.revolt.chat"
|
||||
api = "http://local.revolt.chat/api"
|
||||
events = "ws://local.revolt.chat/ws"
|
||||
@@ -14,58 +23,174 @@ voso_legacy_ws = ""
|
||||
[api]
|
||||
|
||||
[api.registration]
|
||||
# Whether an invite should be required for registration
|
||||
# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
|
||||
invite_only = false
|
||||
|
||||
[api.smtp]
|
||||
# Email server configuration for verification
|
||||
# Defaults to no email verification (host field is empty)
|
||||
host = ""
|
||||
username = ""
|
||||
password = ""
|
||||
from_address = ""
|
||||
from_address = "noreply@example.com"
|
||||
# reply_to = "noreply@example.com"
|
||||
# port = 587
|
||||
# use_tls = true
|
||||
|
||||
[api.vapid]
|
||||
# Generate your own keys:
|
||||
# 1. Run `openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem`
|
||||
# 2. Find `private_key` using `base64 vapid_private.pem`
|
||||
# 3. Find `public_key` using `openssl ec -in vapid_private.pem -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n'`
|
||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
||||
|
||||
[api.fcm]
|
||||
api_key = ""
|
||||
# Google Firebase Cloud Messaging Service Account Key
|
||||
# Obtained from the cloud messaging console
|
||||
key_type = ""
|
||||
project_id = ""
|
||||
private_key_id = ""
|
||||
private_key = ""
|
||||
client_email = ""
|
||||
client_id = ""
|
||||
auth_uri = ""
|
||||
token_uri = ""
|
||||
auth_provider_x509_cert_url = ""
|
||||
client_x509_cert_url = ""
|
||||
|
||||
[api.apn]
|
||||
# Apple Push Notifications keys for sending notifications
|
||||
sandbox = false
|
||||
pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
[api.security]
|
||||
# Authifier Shield API key
|
||||
authifier_shield_key = ""
|
||||
# Legacy voice server management token
|
||||
voso_legacy_token = ""
|
||||
# Whether services are behind the Cloudflare network
|
||||
trust_cloudflare = false
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
hcaptcha_key = ""
|
||||
hcaptcha_sitekey = ""
|
||||
|
||||
[api.workers]
|
||||
# Maximum concurrent connections (to proxy server)
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[files]
|
||||
# Encryption key for stored files
|
||||
# Generate your own key using `openssl rand -base64 32`
|
||||
encryption_key = "qcuMA+ssxhMyKaNAKBGFfryfFtUH8NDlamQyDwGW6fU="
|
||||
# Quality used for lossy WebP previews (set to 100 for lossless)
|
||||
webp_quality = 80.0
|
||||
# Mime types that cannot be uploaded or served
|
||||
#
|
||||
# Example for Windows executables and Android installation files:
|
||||
# ["application/vnd.microsoft.portable-executable", "application/vnd.android.package-archive"]
|
||||
blocked_mime_types = []
|
||||
# ClamAV service
|
||||
# hostname:port
|
||||
clamd_host = ""
|
||||
|
||||
[files.limit]
|
||||
# Minimum image resolution
|
||||
min_resolution = [1, 1]
|
||||
# Maximum MP of images
|
||||
max_mega_pixels = 40
|
||||
# Maximum pixel side of an image
|
||||
max_pixel_side = 10_000
|
||||
|
||||
[files.preview]
|
||||
# Maximum image resolution
|
||||
attachments = [1280, 1280]
|
||||
avatars = [128, 128]
|
||||
backgrounds = [1280, 720]
|
||||
icons = [128, 128]
|
||||
banners = [480, 480]
|
||||
emojis = [128, 128]
|
||||
|
||||
[files.s3]
|
||||
# Configuration for S3
|
||||
# Defaults included for MinIO + self-hosted setup
|
||||
#
|
||||
# Backblaze B2:
|
||||
# - endpoint is listed on the "Buckets" page
|
||||
# - region is `eu-central-003` string from endpoint URL
|
||||
# - access_key_id is keyID generated on the "Application Keys" page
|
||||
# - secret_access_key is token generated on the "Application Keys" page
|
||||
# - default_bucket matches the name of the bucket you've created
|
||||
|
||||
# S3 protocol endpoint
|
||||
endpoint = "http://minio:9000"
|
||||
# S3 region name
|
||||
region = "minio"
|
||||
# S3 protocol key ID
|
||||
access_key_id = "minioautumn"
|
||||
# S3 protocol access key
|
||||
secret_access_key = "minioautumn"
|
||||
# Bucket to upload to by default
|
||||
default_bucket = "revolt-uploads"
|
||||
|
||||
[features]
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
[features.limits.default]
|
||||
[features.limits.global]
|
||||
group_size = 100
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_embeds = 5
|
||||
message_replies = 5
|
||||
message_attachments = 5
|
||||
message_reactions = 20
|
||||
servers = 100
|
||||
server_emoji = 100
|
||||
server_roles = 200
|
||||
server_channels = 200
|
||||
|
||||
attachment_size = 20000000
|
||||
avatar_size = 4000000
|
||||
background_size = 6000000
|
||||
icon_size = 2500000
|
||||
banner_size = 6000000
|
||||
emoji_size = 500000
|
||||
# How many days since creation a user is considered new
|
||||
new_user_days = 3
|
||||
|
||||
# Maximum permissible body size in bytes for uploads
|
||||
# (should be greater than any one file upload limit)
|
||||
body_limit_size = 20_000_000
|
||||
|
||||
[features.limits.new_user]
|
||||
outgoing_friend_requests = 5
|
||||
|
||||
bots = 2
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20_000_000
|
||||
avatar_size = 4_000_000
|
||||
background_size = 6_000_000
|
||||
icon_size = 2_500_000
|
||||
banner_size = 6_000_000
|
||||
emoji_size = 500_000
|
||||
|
||||
[features.limits.default]
|
||||
outgoing_friend_requests = 10
|
||||
|
||||
bots = 5
|
||||
message_length = 2000
|
||||
message_attachments = 5
|
||||
servers = 100
|
||||
|
||||
attachment_size = 20_000_000
|
||||
avatar_size = 4_000_000
|
||||
background_size = 6_000_000
|
||||
icon_size = 2_500_000
|
||||
banner_size = 6_000_000
|
||||
emoji_size = 500_000
|
||||
|
||||
[sentry]
|
||||
# Configuration for Sentry error reporting
|
||||
api = ""
|
||||
events = ""
|
||||
|
||||
@@ -6,6 +6,8 @@ use futures_locks::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub use sentry::capture_error;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::env;
|
||||
|
||||
@@ -23,6 +25,8 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
));
|
||||
} else if std::path::Path::new("Revolt.toml").exists() {
|
||||
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
|
||||
} else if std::path::Path::new("/Revolt.toml").exists() {
|
||||
builder = builder.add_source(File::new("/Revolt.toml", FileFormat::Toml));
|
||||
}
|
||||
|
||||
builder.build().unwrap()
|
||||
@@ -72,7 +76,24 @@ pub struct ApiVapid {
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiFcm {
|
||||
pub api_key: String,
|
||||
pub key_type: String,
|
||||
pub project_id: String,
|
||||
pub private_key_id: String,
|
||||
pub private_key: String,
|
||||
pub client_email: String,
|
||||
pub client_id: String,
|
||||
pub auth_uri: String,
|
||||
pub token_uri: String,
|
||||
pub auth_provider_x509_cert_url: String,
|
||||
pub client_x509_cert_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiApn {
|
||||
pub sandbox: bool,
|
||||
pub pkcs8: String,
|
||||
pub key_id: String,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -100,24 +121,63 @@ pub struct Api {
|
||||
pub smtp: ApiSmtp,
|
||||
pub vapid: ApiVapid,
|
||||
pub fcm: ApiFcm,
|
||||
pub apn: ApiApn,
|
||||
pub security: ApiSecurity,
|
||||
pub workers: ApiWorkers,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimits {
|
||||
pub struct FilesLimit {
|
||||
pub min_resolution: [usize; 2],
|
||||
pub max_mega_pixels: usize,
|
||||
pub max_pixel_side: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FilesS3 {
|
||||
pub endpoint: String,
|
||||
pub region: String,
|
||||
pub access_key_id: String,
|
||||
pub secret_access_key: String,
|
||||
pub default_bucket: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Files {
|
||||
pub encryption_key: String,
|
||||
pub webp_quality: f32,
|
||||
pub blocked_mime_types: Vec<String>,
|
||||
pub clamd_host: String,
|
||||
|
||||
pub limit: FilesLimit,
|
||||
pub preview: HashMap<String, [usize; 2]>,
|
||||
pub s3: FilesS3,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct GlobalLimits {
|
||||
pub group_size: usize,
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_replies: usize,
|
||||
pub message_attachments: usize,
|
||||
pub message_embeds: usize,
|
||||
pub message_replies: usize,
|
||||
pub message_reactions: usize,
|
||||
pub servers: usize,
|
||||
pub server_emoji: usize,
|
||||
pub server_roles: usize,
|
||||
pub server_channels: usize,
|
||||
|
||||
pub new_user_days: usize,
|
||||
|
||||
pub body_limit_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimits {
|
||||
pub outgoing_friend_requests: usize,
|
||||
|
||||
pub bots: usize,
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
|
||||
pub attachment_size: usize,
|
||||
pub avatar_size: usize,
|
||||
pub background_size: usize,
|
||||
@@ -128,6 +188,9 @@ pub struct FeaturesLimits {
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesLimitsCollection {
|
||||
pub global: GlobalLimits,
|
||||
|
||||
pub new_user: FeaturesLimits,
|
||||
pub default: FeaturesLimits,
|
||||
|
||||
#[serde(flatten)]
|
||||
@@ -151,6 +214,7 @@ pub struct Settings {
|
||||
pub database: Database,
|
||||
pub hosts: Hosts,
|
||||
pub api: Api,
|
||||
pub files: Files,
|
||||
pub features: Features,
|
||||
pub sentry: Sentry,
|
||||
}
|
||||
@@ -158,22 +222,10 @@ pub struct Settings {
|
||||
impl Settings {
|
||||
pub fn preflight_checks(&self) {
|
||||
if self.api.smtp.host.is_empty() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
|
||||
panic!("Running in production without email is not recommended, set REVOLT_UNSAFE_NO_EMAIL=1 to override.");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
log::warn!("No SMTP settings specified! Remember to configure email.");
|
||||
}
|
||||
|
||||
if self.api.security.captcha.hcaptcha_key.is_empty() {
|
||||
#[cfg(not(debug_assertions))]
|
||||
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
|
||||
panic!("Running in production without CAPTCHA is not recommended, set REVOLT_UNSAFE_NO_CAPTCHA=1 to override.");
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.7.1"
|
||||
version = "0.7.16"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.7.1", path = "../config" }
|
||||
revolt-result = { version = "0.7.1", path = "../result" }
|
||||
revolt-models = { version = "0.7.1", path = "../models", features = [
|
||||
revolt-config = { version = "0.7.16", path = "../config" }
|
||||
revolt-result = { version = "0.7.16", path = "../result" }
|
||||
revolt-models = { version = "0.7.16", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.1", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.1", path = "../permissions", features = [
|
||||
revolt-presence = { version = "0.7.16", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.16", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
@@ -85,8 +85,9 @@ revolt_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
||||
|
||||
# Notifications
|
||||
fcm = "0.9.2"
|
||||
fcm_v1 = "0.3.0"
|
||||
web-push = "0.10.0"
|
||||
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0.8" }
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use authifier::config::Captcha;
|
||||
use authifier::config::EmailVerificationConfig;
|
||||
use authifier::config::ResolveIp;
|
||||
use authifier::config::SMTPSettings;
|
||||
use authifier::config::Shield;
|
||||
use authifier::config::Template;
|
||||
use authifier::config::Templates;
|
||||
use authifier::Authifier;
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
|
||||
@@ -87,3 +95,87 @@ impl DatabaseInfo {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Create an Authifier reference
|
||||
pub async fn to_authifier(self) -> Authifier {
|
||||
let config = config().await;
|
||||
|
||||
let mut auth_config = authifier::Config {
|
||||
email_verification: if !config.api.smtp.host.is_empty() {
|
||||
EmailVerificationConfig::Enabled {
|
||||
smtp: SMTPSettings {
|
||||
from: config.api.smtp.from_address,
|
||||
host: config.api.smtp.host,
|
||||
username: config.api.smtp.username,
|
||||
password: config.api.smtp.password,
|
||||
reply_to: Some(
|
||||
config
|
||||
.api
|
||||
.smtp
|
||||
.reply_to
|
||||
.unwrap_or("support@revolt.chat".into()),
|
||||
),
|
||||
port: config.api.smtp.port,
|
||||
use_tls: config.api.smtp.use_tls,
|
||||
},
|
||||
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,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
EmailVerificationConfig::Disabled
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
auth_config.invite_only = config.api.registration.invite_only;
|
||||
|
||||
if !config.api.security.captcha.hcaptcha_key.is_empty() {
|
||||
auth_config.captcha = Captcha::HCaptcha {
|
||||
secret: config.api.security.captcha.hcaptcha_key,
|
||||
};
|
||||
}
|
||||
|
||||
if !config.api.security.authifier_shield_key.is_empty() {
|
||||
auth_config.shield = Shield::Enabled {
|
||||
api_key: config.api.security.authifier_shield_key,
|
||||
strict: false,
|
||||
};
|
||||
}
|
||||
|
||||
if config.api.security.trust_cloudflare {
|
||||
auth_config.resolve_ip = ResolveIp::Cloudflare;
|
||||
}
|
||||
|
||||
Authifier {
|
||||
database: match self {
|
||||
Database::Reference(_) => Default::default(),
|
||||
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: auth_config,
|
||||
event_channel: Some(crate::tasks::authifier_relay::sender()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
|
||||
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
@@ -18,6 +18,7 @@ database_derived!(
|
||||
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
|
||||
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
|
||||
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
|
||||
pub file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
|
||||
pub files: Arc<Mutex<HashMap<String, File>>>,
|
||||
pub messages: Arc<Mutex<HashMap<String, Message>>>,
|
||||
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
|
||||
|
||||
@@ -2,10 +2,7 @@ use authifier::AuthifierEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
|
||||
FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
|
||||
UserSettings, Webhook,
|
||||
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
|
||||
};
|
||||
use revolt_result::Error;
|
||||
|
||||
@@ -39,6 +36,19 @@ pub enum ErrorEvent {
|
||||
APIError(Error),
|
||||
}
|
||||
|
||||
/// Fields provided in Ready payload
|
||||
#[derive(PartialEq)]
|
||||
pub enum ReadyPayloadFields {
|
||||
Users,
|
||||
Servers,
|
||||
Channels,
|
||||
Members,
|
||||
Emoji,
|
||||
|
||||
UserSettings(Vec<String>),
|
||||
ChannelUnreads,
|
||||
}
|
||||
|
||||
/// Protocol Events
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
@@ -48,13 +58,25 @@ pub enum EventV1 {
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
/// Logged out
|
||||
Logout,
|
||||
/// Basic data to cache
|
||||
Ready {
|
||||
users: Vec<User>,
|
||||
servers: Vec<Server>,
|
||||
channels: Vec<Channel>,
|
||||
members: Vec<Member>,
|
||||
emojis: Vec<Emoji>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
users: Option<Vec<User>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
servers: Option<Vec<Server>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channels: Option<Vec<Channel>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
members: Option<Vec<Member>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_settings: Option<UserSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
@@ -67,6 +89,7 @@ pub enum EventV1 {
|
||||
id: String,
|
||||
channel: String,
|
||||
data: PartialMessage,
|
||||
clear: Vec<FieldsMessage>,
|
||||
},
|
||||
|
||||
/// Append information to existing message
|
||||
@@ -134,7 +157,11 @@ pub enum EventV1 {
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave { id: String, user: String },
|
||||
ServerMemberLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
reason: RemovalIntention,
|
||||
},
|
||||
|
||||
/// Server role created or updated
|
||||
ServerRoleUpdate {
|
||||
@@ -247,7 +274,7 @@ impl EventV1 {
|
||||
// TODO: this should be captured by member list in the future and not immediately fanned out to users
|
||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
||||
for member in members {
|
||||
self.clone().p(member.id.server).await;
|
||||
self.clone().server(member.id.server).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,6 +284,11 @@ impl EventV1 {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish server member event
|
||||
pub async fn server(self, id: String) {
|
||||
self.p(format!("{id}u")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
|
||||
@@ -8,5 +8,6 @@ pub enum ClientMessage {
|
||||
Authenticate { token: String },
|
||||
BeginTyping { channel: String },
|
||||
EndTyping { channel: String },
|
||||
Subscribe { server_id: String },
|
||||
Ping { data: Ping, responded: Option<()> },
|
||||
}
|
||||
|
||||
@@ -87,3 +87,8 @@ pub mod tasks;
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
}
|
||||
|
||||
/// Utility function to check if an option doesnt contain true
|
||||
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||
t != &Some(true)
|
||||
}
|
||||
@@ -146,7 +146,14 @@ pub async fn create_database(db: &MongoDb) {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
"name": "channel_pinned_compound"
|
||||
},
|
||||
]
|
||||
},
|
||||
None,
|
||||
|
||||
@@ -5,8 +5,9 @@ use crate::{
|
||||
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
||||
options::FindOptions,
|
||||
},
|
||||
MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
||||
Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
||||
};
|
||||
use bson::oid::ObjectId;
|
||||
use futures::StreamExt;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
|
||||
@@ -19,7 +20,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 26;
|
||||
pub const LATEST_REVISION: i32 = 28;
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -983,6 +984,116 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
}
|
||||
|
||||
if revision <= 26 {
|
||||
info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging.");
|
||||
|
||||
auto_derived!(
|
||||
pub enum OldInvite {
|
||||
Server {
|
||||
#[serde(rename = "_id")]
|
||||
code: String,
|
||||
server: String,
|
||||
creator: String,
|
||||
channel: String,
|
||||
},
|
||||
Group {
|
||||
#[serde(rename = "_id")]
|
||||
code: String,
|
||||
creator: String,
|
||||
channel: String,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct Outer {
|
||||
_id: ObjectId,
|
||||
#[serde(flatten)]
|
||||
invite: OldInvite,
|
||||
}
|
||||
|
||||
let invites = db
|
||||
.db()
|
||||
.collection::<Outer>("channel_invites")
|
||||
.find(
|
||||
doc! {
|
||||
"type": { "$exists": false }
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("failed to find invites")
|
||||
.filter_map(|s| async { s.ok() })
|
||||
.collect::<Vec<Outer>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|invite| match invite.invite {
|
||||
OldInvite::Server {
|
||||
code,
|
||||
server,
|
||||
creator,
|
||||
channel,
|
||||
} => Invite::Server {
|
||||
code,
|
||||
server,
|
||||
creator,
|
||||
channel,
|
||||
},
|
||||
OldInvite::Group {
|
||||
code,
|
||||
creator,
|
||||
channel,
|
||||
} => Invite::Group {
|
||||
code,
|
||||
creator,
|
||||
channel,
|
||||
},
|
||||
})
|
||||
.collect::<Vec<Invite>>();
|
||||
|
||||
if !invites.is_empty() {
|
||||
db.db()
|
||||
.collection("channel_invites")
|
||||
.insert_many(invites, None)
|
||||
.await
|
||||
.expect("failed to insert corrected invite");
|
||||
|
||||
db.db()
|
||||
.collection::<Outer>("channel_invites")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"type": { "$exists": false }
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("failed to find invites");
|
||||
}
|
||||
}
|
||||
|
||||
if revision <= 27 {
|
||||
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,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create message index.");
|
||||
}
|
||||
|
||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
|
||||
@@ -2,7 +2,7 @@ use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{BotInformation, Database, PartialUser, User};
|
||||
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Bot
|
||||
@@ -72,7 +72,12 @@ impl Default for Bot {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Bot {
|
||||
/// Create a new bot
|
||||
pub async fn create<D>(db: &Database, username: String, owner: &User, data: D) -> Result<Bot>
|
||||
pub async fn create<D>(
|
||||
db: &Database,
|
||||
username: String,
|
||||
owner: &User,
|
||||
data: D,
|
||||
) -> Result<(Bot, User)>
|
||||
where
|
||||
D: Into<Option<PartialBot>>,
|
||||
{
|
||||
@@ -80,14 +85,13 @@ impl Bot {
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
|
||||
if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
|
||||
return Err(create_error!(ReachedMaximumBots));
|
||||
}
|
||||
|
||||
let id = Ulid::new().to_string();
|
||||
|
||||
User::create(
|
||||
let user = User::create(
|
||||
db,
|
||||
username,
|
||||
Some(id.to_string()),
|
||||
@@ -112,7 +116,7 @@ impl Bot {
|
||||
}
|
||||
|
||||
db.insert_bot(&bot).await?;
|
||||
Ok(bot)
|
||||
Ok((bot, user))
|
||||
}
|
||||
|
||||
/// Remove a field from this object
|
||||
@@ -142,6 +146,10 @@ impl Bot {
|
||||
|
||||
db.update_bot(&self.id, &partial, remove).await?;
|
||||
|
||||
if partial.token.is_some() {
|
||||
EventV1::Logout.private(self.id.clone()).await;
|
||||
}
|
||||
|
||||
self.apply_options(partial);
|
||||
Ok(())
|
||||
}
|
||||
@@ -164,7 +172,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bot = Bot::create(
|
||||
let (bot, _) = Bot::create(
|
||||
&db,
|
||||
"Bot Name".to_string(),
|
||||
&owner,
|
||||
|
||||
@@ -10,6 +10,7 @@ static ALPHABET: [char; 54] = [
|
||||
|
||||
auto_derived!(
|
||||
/// Invite
|
||||
#[serde(tag = "type")]
|
||||
pub enum Invite {
|
||||
/// Invite to a specific server channel
|
||||
Server {
|
||||
|
||||
@@ -7,13 +7,13 @@ mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractChannelUnreads: Sync + Send {
|
||||
/// Acknowledge a message.
|
||||
/// Acknowledge a message, and returns updated channel unread.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()>;
|
||||
) -> Result<Option<ChannelUnread>>;
|
||||
|
||||
/// Acknowledge many channels.
|
||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
|
||||
@@ -28,4 +28,7 @@ pub trait AbstractChannelUnreads: Sync + Send {
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use bson::Document;
|
||||
use mongodb::options::FindOneAndUpdateOptions;
|
||||
use mongodb::options::ReturnDocument;
|
||||
use mongodb::options::UpdateOptions;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
@@ -12,31 +14,35 @@ static COL: &str = "channel_unreads";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractChannelUnreads for MongoDb {
|
||||
/// Acknowledge a message.
|
||||
/// Acknowledge a message, and returns updated channel unread.
|
||||
async fn acknowledge_message(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
) -> Result<Option<ChannelUnread>> {
|
||||
self.col::<ChannelUnread>(COL)
|
||||
.find_one_and_update(
|
||||
doc! {
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": user_id,
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
"mentions": 1_i32
|
||||
"$pull": {
|
||||
"mentions": {
|
||||
"$lt": message_id
|
||||
}
|
||||
},
|
||||
"$set": {
|
||||
"last_id": message_id
|
||||
}
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
FindOneAndUpdateOptions::builder()
|
||||
.upsert(true)
|
||||
.return_document(ReturnDocument::After)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
@@ -116,4 +122,18 @@ impl AbstractChannelUnreads for MongoDb {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.user": user_id,
|
||||
"_id.channel": channel_id
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
channel_id: &str,
|
||||
user_id: &str,
|
||||
message_id: &str,
|
||||
) -> Result<()> {
|
||||
) -> Result<Option<ChannelUnread>> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
@@ -27,14 +27,14 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
unreads.insert(
|
||||
key.clone(),
|
||||
ChannelUnread {
|
||||
id: key,
|
||||
id: key.clone(),
|
||||
last_id: Some(message_id.to_string()),
|
||||
mentions: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(unreads.get(&key).cloned())
|
||||
}
|
||||
|
||||
/// Acknowledge many channels.
|
||||
@@ -87,4 +87,14 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch unread for a specific user in a channel.
|
||||
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||
let unreads = self.channel_unreads.lock().await;
|
||||
|
||||
Ok(unreads.get(&ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string()
|
||||
}).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,9 +201,9 @@ impl Channel {
|
||||
update_server: bool,
|
||||
) -> Result<Channel> {
|
||||
let config = config().await;
|
||||
if server.channels.len() > config.features.limits.default.server_channels {
|
||||
if server.channels.len() > config.features.limits.global.server_channels {
|
||||
return Err(create_error!(TooManyChannels {
|
||||
max: config.features.limits.default.server_channels,
|
||||
max: config.features.limits.global.server_channels,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -263,9 +263,9 @@ impl Channel {
|
||||
data.users.insert(owner_id.to_string());
|
||||
|
||||
let config = config().await;
|
||||
if data.users.len() > config.features.limits.default.group_size {
|
||||
if data.users.len() > config.features.limits.global.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.default.group_size,
|
||||
max: config.features.limits.global.group_size,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -346,9 +346,9 @@ impl Channel {
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
if recipients.len() >= config.features.limits.default.group_size {
|
||||
if recipients.len() >= config.features.limits.global.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.default.group_size
|
||||
max: config.features.limits.global.group_size
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -377,6 +377,8 @@ impl Channel {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
@@ -415,13 +417,13 @@ impl Channel {
|
||||
}
|
||||
|
||||
/// Clone this channel's id
|
||||
pub fn id(&self) -> String {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Channel::DirectMessage { id, .. }
|
||||
| Channel::Group { id, .. }
|
||||
| Channel::SavedMessages { id, .. }
|
||||
| Channel::TextChannel { id, .. }
|
||||
| Channel::VoiceChannel { id, .. } => id.clone(),
|
||||
| Channel::VoiceChannel { id, .. } => id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,6 +690,8 @@ impl Channel {
|
||||
username: name,
|
||||
avatar: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
@@ -698,6 +702,8 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
db.remove_user_from_group(id, &user.id).await?;
|
||||
|
||||
EventV1::ChannelGroupLeave {
|
||||
id: id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
@@ -723,6 +729,8 @@ impl Channel {
|
||||
username: &user.username,
|
||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
self,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -266,7 +266,7 @@ impl AbstractChannels for MongoDb {
|
||||
.await?;
|
||||
|
||||
// Delete the channel itself
|
||||
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
|
||||
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ impl AbstractChannels for ReferenceDb {
|
||||
/// Insert a new channel in the database
|
||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
|
||||
if let Entry::Vacant(entry) = channels.entry(channel.id().to_string()) {
|
||||
entry.insert(channel.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -148,7 +148,7 @@ impl AbstractChannels for ReferenceDb {
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if channels.remove(&channel.id()).is_some() {
|
||||
if channels.remove(channel.id()).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
|
||||
5
crates/core/database/src/models/file_hashes/mod.rs
Normal file
5
crates/core/database/src/models/file_hashes/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
53
crates/core/database/src/models/file_hashes/model.rs
Normal file
53
crates/core/database/src/models/file_hashes/model.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// File hash
|
||||
pub struct FileHash {
|
||||
/// Sha256 hash of the file
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Sha256 hash of file after it has been processed
|
||||
pub processed_hash: String,
|
||||
|
||||
/// When this file was created in system
|
||||
pub created_at: Timestamp,
|
||||
|
||||
/// The bucket this file is stored in
|
||||
pub bucket_id: String,
|
||||
/// The path at which this file exists in
|
||||
pub path: String,
|
||||
/// Cryptographic nonce used to encrypt this file
|
||||
pub iv: String,
|
||||
|
||||
/// Parsed metadata of this file
|
||||
pub metadata: Metadata,
|
||||
/// Raw content type of this file
|
||||
pub content_type: String,
|
||||
/// Size of this file (in bytes)
|
||||
pub size: isize,
|
||||
},
|
||||
"PartialFile"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Metadata associated with a file
|
||||
#[serde(tag = "type")]
|
||||
#[derive(Default)]
|
||||
pub enum Metadata {
|
||||
/// File is just a generic uncategorised file
|
||||
#[default]
|
||||
File,
|
||||
/// File contains textual data and should be displayed as such
|
||||
Text,
|
||||
/// File is an image with specific dimensions
|
||||
Image {
|
||||
width: isize,
|
||||
height: isize,
|
||||
// animated: bool // TODO: https://docs.rs/image/latest/image/trait.AnimationDecoder.html
|
||||
},
|
||||
/// File is a video with specific dimensions
|
||||
Video { width: isize, height: isize },
|
||||
/// File is audio
|
||||
Audio,
|
||||
}
|
||||
);
|
||||
12
crates/core/database/src/models/file_hashes/ops.rs
Normal file
12
crates/core/database/src/models/file_hashes/ops.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::FileHash;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAttachmentHashes: Sync + Send {
|
||||
/// Fetch an attachment hash entry by sha256 hash.
|
||||
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash>;
|
||||
}
|
||||
24
crates/core/database/src/models/file_hashes/ops/mongodb.rs
Normal file
24
crates/core/database/src/models/file_hashes/ops/mongodb.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::FileHash;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractAttachmentHashes;
|
||||
|
||||
static COL: &str = "attachment_hashes";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAttachmentHashes for MongoDb {
|
||||
/// Fetch an attachment hash entry by sha256 hash.
|
||||
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id": hash
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
23
crates/core/database/src/models/file_hashes/ops/reference.rs
Normal file
23
crates/core/database/src/models/file_hashes/ops/reference.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::FileHash;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractAttachmentHashes;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAttachmentHashes for ReferenceDb {
|
||||
/// Fetch an attachment hash entry by sha256 hash.
|
||||
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash> {
|
||||
let hashes = self.file_hashes.lock().await;
|
||||
if let Some(file) = hashes.get(hash) {
|
||||
if file.id == hash {
|
||||
Ok(file.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::Database;
|
||||
use crate::{Database, FileHash, Metadata};
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -12,12 +13,16 @@ auto_derived_partial!(
|
||||
pub tag: String,
|
||||
/// Original filename
|
||||
pub filename: String,
|
||||
/// Parsed metadata of this file
|
||||
pub metadata: Metadata,
|
||||
/// Raw content type of this file
|
||||
pub content_type: String,
|
||||
/// Size of this file (in bytes)
|
||||
pub size: isize,
|
||||
/// Hash of this file
|
||||
pub hash: Option<String>, // these are Option<>s to not break file uploads on legacy Autumn
|
||||
|
||||
/// When this file was uploaded
|
||||
pub uploaded_at: Option<Timestamp>, // these are Option<>s to not break file uploads on legacy Autumn
|
||||
/// ID of user who uploaded this file
|
||||
pub uploaded_id: Option<String>, // these are Option<>s to not break file uploads on legacy Autumn
|
||||
|
||||
/// What the file was used for
|
||||
pub used_for: Option<FileUsedFor>,
|
||||
|
||||
/// Whether this file was deleted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -26,6 +31,14 @@ auto_derived_partial!(
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reported: Option<bool>,
|
||||
|
||||
// !!! DEPRECATED:
|
||||
/// Parsed metadata of this file
|
||||
pub metadata: Metadata,
|
||||
/// Raw content type of this file
|
||||
pub content_type: String,
|
||||
/// Size of this file (in bytes)
|
||||
pub size: isize,
|
||||
|
||||
// TODO: migrate this mess to having:
|
||||
// - author_id
|
||||
// - parent: Parent { Message(id), User(id), etc }
|
||||
@@ -44,25 +57,35 @@ auto_derived_partial!(
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Metadata associated with a file
|
||||
#[serde(tag = "type")]
|
||||
#[derive(Default)]
|
||||
pub enum Metadata {
|
||||
/// File is just a generic uncategorised file
|
||||
#[default]
|
||||
File,
|
||||
/// File contains textual data and should be displayed as such
|
||||
Text,
|
||||
/// File is an image with specific dimensions
|
||||
Image { width: isize, height: isize },
|
||||
/// File is a video with specific dimensions
|
||||
Video { width: isize, height: isize },
|
||||
/// File is audio
|
||||
Audio,
|
||||
/// Type of object file was used for
|
||||
pub enum FileUsedForType {
|
||||
// TODO: changing this requires a db migration
|
||||
message,
|
||||
serverBanner,
|
||||
emoji,
|
||||
userAvatar,
|
||||
userProfileBackground,
|
||||
legacyGroupIcon,
|
||||
channelIcon,
|
||||
serverIcon,
|
||||
}
|
||||
|
||||
/// Information about what the file was used for
|
||||
pub struct FileUsedFor {
|
||||
/// Type of the object
|
||||
#[serde(rename = "type")]
|
||||
pub object_type: FileUsedForType,
|
||||
/// ID of the object
|
||||
pub id: String,
|
||||
}
|
||||
);
|
||||
|
||||
impl File {
|
||||
/// Get the hash entry for this file
|
||||
pub async fn as_hash(&self, db: &Database) -> Result<FileHash> {
|
||||
db.fetch_attachment_hash(self.hash.as_ref().unwrap()).await
|
||||
}
|
||||
|
||||
/// Use a file for a message attachment
|
||||
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "attachments", "message", parent)
|
||||
|
||||
@@ -10,6 +10,9 @@ pub trait AbstractAttachments: Sync + Send {
|
||||
/// Insert attachment into database.
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
|
||||
|
||||
/// Fetch an attachment by its id.
|
||||
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File>;
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
|
||||
@@ -15,6 +15,20 @@ impl AbstractAttachments for MongoDb {
|
||||
query!(self, insert_one, COL, &attachment).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch an attachment by its id.
|
||||
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id": file_id,
|
||||
"tag": tag
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
|
||||
@@ -18,6 +18,20 @@ impl AbstractAttachments for ReferenceDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch an attachment by its id.
|
||||
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File> {
|
||||
let files = self.files.lock().await;
|
||||
if let Some(file) = files.get(file_id) {
|
||||
if file.tag == tag {
|
||||
Ok(file.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
|
||||
@@ -2,10 +2,10 @@ use std::collections::HashSet;
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::config;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0::{
|
||||
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
|
||||
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
||||
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
|
||||
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
||||
};
|
||||
use revolt_permissions::{ChannelPermission, PermissionValue};
|
||||
use revolt_result::Result;
|
||||
@@ -65,6 +65,13 @@ auto_derived_partial!(
|
||||
/// Name and / or avatar overrides for this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub masquerade: Option<Masquerade>,
|
||||
/// Whether or not the message in pinned
|
||||
#[serde(skip_serializing_if = "crate::if_option_false")]
|
||||
pub pinned: Option<bool>,
|
||||
|
||||
/// Bitfield of message flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
},
|
||||
"PartialMessage"
|
||||
);
|
||||
@@ -95,6 +102,10 @@ auto_derived!(
|
||||
ChannelIconChanged { by: String },
|
||||
#[serde(rename = "channel_ownership_changed")]
|
||||
ChannelOwnershipChanged { from: String, to: String },
|
||||
#[serde(rename = "message_pinned")]
|
||||
MessagePinned { id: String, by: String },
|
||||
#[serde(rename = "message_unpinned")]
|
||||
MessageUnpinned { id: String, by: String },
|
||||
}
|
||||
|
||||
/// Name and / or avatar override information
|
||||
@@ -164,6 +175,8 @@ auto_derived!(
|
||||
pub author: Option<String>,
|
||||
/// Search query
|
||||
pub query: Option<String>,
|
||||
/// Search for pinned
|
||||
pub pinned: Option<bool>,
|
||||
}
|
||||
|
||||
/// Message Query
|
||||
@@ -179,6 +192,11 @@ auto_derived!(
|
||||
#[serde(flatten)]
|
||||
pub time_period: MessageTimePeriod,
|
||||
}
|
||||
|
||||
/// Optional fields on message
|
||||
pub enum FieldsMessage {
|
||||
Pinned,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
@@ -200,6 +218,8 @@ impl Default for Message {
|
||||
reactions: Default::default(),
|
||||
interactions: Default::default(),
|
||||
masquerade: None,
|
||||
flags: None,
|
||||
pinned: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,11 +227,15 @@ impl Default for Message {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Message {
|
||||
/// Create message from API data
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_from_api(
|
||||
db: &Database,
|
||||
channel: Channel,
|
||||
data: DataMessageSend,
|
||||
author: MessageAuthor<'_>,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
limits: FeaturesLimits,
|
||||
mut idempotency: IdempotencyKey,
|
||||
generate_embeds: bool,
|
||||
allow_mentions: bool,
|
||||
@@ -221,7 +245,7 @@ impl Message {
|
||||
Message::validate_sum(
|
||||
&data.content,
|
||||
data.embeds.as_deref().unwrap_or_default(),
|
||||
config.features.limits.default.message_length,
|
||||
limits.message_length,
|
||||
)?;
|
||||
|
||||
idempotency
|
||||
@@ -237,6 +261,13 @@ impl Message {
|
||||
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 {
|
||||
return Err(create_error!(InvalidProperty));
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure restrict_reactions is not specified without reactions list
|
||||
if let Some(interactions) = &data.interactions {
|
||||
if interactions.restrict_reactions {
|
||||
@@ -262,7 +293,7 @@ impl Message {
|
||||
let message_id = Ulid::new().to_string();
|
||||
let mut message = Message {
|
||||
id: message_id.clone(),
|
||||
channel: channel.id(),
|
||||
channel: channel.id().to_string(),
|
||||
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
|
||||
interactions: data
|
||||
.interactions
|
||||
@@ -270,6 +301,7 @@ impl Message {
|
||||
.unwrap_or_default(),
|
||||
author: author_id,
|
||||
webhook: webhook.map(|w| w.into()),
|
||||
flags: data.flags.map(|v| v as i32),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -288,9 +320,9 @@ impl Message {
|
||||
// Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > config.features.limits.default.message_replies {
|
||||
if entries.len() > config.features.limits.global.message_replies {
|
||||
return Err(create_error!(TooManyReplies {
|
||||
max: config.features.limits.default.message_replies,
|
||||
max: config.features.limits.global.message_replies,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -320,20 +352,20 @@ impl Message {
|
||||
if data
|
||||
.attachments
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
|
||||
.is_some_and(|v| v.len() > limits.message_attachments)
|
||||
{
|
||||
return Err(create_error!(TooManyAttachments {
|
||||
max: config.features.limits.default.message_attachments,
|
||||
max: limits.message_attachments,
|
||||
}));
|
||||
}
|
||||
|
||||
if data
|
||||
.embeds
|
||||
.as_ref()
|
||||
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
|
||||
.is_some_and(|v| v.len() > config.features.limits.global.message_embeds)
|
||||
{
|
||||
return Err(create_error!(TooManyEmbeds {
|
||||
max: config.features.limits.default.message_embeds,
|
||||
max: config.features.limits.global.message_embeds,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -360,7 +392,9 @@ impl Message {
|
||||
message.nonce = Some(idempotency.into_key());
|
||||
|
||||
// Send the message
|
||||
message.send(db, author, &channel, generate_embeds).await?;
|
||||
message
|
||||
.send(db, author, user, member, &channel, generate_embeds)
|
||||
.await?;
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
@@ -369,13 +403,15 @@ impl Message {
|
||||
pub async fn send_without_notifications(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
is_dm: bool,
|
||||
generate_embeds: bool,
|
||||
) -> Result<()> {
|
||||
db.insert_message(self).await?;
|
||||
|
||||
// Fan out events
|
||||
EventV1::Message(self.clone().into())
|
||||
EventV1::Message(self.clone().into_model(user, member))
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
@@ -416,29 +452,40 @@ impl Message {
|
||||
&mut self,
|
||||
db: &Database,
|
||||
author: MessageAuthor<'_>,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
channel: &Channel,
|
||||
generate_embeds: bool,
|
||||
) -> Result<()> {
|
||||
self.send_without_notifications(
|
||||
db,
|
||||
user.clone(),
|
||||
member.clone(),
|
||||
matches!(channel, Channel::DirectMessage { .. }),
|
||||
generate_embeds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Push out Web Push notifications
|
||||
crate::tasks::web_push::queue(
|
||||
{
|
||||
match channel {
|
||||
Channel::DirectMessage { recipients, .. }
|
||||
| Channel::Group { recipients, .. } => recipients.clone(),
|
||||
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
|
||||
_ => vec![],
|
||||
}
|
||||
},
|
||||
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
|
||||
)
|
||||
.await;
|
||||
if !self.has_suppressed_notifications() {
|
||||
// Push out Web Push notifications
|
||||
crate::tasks::web_push::queue(
|
||||
{
|
||||
match channel {
|
||||
Channel::DirectMessage { recipients, .. }
|
||||
| Channel::Group { recipients, .. } => recipients.clone(),
|
||||
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
|
||||
_ => vec![],
|
||||
}
|
||||
},
|
||||
PushNotification::from(
|
||||
self.clone().into_model(user, member),
|
||||
Some(author),
|
||||
channel.id(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -470,15 +517,37 @@ 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
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Update message data
|
||||
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialMessage,
|
||||
remove: Vec<FieldsMessage>,
|
||||
) -> Result<()> {
|
||||
self.apply_options(partial.clone());
|
||||
db.update_message(&self.id, &partial).await?;
|
||||
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
db.update_message(&self.id, &partial, remove.clone())
|
||||
.await?;
|
||||
|
||||
EventV1::MessageUpdate {
|
||||
id: self.id.clone(),
|
||||
channel: self.channel.clone(),
|
||||
data: partial.into(),
|
||||
clear: remove.into_iter().map(|field| field.into()).collect(),
|
||||
}
|
||||
.p(self.channel.clone())
|
||||
.await;
|
||||
@@ -498,13 +567,47 @@ impl Message {
|
||||
.fetch_messages(query)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.map(|msg| msg.into_model(None, None))
|
||||
.collect();
|
||||
|
||||
if let Some(true) = include_users {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.map(|m| m.author.clone())
|
||||
.flat_map(|m| {
|
||||
let mut users = vec![m.author.clone()];
|
||||
if let Some(system) = &m.system {
|
||||
match system {
|
||||
v0::SystemMessage::ChannelDescriptionChanged { by } => {
|
||||
users.push(by.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelIconChanged { by } => users.push(by.clone()),
|
||||
v0::SystemMessage::ChannelOwnershipChanged { from, to, .. } => {
|
||||
users.push(from.clone());
|
||||
users.push(to.clone())
|
||||
}
|
||||
v0::SystemMessage::ChannelRenamed { by, .. } => users.push(by.clone()),
|
||||
v0::SystemMessage::UserAdded { by, id, .. }
|
||||
| v0::SystemMessage::UserRemove { by, id, .. } => {
|
||||
users.push(by.clone());
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::UserBanned { id, .. }
|
||||
| v0::SystemMessage::UserKicked { id, .. }
|
||||
| v0::SystemMessage::UserJoined { id, .. }
|
||||
| v0::SystemMessage::UserLeft { id, .. } => {
|
||||
users.push(id.clone());
|
||||
}
|
||||
v0::SystemMessage::Text { .. } => {}
|
||||
v0::SystemMessage::MessagePinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
v0::SystemMessage::MessageUnpinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
users
|
||||
})
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
@@ -588,7 +691,7 @@ impl Message {
|
||||
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
|
||||
// Check how many reactions are already on the message
|
||||
let config = config().await;
|
||||
if self.reactions.len() >= config.features.limits.default.message_reactions
|
||||
if self.reactions.len() >= config.features.limits.global.message_reactions
|
||||
&& !self.reactions.contains_key(emoji)
|
||||
{
|
||||
return Err(create_error!(InvalidOperation));
|
||||
@@ -730,6 +833,12 @@ impl Message {
|
||||
// Write to database
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
}
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsMessage) {
|
||||
match field {
|
||||
FieldsMessage::Pinned => self.pinned = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemMessage {
|
||||
@@ -753,7 +862,7 @@ impl Interactions {
|
||||
if let Some(reactions) = &self.reactions {
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
||||
|
||||
if reactions.len() > config.features.limits.default.message_reactions {
|
||||
if reactions.len() > config.features.limits.global.message_reactions {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
@@ -20,7 +20,7 @@ pub trait AbstractMessages: Sync + Send {
|
||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()>;
|
||||
|
||||
/// Append information to a given message
|
||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
|
||||
|
||||
@@ -5,7 +5,8 @@ use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{
|
||||
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
|
||||
AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath, Message, MessageQuery,
|
||||
MessageTimePeriod, MongoDb, PartialMessage,
|
||||
};
|
||||
|
||||
use super::AbstractMessages;
|
||||
@@ -50,6 +51,10 @@ impl AbstractMessages for MongoDb {
|
||||
false
|
||||
};
|
||||
|
||||
if let Some(pinned) = query.filter.pinned {
|
||||
filter.insert("pinned", pinned);
|
||||
};
|
||||
|
||||
// 2. Find query limit
|
||||
let limit = query.limit.unwrap_or(50);
|
||||
|
||||
@@ -177,8 +182,22 @@ impl AbstractMessages for MongoDb {
|
||||
}
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
|
||||
async fn update_message(
|
||||
&self,
|
||||
id: &str,
|
||||
message: &PartialMessage,
|
||||
remove: Vec<FieldsMessage>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
message,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Append information to a given message
|
||||
@@ -296,6 +315,14 @@ impl AbstractMessages for MongoDb {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsMessage {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsMessage::Pinned => "pinned",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
|
||||
let mut for_attachments = projection.clone();
|
||||
|
||||
@@ -2,7 +2,7 @@ use futures::future::try_join_all;
|
||||
use indexmap::IndexSet;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
||||
|
||||
use super::AbstractMessages;
|
||||
|
||||
@@ -56,6 +56,12 @@ impl AbstractMessages for ReferenceDb {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pinned) = query.filter.pinned {
|
||||
if message.pinned.unwrap_or_default() == pinned {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.cloned()
|
||||
@@ -183,10 +189,15 @@ impl AbstractMessages for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message_data) = messages.get_mut(id) {
|
||||
message_data.apply_options(message.to_owned());
|
||||
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
message_data.remove_field(&field);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
|
||||
@@ -5,6 +5,7 @@ mod channel_unreads;
|
||||
mod channel_webhooks;
|
||||
mod channels;
|
||||
mod emojis;
|
||||
mod file_hashes;
|
||||
mod files;
|
||||
mod messages;
|
||||
mod ratelimit_events;
|
||||
@@ -23,6 +24,7 @@ pub use channel_unreads::*;
|
||||
pub use channel_webhooks::*;
|
||||
pub use channels::*;
|
||||
pub use emojis::*;
|
||||
pub use file_hashes::*;
|
||||
pub use files::*;
|
||||
pub use messages::*;
|
||||
pub use ratelimit_events::*;
|
||||
@@ -46,6 +48,7 @@ pub trait AbstractDatabase:
|
||||
+ channel_unreads::AbstractChannelUnreads
|
||||
+ channel_webhooks::AbstractWebhooks
|
||||
+ emojis::AbstractEmojis
|
||||
+ file_hashes::AbstractAttachmentHashes
|
||||
+ files::AbstractAttachments
|
||||
+ messages::AbstractMessages
|
||||
+ ratelimit_events::AbstractRatelimitEvents
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Member {
|
||||
server: &Server,
|
||||
user: &User,
|
||||
channels: Option<Vec<Channel>>,
|
||||
) -> Result<Vec<Channel>> {
|
||||
) -> Result<(Member, Vec<Channel>)> {
|
||||
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
|
||||
return Err(create_error!(Banned));
|
||||
}
|
||||
@@ -150,12 +150,12 @@ impl Member {
|
||||
id: user.id.clone(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.send_without_notifications(db, false, false)
|
||||
.send_without_notifications(db, None, None, false, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(channels)
|
||||
Ok((member, channels))
|
||||
}
|
||||
|
||||
/// Update member data
|
||||
@@ -229,6 +229,7 @@ impl Member {
|
||||
EventV1::ServerMemberLeave {
|
||||
id: self.id.server.to_string(),
|
||||
user: self.id.user.to_string(),
|
||||
reason: intention.clone().into(),
|
||||
}
|
||||
.p(self.id.server.to_string())
|
||||
.await;
|
||||
@@ -250,7 +251,7 @@ impl Member {
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
// TODO: support notifications here in the future?
|
||||
.send_without_notifications(db, false, false)
|
||||
.send_without_notifications(db, None, None, false, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ impl Server {
|
||||
vec![]
|
||||
};
|
||||
|
||||
server.channels = channels.iter().map(|c| c.id()).collect();
|
||||
server.channels = channels.iter().map(|c| c.id().to_string()).collect();
|
||||
db.insert_server(&server).await?;
|
||||
Ok((server, channels))
|
||||
}
|
||||
|
||||
@@ -214,14 +214,21 @@ impl MongoDb {
|
||||
|
||||
// Delete all emoji.
|
||||
self.col::<Document>("emojis")
|
||||
.delete_many(
|
||||
.update_many(
|
||||
doc! {
|
||||
"parent.id": &server_id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"parent": {
|
||||
"type": "Detached"
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "emojis"))?;
|
||||
.map_err(|_| create_database_error!("update_many", "emojis"))?;
|
||||
|
||||
// Delete all channels.
|
||||
self.col::<Document>("channels")
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use std::{collections::HashSet, time::Duration};
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
|
||||
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
|
||||
|
||||
use authifier::config::{EmailVerificationConfig, Template};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0::{self, UserFlags};
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde_json::json;
|
||||
use ulid::Ulid;
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -49,6 +52,10 @@ auto_derived_partial!(
|
||||
/// Bot information
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bot: Option<BotInformation>,
|
||||
|
||||
/// Time until user is unsuspended
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suspended_until: Option<Timestamp>,
|
||||
},
|
||||
"PartialUser"
|
||||
);
|
||||
@@ -61,6 +68,11 @@ auto_derived!(
|
||||
StatusPresence,
|
||||
ProfileContent,
|
||||
ProfileBackground,
|
||||
DisplayName,
|
||||
|
||||
// internal fields
|
||||
Suspension,
|
||||
None,
|
||||
}
|
||||
|
||||
/// User's relationship with another user (or themselves)
|
||||
@@ -164,6 +176,7 @@ impl Default for User {
|
||||
flags: Default::default(),
|
||||
privileged: Default::default(),
|
||||
bot: Default::default(),
|
||||
suspended_until: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,6 +210,22 @@ impl User {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
/// Get limits for this user
|
||||
pub async fn limits(&self) -> FeaturesLimits {
|
||||
let config = config().await;
|
||||
if ulid::Ulid::from_str(&self.id)
|
||||
.expect("`ulid`")
|
||||
.datetime()
|
||||
.elapsed()
|
||||
.expect("time went backwards")
|
||||
<= Duration::from_secs(86400u64 * config.features.limits.global.new_user_days as u64)
|
||||
{
|
||||
config.features.limits.new_user
|
||||
} else {
|
||||
config.features.limits.default
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the relationship with another user
|
||||
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
|
||||
if self.id == user_b {
|
||||
@@ -235,12 +264,11 @@ impl User {
|
||||
|
||||
/// Check if this user can acquire another server
|
||||
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
|
||||
let config = config().await;
|
||||
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
|
||||
if db.fetch_server_count(&self.id).await? <= self.limits().await.servers {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(TooManyServers {
|
||||
max: config.features.limits.default.servers
|
||||
max: self.limits().await.servers
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -276,23 +304,27 @@ impl User {
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
/// Find a user from a given token and hint
|
||||
/// Find a user and session ID from a given token and hint
|
||||
#[async_recursion]
|
||||
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<User> {
|
||||
pub async fn from_token(db: &Database, token: &str, hint: UserHint) -> Result<(User, String)> {
|
||||
match hint {
|
||||
UserHint::Bot => {
|
||||
UserHint::Bot => Ok((
|
||||
db.fetch_user(
|
||||
&db.fetch_bot_by_token(token)
|
||||
.await
|
||||
.map_err(|_| create_error!(InvalidSession))?
|
||||
.id,
|
||||
)
|
||||
.await
|
||||
.await?,
|
||||
String::new(),
|
||||
)),
|
||||
UserHint::User => {
|
||||
let session = db.fetch_session_by_token(token).await?;
|
||||
Ok((db.fetch_user(&session.user_id).await?, session.id))
|
||||
}
|
||||
UserHint::User => db.fetch_user_by_token(token).await,
|
||||
UserHint::Any => {
|
||||
if let Ok(user) = User::from_token(db, token, UserHint::User).await {
|
||||
Ok(user)
|
||||
if let Ok(result) = User::from_token(db, token, UserHint::User).await {
|
||||
Ok(result)
|
||||
} else {
|
||||
User::from_token(db, token, UserHint::Bot).await
|
||||
}
|
||||
@@ -473,6 +505,7 @@ impl User {
|
||||
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
|
||||
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
|
||||
RelationshipStatus::Incoming => {
|
||||
// Accept incoming friend request
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
@@ -482,6 +515,26 @@ impl User {
|
||||
.await
|
||||
}
|
||||
RelationshipStatus::None => {
|
||||
// Get this user's current count of outgoing friend requests
|
||||
let count = self
|
||||
.relations
|
||||
.as_ref()
|
||||
.map(|relations| {
|
||||
relations
|
||||
.iter()
|
||||
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
|
||||
.count()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// If we're over the limit, don't allow creating more requests
|
||||
if count >= self.limits().await.outgoing_friend_requests {
|
||||
return Err(create_error!(TooManyPendingFriendRequests {
|
||||
max: self.limits().await.outgoing_friend_requests
|
||||
}));
|
||||
}
|
||||
|
||||
// Send the friend request
|
||||
self.apply_relationship(
|
||||
db,
|
||||
target,
|
||||
@@ -617,9 +670,107 @@ impl User {
|
||||
x.background = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::DisplayName => self.display_name = None,
|
||||
FieldsUser::Suspension => self.suspended_until = None,
|
||||
FieldsUser::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Suspend the user
|
||||
///
|
||||
/// - If a duration is specified, the user will be automatically unsuspended after the given time.
|
||||
/// - If a reason is specified, an email will be sent.
|
||||
pub async fn suspend(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
duration_days: Option<usize>,
|
||||
reason: Option<Vec<String>>,
|
||||
) -> Result<()> {
|
||||
let authifier = db.clone().to_authifier().await;
|
||||
let mut account = authifier
|
||||
.database
|
||||
.find_account(&self.id)
|
||||
.await
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
account
|
||||
.disable(&authifier)
|
||||
.await
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
account
|
||||
.delete_all_sessions(&authifier, None)
|
||||
.await
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
self.update(
|
||||
db,
|
||||
PartialUser {
|
||||
flags: Some(UserFlags::SuspendedUntil as i32),
|
||||
suspended_until: duration_days.and_then(|dur| {
|
||||
Timestamp::now_utc().checked_add(iso8601_timestamp::Duration::days(dur as i64))
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(reason) = reason {
|
||||
if let EmailVerificationConfig::Enabled { smtp, .. } =
|
||||
authifier.config.email_verification
|
||||
{
|
||||
smtp.send_email(
|
||||
account.email.clone(),
|
||||
// maybe move this to common area?
|
||||
&Template {
|
||||
title: "Account Suspension".to_string(),
|
||||
html: Some(include_str!("../../../templates/suspension.html").to_owned()),
|
||||
text: include_str!("../../../templates/suspension.txt").to_owned(),
|
||||
url: Default::default(),
|
||||
},
|
||||
json!({
|
||||
"email": account.email,
|
||||
"list": reason.join(", "),
|
||||
"duration": duration_days,
|
||||
"duration_display": if duration_days.is_some() {
|
||||
"block"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unsuspend the user
|
||||
pub async fn unsuspend(&mut self, db: &Database) -> Result<()> {
|
||||
self.update(
|
||||
db,
|
||||
PartialUser {
|
||||
flags: Some(0),
|
||||
suspended_until: None,
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Permanently ban the user
|
||||
///
|
||||
/// - If a reason is specified, an email will be sent.
|
||||
pub async fn ban(&mut self, _db: &Database, _reason: Option<String>) -> Result<()> {
|
||||
// Send ban email (if reason provided)
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Mark as deleted
|
||||
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
|
||||
self.update(
|
||||
@@ -635,6 +786,7 @@ impl User {
|
||||
FieldsUser::StatusPresence,
|
||||
FieldsUser::ProfileContent,
|
||||
FieldsUser::ProfileBackground,
|
||||
FieldsUser::Suspension,
|
||||
],
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use authifier::models::Session;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
@@ -16,8 +17,8 @@ pub trait AbstractUsers: Sync + Send {
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User>;
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, token: &str) -> Result<Session>;
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
|
||||
@@ -57,4 +58,7 @@ pub trait AbstractUsers: Sync + Send {
|
||||
|
||||
/// Delete a user by their id
|
||||
async fn delete_user(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Remove push subscription for a session by session id (TODO: remove)
|
||||
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -46,10 +46,9 @@ impl AbstractUsers for MongoDb {
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
|
||||
let session = self
|
||||
.col::<Session>("sessions")
|
||||
/// 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
|
||||
@@ -58,9 +57,7 @@ impl AbstractUsers for MongoDb {
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find_one", "sessions"))?
|
||||
.ok_or_else(|| create_error!(InvalidSession))?;
|
||||
|
||||
self.fetch_user(&session.user_id).await
|
||||
.ok_or_else(|| create_error!(InvalidSession))
|
||||
}
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
@@ -319,6 +316,25 @@ impl AbstractUsers for MongoDb {
|
||||
async fn delete_user(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
|
||||
/// Remove push subscription for a session by session id (TODO: remove)
|
||||
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()> {
|
||||
self.col::<User>("sessions")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": session_id
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
"subscription": 1
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsUser {
|
||||
@@ -329,6 +345,9 @@ impl IntoDocumentPath for FieldsUser {
|
||||
FieldsUser::ProfileContent => "profile.content",
|
||||
FieldsUser::StatusPresence => "status.presence",
|
||||
FieldsUser::StatusText => "status.text",
|
||||
FieldsUser::DisplayName => "display_name",
|
||||
FieldsUser::Suspension => "suspended_until",
|
||||
FieldsUser::None => "none",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use authifier::models::Session;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
@@ -40,8 +41,8 @@ impl AbstractUsers for ReferenceDb {
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> {
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, _token: &str) -> Result<Session> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -162,4 +163,9 @@ impl AbstractUsers for ReferenceDb {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove push subscription for a session by session id (TODO: remove)
|
||||
async fn remove_push_subscription_by_session_id(&self, _session_id: &str) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ use deadqueue::limited::Queue;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
use super::DelayedTask;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::{apple_notifications::{self, ApnJob}, DelayedTask};
|
||||
|
||||
/// Enumeration of possible events
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@@ -52,8 +54,43 @@ pub async fn queue(channel: String, user: String, event: AckEvent) {
|
||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||
}
|
||||
|
||||
pub async fn handle_ack_event(event: &AckEvent, db: &Database, authifier_db: &authifier::Database, user: &str, channel: &str) -> Result<()> {
|
||||
match &event {
|
||||
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
|
||||
AckEvent::AckMessage { id } => {
|
||||
let unread = db.fetch_unread(user, channel).await?;
|
||||
let updated = db.acknowledge_message(channel, user, id).await?;
|
||||
|
||||
if let (Some(before), Some(after)) = (unread, updated) {
|
||||
let before_mentions = before.mentions.unwrap_or_default().len();
|
||||
let after_mentions = after.mentions.unwrap_or_default().len();
|
||||
|
||||
let mentions_acked = before_mentions - after_mentions;
|
||||
|
||||
if mentions_acked > 0 {
|
||||
if let Ok(sessions) = authifier_db.find_sessions(user).await {
|
||||
for session in sessions {
|
||||
if let Some(sub) = session.subscription {
|
||||
if sub.endpoint == "apn" {
|
||||
apple_notifications::queue(ApnJob::from_ack(session.id, user.to_string(), sub.auth)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
},
|
||||
AckEvent::AddMention { ids } => {
|
||||
db.add_mention_to_unread(channel, user, ids).await?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
pub async fn worker(db: Database, authifier_db: authifier::Database) {
|
||||
let mut tasks = HashMap::<(String, String), DelayedTask<Task>>::new();
|
||||
let mut keys = vec![];
|
||||
|
||||
@@ -71,13 +108,7 @@ pub async fn worker(db: Database) {
|
||||
let Task { event } = task.data;
|
||||
let (user, channel) = key;
|
||||
|
||||
if let Err(err) = match &event {
|
||||
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
|
||||
AckEvent::AckMessage { id } => db.acknowledge_message(channel, user, id).await,
|
||||
AckEvent::AddMention { ids } => {
|
||||
db.add_mention_to_unread(channel, user, ids).await
|
||||
}
|
||||
} {
|
||||
if let Err(err) = handle_ack_event(&event, &db, &authifier_db, user, channel).await {
|
||||
error!("{err:?} for {event:?}. ({user}, {channel})");
|
||||
} else {
|
||||
info!("User {user} ack in {channel} with {event:?}");
|
||||
|
||||
312
crates/core/database/src/tasks/apple_notifications.rs
Normal file
312
crates/core/database/src/tasks/apple_notifications.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use base64::{
|
||||
engine::{self},
|
||||
Engine as _,
|
||||
};
|
||||
use deadqueue::limited::Queue;
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_a2::{
|
||||
request::{
|
||||
notification::{DefaultAlert, NotificationOptions},
|
||||
payload::{APSAlert, APSSound, PayloadLike, APS},
|
||||
},
|
||||
Client, ClientConfig, Endpoint, Error, ErrorBody, ErrorReason, Priority, PushType, Response,
|
||||
};
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::{Message, PushNotification};
|
||||
|
||||
use crate::Database;
|
||||
|
||||
/// Payload information, before assembly
|
||||
#[derive(Debug)]
|
||||
pub struct ApnPayload {
|
||||
message: Message,
|
||||
url: String,
|
||||
authorAvatar: String,
|
||||
authorDisplayName: String,
|
||||
channelName: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct Payload<'a> {
|
||||
aps: APS<'a>,
|
||||
#[serde(skip_serializing)]
|
||||
options: NotificationOptions<'a>,
|
||||
#[serde(skip_serializing)]
|
||||
device_token: &'a str,
|
||||
|
||||
message: &'a Message,
|
||||
url: &'a str,
|
||||
authorAvatar: &'a str,
|
||||
authorDisplayName: &'a str,
|
||||
channelName: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> PayloadLike for Payload<'a> {
|
||||
fn get_device_token(&self) -> &'a str {
|
||||
self.device_token
|
||||
}
|
||||
fn get_options(&self) -> &NotificationOptions {
|
||||
&self.options
|
||||
}
|
||||
}
|
||||
|
||||
/// Task information
|
||||
#[derive(Debug)]
|
||||
pub struct AlertJob {
|
||||
/// Session Id
|
||||
session_id: String,
|
||||
|
||||
/// Device token
|
||||
device_token: String,
|
||||
|
||||
/// User Id
|
||||
user_id: String,
|
||||
|
||||
/// Title
|
||||
title: String,
|
||||
|
||||
/// Body
|
||||
body: String,
|
||||
|
||||
/// Thread Id
|
||||
thread_id: String,
|
||||
|
||||
/// Category (informs the client what kind of notification is being sent.)
|
||||
category: String,
|
||||
|
||||
/// Payload used by the iOS client to modify the notification
|
||||
custom_payload: ApnPayload,
|
||||
}
|
||||
|
||||
impl AlertJob {
|
||||
fn format_title(notification: &PushNotification) -> String {
|
||||
// ideally this changes depending on context
|
||||
// in a server, it would look like "Sendername, #channelname in servername"
|
||||
// in a group, it would look like "Sendername in groupname"
|
||||
// in a dm it should just be "Sendername".
|
||||
// not sure how feasible all those are given the PushNotification object as it currently stands.
|
||||
format!(
|
||||
"{} in {}",
|
||||
notification.author, notification.message.channel
|
||||
) // TODO: this absolutely needs a channel name
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BadgeJob {
|
||||
/// Session Id
|
||||
session_id: String,
|
||||
|
||||
/// Device token
|
||||
device_token: String,
|
||||
|
||||
/// User Id
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum JobType {
|
||||
Alert(AlertJob),
|
||||
Badge(BadgeJob),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApnJob {
|
||||
job_type: JobType,
|
||||
}
|
||||
|
||||
impl ApnJob {
|
||||
pub fn from_notification(
|
||||
session_id: String,
|
||||
user_id: String,
|
||||
device_token: String,
|
||||
notification: &PushNotification,
|
||||
) -> ApnJob {
|
||||
ApnJob {
|
||||
job_type: JobType::Alert(AlertJob {
|
||||
session_id,
|
||||
device_token,
|
||||
user_id,
|
||||
title: AlertJob::format_title(notification),
|
||||
body: notification.body.to_string(),
|
||||
thread_id: notification.tag.to_string(),
|
||||
category: "ALERT_MESSAGE".to_string(),
|
||||
custom_payload: ApnPayload {
|
||||
message: notification.message.clone(),
|
||||
url: notification.url.clone(),
|
||||
authorAvatar: notification.icon.clone(),
|
||||
authorDisplayName: notification.author.clone(),
|
||||
channelName: "#fetchchannelnamehere".to_string(), // TODO: get actual channel name
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_ack(session_id: String, user_id: String, device_token: String) -> ApnJob {
|
||||
ApnJob {
|
||||
job_type: JobType::Badge(BadgeJob {
|
||||
session_id,
|
||||
device_token,
|
||||
user_id,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AssembledPayload<'a> {
|
||||
Alert(Payload<'a>),
|
||||
Default(revolt_a2::request::payload::Payload<'a>),
|
||||
}
|
||||
|
||||
static Q: Lazy<Queue<ApnJob>> = Lazy::new(|| Queue::new(10_000));
|
||||
|
||||
/// Queue a new task for a worker
|
||||
pub async fn queue(task: ApnJob) {
|
||||
Q.try_push(task).ok();
|
||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||
}
|
||||
|
||||
async fn get_badge_count(db: &Database, user: &str) -> Option<u32> {
|
||||
if let Ok(unreads) = db.fetch_unreads(user).await {
|
||||
let mut mention_count = 0;
|
||||
for channel in unreads {
|
||||
if let Some(mentions) = channel.mentions {
|
||||
mention_count += mentions.len() as u32
|
||||
}
|
||||
}
|
||||
|
||||
return Some(mention_count);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
let config = config().await;
|
||||
if config.api.apn.pkcs8.is_empty()
|
||||
|| config.api.apn.key_id.is_empty()
|
||||
|| config.api.apn.team_id.is_empty()
|
||||
{
|
||||
eprintln!("Missing APN keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
let endpoint = if config.api.apn.sandbox {
|
||||
Endpoint::Sandbox
|
||||
} else {
|
||||
Endpoint::Production
|
||||
};
|
||||
|
||||
let pkcs8 = engine::general_purpose::STANDARD
|
||||
.decode(config.api.apn.pkcs8)
|
||||
.expect("valid `pcks8`");
|
||||
|
||||
let client_config = ClientConfig::new(endpoint);
|
||||
|
||||
let client = Client::token(
|
||||
&mut Cursor::new(pkcs8),
|
||||
config.api.apn.key_id,
|
||||
config.api.apn.team_id,
|
||||
client_config,
|
||||
)
|
||||
.expect("could not create APN client");
|
||||
|
||||
let payload_options = NotificationOptions {
|
||||
apns_id: None,
|
||||
apns_push_type: Some(PushType::Alert),
|
||||
apns_expiration: None,
|
||||
apns_priority: Some(Priority::High),
|
||||
apns_topic: Some("chat.revolt.app"),
|
||||
apns_collapse_id: None,
|
||||
};
|
||||
|
||||
loop {
|
||||
let task = Q.pop().await;
|
||||
let payload: AssembledPayload;
|
||||
|
||||
match task.job_type {
|
||||
JobType::Alert(ref alert) => {
|
||||
payload = AssembledPayload::Alert(Payload {
|
||||
aps: APS {
|
||||
alert: Some(APSAlert::Default(DefaultAlert {
|
||||
title: Some(&alert.title),
|
||||
subtitle: None,
|
||||
body: Some(&alert.body),
|
||||
title_loc_key: None,
|
||||
title_loc_args: None,
|
||||
action_loc_key: None,
|
||||
loc_key: None,
|
||||
loc_args: None,
|
||||
launch_image: None,
|
||||
})),
|
||||
badge: get_badge_count(&db, &alert.user_id).await,
|
||||
sound: Some(APSSound::Sound("default")),
|
||||
thread_id: Some(&alert.thread_id),
|
||||
content_available: None,
|
||||
category: Some(&alert.category),
|
||||
mutable_content: Some(1),
|
||||
url_args: None,
|
||||
},
|
||||
device_token: &alert.device_token,
|
||||
options: payload_options.clone(),
|
||||
message: &alert.custom_payload.message,
|
||||
url: &alert.custom_payload.url,
|
||||
authorAvatar: &alert.custom_payload.authorAvatar,
|
||||
authorDisplayName: &alert.custom_payload.authorDisplayName,
|
||||
channelName: &alert.custom_payload.channelName,
|
||||
});
|
||||
}
|
||||
JobType::Badge(ref alert) => {
|
||||
payload = AssembledPayload::Default(revolt_a2::request::payload::Payload {
|
||||
aps: APS {
|
||||
alert: None,
|
||||
badge: get_badge_count(&db, &alert.user_id).await,
|
||||
sound: None,
|
||||
thread_id: None,
|
||||
content_available: None,
|
||||
category: None,
|
||||
mutable_content: None,
|
||||
url_args: None,
|
||||
},
|
||||
device_token: &alert.device_token,
|
||||
options: payload_options.clone(),
|
||||
data: std::collections::BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let resp = match payload {
|
||||
AssembledPayload::Alert(p) => client.send(p).await,
|
||||
AssembledPayload::Default(p) => client.send(p).await,
|
||||
};
|
||||
//println!("response from APNS: {:?}", resp);
|
||||
|
||||
if let Err(err) = resp {
|
||||
match err {
|
||||
Error::ResponseError(Response {
|
||||
error:
|
||||
Some(ErrorBody {
|
||||
reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered,
|
||||
..
|
||||
}),
|
||||
..
|
||||
}) => {
|
||||
if let Err(err) = db
|
||||
.remove_push_subscription_by_session_id(match task.job_type {
|
||||
JobType::Alert(ref a) => &a.session_id.as_str(),
|
||||
JobType::Badge(ref a) => &a.session_id.as_str(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
}
|
||||
err => {
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
crates/core/database/src/tasks/authifier_relay.rs
Normal file
29
crates/core/database/src/tasks/authifier_relay.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use async_std::channel::{unbounded, Receiver, Sender};
|
||||
use authifier::AuthifierEvent;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::events::client::EventV1;
|
||||
|
||||
static Q: Lazy<(Sender<AuthifierEvent>, Receiver<AuthifierEvent>)> = Lazy::new(|| unbounded());
|
||||
|
||||
/// Get sender
|
||||
pub fn sender() -> Sender<AuthifierEvent> {
|
||||
Q.0.clone()
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker() {
|
||||
loop {
|
||||
let event = Q.1.recv().await.unwrap();
|
||||
match &event {
|
||||
AuthifierEvent::CreateSession { .. } | AuthifierEvent::CreateAccount { .. } => {
|
||||
EventV1::Auth(event).global().await
|
||||
}
|
||||
AuthifierEvent::DeleteSession { user_id, .. }
|
||||
| AuthifierEvent::DeleteAllSessions { user_id, .. } => {
|
||||
let id = user_id.to_string();
|
||||
EventV1::Auth(event).private(id).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,17 +8,22 @@ use std::time::Instant;
|
||||
const WORKER_COUNT: usize = 5;
|
||||
|
||||
pub mod ack;
|
||||
pub mod apple_notifications;
|
||||
pub mod authifier_relay;
|
||||
pub mod last_message_id;
|
||||
pub mod process_embeds;
|
||||
pub mod web_push;
|
||||
|
||||
/// Spawn background workers
|
||||
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
|
||||
pub fn start_workers(db: Database, authifier_db: authifier::Database) {
|
||||
task::spawn(authifier_relay::worker());
|
||||
task::spawn(apple_notifications::worker(db.clone()));
|
||||
|
||||
for _ in 0..WORKER_COUNT {
|
||||
task::spawn(ack::worker(db.clone()));
|
||||
task::spawn(ack::worker(db.clone(), authifier_db.clone()));
|
||||
task::spawn(last_message_id::worker(db.clone()));
|
||||
task::spawn(process_embeds::worker(db.clone()));
|
||||
task::spawn(web_push::worker(authifier_db.clone()));
|
||||
task::spawn(web_push::worker(db.clone(), authifier_db.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ pub async fn worker(db: Database) {
|
||||
let embeds = generate(
|
||||
task.content,
|
||||
&config.hosts.january,
|
||||
config.features.limits.default.message_embeds,
|
||||
config.features.limits.global.message_embeds,
|
||||
semaphore,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use std::collections::HashSet;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use authifier::Database;
|
||||
use authifier::Database as AuthifierDatabase;
|
||||
use base64::{
|
||||
engine::{self},
|
||||
Engine as _,
|
||||
};
|
||||
use deadqueue::limited::Queue;
|
||||
use fcm_v1::auth::{Authenticator, ServiceAccountKey};
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::PushNotification;
|
||||
@@ -16,6 +20,10 @@ use web_push::{
|
||||
WebPushClient, WebPushMessageBuilder,
|
||||
};
|
||||
|
||||
use crate::Database;
|
||||
|
||||
use super::apple_notifications;
|
||||
|
||||
/// Task information
|
||||
#[derive(Debug)]
|
||||
struct PushTask {
|
||||
@@ -48,14 +56,32 @@ pub async fn queue(recipients: Vec<String>, payload: PushNotification) {
|
||||
}
|
||||
|
||||
/// Start a new worker
|
||||
pub async fn worker(db: Database) {
|
||||
pub async fn worker(db: Database, authifier_db: AuthifierDatabase) {
|
||||
let config = config().await;
|
||||
|
||||
let web_push_client = IsahcWebPushClient::new().unwrap();
|
||||
let fcm_client = if config.api.fcm.api_key.is_empty() {
|
||||
let fcm_client = if config.api.fcm.key_type.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(fcm::Client::new())
|
||||
Some(fcm_v1::Client::new(
|
||||
Authenticator::service_account::<&str>(ServiceAccountKey {
|
||||
key_type: Some(config.api.fcm.key_type),
|
||||
project_id: Some(config.api.fcm.project_id.clone()),
|
||||
private_key_id: Some(config.api.fcm.private_key_id),
|
||||
private_key: config.api.fcm.private_key,
|
||||
client_email: config.api.fcm.client_email,
|
||||
client_id: Some(config.api.fcm.client_id),
|
||||
auth_uri: Some(config.api.fcm.auth_uri),
|
||||
token_uri: config.api.fcm.token_uri,
|
||||
auth_provider_x509_cert_url: Some(config.api.fcm.auth_provider_x509_cert_url),
|
||||
client_x509_cert_url: Some(config.api.fcm.client_x509_cert_url),
|
||||
})
|
||||
.await
|
||||
.unwrap(),
|
||||
config.api.fcm.project_id,
|
||||
false,
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
};
|
||||
|
||||
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
|
||||
@@ -65,42 +91,53 @@ pub async fn worker(db: Database) {
|
||||
loop {
|
||||
let task = Q.pop().await;
|
||||
|
||||
if let Ok(sessions) = db.find_sessions_with_subscription(&task.recipients).await {
|
||||
if let Ok(sessions) = authifier_db
|
||||
.find_sessions_with_subscription(&task.recipients)
|
||||
.await
|
||||
{
|
||||
for session in sessions {
|
||||
if let Some(sub) = session.subscription {
|
||||
if sub.endpoint == "fcm" {
|
||||
// Use Firebase Cloud Messaging
|
||||
if let Some(client) = &fcm_client {
|
||||
let PushNotification {
|
||||
author,
|
||||
icon,
|
||||
image: _,
|
||||
body,
|
||||
tag,
|
||||
timestamp: _,
|
||||
url: _,
|
||||
} = &task.payload;
|
||||
let message = fcm_v1::message::Message {
|
||||
token: Some(sub.auth),
|
||||
data: Some(HashMap::from([(
|
||||
"payload".to_owned(),
|
||||
serde_json::Value::String(json!(&task.payload).to_string()),
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut notification = fcm::NotificationBuilder::new();
|
||||
notification.title(author);
|
||||
notification.icon(icon);
|
||||
notification.body(body);
|
||||
notification.tag(tag);
|
||||
// TODO: expand support for fields
|
||||
let notification = notification.finalize();
|
||||
|
||||
let mut message_builder =
|
||||
fcm::MessageBuilder::new(&config.api.fcm.api_key, &sub.auth);
|
||||
message_builder.notification(notification);
|
||||
|
||||
if let Err(err) = client.send(message_builder.finalize()).await {
|
||||
if let Err(err) = client.send(&message).await {
|
||||
error!("Failed to send FCM notification! {:?}", err);
|
||||
|
||||
if let fcm_v1::Error::FCM(fcm_error) = err {
|
||||
if fcm_error.contains("404 (Not Found)") {
|
||||
println!("Unregistering {:?}", session.id);
|
||||
|
||||
if let Err(err) = db
|
||||
.remove_push_subscription_by_session_id(&session.id)
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Sent FCM notification to {:?}.", session.id);
|
||||
}
|
||||
} else {
|
||||
info!("No FCM token was specified!");
|
||||
}
|
||||
} else if sub.endpoint == "apn" {
|
||||
apple_notifications::queue(apple_notifications::ApnJob::from_notification(
|
||||
session.id,
|
||||
session.user_id,
|
||||
sub.auth,
|
||||
&task.payload,
|
||||
))
|
||||
.await;
|
||||
} else {
|
||||
// Use Web Push Standard
|
||||
let subscription = SubscriptionInfo {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use revolt_models::v0::*;
|
||||
use revolt_permissions::{calculate_user_permissions, UserPermission};
|
||||
|
||||
use crate::{util::permissions::DatabasePermissionQuery, Database};
|
||||
use crate::{util::permissions::DatabasePermissionQuery, Database, FileUsedFor};
|
||||
|
||||
impl crate::Bot {
|
||||
pub fn into_public_bot(self, user: crate::User) -> PublicBot {
|
||||
@@ -419,6 +419,10 @@ impl From<File> for crate::File {
|
||||
user_id: value.user_id,
|
||||
server_id: value.server_id,
|
||||
object_id: value.object_id,
|
||||
hash: None,
|
||||
uploaded_at: None,
|
||||
uploaded_id: None,
|
||||
used_for: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -459,26 +463,30 @@ impl From<Metadata> for crate::Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Message> for Message {
|
||||
fn from(value: crate::Message) -> Self {
|
||||
impl crate::Message {
|
||||
pub fn into_model(self, user: Option<User>, member: Option<Member>) -> Message {
|
||||
Message {
|
||||
id: value.id,
|
||||
nonce: value.nonce,
|
||||
channel: value.channel,
|
||||
author: value.author,
|
||||
webhook: value.webhook,
|
||||
content: value.content,
|
||||
system: value.system.map(|system| system.into()),
|
||||
attachments: value
|
||||
id: self.id,
|
||||
nonce: self.nonce,
|
||||
channel: self.channel,
|
||||
author: self.author,
|
||||
user,
|
||||
member,
|
||||
webhook: self.webhook,
|
||||
content: self.content,
|
||||
system: self.system.map(Into::into),
|
||||
attachments: self
|
||||
.attachments
|
||||
.map(|v| v.into_iter().map(|f| f.into()).collect()),
|
||||
edited: value.edited,
|
||||
embeds: value.embeds,
|
||||
mentions: value.mentions,
|
||||
replies: value.replies,
|
||||
reactions: value.reactions,
|
||||
interactions: value.interactions.into(),
|
||||
masquerade: value.masquerade.map(|masq| masq.into()),
|
||||
edited: self.edited,
|
||||
embeds: self.embeds,
|
||||
mentions: self.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(),
|
||||
pinned: self.pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,9 +498,11 @@ impl From<crate::PartialMessage> for PartialMessage {
|
||||
nonce: value.nonce,
|
||||
channel: value.channel,
|
||||
author: value.author,
|
||||
user: None,
|
||||
member: None,
|
||||
webhook: value.webhook,
|
||||
content: value.content,
|
||||
system: value.system.map(|system| system.into()),
|
||||
system: value.system.map(Into::into),
|
||||
attachments: value
|
||||
.attachments
|
||||
.map(|v| v.into_iter().map(|f| f.into()).collect()),
|
||||
@@ -501,8 +511,10 @@ impl From<crate::PartialMessage> for PartialMessage {
|
||||
mentions: value.mentions,
|
||||
replies: value.replies,
|
||||
reactions: value.reactions,
|
||||
interactions: value.interactions.map(|interactions| interactions.into()),
|
||||
masquerade: value.masquerade.map(|masq| masq.into()),
|
||||
interactions: value.interactions.map(Into::into),
|
||||
masquerade: value.masquerade.map(Into::into),
|
||||
flags: value.flags.map(|flags| flags as u32),
|
||||
pinned: value.pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -525,6 +537,8 @@ impl From<crate::SystemMessage> for SystemMessage {
|
||||
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
|
||||
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
|
||||
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
|
||||
crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by },
|
||||
crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -995,26 +1009,31 @@ impl crate::User {
|
||||
vec![]
|
||||
},
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: can_see_profile
|
||||
&& revolt_presence::is_online(&self.id).await
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: if can_see_profile {
|
||||
self.status.map(|status| status.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
profile: if can_see_profile {
|
||||
self.profile.map(|profile| profile.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && revolt_presence::is_online(&self.id).await,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
where
|
||||
P: Into<Option<&'a crate::User>>,
|
||||
@@ -1050,40 +1069,57 @@ impl crate::User {
|
||||
discriminator: self.discriminator,
|
||||
display_name: self.display_name,
|
||||
avatar: self.avatar.map(|file| file.into()),
|
||||
relations: if let Some(crate::User { id, .. }) = perspective {
|
||||
if id == &self.id {
|
||||
self.relations
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|relation| relation.into())
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
relations: vec![],
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: can_see_profile
|
||||
&& is_online
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: if can_see_profile {
|
||||
self.status.map(|status| status.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
profile: if can_see_profile {
|
||||
self.profile.map(|profile| profile.into())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && is_online,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn into_self(self) -> User {
|
||||
/// Convert user object into user model without presence information
|
||||
pub fn into_known_static<'a>(self, is_online: bool) -> User {
|
||||
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,
|
||||
online: is_online
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: self.status.map(|status| status.into()),
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship: RelationshipStatus::None, // events client will populate this from cache
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn into_self(self, force_online: bool) -> User {
|
||||
User {
|
||||
username: self.username,
|
||||
discriminator: self.discriminator,
|
||||
@@ -1099,13 +1135,19 @@ impl crate::User {
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
online: (force_online || revolt_presence::is_online(&self.id).await)
|
||||
&& !matches!(
|
||||
self.status,
|
||||
Some(crate::UserStatus {
|
||||
presence: Some(crate::Presence::Invisible),
|
||||
..
|
||||
})
|
||||
),
|
||||
status: self.status.map(|status| status.into()),
|
||||
profile: self.profile.map(|profile| profile.into()),
|
||||
flags: self.flags.unwrap_or_default() as u32,
|
||||
privileged: self.privileged,
|
||||
bot: self.bot.map(|bot| bot.into()),
|
||||
relationship: RelationshipStatus::User,
|
||||
online: revolt_presence::is_online(&self.id).await,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
@@ -1129,10 +1171,11 @@ impl From<User> for crate::User {
|
||||
relations: None,
|
||||
badges: Some(value.badges as i32),
|
||||
status: value.status.map(Into::into),
|
||||
profile: value.profile.map(Into::into),
|
||||
profile: None,
|
||||
flags: Some(value.flags as i32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(Into::into),
|
||||
suspended_until: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1152,7 +1195,6 @@ impl From<crate::PartialUser> for PartialUser {
|
||||
}),
|
||||
badges: value.badges.map(|badges| badges as u32),
|
||||
status: value.status.map(|status| status.into()),
|
||||
profile: value.profile.map(|profile| profile.into()),
|
||||
flags: value.flags.map(|flags| flags as u32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(|bot| bot.into()),
|
||||
@@ -1171,6 +1213,9 @@ impl From<FieldsUser> for crate::FieldsUser {
|
||||
FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent,
|
||||
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
|
||||
FieldsUser::StatusText => crate::FieldsUser::StatusText,
|
||||
FieldsUser::DisplayName => crate::FieldsUser::DisplayName,
|
||||
|
||||
FieldsUser::Internal => crate::FieldsUser::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1183,6 +1228,10 @@ impl From<crate::FieldsUser> for FieldsUser {
|
||||
crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent,
|
||||
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
|
||||
crate::FieldsUser::StatusText => FieldsUser::StatusText,
|
||||
crate::FieldsUser::DisplayName => FieldsUser::DisplayName,
|
||||
|
||||
crate::FieldsUser::Suspension => FieldsUser::Internal,
|
||||
crate::FieldsUser::None => FieldsUser::Internal,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1285,3 +1334,18 @@ impl From<BotInformation> for crate::BotInformation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsMessage> for FieldsMessage {
|
||||
fn from(value: crate::FieldsMessage) -> Self {
|
||||
match value {
|
||||
crate::FieldsMessage::Pinned => FieldsMessage::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<FieldsMessage> for crate::FieldsMessage {
|
||||
fn from(value: FieldsMessage) -> Self {
|
||||
match value {
|
||||
FieldsMessage::Pinned => crate::FieldsMessage::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
|
||||
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
|
||||
|
||||
impl IdempotencyKey {
|
||||
pub fn unchecked_from_string(key: String) -> Self {
|
||||
Self { key }
|
||||
}
|
||||
|
||||
// Backwards compatibility.
|
||||
// Issue #109
|
||||
pub async fn consume_nonce(&mut self, v: Option<String>) -> Result<()> {
|
||||
|
||||
@@ -53,6 +53,12 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
|
||||
if let Some(other_user) = &self.user {
|
||||
if self.perspective.id == other_user.id {
|
||||
return RelationshipStatus::User;
|
||||
} else if let Some(bot) = &other_user.bot {
|
||||
// For the purposes of permissions checks,
|
||||
// assume owner is the same as bot
|
||||
if self.perspective.id == bot.owner {
|
||||
return RelationshipStatus::User;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(relations) = &self.perspective.relations {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use revolt_result::Result;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use rocket::request::FromParam;
|
||||
@@ -44,9 +46,27 @@ impl Reference {
|
||||
db.fetch_channel(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch invite from Ref
|
||||
/// Fetch invite from Ref or create invite to server if discoverable
|
||||
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
||||
db.fetch_invite(&self.id).await
|
||||
if ulid::Ulid::from_str(&self.id).is_ok() {
|
||||
let server = self.as_server(db).await?;
|
||||
if !server.discoverable {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
Ok(Invite::Server {
|
||||
code: self.id.to_string(),
|
||||
server: server.id,
|
||||
creator: server.owner,
|
||||
channel: server
|
||||
.channels
|
||||
.into_iter()
|
||||
.next()
|
||||
.ok_or(create_error!(NotFound))?,
|
||||
})
|
||||
} else {
|
||||
db.fetch_invite(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch message from Ref
|
||||
|
||||
554
crates/core/database/templates/deletion.html
Normal file
554
crates/core/database/templates/deletion.html
Normal file
@@ -0,0 +1,554 @@
|
||||
<!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" />
|
||||
<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"
|
||||
>
|
||||
 
|
||||
</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
|
||||
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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
line-height: 40px;
|
||||
font-size: 40px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
"
|
||||
align="left"
|
||||
width="100%"
|
||||
height="40"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
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>
|
||||
</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>
|
||||
<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>
|
||||
</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;
|
||||
"
|
||||
>Confirm</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"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
 
|
||||
</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>
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="text-muted text-center my-6">
|
||||
This email is intended for {{email}}<br />
|
||||
Sent from Revolt<br />
|
||||
Made in the EU
|
||||
Made in Europe
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -4,4 +4,4 @@ Please navigate to: {{url}}
|
||||
|
||||
This email is intended for {{email}}
|
||||
Sent by Revolt
|
||||
Made in the EU
|
||||
Made in Europe
|
||||
552
crates/core/database/templates/reset.html
Normal file
552
crates/core/database/templates/reset.html
Normal file
@@ -0,0 +1,552 @@
|
||||
<!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" />
|
||||
<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"
|
||||
>
|
||||
 
|
||||
</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
|
||||
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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
line-height: 40px;
|
||||
font-size: 40px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
"
|
||||
align="left"
|
||||
width="100%"
|
||||
height="40"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
 
|
||||
</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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
line-height: 16px;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
align="left"
|
||||
width="100%"
|
||||
height="16"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
 
|
||||
</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>
|
||||
@@ -19,7 +19,7 @@
|
||||
<div class="text-muted text-center my-6">
|
||||
This email is intended for {{email}}<br />
|
||||
Sent from Revolt<br />
|
||||
Made in the EU
|
||||
Made in Europe
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -4,4 +4,4 @@ Please navigate to: {{url}}
|
||||
|
||||
This email is intended for {{email}}
|
||||
Sent by Revolt
|
||||
Made in the EU
|
||||
Made in Europe
|
||||
625
crates/core/database/templates/suspension.html
Normal file
625
crates/core/database/templates/suspension.html
Normal file
@@ -0,0 +1,625 @@
|
||||
<!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-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"
|
||||
>
|
||||
 
|
||||
</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
|
||||
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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
line-height: 40px;
|
||||
font-size: 40px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
"
|
||||
align="left"
|
||||
width="100%"
|
||||
height="40"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
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>
|
||||
</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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
line-height: 16px;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
align="left"
|
||||
width="100%"
|
||||
height="16"
|
||||
>
|
||||
 
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ul class="">
|
||||
{{list}}
|
||||
</ul>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</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>
|
||||
</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"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
 
|
||||
</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>
|
||||
38
crates/core/database/templates/suspension.original.html
Normal file
38
crates/core/database/templates/suspension.original.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<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">Account Suspended</h1>
|
||||
<p>Your account has been suspended, for one or more reasons:</p>
|
||||
<ul>
|
||||
{{list}}
|
||||
</ul>
|
||||
<p style="display:{{duration_display}}">
|
||||
You will be able to use your account again in {{duration}} days.
|
||||
</p>
|
||||
<p>
|
||||
Further violations may result in a permanent ban depending on
|
||||
severity, please abide by the
|
||||
<a href="https://revolt.chat/aup">Acceptable Usage Policy</a>.
|
||||
</p>
|
||||
<p>Ban evasion is prohibited and will be dealt with accordingly.</p>
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
12
crates/core/database/templates/suspension.txt
Normal file
12
crates/core/database/templates/suspension.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
Your account has been suspended, for one or more reasons:
|
||||
{{list}}
|
||||
|
||||
You will be able to use your account again in {{duration}} days.
|
||||
|
||||
Further violations may result in a permanent ban depending on severity, please abide by the Acceptable Usage Policy (https://revolt.chat/aup).
|
||||
|
||||
Ban evasion is prohibited and will be dealt with accordingly.
|
||||
|
||||
This email is intended for {{email}}
|
||||
Sent by Revolt
|
||||
Made in Europe
|
||||
552
crates/core/database/templates/verify.html
Normal file
552
crates/core/database/templates/verify.html
Normal file
@@ -0,0 +1,552 @@
|
||||
<!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" />
|
||||
<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"
|
||||
>
|
||||
 
|
||||
</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
|
||||
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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
line-height: 40px;
|
||||
font-size: 40px;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
"
|
||||
align="left"
|
||||
width="100%"
|
||||
height="40"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
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>
|
||||
</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%"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
line-height: 16px;
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
"
|
||||
align="left"
|
||||
width="100%"
|
||||
height="16"
|
||||
>
|
||||
 
|
||||
</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;
|
||||
"
|
||||
>Confirm</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"
|
||||
>
|
||||
 
|
||||
</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"
|
||||
>
|
||||
 
|
||||
</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>
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="text-muted text-center my-6">
|
||||
This email is intended for {{email}}<br />
|
||||
Sent from Revolt<br />
|
||||
Made in the EU
|
||||
Made in Europe
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -5,4 +5,4 @@ Please navigate to: {{url}}
|
||||
|
||||
This email is intended for {{email}}
|
||||
Sent by Revolt
|
||||
Made in the EU
|
||||
Made in Europe
|
||||
18
crates/core/files/Cargo.toml
Normal file
18
crates/core/files/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "revolt-files"
|
||||
version = "0.7.16"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: S3 and encryption subroutines"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
aes-gcm = "0.10.3"
|
||||
typenum = "1.17.0"
|
||||
|
||||
aws-config = "1.5.5"
|
||||
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
|
||||
|
||||
revolt-config = { version = "0.7.16", path = "../config" }
|
||||
revolt-result = { version = "0.7.16", path = "../result" }
|
||||
80
crates/core/files/src/lib.rs
Normal file
80
crates/core/files/src/lib.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::io::Write;
|
||||
|
||||
use aes_gcm::{aead::AeadMutInPlace, Aes256Gcm, Key, KeyInit, Nonce};
|
||||
use revolt_config::{config, FilesS3};
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use aws_sdk_s3::{
|
||||
config::{Credentials, Region},
|
||||
Client, Config,
|
||||
};
|
||||
|
||||
use base64::prelude::*;
|
||||
|
||||
pub static AUTHENTICATION_TAG_SIZE_BYTES: usize = 16;
|
||||
|
||||
/// Create an S3 client
|
||||
pub fn create_client(s3_config: FilesS3) -> Client {
|
||||
let provider_name = "my-creds";
|
||||
let creds = Credentials::new(
|
||||
s3_config.access_key_id,
|
||||
s3_config.secret_access_key,
|
||||
None,
|
||||
None,
|
||||
provider_name,
|
||||
);
|
||||
|
||||
let config = Config::builder()
|
||||
.region(Region::new(s3_config.region))
|
||||
.endpoint_url(s3_config.endpoint)
|
||||
.credentials_provider(creds)
|
||||
.build();
|
||||
|
||||
Client::from_conf(config)
|
||||
}
|
||||
|
||||
/// Create an AES-256-GCM cipher
|
||||
pub fn create_cipher(key: &str) -> Aes256Gcm {
|
||||
let key = &BASE64_STANDARD.decode(key).unwrap()[..];
|
||||
let key: &Key<Aes256Gcm> = key.into();
|
||||
Aes256Gcm::new(key)
|
||||
}
|
||||
|
||||
/// Fetch a file from S3 (and decrypt it)
|
||||
pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<Vec<u8>> {
|
||||
let config = config().await;
|
||||
let client = create_client(config.files.s3);
|
||||
|
||||
// Send a request for the file
|
||||
let mut obj = client
|
||||
.get_object()
|
||||
.bucket(bucket_id)
|
||||
.key(path)
|
||||
.send()
|
||||
.await
|
||||
.inspect_err(|err| {
|
||||
revolt_config::capture_error(err);
|
||||
})
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
// Read the file from remote
|
||||
let mut buf = vec![];
|
||||
while let Some(bytes) = obj.body.next().await {
|
||||
let data = bytes.map_err(|_| create_error!(InternalError))?;
|
||||
buf.write_all(&data)
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
// is there a more efficient way to do this?
|
||||
// we just want the Vec<u8>
|
||||
}
|
||||
|
||||
// Recover nonce as bytes
|
||||
let nonce = &BASE64_STANDARD.decode(nonce).unwrap()[..];
|
||||
let nonce: &Nonce<typenum::consts::U12> = nonce.into();
|
||||
|
||||
// Decrypt the file
|
||||
create_cipher(&config.files.encryption_key)
|
||||
.decrypt_in_place(nonce, b"", &mut buf)
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.7.1"
|
||||
version = "0.7.16"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: API Models"
|
||||
|
||||
@@ -11,16 +11,17 @@ description = "Revolt Backend: API Models"
|
||||
[features]
|
||||
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
|
||||
schemas = ["dep:schemars", "revolt-permissions/schemas"]
|
||||
utoipa = ["dep:utoipa"]
|
||||
validator = ["dep:validator"]
|
||||
rocket = ["dep:rocket"]
|
||||
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
|
||||
partials = ["dep:revolt_optional_struct", "serde", "schemas", "utoipa"]
|
||||
|
||||
default = ["serde", "partials", "rocket"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.7.1", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.1", path = "../permissions" }
|
||||
revolt-config = { version = "0.7.16", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.16", path = "../permissions" }
|
||||
|
||||
# Utility
|
||||
regex = "1"
|
||||
@@ -37,6 +38,7 @@ iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
|
||||
|
||||
# Spec Generation
|
||||
schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] }
|
||||
utoipa = { version = "4.2.3", optional = true }
|
||||
|
||||
# Validation
|
||||
validator = { version = "0.16.0", optional = true, features = ["derive"] }
|
||||
|
||||
9
crates/core/models/LICENSE
Normal file
9
crates/core/models/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -6,6 +6,10 @@ extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate schemars;
|
||||
|
||||
#[cfg(feature = "utoipa")]
|
||||
#[macro_use]
|
||||
extern crate utoipa;
|
||||
|
||||
#[cfg(feature = "partials")]
|
||||
#[macro_use]
|
||||
extern crate revolt_optional_struct;
|
||||
@@ -18,6 +22,7 @@ macro_rules! auto_derived {
|
||||
$(
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
$item
|
||||
)+
|
||||
@@ -66,3 +71,8 @@ pub fn if_false(t: &bool) -> bool {
|
||||
pub fn if_zero_u32(t: &u32) -> bool {
|
||||
t == &0
|
||||
}
|
||||
|
||||
/// Utility function to check if an option doesnt contain true
|
||||
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||
t != &Some(true)
|
||||
}
|
||||
|
||||
@@ -162,4 +162,11 @@ auto_derived!(
|
||||
/// User objects
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
/// Bot with user response
|
||||
pub struct BotWithUserResponse {
|
||||
#[serde(flatten)]
|
||||
pub bot: Bot,
|
||||
pub user: User,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::{Channel, File, Server, User};
|
||||
|
||||
auto_derived!(
|
||||
/// Invite
|
||||
#[serde(tag = "type")]
|
||||
pub enum Invite {
|
||||
/// Invite to a specific server channel
|
||||
Server {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user