Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6043ba6004 | ||
|
|
657a3f08e5 | ||
|
|
af78ac0586 | ||
|
|
d65c1a1ab3 | ||
|
|
4fb99e3bd0 | ||
|
|
f0a83abcfa | ||
|
|
6f1c715b8c | ||
|
|
38dd4d1079 | ||
|
|
154204742d | ||
|
|
db55998546 | ||
|
|
5885e067a6 | ||
|
|
a92152d86d | ||
|
|
b5cd5e30ef | ||
|
|
b0c977b324 | ||
|
|
bfe4018e43 | ||
|
|
cc7a7962a8 | ||
|
|
fb4011084d | ||
|
|
3a3415915f | ||
|
|
964884a5de | ||
|
|
db57706794 | ||
|
|
a186732ca5 | ||
|
|
e635bc23ec | ||
|
|
3cb7da95e3 | ||
|
|
dea0f675dd | ||
|
|
480f210ce8 | ||
|
|
caa8607468 | ||
|
|
32e6600272 | ||
|
|
5335124306 | ||
|
|
1561481eb4 | ||
|
|
8f9015a6ff | ||
|
|
67773d3e43 | ||
|
|
14ea180683 | ||
|
|
83c15404a5 | ||
|
|
9d4bcb5e3d | ||
|
|
f895ca7b23 | ||
|
|
ef65223c89 | ||
|
|
660e646b2b | ||
|
|
5a5f84f207 | ||
|
|
11eee02cfe | ||
|
|
1e5b27ff9e | ||
|
|
3ae25fbcfe | ||
|
|
9e05e5be7e | ||
|
|
ed22b3a5ce | ||
|
|
65fbd36624 | ||
|
|
050ba16d4a | ||
|
|
65bc6c8fc6 | ||
|
|
6ad3da5f35 | ||
|
|
947eb15771 | ||
|
|
f4ee35fb09 | ||
|
|
6048587d34 | ||
|
|
80cf8fc4e8 |
@@ -49,13 +49,6 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
# Authenticate with Docker Hub and GHCR
|
# Authenticate with Docker Hub and GHCR
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: docker.io
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to Github Container Registry
|
- name: Login to Github Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -72,14 +65,13 @@ jobs:
|
|||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
||||||
|
|
||||||
# revoltchat/server
|
# stoatchat/api
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta-delta
|
id: meta-delta
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
docker.io/revoltchat/server
|
ghcr.io/stoatchat/api
|
||||||
ghcr.io/revoltchat/server
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -92,14 +84,13 @@ jobs:
|
|||||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||||
labels: ${{ steps.meta-delta.outputs.labels }}
|
labels: ${{ steps.meta-delta.outputs.labels }}
|
||||||
|
|
||||||
# revoltchat/bonfire
|
# stoatchat/events
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta-bonfire
|
id: meta-bonfire
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
docker.io/revoltchat/bonfire
|
ghcr.io/stoatchat/events
|
||||||
ghcr.io/revoltchat/bonfire
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -112,14 +103,13 @@ jobs:
|
|||||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||||
labels: ${{ steps.meta-bonfire.outputs.labels }}
|
labels: ${{ steps.meta-bonfire.outputs.labels }}
|
||||||
|
|
||||||
# revoltchat/autumn
|
# stoatchat/file-server
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta-autumn
|
id: meta-autumn
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
docker.io/revoltchat/autumn
|
ghcr.io/stoatchat/file-server
|
||||||
ghcr.io/revoltchat/autumn
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -132,14 +122,13 @@ jobs:
|
|||||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||||
labels: ${{ steps.meta-autumn.outputs.labels }}
|
labels: ${{ steps.meta-autumn.outputs.labels }}
|
||||||
|
|
||||||
# revoltchat/january
|
# stoatchat/proxy
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta-january
|
id: meta-january
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
docker.io/revoltchat/january
|
ghcr.io/stoatchat/proxy
|
||||||
ghcr.io/revoltchat/january
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -152,14 +141,32 @@ jobs:
|
|||||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||||
labels: ${{ steps.meta-january.outputs.labels }}
|
labels: ${{ steps.meta-january.outputs.labels }}
|
||||||
|
|
||||||
# revoltchat/crond
|
# stoatchat/gifbox
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta-gifbox
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
ghcr.io/stoatchat/gifbox
|
||||||
|
- name: Publish
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
file: crates/services/gifbox/Dockerfile
|
||||||
|
tags: ${{ steps.meta-gifbox.outputs.tags }}
|
||||||
|
build-args: |
|
||||||
|
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||||
|
labels: ${{ steps.meta-gifbox.outputs.labels }}
|
||||||
|
|
||||||
|
# stoatchat/crond
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta-crond
|
id: meta-crond
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
docker.io/revoltchat/crond
|
ghcr.io/stoatchat/crond
|
||||||
ghcr.io/revoltchat/crond
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
@@ -172,14 +179,13 @@ jobs:
|
|||||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||||
labels: ${{ steps.meta-crond.outputs.labels }}
|
labels: ${{ steps.meta-crond.outputs.labels }}
|
||||||
|
|
||||||
# revoltchat/pushd
|
# stoatchat/pushd
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta-pushd
|
id: meta-pushd
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
docker.io/revoltchat/pushd
|
ghcr.io/stoatchat/pushd
|
||||||
ghcr.io/revoltchat/pushd
|
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: revoltchat/api
|
repository: stoatchat/api
|
||||||
path: api
|
path: api
|
||||||
token: ${{ secrets.PAT }}
|
token: ${{ secrets.PAT }}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
gh api graphql -f query='
|
gh api graphql -f query='
|
||||||
query {
|
query {
|
||||||
organization(login: "revoltchat"){
|
organization(login: "stoatchat"){
|
||||||
projectV2(number: 3) {
|
projectV2(number: 3) {
|
||||||
id
|
id
|
||||||
fields(first:20) {
|
fields(first:20) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
gh api graphql -f query='
|
gh api graphql -f query='
|
||||||
query {
|
query {
|
||||||
organization(login: "revoltchat"){
|
organization(login: "stoatchat"){
|
||||||
projectV2(number: 5) {
|
projectV2(number: 5) {
|
||||||
id
|
id
|
||||||
fields(first:20) {
|
fields(first:20) {
|
||||||
|
|||||||
Generated
+1023
-758
File diff suppressed because it is too large
Load Diff
@@ -27,8 +27,11 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
|
|||||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||||
|
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
|
||||||
|
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
|
||||||
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||||
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
||||||
|
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
|
||||||
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
|
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
|
||||||
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
|
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
|
||||||
RUN sh /tmp/build-image-layer.sh deps
|
RUN sh /tmp/build-image-layer.sh deps
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
|
|||||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||||
|
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
|
||||||
|
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
|
||||||
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||||
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
||||||
|
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
|
||||||
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
|
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
|
||||||
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
|
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
|
||||||
RUN sh /tmp/build-image-layer.sh deps
|
RUN sh /tmp/build-image-layer.sh deps
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ The services and libraries that power the Revolt service.<br/>
|
|||||||
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |     |
|
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |     |
|
||||||
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |     |
|
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |     |
|
||||||
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |     |
|
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |     |
|
||||||
|
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service |     |
|
||||||
| `delta` | [crates/delta](crates/delta) | REST API server |  |
|
| `delta` | [crates/delta](crates/delta) | REST API server |  |
|
||||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |  |
|
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |  |
|
||||||
| `services/january` | [crates/services/january](crates/services/january) | Proxy server |  |
|
| `services/january` | [crates/services/january](crates/services/january) | Proxy server |  |
|
||||||
|
| `services/gifbox` | [crates/services/gifbox](crates/services/gifbox) | Tenor proxy server |  |
|
||||||
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server |  |
|
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server |  |
|
||||||
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server |  |
|
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server |  |
|
||||||
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server |  |
|
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server |  |
|
||||||
@@ -66,6 +68,7 @@ As a heads-up, the development environment uses the following ports:
|
|||||||
| `crates/bonfire` | 14703 |
|
| `crates/bonfire` | 14703 |
|
||||||
| `crates/services/autumn` | 14704 |
|
| `crates/services/autumn` | 14704 |
|
||||||
| `crates/services/january` | 14705 |
|
| `crates/services/january` | 14705 |
|
||||||
|
| `crates/services/gifbox` | 14706 |
|
||||||
|
|
||||||
Now you can clone and build the project:
|
Now you can clone and build the project:
|
||||||
|
|
||||||
@@ -141,6 +144,8 @@ cargo run --bin revolt-bonfire
|
|||||||
cargo run --bin revolt-autumn
|
cargo run --bin revolt-autumn
|
||||||
# run the proxy server
|
# run the proxy server
|
||||||
cargo run --bin revolt-january
|
cargo run --bin revolt-january
|
||||||
|
# run the tenor proxy
|
||||||
|
cargo run --bin revolt-gifbox
|
||||||
# run the push daemon (not usually needed in regular development)
|
# run the push daemon (not usually needed in regular development)
|
||||||
cargo run --bin revolt-pushd
|
cargo run --bin revolt-pushd
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ port = 14025
|
|||||||
use_tls = false
|
use_tls = false
|
||||||
use_starttls = false
|
use_starttls = false
|
||||||
|
|
||||||
|
[api.security]
|
||||||
|
token_secret = "trolt"
|
||||||
|
|
||||||
[files.s3]
|
[files.s3]
|
||||||
# S3 protocol endpoint
|
# S3 protocol endpoint
|
||||||
endpoint = "http://127.0.0.1:14009"
|
endpoint = "http://127.0.0.1:14009"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-bonfire"
|
name = "revolt-bonfire"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ async-channel = "2.3.1"
|
|||||||
|
|
||||||
# parsing
|
# parsing
|
||||||
querystring = "1.1.0"
|
querystring = "1.1.0"
|
||||||
|
regex = "1.11.1"
|
||||||
|
|
||||||
# serde
|
# serde
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
@@ -41,7 +42,7 @@ revolt-result = { path = "../core/result" }
|
|||||||
revolt-models = { path = "../core/models" }
|
revolt-models = { path = "../core/models" }
|
||||||
revolt-config = { path = "../core/config" }
|
revolt-config = { path = "../core/config" }
|
||||||
revolt-database = { path = "../core/database" }
|
revolt-database = { path = "../core/database" }
|
||||||
revolt-permissions = { version = "0.8.8", path = "../core/permissions" }
|
revolt-permissions = { version = "0.8.9", path = "../core/permissions" }
|
||||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||||
|
|
||||||
# redis
|
# redis
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Build Stage
|
# Build Stage
|
||||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
FROM ghcr.io/stoatchat/base:latest AS builder
|
||||||
FROM debian:12 AS debian
|
FROM debian:12 AS debian
|
||||||
|
|
||||||
# Bundle Stage
|
# Bundle Stage
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
use async_tungstenite::tungstenite::{handshake, Message};
|
use async_tungstenite::tungstenite::{handshake, Message};
|
||||||
use futures::channel::oneshot::Sender;
|
use futures::channel::oneshot::Sender;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
use revolt_database::events::client::ReadyPayloadFields;
|
use revolt_database::events::client::ReadyPayloadFields;
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// matches either a single word ie "users" or a key and value ie "settings[notifications]"
|
||||||
|
static READY_PAYLOAD_FIELD_REGEX: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap());
|
||||||
|
|
||||||
/// Enumeration of supported protocol formats
|
/// Enumeration of supported protocol formats
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ProtocolFormat {
|
pub enum ProtocolFormat {
|
||||||
@@ -17,6 +23,7 @@ pub struct ProtocolConfiguration {
|
|||||||
protocol_version: i32,
|
protocol_version: i32,
|
||||||
format: ProtocolFormat,
|
format: ProtocolFormat,
|
||||||
session_token: Option<String>,
|
session_token: Option<String>,
|
||||||
|
ready_payload_fields: ReadyPayloadFields,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProtocolConfiguration {
|
impl ProtocolConfiguration {
|
||||||
@@ -25,11 +32,13 @@ impl ProtocolConfiguration {
|
|||||||
protocol_version: i32,
|
protocol_version: i32,
|
||||||
format: ProtocolFormat,
|
format: ProtocolFormat,
|
||||||
session_token: Option<String>,
|
session_token: Option<String>,
|
||||||
|
ready_payload_fields: ReadyPayloadFields,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
protocol_version,
|
protocol_version,
|
||||||
format,
|
format,
|
||||||
session_token,
|
session_token,
|
||||||
|
ready_payload_fields,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +95,8 @@ impl ProtocolConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get ready payload fields
|
/// Get ready payload fields
|
||||||
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
|
pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields {
|
||||||
vec![
|
&self.ready_payload_fields
|
||||||
ReadyPayloadFields::Users,
|
|
||||||
ReadyPayloadFields::Servers,
|
|
||||||
ReadyPayloadFields::Channels,
|
|
||||||
ReadyPayloadFields::Members,
|
|
||||||
ReadyPayloadFields::Emoji,
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +127,22 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
|||||||
let mut protocol_version = 1;
|
let mut protocol_version = 1;
|
||||||
let mut format = ProtocolFormat::Json;
|
let mut format = ProtocolFormat::Json;
|
||||||
let mut session_token = None;
|
let mut session_token = None;
|
||||||
|
let mut ready_payload_fields = if params.iter().any(|(k, _)| *k == "ready") {
|
||||||
|
// If they pass the ready field, set all fields to false
|
||||||
|
|
||||||
|
ReadyPayloadFields {
|
||||||
|
users: false,
|
||||||
|
servers: false,
|
||||||
|
channels: false,
|
||||||
|
members: false,
|
||||||
|
emojis: false,
|
||||||
|
user_settings: Vec::new(),
|
||||||
|
channel_unreads: false,
|
||||||
|
policy_changes: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ReadyPayloadFields::default()
|
||||||
|
};
|
||||||
|
|
||||||
// Parse and map parameters from key-value to known variables.
|
// Parse and map parameters from key-value to known variables.
|
||||||
for (key, value) in params {
|
for (key, value) in params {
|
||||||
@@ -139,6 +158,30 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
|||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
"token" => session_token = Some(value.into()),
|
"token" => session_token = Some(value.into()),
|
||||||
|
"ready" => {
|
||||||
|
// Re-enable all the fields the client specifies
|
||||||
|
if let Some(captures) = READY_PAYLOAD_FIELD_REGEX.captures(value) {
|
||||||
|
if let Some(field) = captures.get(0) {
|
||||||
|
match field.as_str() {
|
||||||
|
"users" => ready_payload_fields.users = true,
|
||||||
|
"servers" => ready_payload_fields.servers = true,
|
||||||
|
"channels" => ready_payload_fields.channels = true,
|
||||||
|
"members" => ready_payload_fields.members = true,
|
||||||
|
"emojis" => ready_payload_fields.emojis = true,
|
||||||
|
"channel_unreads" => ready_payload_fields.channel_unreads = true,
|
||||||
|
"user_settings" => {
|
||||||
|
if let Some(subkey) = captures.get(1) {
|
||||||
|
ready_payload_fields
|
||||||
|
.user_settings
|
||||||
|
.push(subkey.as_str().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"policy_changes" => ready_payload_fields.policy_changes = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,6 +194,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
|||||||
protocol_version,
|
protocol_version,
|
||||||
format,
|
format,
|
||||||
session_token,
|
session_token,
|
||||||
|
ready_payload_fields,
|
||||||
})
|
})
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,21 +95,23 @@ impl State {
|
|||||||
pub async fn generate_ready_payload(
|
pub async fn generate_ready_payload(
|
||||||
&mut self,
|
&mut self,
|
||||||
db: &Database,
|
db: &Database,
|
||||||
fields: Vec<ReadyPayloadFields>,
|
fields: &ReadyPayloadFields,
|
||||||
) -> Result<EventV1> {
|
) -> Result<EventV1> {
|
||||||
let user = self.clone_user();
|
let user = self.clone_user();
|
||||||
self.cache.is_bot = user.bot.is_some();
|
self.cache.is_bot = user.bot.is_some();
|
||||||
|
|
||||||
// Fetch pending policy changes.
|
// Fetch pending policy changes.
|
||||||
let policy_changes = if user.bot.is_some() {
|
let policy_changes = if user.bot.is_some() || !fields.policy_changes {
|
||||||
vec![]
|
None
|
||||||
} else {
|
} else {
|
||||||
db.fetch_policy_changes()
|
Some(
|
||||||
.await?
|
db.fetch_policy_changes()
|
||||||
.into_iter()
|
.await?
|
||||||
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
|
.into_iter()
|
||||||
.map(Into::into)
|
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
|
||||||
.collect()
|
.map(Into::into)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find all relationships to the user.
|
// Find all relationships to the user.
|
||||||
@@ -168,7 +170,7 @@ impl State {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Fetch customisations.
|
// Fetch customisations.
|
||||||
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
|
let emojis = if fields.emojis {
|
||||||
Some(
|
Some(
|
||||||
db.fetch_emoji_by_parent_ids(
|
db.fetch_emoji_by_parent_ids(
|
||||||
&servers
|
&servers
|
||||||
@@ -176,25 +178,34 @@ impl State {
|
|||||||
.map(|x| x.id.to_string())
|
.map(|x| x.id.to_string())
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
)
|
)
|
||||||
.await?,
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|emoji| emoji.into())
|
||||||
|
.collect(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch user settings
|
// Fetch user settings
|
||||||
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
|
let user_settings = if !fields.user_settings.is_empty() {
|
||||||
.iter()
|
Some(
|
||||||
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
|
db.fetch_user_settings(&user.id, &fields.user_settings)
|
||||||
{
|
.await?,
|
||||||
Some(db.fetch_user_settings(&user.id, keys).await?)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch channel unreads
|
// Fetch channel unreads
|
||||||
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
|
let channel_unreads = if fields.channel_unreads {
|
||||||
Some(db.fetch_unreads(&user.id).await?)
|
Some(
|
||||||
|
db.fetch_unreads(&user.id)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|unread| unread.into())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -241,30 +252,25 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(EventV1::Ready {
|
Ok(EventV1::Ready {
|
||||||
users: if fields.contains(&ReadyPayloadFields::Users) {
|
users: if fields.users { Some(users) } else { None },
|
||||||
Some(users)
|
servers: if fields.servers {
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
servers: if fields.contains(&ReadyPayloadFields::Servers) {
|
|
||||||
Some(servers.into_iter().map(Into::into).collect())
|
Some(servers.into_iter().map(Into::into).collect())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
channels: if fields.contains(&ReadyPayloadFields::Channels) {
|
channels: if fields.channels {
|
||||||
Some(channels.into_iter().map(Into::into).collect())
|
Some(channels.into_iter().map(Into::into).collect())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
members: if fields.contains(&ReadyPayloadFields::Members) {
|
members: if fields.members {
|
||||||
Some(members.into_iter().map(Into::into).collect())
|
Some(members.into_iter().map(Into::into).collect())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
|
emojis,
|
||||||
|
|
||||||
user_settings,
|
user_settings,
|
||||||
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
|
channel_unreads,
|
||||||
|
|
||||||
policy_changes,
|
policy_changes,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
|||||||
use revolt_config::report_internal_error;
|
use revolt_config::report_internal_error;
|
||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
events::{client::EventV1, server::ClientMessage},
|
events::{client::EventV1, server::ClientMessage},
|
||||||
|
util::oauth2,
|
||||||
iso8601_timestamp::Timestamp,
|
iso8601_timestamp::Timestamp,
|
||||||
Database, User, UserHint,
|
Database, User, UserHint,
|
||||||
};
|
};
|
||||||
|
use revolt_models::v0;
|
||||||
use revolt_presence::{create_session, delete_session};
|
use revolt_presence::{create_session, delete_session};
|
||||||
|
|
||||||
use async_std::{
|
use async_std::{
|
||||||
@@ -88,23 +90,51 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Presume the token is a proper token first
|
||||||
let (user, session_id) = 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,
|
Ok((user, session_id)) => {
|
||||||
Err(err) => {
|
db.update_session_last_seen(&session_id, Timestamp::now_utc())
|
||||||
write
|
|
||||||
.send(config.encode(&EventV1::Error { data: err }))
|
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
return;
|
|
||||||
|
(user, session_id)
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
let revolt_config = revolt_config::config().await;
|
||||||
|
|
||||||
|
// If it fails to find the user from the token see if its an OAuth2 token
|
||||||
|
let res = match oauth2::decode_token(&revolt_config.api.security.token_secret, token) {
|
||||||
|
// Check if the OAuth2 token is allowed to establish an events websocket
|
||||||
|
Ok(claims) => if !claims.scopes.contains(&v0::OAuth2Scope::Events) {
|
||||||
|
// TODO: maybe a last_seen system for OAuth2 as well
|
||||||
|
db.fetch_user(&claims.sub).await.map(|user| (user, claims.jti))
|
||||||
|
} else {
|
||||||
|
Err(create_error!(MissingScope { scope: v0::OAuth2Scope::Events.to_string() }))
|
||||||
|
},
|
||||||
|
// If its expired return an error
|
||||||
|
Err(e) => if e.into_kind() == oauth2::JWTErrorKind::ExpiredSignature {
|
||||||
|
Err(create_error!(ExpiredToken))
|
||||||
|
} else {
|
||||||
|
// Finally re-return the error from User::from_token if everything else fails to avoid a confusing error
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(user) => user,
|
||||||
|
Err(err) => {
|
||||||
|
write
|
||||||
|
.send(config.encode(&EventV1::Error { data: err }))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("User {addr:?} authenticated as @{}", user.username);
|
info!("User {addr:?} authenticated as @{}", user.username);
|
||||||
|
|
||||||
db.update_session_last_seen(&session_id, Timestamp::now_utc())
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Create local state.
|
// Create local state.
|
||||||
let mut state = State::from(user, session_id);
|
let mut state = State::from(user, session_id);
|
||||||
let user_id = state.cache.user_id.clone();
|
let user_id = state.cache.user_id.clone();
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "revolt-coalesced"
|
||||||
|
version = "0.8.9"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
|
||||||
|
description = "Revolt Backend: Coalescion service"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
tokio = ["dep:tokio"]
|
||||||
|
queue = ["dep:indexmap"]
|
||||||
|
cache = ["dep:lru"]
|
||||||
|
|
||||||
|
default = ["tokio"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.47.0", features = ["sync"], optional = true }
|
||||||
|
indexmap = { version = "*", optional = true }
|
||||||
|
lru = { version = "*", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.47.0", features = ["rt", "rt-multi-thread", "macros", "time"] }
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
/// Config values for [`CoalescionService`].
|
||||||
|
pub struct CoalescionServiceConfig {
|
||||||
|
/// How many tasks are running at once
|
||||||
|
pub max_concurrent: Option<usize>,
|
||||||
|
/// Whether to queue tasks once `max_concurrent` is reached
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
pub queue_requests: bool,
|
||||||
|
/// Max amount of tasks in the buffer queue
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
pub max_queue: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CoalescionServiceConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_concurrent: Some(100),
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
queue_requests: true,
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
max_queue: Some(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||||
|
/// Coalescion service error.
|
||||||
|
pub enum Error {
|
||||||
|
/// Failed to receive the actions return from the channel for unknown reason
|
||||||
|
RecvError,
|
||||||
|
/// Reached the `max_concurrent` amount of actions running at once and could not queue the action
|
||||||
|
MaxConcurrent,
|
||||||
|
/// Reached the `max_queue` amount of actions in the queue
|
||||||
|
MaxQueue,
|
||||||
|
/// Failed to downcast the type to the current type being returned, this will be most likely an ID collision
|
||||||
|
DowncastError,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::RecvError => write!(f, "Unable to receive data from the channel"),
|
||||||
|
Error::MaxConcurrent => write!(f, "Max number of tasks running at once"),
|
||||||
|
Error::MaxQueue => write!(f, "Max number of tasks in queue"),
|
||||||
|
Error::DowncastError => write!(f, "Failed to downcast type, possible key collision with different types")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//! # Coalesced
|
||||||
|
//!
|
||||||
|
//! Coalescion service to group, caching and queue duplicate actions.
|
||||||
|
//! useful for deduplicating web requests, database lookups and other similar resource
|
||||||
|
//! intensive or rate-limited actions.
|
||||||
|
//!
|
||||||
|
//! ## Features
|
||||||
|
//! - `tokio`: Uses tokio for the async backend, this is currently the only backend.
|
||||||
|
//! - `queue`: Whether to support queueing requests to only allow X amount of actions running at once.
|
||||||
|
//! - `cache`: Whether to cache the actions results for future actions with the same id, uses an LRU cache internally.
|
||||||
|
//!
|
||||||
|
//! [`CoalescionService`] uses both [`Arc`] and [`RwLock`] internally and can be cheaply cloned to
|
||||||
|
//! use in your codebase.
|
||||||
|
//!
|
||||||
|
//! It is common practice to wrap the service and in your own which delegates the executions to ensure all ids are tracked in one location across your codebase.
|
||||||
|
//!
|
||||||
|
//! All values are stored using [`Any`] and must be [`'static`] + [`Send`] + [`Sync`], if there is an id mismatch
|
||||||
|
//! and a type is wrong the library will return an error, values returned from the service are also
|
||||||
|
//! wrapped in an [`Arc`] as they are shared to each duplicate action.
|
||||||
|
//!
|
||||||
|
//! ## Example:
|
||||||
|
//! ```rs
|
||||||
|
//! use revolt_coalesced::CoalescionService;
|
||||||
|
//!
|
||||||
|
//! let service = CoalescionService::new();
|
||||||
|
//!
|
||||||
|
//! let user_id = "my_user_id";
|
||||||
|
//! let user = service.execute(user_id, || async move {
|
||||||
|
//! database.fetch_user(user_id).await.unwrap()
|
||||||
|
//! }).await;
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod error;
|
||||||
|
mod service;
|
||||||
|
|
||||||
|
pub use config::CoalescionServiceConfig;
|
||||||
|
pub use error::Error;
|
||||||
|
pub use service::CoalescionService;
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc};
|
||||||
|
|
||||||
|
use tokio::sync::{
|
||||||
|
watch::{channel as watch_channel, Receiver},
|
||||||
|
RwLock,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "cache")]
|
||||||
|
use lru::LruCache;
|
||||||
|
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
|
use crate::{CoalescionServiceConfig, Error};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
/// # Coalescion service
|
||||||
|
///
|
||||||
|
/// See module description for example usage.
|
||||||
|
pub struct CoalescionService<Id: Hash + Clone + Eq> {
|
||||||
|
config: Arc<CoalescionServiceConfig>,
|
||||||
|
watchers: Arc<RwLock<HashMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
queue: Arc<RwLock<IndexMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
|
||||||
|
#[cfg(feature = "cache")]
|
||||||
|
cache: Option<Arc<tokio::sync::Mutex<LruCache<Id, Arc<dyn Any + Send + Sync>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Id: Hash + Clone + Eq> CoalescionService<Id> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_config(config: CoalescionServiceConfig) -> Self {
|
||||||
|
Self {
|
||||||
|
config: Arc::new(config),
|
||||||
|
watchers: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
queue: Arc::new(RwLock::new(IndexMap::new())),
|
||||||
|
#[cfg(feature = "cache")]
|
||||||
|
cache: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cache")]
|
||||||
|
pub fn from_cache(
|
||||||
|
config: CoalescionServiceConfig,
|
||||||
|
cache: LruCache<Id, Arc<dyn Any + Send + Sync>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
cache: Some(Arc::new(Mutex::new(cache))),
|
||||||
|
..Self::from_config(config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for<Value: Any + Send + Sync>(
|
||||||
|
&self,
|
||||||
|
mut receiver: Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>,
|
||||||
|
) -> Result<Arc<Value>, Error> {
|
||||||
|
receiver
|
||||||
|
.wait_for(|v| v.is_some())
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::RecvError)
|
||||||
|
.and_then(|r| r.clone().unwrap())
|
||||||
|
.and_then(|arc| Arc::downcast(arc).map_err(|_| Error::DowncastError))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_and_execute<
|
||||||
|
Value: Send + Sync + 'static,
|
||||||
|
F: FnOnce() -> Fut,
|
||||||
|
Fut: Future<Output = Value>,
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
id: Id,
|
||||||
|
func: F,
|
||||||
|
) -> Result<Arc<Value>, Error> {
|
||||||
|
let (send, recv) = watch_channel(None);
|
||||||
|
|
||||||
|
self.watchers.write().await.insert(id.clone(), recv);
|
||||||
|
|
||||||
|
let value = Ok(Arc::new(func().await));
|
||||||
|
|
||||||
|
send.send_modify(|opt| {
|
||||||
|
opt.replace(value.clone().map(|v| v as Arc<dyn Any + Send + Sync>));
|
||||||
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "cache")]
|
||||||
|
if let Some(cache) = self.cache.as_ref() {
|
||||||
|
if let Ok(value) = &value {
|
||||||
|
cache.lock().await.push(id.clone(), value.clone());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.watchers.write().await.remove(&id);
|
||||||
|
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coalesces an function, the actual function may not run if one with the same id is already running,
|
||||||
|
/// queued to be ran, or cached, the id should be globally unique for this specific action.
|
||||||
|
pub async fn execute<
|
||||||
|
Value: Send + Sync + 'static,
|
||||||
|
F: FnOnce() -> Fut,
|
||||||
|
Fut: Future<Output = Value>,
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
id: Id,
|
||||||
|
func: F,
|
||||||
|
) -> Result<Arc<Value>, Error> {
|
||||||
|
#[cfg(feature = "cache")]
|
||||||
|
if let Some(cache) = self.cache.as_ref() {
|
||||||
|
if let Some(value) = cache.lock().await.get(&id) {
|
||||||
|
return Arc::downcast::<Value>(value.clone()).map_err(|_| Error::DowncastError);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (receiver, length) = {
|
||||||
|
let watchers = self.watchers.read().await;
|
||||||
|
let length = watchers.len();
|
||||||
|
|
||||||
|
(watchers.get(&id).cloned(), length)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(receiver) = receiver {
|
||||||
|
self.wait_for(receiver).await
|
||||||
|
} else {
|
||||||
|
match self.config.max_concurrent {
|
||||||
|
Some(max_concurrent) if length >= max_concurrent => {
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
if self.config.queue_requests {
|
||||||
|
let (receiver, length) = {
|
||||||
|
let queue = self.queue.read().await;
|
||||||
|
|
||||||
|
(queue.get(&id).cloned(), queue.len())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(receiver) = receiver {
|
||||||
|
return self.wait_for(receiver).await;
|
||||||
|
} else {
|
||||||
|
if self
|
||||||
|
.config
|
||||||
|
.max_queue
|
||||||
|
.is_some_and(|max_queue| max_queue >= length)
|
||||||
|
{
|
||||||
|
return Err(Error::MaxQueue);
|
||||||
|
};
|
||||||
|
|
||||||
|
let (send, recv) = watch_channel(None);
|
||||||
|
|
||||||
|
self.queue.write().await.insert(id.clone(), recv);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let length = self.watchers.read().await.len();
|
||||||
|
|
||||||
|
if length < max_concurrent {
|
||||||
|
let first_key = {
|
||||||
|
let queue = self.queue.read().await;
|
||||||
|
queue.first().map(|v| v.0).cloned()
|
||||||
|
};
|
||||||
|
|
||||||
|
if first_key == Some(id.clone()) {
|
||||||
|
self.queue.write().await.shift_remove(&id);
|
||||||
|
|
||||||
|
let response = self.insert_and_execute(id, func).await;
|
||||||
|
|
||||||
|
send.send_modify(|opt| {
|
||||||
|
opt.replace(
|
||||||
|
response
|
||||||
|
.clone()
|
||||||
|
.map(|v| v as Arc<dyn Any + Send + Sync>),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(Error::MaxConcurrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "queue"))]
|
||||||
|
Err(Error::MaxConcurrent)
|
||||||
|
}
|
||||||
|
_ => self.insert_and_execute(id, func).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches the amount of currently running tasks
|
||||||
|
pub async fn current_task_count(&self) -> usize {
|
||||||
|
self.watchers.read().await.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "queue")]
|
||||||
|
/// Fetches the current length of the queue
|
||||||
|
pub async fn current_queue_len(&self) -> usize {
|
||||||
|
self.queue.read().await.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Id: Hash + Clone + Eq> Default for CoalescionService<Id> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from_config(CoalescionServiceConfig::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-config"
|
name = "revolt-config"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -11,8 +11,9 @@ description = "Revolt Backend: Configuration"
|
|||||||
[features]
|
[features]
|
||||||
anyhow = ["dep:sentry-anyhow"]
|
anyhow = ["dep:sentry-anyhow"]
|
||||||
report-macros = ["revolt-result"]
|
report-macros = ["revolt-result"]
|
||||||
|
sentry = ["dep:sentry"]
|
||||||
test = ["async-std"]
|
test = ["async-std"]
|
||||||
default = ["test", "anyhow"]
|
default = ["test", "sentry"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Utility
|
# Utility
|
||||||
@@ -32,8 +33,8 @@ log = "0.4.14"
|
|||||||
pretty_env_logger = "0.4.0"
|
pretty_env_logger = "0.4.0"
|
||||||
|
|
||||||
# Sentry
|
# Sentry
|
||||||
sentry = "0.31.5"
|
sentry = { version = "0.31.5", optional = true }
|
||||||
sentry-anyhow = { version = "0.38.1", optional = true }
|
sentry-anyhow = { version = "0.38.1", optional = true }
|
||||||
|
|
||||||
# Core
|
# Core
|
||||||
revolt-result = { version = "0.8.8", path = "../result", optional = true }
|
revolt-result = { version = "0.8.9", path = "../result", optional = true }
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ voso_legacy_token = ""
|
|||||||
trust_cloudflare = false
|
trust_cloudflare = false
|
||||||
# easypwned endpoint
|
# easypwned endpoint
|
||||||
easypwned = ""
|
easypwned = ""
|
||||||
|
# Secret used to encode and decode tokens
|
||||||
|
token_secret = ""
|
||||||
|
# Tenor API Key
|
||||||
|
tenor_key = ""
|
||||||
|
|
||||||
[api.security.captcha]
|
[api.security.captcha]
|
||||||
# hCaptcha configuration
|
# hCaptcha configuration
|
||||||
@@ -277,3 +281,4 @@ files = ""
|
|||||||
proxy = ""
|
proxy = ""
|
||||||
pushd = ""
|
pushd = ""
|
||||||
crond = ""
|
crond = ""
|
||||||
|
gifbox = ""
|
||||||
@@ -6,10 +6,12 @@ use futures_locks::RwLock;
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[cfg(feature = "sentry")]
|
||||||
pub use sentry::{capture_error, capture_message, Level};
|
pub use sentry::{capture_error, capture_message, Level};
|
||||||
|
#[cfg(feature = "anyhow")]
|
||||||
pub use sentry_anyhow::capture_anyhow;
|
pub use sentry_anyhow::capture_anyhow;
|
||||||
|
|
||||||
#[cfg(feature = "report-macros")]
|
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! report_error {
|
macro_rules! report_error {
|
||||||
( $expr: expr, $error: ident $( $tt:tt )? ) => {
|
( $expr: expr, $error: ident $( $tt:tt )? ) => {
|
||||||
@@ -24,7 +26,7 @@ macro_rules! report_error {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "report-macros")]
|
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! capture_internal_error {
|
macro_rules! capture_internal_error {
|
||||||
( $expr: expr ) => {
|
( $expr: expr ) => {
|
||||||
@@ -35,7 +37,7 @@ macro_rules! capture_internal_error {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "report-macros")]
|
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! report_internal_error {
|
macro_rules! report_internal_error {
|
||||||
( $expr: expr ) => {
|
( $expr: expr ) => {
|
||||||
@@ -188,6 +190,8 @@ pub struct ApiSecurity {
|
|||||||
pub captcha: ApiSecurityCaptcha,
|
pub captcha: ApiSecurityCaptcha,
|
||||||
pub trust_cloudflare: bool,
|
pub trust_cloudflare: bool,
|
||||||
pub easypwned: String,
|
pub easypwned: String,
|
||||||
|
pub token_secret: String,
|
||||||
|
pub tenor_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
@@ -363,6 +367,7 @@ pub struct Sentry {
|
|||||||
pub proxy: String,
|
pub proxy: String,
|
||||||
pub pushd: String,
|
pub pushd: String,
|
||||||
pub crond: String,
|
pub crond: String,
|
||||||
|
pub gifbox: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
@@ -419,6 +424,7 @@ pub async fn config() -> Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Configure logging and common Rust variables
|
/// Configure logging and common Rust variables
|
||||||
|
#[cfg(feature = "sentry")]
|
||||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
if std::env::var("RUST_LOG").is_err() {
|
||||||
std::env::set_var("RUST_LOG", "info");
|
std::env::set_var("RUST_LOG", "info");
|
||||||
@@ -444,6 +450,7 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sentry")]
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! configure {
|
macro_rules! configure {
|
||||||
($application: ident) => {
|
($application: ident) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-database"
|
name = "revolt-database"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -10,12 +10,12 @@ description = "Revolt Backend: Database Implementation"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Databases
|
# Databases
|
||||||
mongodb = ["dep:mongodb", "bson"]
|
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
|
||||||
|
|
||||||
# ... Other
|
# ... Other
|
||||||
tasks = ["isahc", "linkify", "url-escape"]
|
tasks = ["isahc", "linkify", "url-escape"]
|
||||||
async-std-runtime = ["async-std"]
|
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
|
||||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
|
||||||
axum-impl = ["axum"]
|
axum-impl = ["axum"]
|
||||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||||
|
|
||||||
@@ -24,19 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-config = { version = "0.8.8", path = "../config", features = [
|
revolt-config = { version = "0.8.9", path = "../config", features = [
|
||||||
"report-macros",
|
"report-macros",
|
||||||
] }
|
] }
|
||||||
revolt-result = { version = "0.8.8", path = "../result" }
|
revolt-result = { version = "0.8.9", path = "../result" }
|
||||||
revolt-models = { version = "0.8.8", path = "../models", features = [
|
revolt-models = { version = "0.8.9", path = "../models", features = [
|
||||||
"validator",
|
"validator",
|
||||||
] }
|
] }
|
||||||
revolt-presence = { version = "0.8.8", path = "../presence" }
|
revolt-presence = { version = "0.8.9", path = "../presence" }
|
||||||
revolt-permissions = { version = "0.8.8", path = "../permissions", features = [
|
revolt-permissions = { version = "0.8.9", path = "../permissions", features = [
|
||||||
"serde",
|
"serde",
|
||||||
"bson",
|
"bson",
|
||||||
] }
|
] }
|
||||||
revolt-parser = { version = "0.8.8", path = "../parser" }
|
revolt-parser = { version = "0.8.9", path = "../parser" }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
@@ -53,6 +53,8 @@ linkify = { optional = true, version = "0.8.1" }
|
|||||||
url-escape = { optional = true, version = "0.1.1" }
|
url-escape = { optional = true, version = "0.1.1" }
|
||||||
validator = { version = "0.16", features = ["derive"] }
|
validator = { version = "0.16", features = ["derive"] }
|
||||||
isahc = { optional = true, version = "1.7", features = ["json"] }
|
isahc = { optional = true, version = "1.7", features = ["json"] }
|
||||||
|
jsonwebtoken = "9.3.1"
|
||||||
|
chrono = "0.4"
|
||||||
|
|
||||||
# Serialisation
|
# Serialisation
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -91,13 +93,8 @@ rocket = { version = "0.5.1", default-features = false, features = [
|
|||||||
revolt_okapi = { version = "0.9.1", optional = true }
|
revolt_okapi = { version = "0.9.1", optional = true }
|
||||||
revolt_rocket_okapi = { version = "0.10.0", optional = true }
|
revolt_rocket_okapi = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
# Notifications
|
|
||||||
fcm_v1 = "0.3.0"
|
|
||||||
web-push = "0.10.0"
|
|
||||||
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
|
|
||||||
|
|
||||||
# Authifier
|
# Authifier
|
||||||
authifier = { version = "1.0.15", features = ["rocket_impl"] }
|
authifier = { version = "1.0.15" }
|
||||||
|
|
||||||
# RabbitMQ
|
# RabbitMQ
|
||||||
amqprs = { version = "1.7.0" }
|
amqprs = { version = "1.7.0" }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ use authifier::Authifier;
|
|||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use revolt_config::config;
|
use revolt_config::config;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
pub use self::mongodb::*;
|
pub use self::mongodb::*;
|
||||||
pub use self::reference::*;
|
pub use self::reference::*;
|
||||||
|
|
||||||
@@ -25,8 +27,10 @@ pub enum DatabaseInfo {
|
|||||||
/// Use the mock database
|
/// Use the mock database
|
||||||
Reference,
|
Reference,
|
||||||
/// Connect to MongoDB
|
/// Connect to MongoDB
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
MongoDb { uri: String, database_name: String },
|
MongoDb { uri: String, database_name: String },
|
||||||
/// Use existing MongoDB connection
|
/// Use existing MongoDB connection
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
MongoDbFromClient(::mongodb::Client, String),
|
MongoDbFromClient(::mongodb::Client, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +40,7 @@ pub enum Database {
|
|||||||
/// Mock database
|
/// Mock database
|
||||||
Reference(ReferenceDb),
|
Reference(ReferenceDb),
|
||||||
/// MongoDB database
|
/// MongoDB database
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
MongoDb(MongoDb),
|
MongoDb(MongoDb),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +50,7 @@ impl DatabaseInfo {
|
|||||||
pub async fn connect(self) -> Result<Database, String> {
|
pub async fn connect(self) -> Result<Database, String> {
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
|
|
||||||
Ok(match self {
|
match self {
|
||||||
DatabaseInfo::Auto => {
|
DatabaseInfo::Auto => {
|
||||||
if std::env::var("TEST_DB").is_ok() {
|
if std::env::var("TEST_DB").is_ok() {
|
||||||
DatabaseInfo::Test(format!(
|
DatabaseInfo::Test(format!(
|
||||||
@@ -53,16 +58,20 @@ impl DatabaseInfo {
|
|||||||
rand::thread_rng().gen_range(1_000_000..10_000_000)
|
rand::thread_rng().gen_range(1_000_000..10_000_000)
|
||||||
))
|
))
|
||||||
.connect()
|
.connect()
|
||||||
.await?
|
.await
|
||||||
} else if !config.database.mongodb.is_empty() {
|
} else if !config.database.mongodb.is_empty() {
|
||||||
DatabaseInfo::MongoDb {
|
#[cfg(feature = "mongodb")]
|
||||||
|
return DatabaseInfo::MongoDb {
|
||||||
uri: config.database.mongodb,
|
uri: config.database.mongodb,
|
||||||
database_name: "revolt".to_string(),
|
database_name: "revolt".to_string(),
|
||||||
}
|
}
|
||||||
.connect()
|
.connect()
|
||||||
.await?
|
.await;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "mongodb"))]
|
||||||
|
return Err("MongoDB not enabled.".to_string())
|
||||||
} else {
|
} else {
|
||||||
DatabaseInfo::Reference.connect().await?
|
DatabaseInfo::Reference.connect().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DatabaseInfo::Test(database_name) => {
|
DatabaseInfo::Test(database_name) => {
|
||||||
@@ -70,30 +79,36 @@ impl DatabaseInfo {
|
|||||||
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
|
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
|
||||||
.as_str()
|
.as_str()
|
||||||
{
|
{
|
||||||
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
|
"REFERENCE" => DatabaseInfo::Reference.connect().await,
|
||||||
"MONGODB" => {
|
"MONGODB" => {
|
||||||
DatabaseInfo::MongoDb {
|
#[cfg(feature = "mongodb")]
|
||||||
|
return DatabaseInfo::MongoDb {
|
||||||
uri: config.database.mongodb,
|
uri: config.database.mongodb,
|
||||||
database_name,
|
database_name,
|
||||||
}
|
}
|
||||||
.connect()
|
.connect()
|
||||||
.await?
|
.await;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "mongodb"))]
|
||||||
|
return Err("MongoDB not enabled.".to_string())
|
||||||
}
|
}
|
||||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DatabaseInfo::Reference => Database::Reference(Default::default()),
|
DatabaseInfo::Reference => Ok(Database::Reference(Default::default())),
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
DatabaseInfo::MongoDb { uri, database_name } => {
|
DatabaseInfo::MongoDb { uri, database_name } => {
|
||||||
let client = ::mongodb::Client::with_uri_str(uri)
|
let client = ::mongodb::Client::with_uri_str(uri)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| "Failed to init db connection.".to_string())?;
|
.map_err(|_| "Failed to init db connection.".to_string())?;
|
||||||
|
|
||||||
Database::MongoDb(MongoDb(client, database_name))
|
Ok(Database::MongoDb(MongoDb(client, database_name)))
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
DatabaseInfo::MongoDbFromClient(client, database_name) => {
|
DatabaseInfo::MongoDbFromClient(client, database_name) => {
|
||||||
Database::MongoDb(MongoDb(client, database_name))
|
Ok(Database::MongoDb(MongoDb(client, database_name)))
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,12 +234,16 @@ impl Database {
|
|||||||
Authifier {
|
Authifier {
|
||||||
database: match self {
|
database: match self {
|
||||||
Database::Reference(_) => Default::default(),
|
Database::Reference(_) => Default::default(),
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
||||||
authifier::database::MongoDb(client.database("revolt")),
|
authifier::database::MongoDb(client.database("revolt")),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
config: auth_config,
|
config: auth_config,
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
event_channel: Some(crate::tasks::authifier_relay::sender()),
|
event_channel: Some(crate::tasks::authifier_relay::sender()),
|
||||||
|
#[cfg(not(feature = "tasks"))]
|
||||||
|
event_channel: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use serde::de::DeserializeOwned;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
database_derived!(
|
database_derived!(
|
||||||
#[cfg(feature = "mongodb")]
|
|
||||||
/// MongoDB implementation
|
/// MongoDB implementation
|
||||||
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ use futures::lock::Mutex;
|
|||||||
use crate::{
|
use crate::{
|
||||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
|
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
|
||||||
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
|
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
|
||||||
User, UserSettings, Webhook,
|
User, UserSettings, Webhook, AuthorizedBotId, AuthorizedBot,
|
||||||
};
|
};
|
||||||
|
|
||||||
database_derived!(
|
database_derived!(
|
||||||
/// Reference implementation
|
/// Reference implementation
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ReferenceDb {
|
pub struct ReferenceDb {
|
||||||
|
pub authorized_bots: Arc<Mutex<HashMap<AuthorizedBotId, AuthorizedBot>>>,
|
||||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||||
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
|
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
|
||||||
|
|||||||
@@ -20,16 +20,31 @@ pub enum Ping {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fields provided in Ready payload
|
/// Fields provided in Ready payload
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq, Debug, Clone, Deserialize)]
|
||||||
pub enum ReadyPayloadFields {
|
pub struct ReadyPayloadFields {
|
||||||
Users,
|
pub users: bool,
|
||||||
Servers,
|
pub servers: bool,
|
||||||
Channels,
|
pub channels: bool,
|
||||||
Members,
|
pub members: bool,
|
||||||
Emoji,
|
pub emojis: bool,
|
||||||
|
pub user_settings: Vec<String>,
|
||||||
|
pub channel_unreads: bool,
|
||||||
|
pub policy_changes: bool,
|
||||||
|
}
|
||||||
|
|
||||||
UserSettings(Vec<String>),
|
impl Default for ReadyPayloadFields {
|
||||||
ChannelUnreads,
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
users: true,
|
||||||
|
servers: true,
|
||||||
|
channels: true,
|
||||||
|
members: true,
|
||||||
|
emojis: true,
|
||||||
|
user_settings: Vec::new(),
|
||||||
|
channel_unreads: false,
|
||||||
|
policy_changes: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Protocol Events
|
/// Protocol Events
|
||||||
@@ -63,7 +78,8 @@ pub enum EventV1 {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
channel_unreads: Option<Vec<ChannelUnread>>,
|
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||||
|
|
||||||
policy_changes: Vec<PolicyChange>,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
policy_changes: Option<Vec<PolicyChange>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Ping response
|
/// Ping response
|
||||||
@@ -144,7 +160,13 @@ pub enum EventV1 {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// User joins server
|
/// User joins server
|
||||||
ServerMemberJoin { id: String, user: String },
|
ServerMemberJoin {
|
||||||
|
id: String,
|
||||||
|
// Deprecated: use member.id.user
|
||||||
|
#[deprecated = "Use member.id.user instead"]
|
||||||
|
user: String,
|
||||||
|
member: Member,
|
||||||
|
},
|
||||||
|
|
||||||
/// User left server
|
/// User left server
|
||||||
ServerMemberLeave {
|
ServerMemberLeave {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use serde::Deserialize;
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use super::client::Ping;
|
use super::client::Ping;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum ClientMessage {
|
pub enum ClientMessage {
|
||||||
Authenticate { token: String },
|
Authenticate { token: String },
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ pub use mongodb;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate bson;
|
extern crate bson;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "async-std-runtime"))]
|
||||||
|
compile_error!("async-std-runtime feature must be enabled.");
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
macro_rules! query {
|
macro_rules! query {
|
||||||
@@ -103,6 +106,7 @@ pub mod util;
|
|||||||
pub use models::*;
|
pub use models::*;
|
||||||
|
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
pub mod tasks;
|
pub mod tasks;
|
||||||
|
|
||||||
mod amqp;
|
mod amqp;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
mod model;
|
||||||
|
mod ops;
|
||||||
|
|
||||||
|
pub use model::*;
|
||||||
|
pub use ops::*;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
|
|
||||||
|
use crate::OAuth2Scope;
|
||||||
|
|
||||||
|
auto_derived! (
|
||||||
|
/// Unique id of the user and bot
|
||||||
|
#[derive(Hash)]
|
||||||
|
pub struct AuthorizedBotId {
|
||||||
|
/// User id
|
||||||
|
pub user: String,
|
||||||
|
|
||||||
|
/// Bot Id
|
||||||
|
pub bot: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthorizedBot {
|
||||||
|
/// Unique Id
|
||||||
|
#[serde(rename = "_id")]
|
||||||
|
pub id: AuthorizedBotId,
|
||||||
|
|
||||||
|
/// When the authorized oauth2 bot connection was created at
|
||||||
|
pub created_at: Timestamp,
|
||||||
|
|
||||||
|
/// If and when the authorized oauth2 bot connection was revoked at
|
||||||
|
pub deauthorized_at: Option<Timestamp>,
|
||||||
|
|
||||||
|
/// Scopes the bot has access to
|
||||||
|
pub scope: Vec<OAuth2Scope>,
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
use revolt_result::Result;
|
||||||
|
|
||||||
|
use crate::{AuthorizedBot, AuthorizedBotId};
|
||||||
|
|
||||||
|
mod mongodb;
|
||||||
|
mod reference;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AbstractAuthorizedBots: Sync + Send {
|
||||||
|
/// Insert emoji into database.
|
||||||
|
async fn insert_authorized_bot(&self, authorized_bot: &AuthorizedBot) -> Result<()>;
|
||||||
|
|
||||||
|
/// Fetch an authorized bot by its id
|
||||||
|
async fn fetch_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot>;
|
||||||
|
|
||||||
|
/// Fetch a users authorized bot by its id
|
||||||
|
async fn fetch_users_authorized_bots(&self, user_id: &str) -> Result<Vec<AuthorizedBot>>;
|
||||||
|
|
||||||
|
/// Deletes an authorized bot
|
||||||
|
async fn delete_authorized_bot(&self, id: &AuthorizedBotId) -> Result<()>;
|
||||||
|
|
||||||
|
/// Deauthorizes an authorized bot
|
||||||
|
async fn deauthorize_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot>;
|
||||||
|
|
||||||
|
// Fetches all authorized bots which have been deauthorized
|
||||||
|
async fn fetch_deauthorized_authorized_bots(&self) -> Result<Vec<AuthorizedBot>>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
use bson::to_bson;
|
||||||
|
use revolt_result::Result;
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
|
|
||||||
|
use crate::{MongoDb, AuthorizedBot, AuthorizedBotId};
|
||||||
|
|
||||||
|
use super::AbstractAuthorizedBots;
|
||||||
|
|
||||||
|
static COL: &str = "authorized_bots";
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AbstractAuthorizedBots for MongoDb {
|
||||||
|
/// Insert an authorized bot into database.
|
||||||
|
async fn insert_authorized_bot(&self, authorized_bot: &AuthorizedBot) -> Result<()> {
|
||||||
|
query!(self, insert_one, COL, &authorized_bot).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch an authorized bot by its id
|
||||||
|
async fn fetch_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot> {
|
||||||
|
query!(
|
||||||
|
self,
|
||||||
|
find_one,
|
||||||
|
COL,
|
||||||
|
doc! {
|
||||||
|
"_id.user": &id.user,
|
||||||
|
"_id.bot": &id.bot
|
||||||
|
}
|
||||||
|
)?.ok_or_else(|| create_error!(NotFound))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch a users authorized bot by its id
|
||||||
|
async fn fetch_users_authorized_bots(&self, user_id: &str) -> Result<Vec<AuthorizedBot>> {
|
||||||
|
query!(self, find, COL, doc! { "_id.user": &user_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes an authorized bot
|
||||||
|
async fn delete_authorized_bot(&self, id: &AuthorizedBotId) -> Result<()> {
|
||||||
|
query!(self, delete_one, COL, doc! { "_id.user": &id.user, "_id.bot": &id.bot }).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deauthorizes an authorized bot
|
||||||
|
async fn deauthorize_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot> {
|
||||||
|
self.col::<AuthorizedBot>(COL)
|
||||||
|
.find_one_and_update(
|
||||||
|
doc! {
|
||||||
|
"_id.user": &id.user,
|
||||||
|
"_id.bot": &id.bot
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"deauthorized_at": to_bson(&Timestamp::now_utc()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_database_error!("find_one_and_update", COL))
|
||||||
|
.and_then(|opt| opt.ok_or_else(|| create_database_error!("find_one_and_update", COL)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches all authorized bots which have been deauthorized
|
||||||
|
async fn fetch_deauthorized_authorized_bots(&self) -> Result<Vec<AuthorizedBot>> {
|
||||||
|
query!(
|
||||||
|
self,
|
||||||
|
find,
|
||||||
|
COL,
|
||||||
|
doc! {
|
||||||
|
"deauthorized_at": { "$exists": true }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use revolt_result::Result;
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
|
|
||||||
|
use crate::{ReferenceDb, AuthorizedBot, AuthorizedBotId};
|
||||||
|
|
||||||
|
use super::AbstractAuthorizedBots;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AbstractAuthorizedBots for ReferenceDb {
|
||||||
|
/// Insert an authorized bot into database.
|
||||||
|
async fn insert_authorized_bot(&self, authorized_bot: &AuthorizedBot) -> Result<()> {
|
||||||
|
let mut authorized_bots = self.authorized_bots.lock().await;
|
||||||
|
|
||||||
|
if authorized_bots.contains_key(&authorized_bot.id) {
|
||||||
|
Err(create_database_error!("insert", "authorized_bots"))
|
||||||
|
} else {
|
||||||
|
authorized_bots.insert(authorized_bot.id.clone(), authorized_bot.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch an authorized bot by its id
|
||||||
|
async fn fetch_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot> {
|
||||||
|
let authorized_bots = self.authorized_bots.lock().await;
|
||||||
|
|
||||||
|
authorized_bots.get(id).cloned().ok_or_else(|| create_error!(NotFound))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch a users authorized bot by its id
|
||||||
|
async fn fetch_users_authorized_bots(&self, user_id: &str) -> Result<Vec<AuthorizedBot>> {
|
||||||
|
let authorized_bots = self.authorized_bots.lock().await;
|
||||||
|
|
||||||
|
Ok(authorized_bots
|
||||||
|
.values()
|
||||||
|
.filter(|authorized_bot| authorized_bot.id.user == user_id)
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes an authorized bot
|
||||||
|
async fn delete_authorized_bot(&self, id: &AuthorizedBotId) -> Result<()> {
|
||||||
|
let mut authorized_bots = self.authorized_bots.lock().await;
|
||||||
|
|
||||||
|
if authorized_bots.remove(id).is_some() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error!(NotFound))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deauthorizes an authorized bot
|
||||||
|
async fn deauthorize_authorized_bot(&self, id: &AuthorizedBotId) -> Result<AuthorizedBot> {
|
||||||
|
let mut authorized_bots = self.authorized_bots.lock().await;
|
||||||
|
|
||||||
|
if let Some(authorized_bot) = authorized_bots.get_mut(id) {
|
||||||
|
authorized_bot.deauthorized_at = Some(Timestamp::now_utc());
|
||||||
|
|
||||||
|
Ok(authorized_bot.clone())
|
||||||
|
} else {
|
||||||
|
Err(create_error!(NotFound))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches all authorized bots which have been deauthorized
|
||||||
|
async fn fetch_deauthorized_authorized_bots(&self) -> Result<Vec<AuthorizedBot>> {
|
||||||
|
let authorized_bots = self.authorized_bots.lock().await;
|
||||||
|
|
||||||
|
Ok(authorized_bots
|
||||||
|
.values()
|
||||||
|
.filter(|authorized_bot| authorized_bot.deauthorized_at.is_some())
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
use crate::{events::client::EventV1, BotInformation, Database, PartialUser, User};
|
||||||
|
|
||||||
@@ -35,6 +36,10 @@ auto_derived_partial!(
|
|||||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||||
pub privacy_policy_url: String,
|
pub privacy_policy_url: String,
|
||||||
|
|
||||||
|
/// Oauth2 bot settings
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub oauth2: Option<BotOauth2>,
|
||||||
|
|
||||||
/// Enum of bot flags
|
/// Enum of bot flags
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub flags: Option<i32>,
|
pub flags: Option<i32>,
|
||||||
@@ -42,11 +47,52 @@ auto_derived_partial!(
|
|||||||
"PartialBot"
|
"PartialBot"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
auto_derived!(
|
||||||
|
#[derive(Copy, Hash)]
|
||||||
|
pub enum OAuth2Scope {
|
||||||
|
#[serde(rename = "read:identify")]
|
||||||
|
ReadIdentify,
|
||||||
|
#[serde(rename = "read:servers")]
|
||||||
|
ReadServers,
|
||||||
|
#[serde(rename = "write:files")]
|
||||||
|
WriteFiles,
|
||||||
|
#[serde(rename = "events")]
|
||||||
|
Events,
|
||||||
|
#[serde(rename = "full")]
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OAuth2ScopeReasoning {
|
||||||
|
pub allow: String,
|
||||||
|
pub deny: String
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
auto_derived_partial!(
|
||||||
|
pub struct BotOauth2 {
|
||||||
|
/// Whether the oauth2 client is public and should not receive a secret key
|
||||||
|
#[serde(default)]
|
||||||
|
pub public: bool,
|
||||||
|
/// Secret key used for authorisation, not provided if the client is public
|
||||||
|
#[serde(default)]
|
||||||
|
pub secret: Option<String>,
|
||||||
|
/// Allowed redirects for the authorisation
|
||||||
|
#[serde(default)]
|
||||||
|
pub redirects: Vec<String>,
|
||||||
|
/// Mapping of allowed scopes and the reasonings
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>,
|
||||||
|
},
|
||||||
|
"PartialBotOauth2"
|
||||||
|
);
|
||||||
|
|
||||||
auto_derived!(
|
auto_derived!(
|
||||||
/// Optional fields on bot object
|
/// Optional fields on bot object
|
||||||
pub enum FieldsBot {
|
pub enum FieldsBot {
|
||||||
Token,
|
Token,
|
||||||
InteractionsURL,
|
InteractionsURL,
|
||||||
|
Oauth2,
|
||||||
|
Oauth2Secret,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -63,11 +109,24 @@ impl Default for Bot {
|
|||||||
interactions_url: Default::default(),
|
interactions_url: Default::default(),
|
||||||
terms_of_service_url: Default::default(),
|
terms_of_service_url: Default::default(),
|
||||||
privacy_policy_url: Default::default(),
|
privacy_policy_url: Default::default(),
|
||||||
|
oauth2: Default::default(),
|
||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::derivable_impls)]
|
||||||
|
impl Default for BotOauth2 {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
public: false,
|
||||||
|
secret: Some(nanoid::nanoid!(64)),
|
||||||
|
redirects: Vec::new(),
|
||||||
|
allowed_scopes: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
impl Bot {
|
impl Bot {
|
||||||
/// Create a new bot
|
/// Create a new bot
|
||||||
@@ -124,6 +183,14 @@ impl Bot {
|
|||||||
FieldsBot::Token => self.token = nanoid::nanoid!(64),
|
FieldsBot::Token => self.token = nanoid::nanoid!(64),
|
||||||
FieldsBot::InteractionsURL => {
|
FieldsBot::InteractionsURL => {
|
||||||
self.interactions_url = String::new();
|
self.interactions_url = String::new();
|
||||||
|
},
|
||||||
|
FieldsBot::Oauth2 => self.oauth2 = None,
|
||||||
|
FieldsBot::Oauth2Secret => {
|
||||||
|
if let Some(oauth2) = &mut self.oauth2 {
|
||||||
|
if !oauth2.public {
|
||||||
|
oauth2.secret = Some(nanoid::nanoid!(64))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::{Bot, FieldsBot, PartialBot};
|
use crate::{Bot, FieldsBot, PartialBot};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ impl IntoDocumentPath for FieldsBot {
|
|||||||
match self {
|
match self {
|
||||||
FieldsBot::InteractionsURL => Some("interactions_url"),
|
FieldsBot::InteractionsURL => Some("interactions_url"),
|
||||||
FieldsBot::Token => None,
|
FieldsBot::Token => None,
|
||||||
|
FieldsBot::Oauth2 => Some("oauth2"),
|
||||||
|
FieldsBot::Oauth2Secret => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::Invite;
|
use crate::Invite;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::ChannelUnread;
|
use crate::ChannelUnread;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ use serde::{Deserialize, Serialize};
|
|||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
|
events::client::EventV1, Database, File, PartialServer,
|
||||||
Server, SystemMessage, User, AMQP,
|
Server, SystemMessage, User, AMQP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
|
use crate::IntoDocumentPath;
|
||||||
|
|
||||||
auto_derived!(
|
auto_derived!(
|
||||||
#[serde(tag = "channel_type")]
|
#[serde(tag = "channel_type")]
|
||||||
pub enum Channel {
|
pub enum Channel {
|
||||||
@@ -646,10 +649,11 @@ impl Channel {
|
|||||||
.private(user.to_string())
|
.private(user.to_string())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
crate::tasks::ack::queue_ack(
|
crate::tasks::ack::queue_ack(
|
||||||
self.id().to_string(),
|
self.id().to_string(),
|
||||||
user.to_string(),
|
user.to_string(),
|
||||||
AckEvent::AckMessage {
|
crate::tasks::ack::AckEvent::AckMessage {
|
||||||
id: message.to_string(),
|
id: message.to_string(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -766,6 +770,7 @@ impl Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
impl IntoDocumentPath for FieldsChannel {
|
impl IntoDocumentPath for FieldsChannel {
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
fn as_path(&self) -> Option<&'static str> {
|
||||||
Some(match self {
|
Some(match self {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
||||||
use revolt_permissions::OverrideField;
|
use revolt_permissions::OverrideField;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::Emoji;
|
use crate::Emoji;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::FileHash;
|
use crate::FileHash;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::File;
|
|||||||
|
|
||||||
use super::FileUsedFor;
|
use super::FileUsedFor;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ use validator::Validate;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
events::client::EventV1,
|
events::client::EventV1,
|
||||||
tasks::{self, ack::AckEvent},
|
|
||||||
util::{
|
util::{
|
||||||
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
|
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
|
||||||
permissions::DatabasePermissionQuery,
|
permissions::DatabasePermissionQuery,
|
||||||
@@ -22,6 +21,9 @@ use crate::{
|
|||||||
Channel, Database, Emoji, File, User, AMQP,
|
Channel, Database, Emoji, File, User, AMQP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
|
use crate::tasks::{self, ack::AckEvent};
|
||||||
|
|
||||||
auto_derived_partial!(
|
auto_derived_partial!(
|
||||||
/// Message
|
/// Message
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
@@ -438,7 +440,8 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify replies are valid.
|
// Verify replies are valid.
|
||||||
let mut replies = HashSet::new();
|
let mut replies = Vec::new();
|
||||||
|
|
||||||
if let Some(entries) = data.replies {
|
if let Some(entries) = data.replies {
|
||||||
if entries.len() > config.features.limits.global.message_replies {
|
if entries.len() > config.features.limits.global.message_replies {
|
||||||
return Err(create_error!(TooManyReplies {
|
return Err(create_error!(TooManyReplies {
|
||||||
@@ -446,6 +449,8 @@ impl Message {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replies.reserve(entries.len());
|
||||||
|
|
||||||
for ReplyIntent {
|
for ReplyIntent {
|
||||||
id,
|
id,
|
||||||
mention,
|
mention,
|
||||||
@@ -459,7 +464,12 @@ impl Message {
|
|||||||
user_mentions.insert(message.author.to_owned());
|
user_mentions.insert(message.author.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
replies.insert(message.id);
|
// This is O(n^2), but this is faster than a HashSet
|
||||||
|
// when n < 20; as long as the message_replies limit
|
||||||
|
// is reasonable, this will be fast.
|
||||||
|
if !replies.contains(&message.id) {
|
||||||
|
replies.push(message.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If the referenced message doesn't exist and fail_if_not_exists
|
// If the referenced message doesn't exist and fail_if_not_exists
|
||||||
// is set to false, send the message without the reply.
|
// is set to false, send the message without the reply.
|
||||||
@@ -532,9 +542,7 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !replies.is_empty() {
|
if !replies.is_empty() {
|
||||||
message
|
message.replies.replace(replies);
|
||||||
.replies
|
|
||||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate final message flags
|
// Calculate final message flags
|
||||||
@@ -616,9 +624,11 @@ impl Message {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Update last_message_id
|
// Update last_message_id
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
|
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
|
||||||
|
|
||||||
// Add mentions for affected users
|
// Add mentions for affected users
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
if !mentions_elsewhere {
|
if !mentions_elsewhere {
|
||||||
if let Some(mentions) = &self.mentions {
|
if let Some(mentions) = &self.mentions {
|
||||||
tasks::ack::queue_message(
|
tasks::ack::queue_message(
|
||||||
@@ -637,6 +647,7 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate embeds
|
// Generate embeds
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
if generate_embeds {
|
if generate_embeds {
|
||||||
if let Some(content) = &self.content {
|
if let Some(content) = &self.content {
|
||||||
tasks::process_embeds::queue(
|
tasks::process_embeds::queue(
|
||||||
@@ -673,10 +684,12 @@ impl Message {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
||||||
if !self.has_suppressed_notifications()
|
if !self.has_suppressed_notifications()
|
||||||
&& (self.mentions.is_some() || self.contains_mass_push_mention())
|
&& (self.mentions.is_some() || self.contains_mass_push_mention())
|
||||||
{
|
{
|
||||||
// send Push notifications
|
// send Push notifications
|
||||||
|
#[cfg(feature = "tasks")]
|
||||||
tasks::ack::queue_message(
|
tasks::ack::queue_message(
|
||||||
self.channel.to_string(),
|
self.channel.to_string(),
|
||||||
AckEvent::ProcessMessage {
|
AckEvent::ProcessMessage {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod admin_migrations;
|
mod admin_migrations;
|
||||||
|
mod authorized_bots;
|
||||||
mod bots;
|
mod bots;
|
||||||
mod channel_invites;
|
mod channel_invites;
|
||||||
mod channel_unreads;
|
mod channel_unreads;
|
||||||
@@ -19,6 +20,7 @@ mod user_settings;
|
|||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
pub use admin_migrations::*;
|
pub use admin_migrations::*;
|
||||||
|
pub use authorized_bots::*;
|
||||||
pub use bots::*;
|
pub use bots::*;
|
||||||
pub use channel_invites::*;
|
pub use channel_invites::*;
|
||||||
pub use channel_unreads::*;
|
pub use channel_unreads::*;
|
||||||
@@ -38,11 +40,15 @@ pub use servers::*;
|
|||||||
pub use user_settings::*;
|
pub use user_settings::*;
|
||||||
pub use users::*;
|
pub use users::*;
|
||||||
|
|
||||||
use crate::{Database, MongoDb, ReferenceDb};
|
use crate::{Database, ReferenceDb};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
|
use crate::MongoDb;
|
||||||
|
|
||||||
pub trait AbstractDatabase:
|
pub trait AbstractDatabase:
|
||||||
Sync
|
Sync
|
||||||
+ Send
|
+ Send
|
||||||
|
+ authorized_bots::AbstractAuthorizedBots
|
||||||
+ admin_migrations::AbstractMigrations
|
+ admin_migrations::AbstractMigrations
|
||||||
+ bots::AbstractBots
|
+ bots::AbstractBots
|
||||||
+ channels::AbstractChannels
|
+ channels::AbstractChannels
|
||||||
@@ -66,6 +72,8 @@ pub trait AbstractDatabase:
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AbstractDatabase for ReferenceDb {}
|
impl AbstractDatabase for ReferenceDb {}
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
impl AbstractDatabase for MongoDb {}
|
impl AbstractDatabase for MongoDb {}
|
||||||
|
|
||||||
impl std::ops::Deref for Database {
|
impl std::ops::Deref for Database {
|
||||||
@@ -74,6 +82,7 @@ impl std::ops::Deref for Database {
|
|||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
match &self {
|
match &self {
|
||||||
Database::Reference(dummy) => dummy,
|
Database::Reference(dummy) => dummy,
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
Database::MongoDb(mongo) => mongo,
|
Database::MongoDb(mongo) => mongo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::PolicyChange;
|
use crate::PolicyChange;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
|
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::Report;
|
use crate::Report;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::Snapshot;
|
use crate::Snapshot;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::{MemberCompositeKey, ServerBan};
|
use crate::{MemberCompositeKey, ServerBan};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ auto_derived_partial!(
|
|||||||
/// Timestamp this member is timed out until
|
/// Timestamp this member is timed out until
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub timeout: Option<Timestamp>,
|
pub timeout: Option<Timestamp>,
|
||||||
|
// This value only exists in the database, not the models.
|
||||||
|
// If it is not-None, the database layer should return None to member fetching queries.
|
||||||
|
// pub pending_deletion_at: Option<Timestamp>
|
||||||
},
|
},
|
||||||
"PartialMember"
|
"PartialMember"
|
||||||
);
|
);
|
||||||
@@ -50,6 +53,7 @@ auto_derived!(
|
|||||||
Avatar,
|
Avatar,
|
||||||
Roles,
|
Roles,
|
||||||
Timeout,
|
Timeout,
|
||||||
|
JoinedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Member removal intention
|
/// Member removal intention
|
||||||
@@ -90,7 +94,7 @@ impl Member {
|
|||||||
return Err(create_error!(AlreadyInServer));
|
return Err(create_error!(AlreadyInServer));
|
||||||
}
|
}
|
||||||
|
|
||||||
let member = Member {
|
let mut member = Member {
|
||||||
id: MemberCompositeKey {
|
id: MemberCompositeKey {
|
||||||
server: server.id.to_string(),
|
server: server.id.to_string(),
|
||||||
user: user.id.to_string(),
|
user: user.id.to_string(),
|
||||||
@@ -98,7 +102,9 @@ impl Member {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
db.insert_member(&member).await?;
|
if let Some(updated) = db.insert_or_merge_member(&member).await? {
|
||||||
|
member = updated;
|
||||||
|
}
|
||||||
|
|
||||||
let should_fetch = channels.is_none();
|
let should_fetch = channels.is_none();
|
||||||
let mut channels = channels.unwrap_or_default();
|
let mut channels = channels.unwrap_or_default();
|
||||||
@@ -124,6 +130,7 @@ impl Member {
|
|||||||
EventV1::ServerMemberJoin {
|
EventV1::ServerMemberJoin {
|
||||||
id: server.id.clone(),
|
id: server.id.clone(),
|
||||||
user: user.id.clone(),
|
user: user.id.clone(),
|
||||||
|
member: member.clone().into(),
|
||||||
}
|
}
|
||||||
.p(server.id.clone())
|
.p(server.id.clone())
|
||||||
.await;
|
.await;
|
||||||
@@ -186,6 +193,7 @@ impl Member {
|
|||||||
|
|
||||||
pub fn remove_field(&mut self, field: &FieldsMember) {
|
pub fn remove_field(&mut self, field: &FieldsMember) {
|
||||||
match field {
|
match field {
|
||||||
|
FieldsMember::JoinedAt => (),
|
||||||
FieldsMember::Avatar => self.avatar = None,
|
FieldsMember::Avatar => self.avatar = None,
|
||||||
FieldsMember::Nickname => self.nickname = None,
|
FieldsMember::Nickname => self.nickname = None,
|
||||||
FieldsMember::Roles => self.roles.clear(),
|
FieldsMember::Roles => self.roles.clear(),
|
||||||
@@ -224,7 +232,7 @@ impl Member {
|
|||||||
intention: RemovalIntention,
|
intention: RemovalIntention,
|
||||||
silent: bool,
|
silent: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
db.delete_member(&self.id).await?;
|
db.soft_delete_member(&self.id).await?;
|
||||||
|
|
||||||
EventV1::ServerMemberLeave {
|
EventV1::ServerMemberLeave {
|
||||||
id: self.id.server.to_string(),
|
id: self.id.server.to_string(),
|
||||||
@@ -260,3 +268,74 @@ impl Member {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use iso8601_timestamp::{Duration, Timestamp};
|
||||||
|
use revolt_models::v0::DataCreateServer;
|
||||||
|
|
||||||
|
use crate::{Member, PartialMember, RemovalIntention, Server, User};
|
||||||
|
|
||||||
|
#[async_std::test]
|
||||||
|
async fn muted_member_rejoin() {
|
||||||
|
database_test!(|db| async move {
|
||||||
|
match db {
|
||||||
|
crate::Database::Reference(_) => return,
|
||||||
|
crate::Database::MongoDb(_) => (),
|
||||||
|
}
|
||||||
|
let owner = User::create(&db, "Server Owner".to_string(), None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let kickable_user = User::create(&db, "Member".to_string(), None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let server = Server::create(
|
||||||
|
&db,
|
||||||
|
DataCreateServer {
|
||||||
|
name: "Server".to_string(),
|
||||||
|
description: None,
|
||||||
|
nsfw: None,
|
||||||
|
},
|
||||||
|
&owner,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
|
Member::create(&db, &server, &owner, None).await.unwrap();
|
||||||
|
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
|
kickable_member
|
||||||
|
.update(
|
||||||
|
&db,
|
||||||
|
PartialMember {
|
||||||
|
timeout: Some(Timestamp::now_utc() + Duration::minutes(5)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(kickable_member.in_timeout());
|
||||||
|
|
||||||
|
kickable_member
|
||||||
|
.remove(&db, &server, RemovalIntention::Kick, false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let kickable_member = Member::create(&db, &server, &kickable_user, None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
|
||||||
|
assert!(kickable_member.in_timeout())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
use ::mongodb::SessionCursor;
|
#[cfg(feature = "mongodb")]
|
||||||
|
use ::mongodb::{ClientSession, SessionCursor};
|
||||||
|
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum ChunkedServerMembersGenerator {
|
pub enum ChunkedServerMembersGenerator {
|
||||||
#[cfg(feature = "mongodb")]
|
#[cfg(feature = "mongodb")]
|
||||||
MongoDb {
|
MongoDb {
|
||||||
session: ::mongodb::ClientSession,
|
session: ClientSession,
|
||||||
cursor: Option<SessionCursor<Member>>,
|
cursor: Option<SessionCursor<Member>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -22,7 +26,7 @@ pub enum ChunkedServerMembersGenerator {
|
|||||||
|
|
||||||
impl ChunkedServerMembersGenerator {
|
impl ChunkedServerMembersGenerator {
|
||||||
#[cfg(feature = "mongodb")]
|
#[cfg(feature = "mongodb")]
|
||||||
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
|
pub fn new_mongo(session: ClientSession, cursor: SessionCursor<Member>) -> Self {
|
||||||
ChunkedServerMembersGenerator::MongoDb {
|
ChunkedServerMembersGenerator::MongoDb {
|
||||||
session,
|
session,
|
||||||
cursor: Some(cursor),
|
cursor: Some(cursor),
|
||||||
@@ -69,13 +73,13 @@ impl ChunkedServerMembersGenerator {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AbstractServerMembers: Sync + Send {
|
pub trait AbstractServerMembers: Sync + Send {
|
||||||
/// Insert a new server member into the database
|
/// Insert a new server member into the database
|
||||||
async fn insert_member(&self, member: &Member) -> Result<()>;
|
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>>;
|
||||||
|
|
||||||
/// Fetch a server member by their id
|
/// Fetch a server member by their id
|
||||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
||||||
|
|
||||||
/// Fetch all members in a server
|
/// Fetch all members in a server
|
||||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
|
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>>;
|
||||||
|
|
||||||
/// Fetch all members in a server as an iterator
|
/// Fetch all members in a server as an iterator
|
||||||
async fn fetch_all_members_chunked(
|
async fn fetch_all_members_chunked(
|
||||||
@@ -96,10 +100,10 @@ pub trait AbstractServerMembers: Sync + Send {
|
|||||||
) -> Result<ChunkedServerMembersGenerator>;
|
) -> Result<ChunkedServerMembersGenerator>;
|
||||||
|
|
||||||
/// Fetch all memberships for a user
|
/// Fetch all memberships for a user
|
||||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
|
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>>;
|
||||||
|
|
||||||
/// Fetch multiple members by their ids
|
/// Fetch multiple members by their ids
|
||||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>;
|
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>>;
|
||||||
|
|
||||||
/// Fetch member count of a server
|
/// Fetch member count of a server
|
||||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
|
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
|
||||||
@@ -115,6 +119,14 @@ pub trait AbstractServerMembers: Sync + Send {
|
|||||||
remove: Vec<FieldsMember>,
|
remove: Vec<FieldsMember>,
|
||||||
) -> Result<()>;
|
) -> Result<()>;
|
||||||
|
|
||||||
/// Delete a server member by their id
|
/// Marks a user as no longer a member of a server, while retaining the database value.
|
||||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
/// This is used to keep information such as timeouts in place, but will remove information such as join date and applied roles.
|
||||||
|
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||||
|
|
||||||
|
/// Forcibly delete a server member by their id.
|
||||||
|
/// This will cancel any pending timeouts or other longer term actions, and they will not be reapplied on rejoin.
|
||||||
|
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||||
|
|
||||||
|
/// Fetch all members who have been marked for deletion.
|
||||||
|
async fn remove_dangling_members(&self) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
use bson::Document;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
use mongodb::options::ReadConcern;
|
use mongodb::options::ReadConcern;
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
@@ -11,9 +13,42 @@ static COL: &str = "server_members";
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AbstractServerMembers for MongoDb {
|
impl AbstractServerMembers for MongoDb {
|
||||||
/// Insert a new server member into the database
|
/// Insert a new server member (or use the existing member if one is found)
|
||||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
|
||||||
query!(self, insert_one, COL, &member).map(|_| ())
|
let existing: Result<Option<Document>> = query!(
|
||||||
|
self,
|
||||||
|
find_one,
|
||||||
|
COL,
|
||||||
|
doc! {
|
||||||
|
"_id.server": &member.id.server,
|
||||||
|
"_id.user": &member.id.user,
|
||||||
|
"pending_deletion_at": {"$exists": true}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Update the existing record if it exist, otherwise make a new record
|
||||||
|
if existing.is_ok_and(|x| x.is_some()) {
|
||||||
|
self.col::<Member>(COL)
|
||||||
|
.find_one_and_update(
|
||||||
|
doc! {
|
||||||
|
"_id.server": &member.id.server,
|
||||||
|
"_id.user": &member.id.user,
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"joined_at": member.joined_at.duration_since(Timestamp::UNIX_EPOCH).whole_seconds(),
|
||||||
|
},
|
||||||
|
"$unset": {
|
||||||
|
"pending_deletion_at": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.return_document(mongodb::options::ReturnDocument::After)
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_database_error!("update_one", COL))
|
||||||
|
} else {
|
||||||
|
query!(self, insert_one, COL, &member).map(|_| ())?;
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a server member by their id
|
/// Fetch a server member by their id
|
||||||
@@ -24,18 +59,20 @@ impl AbstractServerMembers for MongoDb {
|
|||||||
COL,
|
COL,
|
||||||
doc! {
|
doc! {
|
||||||
"_id.server": server_id,
|
"_id.server": server_id,
|
||||||
"_id.user": user_id
|
"_id.user": user_id,
|
||||||
|
"pending_deletion_at": {"$exists": false}
|
||||||
}
|
}
|
||||||
)?
|
)?
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
.ok_or_else(|| create_error!(NotFound))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch all members in a server
|
/// Fetch all members in a server
|
||||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
|
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.col::<Member>(COL)
|
.col::<Member>(COL)
|
||||||
.find(doc! {
|
.find(doc! {
|
||||||
"_id.server": server_id
|
"_id.server": server_id,
|
||||||
|
"pending_deletion_at": {"$exists": false}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| create_database_error!("find", COL))?
|
.map_err(|_| create_database_error!("find", COL))?
|
||||||
@@ -139,11 +176,12 @@ impl AbstractServerMembers for MongoDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch all memberships for a user
|
/// Fetch all memberships for a user
|
||||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.col::<Member>(COL)
|
.col::<Member>(COL)
|
||||||
.find(doc! {
|
.find(doc! {
|
||||||
"_id.user": user_id
|
"_id.user": user_id,
|
||||||
|
"pending_deletion_at": {"$exists": false}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| create_database_error!("find", COL))?
|
.map_err(|_| create_database_error!("find", COL))?
|
||||||
@@ -159,11 +197,12 @@ impl AbstractServerMembers for MongoDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch multiple members by their ids
|
/// Fetch multiple members by their ids
|
||||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.col::<Member>(COL)
|
.col::<Member>(COL)
|
||||||
.find(doc! {
|
.find(doc! {
|
||||||
"_id.server": server_id,
|
"_id.server": server_id,
|
||||||
|
"pending_deletion_at": {"$exists": false},
|
||||||
"_id.user": {
|
"_id.user": {
|
||||||
"$in": ids
|
"$in": ids
|
||||||
}
|
}
|
||||||
@@ -185,7 +224,8 @@ impl AbstractServerMembers for MongoDb {
|
|||||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
|
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
|
||||||
self.col::<Member>(COL)
|
self.col::<Member>(COL)
|
||||||
.count_documents(doc! {
|
.count_documents(doc! {
|
||||||
"_id.server": server_id
|
"_id.server": server_id,
|
||||||
|
"pending_deletion_at": {"$exists": false}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map(|c| c as usize)
|
.map(|c| c as usize)
|
||||||
@@ -196,7 +236,8 @@ impl AbstractServerMembers for MongoDb {
|
|||||||
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
|
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
|
||||||
self.col::<Member>(COL)
|
self.col::<Member>(COL)
|
||||||
.count_documents(doc! {
|
.count_documents(doc! {
|
||||||
"_id.user": user_id
|
"_id.user": user_id,
|
||||||
|
"pending_deletion_at": {"$exists": false}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map(|c| c as usize)
|
.map(|c| c as usize)
|
||||||
@@ -225,8 +266,42 @@ impl AbstractServerMembers for MongoDb {
|
|||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marks a member for deletion.
|
||||||
|
/// This will remove the record if the user has no pending actions (eg. timeout),
|
||||||
|
/// otherwise will slate the record for deletion by revolt_crond once the actions expire.
|
||||||
|
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||||
|
let member = self.fetch_member(&id.server, &id.user).await;
|
||||||
|
if let Ok(member) = member {
|
||||||
|
if member.in_timeout() {
|
||||||
|
self.col::<Document>(COL)
|
||||||
|
.update_many(
|
||||||
|
doc! {
|
||||||
|
"_id.server": &id.server,
|
||||||
|
"_id.user": &id.user,
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {"pending_deletion_at": format!("{}", member.timeout.unwrap().format())},
|
||||||
|
"$unset": {
|
||||||
|
"joined_at": "",
|
||||||
|
"avatar": "",
|
||||||
|
"nickname": "",
|
||||||
|
"roles": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| create_database_error!("update_many", COL))
|
||||||
|
} else {
|
||||||
|
self.force_delete_member(id).await
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(create_database_error!("fetch_member", COL))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a server member by their id
|
/// Delete a server member by their id
|
||||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||||
query!(
|
query!(
|
||||||
self,
|
self,
|
||||||
delete_one,
|
delete_one,
|
||||||
@@ -238,11 +313,25 @@ impl AbstractServerMembers for MongoDb {
|
|||||||
)
|
)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn remove_dangling_members(&self) -> Result<()> {
|
||||||
|
let now = Timestamp::now_utc();
|
||||||
|
let date = bson::to_bson(&now).expect("Failed to serialize timestamp");
|
||||||
|
|
||||||
|
self.col::<Document>(COL)
|
||||||
|
.delete_many(doc! {
|
||||||
|
"pending_deletion_at": {"$lt": date}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| create_database_error!("count_documents", COL))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsMember {
|
impl IntoDocumentPath for FieldsMember {
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
fn as_path(&self) -> Option<&'static str> {
|
||||||
Some(match self {
|
Some(match self {
|
||||||
|
FieldsMember::JoinedAt => "joined_at",
|
||||||
FieldsMember::Avatar => "avatar",
|
FieldsMember::Avatar => "avatar",
|
||||||
FieldsMember::Nickname => "nickname",
|
FieldsMember::Nickname => "nickname",
|
||||||
FieldsMember::Roles => "roles",
|
FieldsMember::Roles => "roles",
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AbstractServerMembers for ReferenceDb {
|
impl AbstractServerMembers for ReferenceDb {
|
||||||
/// Insert a new server member into the database
|
/// Insert a new server member into the database
|
||||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
|
||||||
let mut server_members = self.server_members.lock().await;
|
let mut server_members = self.server_members.lock().await;
|
||||||
if server_members.contains_key(&member.id) {
|
if server_members.contains_key(&member.id) {
|
||||||
Err(create_database_error!("insert", "member"))
|
Err(create_database_error!("insert", "member"))
|
||||||
} else {
|
} else {
|
||||||
server_members.insert(member.id.clone(), member.clone());
|
server_members.insert(member.id.clone(), member.clone());
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ impl AbstractServerMembers for ReferenceDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch all members in a server
|
/// Fetch all members in a server
|
||||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
|
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||||
let server_members = self.server_members.lock().await;
|
let server_members = self.server_members.lock().await;
|
||||||
Ok(server_members
|
Ok(server_members
|
||||||
.values()
|
.values()
|
||||||
@@ -105,7 +105,7 @@ impl AbstractServerMembers for ReferenceDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch all memberships for a user
|
/// Fetch all memberships for a user
|
||||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||||
let server_members = self.server_members.lock().await;
|
let server_members = self.server_members.lock().await;
|
||||||
Ok(server_members
|
Ok(server_members
|
||||||
.values()
|
.values()
|
||||||
@@ -115,7 +115,7 @@ impl AbstractServerMembers for ReferenceDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch multiple members by their ids
|
/// Fetch multiple members by their ids
|
||||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
|
||||||
let server_members = self.server_members.lock().await;
|
let server_members = self.server_members.lock().await;
|
||||||
Ok(ids
|
Ok(ids
|
||||||
.iter()
|
.iter()
|
||||||
@@ -169,8 +169,26 @@ impl AbstractServerMembers for ReferenceDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Soft delete a member
|
||||||
|
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||||
|
let mut server_members = self.server_members.lock().await;
|
||||||
|
|
||||||
|
let member = server_members.get_mut(id);
|
||||||
|
if let Some(member) = member {
|
||||||
|
if member.in_timeout() {
|
||||||
|
panic!("Soft deletion is not implemented.")
|
||||||
|
} else if server_members.remove(id).is_some() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(create_error!(NotFound))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(create_error!(NotFound))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a server member by their id
|
/// Delete a server member by their id
|
||||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||||
let mut server_members = self.server_members.lock().await;
|
let mut server_members = self.server_members.lock().await;
|
||||||
if server_members.remove(id).is_some() {
|
if server_members.remove(id).is_some() {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -178,4 +196,8 @@ impl AbstractServerMembers for ReferenceDb {
|
|||||||
Err(create_error!(NotFound))
|
Err(create_error!(NotFound))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn remove_dangling_members(&self) -> Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::UserSettings;
|
use crate::UserSettings;
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
use axum::{extract::FromRequestParts, http::request::Parts};
|
use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts};
|
||||||
|
|
||||||
|
use revolt_config::config;
|
||||||
|
use revolt_models::v0;
|
||||||
use revolt_result::{create_error, Error, Result};
|
use revolt_result::{create_error, Error, Result};
|
||||||
|
|
||||||
use crate::{Database, User};
|
use crate::{util::oauth2, Database, OAuth2Scope, User};
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl FromRequestParts<Database> for User {
|
impl<S> FromRequestParts<S> for User
|
||||||
|
where
|
||||||
|
Database: FromRef<S>,
|
||||||
|
S: Send + Sync
|
||||||
|
{
|
||||||
type Rejection = Error;
|
type Rejection = Error;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> {
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> {
|
||||||
|
let db = Database::from_ref(state);
|
||||||
|
|
||||||
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
|
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
|
||||||
let bot = db.fetch_bot_by_token(bot_token).await?;
|
let bot = db.fetch_bot_by_token(bot_token).await?;
|
||||||
db.fetch_user(&bot.id).await
|
db.fetch_user(&bot.id).await
|
||||||
@@ -17,6 +25,24 @@ impl FromRequestParts<Database> for User {
|
|||||||
{
|
{
|
||||||
let session = db.fetch_session_by_token(session_token).await?;
|
let session = db.fetch_session_by_token(session_token).await?;
|
||||||
db.fetch_user(&session.user_id).await
|
db.fetch_user(&session.user_id).await
|
||||||
|
} else if let Some(Ok(header_oauth_token)) = parts.headers.get("x-oauth2-token").map(|v| v.to_str()) {
|
||||||
|
let config = config().await;
|
||||||
|
|
||||||
|
let claims = oauth2::decode_token(
|
||||||
|
&config.api.security.token_secret,
|
||||||
|
header_oauth_token,
|
||||||
|
).map_err(|_| create_error!(NotAuthenticated))?;
|
||||||
|
|
||||||
|
let required_scope: v0::OAuth2Scope = parts.extensions.get::<OAuth2Scope>()
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| create_error!(NotAuthenticated))?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
if claims.scopes.contains(&v0::OAuth2Scope::Full) || claims.scopes.contains(&required_scope) {
|
||||||
|
db.fetch_user(&claims.sub).await
|
||||||
|
} else {
|
||||||
|
Err(create_error!(MissingScope { scope: required_scope.to_string() }))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Err(create_error!(NotAuthenticated))
|
Err(create_error!(NotAuthenticated))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||||
|
|
||||||
|
#[cfg(feature = "mongodb")]
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
|
|||||||
@@ -212,16 +212,34 @@ impl AbstractUsers for MongoDb {
|
|||||||
partial: &PartialUser,
|
partial: &PartialUser,
|
||||||
remove: Vec<FieldsUser>,
|
remove: Vec<FieldsUser>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
query!(
|
if remove.contains(&FieldsUser::StatusText) && partial.status.is_some() {
|
||||||
self,
|
// stupid-ass workaround to fix mongo conflicting the same item
|
||||||
update_one_by_id,
|
let _: Result<()> = query!(
|
||||||
COL,
|
self,
|
||||||
id,
|
update_one_by_id,
|
||||||
partial,
|
COL,
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
id,
|
||||||
None
|
PartialUser {
|
||||||
)
|
..Default::default()
|
||||||
.map(|_| ())
|
},
|
||||||
|
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.map(|_| ());
|
||||||
|
|
||||||
|
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
|
||||||
|
} else {
|
||||||
|
query!(
|
||||||
|
self,
|
||||||
|
update_one_by_id,
|
||||||
|
COL,
|
||||||
|
id,
|
||||||
|
partial,
|
||||||
|
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set relationship with another user
|
/// Set relationship with another user
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use authifier::models::Session;
|
use authifier::models::Session;
|
||||||
|
use revolt_config::config;
|
||||||
|
use revolt_models::v0;
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::request::{self, FromRequest, Outcome, Request};
|
use rocket::request::{self, FromRequest, Outcome, Request};
|
||||||
|
|
||||||
|
use crate::util::oauth2;
|
||||||
use crate::{Database, User};
|
use crate::{Database, User};
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
@@ -19,12 +22,38 @@ impl<'r> FromRequest<'r> for User {
|
|||||||
.next()
|
.next()
|
||||||
.map(|x| x.to_string());
|
.map(|x| x.to_string());
|
||||||
|
|
||||||
|
let header_oauth_token = request
|
||||||
|
.headers()
|
||||||
|
.get("x-oauth2-token")
|
||||||
|
.next()
|
||||||
|
.map(|x| x.to_string());
|
||||||
|
|
||||||
if let Some(bot_token) = header_bot_token {
|
if let Some(bot_token) = header_bot_token {
|
||||||
if let Ok(bot) = db.fetch_bot_by_token(&bot_token).await {
|
if let Ok(bot) = db.fetch_bot_by_token(&bot_token).await {
|
||||||
if let Ok(user) = db.fetch_user(&bot.id).await {
|
if let Ok(user) = db.fetch_user(&bot.id).await {
|
||||||
return Some(user);
|
return Some(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if let Some(header_oauth_token) = header_oauth_token {
|
||||||
|
let config = config().await;
|
||||||
|
|
||||||
|
let claims = oauth2::decode_token(
|
||||||
|
&config.api.security.token_secret,
|
||||||
|
&header_oauth_token,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
// access the scope required for the route stored in the request local cache set via `OAuth2Scoped`
|
||||||
|
let required_scope: v0::OAuth2Scope = request.local_cache(|| None::<crate::OAuth2Scope>)
|
||||||
|
.as_ref()
|
||||||
|
.copied()?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
if claims.scopes.contains(&v0::OAuth2Scope::Full) || claims.scopes.contains(&required_scope) {
|
||||||
|
if let Ok(user) = db.fetch_user(&claims.sub).await {
|
||||||
|
return Some(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if let Outcome::Success(session) = request.guard::<Session>().await {
|
} else if let Outcome::Success(session) = request.guard::<Session>().await {
|
||||||
if let Ok(user) = db.fetch_user(&session.user_id).await {
|
if let Ok(user) = db.fetch_user(&session.user_id).await {
|
||||||
return Some(user);
|
return Some(user);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ impl From<crate::Bot> for Bot {
|
|||||||
interactions_url: value.interactions_url,
|
interactions_url: value.interactions_url,
|
||||||
terms_of_service_url: value.terms_of_service_url,
|
terms_of_service_url: value.terms_of_service_url,
|
||||||
privacy_policy_url: value.privacy_policy_url,
|
privacy_policy_url: value.privacy_policy_url,
|
||||||
|
oauth2: value.oauth2.map(|oauth2| oauth2.into()),
|
||||||
flags: value.flags.unwrap_or_default() as u32,
|
flags: value.flags.unwrap_or_default() as u32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,8 @@ impl From<FieldsBot> for crate::FieldsBot {
|
|||||||
match value {
|
match value {
|
||||||
FieldsBot::InteractionsURL => crate::FieldsBot::InteractionsURL,
|
FieldsBot::InteractionsURL => crate::FieldsBot::InteractionsURL,
|
||||||
FieldsBot::Token => crate::FieldsBot::Token,
|
FieldsBot::Token => crate::FieldsBot::Token,
|
||||||
|
FieldsBot::Oauth2 => crate::FieldsBot::Oauth2,
|
||||||
|
FieldsBot::Oauth2Secret => crate::FieldsBot::Oauth2Secret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +55,78 @@ impl From<crate::FieldsBot> for FieldsBot {
|
|||||||
match value {
|
match value {
|
||||||
crate::FieldsBot::InteractionsURL => FieldsBot::InteractionsURL,
|
crate::FieldsBot::InteractionsURL => FieldsBot::InteractionsURL,
|
||||||
crate::FieldsBot::Token => FieldsBot::Token,
|
crate::FieldsBot::Token => FieldsBot::Token,
|
||||||
|
crate::FieldsBot::Oauth2 => FieldsBot::Oauth2,
|
||||||
|
crate::FieldsBot::Oauth2Secret => FieldsBot::Oauth2Secret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BotOauth2> for crate::BotOauth2 {
|
||||||
|
fn from(value: BotOauth2) -> Self {
|
||||||
|
crate::BotOauth2 {
|
||||||
|
public: value.public,
|
||||||
|
secret: value.secret,
|
||||||
|
redirects: value.redirects,
|
||||||
|
allowed_scopes: value.allowed_scopes
|
||||||
|
.into_iter()
|
||||||
|
.map(|(scope, value)| (scope.into(), value.into()))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::BotOauth2> for BotOauth2 {
|
||||||
|
fn from(value: crate::BotOauth2) -> Self {
|
||||||
|
BotOauth2 {
|
||||||
|
public: value.public,
|
||||||
|
secret: value.secret,
|
||||||
|
redirects: value.redirects,
|
||||||
|
allowed_scopes: value.allowed_scopes
|
||||||
|
.into_iter()
|
||||||
|
.map(|(scope, value)| (scope.into(), value.into()))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::OAuth2Scope> for OAuth2Scope {
|
||||||
|
fn from(value: crate::OAuth2Scope) -> Self {
|
||||||
|
match value {
|
||||||
|
crate::OAuth2Scope::ReadIdentify => OAuth2Scope::ReadIdentify,
|
||||||
|
crate::OAuth2Scope::ReadServers => OAuth2Scope::ReadServers,
|
||||||
|
crate::OAuth2Scope::WriteFiles => OAuth2Scope::WriteFiles,
|
||||||
|
crate::OAuth2Scope::Events => OAuth2Scope::Events,
|
||||||
|
crate::OAuth2Scope::Full => OAuth2Scope::Full,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OAuth2Scope> for crate::OAuth2Scope {
|
||||||
|
fn from(value: OAuth2Scope) -> Self {
|
||||||
|
match value {
|
||||||
|
OAuth2Scope::ReadIdentify => crate::OAuth2Scope::ReadIdentify,
|
||||||
|
OAuth2Scope::ReadServers => crate::OAuth2Scope::ReadServers,
|
||||||
|
OAuth2Scope::WriteFiles => crate::OAuth2Scope::WriteFiles,
|
||||||
|
OAuth2Scope::Events => crate::OAuth2Scope::Events,
|
||||||
|
OAuth2Scope::Full => crate::OAuth2Scope::Full,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::OAuth2ScopeReasoning> for OAuth2ScopeReasoning {
|
||||||
|
fn from(value: crate::OAuth2ScopeReasoning) -> Self {
|
||||||
|
OAuth2ScopeReasoning {
|
||||||
|
allow: value.allow,
|
||||||
|
deny: value.deny,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OAuth2ScopeReasoning> for crate::OAuth2ScopeReasoning {
|
||||||
|
fn from(value: OAuth2ScopeReasoning) -> Self {
|
||||||
|
crate::OAuth2ScopeReasoning {
|
||||||
|
allow: value.allow,
|
||||||
|
deny: value.deny,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -707,6 +782,7 @@ impl From<crate::FieldsMember> for FieldsMember {
|
|||||||
crate::FieldsMember::Nickname => FieldsMember::Nickname,
|
crate::FieldsMember::Nickname => FieldsMember::Nickname,
|
||||||
crate::FieldsMember::Roles => FieldsMember::Roles,
|
crate::FieldsMember::Roles => FieldsMember::Roles,
|
||||||
crate::FieldsMember::Timeout => FieldsMember::Timeout,
|
crate::FieldsMember::Timeout => FieldsMember::Timeout,
|
||||||
|
crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -718,6 +794,7 @@ impl From<FieldsMember> for crate::FieldsMember {
|
|||||||
FieldsMember::Nickname => crate::FieldsMember::Nickname,
|
FieldsMember::Nickname => crate::FieldsMember::Nickname,
|
||||||
FieldsMember::Roles => crate::FieldsMember::Roles,
|
FieldsMember::Roles => crate::FieldsMember::Roles,
|
||||||
FieldsMember::Timeout => crate::FieldsMember::Timeout,
|
FieldsMember::Timeout => crate::FieldsMember::Timeout,
|
||||||
|
FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1385,3 +1462,43 @@ impl From<FieldsMessage> for crate::FieldsMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AuthorizedBotId> for crate::AuthorizedBotId {
|
||||||
|
fn from(value: AuthorizedBotId) -> Self {
|
||||||
|
crate::AuthorizedBotId {
|
||||||
|
user: value.user,
|
||||||
|
bot: value.bot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::AuthorizedBotId> for AuthorizedBotId {
|
||||||
|
fn from(value: crate::AuthorizedBotId) -> Self {
|
||||||
|
AuthorizedBotId {
|
||||||
|
user: value.user,
|
||||||
|
bot: value.bot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AuthorizedBot> for crate::AuthorizedBot {
|
||||||
|
fn from(value: AuthorizedBot) -> Self {
|
||||||
|
crate::AuthorizedBot {
|
||||||
|
id: value.id.into(),
|
||||||
|
created_at: value.created_at,
|
||||||
|
deauthorized_at: value.deauthorized_at,
|
||||||
|
scope: value.scope.into_iter().map(|scope| scope.into()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::AuthorizedBot> for AuthorizedBot {
|
||||||
|
fn from(value: crate::AuthorizedBot) -> Self {
|
||||||
|
AuthorizedBot {
|
||||||
|
id: value.id.into(),
|
||||||
|
created_at: value.created_at,
|
||||||
|
deauthorized_at: value.deauthorized_at,
|
||||||
|
scope: value.scope.into_iter().map(|scope| scope.into()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ pub mod idempotency;
|
|||||||
pub mod permissions;
|
pub mod permissions;
|
||||||
pub mod reference;
|
pub mod reference;
|
||||||
pub mod test_fixtures;
|
pub mod test_fixtures;
|
||||||
|
pub mod oauth2;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||||
|
use revolt_result::{Error, Result};
|
||||||
|
|
||||||
|
use super::{OAuth2Scoped, scopes::OAuth2Scope};
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<S: Send + Sync, Scope: OAuth2Scope> FromRequestParts<S> for OAuth2Scoped<Scope> {
|
||||||
|
type Rejection = Error;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self> {
|
||||||
|
parts.extensions.insert(Scope::SCOPE);
|
||||||
|
|
||||||
|
Ok(OAuth2Scoped { _scope: PhantomData })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use chrono::{TimeDelta, Utc};
|
||||||
|
use jsonwebtoken::{decode, encode, errors::Error, DecodingKey, EncodingKey, Header, Validation};
|
||||||
|
use redis_kiss::AsyncCommands;
|
||||||
|
use revolt_models::v0;
|
||||||
|
use revolt_result::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod scopes;
|
||||||
|
pub use scopes::OAuth2Scoped;
|
||||||
|
|
||||||
|
#[cfg(feature = "rocket")]
|
||||||
|
pub mod rocket;
|
||||||
|
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
pub mod axum;
|
||||||
|
|
||||||
|
pub use jsonwebtoken::errors::{Error as JWTError, ErrorKind as JWTErrorKind};
|
||||||
|
use ulid::Ulid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum TokenType {
|
||||||
|
Auth,
|
||||||
|
Access,
|
||||||
|
Refresh,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenType {
|
||||||
|
pub fn lifetime(self) -> TimeDelta {
|
||||||
|
match self {
|
||||||
|
TokenType::Access => TimeDelta::weeks(1),
|
||||||
|
TokenType::Auth => TimeDelta::minutes(5),
|
||||||
|
TokenType::Refresh => TimeDelta::weeks(4),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Claims {
|
||||||
|
pub jti: String,
|
||||||
|
pub sub: String,
|
||||||
|
pub exp: i64,
|
||||||
|
|
||||||
|
pub r#type: TokenType,
|
||||||
|
pub client_id: String,
|
||||||
|
pub scopes: Vec<v0::OAuth2Scope>,
|
||||||
|
pub redirect_uri: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub code_challange_method: Option<v0::OAuth2CodeChallangeMethod>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn encode_token(
|
||||||
|
token_secret: &str,
|
||||||
|
token_type: TokenType,
|
||||||
|
user_id: String,
|
||||||
|
client_id: String,
|
||||||
|
redirect_uri: String,
|
||||||
|
scopes: Vec<v0::OAuth2Scope>,
|
||||||
|
code_challange_method: Option<v0::OAuth2CodeChallangeMethod>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
let exp = now.checked_add_signed(token_type.lifetime()).unwrap();
|
||||||
|
|
||||||
|
println!("{now:?} {exp:}");
|
||||||
|
|
||||||
|
let claims = Claims {
|
||||||
|
jti: Ulid::new().to_string(),
|
||||||
|
sub: user_id,
|
||||||
|
exp: exp.timestamp(),
|
||||||
|
|
||||||
|
r#type: token_type,
|
||||||
|
client_id,
|
||||||
|
scopes,
|
||||||
|
redirect_uri,
|
||||||
|
code_challange_method,
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoding_key = EncodingKey::from_secret(token_secret.as_bytes());
|
||||||
|
|
||||||
|
encode(&Header::default(), &claims, &encoding_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode_token(token_secret: &str, code: &str) -> Result<Claims, Error> {
|
||||||
|
let decoding_key = DecodingKey::from_secret(token_secret.as_bytes());
|
||||||
|
|
||||||
|
let data = decode(
|
||||||
|
code,
|
||||||
|
&decoding_key,
|
||||||
|
&Validation::new(jsonwebtoken::Algorithm::HS256),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(data.claims)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_code_challange(token: &str, code_challenge: &str) -> Result<()> {
|
||||||
|
let mut conn = redis_kiss::get_connection()
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_error!(InternalError))?;
|
||||||
|
|
||||||
|
conn.pset_ex::<_, _, ()>(
|
||||||
|
format!("oauth2:{token}:code_challenge"),
|
||||||
|
code_challenge,
|
||||||
|
TokenType::Access.lifetime().num_milliseconds() as usize,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_error!(InternalError))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_code_challange(token: &str) -> Result<Option<String>> {
|
||||||
|
let mut conn = redis_kiss::get_connection()
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_error!(InternalError))?;
|
||||||
|
|
||||||
|
conn.get(format!("oauth2:{token}:code_challenge"))
|
||||||
|
.await
|
||||||
|
.map_err(|_| create_error!(InternalError))
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
use std::{convert::Infallible, marker::PhantomData};
|
||||||
|
|
||||||
|
use revolt_okapi::openapi3::{OAuthFlows, SecurityScheme, SecuritySchemeData};
|
||||||
|
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
|
||||||
|
use rocket::{request::{self, FromRequest}, Request};
|
||||||
|
|
||||||
|
use super::{OAuth2Scoped, scopes::OAuth2Scope};
|
||||||
|
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r, Scope: OAuth2Scope> FromRequest<'r> for OAuth2Scoped<Scope> {
|
||||||
|
type Error = Infallible;
|
||||||
|
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||||
|
request.local_cache(|| Some(Scope::SCOPE));
|
||||||
|
|
||||||
|
request::Outcome::Success(OAuth2Scoped { _scope: PhantomData })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r, Scope: OAuth2Scope> OpenApiFromRequest<'r> for OAuth2Scoped<Scope> {
|
||||||
|
fn from_request_input(
|
||||||
|
_gen: &mut revolt_rocket_okapi::r#gen::OpenApiGenerator,
|
||||||
|
_name: String,
|
||||||
|
_required: bool,
|
||||||
|
) -> revolt_rocket_okapi::Result<revolt_rocket_okapi::request::RequestHeaderInput> {
|
||||||
|
Ok(RequestHeaderInput::Security(
|
||||||
|
"OAuth2".to_owned(),
|
||||||
|
SecurityScheme {
|
||||||
|
description: None,
|
||||||
|
extensions: Default::default(),
|
||||||
|
data: SecuritySchemeData::OAuth2 {
|
||||||
|
flows: OAuthFlows::AuthorizationCode {
|
||||||
|
authorization_url: "todo".to_string(),
|
||||||
|
token_url: "todo".to_string(),
|
||||||
|
refresh_url: Some("todo".to_string()),
|
||||||
|
scopes: revolt_okapi::map! {
|
||||||
|
"read:identify".to_string() => "".to_string(),
|
||||||
|
"read:servers".to_string() => "".to_string(),
|
||||||
|
"write:files".to_string() => "".to_string(),
|
||||||
|
"events".to_string() => "".to_string(),
|
||||||
|
"full".to_string() => "".to_string(),
|
||||||
|
},
|
||||||
|
extensions: Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
revolt_okapi::map! {
|
||||||
|
"OAuth2".to_owned() => vec![Scope::MODEL.to_string()]
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
pub struct OAuth2Scoped<Scope> {
|
||||||
|
pub(crate) _scope: PhantomData<Scope>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OAuth2Scope {
|
||||||
|
const SCOPE: crate::OAuth2Scope;
|
||||||
|
const MODEL: revolt_models::v0::OAuth2Scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! define_oauth2_scope {
|
||||||
|
($struct_name:ident) => {
|
||||||
|
pub struct $struct_name;
|
||||||
|
|
||||||
|
impl OAuth2Scope for $struct_name {
|
||||||
|
const SCOPE: crate::OAuth2Scope = crate::OAuth2Scope::$struct_name;
|
||||||
|
const MODEL: revolt_models::v0::OAuth2Scope = revolt_models::v0::OAuth2Scope::$struct_name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This must match the OAuth2Scope enum
|
||||||
|
// TODO: automatically sync this
|
||||||
|
define_oauth2_scope!(ReadIdentify);
|
||||||
|
define_oauth2_scope!(ReadServers);
|
||||||
|
define_oauth2_scope!(WriteFiles);
|
||||||
|
define_oauth2_scope!(Events);
|
||||||
|
define_oauth2_scope!(Full);
|
||||||
@@ -14,41 +14,40 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Reference to some object in the database
|
/// Reference to some object in the database
|
||||||
#[derive(Serialize, Deserialize)]
|
pub struct Reference<'a> {
|
||||||
pub struct Reference {
|
|
||||||
/// Id of object
|
/// Id of object
|
||||||
pub id: String,
|
pub id: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reference {
|
impl<'a> Reference<'a> {
|
||||||
/// Create a Ref from an unchecked string
|
/// Create a Ref from an unchecked string
|
||||||
pub fn from_unchecked(id: String) -> Reference {
|
pub fn from_unchecked(id: &'a str) -> Reference<'a> {
|
||||||
Reference { id }
|
Reference { id }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch ban from Ref
|
/// Fetch ban from Ref
|
||||||
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
|
pub async fn as_ban(&self, db: &Database, server: &str) -> Result<ServerBan> {
|
||||||
db.fetch_ban(server, &self.id).await
|
db.fetch_ban(server, self.id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch bot from Ref
|
/// Fetch bot from Ref
|
||||||
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
|
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
|
||||||
db.fetch_bot(&self.id).await
|
db.fetch_bot(self.id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch emoji from Ref
|
/// Fetch emoji from Ref
|
||||||
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
|
pub async fn as_emoji(&self, db: &Database) -> Result<Emoji> {
|
||||||
db.fetch_emoji(&self.id).await
|
db.fetch_emoji(self.id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch channel from Ref
|
/// Fetch channel from Ref
|
||||||
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
|
pub async fn as_channel(&self, db: &Database) -> Result<Channel> {
|
||||||
db.fetch_channel(&self.id).await
|
db.fetch_channel(self.id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch invite from Ref or create invite to server if discoverable
|
/// Fetch invite from Ref or create invite to server if discoverable
|
||||||
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
pub async fn as_invite(&self, db: &Database) -> Result<Invite> {
|
||||||
if ulid::Ulid::from_str(&self.id).is_ok() {
|
if ulid::Ulid::from_str(self.id).is_ok() {
|
||||||
let server = self.as_server(db).await?;
|
let server = self.as_server(db).await?;
|
||||||
if !server.discoverable {
|
if !server.discoverable {
|
||||||
return Err(create_error!(NotFound));
|
return Err(create_error!(NotFound));
|
||||||
@@ -65,18 +64,18 @@ impl Reference {
|
|||||||
.ok_or(create_error!(NotFound))?,
|
.ok_or(create_error!(NotFound))?,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
db.fetch_invite(&self.id).await
|
db.fetch_invite(self.id).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch message from Ref
|
/// Fetch message from Ref
|
||||||
pub async fn as_message(&self, db: &Database) -> Result<Message> {
|
pub async fn as_message(&self, db: &Database) -> Result<Message> {
|
||||||
db.fetch_message(&self.id).await
|
db.fetch_message(self.id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch message from Ref and validate channel
|
/// Fetch message from Ref and validate channel
|
||||||
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
|
pub async fn as_message_in_channel(&self, db: &Database, channel: &str) -> Result<Message> {
|
||||||
let msg = db.fetch_message(&self.id).await?;
|
let msg = db.fetch_message(self.id).await?;
|
||||||
if msg.channel != channel {
|
if msg.channel != channel {
|
||||||
return Err(create_error!(NotFound));
|
return Err(create_error!(NotFound));
|
||||||
}
|
}
|
||||||
@@ -86,36 +85,36 @@ impl Reference {
|
|||||||
|
|
||||||
/// Fetch member from Ref
|
/// Fetch member from Ref
|
||||||
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
|
pub async fn as_member(&self, db: &Database, server: &str) -> Result<Member> {
|
||||||
db.fetch_member(server, &self.id).await
|
db.fetch_member(server, self.id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch server from Ref
|
/// Fetch server from Ref
|
||||||
pub async fn as_server(&self, db: &Database) -> Result<Server> {
|
pub async fn as_server(&self, db: &Database) -> Result<Server> {
|
||||||
db.fetch_server(&self.id).await
|
db.fetch_server(self.id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch user from Ref
|
/// Fetch user from Ref
|
||||||
pub async fn as_user(&self, db: &Database) -> Result<User> {
|
pub async fn as_user(&self, db: &Database) -> Result<User> {
|
||||||
db.fetch_user(&self.id).await
|
db.fetch_user(self.id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch webhook from Ref
|
/// Fetch webhook from Ref
|
||||||
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
|
pub async fn as_webhook(&self, db: &Database) -> Result<Webhook> {
|
||||||
db.fetch_webhook(&self.id).await
|
db.fetch_webhook(self.id).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rocket-impl")]
|
#[cfg(feature = "rocket-impl")]
|
||||||
impl<'r> FromParam<'r> for Reference {
|
impl<'r> FromParam<'r> for Reference<'r> {
|
||||||
type Error = &'r str;
|
type Error = &'r str;
|
||||||
|
|
||||||
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||||
Ok(Reference::from_unchecked(param.into()))
|
Ok(Reference::from_unchecked(param))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "rocket-impl")]
|
#[cfg(feature = "rocket-impl")]
|
||||||
impl JsonSchema for Reference {
|
impl<'a> JsonSchema for Reference<'a> {
|
||||||
fn schema_name() -> String {
|
fn schema_name() -> String {
|
||||||
"Id".to_string()
|
"Id".to_string()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ pub async fn load_fixture(db: &Database, input: &str) -> HashMap<String, String>
|
|||||||
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
|
LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(),
|
||||||
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
|
LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(),
|
||||||
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
|
LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(),
|
||||||
LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(),
|
LoadedFixture::ServerMember(member) => {
|
||||||
|
db.insert_or_merge_member(&member).await.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-files"
|
name = "revolt-files"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -20,10 +20,10 @@ typenum = "1.17.0"
|
|||||||
aws-config = "1.5.5"
|
aws-config = "1.5.5"
|
||||||
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
|
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
|
||||||
|
|
||||||
revolt-config = { version = "0.8.8", path = "../config", features = [
|
revolt-config = { version = "0.8.9", path = "../config", features = [
|
||||||
"report-macros",
|
"report-macros",
|
||||||
] }
|
] }
|
||||||
revolt-result = { version = "0.8.8", path = "../result" }
|
revolt-result = { version = "0.8.9", path = "../result" }
|
||||||
|
|
||||||
# image processing
|
# image processing
|
||||||
jxl-oxide = "0.8.1"
|
jxl-oxide = "0.8.1"
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
|
|||||||
.decrypt_in_place(nonce, b"", &mut buf)
|
.decrypt_in_place(nonce, b"", &mut buf)
|
||||||
.map_err(|_| create_error!(InternalError))?;
|
.map_err(|_| create_error!(InternalError))?;
|
||||||
|
|
||||||
|
// Remove the authentication tag bytes that were added during encryption
|
||||||
|
buf.truncate(buf.len() - AUTHENTICATION_TAG_SIZE_BYTES);
|
||||||
|
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-models"
|
name = "revolt-models"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -20,8 +20,8 @@ default = ["serde", "partials", "rocket"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-config = { version = "0.8.8", path = "../config" }
|
revolt-config = { version = "0.8.9", path = "../config" }
|
||||||
revolt-permissions = { version = "0.8.8", path = "../permissions" }
|
revolt-permissions = { version = "0.8.9", path = "../permissions" }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
|
|
||||||
|
use crate::v0::{PublicBot, OAuth2Scope};
|
||||||
|
|
||||||
|
auto_derived!(
|
||||||
|
/// Unique id of the user and bot
|
||||||
|
pub struct AuthorizedBotId {
|
||||||
|
/// User id
|
||||||
|
pub user: String,
|
||||||
|
|
||||||
|
/// Bot Id
|
||||||
|
pub bot: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthorizedBot {
|
||||||
|
/// Unique Id
|
||||||
|
#[serde(rename = "_id")]
|
||||||
|
pub id: AuthorizedBotId,
|
||||||
|
|
||||||
|
/// When the authorized oauth2 bot connection was created at
|
||||||
|
pub created_at: Timestamp,
|
||||||
|
|
||||||
|
/// If and when the authorized oauth2 bot connection was revoked at
|
||||||
|
pub deauthorized_at: Option<Timestamp>,
|
||||||
|
|
||||||
|
/// Scopes the bot has access to
|
||||||
|
pub scope: Vec<OAuth2Scope>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AuthorizedBotsResponse {
|
||||||
|
pub public_bot: PublicBot,
|
||||||
|
pub authorized_bot: AuthorizedBot,
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
use super::User;
|
use super::{User, OAuth2Scope, OAuth2ScopeReasoning};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[cfg(feature = "validator")]
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
auto_derived!(
|
auto_derived!(
|
||||||
/// Bot
|
/// Bot
|
||||||
@@ -48,6 +52,13 @@ auto_derived!(
|
|||||||
)]
|
)]
|
||||||
pub privacy_policy_url: String,
|
pub privacy_policy_url: String,
|
||||||
|
|
||||||
|
/// Oauth2 bot settings
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
serde(skip_serializing_if = "Option::is_none", default)
|
||||||
|
)]
|
||||||
|
pub oauth2: Option<BotOauth2>,
|
||||||
|
|
||||||
/// Enum of bot flags
|
/// Enum of bot flags
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "serde",
|
feature = "serde",
|
||||||
@@ -60,6 +71,8 @@ auto_derived!(
|
|||||||
pub enum FieldsBot {
|
pub enum FieldsBot {
|
||||||
Token,
|
Token,
|
||||||
InteractionsURL,
|
InteractionsURL,
|
||||||
|
Oauth2,
|
||||||
|
Oauth2Secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flags that may be attributed to a bot
|
/// Flags that may be attributed to a bot
|
||||||
@@ -101,7 +114,7 @@ auto_derived!(
|
|||||||
|
|
||||||
/// Bot Details
|
/// Bot Details
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||||
pub struct DataCreateBot {
|
pub struct DataCreateBot {
|
||||||
/// Bot username
|
/// Bot username
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
@@ -113,7 +126,7 @@ auto_derived!(
|
|||||||
|
|
||||||
/// New Bot Details
|
/// New Bot Details
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[cfg_attr(feature = "validator", derive(validator::Validate))]
|
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||||
pub struct DataEditBot {
|
pub struct DataEditBot {
|
||||||
/// Bot username
|
/// Bot username
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
@@ -131,9 +144,22 @@ auto_derived!(
|
|||||||
/// Interactions URL
|
/// Interactions URL
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
|
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 2048)))]
|
||||||
pub interactions_url: Option<String>,
|
pub interactions_url: Option<String>,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "validator", validate)]
|
||||||
|
pub oauth2: Option<DataEditBotOauth2>,
|
||||||
|
|
||||||
/// Fields to remove from bot object
|
/// Fields to remove from bot object
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub remove: Option<Vec<FieldsBot>>,
|
pub remove: Vec<FieldsBot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||||
|
pub struct DataEditBotOauth2 {
|
||||||
|
pub public: Option<bool>,
|
||||||
|
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 10)))]
|
||||||
|
pub redirects: Option<Vec<String>>,
|
||||||
|
pub allowed_scopes: Option<HashMap<OAuth2Scope, OAuth2ScopeReasoning>>
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where we are inviting a bot to
|
/// Where we are inviting a bot to
|
||||||
@@ -170,3 +196,22 @@ auto_derived!(
|
|||||||
pub user: User,
|
pub user: User,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
auto_derived_partial!(
|
||||||
|
/// Bot Oauth2 information
|
||||||
|
pub struct BotOauth2 {
|
||||||
|
/// Whether the oauth client is public and should not receive a secret key
|
||||||
|
#[serde(default)]
|
||||||
|
pub public: bool,
|
||||||
|
/// Secret key used for authorisation, not provided if the client is public
|
||||||
|
#[serde(default)]
|
||||||
|
pub secret: Option<String>,
|
||||||
|
/// Allowed redirects for the authorisation
|
||||||
|
#[serde(default)]
|
||||||
|
pub redirects: Vec<String>,
|
||||||
|
/// Mapping of allowed scopes and the reasonings
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>,
|
||||||
|
},
|
||||||
|
"PartialBotOauth2"
|
||||||
|
);
|
||||||
@@ -207,7 +207,7 @@ auto_derived!(
|
|||||||
|
|
||||||
/// Fields to remove from channel
|
/// Fields to remove from channel
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub remove: Option<Vec<FieldsChannel>>,
|
pub remove: Vec<FieldsChannel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create new group
|
/// Create new group
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ auto_derived!(
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
#[cfg_attr(feature = "validator", derive(Validate))]
|
#[cfg_attr(feature = "validator", derive(Validate))]
|
||||||
pub struct SendableEmbed {
|
pub struct SendableEmbed {
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
|
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
|
||||||
pub icon_url: Option<String>,
|
pub icon_url: Option<String>,
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
|
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod authorized_bots;
|
||||||
mod bots;
|
mod bots;
|
||||||
mod channel_invites;
|
mod channel_invites;
|
||||||
mod channel_unreads;
|
mod channel_unreads;
|
||||||
@@ -7,6 +8,7 @@ mod embeds;
|
|||||||
mod emojis;
|
mod emojis;
|
||||||
mod files;
|
mod files;
|
||||||
mod messages;
|
mod messages;
|
||||||
|
mod oauth2;
|
||||||
mod policy_changes;
|
mod policy_changes;
|
||||||
mod safety_reports;
|
mod safety_reports;
|
||||||
mod server_bans;
|
mod server_bans;
|
||||||
@@ -15,6 +17,7 @@ mod servers;
|
|||||||
mod user_settings;
|
mod user_settings;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
|
pub use authorized_bots::*;
|
||||||
pub use bots::*;
|
pub use bots::*;
|
||||||
pub use channel_invites::*;
|
pub use channel_invites::*;
|
||||||
pub use channel_unreads::*;
|
pub use channel_unreads::*;
|
||||||
@@ -24,6 +27,7 @@ pub use embeds::*;
|
|||||||
pub use emojis::*;
|
pub use emojis::*;
|
||||||
pub use files::*;
|
pub use files::*;
|
||||||
pub use messages::*;
|
pub use messages::*;
|
||||||
|
pub use oauth2::*;
|
||||||
pub use policy_changes::*;
|
pub use policy_changes::*;
|
||||||
pub use safety_reports::*;
|
pub use safety_reports::*;
|
||||||
pub use server_bans::*;
|
pub use server_bans::*;
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
use crate::v0::{PublicBot, User};
|
||||||
|
use std::{collections::HashMap, fmt::Display};
|
||||||
|
|
||||||
|
auto_derived!(
|
||||||
|
pub struct OAuth2AuthorizeAuthResponse {
|
||||||
|
/// Redirect URI which will contain the access token
|
||||||
|
pub redirect_uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OAuth2ScopeReasoning {
|
||||||
|
pub allow: String,
|
||||||
|
pub deny: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OAuth2AuthorizeInfoResponse {
|
||||||
|
pub bot: PublicBot,
|
||||||
|
pub user: User,
|
||||||
|
pub allowed_scopes: HashMap<OAuth2Scope, OAuth2ScopeReasoning>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy)]
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "lowercase"))]
|
||||||
|
#[cfg_attr(feature = "rocket", derive(rocket::FromFormField))]
|
||||||
|
pub enum OAuth2ResponseType {
|
||||||
|
#[cfg_attr(feature = "rocket", field(value = "code"))]
|
||||||
|
Code,
|
||||||
|
#[cfg_attr(feature = "rocket", field(value = "token"))]
|
||||||
|
Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy)]
|
||||||
|
#[cfg_attr(feature = "rocket", derive(rocket::FromFormField))]
|
||||||
|
pub enum OAuth2GrantType {
|
||||||
|
#[cfg_attr(feature = "rocket", field(value = "authorization_code"))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "authorization_code"))]
|
||||||
|
AuthorizationCode,
|
||||||
|
#[cfg_attr(feature = "rocket", field(value = "implicit"))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "implicit"))]
|
||||||
|
Implicit,
|
||||||
|
#[cfg_attr(feature = "rocket", field(value = "refresh_token"))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "refresh_token"))]
|
||||||
|
RefreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy)]
|
||||||
|
#[cfg_attr(feature = "rocket", derive(rocket::FromFormField))]
|
||||||
|
pub enum OAuth2CodeChallangeMethod {
|
||||||
|
#[cfg_attr(feature = "rocket", field(value = "plain"))]
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "plain"))]
|
||||||
|
Plain,
|
||||||
|
S256
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "rocket", derive(rocket::FromForm))]
|
||||||
|
pub struct OAuth2AuthorizationForm {
|
||||||
|
pub client_id: String,
|
||||||
|
pub scope: String,
|
||||||
|
pub redirect_uri: String,
|
||||||
|
pub response_type: OAuth2ResponseType,
|
||||||
|
pub state: Option<String>,
|
||||||
|
pub code_challenge: Option<String>,
|
||||||
|
pub code_challenge_method: Option<OAuth2CodeChallangeMethod>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "rocket", derive(rocket::FromForm))]
|
||||||
|
pub struct OAuth2TokenExchangeForm {
|
||||||
|
pub grant_type: OAuth2GrantType,
|
||||||
|
|
||||||
|
pub client_id: String,
|
||||||
|
pub client_secret: Option<String>,
|
||||||
|
|
||||||
|
/// Authorization code
|
||||||
|
pub code: Option<String>,
|
||||||
|
// Refresh token to generate new access token
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
|
||||||
|
pub code_verifier: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OAuth2TokenExchangeResponse {
|
||||||
|
pub access_token: String,
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
pub token_type: String,
|
||||||
|
pub scope: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "snake_case"))]
|
||||||
|
pub enum OAuth2Scope {
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "read:identify"))]
|
||||||
|
ReadIdentify,
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "read:servers"))]
|
||||||
|
ReadServers,
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "write:files"))]
|
||||||
|
WriteFiles,
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "events"))]
|
||||||
|
Events,
|
||||||
|
#[cfg_attr(feature = "serde", serde(rename = "full"))]
|
||||||
|
Full,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
impl Display for OAuth2Scope {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
OAuth2Scope::ReadIdentify => "read:identify",
|
||||||
|
OAuth2Scope::ReadServers => "read:servers",
|
||||||
|
OAuth2Scope::WriteFiles => "write:files",
|
||||||
|
OAuth2Scope::Events => "events",
|
||||||
|
OAuth2Scope::Full => "full",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OAuth2Scope {
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn from_str(str: &str) -> Option<OAuth2Scope> {
|
||||||
|
match str {
|
||||||
|
"read:identify" => Some(OAuth2Scope::ReadIdentify),
|
||||||
|
"read:servers" => Some(OAuth2Scope::ReadServers),
|
||||||
|
"events" => Some(OAuth2Scope::Events),
|
||||||
|
"full" => Some(OAuth2Scope::Full),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scopes_from_str(string: &str) -> Option<Vec<OAuth2Scope>> {
|
||||||
|
let mut scopes = Vec::new();
|
||||||
|
|
||||||
|
for scope in string.split(' ') {
|
||||||
|
if let Some(scope) = OAuth2Scope::from_str(scope) {
|
||||||
|
scopes.push(scope);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(scopes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ auto_derived!(
|
|||||||
Avatar,
|
Avatar,
|
||||||
Roles,
|
Roles,
|
||||||
Timeout,
|
Timeout,
|
||||||
|
JoinedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Member removal intention
|
/// Member removal intention
|
||||||
@@ -124,7 +125,7 @@ auto_derived!(
|
|||||||
/// Timestamp this member is timed out until
|
/// Timestamp this member is timed out until
|
||||||
pub timeout: Option<Timestamp>,
|
pub timeout: Option<Timestamp>,
|
||||||
/// Fields to remove from channel object
|
/// Fields to remove from channel object
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub remove: Option<Vec<FieldsMember>>,
|
pub remove: Vec<FieldsMember>,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ auto_derived!(
|
|||||||
/// Ranking position
|
/// Ranking position
|
||||||
///
|
///
|
||||||
/// Smaller values take priority.
|
/// Smaller values take priority.
|
||||||
|
///
|
||||||
|
/// **Removed** - no effect, use the edit server role positions route
|
||||||
pub rank: Option<i64>,
|
pub rank: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,8 +249,8 @@ auto_derived!(
|
|||||||
pub analytics: Option<bool>,
|
pub analytics: Option<bool>,
|
||||||
|
|
||||||
/// Fields to remove from server object
|
/// Fields to remove from server object
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub remove: Option<Vec<FieldsServer>>,
|
pub remove: Vec<FieldsServer>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// New role information
|
/// New role information
|
||||||
@@ -270,8 +272,8 @@ auto_derived!(
|
|||||||
/// **Removed** - no effect, use the edit server role positions route
|
/// **Removed** - no effect, use the edit server role positions route
|
||||||
pub rank: Option<i64>,
|
pub rank: Option<i64>,
|
||||||
/// Fields to remove from role object
|
/// Fields to remove from role object
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub remove: Option<Vec<FieldsRole>>,
|
pub remove: Vec<FieldsRole>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// New role permissions
|
/// New role permissions
|
||||||
|
|||||||
@@ -245,8 +245,8 @@ auto_derived!(
|
|||||||
pub flags: Option<i32>,
|
pub flags: Option<i32>,
|
||||||
|
|
||||||
/// Fields to remove from user object
|
/// Fields to remove from user object
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1)))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub remove: Option<Vec<FieldsUser>>,
|
pub remove: Vec<FieldsUser>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User flag reponse
|
/// User flag reponse
|
||||||
@@ -255,12 +255,14 @@ auto_derived!(
|
|||||||
pub flags: i32,
|
pub flags: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mutual friends and servers response
|
/// Mutual friends, servers, groups and DMs response
|
||||||
pub struct MutualResponse {
|
pub struct MutualResponse {
|
||||||
/// Array of mutual user IDs that both users are friends with
|
/// Array of mutual user IDs that both users are friends with
|
||||||
pub users: Vec<String>,
|
pub users: Vec<String>,
|
||||||
/// Array of mutual server IDs that both users are in
|
/// Array of mutual server IDs that both users are in
|
||||||
pub servers: Vec<String>,
|
pub servers: Vec<String>,
|
||||||
|
/// Array of mutual group and dm IDs that both users are in
|
||||||
|
pub channels: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bot information for if the user is a bot
|
/// Bot information for if the user is a bot
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-parser"
|
name = "revolt-parser"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "MIT"
|
||||||
|
authors = ["Zomatree <me@zomatree.live>", "Paul Makles <me@insrt.uk>"]
|
||||||
description = "Revolt Backend: Message Parser"
|
description = "Revolt Backend: Message Parser"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -4,15 +4,18 @@ use logos::Logos;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Logos, PartialEq)]
|
#[derive(Debug, Clone, Logos, PartialEq)]
|
||||||
#[logos(skip "\n")]
|
#[logos(skip "\n")]
|
||||||
pub enum MessageToken {
|
#[logos(subpattern id="[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}")]
|
||||||
|
pub enum MessageToken<'a> {
|
||||||
#[token("\\")]
|
#[token("\\")]
|
||||||
Escape,
|
Escape,
|
||||||
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())]
|
#[regex("```[^`\n]*", |_| 3)]
|
||||||
|
#[regex("``", |_| 2)]
|
||||||
|
#[regex("`", |_| 1)]
|
||||||
CodeblockMarker(usize),
|
CodeblockMarker(usize),
|
||||||
#[regex("<@[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
|
#[regex("<@(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1])]
|
||||||
UserMention(String),
|
UserMention(&'a str),
|
||||||
#[regex("<%[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
|
#[regex("<%(?&id)>", |lex| &lex.slice()[2..lex.slice().len() - 1],)]
|
||||||
RoleMention(String),
|
RoleMention(&'a str),
|
||||||
#[token("@everyone")]
|
#[token("@everyone")]
|
||||||
MentionEveryone,
|
MentionEveryone,
|
||||||
#[token("@online")]
|
#[token("@online")]
|
||||||
@@ -27,13 +30,13 @@ pub struct MessageResults {
|
|||||||
pub mentions_online: bool
|
pub mentions_online: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MessageParserIterator<I> {
|
struct MessageParserIterator<'a, I> {
|
||||||
inner: I,
|
inner: I,
|
||||||
temp: VecDeque<MessageToken>
|
temp: VecDeque<MessageToken<'a>>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> {
|
impl<'a, I: Iterator<Item = MessageToken<'a>>> Iterator for MessageParserIterator<'a, I> {
|
||||||
type Item = MessageToken;
|
type Item = MessageToken<'a>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if !self.temp.is_empty() {
|
if !self.temp.is_empty() {
|
||||||
@@ -80,8 +83,8 @@ pub fn parse_message(text: &str) -> MessageResults {
|
|||||||
match token {
|
match token {
|
||||||
MessageToken::Escape => {}
|
MessageToken::Escape => {}
|
||||||
MessageToken::CodeblockMarker(_) => {},
|
MessageToken::CodeblockMarker(_) => {},
|
||||||
MessageToken::UserMention(id) => { results.user_mentions.insert(id); },
|
MessageToken::UserMention(id) => { results.user_mentions.insert(id.to_string()); },
|
||||||
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); },
|
MessageToken::RoleMention(id) => { results.role_mentions.insert(id.to_string()); },
|
||||||
MessageToken::MentionEveryone => results.mentions_everyone = true,
|
MessageToken::MentionEveryone => results.mentions_everyone = true,
|
||||||
MessageToken::MentionOnline => results.mentions_online = true,
|
MessageToken::MentionOnline => results.mentions_online = true,
|
||||||
};
|
};
|
||||||
@@ -106,7 +109,7 @@ mod tests {
|
|||||||
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
|
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(output.len(), 1);
|
assert_eq!(output.len(), 1);
|
||||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -114,7 +117,7 @@ mod tests {
|
|||||||
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
|
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(output.len(), 1);
|
assert_eq!(output.len(), 1);
|
||||||
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -138,8 +141,8 @@ mod tests {
|
|||||||
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>();
|
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(output.len(), 4);
|
assert_eq!(output.len(), 4);
|
||||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
assert_eq!(output[2], MessageToken::MentionEveryone);
|
assert_eq!(output[2], MessageToken::MentionEveryone);
|
||||||
assert_eq!(output[3], MessageToken::MentionOnline);
|
assert_eq!(output[3], MessageToken::MentionOnline);
|
||||||
}
|
}
|
||||||
@@ -149,8 +152,8 @@ mod tests {
|
|||||||
let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
|
let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(output.len(), 4);
|
assert_eq!(output.len(), 4);
|
||||||
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
assert_eq!(output[2], MessageToken::MentionEveryone);
|
assert_eq!(output[2], MessageToken::MentionEveryone);
|
||||||
assert_eq!(output[3], MessageToken::MentionOnline);
|
assert_eq!(output[3], MessageToken::MentionOnline);
|
||||||
}
|
}
|
||||||
@@ -170,15 +173,14 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(output.len(), 5);
|
assert_eq!(output.len(), 5);
|
||||||
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
|
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
|
||||||
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
assert_eq!(output[3], MessageToken::MentionEveryone);
|
assert_eq!(output[3], MessageToken::MentionEveryone);
|
||||||
assert_eq!(output[4], MessageToken::MentionOnline);
|
assert_eq!(output[4], MessageToken::MentionOnline);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_inline_codeblock_no_mentions() {
|
fn test_inline_codeblock_no_mentions() {
|
||||||
|
|
||||||
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>();
|
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>();
|
||||||
|
|
||||||
assert_eq!(output.len(), 2);
|
assert_eq!(output.len(), 2);
|
||||||
@@ -192,12 +194,21 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(output.len(), 5);
|
assert_eq!(output.len(), 5);
|
||||||
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
|
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
|
||||||
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
|
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3"));
|
||||||
assert_eq!(output[3], MessageToken::MentionEveryone);
|
assert_eq!(output[3], MessageToken::MentionEveryone);
|
||||||
assert_eq!(output[4], MessageToken::MentionOnline);
|
assert_eq!(output[4], MessageToken::MentionOnline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_codeblock_with_language_no_mentions() {
|
||||||
|
let output = parse_message_iter("```rust\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online```").collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(output.len(), 2);
|
||||||
|
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
|
||||||
|
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_double_inline_codeblock() {
|
fn test_double_inline_codeblock() {
|
||||||
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();
|
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-permissions"
|
name = "revolt-permissions"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-result = { version = "0.8.8", path = "../result" }
|
revolt-result = { version = "0.8.9", path = "../result" }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
auto_ops = "0.3.0"
|
auto_ops = "0.3.0"
|
||||||
|
|||||||
@@ -8,10 +8,18 @@ pub use server::*;
|
|||||||
pub use user::*;
|
pub use user::*;
|
||||||
|
|
||||||
/// Holds a permission value to manipulate.
|
/// Holds a permission value to manipulate.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct PermissionValue(u64);
|
pub struct PermissionValue(u64);
|
||||||
|
|
||||||
impl PermissionValue {
|
impl PermissionValue {
|
||||||
|
pub fn from_raw(value: u64) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_raw(self) -> u64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a given override to this value
|
/// Apply a given override to this value
|
||||||
pub fn apply(&mut self, v: Override) {
|
pub fn apply(&mut self, v: Override) {
|
||||||
self.allow(v.allow);
|
self.allow(v.allow);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-presence"
|
name = "revolt-presence"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -16,7 +16,7 @@ redis-is-patched = []
|
|||||||
async-std = { version = "1.8.0", features = ["attributes"] }
|
async-std = { version = "1.8.0", features = ["attributes"] }
|
||||||
|
|
||||||
# Config for loading Redis URI
|
# Config for loading Redis URI
|
||||||
revolt-config = { version = "0.8.8", path = "../config" }
|
revolt-config = { version = "0.8.9", path = "../config" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Utility
|
# Utility
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "revolt-ratelimits"
|
||||||
|
version = "0.8.9"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
rocket = ["dep:rocket", "dep:revolt_rocket_okapi", "revolt-database/rocket-impl"]
|
||||||
|
axum = ["dep:axum", "revolt-database/axum-impl"]
|
||||||
|
|
||||||
|
default = ["rocket", "axum"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
revolt-database = { version = "0.8.9", path = "../database"}
|
||||||
|
revolt-result = { version = "0.8.9", path = "../result" }
|
||||||
|
revolt-config = { version = "0.8.9", path = "../config" }
|
||||||
|
|
||||||
|
rocket = { version = "0.5.1", optional = true }
|
||||||
|
revolt_rocket_okapi = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
|
axum = { version = "0.7.5", optional = true, features = ["macros"] }
|
||||||
|
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
authifier = { version = "1.0.15" }
|
||||||
|
dashmap = "5.2.0"
|
||||||
|
async-trait = "0.1.81"
|
||||||
|
log = "0.4"
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use axum::{
|
||||||
|
Json, RequestPartsExt, Router,
|
||||||
|
body::Body,
|
||||||
|
extract::{ConnectInfo, FromRef, FromRequestParts, State},
|
||||||
|
http::{HeaderValue, Request, StatusCode, request::Parts},
|
||||||
|
middleware::Next,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
routing::get,
|
||||||
|
};
|
||||||
|
use revolt_database::{Database, User};
|
||||||
|
use revolt_config::config;
|
||||||
|
|
||||||
|
use crate::ratelimiter::{RatelimitInformation, Ratelimiter, RequestKind};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct AxumRequestKind;
|
||||||
|
|
||||||
|
impl RequestKind for AxumRequestKind {
|
||||||
|
type R<'a> = Parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<AxumRequestKind>;
|
||||||
|
|
||||||
|
fn to_ip(parts: &Parts) -> String {
|
||||||
|
parts
|
||||||
|
.extensions
|
||||||
|
.get::<ConnectInfo<SocketAddr>>()
|
||||||
|
.map(|info| info.ip().to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn to_real_ip(parts: &Parts) -> String {
|
||||||
|
if config().await.api.security.trust_cloudflare {
|
||||||
|
parts
|
||||||
|
.headers
|
||||||
|
.get("CF-Connecting-IP")
|
||||||
|
.map(|x| x.to_str().unwrap().to_string())
|
||||||
|
.unwrap_or_else(|| to_ip(parts))
|
||||||
|
} else {
|
||||||
|
to_ip(parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S: Send + Sync> FromRequestParts<S> for Ratelimiter
|
||||||
|
where
|
||||||
|
Database: FromRef<S>,
|
||||||
|
RatelimitStorage: FromRef<S>,
|
||||||
|
{
|
||||||
|
type Rejection = Json<Ratelimiter>;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
if parts
|
||||||
|
.extensions
|
||||||
|
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
let storage = RatelimitStorage::from_ref(state);
|
||||||
|
|
||||||
|
let identifier = if let Ok(user) = parts.extract_with_state::<User, _>(state).await {
|
||||||
|
user.id
|
||||||
|
} else {
|
||||||
|
to_real_ip(parts).await
|
||||||
|
};
|
||||||
|
|
||||||
|
let (bucket, resource) = storage.resolver.resolve_bucket(parts);
|
||||||
|
let limit = storage.resolver.resolve_bucket_limit(bucket);
|
||||||
|
|
||||||
|
let ratelimiter =
|
||||||
|
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource));
|
||||||
|
|
||||||
|
parts.extensions.insert(ratelimiter.map_err(Json));
|
||||||
|
};
|
||||||
|
|
||||||
|
*parts
|
||||||
|
.extensions
|
||||||
|
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S: Send + Sync> FromRequestParts<S> for RatelimitInformation
|
||||||
|
where
|
||||||
|
Database: FromRef<S>,
|
||||||
|
RatelimitStorage: FromRef<S>,
|
||||||
|
{
|
||||||
|
type Rejection = Json<RatelimitInformation>;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
if parts
|
||||||
|
.extensions
|
||||||
|
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
let ratelimiter = parts.extract_with_state::<Ratelimiter, S>(state).await;
|
||||||
|
|
||||||
|
parts.extensions.insert(ratelimiter);
|
||||||
|
};
|
||||||
|
|
||||||
|
let ratelimiter = *parts
|
||||||
|
.extensions
|
||||||
|
.get::<Result<Ratelimiter, Json<Ratelimiter>>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match ratelimiter {
|
||||||
|
Ok(ratelimter) => Ok(RatelimitInformation::Success(ratelimter)),
|
||||||
|
Err(ratelimiter) => Err(Json(RatelimitInformation::Failure {
|
||||||
|
retry_after: ratelimiter.reset,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn ratelimit_middleware(
|
||||||
|
State(database): State<Database>,
|
||||||
|
State(ratelimit_storage): State<RatelimitStorage>,
|
||||||
|
request: Request<Body>,
|
||||||
|
next: Next,
|
||||||
|
) -> Response {
|
||||||
|
#[derive(axum::extract::FromRef)]
|
||||||
|
struct TempState {
|
||||||
|
database: Database,
|
||||||
|
ratelimit_storage: RatelimitStorage,
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = TempState {
|
||||||
|
database,
|
||||||
|
ratelimit_storage,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut parts, body) = request.into_parts();
|
||||||
|
|
||||||
|
let res = Ratelimiter::from_request_parts(&mut parts, &state).await;
|
||||||
|
|
||||||
|
let (Ok(ratelimiter) | Err(Json(ratelimiter))) = &res;
|
||||||
|
|
||||||
|
let mut response = if res.is_ok() {
|
||||||
|
let request = Request::from_parts(parts, body);
|
||||||
|
|
||||||
|
next.run(request).await
|
||||||
|
} else {
|
||||||
|
let ratelimit_info = RatelimitInformation::from_request_parts(&mut parts, &state).await;
|
||||||
|
|
||||||
|
ratelimit_info.map(Json).into_response()
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ratelimiter {
|
||||||
|
key,
|
||||||
|
limit,
|
||||||
|
remaining,
|
||||||
|
reset,
|
||||||
|
} = ratelimiter;
|
||||||
|
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
|
||||||
|
headers.insert(
|
||||||
|
"X-RateLimit-Limit",
|
||||||
|
HeaderValue::from_str(&limit.to_string()).unwrap(),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
"X-RateLimit-Bucket",
|
||||||
|
HeaderValue::from_str(&key.to_string()).unwrap(),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
"X-RateLimit-Remaining",
|
||||||
|
HeaderValue::from_str(&remaining.to_string()).unwrap(),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
"X-RateLimit-Reset-After",
|
||||||
|
HeaderValue::from_str(&reset.to_string()).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if res.is_err() {
|
||||||
|
*response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
|
||||||
|
};
|
||||||
|
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
|
||||||
|
Json(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes<S: Clone + Send + Sync + 'static>() -> Router<S>
|
||||||
|
where
|
||||||
|
Database: FromRef<S>,
|
||||||
|
RatelimitStorage: FromRef<S>,
|
||||||
|
{
|
||||||
|
Router::new().route("/ratelimit", get(ratelimit_info))
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod ratelimiter;
|
||||||
|
|
||||||
|
#[cfg(feature = "rocket")]
|
||||||
|
pub mod rocket;
|
||||||
|
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
pub mod axum;
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::hash::Hasher;
|
||||||
|
use std::ops::Add;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use dashmap::DashMap;
|
||||||
|
|
||||||
|
pub trait RequestKind {
|
||||||
|
type R<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait RatelimitResolver<R>: Send + Sync {
|
||||||
|
fn resolve_bucket<'a>(&self, request: &'a R) -> (&'a str, Option<&'a str>);
|
||||||
|
fn resolve_bucket_limit(&self, bucket: &str) -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RatelimitStorage<K: RequestKind> {
|
||||||
|
pub resolver: Arc<dyn for<'a> RatelimitResolver<K::R<'a>>>,
|
||||||
|
pub map: Arc<DashMap<u64, Entry>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: RequestKind> RatelimitStorage<K> {
|
||||||
|
pub fn new<R: for<'a> RatelimitResolver<K::R<'a>> + 'static>(resolver: R) -> Self {
|
||||||
|
Self {
|
||||||
|
resolver: Arc::new(resolver),
|
||||||
|
map: Arc::new(DashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ratelimit Bucket
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Entry {
|
||||||
|
used: u32,
|
||||||
|
reset: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current time from Unix Epoch as a Duration
|
||||||
|
fn now() -> Duration {
|
||||||
|
SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.expect("Time went backwards...")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entry {
|
||||||
|
/// Find bucket by its key
|
||||||
|
pub fn from(map: &DashMap<u64, Entry>, key: u64) -> Entry {
|
||||||
|
map.get(&key).map(|x| *x).unwrap_or_else(|| Entry {
|
||||||
|
used: 0,
|
||||||
|
reset: now().add(Duration::from_secs(10)).as_millis(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deduct one unit from the bucket and save
|
||||||
|
pub fn deduct(&mut self) {
|
||||||
|
let current_time = now().as_millis();
|
||||||
|
if current_time > self.reset {
|
||||||
|
self.used = 1;
|
||||||
|
self.reset = now().add(Duration::from_secs(10)).as_millis();
|
||||||
|
} else {
|
||||||
|
self.used += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save information
|
||||||
|
pub fn save(self, map: &DashMap<u64, Entry>, key: u64) {
|
||||||
|
map.insert(key, self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get remaining units in the bucket
|
||||||
|
pub fn get_remaining(&self, limit: u32) -> u32 {
|
||||||
|
if now().as_millis() > self.reset {
|
||||||
|
limit
|
||||||
|
} else {
|
||||||
|
limit - self.used
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get how long bucket has until reset
|
||||||
|
pub fn left_until_reset(&self) -> u128 {
|
||||||
|
let current_time = now().as_millis();
|
||||||
|
self.reset.saturating_sub(current_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ratelimit Guard
|
||||||
|
#[derive(Serialize, Clone, Copy, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct Ratelimiter {
|
||||||
|
pub key: u64,
|
||||||
|
pub limit: u32,
|
||||||
|
pub remaining: u32,
|
||||||
|
pub reset: u128,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ratelimiter {
|
||||||
|
/// Generate guard from identifier and target bucket
|
||||||
|
pub fn from(
|
||||||
|
map: &DashMap<u64, Entry>,
|
||||||
|
identifier: &str,
|
||||||
|
limit: u32,
|
||||||
|
(bucket, resource): (&str, Option<&str>),
|
||||||
|
) -> Result<Ratelimiter, Ratelimiter> {
|
||||||
|
let mut key = DefaultHasher::new();
|
||||||
|
key.write(identifier.as_bytes());
|
||||||
|
key.write(bucket.as_bytes());
|
||||||
|
|
||||||
|
if let Some(id) = resource {
|
||||||
|
key.write(id.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = key.finish();
|
||||||
|
let mut entry = Entry::from(map, key);
|
||||||
|
|
||||||
|
let remaining = entry.get_remaining(limit);
|
||||||
|
let reset = entry.left_until_reset();
|
||||||
|
let mut ratelimiter = Ratelimiter {
|
||||||
|
key,
|
||||||
|
limit,
|
||||||
|
remaining,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
if remaining == 0 {
|
||||||
|
return Err(ratelimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.deduct();
|
||||||
|
entry.save(map, key);
|
||||||
|
ratelimiter.remaining -= 1;
|
||||||
|
ratelimiter.reset = entry.left_until_reset();
|
||||||
|
|
||||||
|
Ok(ratelimiter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum RatelimitInformation {
|
||||||
|
Success(Ratelimiter),
|
||||||
|
Failure { retry_after: u128 },
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
use log::info;
|
||||||
|
use rocket::fairing::{Fairing, Info, Kind};
|
||||||
|
use rocket::http::uri::Origin;
|
||||||
|
use rocket::http::{Method, Status};
|
||||||
|
use rocket::request::{FromRequest, Outcome};
|
||||||
|
use rocket::serde::json::Json;
|
||||||
|
use rocket::{Data, Request, Response, State};
|
||||||
|
use revolt_config::config;
|
||||||
|
|
||||||
|
use revolt_rocket_okapi::r#gen::OpenApiGenerator;
|
||||||
|
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
|
||||||
|
|
||||||
|
use authifier::models::Session;
|
||||||
|
|
||||||
|
use crate::ratelimiter::RequestKind;
|
||||||
|
use crate::ratelimiter::{RatelimitInformation, Ratelimiter};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct RocketRequestKind;
|
||||||
|
|
||||||
|
impl RequestKind for RocketRequestKind {
|
||||||
|
type R<'a> = Request<'a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage<RocketRequestKind>;
|
||||||
|
|
||||||
|
/// Find the remote IP of the client
|
||||||
|
fn to_ip(request: &'_ rocket::Request<'_>) -> String {
|
||||||
|
request
|
||||||
|
.remote()
|
||||||
|
.map(|x| x.ip().to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the actual IP of the client
|
||||||
|
async fn to_real_ip(request: &'_ rocket::Request<'_>) -> String {
|
||||||
|
if config().await.api.security.trust_cloudflare {
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.get_one("CF-Connecting-IP")
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_else(|| to_ip(request))
|
||||||
|
} else {
|
||||||
|
to_ip(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for Ratelimiter {
|
||||||
|
type Error = Ratelimiter;
|
||||||
|
|
||||||
|
async fn from_request<'a>(request: &'r rocket::Request<'a>) -> Outcome<Self, Self::Error> {
|
||||||
|
let ratelimiter = request
|
||||||
|
.local_cache_async(async {
|
||||||
|
use rocket::outcome::Outcome;
|
||||||
|
|
||||||
|
let storage = request.guard::<&State<RatelimitStorage>>().await.unwrap();
|
||||||
|
|
||||||
|
let identifier = if let Outcome::Success(session) = request.guard::<Session>().await
|
||||||
|
{
|
||||||
|
session.id
|
||||||
|
} else {
|
||||||
|
to_real_ip(request).await
|
||||||
|
};
|
||||||
|
|
||||||
|
let (bucket, resource) = storage.resolver.resolve_bucket(request);
|
||||||
|
let limit = storage.resolver.resolve_bucket_limit(bucket);
|
||||||
|
|
||||||
|
Ratelimiter::from(&storage.map, &identifier, limit, (bucket, resource))
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match ratelimiter {
|
||||||
|
Ok(ratelimiter) => Outcome::Success(*ratelimiter),
|
||||||
|
Err(ratelimiter) => Outcome::Error((Status::TooManyRequests, *ratelimiter)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenApiFromRequest<'_> for Ratelimiter {
|
||||||
|
fn from_request_input(
|
||||||
|
_gen: &mut OpenApiGenerator,
|
||||||
|
_name: String,
|
||||||
|
_required: bool,
|
||||||
|
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
||||||
|
Ok(RequestHeaderInput::None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach ratelimiter to the Rocket application
|
||||||
|
pub struct RatelimitFairing;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Fairing for RatelimitFairing {
|
||||||
|
fn info(&self) -> Info {
|
||||||
|
Info {
|
||||||
|
name: "Ratelimiter",
|
||||||
|
kind: Kind::Request | Kind::Response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
|
||||||
|
use rocket::outcome::Outcome;
|
||||||
|
if let Outcome::Error(_) = request.guard::<Ratelimiter>().await {
|
||||||
|
info!(
|
||||||
|
"User rate-limited on route {}! (IP = {:?})",
|
||||||
|
request.uri(),
|
||||||
|
to_real_ip(request).await
|
||||||
|
);
|
||||||
|
|
||||||
|
request.set_method(Method::Get);
|
||||||
|
request.set_uri(Origin::parse("/ratelimit").unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
|
||||||
|
let guard = request.guard::<Ratelimiter>().await;
|
||||||
|
let (Outcome::Success(ratelimiter) | Outcome::Error((_, ratelimiter))) = guard else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let Ratelimiter {
|
||||||
|
key,
|
||||||
|
limit,
|
||||||
|
remaining,
|
||||||
|
reset,
|
||||||
|
} = ratelimiter;
|
||||||
|
|
||||||
|
response.set_raw_header("X-RateLimit-Limit", limit.to_string());
|
||||||
|
response.set_raw_header("X-RateLimit-Bucket", key.to_string());
|
||||||
|
response.set_raw_header("X-RateLimit-Remaining", remaining.to_string());
|
||||||
|
response.set_raw_header("X-RateLimit-Reset-After", reset.to_string());
|
||||||
|
|
||||||
|
if guard.is_error() {
|
||||||
|
response.set_status(Status::TooManyRequests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for RatelimitInformation {
|
||||||
|
type Error = u128;
|
||||||
|
|
||||||
|
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let info = match request.guard::<Ratelimiter>().await {
|
||||||
|
Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter),
|
||||||
|
Outcome::Error((_, ratelimiter)) => RatelimitInformation::Failure {
|
||||||
|
retry_after: ratelimiter.reset,
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
Outcome::Success(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rocket::get("/ratelimit")]
|
||||||
|
fn ratelimit_info(info: RatelimitInformation) -> Json<RatelimitInformation> {
|
||||||
|
Json(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<rocket::Route> {
|
||||||
|
rocket::routes![ratelimit_info]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-result"
|
name = "revolt-result"
|
||||||
version = "0.8.8"
|
version = "0.8.9"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ impl IntoResponse for Error {
|
|||||||
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
|
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
|
||||||
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
|
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
|
||||||
|
|
||||||
|
ErrorType::MissingScope { .. } => StatusCode::UNAUTHORIZED,
|
||||||
|
ErrorType::ExpiredToken => StatusCode::UNAUTHORIZED,
|
||||||
|
|
||||||
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
|
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
|
||||||
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
|
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
ErrorType::FileTooLarge { .. } => StatusCode::UNPROCESSABLE_ENTITY,
|
ErrorType::FileTooLarge { .. } => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ impl std::error::Error for Error {}
|
|||||||
#[cfg_attr(feature = "serde", serde(tag = "type"))]
|
#[cfg_attr(feature = "serde", serde(tag = "type"))]
|
||||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||||
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
|
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub enum ErrorType {
|
pub enum ErrorType {
|
||||||
/// This error was not labeled :(
|
/// This error was not labeled :(
|
||||||
LabelMe,
|
LabelMe,
|
||||||
@@ -168,6 +168,12 @@ pub enum ErrorType {
|
|||||||
ImageProcessingFailed,
|
ImageProcessingFailed,
|
||||||
NoEmbedData,
|
NoEmbedData,
|
||||||
|
|
||||||
|
// ? OAuth2 Errors
|
||||||
|
ExpiredToken,
|
||||||
|
MissingScope {
|
||||||
|
scope: String
|
||||||
|
},
|
||||||
|
|
||||||
// ? Legacy errors
|
// ? Legacy errors
|
||||||
VosoUnavailable,
|
VosoUnavailable,
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ impl<'r> Responder<'r, 'static> for Error {
|
|||||||
ErrorType::FailedValidation { .. } => Status::BadRequest,
|
ErrorType::FailedValidation { .. } => Status::BadRequest,
|
||||||
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
|
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
|
||||||
|
|
||||||
|
ErrorType::MissingScope { .. } => Status::Unauthorized,
|
||||||
|
ErrorType::ExpiredToken => Status::Unauthorized,
|
||||||
|
|
||||||
ErrorType::ProxyError => Status::BadRequest,
|
ErrorType::ProxyError => Status::BadRequest,
|
||||||
ErrorType::FileTooSmall => Status::UnprocessableEntity,
|
ErrorType::FileTooSmall => Status::UnprocessableEntity,
|
||||||
ErrorType::FileTooLarge { .. } => Status::UnprocessableEntity,
|
ErrorType::FileTooLarge { .. } => Status::UnprocessableEntity,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user