forked from jmug/stoatchat
Compare commits
56 Commits
20240212-1
...
20240611-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
962c7d62c7 | ||
|
|
d677716f93 | ||
|
|
8099310f89 | ||
|
|
5c40f66010 | ||
|
|
8e7dd21bce | ||
|
|
4868205df4 | ||
|
|
96fb0eecca | ||
|
|
2cb20618da | ||
|
|
92e948aabc | ||
|
|
cb4435d700 | ||
|
|
4c6e78e1a5 | ||
|
|
da9a91e05f | ||
|
|
506968634e | ||
|
|
a5d0cdf0dd | ||
|
|
c59aad43c8 | ||
|
|
19e72babc9 | ||
|
|
bfcdb13d6d | ||
|
|
878c1a83ad | ||
|
|
471e0f55e4 | ||
|
|
08127218ce | ||
|
|
b69202c222 | ||
|
|
b3494b12fc | ||
|
|
7703475868 | ||
|
|
4be3bdc4c3 | ||
|
|
3e26e7e89d | ||
|
|
13b95d383e | ||
|
|
3c76005c33 | ||
|
|
3065e153d5 | ||
|
|
a1eeedabb9 | ||
|
|
0fc59b8e2a | ||
|
|
265d3ba8ef | ||
|
|
20d398d02c | ||
|
|
6bc74749d2 | ||
|
|
301676fb54 | ||
|
|
6b488f347e | ||
|
|
1a8e43d280 | ||
|
|
acb4190f70 | ||
|
|
9e42567050 | ||
|
|
78fb6e1982 | ||
|
|
9de963fb56 | ||
|
|
03340a2b5a | ||
|
|
b2d3344ddd | ||
|
|
49bb235938 | ||
|
|
4e7049d51e | ||
|
|
2de7598f8d | ||
|
|
f6a565385e | ||
|
|
569bd1d5e1 | ||
|
|
aca1fe6dff | ||
|
|
aded2e3239 | ||
|
|
54a4eff623 | ||
|
|
50c36dcefd | ||
|
|
f16c275bcb | ||
|
|
74ff072199 | ||
|
|
52431bd1dd | ||
|
|
fa67e87467 | ||
|
|
8f9b18444a |
25
.env.example
25
.env.example
@@ -1,5 +1,7 @@
|
||||
# MongoDB URI
|
||||
MONGODB=mongodb://localhost
|
||||
AUTUMN_MONGO_URI=mongodb://database
|
||||
REDIS_URI=redis://localhost/
|
||||
|
||||
# URL to where the Revolt app is publicly accessible
|
||||
REVOLT_APP_URL=http://local.revolt.chat:5000
|
||||
@@ -80,3 +82,26 @@ REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nf
|
||||
##
|
||||
|
||||
# VOSO_MANAGE_TOKEN=CHANGEME
|
||||
|
||||
|
||||
##
|
||||
## Autumn configuration
|
||||
##
|
||||
|
||||
# S3 Region
|
||||
AUTUMN_S3_REGION=minio
|
||||
|
||||
# S3 Endpoint
|
||||
AUTUMN_S3_ENDPOINT=http://minio:9000
|
||||
|
||||
# MinIO Root User
|
||||
MINIO_ROOT_USER=minioautumn
|
||||
|
||||
# MinIO Root Password
|
||||
MINIO_ROOT_PASSWORD=minioautumn
|
||||
|
||||
# AWS Access Key ID
|
||||
AWS_ACCESS_KEY_ID=minioautumn
|
||||
|
||||
# AWS Secret Key
|
||||
AWS_SECRET_ACCESS_KEY=minioautumn
|
||||
|
||||
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
@@ -3,19 +3,20 @@ name: Docker Test & Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "main"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "main"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
20
.github/workflows/rust.yaml
vendored
20
.github/workflows/rust.yaml
vendored
@@ -2,8 +2,16 @@ name: Rust build, test, and generate specification
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/rust.yaml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -53,20 +61,20 @@ jobs:
|
||||
cargo nextest run
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
|
||||
|
||||
- name: Wait for API to go up
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: nev7n/wait_for_response@v1
|
||||
with:
|
||||
url: "http://localhost:8000/"
|
||||
|
||||
- name: Checkout API repository
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: revoltchat/api
|
||||
@@ -74,11 +82,11 @@ jobs:
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
with:
|
||||
cwd: "api"
|
||||
|
||||
4
.github/workflows/triage_pr.yml
vendored
4
.github/workflows/triage_pr.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
projectV2(number: 5) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
|
||||
528
Cargo.lock
generated
528
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
|
||||
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
|
||||
|
||||
[patch.crates-io]
|
||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||
|
||||
@@ -19,7 +19,6 @@ RUN sh /tmp/build-image-layer.sh tools
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/quark/Cargo.toml ./crates/quark/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
|
||||
@@ -15,7 +15,6 @@ COPY scripts/build-image-layer.sh /tmp/
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/quark/Cargo.toml ./crates/quark/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
|
||||
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/>
|
||||
|
||||
156
README.md
156
README.md
@@ -2,34 +2,150 @@
|
||||
|
||||
This is a monorepo for the Revolt backend.
|
||||
|
||||
| Crate | Path | Description |
|
||||
| ---------------- | ---------------------------------------------- | ------------------------------------ |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
|
||||
| `quark` | [crates/quark](crates/quark) | Models and logic |
|
||||
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer |
|
||||
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer |
|
||||
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |-->
|
||||
| Crate | Path | Description |
|
||||
| ------------------ | -------------------------------------------------- | ----------------------------- |
|
||||
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |
|
||||
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |
|
||||
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |
|
||||
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |
|
||||
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |
|
||||
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
|
||||
|
||||
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
||||
|
||||
## Resources
|
||||
|
||||
### Revolt
|
||||
|
||||
- [Revolt Project Board](https://github.com/revoltchat/revolt/discussions) (Submit feature requests here)
|
||||
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
|
||||
- [Contribution Guide](https://developers.revolt.chat/contributing)
|
||||
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
Rust 1.70 or higher.
|
||||
|
||||
## Contributing
|
||||
## Development Guide
|
||||
|
||||
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
|
||||
Please note that a pull request should only take care of one issue so that we can review it quickly.
|
||||
Before getting started, you'll want to install:
|
||||
|
||||
- Rust toolchain (rustup recommended)
|
||||
- Docker
|
||||
- Git
|
||||
- mold (optional, faster compilation)
|
||||
|
||||
> A **default.nix** is available for Nix users!
|
||||
> Just run `nix-shell` and continue.
|
||||
|
||||
Now you can clone and build the project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/revoltchat/backend revolt-backend
|
||||
cd revolt-backend
|
||||
cargo build
|
||||
```
|
||||
|
||||
If you want to run the API and event servers:
|
||||
|
||||
```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...
|
||||
```
|
||||
|
||||
You may want to copy the following configuration:
|
||||
|
||||
```toml
|
||||
# Revolt.toml
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost"
|
||||
|
||||
[hosts]
|
||||
app = "http://local.revolt.chat"
|
||||
api = "http://local.revolt.chat:8000"
|
||||
events = "ws://local.revolt.chat:9000"
|
||||
autumn = "http://local.revolt.chat:3000"
|
||||
january = "http://local.revolt.chat:7000"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
```
|
||||
|
||||
Then continue:
|
||||
|
||||
```bash
|
||||
# start other necessary services
|
||||
docker compose up -d
|
||||
|
||||
# run the API server
|
||||
cargo run --bin revolt-delta
|
||||
# run the events server
|
||||
cargo run --bin revolt-bonfire
|
||||
|
||||
# hint:
|
||||
# mold -run <cargo build, cargo run, etc...>
|
||||
```
|
||||
|
||||
You can start a web client by doing the following:
|
||||
|
||||
```bash
|
||||
# if you do not have yarn yet and have a modern Node.js:
|
||||
corepack enable
|
||||
|
||||
# clone the web client and run it:
|
||||
git clone --recursive https://github.com/revoltchat/revite
|
||||
cd revite
|
||||
yarn
|
||||
yarn build:deps
|
||||
yarn dev --port 3001
|
||||
```
|
||||
|
||||
Then go to https://local.revolt.chat:3001
|
||||
|
||||
## Deployment Guide
|
||||
|
||||
### Cutting new crate releases
|
||||
|
||||
Begin by bumping crate versions:
|
||||
|
||||
```bash
|
||||
just patch # 0.0.X
|
||||
just minor # 0.X.0
|
||||
just major # X.0.0
|
||||
```
|
||||
|
||||
Then commit the changes to package files.
|
||||
|
||||
Proceed to publish all the new crates:
|
||||
|
||||
```bash
|
||||
just publish
|
||||
```
|
||||
|
||||
### Cutting new binary releases
|
||||
|
||||
Tag and push a new release by running:
|
||||
|
||||
```bash
|
||||
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=MONGOBD 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). Please check individual crates for further license information.
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
|
||||
|
||||
**Individual crates may supply their own licenses!**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.6.13"
|
||||
version = "0.7.4"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -9,14 +9,15 @@ edition = "2021"
|
||||
[dependencies]
|
||||
# util
|
||||
log = "*"
|
||||
sentry = "0.31.5"
|
||||
lru = "0.7.6"
|
||||
ulid = "0.5.0"
|
||||
once_cell = "1.9.0"
|
||||
redis-kiss = "0.1.4"
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# serde
|
||||
bincode = "1.3.3"
|
||||
serde_json = "1.0.79"
|
||||
@@ -33,8 +34,13 @@ 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.4", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
sentry = "0.31.5"
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use revolt_quark::{Error, Result};
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Enumeration of supported protocol formats
|
||||
@@ -37,16 +37,16 @@ impl ProtocolConfiguration {
|
||||
match self.format {
|
||||
ProtocolFormat::Json => {
|
||||
if let Message::Text(text) = msg {
|
||||
serde_json::from_str(text).map_err(|_| Error::InternalError)
|
||||
serde_json::from_str(text).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
ProtocolFormat::Msgpack => {
|
||||
if let Message::Binary(buf) = msg {
|
||||
rmp_serde::from_slice(buf).map_err(|_| Error::InternalError)
|
||||
rmp_serde::from_slice(buf).map_err(|_| create_error!(InternalError))
|
||||
} else {
|
||||
Err(Error::InternalError)
|
||||
Err(create_error!(InternalError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use revolt_quark::{Database, DatabaseInfo};
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
static DBCONN: OnceCell<Database> = OnceCell::new();
|
||||
|
||||
@@ -10,7 +10,9 @@ pub async fn connect() {
|
||||
.await
|
||||
.expect("Failed to connect to the database.");
|
||||
|
||||
DBCONN.set(database).expect("Setting `Database`");
|
||||
if DBCONN.set(database).is_err() {
|
||||
panic!("couldn't set database")
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the current database.
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{
|
||||
get_relationship,
|
||||
models::{
|
||||
server_member::FieldsMember,
|
||||
user::{PartialUser, Presence, RelationshipStatus},
|
||||
Channel, Member, User,
|
||||
},
|
||||
perms, Database, Permission, Result,
|
||||
use revolt_database::{
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
|
||||
MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::{
|
||||
client::EventV1,
|
||||
state::{Cache, State},
|
||||
};
|
||||
use super::state::{Cache, State};
|
||||
|
||||
/// Cache Manager
|
||||
impl Cache {
|
||||
@@ -25,20 +19,22 @@ impl Cache {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
let member = self.members.get(server);
|
||||
let server = self.servers.get(server);
|
||||
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
||||
let mut query =
|
||||
DatabasePermissionQuery::new(db, self.users.get(&self.user_id).unwrap())
|
||||
.channel(channel);
|
||||
// let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
|
||||
|
||||
if let Some(member) = member {
|
||||
perms.member.set_ref(member);
|
||||
query = query.member(member);
|
||||
}
|
||||
|
||||
if let Some(server) = server {
|
||||
perms.server.set_ref(server);
|
||||
query = query.server(server);
|
||||
}
|
||||
|
||||
perms
|
||||
.has_permission(db, Permission::ViewChannel)
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.has_channel_permission(ChannelPermission::ViewChannel)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
@@ -63,7 +59,7 @@ impl Cache {
|
||||
/// Check whether we can subscribe to another user
|
||||
pub fn can_subscribe_to_user(&self, user_id: &str) -> bool {
|
||||
if let Some(user) = self.users.get(&self.user_id) {
|
||||
match get_relationship(user, user_id) {
|
||||
match user.relationship_with(user_id) {
|
||||
RelationshipStatus::Friend
|
||||
| RelationshipStatus::Incoming
|
||||
| RelationshipStatus::Outgoing
|
||||
@@ -95,7 +91,7 @@ impl Cache {
|
||||
impl State {
|
||||
/// Generate a Ready packet for the current user
|
||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
||||
let mut user = self.clone_user();
|
||||
let user = self.clone_user();
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
@@ -141,7 +137,6 @@ impl State {
|
||||
|
||||
// Fetch presence data for known users.
|
||||
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
|
||||
user.online = Some(true);
|
||||
|
||||
// Fetch user data.
|
||||
let users = db
|
||||
@@ -154,15 +149,14 @@ impl State {
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
let emojis = db
|
||||
.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
.iter()
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
.await?;
|
||||
|
||||
// Copy data into local state cache.
|
||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||
@@ -176,40 +170,39 @@ impl State {
|
||||
.collect();
|
||||
|
||||
// Make all users appear from our perspective.
|
||||
let mut users: Vec<User> = users
|
||||
let mut users: Vec<v0::User> = users
|
||||
.into_iter()
|
||||
.map(|mut x| {
|
||||
x.online = Some(online_ids.contains(&x.id));
|
||||
x.with_relationship(&user)
|
||||
.map(|other_user| {
|
||||
let is_online = online_ids.contains(&other_user.id);
|
||||
other_user.into_known(&user, is_online)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Make sure we see our own user correctly.
|
||||
user.relationship = Some(RelationshipStatus::User);
|
||||
users.push(user.foreign());
|
||||
users.push(user.into_self().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;
|
||||
}
|
||||
|
||||
for channel in &channels {
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
}
|
||||
|
||||
Ok(EventV1::Ready {
|
||||
users,
|
||||
servers,
|
||||
channels,
|
||||
members,
|
||||
emojis,
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -243,11 +236,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 });
|
||||
@@ -270,8 +263,8 @@ impl State {
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
|
||||
self.insert_subscription(channel.id().to_string());
|
||||
bulk_events.push(EventV1::ChannelCreate(channel));
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
bulk_events.push(EventV1::ChannelCreate(channel.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,7 +289,7 @@ impl State {
|
||||
} {
|
||||
let event = EventV1::UserUpdate {
|
||||
id: self.cache.user_id.clone(),
|
||||
data: PartialUser {
|
||||
data: v0::PartialUser {
|
||||
online: Some(target),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -343,8 +336,8 @@ impl State {
|
||||
match event {
|
||||
EventV1::ChannelCreate(channel) => {
|
||||
let id = channel.id().to_string();
|
||||
self.insert_subscription(id.clone());
|
||||
self.cache.channels.insert(id, channel.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
self.cache.channels.insert(id, channel.clone().into());
|
||||
}
|
||||
EventV1::ChannelUpdate {
|
||||
id, data, clear, ..
|
||||
@@ -357,10 +350,10 @@ impl State {
|
||||
|
||||
if let Some(channel) = self.cache.channels.get_mut(id) {
|
||||
for field in clear {
|
||||
channel.remove(field);
|
||||
channel.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
channel.apply_options(data.clone());
|
||||
channel.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if !self.cache.channels.contains_key(id) {
|
||||
@@ -374,7 +367,7 @@ impl State {
|
||||
if could_view != can_view {
|
||||
if can_view {
|
||||
queue_add = Some(id.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone());
|
||||
*event = EventV1::ChannelCreate(channel.clone().into());
|
||||
} else {
|
||||
queue_remove = Some(id.clone());
|
||||
*event = EventV1::ChannelDelete { id: id.clone() };
|
||||
@@ -383,17 +376,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,15 +396,21 @@ impl State {
|
||||
channels,
|
||||
emojis: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone());
|
||||
self.cache.servers.insert(id.clone(), server.clone());
|
||||
let member = Member::new(id.clone(), self.cache.user_id.clone());
|
||||
self.insert_subscription(id.clone()).await;
|
||||
self.cache.servers.insert(id.clone(), server.clone().into());
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.clone(),
|
||||
user: self.cache.user_id.clone(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
self.cache.members.insert(id.clone(), member);
|
||||
|
||||
for channel in channels {
|
||||
self.cache
|
||||
.channels
|
||||
.insert(channel.id().to_string(), channel.clone());
|
||||
.insert(channel.id().to_string(), channel.clone().into());
|
||||
}
|
||||
|
||||
queue_server = Some(id.clone());
|
||||
@@ -421,10 +420,10 @@ impl State {
|
||||
} => {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
for field in clear {
|
||||
server.remove(field);
|
||||
server.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
server.apply_options(data.clone());
|
||||
server.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.default_permissions.is_some() {
|
||||
@@ -436,11 +435,11 @@ impl State {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -448,11 +447,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);
|
||||
}
|
||||
}
|
||||
@@ -462,13 +461,13 @@ impl State {
|
||||
if id.user == self.cache.user_id {
|
||||
if let Some(member) = self.cache.members.get_mut(&id.server) {
|
||||
for field in &clear.clone() {
|
||||
member.remove(field);
|
||||
member.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
member.apply_options(data.clone());
|
||||
member.apply_options(data.clone().into());
|
||||
}
|
||||
|
||||
if data.roles.is_some() || clear.contains(&FieldsMember::Roles) {
|
||||
if data.roles.is_some() || clear.contains(&v0::FieldsMember::Roles) {
|
||||
queue_server = Some(id.server.clone());
|
||||
}
|
||||
}
|
||||
@@ -483,10 +482,10 @@ impl State {
|
||||
if let Some(server) = self.cache.servers.get_mut(id) {
|
||||
if let Some(role) = server.roles.get_mut(role_id) {
|
||||
for field in &clear.clone() {
|
||||
role.remove(field);
|
||||
role.remove_field(&field.clone().into());
|
||||
}
|
||||
|
||||
role.apply_options(data.clone());
|
||||
role.apply_options(data.clone().into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,12 +521,12 @@ impl State {
|
||||
*event_id = None;
|
||||
}
|
||||
EventV1::UserRelationship { id, user, .. } => {
|
||||
self.cache.users.insert(id.clone(), user.clone());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,49 +540,13 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
/// Publish helper wrapper
|
||||
pub async fn p(self, channel: String) {
|
||||
#[cfg(not(debug_assertions))]
|
||||
redis_kiss::p(channel, self).await;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
info!("Publishing event to {channel}: {self:?}");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
redis_kiss::publish(channel, self).await.unwrap();
|
||||
}
|
||||
|
||||
/// Publish user event
|
||||
pub async fn p_user(self, id: String, db: &Database) {
|
||||
self.clone().p(id.clone()).await;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Publish private event
|
||||
pub async fn private(self, id: String) {
|
||||
self.p(format!("{id}!")).await;
|
||||
}
|
||||
|
||||
/// Publish internal global event
|
||||
pub async fn global(self) {
|
||||
self.p("global".to_string()).await;
|
||||
}
|
||||
}
|
||||
2
crates/bonfire/src/events/mod.rs
Normal file
2
crates/bonfire/src/events/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod r#impl;
|
||||
pub mod state;
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use async_std::sync::RwLock;
|
||||
use lru::LruCache;
|
||||
|
||||
use crate::models::{Channel, Member, Server, User};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
|
||||
/// Enumeration representing some change in subscriptions
|
||||
pub enum SubscriptionStateChange {
|
||||
@@ -59,14 +62,15 @@ 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>>>,
|
||||
}
|
||||
|
||||
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());
|
||||
@@ -81,22 +85,24 @@ impl State {
|
||||
|
||||
State {
|
||||
cache,
|
||||
subscribed,
|
||||
subscribed: Arc::new(RwLock::new(subscribed)),
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,20 +114,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;
|
||||
}
|
||||
|
||||
@@ -138,12 +140,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;
|
||||
}
|
||||
|
||||
@@ -160,6 +163,6 @@ impl State {
|
||||
SubscriptionStateChange::Reset => panic!("Should not remove during a reset!"),
|
||||
}
|
||||
|
||||
self.subscribed.remove(subscription);
|
||||
subscribed.remove(subscription);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use revolt_presence::clear_region;
|
||||
extern crate log;
|
||||
|
||||
pub mod config;
|
||||
pub mod events;
|
||||
|
||||
mod database;
|
||||
mod websocket;
|
||||
@@ -14,7 +15,7 @@ mod websocket;
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
// Configure requirements for Bonfire.
|
||||
revolt_quark::configure!();
|
||||
revolt_config::configure!(events);
|
||||
database::connect().await;
|
||||
|
||||
// Clean up the current region information.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
@@ -11,21 +12,21 @@ use futures::{
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
use revolt_quark::{
|
||||
events::{
|
||||
client::EventV1,
|
||||
server::ClientMessage,
|
||||
state::{State, SubscriptionStateChange},
|
||||
},
|
||||
models::{user::UserHint, User},
|
||||
redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI},
|
||||
Database,
|
||||
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
||||
use revolt_database::{
|
||||
events::{client::EventV1, server::ClientMessage},
|
||||
Database, User, UserHint,
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
|
||||
use async_std::{net::TcpStream, sync::Mutex};
|
||||
use async_std::{
|
||||
net::TcpStream,
|
||||
sync::{Mutex, RwLock},
|
||||
};
|
||||
use revolt_result::create_error;
|
||||
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
use crate::events::state::{State, SubscriptionStateChange};
|
||||
|
||||
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
|
||||
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
|
||||
@@ -46,6 +47,7 @@ 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;
|
||||
@@ -73,19 +75,22 @@ 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.
|
||||
@@ -101,6 +106,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
let Ok(ready_payload) = state.generate_ready_payload(db).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
if write.send(config.encode(&ready_payload)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
@@ -115,10 +121,12 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
|
||||
{
|
||||
let write = Mutex::new(write);
|
||||
let subscribed = state.subscribed.clone();
|
||||
|
||||
// Create a PubSub connection to poll on.
|
||||
let listener = listener(db, &mut state, addr, &config, &write).fuse();
|
||||
// Read from WebSocket stream.
|
||||
let worker = worker(user_id.clone(), &config, read, &write).fuse();
|
||||
let worker = worker(addr, subscribed, user_id.clone(), &config, read, &write).fuse();
|
||||
|
||||
// Pin both tasks.
|
||||
pin_mut!(listener, worker);
|
||||
@@ -156,10 +164,12 @@ async fn listener(
|
||||
let mut message_rx = subscriber.message_rx();
|
||||
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() {
|
||||
|
||||
let subscribed = state.subscribed.read().await;
|
||||
for id in subscribed.iter() {
|
||||
subscriber.subscribe(id).await.unwrap();
|
||||
}
|
||||
|
||||
@@ -186,11 +196,12 @@ async fn listener(
|
||||
|
||||
// Handle incoming events.
|
||||
let Ok(message) = message_rx.recv().await.map_err(|e| {
|
||||
info!("Error while consuming pub/sub messages: {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
|
||||
@@ -205,37 +216,84 @@ async fn listener(
|
||||
.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
|
||||
&& write
|
||||
.lock()
|
||||
.await
|
||||
.send(config.encode(&event))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
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) {
|
||||
warn!("Error while sending an event to {addr:?}: {e:?}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if let EventV1::Logout = event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn worker(
|
||||
addr: SocketAddr,
|
||||
subscribed: Arc<RwLock<HashSet<String>>>,
|
||||
user_id: String,
|
||||
config: &ProtocolConfiguration,
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
while let Ok(Some(msg)) = read.try_next().await {
|
||||
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 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(),
|
||||
@@ -244,6 +302,10 @@ async fn worker(
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.6.13"
|
||||
version = "0.7.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Configuration"
|
||||
|
||||
@@ -14,6 +14,7 @@ default = ["test"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
dotenv = "0.15.0"
|
||||
config = "0.13.3"
|
||||
cached = "0.44.0"
|
||||
once_cell = "1.18.0"
|
||||
@@ -24,3 +25,10 @@ serde = { version = "1", features = ["derive"] }
|
||||
# Async
|
||||
futures-locks = "0.7.1"
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
|
||||
# Logging
|
||||
log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
# Sentry
|
||||
sentry = "0.31.5"
|
||||
|
||||
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.
|
||||
@@ -12,7 +12,6 @@ voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
[api]
|
||||
staging = false
|
||||
|
||||
[api.registration]
|
||||
invite_only = false
|
||||
@@ -33,6 +32,7 @@ api_key = ""
|
||||
[api.security]
|
||||
authifier_shield_key = ""
|
||||
voso_legacy_token = ""
|
||||
trust_cloudflare = false
|
||||
|
||||
[api.security.captcha]
|
||||
hcaptcha_key = ""
|
||||
@@ -42,6 +42,7 @@ hcaptcha_sitekey = ""
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[features]
|
||||
webhooks_enabled = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
@@ -64,3 +65,7 @@ background_size = 6000000
|
||||
icon_size = 2500000
|
||||
banner_size = 6000000
|
||||
emoji_size = 500000
|
||||
|
||||
[sentry]
|
||||
api = ""
|
||||
events = ""
|
||||
|
||||
@@ -6,6 +6,9 @@ use futures_locks::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
use std::env;
|
||||
|
||||
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
RwLock::new({
|
||||
let mut builder = Config::builder().add_source(File::from_str(
|
||||
@@ -56,6 +59,9 @@ pub struct ApiSmtp {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub from_address: String,
|
||||
pub reply_to: Option<String>,
|
||||
pub port: Option<i32>,
|
||||
pub use_tls: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -80,6 +86,7 @@ pub struct ApiSecurity {
|
||||
pub authifier_shield_key: String,
|
||||
pub voso_legacy_token: String,
|
||||
pub captcha: ApiSecurityCaptcha,
|
||||
pub trust_cloudflare: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -89,7 +96,6 @@ pub struct ApiWorkers {
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Api {
|
||||
pub staging: bool,
|
||||
pub registration: ApiRegistration,
|
||||
pub smtp: ApiSmtp,
|
||||
pub vapid: ApiVapid,
|
||||
@@ -131,6 +137,13 @@ pub struct FeaturesLimitsCollection {
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Features {
|
||||
pub limits: FeaturesLimitsCollection,
|
||||
pub webhooks_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Sentry {
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -139,6 +152,31 @@ pub struct Settings {
|
||||
pub hosts: Hosts,
|
||||
pub api: Api,
|
||||
pub features: Features,
|
||||
pub sentry: Sentry,
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init() {
|
||||
@@ -157,6 +195,46 @@ pub async fn config() -> Settings {
|
||||
read().await.try_deserialize::<Settings>().unwrap()
|
||||
}
|
||||
|
||||
/// Configure logging and common Rust variables
|
||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
if std::env::var("ROCKET_ADDRESS").is_err() {
|
||||
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
|
||||
}
|
||||
|
||||
pretty_env_logger::init();
|
||||
log::info!("Starting {release}");
|
||||
|
||||
if dsn.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(sentry::init((
|
||||
dsn,
|
||||
sentry::ClientOptions {
|
||||
release: Some(release.into()),
|
||||
..Default::default()
|
||||
},
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! configure {
|
||||
($application: ident) => {
|
||||
let config = $crate::config().await;
|
||||
let _sentry = $crate::setup_logging(
|
||||
concat!(env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION")),
|
||||
config.sentry.$application,
|
||||
)
|
||||
.await;
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.6.13"
|
||||
version = "0.7.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -23,11 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.6.13", path = "../config" }
|
||||
revolt-result = { version = "0.6.13", path = "../result" }
|
||||
revolt-models = { version = "0.6.13", path = "../models" }
|
||||
revolt-presence = { version = "0.6.13", path = "../presence" }
|
||||
revolt-permissions = { version = "0.6.13", path = "../permissions", features = [
|
||||
revolt-config = { version = "0.7.4", path = "../config" }
|
||||
revolt-result = { version = "0.7.4", path = "../result" }
|
||||
revolt-models = { version = "0.7.4", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.7.4", path = "../presence" }
|
||||
revolt-permissions = { version = "0.7.4", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
@@ -45,6 +47,7 @@ decancer = "1.6.2"
|
||||
deadqueue = "0.2.4"
|
||||
linkify = { optional = true, version = "0.8.1" }
|
||||
url-escape = { optional = true, version = "0.1.1" }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
isahc = { optional = true, version = "1.7", features = ["json"] }
|
||||
|
||||
# Serialisation
|
||||
|
||||
@@ -4,7 +4,8 @@ use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
|
||||
MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, User, UserSettings, Webhook,
|
||||
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
@@ -25,7 +26,7 @@ database_derived!(
|
||||
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
|
||||
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
|
||||
pub servers: Arc<Mutex<HashMap<String, Server>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
|
||||
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, User,
|
||||
FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
|
||||
UserSettings, Webhook,
|
||||
};
|
||||
use revolt_result::Error;
|
||||
@@ -48,17 +48,19 @@ pub enum EventV1 {
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
/* /// Basic data to cache
|
||||
/// Logged out
|
||||
Logout,
|
||||
/// Basic data to cache
|
||||
Ready {
|
||||
users: Vec<User>,
|
||||
servers: Vec<Server>,
|
||||
channels: Vec<Channel>,
|
||||
members: Vec<Member>,
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
emojis: Vec<Emoji>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping }, */
|
||||
Pong { data: Ping },
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
@@ -176,8 +178,8 @@ pub enum EventV1 {
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
|
||||
/*/// New report
|
||||
ReportCreate(Report), */
|
||||
/// New report
|
||||
ReportCreate(Report),
|
||||
/// New channel
|
||||
ChannelCreate(Channel),
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
|
||||
@@ -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 = 27;
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -983,6 +984,93 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -93,7 +93,7 @@ impl Bot {
|
||||
Some(id.to_string()),
|
||||
Some(PartialUser {
|
||||
bot: Some(BotInformation {
|
||||
owner: id.to_string(),
|
||||
owner: owner.id.to_string(),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
@@ -142,6 +142,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(())
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use crate::Database;
|
||||
use crate::{Channel, Database, User};
|
||||
|
||||
/* static ALPHABET: [char; 54] = [
|
||||
static ALPHABET: [char; 54] = [
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
|
||||
'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
|
||||
]; */
|
||||
];
|
||||
|
||||
auto_derived!(
|
||||
/// Invite
|
||||
#[serde(tag = "type")]
|
||||
pub enum Invite {
|
||||
/// Invite to a specific server channel
|
||||
Server {
|
||||
@@ -56,9 +57,13 @@ impl Invite {
|
||||
}
|
||||
|
||||
/// Create a new invite from given information
|
||||
/*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> {
|
||||
pub async fn create_channel_invite(
|
||||
db: &Database,
|
||||
creator: &User,
|
||||
channel: &Channel,
|
||||
) -> Result<Invite> {
|
||||
let code = nanoid::nanoid!(8, &ALPHABET);
|
||||
let invite = match &target {
|
||||
let invite = match &channel {
|
||||
Channel::Group { id, .. } => Ok(Invite::Group {
|
||||
code,
|
||||
creator: creator.id.clone(),
|
||||
@@ -72,12 +77,12 @@ impl Invite {
|
||||
channel: id.clone(),
|
||||
})
|
||||
}
|
||||
_ => Err(Error::InvalidOperation),
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}?;
|
||||
|
||||
db.insert_invite(&invite).await?;
|
||||
Ok(invite)
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Resolve an invite by its ID or by a public server ID
|
||||
pub async fn find(db: &Database, code: &str) -> Result<Invite> {
|
||||
|
||||
@@ -5,10 +5,11 @@ use revolt_models::v0::{self, MessageAuthor};
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server,
|
||||
SystemMessage, User,
|
||||
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
|
||||
Server, SystemMessage, User,
|
||||
};
|
||||
|
||||
auto_derived!(
|
||||
@@ -295,6 +296,43 @@ impl Channel {
|
||||
Ok(channel)
|
||||
}
|
||||
|
||||
/// Create a DM (or return the existing one / saved messages)
|
||||
pub async fn create_dm(db: &Database, user_a: &User, user_b: &User) -> Result<Channel> {
|
||||
// Try to find existing channel
|
||||
if let Ok(channel) = db.find_direct_message_channel(&user_a.id, &user_b.id).await {
|
||||
Ok(channel)
|
||||
} else {
|
||||
let channel = if user_a.id == user_b.id {
|
||||
// Create a new saved messages channel
|
||||
Channel::SavedMessages {
|
||||
id: Ulid::new().to_string(),
|
||||
user: user_a.id.to_string(),
|
||||
}
|
||||
} else {
|
||||
// Create a new DM channel
|
||||
Channel::DirectMessage {
|
||||
id: Ulid::new().to_string(),
|
||||
active: true, // show by default
|
||||
recipients: vec![user_a.id.clone(), user_b.id.clone()],
|
||||
last_message_id: None,
|
||||
}
|
||||
};
|
||||
|
||||
db.insert_channel(&channel).await?;
|
||||
|
||||
match &channel {
|
||||
Channel::DirectMessage { .. } => {
|
||||
let event = EventV1::ChannelCreate(channel.clone().into());
|
||||
event.clone().private(user_a.id.clone()).await;
|
||||
event.private(user_b.id.clone()).await;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
Ok(channel)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add user to a group
|
||||
pub async fn add_user_to_group(
|
||||
&mut self,
|
||||
@@ -307,6 +345,13 @@ impl Channel {
|
||||
return Err(create_error!(AlreadyInGroup));
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
if recipients.len() >= config.features.limits.default.group_size {
|
||||
return Err(create_error!(GroupTooLarge {
|
||||
max: config.features.limits.default.group_size
|
||||
}));
|
||||
}
|
||||
|
||||
recipients.push(String::from(&user.id));
|
||||
}
|
||||
|
||||
@@ -582,6 +627,28 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledge a message
|
||||
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
|
||||
EventV1::ChannelAck {
|
||||
id: self.id().to_string(),
|
||||
user: user.to_string(),
|
||||
message_id: message.to_string(),
|
||||
}
|
||||
.private(user.to_string())
|
||||
.await;
|
||||
|
||||
crate::tasks::ack::queue(
|
||||
self.id().to_string(),
|
||||
user.to_string(),
|
||||
AckEvent::AckMessage {
|
||||
id: message.to_string(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove user from a group
|
||||
pub async fn remove_user_from_group(
|
||||
&self,
|
||||
@@ -627,8 +694,7 @@ impl Channel {
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
db.delete_channel(self).await?;
|
||||
return Ok(());
|
||||
return self.delete(db).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,6 +741,9 @@ impl Channel {
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
let id = self.id().to_string();
|
||||
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
|
||||
// TODO: missing functionality:
|
||||
// - group invites
|
||||
// - channels list / categories list on server
|
||||
db.delete_channel(self).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::AbstractChannels;
|
||||
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
||||
use bson::Document;
|
||||
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
||||
use bson::{Bson, Document};
|
||||
use futures::StreamExt;
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
@@ -188,6 +188,125 @@ impl AbstractChannels for MongoDb {
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let id = channel.id().to_string();
|
||||
let server_id = match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Delete invites and unreads.
|
||||
self.delete_associated_channel_objects(Bson::String(id.to_string()))
|
||||
.await?;
|
||||
|
||||
// Delete messages.
|
||||
self.delete_bulk_messages(doc! {
|
||||
"channel": &id
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Remove from server object.
|
||||
if let Some(server) = server_id {
|
||||
let server = self.fetch_server(server).await?;
|
||||
let mut update = doc! {
|
||||
"$pull": {
|
||||
"channels": &id
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(sys) = &server.system_messages {
|
||||
let mut unset = doc! {};
|
||||
|
||||
if let Some(cid) = &sys.user_joined {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_joined", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_left {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_left", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_kicked {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_kicked", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cid) = &sys.user_banned {
|
||||
if &id == cid {
|
||||
unset.insert("system_messages.user_banned", 1_i32);
|
||||
}
|
||||
}
|
||||
|
||||
if !unset.is_empty() {
|
||||
update.insert("$unset", unset);
|
||||
}
|
||||
}
|
||||
|
||||
self.col::<Document>("servers")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": server.id
|
||||
},
|
||||
update,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", "servers"))?;
|
||||
}
|
||||
|
||||
// Delete associated attachments
|
||||
self.delete_many_attachments(doc! {
|
||||
"object_id": &id
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Delete the channel itself
|
||||
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
|
||||
// Delete all invites to these channels.
|
||||
self.col::<Document>("channel_invites")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
|
||||
|
||||
// Delete unread message objects on channels.
|
||||
self.col::<Document>("channel_unreads")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
|
||||
.map(|_| ())?;
|
||||
|
||||
// update many attachments with parent id
|
||||
|
||||
// Delete all webhooks on this channel.
|
||||
self.col::<Document>("webhooks")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "webhooks"))
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,3 +115,21 @@ impl AbstractAttachments for MongoDb {
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_many_attachments(&self, projection: Document) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
projection,
|
||||
doc! {
|
||||
"$set": {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,19 @@ use indexmap::{IndexMap, IndexSet};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::{
|
||||
self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification,
|
||||
ReplyIntent, SendableEmbed, RE_MENTION,
|
||||
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
|
||||
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
||||
};
|
||||
use revolt_permissions::{ChannelPermission, PermissionValue};
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1,
|
||||
tasks::{self, ack::AckEvent},
|
||||
util::idempotency::IdempotencyKey,
|
||||
Channel, Database, Emoji, File,
|
||||
Channel, Database, Emoji, File, User,
|
||||
};
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -155,6 +156,7 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// Message Filter
|
||||
#[derive(Default)]
|
||||
pub struct MessageFilter {
|
||||
/// Parent channel ID
|
||||
pub channel: Option<String>,
|
||||
@@ -441,6 +443,93 @@ impl Message {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create text embed from sendable embed
|
||||
pub async fn create_embed(&self, db: &Database, embed: SendableEmbed) -> Result<Embed> {
|
||||
embed.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
let media = if let Some(id) = embed.media {
|
||||
Some(
|
||||
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Embed::Text(Text {
|
||||
icon_url: embed.icon_url,
|
||||
url: embed.url,
|
||||
title: embed.title,
|
||||
description: embed.description,
|
||||
media: media.map(|m| m.into()),
|
||||
colour: embed.colour,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Update message data
|
||||
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
|
||||
self.apply_options(partial.clone());
|
||||
db.update_message(&self.id, &partial).await?;
|
||||
|
||||
EventV1::MessageUpdate {
|
||||
id: self.id.clone(),
|
||||
channel: self.channel.clone(),
|
||||
data: partial.into(),
|
||||
}
|
||||
.p(self.channel.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to fetch many messages with users
|
||||
pub async fn fetch_with_users(
|
||||
db: &Database,
|
||||
query: MessageQuery,
|
||||
perspective: &User,
|
||||
include_users: Option<bool>,
|
||||
server_id: Option<String>,
|
||||
) -> Result<BulkMessageResponse> {
|
||||
let messages: Vec<v0::Message> = db
|
||||
.fetch_messages(query)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
if let Some(true) = include_users {
|
||||
let user_ids = messages
|
||||
.iter()
|
||||
.map(|m| m.author.clone())
|
||||
.collect::<HashSet<String>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<String>>();
|
||||
let users = User::fetch_many_ids_as_mutuals(db, perspective, &user_ids).await?;
|
||||
|
||||
Ok(BulkMessageResponse::MessagesAndUsers {
|
||||
messages,
|
||||
users,
|
||||
members: if let Some(server_id) = server_id {
|
||||
Some(
|
||||
db.fetch_members(&server_id, &user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
} else {
|
||||
Ok(BulkMessageResponse::JustMessages(messages))
|
||||
}
|
||||
}
|
||||
|
||||
/// Append content to message
|
||||
pub async fn append(
|
||||
db: &Database,
|
||||
@@ -495,6 +584,40 @@ impl Message {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a reaction to a 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
|
||||
&& !self.reactions.contains_key(emoji)
|
||||
{
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Check if the emoji is whitelisted
|
||||
if !self.interactions.can_use(emoji) {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Check if the emoji is usable by us
|
||||
if !Emoji::can_use(db, emoji).await? {
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
|
||||
// Send reaction event
|
||||
EventV1::MessageReact {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
user_id: user.id.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Add emoji
|
||||
db.add_reaction(&self.id, emoji, &user.id).await
|
||||
}
|
||||
|
||||
/// Validate the sum of content of a message is under threshold
|
||||
pub fn validate_sum(
|
||||
content: &Option<String>,
|
||||
@@ -518,6 +641,95 @@ impl Message {
|
||||
Err(create_error!(PayloadTooLarge))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a message
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
let file_ids: Vec<String> = self
|
||||
.attachments
|
||||
.map(|files| files.iter().map(|file| file.id.to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
if !file_ids.is_empty() {
|
||||
db.mark_attachments_as_deleted(&file_ids).await?;
|
||||
}
|
||||
|
||||
db.delete_message(&self.id).await?;
|
||||
|
||||
EventV1::MessageDelete {
|
||||
id: self.id,
|
||||
channel: self.channel.clone(),
|
||||
}
|
||||
.p(self.channel)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bulk delete messages
|
||||
pub async fn bulk_delete(db: &Database, channel: &str, ids: Vec<String>) -> Result<()> {
|
||||
let valid_ids = db
|
||||
.fetch_messages_by_id(&ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|msg| msg.channel == channel)
|
||||
.map(|msg| msg.id)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
db.delete_messages(channel, &valid_ids).await?;
|
||||
EventV1::BulkMessageDelete {
|
||||
channel: channel.to_string(),
|
||||
ids: valid_ids,
|
||||
}
|
||||
.p(channel.to_string())
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
|
||||
// Check if it actually exists
|
||||
let empty = if let Some(users) = self.reactions.get(emoji) {
|
||||
if !users.contains(user) {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
users.len() == 1
|
||||
} else {
|
||||
return Err(create_error!(NotFound));
|
||||
};
|
||||
|
||||
// Send reaction event
|
||||
EventV1::MessageUnreact {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
user_id: user.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
if empty {
|
||||
// If empty, remove the reaction entirely
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
} else {
|
||||
// Otherwise only remove that one reaction
|
||||
db.remove_reaction(&self.id, emoji, user).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
|
||||
// Send reaction event
|
||||
EventV1::MessageRemoveReaction {
|
||||
id: self.id.to_string(),
|
||||
channel_id: self.channel.to_string(),
|
||||
emoji_id: emoji.to_string(),
|
||||
}
|
||||
.p(self.channel.to_string())
|
||||
.await;
|
||||
|
||||
// Write to database
|
||||
db.clear_reaction(&self.id, emoji).await
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemMessage {
|
||||
|
||||
@@ -16,6 +16,9 @@ pub trait AbstractMessages: Sync + Send {
|
||||
/// Fetch multiple messages by given query
|
||||
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
|
||||
|
||||
/// Fetch multiple messages by given IDs
|
||||
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<()>;
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ use mongodb::options::FindOptions;
|
||||
use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage};
|
||||
use crate::{
|
||||
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
|
||||
};
|
||||
|
||||
use super::AbstractMessages;
|
||||
|
||||
@@ -159,6 +161,21 @@ impl AbstractMessages for MongoDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch multiple messages by given IDs
|
||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
|
||||
self.find_with_options(
|
||||
COL,
|
||||
doc! {
|
||||
"ids": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))
|
||||
}
|
||||
|
||||
/// 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(|_| ())
|
||||
@@ -278,3 +295,57 @@ impl AbstractMessages for MongoDb {
|
||||
.map_err(|_| create_database_error!("delete_many", COL))
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
|
||||
let mut for_attachments = projection.clone();
|
||||
for_attachments.insert(
|
||||
"attachments",
|
||||
doc! {
|
||||
"$exists": 1_i32
|
||||
},
|
||||
);
|
||||
|
||||
// Check if there are any attachments we need to delete.
|
||||
let message_ids_with_attachments = self
|
||||
.find_with_options::<_, DocumentId>(
|
||||
COL,
|
||||
for_attachments,
|
||||
FindOptions::builder()
|
||||
.projection(doc! { "_id": 1_i32 })
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find_many", "attachments"))?
|
||||
.into_iter()
|
||||
.map(|x| x.id)
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// If we found any, mark them as deleted.
|
||||
if !message_ids_with_attachments.is_empty() {
|
||||
self.col::<Document>("attachments")
|
||||
.update_many(
|
||||
doc! {
|
||||
"message_id": {
|
||||
"$in": message_ids_with_attachments
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_many", "attachments"))?;
|
||||
}
|
||||
|
||||
// And then delete said messages.
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(projection, None)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("delete_many", COL))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use futures::future::try_join_all;
|
||||
use indexmap::IndexSet;
|
||||
use revolt_result::Result;
|
||||
|
||||
@@ -176,6 +177,11 @@ impl AbstractMessages for ReferenceDb {
|
||||
}*/
|
||||
}
|
||||
|
||||
/// Fetch multiple messages by given IDs
|
||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>> {
|
||||
try_join_all(ids.iter().map(|id| self.fetch_message(id))).await
|
||||
}
|
||||
|
||||
/// Update a given message with new information
|
||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
|
||||
@@ -8,6 +8,8 @@ mod emojis;
|
||||
mod files;
|
||||
mod messages;
|
||||
mod ratelimit_events;
|
||||
mod safety_reports;
|
||||
mod safety_snapshots;
|
||||
mod server_bans;
|
||||
mod server_members;
|
||||
mod servers;
|
||||
@@ -24,6 +26,8 @@ pub use emojis::*;
|
||||
pub use files::*;
|
||||
pub use messages::*;
|
||||
pub use ratelimit_events::*;
|
||||
pub use safety_reports::*;
|
||||
pub use safety_snapshots::*;
|
||||
pub use server_bans::*;
|
||||
pub use server_members::*;
|
||||
pub use servers::*;
|
||||
@@ -45,6 +49,8 @@ pub trait AbstractDatabase:
|
||||
+ files::AbstractAttachments
|
||||
+ messages::AbstractMessages
|
||||
+ ratelimit_events::AbstractRatelimitEvents
|
||||
+ safety_reports::AbstractReport
|
||||
+ safety_snapshots::AbstractSnapshot
|
||||
+ server_bans::AbstractServerBans
|
||||
+ server_members::AbstractServerMembers
|
||||
+ servers::AbstractServers
|
||||
|
||||
5
crates/core/database/src/models/safety_reports/mod.rs
Normal file
5
crates/core/database/src/models/safety_reports/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
22
crates/core/database/src/models/safety_reports/model.rs
Normal file
22
crates/core/database/src/models/safety_reports/model.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use revolt_models::v0::{ReportStatus, ReportedContent};
|
||||
|
||||
auto_derived!(
|
||||
/// User-generated platform moderation report
|
||||
pub struct Report {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Id of the user creating this report
|
||||
pub author_id: String,
|
||||
/// Reported content
|
||||
pub content: ReportedContent,
|
||||
/// Additional report context
|
||||
pub additional_context: String,
|
||||
/// Status of the report
|
||||
#[serde(flatten)]
|
||||
pub status: ReportStatus,
|
||||
/// Additional notes included on the report
|
||||
#[serde(default)]
|
||||
pub notes: String,
|
||||
}
|
||||
);
|
||||
12
crates/core/database/src/models/safety_reports/ops.rs
Normal file
12
crates/core/database/src/models/safety_reports/ops.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Report;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractReport: Sync + Send {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::Report;
|
||||
|
||||
use super::AbstractReport;
|
||||
|
||||
static COL: &str = "safety_reports";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractReport for MongoDb {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()> {
|
||||
query!(self, insert_one, COL, &report).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::Report;
|
||||
|
||||
use super::AbstractReport;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractReport for ReferenceDb {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_report(&self, report: &Report) -> Result<()> {
|
||||
let mut reports = self.safety_reports.lock().await;
|
||||
if reports.contains_key(&report.id) {
|
||||
Err(create_database_error!("insert", "report"))
|
||||
} else {
|
||||
reports.insert(report.id.to_string(), report.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/safety_snapshots/mod.rs
Normal file
5
crates/core/database/src/models/safety_snapshots/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
@@ -1,13 +1,44 @@
|
||||
use crate::{
|
||||
models::{
|
||||
message::{MessageFilter, MessageQuery, MessageSort, MessageTimePeriod},
|
||||
snapshot::SnapshotContent,
|
||||
Message, Server, User,
|
||||
},
|
||||
Database, Result,
|
||||
};
|
||||
use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Database, Message, MessageFilter, MessageQuery, MessageTimePeriod, Server, User};
|
||||
|
||||
auto_derived!(
|
||||
/// Snapshot of some content
|
||||
pub struct Snapshot {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Report parent Id
|
||||
pub report_id: String,
|
||||
/// Snapshot of content
|
||||
pub content: SnapshotContent,
|
||||
}
|
||||
|
||||
/// Enum to map into different models
|
||||
/// that can be saved in a snapshot
|
||||
#[serde(tag = "_type")]
|
||||
pub enum SnapshotContent {
|
||||
Message {
|
||||
/// Context before the message
|
||||
#[serde(rename = "_prior_context", default)]
|
||||
prior_context: Vec<Message>,
|
||||
|
||||
/// Context after the message
|
||||
#[serde(rename = "_leading_context", default)]
|
||||
leading_context: Vec<Message>,
|
||||
|
||||
/// Message
|
||||
#[serde(flatten)]
|
||||
message: Message,
|
||||
},
|
||||
Server(Server),
|
||||
User(User),
|
||||
}
|
||||
);
|
||||
|
||||
impl SnapshotContent {
|
||||
/// Generate snapshot from a given message
|
||||
pub async fn generate_from_message(
|
||||
db: &Database,
|
||||
message: Message,
|
||||
@@ -54,13 +85,14 @@ impl SnapshotContent {
|
||||
Ok((
|
||||
SnapshotContent::Message {
|
||||
message,
|
||||
prior_context,
|
||||
leading_context,
|
||||
prior_context: prior_context.into_iter().map(Into::into).collect(),
|
||||
leading_context: leading_context.into_iter().map(Into::into).collect(),
|
||||
},
|
||||
files,
|
||||
))
|
||||
}
|
||||
|
||||
/// Generate snapshot from a given server
|
||||
pub fn generate_from_server(server: Server) -> Result<(SnapshotContent, Vec<String>)> {
|
||||
// Collect server's icon and banner
|
||||
let files = [&server.icon, &server.banner]
|
||||
@@ -71,6 +103,7 @@ impl SnapshotContent {
|
||||
Ok((SnapshotContent::Server(server), files))
|
||||
}
|
||||
|
||||
/// Generate snapshot from a given user
|
||||
pub fn generate_from_user(user: User) -> Result<(SnapshotContent, Vec<String>)> {
|
||||
// Collect user's avatar and profile background
|
||||
let files = [
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::models::Snapshot;
|
||||
use crate::Result;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Snapshot;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractSnapshot: Sync + Send {
|
||||
/// Insert a new snapshot into the database
|
||||
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>;
|
||||
|
||||
/// Fetch a snapshots by a report's id
|
||||
async fn fetch_snapshots(&self, report_id: &str) -> Result<Vec<Snapshot>>;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::Snapshot;
|
||||
|
||||
use super::AbstractSnapshot;
|
||||
|
||||
static COL: &str = "safety_snapshots";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractSnapshot for MongoDb {
|
||||
/// Insert a new snapshot into the database
|
||||
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
|
||||
query!(self, insert_one, COL, &snapshot).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::Snapshot;
|
||||
|
||||
use super::AbstractSnapshot;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractSnapshot for ReferenceDb {
|
||||
/// Insert a new report into the database
|
||||
async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
|
||||
let mut snapshots = self.safety_snapshots.lock().await;
|
||||
if snapshots.contains_key(&snapshot.id) {
|
||||
Err(create_database_error!("insert", "snapshot"))
|
||||
} else {
|
||||
snapshots.insert(snapshot.id.to_string(), snapshot.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::MemberCompositeKey;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Database, MemberCompositeKey, Server};
|
||||
|
||||
auto_derived!(
|
||||
/// Server Ban
|
||||
@@ -10,3 +12,24 @@ auto_derived!(
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
impl ServerBan {
|
||||
/// Create ban
|
||||
pub async fn create(
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
user_id: &str,
|
||||
reason: Option<String>,
|
||||
) -> Result<ServerBan> {
|
||||
let ban = ServerBan {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
},
|
||||
reason,
|
||||
};
|
||||
|
||||
db.insert_ban(&ban).await?;
|
||||
Ok(ban)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,4 +215,47 @@ impl Member {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove member from server
|
||||
pub async fn remove(
|
||||
self,
|
||||
db: &Database,
|
||||
server: &Server,
|
||||
intention: RemovalIntention,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
db.delete_member(&self.id).await?;
|
||||
|
||||
EventV1::ServerMemberLeave {
|
||||
id: self.id.server.to_string(),
|
||||
user: self.id.user.to_string(),
|
||||
}
|
||||
.p(self.id.server.to_string())
|
||||
.await;
|
||||
|
||||
if !silent {
|
||||
if let Some(id) = server
|
||||
.system_messages
|
||||
.as_ref()
|
||||
.and_then(|x| match intention {
|
||||
RemovalIntention::Leave => x.user_left.as_ref(),
|
||||
RemovalIntention::Kick => x.user_kicked.as_ref(),
|
||||
RemovalIntention::Ban => x.user_banned.as_ref(),
|
||||
})
|
||||
{
|
||||
match intention {
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: self.id.user },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: self.id.user },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: self.id.user },
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
// TODO: support notifications here in the future?
|
||||
.send_without_notifications(db, false, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,71 +253,6 @@ impl Server {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
/// Remove a member from a server
|
||||
pub async fn remove_member(
|
||||
&self,
|
||||
db: &Database,
|
||||
member: Member,
|
||||
intention: RemovalIntention,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
db.delete_member(&member.id).await?;
|
||||
|
||||
/* // TODO: EventV1::ServerMemberLeave {
|
||||
id: self.id.to_string(),
|
||||
user: member.id.user.clone(),
|
||||
}
|
||||
.p(member.id.server)
|
||||
.await; */
|
||||
|
||||
if !silent {
|
||||
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
|
||||
RemovalIntention::Leave => x.user_left.as_ref(),
|
||||
RemovalIntention::Kick => x.user_kicked.as_ref(),
|
||||
RemovalIntention::Ban => x.user_banned.as_ref(),
|
||||
}) {
|
||||
match intention {
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create_no_web_push(db, id, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create ban
|
||||
pub async fn ban_user(
|
||||
self,
|
||||
db: &Database,
|
||||
id: MemberCompositeKey,
|
||||
reason: Option<String>,
|
||||
) -> Result<ServerBan> {
|
||||
let ban = ServerBan { id, reason };
|
||||
db.insert_ban(&ban).await?;
|
||||
Ok(ban)
|
||||
}
|
||||
|
||||
/// Ban a member from a server
|
||||
pub async fn ban_member(
|
||||
self,
|
||||
db: &Database,
|
||||
member: Member,
|
||||
reason: Option<String>,
|
||||
) -> Result<ServerBan> {
|
||||
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
|
||||
.await?;
|
||||
|
||||
self.ban_user(db, member.id, reason).await
|
||||
} */
|
||||
}
|
||||
|
||||
impl Role {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use bson::{to_document, Document};
|
||||
use bson::{to_document, Bson, Document};
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
@@ -67,6 +67,7 @@ impl AbstractServers for MongoDb {
|
||||
|
||||
/// Delete a server by its id
|
||||
async fn delete_server(&self, id: &str) -> Result<()> {
|
||||
self.delete_associated_server_objects(id).await?;
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
|
||||
@@ -181,3 +182,81 @@ impl IntoDocumentPath for FieldsRole {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
pub async fn delete_associated_server_objects(&self, server_id: &str) -> Result<()> {
|
||||
// Find all channels
|
||||
let channels: Vec<String> = self
|
||||
.col::<Document>("channels")
|
||||
.find(
|
||||
doc! {
|
||||
"server": server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", "channels"))?
|
||||
.filter_map(|s| async {
|
||||
s.map(|d| d.get_str("_id").map(|s| s.to_string()).ok())
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// Check if there are any attachments we need to delete.
|
||||
self.delete_bulk_messages(doc! {
|
||||
"channel": {
|
||||
"$in": &channels
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Delete all emoji.
|
||||
self.col::<Document>("emojis")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"parent.id": &server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "emojis"))?;
|
||||
|
||||
// Delete all channels.
|
||||
self.col::<Document>("channels")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"server": &server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channels"))?;
|
||||
|
||||
// Delete any associated objects, e.g. unreads and invites.
|
||||
self.delete_associated_channel_objects(Bson::Document(doc! { "$in": &channels }))
|
||||
.await?;
|
||||
|
||||
// Delete members and bans.
|
||||
for with in &["server_members", "server_bans"] {
|
||||
self.col::<Document>(with)
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.server": &server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", with))?;
|
||||
}
|
||||
|
||||
// Update many attachments with parent id.
|
||||
self.delete_many_attachments(doc! {
|
||||
"object_id": &server_id
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ use crate::{events::client::EventV1, Database, File, RatelimitEvent};
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_config::config;
|
||||
use revolt_result::{create_error, Error, ErrorType, Result};
|
||||
use revolt_models::v0;
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::{create_error, Result};
|
||||
use ulid::Ulid;
|
||||
|
||||
auto_derived_partial!(
|
||||
@@ -94,20 +96,22 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// User's active status
|
||||
#[derive(Default)]
|
||||
pub struct UserStatus {
|
||||
/// Custom status text
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
/// Current presence option
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub presence: Option<Presence>,
|
||||
}
|
||||
|
||||
/// User's profile
|
||||
#[derive(Default)]
|
||||
pub struct UserProfile {
|
||||
/// Text content on user's profile
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub content: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
/// Background visible on user's profile
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<File>,
|
||||
@@ -118,6 +122,16 @@ auto_derived!(
|
||||
/// Id of the owner of this bot
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
/// Enumeration providing a hint to the type of user we are handling
|
||||
pub enum UserHint {
|
||||
/// Could be either a user or a bot
|
||||
Any,
|
||||
/// Only match bots
|
||||
Bot,
|
||||
/// Only match users
|
||||
User,
|
||||
}
|
||||
);
|
||||
|
||||
pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
@@ -262,7 +276,55 @@ impl User {
|
||||
Ok(username)
|
||||
}
|
||||
|
||||
// Find a free discriminator for a given username
|
||||
/// 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, String)> {
|
||||
match hint {
|
||||
UserHint::Bot => Ok((
|
||||
db.fetch_user(
|
||||
&db.fetch_bot_by_token(token)
|
||||
.await
|
||||
.map_err(|_| create_error!(InvalidSession))?
|
||||
.id,
|
||||
)
|
||||
.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::Any => {
|
||||
if let Ok(result) = User::from_token(db, token, UserHint::User).await {
|
||||
Ok(result)
|
||||
} else {
|
||||
User::from_token(db, token, UserHint::Bot).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to fetch many users as a mutually connected user
|
||||
/// (while optimising the online ID query)
|
||||
pub async fn fetch_many_ids_as_mutuals(
|
||||
db: &Database,
|
||||
perspective: &User,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<v0::User>> {
|
||||
let online_ids = filter_online(ids).await;
|
||||
|
||||
Ok(db
|
||||
.fetch_users(ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|user| {
|
||||
let is_online = online_ids.contains(&user.id);
|
||||
user.into_known(perspective, is_online)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Find a free discriminator for a given username
|
||||
pub async fn find_discriminator(
|
||||
db: &Database,
|
||||
username: &str,
|
||||
@@ -348,19 +410,6 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a username is already in use by another user
|
||||
#[allow(dead_code)]
|
||||
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
|
||||
match db.fetch_user_by_username(username).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(Error {
|
||||
error_type: ErrorType::NotFound,
|
||||
..
|
||||
}) => Ok(false),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a relationship to another user
|
||||
pub async fn set_relationship(
|
||||
&mut self,
|
||||
@@ -554,7 +603,7 @@ impl User {
|
||||
FieldsUser::Avatar => self.avatar = None,
|
||||
FieldsUser::StatusText => {
|
||||
if let Some(x) = self.status.as_mut() {
|
||||
x.text = String::new();
|
||||
x.text = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::StatusPresence => {
|
||||
@@ -564,7 +613,7 @@ impl User {
|
||||
}
|
||||
FieldsUser::ProfileContent => {
|
||||
if let Some(x) = self.profile.as_mut() {
|
||||
x.content = String::new();
|
||||
x.content = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::ProfileBackground => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use authifier::models::Session;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
@@ -14,10 +15,10 @@ pub trait AbstractUsers: Sync + Send {
|
||||
async fn fetch_user(&self, id: &str) -> Result<User>;
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User>;
|
||||
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>>;
|
||||
|
||||
@@ -25,13 +25,14 @@ impl AbstractUsers for MongoDb {
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
|
||||
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
|
||||
query!(
|
||||
self,
|
||||
find_one_with_options,
|
||||
COL,
|
||||
doc! {
|
||||
"username": username
|
||||
"username": username,
|
||||
"discriminator": discriminator
|
||||
},
|
||||
FindOneOptions::builder()
|
||||
.collation(
|
||||
@@ -45,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
|
||||
@@ -57,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.id).await
|
||||
.ok_or_else(|| create_error!(InvalidSession))
|
||||
}
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use authifier::models::Session;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
@@ -28,18 +29,20 @@ impl AbstractUsers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
|
||||
async fn fetch_user_by_username(&self, username: &str, discriminator: &str) -> Result<User> {
|
||||
let users = self.users.lock().await;
|
||||
let lowercase = username.to_lowercase();
|
||||
users
|
||||
.values()
|
||||
.find(|user| user.username.to_lowercase() == lowercase)
|
||||
.find(|user| {
|
||||
user.username.to_lowercase() == lowercase && user.discriminator == discriminator
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> {
|
||||
/// Fetch a session from the database by token
|
||||
async fn fetch_session_by_token(&self, _token: &str) -> Result<Session> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ impl crate::Bot {
|
||||
description: user
|
||||
.profile
|
||||
.map(|profile| profile.content)
|
||||
.flatten()
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
@@ -219,6 +220,86 @@ impl From<crate::Channel> for Channel {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Channel> for crate::Channel {
|
||||
fn from(value: Channel) -> crate::Channel {
|
||||
match value {
|
||||
Channel::SavedMessages { id, user } => crate::Channel::SavedMessages { id, user },
|
||||
Channel::DirectMessage {
|
||||
id,
|
||||
active,
|
||||
recipients,
|
||||
last_message_id,
|
||||
} => crate::Channel::DirectMessage {
|
||||
id,
|
||||
active,
|
||||
recipients,
|
||||
last_message_id,
|
||||
},
|
||||
Channel::Group {
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
recipients,
|
||||
icon,
|
||||
last_message_id,
|
||||
permissions,
|
||||
nsfw,
|
||||
} => crate::Channel::Group {
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
description,
|
||||
recipients,
|
||||
icon: icon.map(|file| file.into()),
|
||||
last_message_id,
|
||||
permissions,
|
||||
nsfw,
|
||||
},
|
||||
Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
last_message_id,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
} => crate::Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon: icon.map(|file| file.into()),
|
||||
last_message_id,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
},
|
||||
Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
} => crate::Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon: icon.map(|file| file.into()),
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialChannel> for PartialChannel {
|
||||
fn from(value: crate::PartialChannel) -> Self {
|
||||
PartialChannel {
|
||||
@@ -236,6 +317,23 @@ impl From<crate::PartialChannel> for PartialChannel {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialChannel> for crate::PartialChannel {
|
||||
fn from(value: PartialChannel) -> crate::PartialChannel {
|
||||
crate::PartialChannel {
|
||||
name: value.name,
|
||||
owner: value.owner,
|
||||
description: value.description,
|
||||
icon: value.icon.map(|file| file.into()),
|
||||
nsfw: value.nsfw,
|
||||
active: value.active,
|
||||
permissions: value.permissions,
|
||||
role_permissions: value.role_permissions,
|
||||
default_permissions: value.default_permissions,
|
||||
last_message_id: value.last_message_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsChannel> for crate::FieldsChannel {
|
||||
fn from(value: FieldsChannel) -> Self {
|
||||
match value {
|
||||
@@ -306,6 +404,25 @@ impl From<crate::File> for File {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for crate::File {
|
||||
fn from(value: File) -> crate::File {
|
||||
crate::File {
|
||||
id: value.id,
|
||||
tag: value.tag,
|
||||
filename: value.filename,
|
||||
metadata: value.metadata.into(),
|
||||
content_type: value.content_type,
|
||||
size: value.size,
|
||||
deleted: value.deleted,
|
||||
reported: value.reported,
|
||||
message_id: value.message_id,
|
||||
user_id: value.user_id,
|
||||
server_id: value.server_id,
|
||||
object_id: value.object_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Metadata> for Metadata {
|
||||
fn from(value: crate::Metadata) -> Self {
|
||||
match value {
|
||||
@@ -324,6 +441,24 @@ impl From<crate::Metadata> for Metadata {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Metadata> for crate::Metadata {
|
||||
fn from(value: Metadata) -> crate::Metadata {
|
||||
match value {
|
||||
Metadata::File => crate::Metadata::File,
|
||||
Metadata::Text => crate::Metadata::Text,
|
||||
Metadata::Image { width, height } => crate::Metadata::Image {
|
||||
width: width as isize,
|
||||
height: height as isize,
|
||||
},
|
||||
Metadata::Video { width, height } => crate::Metadata::Video {
|
||||
width: width as isize,
|
||||
height: height as isize,
|
||||
},
|
||||
Metadata::Audio => crate::Metadata::Audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Message> for Message {
|
||||
fn from(value: crate::Message) -> Self {
|
||||
Message {
|
||||
@@ -444,6 +579,19 @@ impl From<Masquerade> for crate::Masquerade {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Report> for Report {
|
||||
fn from(value: crate::Report) -> Self {
|
||||
Report {
|
||||
id: value.id,
|
||||
author_id: value.author_id,
|
||||
content: value.content,
|
||||
additional_context: value.additional_context,
|
||||
status: value.status,
|
||||
notes: value.notes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::ServerBan> for ServerBan {
|
||||
fn from(value: crate::ServerBan) -> Self {
|
||||
ServerBan {
|
||||
@@ -466,6 +614,19 @@ impl From<crate::Member> for Member {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Member> for crate::Member {
|
||||
fn from(value: Member) -> crate::Member {
|
||||
crate::Member {
|
||||
id: value.id.into(),
|
||||
joined_at: value.joined_at,
|
||||
nickname: value.nickname,
|
||||
avatar: value.avatar.map(|f| f.into()),
|
||||
roles: value.roles,
|
||||
timeout: value.timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialMember> for PartialMember {
|
||||
fn from(value: crate::PartialMember) -> Self {
|
||||
PartialMember {
|
||||
@@ -479,6 +640,19 @@ impl From<crate::PartialMember> for PartialMember {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialMember> for crate::PartialMember {
|
||||
fn from(value: PartialMember) -> crate::PartialMember {
|
||||
crate::PartialMember {
|
||||
id: value.id.map(|id| id.into()),
|
||||
joined_at: value.joined_at,
|
||||
nickname: value.nickname,
|
||||
avatar: value.avatar.map(|f| f.into()),
|
||||
roles: value.roles,
|
||||
timeout: value.timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::MemberCompositeKey> for MemberCompositeKey {
|
||||
fn from(value: crate::MemberCompositeKey) -> Self {
|
||||
MemberCompositeKey {
|
||||
@@ -488,6 +662,15 @@ impl From<crate::MemberCompositeKey> for MemberCompositeKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MemberCompositeKey> for crate::MemberCompositeKey {
|
||||
fn from(value: MemberCompositeKey) -> crate::MemberCompositeKey {
|
||||
crate::MemberCompositeKey {
|
||||
server: value.server,
|
||||
user: value.user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsMember> for FieldsMember {
|
||||
fn from(value: crate::FieldsMember) -> Self {
|
||||
match value {
|
||||
@@ -499,6 +682,17 @@ impl From<crate::FieldsMember> for FieldsMember {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsMember> for crate::FieldsMember {
|
||||
fn from(value: FieldsMember) -> crate::FieldsMember {
|
||||
match value {
|
||||
FieldsMember::Avatar => crate::FieldsMember::Avatar,
|
||||
FieldsMember::Nickname => crate::FieldsMember::Nickname,
|
||||
FieldsMember::Roles => crate::FieldsMember::Roles,
|
||||
FieldsMember::Timeout => crate::FieldsMember::Timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::RemovalIntention> for RemovalIntention {
|
||||
fn from(value: crate::RemovalIntention) -> Self {
|
||||
match value {
|
||||
@@ -537,6 +731,34 @@ impl From<crate::Server> for Server {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Server> for crate::Server {
|
||||
fn from(value: Server) -> crate::Server {
|
||||
crate::Server {
|
||||
id: value.id,
|
||||
owner: value.owner,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
channels: value.channels,
|
||||
categories: value
|
||||
.categories
|
||||
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
|
||||
system_messages: value.system_messages.map(|v| v.into()),
|
||||
roles: value
|
||||
.roles
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.into()))
|
||||
.collect(),
|
||||
default_permissions: value.default_permissions,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
banner: value.banner.map(|f| f.into()),
|
||||
flags: Some(value.flags as i32),
|
||||
nsfw: value.nsfw,
|
||||
analytics: value.analytics,
|
||||
discoverable: value.discoverable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialServer> for PartialServer {
|
||||
fn from(value: crate::PartialServer) -> Self {
|
||||
PartialServer {
|
||||
@@ -563,6 +785,32 @@ impl From<crate::PartialServer> for PartialServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialServer> for crate::PartialServer {
|
||||
fn from(value: PartialServer) -> crate::PartialServer {
|
||||
crate::PartialServer {
|
||||
id: value.id,
|
||||
owner: value.owner,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
channels: value.channels,
|
||||
categories: value
|
||||
.categories
|
||||
.map(|categories| categories.into_iter().map(|v| v.into()).collect()),
|
||||
system_messages: value.system_messages.map(|v| v.into()),
|
||||
roles: value
|
||||
.roles
|
||||
.map(|roles| roles.into_iter().map(|(k, v)| (k, v.into())).collect()),
|
||||
default_permissions: value.default_permissions,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
banner: value.banner.map(|f| f.into()),
|
||||
flags: value.flags.map(|v| v as i32),
|
||||
nsfw: value.nsfw,
|
||||
analytics: value.analytics,
|
||||
discoverable: value.discoverable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsServer> for FieldsServer {
|
||||
fn from(value: crate::FieldsServer) -> Self {
|
||||
match value {
|
||||
@@ -575,6 +823,18 @@ impl From<crate::FieldsServer> for FieldsServer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsServer> for crate::FieldsServer {
|
||||
fn from(value: FieldsServer) -> crate::FieldsServer {
|
||||
match value {
|
||||
FieldsServer::Banner => crate::FieldsServer::Banner,
|
||||
FieldsServer::Categories => crate::FieldsServer::Categories,
|
||||
FieldsServer::Description => crate::FieldsServer::Description,
|
||||
FieldsServer::Icon => crate::FieldsServer::Icon,
|
||||
FieldsServer::SystemMessages => crate::FieldsServer::SystemMessages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Category> for Category {
|
||||
fn from(value: crate::Category) -> Self {
|
||||
Category {
|
||||
@@ -585,6 +845,16 @@ impl From<crate::Category> for Category {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Category> for crate::Category {
|
||||
fn from(value: Category) -> Self {
|
||||
crate::Category {
|
||||
id: value.id,
|
||||
title: value.title,
|
||||
channels: value.channels,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::SystemMessageChannels> for SystemMessageChannels {
|
||||
fn from(value: crate::SystemMessageChannels) -> Self {
|
||||
SystemMessageChannels {
|
||||
@@ -596,6 +866,17 @@ impl From<crate::SystemMessageChannels> for SystemMessageChannels {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemMessageChannels> for crate::SystemMessageChannels {
|
||||
fn from(value: SystemMessageChannels) -> Self {
|
||||
crate::SystemMessageChannels {
|
||||
user_joined: value.user_joined,
|
||||
user_left: value.user_left,
|
||||
user_kicked: value.user_kicked,
|
||||
user_banned: value.user_banned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::Role> for Role {
|
||||
fn from(value: crate::Role) -> Self {
|
||||
Role {
|
||||
@@ -608,6 +889,18 @@ impl From<crate::Role> for Role {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Role> for crate::Role {
|
||||
fn from(value: Role) -> crate::Role {
|
||||
crate::Role {
|
||||
name: value.name,
|
||||
permissions: value.permissions,
|
||||
colour: value.colour,
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialRole> for PartialRole {
|
||||
fn from(value: crate::PartialRole) -> Self {
|
||||
PartialRole {
|
||||
@@ -620,6 +913,18 @@ impl From<crate::PartialRole> for PartialRole {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PartialRole> for crate::PartialRole {
|
||||
fn from(value: PartialRole) -> crate::PartialRole {
|
||||
crate::PartialRole {
|
||||
name: value.name,
|
||||
permissions: value.permissions,
|
||||
colour: value.colour,
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::FieldsRole> for FieldsRole {
|
||||
fn from(value: crate::FieldsRole) -> Self {
|
||||
match value {
|
||||
@@ -628,6 +933,14 @@ impl From<crate::FieldsRole> for FieldsRole {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FieldsRole> for crate::FieldsRole {
|
||||
fn from(value: FieldsRole) -> Self {
|
||||
match value {
|
||||
FieldsRole::Colour => crate::FieldsRole::Colour,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::User {
|
||||
pub async fn into<'a, P>(self, db: &Database, perspective: P) -> User
|
||||
where
|
||||
@@ -682,6 +995,92 @@ 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,
|
||||
id: self.id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert user object into user model assuming mutual connection
|
||||
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
|
||||
where
|
||||
P: Into<Option<&'a crate::User>>,
|
||||
{
|
||||
let perspective = perspective.into();
|
||||
let (relationship, can_see_profile) = if self.bot.is_some() {
|
||||
(RelationshipStatus::None, true)
|
||||
} else if let Some(perspective) = perspective {
|
||||
if perspective.id == self.id {
|
||||
(RelationshipStatus::User, true)
|
||||
} else {
|
||||
let relationship = perspective
|
||||
.relations
|
||||
.as_ref()
|
||||
.map(|relations| {
|
||||
relations
|
||||
.iter()
|
||||
.find(|relationship| relationship.id == self.id)
|
||||
.map(|relationship| relationship.status.clone().into())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let can_see_profile = relationship != RelationshipStatus::BlockedOther;
|
||||
(relationship, can_see_profile)
|
||||
}
|
||||
} else {
|
||||
(RelationshipStatus::None, false)
|
||||
};
|
||||
|
||||
User {
|
||||
username: self.username,
|
||||
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![]
|
||||
},
|
||||
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 {
|
||||
@@ -696,7 +1095,6 @@ impl crate::User {
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -717,16 +1115,49 @@ impl crate::User {
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
badges: self.badges.unwrap_or_default() as u32,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_author_for_system(&self) -> MessageAuthor {
|
||||
MessageAuthor::System {
|
||||
username: &self.username,
|
||||
avatar: self.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<User> for crate::User {
|
||||
fn from(value: User) -> crate::User {
|
||||
crate::User {
|
||||
id: value.id,
|
||||
username: value.username,
|
||||
discriminator: value.discriminator,
|
||||
display_name: value.display_name,
|
||||
avatar: value.avatar.map(Into::into),
|
||||
relations: None,
|
||||
badges: Some(value.badges as i32),
|
||||
status: value.status.map(Into::into),
|
||||
profile: value.profile.map(Into::into),
|
||||
flags: Some(value.flags as i32),
|
||||
privileged: value.privileged,
|
||||
bot: value.bot.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::PartialUser> for PartialUser {
|
||||
@@ -814,6 +1245,18 @@ impl From<crate::Presence> for Presence {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Presence> for crate::Presence {
|
||||
fn from(value: Presence) -> crate::Presence {
|
||||
match value {
|
||||
Presence::Online => crate::Presence::Online,
|
||||
Presence::Idle => crate::Presence::Idle,
|
||||
Presence::Focus => crate::Presence::Focus,
|
||||
Presence::Busy => crate::Presence::Busy,
|
||||
Presence::Invisible => crate::Presence::Invisible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::UserStatus> for UserStatus {
|
||||
fn from(value: crate::UserStatus) -> Self {
|
||||
UserStatus {
|
||||
@@ -823,6 +1266,15 @@ impl From<crate::UserStatus> for UserStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserStatus> for crate::UserStatus {
|
||||
fn from(value: UserStatus) -> crate::UserStatus {
|
||||
crate::UserStatus {
|
||||
text: value.text,
|
||||
presence: value.presence.map(|presence| presence.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::UserProfile> for UserProfile {
|
||||
fn from(value: crate::UserProfile) -> Self {
|
||||
UserProfile {
|
||||
@@ -832,6 +1284,15 @@ impl From<crate::UserProfile> for UserProfile {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserProfile> for crate::UserProfile {
|
||||
fn from(value: UserProfile) -> crate::UserProfile {
|
||||
crate::UserProfile {
|
||||
content: value.content,
|
||||
background: value.background.map(|file| file.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::BotInformation> for BotInformation {
|
||||
fn from(value: crate::BotInformation) -> Self {
|
||||
BotInformation {
|
||||
@@ -839,3 +1300,11 @@ impl From<crate::BotInformation> for BotInformation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BotInformation> for crate::BotInformation {
|
||||
fn from(value: BotInformation) -> crate::BotInformation {
|
||||
crate::BotInformation {
|
||||
owner: value.owner_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,4 @@ pub mod bridge;
|
||||
pub mod idempotency;
|
||||
pub mod permissions;
|
||||
pub mod reference;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test_fixtures;
|
||||
|
||||
@@ -464,6 +464,13 @@ impl<'a> DatabasePermissionQuery<'a> {
|
||||
pub fn member_ref(&self) -> &Option<Cow<Member>> {
|
||||
&self.member
|
||||
}
|
||||
|
||||
/// Get the known member's current ranking
|
||||
pub fn get_member_rank(&self) -> Option<i64> {
|
||||
self.member
|
||||
.as_ref()
|
||||
.map(|member| member.get_ranking(self.server.as_ref().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Short-hand for creating a permission calculator
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use revolt_result::Result;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
use rocket::request::FromParam;
|
||||
@@ -7,7 +9,9 @@ use schemars::{
|
||||
JsonSchema,
|
||||
};
|
||||
|
||||
use crate::{Bot, Channel, Database, Emoji, Message, Server, User, Webhook};
|
||||
use crate::{
|
||||
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
|
||||
};
|
||||
|
||||
/// Reference to some object in the database
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -22,6 +26,11 @@ impl Reference {
|
||||
Reference { id }
|
||||
}
|
||||
|
||||
/// Fetch ban from Ref
|
||||
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
|
||||
db.fetch_ban(server, &self.id).await
|
||||
}
|
||||
|
||||
/// Fetch bot from Ref
|
||||
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
|
||||
db.fetch_bot(&self.id).await
|
||||
@@ -37,11 +46,49 @@ impl Reference {
|
||||
db.fetch_channel(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch invite from Ref or create invite to server if discoverable
|
||||
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
||||
if ulid::Ulid::from_str(&self.id).is_ok() {
|
||||
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
|
||||
pub async fn as_message(&self, db: &Database) -> Result<Message> {
|
||||
db.fetch_message(&self.id).await
|
||||
}
|
||||
|
||||
/// Fetch message from Ref and validate channel
|
||||
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
|
||||
let msg = db.fetch_message(&self.id).await?;
|
||||
if msg.channel != channel {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Fetch member from Ref
|
||||
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
|
||||
db.fetch_member(server, &self.id).await
|
||||
}
|
||||
|
||||
/// Fetch server from Ref
|
||||
pub async fn as_server(&self, db: &Database) -> Result<Server> {
|
||||
db.fetch_server(&self.id).await
|
||||
|
||||
@@ -92,7 +92,7 @@ macro_rules! fixture {
|
||||
|
||||
let fixtures = $crate::util::test_fixtures::load_fixture(
|
||||
&$database,
|
||||
include_str!(concat!("../../../fixtures/", $name, ".json")),
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/fixtures/", $name, ".json")),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.6.13"
|
||||
version = "0.7.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: API Models"
|
||||
|
||||
@@ -12,20 +12,24 @@ description = "Revolt Backend: API Models"
|
||||
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
|
||||
schemas = ["dep:schemars", "revolt-permissions/schemas"]
|
||||
validator = ["dep:validator"]
|
||||
rocket = ["dep:rocket"]
|
||||
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
|
||||
|
||||
default = ["serde", "partials"]
|
||||
default = ["serde", "partials", "rocket"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.6.13", path = "../config" }
|
||||
revolt-permissions = { version = "0.6.13", path = "../permissions" }
|
||||
revolt-config = { version = "0.7.4", path = "../config" }
|
||||
revolt-permissions = { version = "0.7.4", path = "../permissions" }
|
||||
|
||||
# Utility
|
||||
regex = "1"
|
||||
indexmap = "1.9.3"
|
||||
once_cell = "1.17.1"
|
||||
|
||||
# Rocket
|
||||
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
|
||||
|
||||
# Serialisation
|
||||
revolt_optional_struct = { version = "0.2.0", optional = true }
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
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.
|
||||
@@ -30,7 +30,16 @@ macro_rules! auto_derived_partial {
|
||||
#[derive(
|
||||
OptionalStruct, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
#[optional_derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[optional_derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
Default
|
||||
)]
|
||||
#[optional_name = $name]
|
||||
#[opt_skip_serializing_none]
|
||||
#[opt_some_priority]
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::{Channel, File, Server, User};
|
||||
|
||||
auto_derived!(
|
||||
/// Invite
|
||||
#[serde(tag = "type")]
|
||||
pub enum Invite {
|
||||
/// Invite to a specific server channel
|
||||
Server {
|
||||
@@ -24,4 +27,77 @@ auto_derived!(
|
||||
channel: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Public invite response
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum InviteResponse {
|
||||
/// Server channel invite
|
||||
Server {
|
||||
/// Invite code
|
||||
code: String,
|
||||
/// Id of the server
|
||||
server_id: String,
|
||||
/// Name of the server
|
||||
server_name: String,
|
||||
/// Attachment for server icon
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_icon: Option<File>,
|
||||
/// Attachment for server banner
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_banner: Option<File>,
|
||||
/// Enum of server flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
server_flags: Option<i32>,
|
||||
/// Id of server channel
|
||||
channel_id: String,
|
||||
/// Name of server channel
|
||||
channel_name: String,
|
||||
/// Description of server channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_description: Option<String>,
|
||||
/// Name of user who created the invite
|
||||
user_name: String,
|
||||
/// Avatar of the user who created the invite
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_avatar: Option<File>,
|
||||
/// Number of members in this server
|
||||
member_count: i64,
|
||||
},
|
||||
/// Group channel invite
|
||||
Group {
|
||||
/// Invite code
|
||||
code: String,
|
||||
/// Id of group channel
|
||||
channel_id: String,
|
||||
/// Name of group channel
|
||||
channel_name: String,
|
||||
/// Description of group channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_description: Option<String>,
|
||||
/// Name of user who created the invite
|
||||
user_name: String,
|
||||
/// Avatar of the user who created the invite
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_avatar: Option<File>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Invite join response
|
||||
#[serde(tag = "type")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum InviteJoinResponse {
|
||||
Server {
|
||||
/// Channels in the server
|
||||
channels: Vec<Channel>,
|
||||
/// Server we are joining
|
||||
server: Server,
|
||||
},
|
||||
Group {
|
||||
/// Group channel we are joining
|
||||
channel: Channel,
|
||||
/// Members of this group
|
||||
users: Vec<User>,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use super::File;
|
||||
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_permissions::{Override, OverrideField};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::FromForm;
|
||||
|
||||
auto_derived!(
|
||||
/// Channel
|
||||
#[serde(tag = "channel_type")]
|
||||
@@ -258,6 +261,38 @@ auto_derived!(
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
/// New default permissions
|
||||
#[serde(untagged)]
|
||||
pub enum DataDefaultChannelPermissions {
|
||||
Value {
|
||||
/// Permission values to set for members in a `Group`
|
||||
permissions: u64,
|
||||
},
|
||||
Field {
|
||||
/// Allow / deny values to set for members in this `TextChannel` or `VoiceChannel`
|
||||
permissions: Override,
|
||||
},
|
||||
}
|
||||
|
||||
/// New role permissions
|
||||
pub struct DataSetRolePermissions {
|
||||
/// Allow / deny values to set for this role
|
||||
pub permissions: Override,
|
||||
}
|
||||
|
||||
/// Options when deleting a channel
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsChannelDelete {
|
||||
/// Whether to not send a leave message
|
||||
pub leave_silently: Option<bool>,
|
||||
}
|
||||
|
||||
/// Voice server token response
|
||||
pub struct LegacyCreateVoiceUserResponse {
|
||||
/// Token for authenticating with the voice server
|
||||
token: String,
|
||||
}
|
||||
);
|
||||
|
||||
impl Channel {
|
||||
|
||||
@@ -8,9 +8,12 @@ use revolt_config::config;
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::{FromForm, FromFormField};
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
use super::{Embed, File, MessageWebhook, User, Webhook, RE_COLOUR};
|
||||
use super::{Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR};
|
||||
|
||||
pub static RE_MENTION: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
|
||||
@@ -66,6 +69,24 @@ auto_derived_partial!(
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Bulk Message Response
|
||||
#[serde(untagged)]
|
||||
pub enum BulkMessageResponse {
|
||||
JustMessages(
|
||||
/// List of messages
|
||||
Vec<Message>,
|
||||
),
|
||||
MessagesAndUsers {
|
||||
/// List of messages
|
||||
messages: Vec<Message>,
|
||||
/// List of users
|
||||
users: Vec<User>,
|
||||
/// List of members
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
members: Option<Vec<Member>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// System Event
|
||||
#[serde(tag = "type")]
|
||||
pub enum SystemMessage {
|
||||
@@ -136,6 +157,7 @@ auto_derived!(
|
||||
///
|
||||
/// Sort used for retrieving messages
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "rocket", derive(FromFormField))]
|
||||
pub enum MessageSort {
|
||||
/// Sort by the most relevant messages
|
||||
#[default]
|
||||
@@ -169,16 +191,19 @@ auto_derived!(
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct SendableEmbed {
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub icon_url: Option<String>,
|
||||
#[validate(length(min = 1, max = 256))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
|
||||
pub url: Option<String>,
|
||||
#[validate(length(min = 1, max = 100))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
|
||||
pub title: Option<String>,
|
||||
#[validate(length(min = 1, max = 2000))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
|
||||
pub description: Option<String>,
|
||||
pub media: Option<String>,
|
||||
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
|
||||
)]
|
||||
pub colour: Option<String>,
|
||||
}
|
||||
|
||||
@@ -196,11 +221,11 @@ auto_derived!(
|
||||
/// Unique token to prevent duplicate message sending
|
||||
///
|
||||
/// **This is deprecated and replaced by `Idempotency-Key`!**
|
||||
#[validate(length(min = 1, max = 64))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
|
||||
pub nonce: Option<String>,
|
||||
|
||||
/// Message content to send
|
||||
#[validate(length(min = 0, max = 2000))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
|
||||
pub content: Option<String>,
|
||||
/// Attachments to include in message
|
||||
pub attachments: Option<Vec<String>>,
|
||||
@@ -209,14 +234,100 @@ auto_derived!(
|
||||
/// Embeds to include in message
|
||||
///
|
||||
/// Text embed content contributes to the content length cap
|
||||
#[validate]
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub embeds: Option<Vec<SendableEmbed>>,
|
||||
/// Masquerade to apply to this message
|
||||
#[validate]
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub masquerade: Option<Masquerade>,
|
||||
/// Information about how this message should be interacted with
|
||||
pub interactions: Option<Interactions>,
|
||||
}
|
||||
|
||||
/// Options for querying messages
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsQueryMessages {
|
||||
/// Maximum number of messages to fetch
|
||||
///
|
||||
/// For fetching nearby messages, this is \`(limit + 1)\`.
|
||||
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
||||
pub limit: Option<i64>,
|
||||
/// Message id before which messages should be fetched
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub before: Option<String>,
|
||||
/// Message id after which messages should be fetched
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub after: Option<String>,
|
||||
/// Message sort direction
|
||||
pub sort: Option<MessageSort>,
|
||||
/// Message id to search around
|
||||
///
|
||||
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
|
||||
/// It will also take half of limit rounded as the limits to each side.
|
||||
/// It also fetches the message ID specified.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub nearby: Option<String>,
|
||||
/// Whether to include user (and member, if server channel) objects
|
||||
pub include_users: Option<bool>,
|
||||
}
|
||||
|
||||
/// Options for searching for messages
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataMessageSearch {
|
||||
/// Full-text search query
|
||||
///
|
||||
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
|
||||
pub query: String,
|
||||
|
||||
/// Maximum number of messages to fetch
|
||||
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
||||
pub limit: Option<i64>,
|
||||
/// Message id before which messages should be fetched
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub before: Option<String>,
|
||||
/// Message id after which messages should be fetched
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 26, max = 26)))]
|
||||
pub after: Option<String>,
|
||||
/// Message sort direction
|
||||
///
|
||||
/// By default, it will be sorted by latest.
|
||||
#[cfg_attr(feature = "serde", serde(default = "MessageSort::default"))]
|
||||
pub sort: MessageSort,
|
||||
/// Whether to include user (and member, if server channel) objects
|
||||
pub include_users: Option<bool>,
|
||||
}
|
||||
|
||||
/// Changes to make to message
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditMessage {
|
||||
/// New message content
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2000)))]
|
||||
pub content: Option<String>,
|
||||
/// Embeds to include in the message
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 10)))]
|
||||
pub embeds: Option<Vec<SendableEmbed>>,
|
||||
}
|
||||
|
||||
/// Options for bulk deleting messages
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
cfg_attr(feature = "validator", derive(Validate))
|
||||
)]
|
||||
pub struct OptionsBulkDelete {
|
||||
/// Message IDs
|
||||
#[validate(length(min = 1, max = 100))]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Options for removing reaction
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsUnreact {
|
||||
/// Remove a specific user's reaction
|
||||
pub user_id: Option<String>,
|
||||
/// Remove all reactions
|
||||
pub remove_all: Option<bool>,
|
||||
}
|
||||
);
|
||||
|
||||
/// Message Author Abstraction
|
||||
|
||||
@@ -7,6 +7,7 @@ mod embeds;
|
||||
mod emojis;
|
||||
mod files;
|
||||
mod messages;
|
||||
mod safety_reports;
|
||||
mod server_bans;
|
||||
mod server_members;
|
||||
mod servers;
|
||||
@@ -22,6 +23,7 @@ pub use embeds::*;
|
||||
pub use emojis::*;
|
||||
pub use files::*;
|
||||
pub use messages::*;
|
||||
pub use safety_reports::*;
|
||||
pub use server_bans::*;
|
||||
pub use server_members::*;
|
||||
pub use servers::*;
|
||||
|
||||
148
crates/core/models/src/v0/safety_reports.rs
Normal file
148
crates/core/models/src/v0/safety_reports.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
auto_derived!(
|
||||
/// User-generated platform moderation report
|
||||
pub struct Report {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Id of the user creating this report
|
||||
pub author_id: String,
|
||||
/// Reported content
|
||||
pub content: ReportedContent,
|
||||
/// Additional report context
|
||||
pub additional_context: String,
|
||||
/// Status of the report
|
||||
#[serde(flatten)]
|
||||
pub status: ReportStatus,
|
||||
/// Additional notes included on the report
|
||||
#[serde(default)]
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
/// Reason for reporting content (message or server)
|
||||
pub enum ContentReportReason {
|
||||
/// No reason has been specified
|
||||
NoneSpecified,
|
||||
|
||||
/// Illegal content catch-all reason
|
||||
Illegal,
|
||||
|
||||
/// Selling or facilitating use of drugs or other illegal goods
|
||||
IllegalGoods,
|
||||
|
||||
/// Extortion or blackmail
|
||||
IllegalExtortion,
|
||||
|
||||
/// Revenge or child pornography
|
||||
IllegalPornography,
|
||||
|
||||
/// Illegal hacking activity
|
||||
IllegalHacking,
|
||||
|
||||
/// Extreme violence, gore, or animal cruelty
|
||||
/// With exception to violence potrayed in media / creative arts
|
||||
ExtremeViolence,
|
||||
|
||||
/// Content that promotes harm to others / self
|
||||
PromotesHarm,
|
||||
|
||||
/// Unsolicited advertisements
|
||||
UnsolicitedSpam,
|
||||
|
||||
/// This is a raid
|
||||
Raid,
|
||||
|
||||
/// Spam or platform abuse
|
||||
SpamAbuse,
|
||||
|
||||
/// Scams or fraud
|
||||
ScamsFraud,
|
||||
|
||||
/// Distribution of malware or malicious links
|
||||
Malware,
|
||||
|
||||
/// Harassment or abuse targeted at another user
|
||||
Harassment,
|
||||
}
|
||||
|
||||
/// Reason for reporting a user
|
||||
pub enum UserReportReason {
|
||||
/// No reason has been specified
|
||||
NoneSpecified,
|
||||
|
||||
/// Unsolicited advertisements
|
||||
UnsolicitedSpam,
|
||||
|
||||
/// User is sending spam or otherwise abusing the platform
|
||||
SpamAbuse,
|
||||
|
||||
/// User's profile contains inappropriate content for a general audience
|
||||
InappropriateProfile,
|
||||
|
||||
/// User is impersonating another user
|
||||
Impersonation,
|
||||
|
||||
/// User is evading a ban
|
||||
BanEvasion,
|
||||
|
||||
/// User is not of minimum age to use the platform
|
||||
Underage,
|
||||
}
|
||||
|
||||
/// The content being reported
|
||||
#[serde(tag = "type")]
|
||||
pub enum ReportedContent {
|
||||
/// Report a message
|
||||
Message {
|
||||
/// ID of the message
|
||||
id: String,
|
||||
/// Reason for reporting message
|
||||
report_reason: ContentReportReason,
|
||||
},
|
||||
/// Report a server
|
||||
Server {
|
||||
/// ID of the server
|
||||
id: String,
|
||||
/// Reason for reporting server
|
||||
report_reason: ContentReportReason,
|
||||
},
|
||||
/// Report a user
|
||||
User {
|
||||
/// ID of the user
|
||||
id: String,
|
||||
/// Reason for reporting a user
|
||||
report_reason: UserReportReason,
|
||||
/// Message context
|
||||
message_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Status of the report
|
||||
#[serde(tag = "status")]
|
||||
pub enum ReportStatus {
|
||||
/// Report is waiting for triage / action
|
||||
Created {},
|
||||
|
||||
/// Report was rejected
|
||||
Rejected {
|
||||
rejection_reason: String,
|
||||
closed_at: Option<Timestamp>,
|
||||
},
|
||||
|
||||
/// Report was actioned and resolved
|
||||
Resolved { closed_at: Option<Timestamp> },
|
||||
}
|
||||
|
||||
/// Just the status of the report
|
||||
pub enum ReportStatusString {
|
||||
/// Report is waiting for triage / action
|
||||
Created,
|
||||
|
||||
/// Report was rejected
|
||||
Rejected,
|
||||
|
||||
/// Report was actioned and resolved
|
||||
Resolved,
|
||||
}
|
||||
);
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::MemberCompositeKey;
|
||||
use super::{File, MemberCompositeKey, User};
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
auto_derived!(
|
||||
/// Server Ban
|
||||
@@ -9,4 +12,44 @@ auto_derived!(
|
||||
/// Reason for ban creation
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Information for new server ban
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataBanCreate {
|
||||
/// Ban reason
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Just enough information to list a ban
|
||||
pub struct BannedUser {
|
||||
/// Id of the banned user
|
||||
#[cfg_attr(feature = "serde", serde(rename = "_id"))]
|
||||
pub id: String,
|
||||
/// Username of the banned user
|
||||
pub username: String,
|
||||
/// Discriminator of the banned user
|
||||
pub discriminator: String,
|
||||
/// Avatar of the banned user
|
||||
pub avatar: Option<File>,
|
||||
}
|
||||
|
||||
/// Ban list result
|
||||
pub struct BanListResult {
|
||||
/// Users objects
|
||||
pub users: Vec<BannedUser>,
|
||||
/// Ban objects
|
||||
pub bans: Vec<ServerBan>,
|
||||
}
|
||||
);
|
||||
|
||||
impl From<User> for BannedUser {
|
||||
fn from(user: User) -> Self {
|
||||
BannedUser {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
discriminator: user.discriminator,
|
||||
avatar: user.avatar,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
use super::File;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{File, Role, User};
|
||||
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::FromForm;
|
||||
|
||||
/// Regex for valid role colours
|
||||
///
|
||||
/// Allows the use of named colours, rgb(a), variables and all gradients.
|
||||
@@ -77,4 +85,46 @@ auto_derived!(
|
||||
Kick,
|
||||
Ban,
|
||||
}
|
||||
|
||||
/// Member response
|
||||
#[serde(untagged)]
|
||||
pub enum MemberResponse {
|
||||
Member(Member),
|
||||
MemberWithRoles {
|
||||
member: Member,
|
||||
roles: HashMap<String, Role>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Options for fetching all members
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsFetchAllMembers {
|
||||
/// Whether to exclude offline users
|
||||
pub exclude_offline: Option<bool>,
|
||||
}
|
||||
|
||||
/// Response with all members
|
||||
pub struct AllMemberResponse {
|
||||
/// List of members
|
||||
pub members: Vec<Member>,
|
||||
/// List of users
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
/// New member information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataMemberEdit {
|
||||
/// Member nickname
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub nickname: Option<String>,
|
||||
/// Attachment Id to set for avatar
|
||||
pub avatar: Option<String>,
|
||||
/// Array of role ids
|
||||
pub roles: Option<Vec<String>>,
|
||||
/// Timestamp this member is timed out until
|
||||
pub timeout: Option<Timestamp>,
|
||||
/// Fields to remove from channel object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsMember>>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use super::{Channel, File};
|
||||
use super::{Channel, File, RE_COLOUR};
|
||||
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_permissions::{Override, OverrideField};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::FromForm;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server
|
||||
pub struct Server {
|
||||
@@ -120,10 +123,13 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// Channel category
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct Category {
|
||||
/// Unique ID for this category
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub id: String,
|
||||
/// Title for this category
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub title: String,
|
||||
/// Channels in this category
|
||||
pub channels: Vec<String>,
|
||||
@@ -160,6 +166,26 @@ auto_derived!(
|
||||
pub nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
/// Information about new role to create
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataCreateRole {
|
||||
/// Role name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: String,
|
||||
/// Ranking position
|
||||
///
|
||||
/// Smaller values take priority.
|
||||
pub rank: Option<i64>,
|
||||
}
|
||||
|
||||
/// Response after creating new role
|
||||
pub struct NewRoleResponse {
|
||||
/// Id of the role
|
||||
pub id: String,
|
||||
/// New role
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
/// Information returned when creating server
|
||||
pub struct CreateServerLegacyResponse {
|
||||
/// Server object
|
||||
@@ -167,4 +193,97 @@ auto_derived!(
|
||||
/// Default channels
|
||||
pub channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
/// Options when fetching server
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsFetchServer {
|
||||
/// Whether to include channels
|
||||
pub include_channels: Option<bool>,
|
||||
}
|
||||
|
||||
/// Fetch server information
|
||||
#[serde(untagged)]
|
||||
pub enum FetchServerResponse {
|
||||
JustServer(Server),
|
||||
ServerWithChannels {
|
||||
#[serde(flatten)]
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
},
|
||||
}
|
||||
|
||||
/// New server information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditServer {
|
||||
/// Server name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: Option<String>,
|
||||
/// Server description
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Attachment Id for icon
|
||||
pub icon: Option<String>,
|
||||
/// Attachment Id for banner
|
||||
pub banner: Option<String>,
|
||||
|
||||
/// Category structure for server
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub categories: Option<Vec<Category>>,
|
||||
/// System message configuration
|
||||
pub system_messages: Option<SystemMessageChannels>,
|
||||
|
||||
/// Bitfield of server flags
|
||||
#[cfg_attr(feature = "validator", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub flags: Option<i32>,
|
||||
|
||||
// Whether this server is age-restricted
|
||||
// nsfw: Option<bool>,
|
||||
/// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg)
|
||||
pub discoverable: Option<bool>,
|
||||
/// Whether analytics should be collected for this server
|
||||
///
|
||||
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
|
||||
pub analytics: Option<bool>,
|
||||
|
||||
/// Fields to remove from server object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsServer>>,
|
||||
}
|
||||
|
||||
/// New role information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditRole {
|
||||
/// Role name
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
|
||||
pub name: Option<String>,
|
||||
/// Role colour
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 1, max = 128), regex = "RE_COLOUR")
|
||||
)]
|
||||
pub colour: Option<String>,
|
||||
/// Whether this role should be displayed separately
|
||||
pub hoist: Option<bool>,
|
||||
/// Ranking position
|
||||
///
|
||||
/// Smaller values take priority.
|
||||
pub rank: Option<i64>,
|
||||
/// Fields to remove from role object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsRole>>,
|
||||
}
|
||||
|
||||
/// New role permissions
|
||||
pub struct DataSetServerRolePermission {
|
||||
/// Allow / deny values for the role in this server.
|
||||
pub permissions: Override,
|
||||
}
|
||||
|
||||
/// Options when leaving a server
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsServerDelete {
|
||||
/// Whether to not send a leave message
|
||||
pub leave_silently: Option<bool>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
#[cfg(feature = "rocket")]
|
||||
use rocket::FromForm;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// HashMap of user settings
|
||||
/// Each key is mapped to a tuple consisting of the
|
||||
/// revision timestamp and serialised data (in JSON format)
|
||||
pub type UserSettings = HashMap<String, (i64, String)>;
|
||||
|
||||
auto_derived!(
|
||||
/// Options for fetching settings
|
||||
pub struct OptionsFetchSettings {
|
||||
/// Keys to fetch
|
||||
pub keys: Vec<String>,
|
||||
}
|
||||
|
||||
/// Additional options for inserting settings
|
||||
#[cfg_attr(feature = "rocket", derive(FromForm))]
|
||||
pub struct OptionsSetSettings {
|
||||
/// Timestamp of settings change.
|
||||
///
|
||||
/// Used to avoid feedback loops.
|
||||
pub timestamp: Option<i64>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -3,12 +3,21 @@ use regex::Regex;
|
||||
|
||||
use super::File;
|
||||
|
||||
#[cfg(feature = "validator")]
|
||||
use validator::Validate;
|
||||
|
||||
/// Regex for valid usernames
|
||||
///
|
||||
/// Block zero width space
|
||||
/// Block lookalike characters
|
||||
pub static RE_USERNAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap());
|
||||
|
||||
/// Regex for valid display names
|
||||
///
|
||||
/// Block zero width space
|
||||
/// Block newline and carriage return
|
||||
pub static RE_DISPLAY_NAME: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[^\u200B\n\r]+$").unwrap());
|
||||
|
||||
auto_derived_partial!(
|
||||
/// User
|
||||
pub struct User {
|
||||
@@ -123,20 +132,26 @@ auto_derived!(
|
||||
}
|
||||
|
||||
/// User's active status
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct UserStatus {
|
||||
/// Custom status text
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
|
||||
pub text: String,
|
||||
#[validate(length(min = 0, max = 128))]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub text: Option<String>,
|
||||
/// Current presence option
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub presence: Option<Presence>,
|
||||
}
|
||||
|
||||
/// User's profile
|
||||
#[derive(Default)]
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct UserProfile {
|
||||
/// Text content on user's profile
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "String::is_empty"))]
|
||||
pub content: String,
|
||||
#[validate(length(min = 0, max = 2000))]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub content: Option<String>,
|
||||
/// Background visible on user's profile
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub background: Option<File>,
|
||||
@@ -182,12 +197,79 @@ auto_derived!(
|
||||
Spam = 8,
|
||||
}
|
||||
|
||||
/// New user profile data
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataUserProfile {
|
||||
/// Text to set as user profile description
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub content: Option<String>,
|
||||
/// Attachment Id for background
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub background: Option<String>,
|
||||
}
|
||||
|
||||
/// New user information
|
||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||
pub struct DataEditUser {
|
||||
/// New display name
|
||||
#[cfg_attr(
|
||||
feature = "validator",
|
||||
validate(length(min = 2, max = 32), regex = "RE_DISPLAY_NAME")
|
||||
)]
|
||||
pub display_name: Option<String>,
|
||||
/// Attachment Id for avatar
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
||||
pub avatar: Option<String>,
|
||||
|
||||
/// New user status
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub status: Option<UserStatus>,
|
||||
/// New user profile data
|
||||
///
|
||||
/// This is applied as a partial.
|
||||
#[cfg_attr(feature = "validator", validate)]
|
||||
pub profile: Option<DataUserProfile>,
|
||||
|
||||
/// Bitfield of user badges
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub badges: Option<i32>,
|
||||
/// Enum of user flags
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub flags: Option<i32>,
|
||||
|
||||
/// Fields to remove from user object
|
||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
||||
pub remove: Option<Vec<FieldsUser>>,
|
||||
}
|
||||
|
||||
/// User flag reponse
|
||||
pub struct FlagResponse {
|
||||
/// Flags
|
||||
pub flags: i32,
|
||||
}
|
||||
|
||||
/// Mutual friends and servers response
|
||||
pub struct MutualResponse {
|
||||
/// Array of mutual user IDs that both users are friends with
|
||||
pub users: Vec<String>,
|
||||
/// Array of mutual server IDs that both users are in
|
||||
pub servers: Vec<String>,
|
||||
}
|
||||
|
||||
/// Bot information for if the user is a bot
|
||||
pub struct BotInformation {
|
||||
/// Id of the owner of this bot
|
||||
#[cfg_attr(feature = "serde", serde(rename = "owner"))]
|
||||
pub owner_id: String,
|
||||
}
|
||||
|
||||
/// User lookup information
|
||||
pub struct DataSendFriendRequest {
|
||||
/// Username and discriminator combo separated by #
|
||||
pub username: String,
|
||||
}
|
||||
);
|
||||
|
||||
pub trait CheckRelationship {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-permissions"
|
||||
version = "0.6.13"
|
||||
version = "0.7.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Permission Logic"
|
||||
|
||||
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.6.13", path = "../result" }
|
||||
revolt-result = { version = "0.7.4", path = "../result" }
|
||||
|
||||
# Utility
|
||||
auto_ops = "0.3.0"
|
||||
|
||||
9
crates/core/permissions/LICENSE
Normal file
9
crates/core/permissions/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.
|
||||
@@ -53,6 +53,17 @@ impl PermissionValue {
|
||||
self.has(permission as u64)
|
||||
}
|
||||
|
||||
/// Throw if missing user permission
|
||||
pub fn throw_if_lacking_user_permission(&self, permission: UserPermission) -> Result<()> {
|
||||
if self.has_user_permission(permission) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(MissingPermission {
|
||||
permission: permission.to_string()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Throw if missing channel permission
|
||||
pub fn throw_if_lacking_channel_permission(&self, permission: ChannelPermission) -> Result<()> {
|
||||
if self.has_channel_permission(permission) {
|
||||
@@ -63,6 +74,35 @@ impl PermissionValue {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Throw an error if we cannot grant permissions on either allows or denies
|
||||
/// going from the previous given value to the next given value.
|
||||
///
|
||||
/// We need to check any:
|
||||
/// - allows added (permissions now granted)
|
||||
/// - denies removed (permissions now neutral or granted)
|
||||
pub async fn throw_permission_override<C>(
|
||||
&self,
|
||||
current_value: C,
|
||||
next_value: &Override,
|
||||
) -> Result<()>
|
||||
where
|
||||
C: Into<Option<Override>>,
|
||||
{
|
||||
let current_value = current_value.into();
|
||||
|
||||
if let Some(current_value) = current_value {
|
||||
if !self.has(!current_value.allows() & next_value.allows())
|
||||
|| !self.has(current_value.denies() & !next_value.denies())
|
||||
{
|
||||
return Err(create_error!(CannotGiveMissingPermissions));
|
||||
}
|
||||
} else if !self.has(next_value.allows()) {
|
||||
return Err(create_error!(CannotGiveMissingPermissions));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for PermissionValue {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-presence"
|
||||
version = "0.6.13"
|
||||
version = "0.7.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "revolt-result"
|
||||
version = "0.6.13"
|
||||
version = "0.7.4"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Result and Error types"
|
||||
|
||||
@@ -12,7 +12,7 @@ description = "Revolt Backend: Result and Error types"
|
||||
serde = ["dep:serde"]
|
||||
schemas = ["dep:schemars"]
|
||||
rocket = ["dep:rocket", "dep:serde_json"]
|
||||
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi"]
|
||||
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
|
||||
|
||||
default = ["serde"]
|
||||
|
||||
|
||||
9
crates/core/result/LICENSE
Normal file
9
crates/core/result/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.
|
||||
@@ -147,7 +147,7 @@ macro_rules! create_error {
|
||||
#[macro_export]
|
||||
macro_rules! create_database_error {
|
||||
( $operation: expr, $collection: expr ) => {
|
||||
create_error!(DatabaseError {
|
||||
$crate::create_error!(DatabaseError {
|
||||
operation: $operation.to_string(),
|
||||
collection: $collection.to_string()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.6.13"
|
||||
version = "0.7.4"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
@@ -36,6 +36,7 @@ nanoid = "0.4.0"
|
||||
serde_json = "1.0.57"
|
||||
serde = { version = "1.0.115", features = ["derive"] }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
iso8601-timestamp = { version = "0.2.11", features = [] }
|
||||
|
||||
# async
|
||||
futures = "0.3.8"
|
||||
@@ -64,9 +65,6 @@ rocket_prometheus = "0.10.0-rc.3"
|
||||
schemars = "0.8.8"
|
||||
revolt_rocket_okapi = { version = "0.9.1", features = ["swagger"] }
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# core
|
||||
authifier = "1.0.8"
|
||||
revolt-config = { path = "../core/config" }
|
||||
@@ -74,7 +72,11 @@ revolt-database = { path = "../core/database", features = [
|
||||
"rocket-impl",
|
||||
"redis-is-patched",
|
||||
] }
|
||||
revolt-models = { path = "../core/models", features = ["schemas", "validator"] }
|
||||
revolt-models = { path = "../core/models", features = [
|
||||
"schemas",
|
||||
"validator",
|
||||
"rocket",
|
||||
] }
|
||||
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
|
||||
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ extern crate serde_json;
|
||||
pub mod routes;
|
||||
pub mod util;
|
||||
|
||||
use revolt_config::config;
|
||||
use revolt_database::events::client::EventV1;
|
||||
use revolt_database::{Database, MongoDb};
|
||||
use rocket::{Build, Rocket};
|
||||
use rocket_cors::{AllowedOrigins, CorsOptions};
|
||||
@@ -16,19 +18,24 @@ use std::net::Ipv4Addr;
|
||||
use std::str::FromStr;
|
||||
|
||||
use async_std::channel::unbounded;
|
||||
use revolt_quark::authifier::{Authifier, AuthifierEvent};
|
||||
use revolt_quark::events::client::EventV1;
|
||||
use revolt_quark::DatabaseInfo;
|
||||
use authifier::config::{
|
||||
Captcha, Config as AuthifierConfig, EmailVerificationConfig, ResolveIp, SMTPSettings, Shield,
|
||||
Template, Templates,
|
||||
};
|
||||
use authifier::{Authifier, AuthifierEvent};
|
||||
use rocket::data::ToByteUnit;
|
||||
|
||||
pub async fn web() -> Rocket<Build> {
|
||||
// Get settings
|
||||
let config = config().await;
|
||||
|
||||
// Ensure environment variables are present
|
||||
config.preflight_checks();
|
||||
|
||||
// Setup database
|
||||
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Legacy database setup from quark
|
||||
let legacy_db = DatabaseInfo::Auto.connect().await.unwrap();
|
||||
|
||||
// Setup Authifier event channel
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
@@ -40,7 +47,8 @@ pub async fn web() -> Rocket<Build> {
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: revolt_quark::util::authifier::config(),
|
||||
config: Default::default(),
|
||||
// config: authifier_config().await,
|
||||
event_channel: Some(sender),
|
||||
};
|
||||
|
||||
@@ -65,10 +73,6 @@ pub async fn web() -> Rocket<Build> {
|
||||
db.clone(),
|
||||
authifier.database.clone(),
|
||||
));
|
||||
async_std::task::spawn(revolt_quark::tasks::start_workers(
|
||||
legacy_db.clone(),
|
||||
authifier.database.clone(),
|
||||
));
|
||||
|
||||
// Configure CORS
|
||||
let cors = CorsOptions {
|
||||
@@ -97,7 +101,7 @@ pub async fn web() -> Rocket<Build> {
|
||||
let rocket = rocket::build();
|
||||
let prometheus = PrometheusMetrics::new();
|
||||
|
||||
routes::mount(rocket)
|
||||
routes::mount(config, rocket)
|
||||
.attach(prometheus.clone())
|
||||
.mount("/metrics", prometheus)
|
||||
.mount("/", rocket_cors::catch_all_options_routes())
|
||||
@@ -105,7 +109,6 @@ pub async fn web() -> Rocket<Build> {
|
||||
.mount("/swagger/", swagger)
|
||||
.manage(authifier)
|
||||
.manage(db)
|
||||
.manage(legacy_db)
|
||||
.manage(cors.clone())
|
||||
.attach(util::ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
@@ -116,13 +119,82 @@ pub async fn web() -> Rocket<Build> {
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn authifier_config() -> AuthifierConfig {
|
||||
let config = config().await;
|
||||
|
||||
let mut auth_config = AuthifierConfig {
|
||||
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;
|
||||
}
|
||||
|
||||
auth_config
|
||||
}
|
||||
|
||||
#[launch]
|
||||
async fn rocket() -> _ {
|
||||
// Configure logging and environment
|
||||
revolt_quark::configure!();
|
||||
|
||||
// Ensure environment variables are present
|
||||
revolt_quark::variables::delta::preflight_checks();
|
||||
revolt_config::configure!(api);
|
||||
|
||||
// Start web server
|
||||
web().await
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
|
||||
use rocket::Route;
|
||||
|
||||
mod stats;
|
||||
|
||||
pub fn routes() -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![stats::stats]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use revolt_quark::models::stats::Stats;
|
||||
use revolt_quark::{Db, Result};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
|
||||
/// # Query Stats
|
||||
///
|
||||
/// Fetch various technical statistics.
|
||||
#[openapi(tag = "Admin")]
|
||||
#[get("/stats")]
|
||||
pub async fn stats(db: &Db) -> Result<Json<Stats>> {
|
||||
Ok(Json(db.generate_stats().await?))
|
||||
}
|
||||
@@ -1,23 +1,84 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, User,
|
||||
};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Acknowledge Message
|
||||
///
|
||||
/// Lets the server and all other clients know that we've seen this message id in this channel.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[put("/<target>/ack/<message>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, message: Ref) -> Result<EmptyResponse> {
|
||||
pub async fn ack(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
message: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(db, Permission::ViewChannel)
|
||||
.await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||
|
||||
channel
|
||||
.ack(&user.id, &message.id)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{rocket, util::test::TestHarness};
|
||||
use revolt_database::{events::client::EventV1, Channel};
|
||||
use revolt_models::v0::DataCreateGroup;
|
||||
use rocket::http::{Header, Status};
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn success_ack_channel() {
|
||||
let mut harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
|
||||
let group = Channel::create_group(
|
||||
&harness.db,
|
||||
DataCreateGroup {
|
||||
..Default::default()
|
||||
},
|
||||
user.id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("`Channel`");
|
||||
|
||||
let message_id = ulid::Ulid::new().to_string();
|
||||
let response = harness
|
||||
.client
|
||||
.put(format!("/channels/{}/ack/{}", group.id(), message_id))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::NoContent);
|
||||
drop(response);
|
||||
|
||||
let event = harness
|
||||
.wait_for_event(&format!("{}!", user.id), |event| match event {
|
||||
EventV1::ChannelAck { id, .. } => id == &group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
|
||||
match event {
|
||||
EventV1::ChannelAck {
|
||||
message_id: m_id, ..
|
||||
} => assert_eq!(m_id, message_id),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
use revolt_quark::{
|
||||
models::{channel::PartialChannel, Channel, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, PartialChannel, User,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Query Parameters
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
|
||||
pub struct OptionsChannelDelete {
|
||||
/// Whether to not send a leave message
|
||||
leave_silently: Option<bool>,
|
||||
}
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Close Channel
|
||||
///
|
||||
/// Deletes a server channel, leaves a group or closes a group.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[delete("/<target>?<options..>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
pub async fn delete(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: OptionsChannelDelete,
|
||||
target: Reference,
|
||||
options: v0::OptionsChannelDelete,
|
||||
) -> Result<EmptyResponse> {
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
let mut perms = perms(&user).channel(&channel);
|
||||
perms.throw_permission(db, Permission::ViewChannel).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
let permissions = calculate_channel_permissions(&mut query).await;
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||
|
||||
match &channel {
|
||||
Channel::SavedMessages { .. } => Err(Error::NoEffect),
|
||||
Channel::SavedMessages { .. } => Err(create_error!(NoEffect)),
|
||||
Channel::DirectMessage { .. } => channel
|
||||
.update(
|
||||
db,
|
||||
@@ -41,20 +39,58 @@ pub async fn req(
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::Group { .. } => channel
|
||||
.remove_user_from_group(
|
||||
db,
|
||||
&user.id,
|
||||
None,
|
||||
options.leave_silently.unwrap_or_default(),
|
||||
)
|
||||
.remove_user_from_group(db, &user, None, options.leave_silently.unwrap_or_default())
|
||||
.await
|
||||
.map(|_| EmptyResponse),
|
||||
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {
|
||||
perms
|
||||
.throw_permission(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
||||
channel.delete(db).await.map(|_| EmptyResponse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{rocket, util::test::TestHarness};
|
||||
use revolt_database::{events::client::EventV1, Channel};
|
||||
use revolt_models::v0::DataCreateGroup;
|
||||
use rocket::http::{Header, Status};
|
||||
|
||||
#[rocket::async_test]
|
||||
async fn success_delete_group() {
|
||||
let mut harness = TestHarness::new().await;
|
||||
let (_, session, user) = harness.new_user().await;
|
||||
|
||||
let group = Channel::create_group(
|
||||
&harness.db,
|
||||
DataCreateGroup {
|
||||
..Default::default()
|
||||
},
|
||||
user.id.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("`Channel`");
|
||||
|
||||
let response = harness
|
||||
.client
|
||||
.delete(format!("/channels/{}", group.id()))
|
||||
.header(Header::new("x-session-token", session.token.to_string()))
|
||||
.dispatch()
|
||||
.await;
|
||||
|
||||
assert_eq!(response.status(), Status::NoContent);
|
||||
drop(response);
|
||||
|
||||
harness
|
||||
.wait_for_event(&group.id(), |event| match event {
|
||||
EventV1::ChannelDelete { id, .. } => id == &group.id(),
|
||||
_ => false,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// TEST: member leaves group (no delete)
|
||||
// TEST: no effect with saved messages
|
||||
// TEST: DM set to inactive
|
||||
// TEST: server channel deleted
|
||||
}
|
||||
|
||||
@@ -1,60 +1,36 @@
|
||||
use revolt_quark::{
|
||||
models::{
|
||||
channel::{Channel, FieldsChannel, PartialChannel},
|
||||
message::SystemMessage,
|
||||
File, User,
|
||||
},
|
||||
perms, Database, Error, Permission, Ref, Result,
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, File, PartialChannel, SystemMessage, User,
|
||||
};
|
||||
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Channel Details
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataEditChannel {
|
||||
/// Channel name
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
name: Option<String>,
|
||||
/// Channel description
|
||||
#[validate(length(min = 0, max = 1024))]
|
||||
description: Option<String>,
|
||||
/// Group owner
|
||||
owner: Option<String>,
|
||||
/// Icon
|
||||
///
|
||||
/// Provide an Autumn attachment Id.
|
||||
#[validate(length(min = 1, max = 128))]
|
||||
icon: Option<String>,
|
||||
/// Whether this channel is age-restricted
|
||||
nsfw: Option<bool>,
|
||||
/// Whether this channel is archived
|
||||
archived: Option<bool>,
|
||||
#[validate(length(min = 1))]
|
||||
remove: Option<Vec<FieldsChannel>>,
|
||||
}
|
||||
|
||||
/// # Edit Channel
|
||||
///
|
||||
/// Edit a channel object by its id.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[patch("/<target>", data = "<data>")]
|
||||
pub async fn req(
|
||||
pub async fn edit(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Ref,
|
||||
data: Json<DataEditChannel>,
|
||||
) -> Result<Json<Channel>> {
|
||||
target: Reference,
|
||||
data: Json<v0::DataEditChannel>,
|
||||
) -> Result<Json<v0::Channel>> {
|
||||
let data = data.into_inner();
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
data.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::ManageChannel)
|
||||
.await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
|
||||
|
||||
if data.name.is_none()
|
||||
&& data.description.is_none()
|
||||
@@ -63,7 +39,7 @@ pub async fn req(
|
||||
&& data.owner.is_none()
|
||||
&& data.remove.is_none()
|
||||
{
|
||||
return Ok(Json(channel));
|
||||
return Ok(Json(channel.into()));
|
||||
}
|
||||
|
||||
let mut partial: PartialChannel = Default::default();
|
||||
@@ -76,12 +52,12 @@ pub async fn req(
|
||||
{
|
||||
// Make sure we are the owner of this group
|
||||
if owner != &user.id {
|
||||
return Err(Error::NotOwner);
|
||||
return Err(create_error!(NotOwner));
|
||||
}
|
||||
|
||||
// Ensure user is part of group
|
||||
if !recipients.contains(&new_owner) {
|
||||
return Err(Error::NotInGroup);
|
||||
return Err(create_error!(NotInGroup));
|
||||
}
|
||||
|
||||
// Transfer ownership
|
||||
@@ -94,10 +70,10 @@ pub async fn req(
|
||||
to: new_owner,
|
||||
}
|
||||
} else {
|
||||
return Err(Error::InvalidOperation);
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.send(db, user.as_author_for_system(), &channel, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
@@ -128,7 +104,7 @@ pub async fn req(
|
||||
..
|
||||
} => {
|
||||
if let Some(fields) = &data.remove {
|
||||
if fields.contains(&FieldsChannel::Icon) {
|
||||
if fields.contains(&v0::FieldsChannel::Icon) {
|
||||
if let Some(icon) = &icon {
|
||||
db.mark_attachment_as_deleted(&icon.id).await?;
|
||||
}
|
||||
@@ -136,10 +112,10 @@ pub async fn req(
|
||||
|
||||
for field in fields {
|
||||
match field {
|
||||
FieldsChannel::Description => {
|
||||
v0::FieldsChannel::Description => {
|
||||
description.take();
|
||||
}
|
||||
FieldsChannel::Icon => {
|
||||
v0::FieldsChannel::Icon => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
@@ -175,7 +151,7 @@ pub async fn req(
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.send(db, user.as_author_for_system(), &channel, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
@@ -185,26 +161,36 @@ pub async fn req(
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.send(db, user.as_author_for_system(), &channel, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
if partial.icon.is_some() {
|
||||
SystemMessage::ChannelIconChanged { by: user.id }
|
||||
.into_message(channel.id().to_string())
|
||||
.create(db, &channel, None)
|
||||
.await
|
||||
.ok();
|
||||
SystemMessage::ChannelIconChanged {
|
||||
by: user.id.clone(),
|
||||
}
|
||||
.into_message(channel.id().to_string())
|
||||
.send(db, user.as_author_for_system(), &channel, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
channel
|
||||
.update(db, partial, data.remove.unwrap_or_default())
|
||||
.update(
|
||||
db,
|
||||
partial,
|
||||
data.remove
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
_ => return Err(Error::InvalidOperation),
|
||||
_ => return Err(create_error!(InvalidOperation)),
|
||||
};
|
||||
|
||||
Ok(Json(channel))
|
||||
Ok(Json(channel.into()))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use rocket::{serde::json::Json, State};
|
||||
/// Fetch channel by its id.
|
||||
#[openapi(tag = "Channel Information")]
|
||||
#[get("/<target>")]
|
||||
pub async fn fetch_channel(
|
||||
pub async fn fetch(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
|
||||
@@ -13,7 +13,7 @@ use rocket_empty::EmptyResponse;
|
||||
/// Adds another user to the group.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[put("/<group_id>/recipients/<member_id>")]
|
||||
pub async fn req(
|
||||
pub async fn add_member(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
group_id: Reference,
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
};
|
||||
use revolt_database::{util::reference::Reference, Channel, Database, User};
|
||||
use revolt_permissions::ChannelPermission;
|
||||
use revolt_result::{create_error, Result};
|
||||
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Remove Member from Group
|
||||
///
|
||||
/// Removes a user from the group.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[delete("/<target>/recipients/<member>")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
|
||||
pub async fn remove_member(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
member: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
@@ -20,24 +27,26 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyR
|
||||
owner, recipients, ..
|
||||
} => {
|
||||
if &user.id != owner {
|
||||
return Error::from_permission(Permission::ManageChannel);
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: ChannelPermission::ManageChannel.to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
let member = member.as_user(db).await?;
|
||||
if user.id == member.id {
|
||||
return Err(Error::CannotRemoveYourself);
|
||||
return Err(create_error!(CannotRemoveYourself));
|
||||
}
|
||||
|
||||
if !recipients.iter().any(|x| *x == member.id) {
|
||||
return Err(Error::NotInGroup);
|
||||
return Err(create_error!(NotInGroup));
|
||||
}
|
||||
|
||||
channel
|
||||
.remove_user_from_group(db, &member.id, Some(&user.id), false)
|
||||
.remove_user_from_group(db, &member, Some(&user.id), false)
|
||||
.await
|
||||
.map(|_| EmptyResponse)
|
||||
}
|
||||
_ => Err(Error::InvalidOperation),
|
||||
_ => Err(create_error!(InvalidOperation)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use revolt_quark::{
|
||||
models::{Invite, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, Invite, User,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Create Invite
|
||||
///
|
||||
@@ -12,16 +15,23 @@ use rocket::serde::json::Json;
|
||||
/// Channel must be a `TextChannel`.
|
||||
#[openapi(tag = "Channel Invites")]
|
||||
#[post("/<target>/invites")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Invite>> {
|
||||
pub async fn create_invite(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
) -> Result<Json<v0::Invite>> {
|
||||
if user.bot.is_some() {
|
||||
return Err(Error::IsBot);
|
||||
return Err(create_error!(IsBot));
|
||||
}
|
||||
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::InviteOthers)
|
||||
.await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?;
|
||||
|
||||
Invite::create(db, &user, &channel).await.map(Json)
|
||||
Invite::create_channel_invite(db, &user, &channel)
|
||||
.await
|
||||
.map(|invite| invite.into())
|
||||
.map(Json)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
use revolt_quark::{
|
||||
models::{Channel, User},
|
||||
perms, Db, Error, Permission, Ref, Result,
|
||||
use futures::future::join_all;
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Channel, Database, User,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
|
||||
/// # Fetch Group Members
|
||||
///
|
||||
/// Retrieves all users who are part of this group.
|
||||
///
|
||||
/// This may not return full user information if users are not friends but have mutual connections.
|
||||
#[openapi(tag = "Groups")]
|
||||
#[get("/<target>/members")]
|
||||
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<User>>> {
|
||||
pub async fn fetch_members(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
) -> Result<Json<Vec<v0::User>>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission(db, Permission::ViewChannel)
|
||||
.await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||
|
||||
if let Channel::Group { recipients, .. } = channel {
|
||||
Ok(Json(
|
||||
db.fetch_users(&recipients)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.with_relationship(&user))
|
||||
.collect::<Vec<User>>(),
|
||||
User::fetch_many_ids_as_mutuals(db, &user, &recipients).await?,
|
||||
))
|
||||
} else {
|
||||
Err(Error::InvalidOperation)
|
||||
Err(create_error!(InvalidOperation))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use chrono::Utc;
|
||||
use revolt_quark::{
|
||||
models::{Message, User},
|
||||
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, Message, User,
|
||||
};
|
||||
use rocket::serde::json::Json;
|
||||
use revolt_models::v0;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use rocket_empty::EmptyResponse;
|
||||
use serde::Deserialize;
|
||||
use validator::Validate;
|
||||
|
||||
/// # Search Parameters
|
||||
#[derive(Validate, Deserialize, JsonSchema)]
|
||||
pub struct OptionsBulkDelete {
|
||||
/// Message IDs
|
||||
#[validate(length(min = 1, max = 100))]
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// # Bulk Delete Messages
|
||||
///
|
||||
/// Delete multiple messages you've sent or one you have permission to delete.
|
||||
@@ -24,34 +20,37 @@ pub struct OptionsBulkDelete {
|
||||
/// Messages must have been sent within the past 1 week.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
pub async fn bulk_delete_messages(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Ref,
|
||||
options: Json<OptionsBulkDelete>,
|
||||
target: Reference,
|
||||
options: Json<v0::OptionsBulkDelete>,
|
||||
) -> Result<EmptyResponse> {
|
||||
let options = options.into_inner();
|
||||
options
|
||||
.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
options.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
for id in &options.ids {
|
||||
if ulid::Ulid::from_string(id)
|
||||
.map_err(|_| Error::InvalidOperation)?
|
||||
.map_err(|_| create_error!(InvalidOperation))?
|
||||
.datetime()
|
||||
.signed_duration_since(Utc::now())
|
||||
.num_days()
|
||||
.abs()
|
||||
> 7
|
||||
{
|
||||
return Err(Error::InvalidOperation);
|
||||
return Err(create_error!(InvalidOperation));
|
||||
}
|
||||
}
|
||||
|
||||
perms(&user)
|
||||
.channel(&target.as_channel(db).await?)
|
||||
.throw_permission(db, Permission::ManageMessages)
|
||||
.await?;
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
|
||||
Message::bulk_delete(db, &target.id, options.ids)
|
||||
.await
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use revolt_quark::{
|
||||
models::{message::PartialMessage, User},
|
||||
perms, Db, EmptyResponse, Permission, Ref, Result,
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, PartialMessage, User,
|
||||
};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::Result;
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Remove All Reactions from Message
|
||||
///
|
||||
@@ -10,15 +14,20 @@ use revolt_quark::{
|
||||
/// Requires `ManageMessages` permission.
|
||||
#[openapi(tag = "Interactions")]
|
||||
#[delete("/<target>/messages/<msg>/reactions")]
|
||||
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
|
||||
pub async fn clear_reactions(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
perms(&user)
|
||||
.channel(&channel)
|
||||
.throw_permission_and_view_channel(db, Permission::ManageMessages)
|
||||
.await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
|
||||
// Fetch relevant message
|
||||
let mut message = msg.as_message_in(db, channel.id()).await?;
|
||||
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
|
||||
|
||||
// Clear reactions
|
||||
message
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
use revolt_quark::{models::User, perms, Db, EmptyResponse, Error, Permission, Ref, Result};
|
||||
use revolt_database::{
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, User,
|
||||
};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::Result;
|
||||
use rocket::State;
|
||||
use rocket_empty::EmptyResponse;
|
||||
|
||||
/// # Delete Message
|
||||
///
|
||||
/// Delete a message you've sent or one you have permission to delete.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[delete("/<target>/messages/<msg>", rank = 2)]
|
||||
pub async fn req(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
|
||||
let message = msg.as_message(db).await?;
|
||||
if message.channel != target.id {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
pub async fn delete(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
) -> Result<EmptyResponse> {
|
||||
let message = msg.as_message_in_channel(db, &target.id).await?;
|
||||
|
||||
if message.author != user.id {
|
||||
perms(&user)
|
||||
.channel(&target.as_channel(db).await?)
|
||||
.throw_permission(db, Permission::ManageMessages)
|
||||
.await?;
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||
}
|
||||
|
||||
message.delete(db).await.map(|_| EmptyResponse)
|
||||
|
||||
@@ -1,60 +1,54 @@
|
||||
use revolt_quark::{
|
||||
models::message::{PartialMessage, SendableEmbed},
|
||||
models::{Message, User},
|
||||
perms,
|
||||
types::january::Embed,
|
||||
Db, Error, Permission, Ref, Result, Timestamp,
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::config;
|
||||
use revolt_database::{
|
||||
tasks,
|
||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||
Database, Message, PartialMessage, User,
|
||||
};
|
||||
|
||||
use rocket::serde::json::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use revolt_models::v0::{self, Embed};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use rocket::{serde::json::Json, State};
|
||||
use validator::Validate;
|
||||
|
||||
/// # Message Details
|
||||
#[derive(Validate, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DataEditMessage {
|
||||
/// New message content
|
||||
#[validate(length(min = 1, max = 2000))]
|
||||
content: Option<String>,
|
||||
/// Embeds to include in the message
|
||||
#[validate(length(min = 0, max = 10))]
|
||||
embeds: Option<Vec<SendableEmbed>>,
|
||||
}
|
||||
|
||||
/// # Edit Message
|
||||
///
|
||||
/// Edits a message that you've previously sent.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[patch("/<target>/messages/<msg>", data = "<edit>")]
|
||||
pub async fn req(
|
||||
db: &Db,
|
||||
pub async fn edit(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Ref,
|
||||
msg: Ref,
|
||||
edit: Json<DataEditMessage>,
|
||||
) -> Result<Json<Message>> {
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
edit: Json<v0::DataEditMessage>,
|
||||
) -> Result<Json<v0::Message>> {
|
||||
let edit = edit.into_inner();
|
||||
edit.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
edit.validate().map_err(|error| {
|
||||
create_error!(FailedValidation {
|
||||
error: error.to_string()
|
||||
})
|
||||
})?;
|
||||
|
||||
let config = config().await;
|
||||
Message::validate_sum(
|
||||
&edit.content,
|
||||
edit.embeds.as_deref().unwrap_or_default(),
|
||||
config.features.limits.default.message_length,
|
||||
)?;
|
||||
|
||||
// Ensure we have permissions to send a message
|
||||
let channel = target.as_channel(db).await?;
|
||||
let mut permissions = perms(&user).channel(&channel);
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::SendMessage)
|
||||
.await?;
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
let permissions = calculate_channel_permissions(&mut query).await;
|
||||
|
||||
let mut message = msg.as_message(db).await?;
|
||||
if message.channel != channel.id() {
|
||||
return Err(Error::NotFound);
|
||||
}
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?;
|
||||
|
||||
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
|
||||
if message.author != user.id {
|
||||
return Err(Error::CannotEditMessage);
|
||||
return Err(create_error!(CannotEditMessage));
|
||||
}
|
||||
|
||||
Message::validate_sum(&edit.content, edit.embeds.as_deref().unwrap_or_default())?;
|
||||
|
||||
message.edited = Some(Timestamp::now_utc());
|
||||
let mut partial = PartialMessage {
|
||||
edited: message.edited,
|
||||
@@ -79,14 +73,12 @@ pub async fn req(
|
||||
// 3. Replace if we are given new embeds
|
||||
if let Some(embeds) = edit.embeds {
|
||||
// Ensure we have permissions to send embeds
|
||||
permissions
|
||||
.throw_permission_and_view_channel(db, Permission::SendEmbeds)
|
||||
.await?;
|
||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendEmbeds)?;
|
||||
|
||||
new_embeds.clear();
|
||||
|
||||
for embed in embeds {
|
||||
new_embeds.push(embed.clone().into_embed(db, &message.id).await?);
|
||||
new_embeds.push(message.create_embed(db, embed).await?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,12 +87,9 @@ pub async fn req(
|
||||
message.update(db, partial).await?;
|
||||
|
||||
// Queue up a task for processing embeds if the we have sufficient permissions
|
||||
if permissions
|
||||
.has_permission(db, Permission::SendEmbeds)
|
||||
.await?
|
||||
{
|
||||
if permissions.has_channel_permission(ChannelPermission::SendEmbeds) {
|
||||
if let Some(content) = edit.content {
|
||||
revolt_quark::tasks::process_embeds::queue(
|
||||
tasks::process_embeds::queue(
|
||||
message.channel.to_string(),
|
||||
message.id.to_string(),
|
||||
content,
|
||||
@@ -109,5 +98,5 @@ pub async fn req(
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(message))
|
||||
Ok(Json(message.into()))
|
||||
}
|
||||
|
||||
@@ -12,14 +12,13 @@ use rocket::{serde::json::Json, State};
|
||||
/// Retrieves a message by its id.
|
||||
#[openapi(tag = "Messaging")]
|
||||
#[get("/<target>/messages/<msg>")]
|
||||
pub async fn req(
|
||||
pub async fn fetch(
|
||||
db: &State<Database>,
|
||||
user: User,
|
||||
target: Reference,
|
||||
msg: Reference,
|
||||
) -> Result<Json<v0::Message>> {
|
||||
let channel = target.as_channel(db).await?;
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||
calculate_channel_permissions(&mut query)
|
||||
.await
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user