forked from jmug/stoatchat
Compare commits
187 Commits
0.0.1
...
20250930-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4789dd4847 | ||
|
|
f91d3bbfdd | ||
|
|
5b50532d15 | ||
|
|
938480ccec | ||
|
|
38dd4d1079 | ||
|
|
154204742d | ||
|
|
db55998546 | ||
|
|
5885e067a6 | ||
|
|
a92152d86d | ||
|
|
b5cd5e30ef | ||
|
|
b0c977b324 | ||
|
|
bfe4018e43 | ||
|
|
cc7a7962a8 | ||
|
|
fb4011084d | ||
|
|
3a3415915f | ||
|
|
964884a5de | ||
|
|
db57706794 | ||
|
|
a186732ca5 | ||
|
|
f422e8e917 | ||
|
|
1632e3dacf | ||
|
|
e635bc23ec | ||
|
|
cc9a68b9c5 | ||
|
|
77a23239ea | ||
|
|
3cb7da95e3 | ||
|
|
dea0f675dd | ||
|
|
480f210ce8 | ||
|
|
caa8607468 | ||
|
|
32e6600272 | ||
|
|
5335124306 | ||
|
|
1561481eb4 | ||
|
|
d03e6be2eb | ||
|
|
0a4d77ffd3 | ||
|
|
8f9015a6ff | ||
|
|
c6f121f585 | ||
|
|
96ddeb79d2 | ||
|
|
a36b138978 | ||
|
|
ed22b3a5ce | ||
|
|
65fbd36624 | ||
|
|
860c47fbad | ||
|
|
050ba16d4a | ||
|
|
65bc6c8fc6 | ||
|
|
6ad3da5f35 | ||
|
|
947eb15771 | ||
|
|
f4ee35fb09 | ||
|
|
6048587d34 | ||
|
|
a770d5822a | ||
|
|
10c9682bba | ||
|
|
80cf8fc4e8 | ||
|
|
4f54227495 | ||
|
|
eb48975428 | ||
|
|
28ed85a779 | ||
|
|
aab1734615 | ||
|
|
40a41ffd64 | ||
|
|
d30ceea373 | ||
|
|
3e8a401077 | ||
|
|
99f400bc7b | ||
|
|
73b576a75f | ||
|
|
4e4e598daf | ||
|
|
77daf82b94 | ||
|
|
e00603f276 | ||
|
|
1b2c7b2fa1 | ||
|
|
c526095d4f | ||
|
|
95ca735b7b | ||
|
|
8cc4bbea4d | ||
|
|
911ffc767e | ||
|
|
1690df998d | ||
|
|
519d3c08a8 | ||
|
|
9846d8aac2 | ||
|
|
c74b6255dd | ||
|
|
df91b8c990 | ||
|
|
901ac56154 | ||
|
|
3dbf3bb7a0 | ||
|
|
45df5724b3 | ||
|
|
ea1c165d80 | ||
|
|
c4728c696d | ||
|
|
8153f5f17a | ||
|
|
99a80f723d | ||
|
|
4f13f5899b | ||
|
|
5362e84730 | ||
|
|
be89e62d54 | ||
|
|
a72134f4ad | ||
|
|
69ae767175 | ||
|
|
2aff76c369 | ||
|
|
01e0f9e558 | ||
|
|
8ea8f50fb8 | ||
|
|
68bcfcb626 | ||
|
|
9632a0dc1b | ||
|
|
a4a0934475 | ||
|
|
2061794305 | ||
|
|
3a7ebad883 | ||
|
|
6daa8223b2 | ||
|
|
4a58d6eae0 | ||
|
|
a8a2e491b0 | ||
|
|
f98f09a5ea | ||
|
|
7a4e12a732 | ||
|
|
aa8f8575bc | ||
|
|
70d6a5ac08 | ||
|
|
7f85ad5e7b | ||
|
|
c785ae615d | ||
|
|
9c8a362e9f | ||
|
|
48e0b48ea9 | ||
|
|
61038ddcf1 | ||
|
|
e708c612cf | ||
|
|
b96f07e615 | ||
|
|
d830fb0911 | ||
|
|
cc9ff9a632 | ||
|
|
02f18705f4 | ||
|
|
a841df3b36 | ||
|
|
2a36f5912d | ||
|
|
a110a087be | ||
|
|
21b9164ee7 | ||
|
|
2f3d1f0c7e | ||
|
|
62204fe50b | ||
|
|
773b890012 | ||
|
|
9c3b2fb364 | ||
|
|
5fe19d9815 | ||
|
|
c1c5b0fd29 | ||
|
|
6737100b4b | ||
|
|
6f0e80eef9 | ||
|
|
bfc6f50ee5 | ||
|
|
7abbad700c | ||
|
|
a671ba6308 | ||
|
|
1d385ef2bc | ||
|
|
6558e9f7c4 | ||
|
|
92f4b44cb3 | ||
|
|
370442bf4e | ||
|
|
d54c5e80fa | ||
|
|
a42746c55b | ||
|
|
9a98c53aff | ||
|
|
4ea08a189d | ||
|
|
ae15a0b4c7 | ||
|
|
9e5fbec9b0 | ||
|
|
0b02c84f04 | ||
|
|
8bfe3ec6b0 | ||
|
|
a7345a7b90 | ||
|
|
12854d0f1b | ||
|
|
9a2a7901e7 | ||
|
|
e8cb0bb88a | ||
|
|
343b90b9af | ||
|
|
a4246f39e4 | ||
|
|
38380db6e7 | ||
|
|
f75d635c81 | ||
|
|
29b6ac3acf | ||
|
|
a1b0e4767a | ||
|
|
f4281c7050 | ||
|
|
d2e83c94f3 | ||
|
|
2540860129 | ||
|
|
93a5de769d | ||
|
|
717d5e9a88 | ||
|
|
18f06467bb | ||
|
|
8d88ea9963 | ||
|
|
e3e1cab02e | ||
|
|
00bd56b687 | ||
|
|
a7727bba5c | ||
|
|
6b8005f530 | ||
|
|
091aa4705b | ||
|
|
714fba7ea6 | ||
|
|
ac05fb385c | ||
|
|
537c147c4b | ||
|
|
7bee609b84 | ||
|
|
859c0214ec | ||
|
|
5eea0bbc9a | ||
|
|
15dec656a8 | ||
|
|
8b5e085232 | ||
|
|
bcf6561c0c | ||
|
|
b177a3e201 | ||
|
|
d1236bf8f2 | ||
|
|
01490f5723 | ||
|
|
3ceefb840b | ||
|
|
9c2cc8736c | ||
|
|
951128032b | ||
|
|
367ac887f4 | ||
|
|
060b4c43f4 | ||
|
|
2593a4272b | ||
|
|
742eb3bb7d | ||
|
|
41a6de67cd | ||
|
|
c8ae34ba73 | ||
|
|
c55b5bf75f | ||
|
|
9de87a01ab | ||
|
|
2f7294705e | ||
|
|
120ca449b8 | ||
|
|
ffbc899792 | ||
|
|
675c7d6c0d | ||
|
|
67569f932e | ||
|
|
7a4421089c | ||
|
|
ae87bc0b67 | ||
|
|
ad0dcad497 |
@@ -1,2 +1,2 @@
|
||||
[profile.default]
|
||||
slow-timeout = { period = "3s", terminate-after = 2 }
|
||||
slow-timeout = { period = "5s", terminate-after = 10 }
|
||||
|
||||
308
.github/workflows/docker.yaml
vendored
308
.github/workflows/docker.yaml
vendored
@@ -1,115 +1,213 @@
|
||||
name: Docker
|
||||
name: Docker Test & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "handmade"
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "handmade"
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
- "main"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
# Builds the shared base image: the whole Rust workspace compiled once.
|
||||
# Every service image (currently only pushd) is derived from this so the
|
||||
# workspace is built a single time per release. On non-release events this
|
||||
# still runs as a compile check, but nothing is pushed.
|
||||
base:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Resolve registry host and owner
|
||||
id: registry
|
||||
# Owner is lowercased: Docker repository names must be lowercase.
|
||||
run: |
|
||||
echo "host=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_OUTPUT"
|
||||
echo "owner=$(echo "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
|
||||
- name: Base image ref
|
||||
id: base
|
||||
run: echo "image=${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/handmade-revolt-base" >> "$GITHUB_OUTPUT"
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
registry: ${{ steps.registry.outputs.host }}
|
||||
username: ${{ secrets.PACKAGE_PUBLISH_USERNAME }}
|
||||
password: ${{ secrets.PACKAGE_PUBLISH_TOKEN }}
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
platforms: linux/amd64
|
||||
# Pinned to the commit so the publish job below consumes this
|
||||
# exact base.
|
||||
tags: |
|
||||
${{ steps.base.outputs.image }}:latest
|
||||
${{ steps.base.outputs.image }}:${{ github.sha }}
|
||||
base:
|
||||
name: Test base image build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# Builds individual service images from the base. Only runs on release.
|
||||
publish:
|
||||
needs: [base]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'release'
|
||||
strategy:
|
||||
matrix:
|
||||
# Only pushd is built. To add more services later, add
|
||||
# entries here (paths/images mirror the upstream revolt layout):
|
||||
# - { project: delta, path: crates/delta, image: handmade-revolt-server }
|
||||
# - { project: bonfire, path: crates/bonfire, image: handmade-revolt-bonfire }
|
||||
# - { project: autumn, path: crates/services/autumn, image: handmade-revolt-autumn }
|
||||
# - { project: january, path: crates/services/january, image: handmade-revolt-january }
|
||||
# - { project: crond, path: crates/daemons/crond, image: handmade-revolt-crond }
|
||||
include:
|
||||
- { project: pushd, path: crates/daemons/pushd, image: handmade-revolt-pushd }
|
||||
name: Build ${{ matrix.project }} image
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Resolve registry host and owner
|
||||
id: registry
|
||||
# Owner is lowercased: Docker repository names must be lowercase,
|
||||
# but the org (e.g. HMC) may be uppercase.
|
||||
run: |
|
||||
echo "host=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_OUTPUT"
|
||||
echo "owner=$(echo "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/${{ matrix.image }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=latest
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ steps.registry.outputs.host }}
|
||||
username: ${{ secrets.PACKAGE_PUBLISH_USERNAME }}
|
||||
password: ${{ secrets.PACKAGE_PUBLISH_TOKEN }}
|
||||
- name: Build and publish
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.path }}/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/handmade-revolt-base:${{ github.sha }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
# Build the image
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
cache-from: type=gha,scope=buildx-base-multi-arch
|
||||
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
|
||||
|
||||
publish:
|
||||
runs-on: self-hosted
|
||||
if: github.event_name != 'pull_request'
|
||||
name: Publish Docker images
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# 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
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Build the image
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
|
||||
# revoltchat/server
|
||||
- name: Docker meta
|
||||
id: meta-delta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/server
|
||||
ghcr.io/revoltchat/server
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/delta/Dockerfile
|
||||
tags: ${{ steps.meta-delta.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-delta.outputs.labels }}
|
||||
|
||||
# revoltchat/bonfire
|
||||
- name: Docker meta
|
||||
id: meta-bonfire
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/bonfire
|
||||
ghcr.io/revoltchat/bonfire
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/bonfire/Dockerfile
|
||||
tags: ${{ steps.meta-bonfire.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-bonfire.outputs.labels }}
|
||||
|
||||
# revoltchat/autumn
|
||||
- name: Docker meta
|
||||
id: meta-autumn
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/autumn
|
||||
ghcr.io/revoltchat/autumn
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/services/autumn/Dockerfile
|
||||
tags: ${{ steps.meta-autumn.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-autumn.outputs.labels }}
|
||||
|
||||
# revoltchat/january
|
||||
- name: Docker meta
|
||||
id: meta-january
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/january
|
||||
ghcr.io/revoltchat/january
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/services/january/Dockerfile
|
||||
tags: ${{ steps.meta-january.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-january.outputs.labels }}
|
||||
|
||||
# revoltchat/gifbox
|
||||
- name: Docker meta
|
||||
id: meta-gifbox
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/gifbox
|
||||
ghcr.io/revoltchat/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 }}
|
||||
|
||||
# revoltchat/crond
|
||||
- name: Docker meta
|
||||
id: meta-crond
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/crond
|
||||
ghcr.io/revoltchat/crond
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/daemons/crond/Dockerfile
|
||||
tags: ${{ steps.meta-crond.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-crond.outputs.labels }}
|
||||
|
||||
# revoltchat/pushd
|
||||
- name: Docker meta
|
||||
id: meta-pushd
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/pushd
|
||||
ghcr.io/revoltchat/pushd
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/daemons/pushd/Dockerfile
|
||||
tags: ${{ steps.meta-pushd.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta-pushd.outputs.labels }}
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -5,8 +5,12 @@ compose.override.yml
|
||||
target
|
||||
.data
|
||||
.env
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
.vercel
|
||||
.DS_Store
|
||||
|
||||
.idea
|
||||
livekit.yml
|
||||
.idea
|
||||
start
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"rust-analyzer.checkOnSave.command": "clippy",
|
||||
"rust-analyzer.check.command": "clippy",
|
||||
"nixEnvSelector.suggestion": false,
|
||||
"nixEnvSelector.nixFile": "${workspaceFolder}/default.nix"
|
||||
}
|
||||
}
|
||||
|
||||
5944
Cargo.lock
generated
5944
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -6,19 +6,17 @@ members = [
|
||||
"crates/bonfire",
|
||||
"crates/core/*",
|
||||
"crates/services/*",
|
||||
"crates/bindings/*",
|
||||
"crates/daemons/*",
|
||||
]
|
||||
|
||||
[patch.crates-io]
|
||||
redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
|
||||
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
|
||||
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
|
||||
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
|
||||
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
|
||||
#authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
|
||||
#rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
|
||||
|
||||
# I'm 99% sure this is overloading the GitHub worker
|
||||
# hence builds have been failing since, let's just
|
||||
# disable it for now. In the future, we could use this
|
||||
# if we were rolling our own CI.
|
||||
# [profile.release]
|
||||
# lto = true
|
||||
# if we were rolling our own CI (that is now).
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.77.2-slim-bookworm
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.86.0-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
@@ -17,20 +17,24 @@ RUN sh /tmp/build-image-layer.sh tools
|
||||
|
||||
# Build all dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/files/Cargo.toml ./crates/core/files/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
COPY crates/core/parser/Cargo.toml ./crates/core/parser/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
COPY crates/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/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/pushd/Cargo.toml ./crates/daemons/pushd/
|
||||
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM rust:1.77.2-slim-bookworm
|
||||
FROM rust:1.86.0-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
@@ -13,20 +13,24 @@ COPY scripts/build-image-layer.sh /tmp/
|
||||
|
||||
# Build all dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/files/Cargo.toml ./crates/core/files/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
COPY crates/core/parser/Cargo.toml ./crates/core/parser/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
COPY crates/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/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/pushd/Cargo.toml ./crates/daemons/pushd/
|
||||
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
|
||||
43
README.md
43
README.md
@@ -12,32 +12,33 @@
|
||||
The services and libraries that power the Revolt service.<br/>
|
||||
<br/>
|
||||
|
||||
| Crate | Path | Description | |
|
||||
| ------------------ | -------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |     |
|
||||
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |     |
|
||||
| `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines |     |
|
||||
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |     |
|
||||
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |     |
|
||||
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |     |
|
||||
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |     |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |  |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |  |
|
||||
| `services/january` | [crates/services/january](crates/services/january) | Proxy server |  |
|
||||
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server |  |
|
||||
| `bindings/node` | [crates/bindings/node](crates/bindings/node) | Node.js bindings for the Revolt software |  |
|
||||
| `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 |  |
|
||||
| Crate | Path | Description | |
|
||||
| ------------------ | -------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |     |
|
||||
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |     |
|
||||
| `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines |     |
|
||||
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |     |
|
||||
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |     |
|
||||
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |     |
|
||||
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |     |
|
||||
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service |     |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |  |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events 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 |  |
|
||||
| `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 |  |
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
## Minimum Supported Rust Version
|
||||
|
||||
Rust 1.76 or higher.
|
||||
Rust 1.86.0 or higher.
|
||||
|
||||
> [!CAUTION]
|
||||
> The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341).
|
||||
> The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341). This is currently solved by build time options but we are looking for a proper fix.
|
||||
|
||||
## Development Guide
|
||||
|
||||
@@ -67,6 +68,7 @@ As a heads-up, the development environment uses the following ports:
|
||||
| `crates/bonfire` | 14703 |
|
||||
| `crates/services/autumn` | 14704 |
|
||||
| `crates/services/january` | 14705 |
|
||||
| `crates/services/gifbox` | 14706 |
|
||||
|
||||
Now you can clone and build the project:
|
||||
|
||||
@@ -115,7 +117,8 @@ If you'd like to change anything, create a `Revolt.overrides.toml` file and spec
|
||||
> And corresponding Revolt configuration:
|
||||
>
|
||||
> ```toml
|
||||
> # Revolt.overrides.toml
|
||||
> # Revolt.overrides.toml
|
||||
> # and Revolt.test-overrides.toml
|
||||
> [database]
|
||||
> mongodb = "mongodb://127.0.0.1:14017"
|
||||
> redis = "redis://127.0.0.1:14079/"
|
||||
@@ -141,6 +144,8 @@ cargo run --bin revolt-bonfire
|
||||
cargo run --bin revolt-autumn
|
||||
# run the proxy server
|
||||
cargo run --bin revolt-january
|
||||
# run the tenor proxy
|
||||
cargo run --bin revolt-gifbox
|
||||
# run the push daemon (not usually needed in regular development)
|
||||
cargo run --bin revolt-pushd
|
||||
|
||||
|
||||
20
Revolt.toml
20
Revolt.toml
@@ -1,5 +1,5 @@
|
||||
# ⚠️ This configuration is intended for development environment.
|
||||
# If you'd like to override anything, create a Revolt.override.toml
|
||||
# If you'd like to override anything, create a Revolt.overrides.toml
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
@@ -26,6 +26,11 @@ january = "http://local.revolt.chat:14705"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
# Public urls for livekit nodes
|
||||
# each entry here should have a corresponding entry under `api.livekit.nodes`
|
||||
[hosts.livekit]
|
||||
worldwide = "ws://local.revolt.chat:14706"
|
||||
|
||||
[api]
|
||||
|
||||
[api.smtp]
|
||||
@@ -38,6 +43,19 @@ from_address = "development@revolt.chat"
|
||||
reply_to = "support@revolt.chat"
|
||||
port = 14025
|
||||
use_tls = false
|
||||
use_starttls = false
|
||||
|
||||
[api.livekit]
|
||||
|
||||
# Config for livekit nodes
|
||||
# Make sure to change the secret when deploying
|
||||
# The key and secret should match the values livekit is using
|
||||
[api.livekit.nodes.worldwide]
|
||||
url = "http://livekit"
|
||||
lat = 0.0
|
||||
lon = 0.0
|
||||
key = "worldwide_key"
|
||||
secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0"
|
||||
|
||||
[files.s3]
|
||||
# S3 protocol endpoint
|
||||
|
||||
@@ -34,13 +34,13 @@ services:
|
||||
- minio
|
||||
entrypoint: >
|
||||
/bin/sh -c "while ! /usr/bin/mc ready minio; do
|
||||
/usr/bin/mc config host add minio http://minio:9000 minioautumn minioautumn;
|
||||
/usr/bin/mc alias set minio http://minio:9000 minioautumn minioautumn;
|
||||
echo 'Waiting minio...' && sleep 1;
|
||||
done; /usr/bin/mc mb minio/revolt-uploads; exit 0;"
|
||||
|
||||
# Rabbit
|
||||
rabbit:
|
||||
image: rabbitmq:3-management
|
||||
image: rabbitmq:4-management
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: rabbituser
|
||||
RABBITMQ_DEFAULT_PASS: rabbitpass
|
||||
|
||||
7
crates/bindings/node/.gitignore
vendored
7
crates/bindings/node/.gitignore
vendored
@@ -1,7 +0,0 @@
|
||||
target
|
||||
index.node
|
||||
**/node_modules
|
||||
**/.DS_Store
|
||||
npm-debug.log*
|
||||
cargo.log
|
||||
cross.log
|
||||
@@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "revolt-nodejs-bindings"
|
||||
version = "0.8.2"
|
||||
description = "Node.js bindings for the Revolt software"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
exclude = ["index.node"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
neon = "1.0.0"
|
||||
neon-serde4 = "1.0.0"
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
async-std = "1.12.0"
|
||||
|
||||
revolt-config = { version = "0.8.2", path = "../../core/config" }
|
||||
revolt-result = { version = "0.8.2", path = "../../core/result" }
|
||||
revolt-database = { version = "0.8.2", path = "../../core/database" }
|
||||
@@ -1,92 +0,0 @@
|
||||
# revolt.node
|
||||
|
||||
**revolt.node:** Node.js bindings for the Revolt software
|
||||
|
||||
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
|
||||
|
||||
## Building revolt.node
|
||||
|
||||
Building revolt.node requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
|
||||
|
||||
To run the build, run:
|
||||
|
||||
```sh
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`.
|
||||
|
||||
## Exploring revolt.node
|
||||
|
||||
After building revolt.node, you can explore its exports at the Node console:
|
||||
|
||||
```sh
|
||||
$ npm i
|
||||
$ npm run build
|
||||
$ node
|
||||
> require('.').hello()
|
||||
'hello node'
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
#### `npm install`
|
||||
|
||||
Installs the project, including running `npm run build`.
|
||||
|
||||
#### `npm run build`
|
||||
|
||||
Builds the Node addon (`index.node`) from source, generating a release build with `cargo --release`.
|
||||
|
||||
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm run build` and similar commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
|
||||
|
||||
```
|
||||
npm run build -- --feature=beetle
|
||||
```
|
||||
|
||||
#### `npm run debug`
|
||||
|
||||
Similar to `npm run build` but generates a debug build with `cargo`.
|
||||
|
||||
#### `npm run cross`
|
||||
|
||||
Similar to `npm run build` but uses [cross-rs](https://github.com/cross-rs/cross) to cross-compile for another platform. Use the [`CARGO_BUILD_TARGET`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget) environment variable to select the build target.
|
||||
|
||||
#### `npm test`
|
||||
|
||||
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
|
||||
|
||||
## Project Layout
|
||||
|
||||
The directory structure of this project is:
|
||||
|
||||
```
|
||||
revolt.node/
|
||||
├── Cargo.toml
|
||||
├── README.md
|
||||
├── src/
|
||||
| └── lib.rs
|
||||
├── index.node
|
||||
├── package.json
|
||||
└── target/
|
||||
```
|
||||
|
||||
| Entry | Purpose |
|
||||
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
|
||||
| `README.md` | This file. |
|
||||
| `src/` | The directory tree containing the Rust source code for the project. |
|
||||
| `lib.rs` | Entry point for the Rust source code. |
|
||||
| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. |
|
||||
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
|
||||
| `target/` | Binary artifacts generated by the Rust build. |
|
||||
|
||||
## Learn More
|
||||
|
||||
Learn more about:
|
||||
|
||||
- [Neon](https://neon-bindings.com).
|
||||
- [Rust](https://www.rust-lang.org).
|
||||
- [Node](https://nodejs.org).
|
||||
84
crates/bindings/node/index.d.ts
vendored
84
crates/bindings/node/index.d.ts
vendored
@@ -1,84 +0,0 @@
|
||||
import { Channel, User } from "revolt-api";
|
||||
|
||||
/**
|
||||
* Opaque type for Revolt database
|
||||
*/
|
||||
export declare interface Database {}
|
||||
|
||||
/**
|
||||
* Opaque type for Revolt database
|
||||
*/
|
||||
export declare interface OpaqueUser {}
|
||||
|
||||
/**
|
||||
* Error type from Revolt backend
|
||||
*/
|
||||
export declare interface Err {
|
||||
type: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises background tasks and logging, must be called before anything else!
|
||||
* Can be called multiple times!
|
||||
*/
|
||||
export declare function init();
|
||||
|
||||
/**
|
||||
* Gets a new handle to the Revolt database
|
||||
* @returns {Database} Handle
|
||||
*/
|
||||
export declare function database(): Database;
|
||||
|
||||
/**
|
||||
* Fetch user from database
|
||||
* @param {string} userId User's ID
|
||||
* @this {Database}
|
||||
*/
|
||||
export declare function database_fetch_user(userId: string): OpaqueUser;
|
||||
|
||||
/**
|
||||
* Fetch user from database
|
||||
* @param {string} username Username
|
||||
* @param {string} discriminator Discriminator
|
||||
* @this {Database}
|
||||
*/
|
||||
export declare function database_fetch_user_by_username(
|
||||
username: string,
|
||||
discriminator: string
|
||||
): OpaqueUser;
|
||||
|
||||
/**
|
||||
* Gets model data as JSON
|
||||
* @this {OpaqueUser}
|
||||
*/
|
||||
export declare function model_data(): User;
|
||||
|
||||
/**
|
||||
* Gets error if the model failed to fetch
|
||||
* @this {OpaqueUser}
|
||||
*/
|
||||
export declare function model_error(): Err;
|
||||
|
||||
/**
|
||||
* Open a direct message channel between two users
|
||||
* @param {string} userA User A ID
|
||||
* @param {string} userB User B ID
|
||||
* @returns Existing or newly created channel
|
||||
*/
|
||||
export declare function proc_channels_create_dm(
|
||||
userA: string,
|
||||
userB: string
|
||||
): Promise<Channel & { error: Err }>;
|
||||
|
||||
/**
|
||||
* Suspend a user
|
||||
* @param {string} user User
|
||||
* @param {number} duration Duration (in days), set to 0 for indefinite
|
||||
* @param {string} reason Pipe-separated list of reasons (e.g. reason1|reason2|reason3)
|
||||
*/
|
||||
export declare function proc_users_suspend(
|
||||
user: OpaqueUser,
|
||||
duration: number,
|
||||
reason: string
|
||||
): Promise<{ error: Err }>;
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "revolt-nodejs-bindings",
|
||||
"version": "0.7.15-rev0.0.3",
|
||||
"description": "Node.js bindings for the Revolt software",
|
||||
"main": "index.node",
|
||||
"scripts": {
|
||||
"test": "cargo test",
|
||||
"cargo-build": "cargo build --message-format=json > cargo.log",
|
||||
"cross-build": "cross build --message-format=json > cross.log",
|
||||
"postcargo-build": "neon dist < cargo.log",
|
||||
"postcross-build": "neon dist -m /target < cross.log",
|
||||
"debug": "npm run cargo-build --",
|
||||
"build": "npm run cargo-build -- --release",
|
||||
"cross": "npm run cross-build -- --release"
|
||||
},
|
||||
"author": "Paul Makles",
|
||||
"license": "AGPL-3.0",
|
||||
"devDependencies": {
|
||||
"@neon-rs/cli": "0.1.73"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/revoltchat/backend"
|
||||
},
|
||||
"keywords": [
|
||||
"revolt",
|
||||
"chat"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/revoltchat/backend/issues"
|
||||
},
|
||||
"homepage": "https://github.com/revoltchat/backend#readme",
|
||||
"dependencies": {
|
||||
"revolt-api": "^0.7.15"
|
||||
}
|
||||
}
|
||||
117
crates/bindings/node/pnpm-lock.yaml
generated
117
crates/bindings/node/pnpm-lock.yaml
generated
@@ -1,117 +0,0 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
revolt-api:
|
||||
specifier: ^0.7.15
|
||||
version: 0.7.15
|
||||
|
||||
devDependencies:
|
||||
'@neon-rs/cli':
|
||||
specifier: 0.1.73
|
||||
version: 0.1.73
|
||||
|
||||
packages:
|
||||
|
||||
/@cargo-messages/android-arm-eabi@0.1.72:
|
||||
resolution: {integrity: sha512-gGZxIM1mj+Y5x+ULND6ZCNr7f70OJi9wDlycSK8hGONy9wrChN6JAIHryddC5cqcwlYAoQ6IDcDFElnhAYbybA==}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/darwin-arm64@0.1.72:
|
||||
resolution: {integrity: sha512-EAzN5MLaXPljZKZDO5qR+aBs44eSq2ZbEnS7AI/FziE3MzeXbrGOS3fLba5+7yWPFXJyZolXzePm8N1EBv8ovg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/darwin-x64@0.1.72:
|
||||
resolution: {integrity: sha512-RLo6j8s3nYbjdd1LDct4wamfChyRit7zokUuxtIYCu9XOlltkN5vnj1vwnrPvoqCMZ/7CbbuHFwSTn9A71de/w==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/linux-arm-gnueabihf@0.1.72:
|
||||
resolution: {integrity: sha512-tHsRshuzfjrX6SDW3jg6al8vMNLTMgczGnVYl5RuBZf/yrAUuwe30KxA9ge6w6mW6Ox797DyBchzAc9OLgTgmQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/linux-x64-gnu@0.1.72:
|
||||
resolution: {integrity: sha512-VGtL6CCnUbhsP4aYuBNT5kfrAL7o0qjrxw97a+ax13t+nJd26tVEEIKHMu5drvvS/Nm/hn7sLT8zMnnCv0pvHg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/win32-arm64-msvc@0.1.72:
|
||||
resolution: {integrity: sha512-V93Cgz39K+yqa3MveNbhh29pYCp8izK5uEavjPoxlNxAbsMCWH+s0verGDdUcfGxjR1H2V7oZ4FszPqR2SqMRQ==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cargo-messages/win32-x64-msvc@0.1.72:
|
||||
resolution: {integrity: sha512-knz3uSrO0OSbq3U5VWfCY8FB4NsM43BOWLZ7x4sfaMOC1XWv+IyvDdkLe6DhJx8KUw46KIAimYs9YROrp6l46Q==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@neon-rs/cli@0.1.73:
|
||||
resolution: {integrity: sha512-1kv8S/feB6UQWQQwsnGfkSkEBOtlFDNExnioL81E2BwvUWgjQPaseHgpi2EpWVgsPUgur5eBm4QowmlpWkD4/w==}
|
||||
hasBin: true
|
||||
optionalDependencies:
|
||||
'@cargo-messages/android-arm-eabi': 0.1.72
|
||||
'@cargo-messages/darwin-arm64': 0.1.72
|
||||
'@cargo-messages/darwin-x64': 0.1.72
|
||||
'@cargo-messages/linux-arm-gnueabihf': 0.1.72
|
||||
'@cargo-messages/linux-x64-gnu': 0.1.72
|
||||
'@cargo-messages/win32-arm64-msvc': 0.1.72
|
||||
'@cargo-messages/win32-x64-msvc': 0.1.72
|
||||
dev: true
|
||||
|
||||
/axios@0.26.1:
|
||||
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
|
||||
dependencies:
|
||||
follow-redirects: 1.15.6
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
|
||||
/follow-redirects@1.15.6:
|
||||
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
dev: false
|
||||
|
||||
/lodash.defaultsdeep@4.6.1:
|
||||
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
|
||||
dev: false
|
||||
|
||||
/revolt-api@0.7.15:
|
||||
resolution: {integrity: sha512-rWRnjBFvoXt/RHRxHg9KuP+x1Jk61KdsOpKOczVJg5tIqGDmqp/6QZHw5rJTtG+wCH3Ah3YKwY+MM5NHfg1W3Q==}
|
||||
dependencies:
|
||||
axios: 0.26.1
|
||||
lodash.defaultsdeep: 4.6.1
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
dev: false
|
||||
@@ -1,221 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use neon::prelude::*;
|
||||
use revolt_database::{Database, DatabaseInfo};
|
||||
|
||||
fn js_init(mut cx: FunctionContext) -> JsResult<JsUndefined> {
|
||||
// static INIT: OnceLock<()> = OnceLock::new();
|
||||
// if INIT.get().is_none() {
|
||||
// INIT.get_or_init(|| {
|
||||
// async_std::task::block_on(async {
|
||||
// revolt_config::configure!(api);
|
||||
|
||||
// match DatabaseInfo::Auto.connect().await {
|
||||
// Ok(db) => {
|
||||
// let authifier_db = db.clone().to_authifier().await.database;
|
||||
// revolt_database::tasks::start_workers(db, authifier_db);
|
||||
// Ok(())
|
||||
// }
|
||||
// Err(err) => Err(err),
|
||||
// }
|
||||
// })
|
||||
// .or_else(|err| cx.throw_error(err))
|
||||
// .unwrap();
|
||||
// });
|
||||
// }
|
||||
|
||||
Ok(cx.undefined())
|
||||
}
|
||||
|
||||
struct DatabaseBinding(Database, Channel);
|
||||
impl Finalize for DatabaseBinding {}
|
||||
impl DatabaseBinding {
|
||||
fn take(&self) -> (Database, Channel) {
|
||||
(self.0.clone(), self.1.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn js_database(mut cx: FunctionContext) -> JsResult<JsBox<DatabaseBinding>> {
|
||||
let db = async_std::task::block_on(DatabaseInfo::Auto.connect())
|
||||
.or_else(|err| cx.throw_error(err))?;
|
||||
|
||||
let channel = cx.channel();
|
||||
Ok(cx.boxed(DatabaseBinding(db, channel)))
|
||||
}
|
||||
|
||||
// Implementations for models
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Model {
|
||||
User(revolt_database::User),
|
||||
Error(revolt_result::Error),
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn give(&self) -> Model {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Finalize for Model {}
|
||||
|
||||
macro_rules! shim_boxed {
|
||||
($cx: ident, $name: ident, $model: ident, $( $variable: ident $type: ident $id: expr )+, $cmd: ident, $( $arg: expr, )+) => {
|
||||
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||
$(
|
||||
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||
)+
|
||||
|
||||
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||
let (deferred, promise) = cx.promise();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
let result = db.$cmd($($arg,)+).await;
|
||||
deferred.settle_with(&channel, move |mut cx| {
|
||||
Ok(cx.boxed(match result {
|
||||
Ok(value) => Model::$model(value),
|
||||
Err(error) => Model::Error(error)
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
$cx.export_function(stringify!($name), $name)?;
|
||||
};
|
||||
}
|
||||
|
||||
fn js_data(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
match cx.this::<JsBox<Model>>()?.give() {
|
||||
Model::Error(_) => neon_serde4::to_value(&mut cx, &None::<()>),
|
||||
Model::User(user) => neon_serde4::to_value(&mut cx, &user),
|
||||
}
|
||||
.or_else(|e| cx.throw_error(e.to_string()))
|
||||
}
|
||||
|
||||
fn js_error(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
let value = match cx.this::<JsBox<Model>>()?.give() {
|
||||
Model::Error(err) => Some(err),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
neon_serde4::to_value(&mut cx, &value).or_else(|e| cx.throw_error(e.to_string()))
|
||||
}
|
||||
|
||||
// Basic data implementation
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ResultBinding<T> {
|
||||
#[serde(flatten)]
|
||||
value: Option<T>,
|
||||
error: Option<revolt_result::Error>,
|
||||
}
|
||||
|
||||
macro_rules! shim {
|
||||
($cx: ident, $name: ident, $( $variable: ident $type: ident $id: expr )*, $( $model: ident $modelType: ident $modelId: expr )*, | $db: ident | $closure: expr, $( $arg: expr, )+) => {
|
||||
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||
$(
|
||||
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||
)*
|
||||
|
||||
$(
|
||||
let mut $model = if let Model::$modelType(value) = cx.argument::<JsBox<Model>>($modelId)?.give() {
|
||||
value
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
)*
|
||||
|
||||
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||
let (deferred, promise) = cx.promise();
|
||||
|
||||
async_std::task::spawn(async move {
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
let result = (|$db: $crate::Database| $closure)(db.clone()).await;
|
||||
deferred.settle_with(&channel, move |mut cx| {
|
||||
neon_serde4::to_value(
|
||||
&mut cx,
|
||||
&match result {
|
||||
Ok(value) => ResultBinding {
|
||||
value: Some(value),
|
||||
error: None,
|
||||
},
|
||||
Err(error) => ResultBinding {
|
||||
value: None,
|
||||
error: Some(error),
|
||||
},
|
||||
},
|
||||
)
|
||||
.or_else(|e| cx.throw_error(e.to_string()))
|
||||
})
|
||||
});
|
||||
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
$cx.export_function(stringify!($name), $name)?;
|
||||
};
|
||||
}
|
||||
|
||||
#[neon::main]
|
||||
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||
// initialise required background stuff
|
||||
cx.export_function("init", js_init)?;
|
||||
|
||||
// database & model stuff
|
||||
cx.export_function("database", js_database)?;
|
||||
cx.export_function("model_data", js_data)?;
|
||||
cx.export_function("model_error", js_error)?;
|
||||
|
||||
shim_boxed!(
|
||||
cx,
|
||||
database_fetch_user,
|
||||
User,
|
||||
user_id JsString 0,
|
||||
fetch_user,
|
||||
&user_id,
|
||||
);
|
||||
|
||||
shim_boxed!(
|
||||
cx,
|
||||
database_fetch_user_by_username,
|
||||
User,
|
||||
username JsString 0
|
||||
discriminator JsString 1,
|
||||
fetch_user_by_username,
|
||||
&username, &discriminator,
|
||||
);
|
||||
|
||||
// procedure calls
|
||||
shim!(
|
||||
cx,
|
||||
proc_channels_create_dm,
|
||||
user_a JsString 0
|
||||
user_b JsString 1,
|
||||
,
|
||||
|db| async move {
|
||||
let user_a = db.fetch_user(&user_a).await?;
|
||||
let user_b = db.fetch_user(&user_b).await?;
|
||||
revolt_database::Channel::create_dm(&db, &user_a, &user_b).await
|
||||
},
|
||||
&userA, &userB,
|
||||
);
|
||||
|
||||
shim!(
|
||||
cx,
|
||||
proc_users_suspend,
|
||||
duration JsNumber 1
|
||||
reason JsString 2,
|
||||
user User 0,
|
||||
|db| async move {
|
||||
let duration = duration as usize;
|
||||
user.suspend(&db, if duration == 0 { None } else { Some(duration) }, Some(reason.split('|').map(|x| x.to_owned()).collect())).await
|
||||
},
|
||||
&user,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
const Internal = require(".");
|
||||
|
||||
// playing around with class wrapper, not practical
|
||||
class Model {
|
||||
constructor(model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
data() {
|
||||
return Internal.model_data.bind(this.model)();
|
||||
}
|
||||
|
||||
error() {
|
||||
return Internal.model_error.bind(this.model)();
|
||||
}
|
||||
}
|
||||
|
||||
class User extends Model {
|
||||
constructor(db, user) {
|
||||
super(user);
|
||||
this.db = db;
|
||||
}
|
||||
}
|
||||
|
||||
class Database {
|
||||
constructor() {
|
||||
this.db = Internal.database();
|
||||
}
|
||||
|
||||
async fetchUser(userId) {
|
||||
return new User(
|
||||
this,
|
||||
await Internal.database_fetch_user.bind(this.db)(userId)
|
||||
);
|
||||
}
|
||||
|
||||
async fetchUserByUsername(username, discriminator) {
|
||||
return new User(
|
||||
this,
|
||||
await Internal.database_fetch_user_by_username.bind(this.db)(
|
||||
username,
|
||||
discriminator
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const db = new Database();
|
||||
db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data()));
|
||||
db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data()));
|
||||
db.fetchUserByUsername("dos", "1111").then((user) =>
|
||||
console.info(user.error())
|
||||
);
|
||||
6
crates/bindings/package-lock.json
generated
6
crates/bindings/package-lock.json
generated
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "bindings",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.8.2"
|
||||
version = "0.8.9"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -19,6 +19,7 @@ async-channel = "2.3.1"
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
regex = "1.11.1"
|
||||
|
||||
# serde
|
||||
bincode = "1.3.3"
|
||||
@@ -36,12 +37,12 @@ async-std = { version = "1.8.0", features = [
|
||||
] }
|
||||
|
||||
# core
|
||||
authifier = { version = "1.0.9" }
|
||||
authifier = { version = "1.0.15" }
|
||||
revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database" }
|
||||
revolt-permissions = { version = "0.8.2", path = "../core/permissions" }
|
||||
revolt-permissions = { version = "0.8.9", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use revolt_database::events::client::ReadyPayloadFields;
|
||||
use revolt_result::{create_error, Result};
|
||||
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
|
||||
#[derive(Debug)]
|
||||
pub enum ProtocolFormat {
|
||||
@@ -17,6 +23,7 @@ pub struct ProtocolConfiguration {
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
ready_payload_fields: ReadyPayloadFields,
|
||||
}
|
||||
|
||||
impl ProtocolConfiguration {
|
||||
@@ -25,11 +32,13 @@ impl ProtocolConfiguration {
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
ready_payload_fields: ReadyPayloadFields,
|
||||
) -> Self {
|
||||
Self {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
ready_payload_fields,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +95,8 @@ impl ProtocolConfiguration {
|
||||
}
|
||||
|
||||
/// Get ready payload fields
|
||||
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
|
||||
vec![
|
||||
ReadyPayloadFields::Users,
|
||||
ReadyPayloadFields::Servers,
|
||||
ReadyPayloadFields::Channels,
|
||||
ReadyPayloadFields::Members,
|
||||
ReadyPayloadFields::Emoji,
|
||||
]
|
||||
pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields {
|
||||
&self.ready_payload_fields
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +127,23 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
let mut protocol_version = 1;
|
||||
let mut format = ProtocolFormat::Json;
|
||||
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,
|
||||
voice_states: false,
|
||||
user_settings: Vec::new(),
|
||||
channel_unreads: false,
|
||||
policy_changes: false,
|
||||
}
|
||||
} else {
|
||||
ReadyPayloadFields::default()
|
||||
};
|
||||
|
||||
// Parse and map parameters from key-value to known variables.
|
||||
for (key, value) in params {
|
||||
@@ -139,6 +159,31 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
_ => {}
|
||||
},
|
||||
"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,
|
||||
"voice_states" => ready_payload_fields.voice_states = 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 +196,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
ready_payload_fields,
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use futures::future::join_all;
|
||||
use revolt_database::{
|
||||
events::client::{EventV1, ReadyPayloadFields},
|
||||
util::permissions::DatabasePermissionQuery,
|
||||
voice::get_channel_voice_state,
|
||||
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
@@ -16,8 +18,9 @@ use super::state::{Cache, State};
|
||||
impl Cache {
|
||||
/// Check whether the current user can view a channel
|
||||
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
|
||||
#[allow(deprecated)]
|
||||
match &channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Channel::TextChannel { server, .. } => {
|
||||
let member = self.members.get(server);
|
||||
let server = self.servers.get(server);
|
||||
let mut query =
|
||||
@@ -94,11 +97,25 @@ impl State {
|
||||
pub async fn generate_ready_payload(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
fields: Vec<ReadyPayloadFields>,
|
||||
fields: &ReadyPayloadFields,
|
||||
) -> Result<EventV1> {
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Fetch pending policy changes.
|
||||
let policy_changes = if user.bot.is_some() || !fields.policy_changes {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
db.fetch_policy_changes()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
// Find all relationships to the user.
|
||||
let mut user_ids: HashSet<String> = user
|
||||
.relations
|
||||
@@ -155,7 +172,7 @@ impl State {
|
||||
.await?;
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
|
||||
let emojis = if fields.emojis {
|
||||
Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
@@ -163,25 +180,56 @@ impl State {
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|emoji| emoji.into())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch user settings
|
||||
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
|
||||
.iter()
|
||||
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
|
||||
{
|
||||
Some(db.fetch_user_settings(&user.id, &keys).await?)
|
||||
let user_settings = if !fields.user_settings.is_empty() {
|
||||
Some(
|
||||
db.fetch_user_settings(&user.id, &fields.user_settings)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch channel unreads
|
||||
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
|
||||
Some(db.fetch_unreads(&user.id).await?)
|
||||
let channel_unreads = if fields.channel_unreads {
|
||||
Some(
|
||||
db.fetch_unreads(&user.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|unread| unread.into())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let voice_states = if fields.voice_states {
|
||||
// fetch voice states for all the channels we can see
|
||||
let mut voice_states = Vec::new();
|
||||
|
||||
for channel in channels.iter().filter(|c| {
|
||||
matches!(
|
||||
c,
|
||||
Channel::DirectMessage { .. }
|
||||
| Channel::Group { .. }
|
||||
| Channel::TextChannel { voice: Some(_), .. }
|
||||
)
|
||||
}) {
|
||||
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
|
||||
voice_states.push(voice_state)
|
||||
}
|
||||
}
|
||||
|
||||
Some(voice_states)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -198,13 +246,11 @@ impl State {
|
||||
.collect();
|
||||
|
||||
// Make all users appear from our perspective.
|
||||
let mut users: Vec<v0::User> = users
|
||||
.into_iter()
|
||||
.map(|other_user| {
|
||||
let is_online = online_ids.contains(&other_user.id);
|
||||
other_user.into_known(&user, is_online)
|
||||
})
|
||||
.collect();
|
||||
let mut users: Vec<v0::User> = join_all(users.into_iter().map(|other_user| async {
|
||||
let is_online = online_ids.contains(&other_user.id);
|
||||
other_user.into_known(&user, is_online).await
|
||||
}))
|
||||
.await;
|
||||
|
||||
// Make sure we see our own user correctly.
|
||||
users.push(user.into_self(true).await);
|
||||
@@ -228,31 +274,31 @@ impl State {
|
||||
for channel in &channels {
|
||||
self.insert_subscription(channel.id().to_string()).await;
|
||||
}
|
||||
|
||||
Ok(EventV1::Ready {
|
||||
users: if fields.contains(&ReadyPayloadFields::Users) {
|
||||
Some(users)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
servers: if fields.contains(&ReadyPayloadFields::Servers) {
|
||||
users: if fields.users { Some(users) } else { None },
|
||||
servers: if fields.servers {
|
||||
Some(servers.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
channels: if fields.contains(&ReadyPayloadFields::Channels) {
|
||||
channels: if fields.channels {
|
||||
Some(channels.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
members: if fields.contains(&ReadyPayloadFields::Members) {
|
||||
members: if fields.members {
|
||||
Some(members.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
voice_states,
|
||||
|
||||
emojis,
|
||||
user_settings,
|
||||
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
channel_unreads,
|
||||
|
||||
policy_changes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -265,19 +311,14 @@ impl State {
|
||||
|
||||
let id = &id.to_string();
|
||||
for (channel_id, channel) in &self.cache.channels {
|
||||
match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
if server == id {
|
||||
channel_ids.insert(channel_id.clone());
|
||||
if channel.server() == Some(id) {
|
||||
channel_ids.insert(channel_id.clone());
|
||||
|
||||
if self.cache.can_view_channel(db, channel).await {
|
||||
added_channels.push(channel_id.clone());
|
||||
} else {
|
||||
removed_channels.push(channel_id.clone());
|
||||
}
|
||||
}
|
||||
if self.cache.can_view_channel(db, channel).await {
|
||||
added_channels.push(channel_id.clone());
|
||||
} else {
|
||||
removed_channels.push(channel_id.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +486,7 @@ impl State {
|
||||
server,
|
||||
channels,
|
||||
emojis: _,
|
||||
voice_states: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone()).await;
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::env;
|
||||
use std::{env, sync::Arc};
|
||||
|
||||
use async_std::net::TcpListener;
|
||||
use revolt_presence::clear_region;
|
||||
use once_cell::sync::OnceCell;
|
||||
use revolt_database::voice::VoiceClient;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
@@ -12,6 +14,15 @@ pub mod events;
|
||||
mod database;
|
||||
mod websocket;
|
||||
|
||||
pub static VOICE_CLIENT: OnceCell<Arc<VoiceClient>> = OnceCell::new();
|
||||
|
||||
pub fn get_voice_client() -> Arc<VoiceClient> {
|
||||
VOICE_CLIENT
|
||||
.get()
|
||||
.expect("get_voice_client called before set")
|
||||
.clone()
|
||||
}
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
// Configure requirements for Bonfire.
|
||||
@@ -24,6 +35,8 @@ async fn main() {
|
||||
clear_region(None).await;
|
||||
}
|
||||
|
||||
VOICE_CLIENT.set(Arc::new(VoiceClient::from_revolt_config().await)).unwrap();
|
||||
|
||||
// Setup a TCP listener to accept WebSocket connections on.
|
||||
// By default, we bind to port 14703 on all interfaces.
|
||||
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into());
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::{RedisError, RedisErrorKind},
|
||||
error::RedisErrorKind,
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
};
|
||||
@@ -17,6 +17,7 @@ use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
||||
use revolt_config::report_internal_error;
|
||||
use revolt_database::{
|
||||
events::{client::EventV1, server::ClientMessage},
|
||||
iso8601_timestamp::Timestamp,
|
||||
Database, User, UserHint,
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
@@ -100,6 +101,10 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
|
||||
info!("User {addr:?} authenticated as @{}", user.username);
|
||||
|
||||
db.update_session_last_seen(&session_id, Timestamp::now_utc())
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Create local state.
|
||||
let mut state = State::from(user, session_id);
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
22
crates/core/coalesced/Cargo.toml
Normal file
22
crates/core/coalesced/Cargo.toml
Normal file
@@ -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"] }
|
||||
9
crates/core/coalesced/LICENSE
Normal file
9
crates/core/coalesced/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
24
crates/core/coalesced/src/config.rs
Normal file
24
crates/core/coalesced/src/config.rs
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
crates/core/coalesced/src/error.rs
Normal file
27
crates/core/coalesced/src/error.rs
Normal file
@@ -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 {}
|
||||
39
crates/core/coalesced/src/lib.rs
Normal file
39
crates/core/coalesced/src/lib.rs
Normal file
@@ -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;
|
||||
208
crates/core/coalesced/src/service.rs
Normal file
208
crates/core/coalesced/src/service.rs
Normal file
@@ -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]
|
||||
name = "revolt-config"
|
||||
version = "0.8.2"
|
||||
version = "0.8.9"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -9,9 +9,11 @@ description = "Revolt Backend: Configuration"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
anyhow = ["dep:sentry-anyhow"]
|
||||
report-macros = ["revolt-result"]
|
||||
sentry = ["dep:sentry"]
|
||||
test = ["async-std"]
|
||||
default = ["test"]
|
||||
default = ["test", "sentry"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
@@ -31,7 +33,8 @@ log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
# Sentry
|
||||
sentry = "0.31.5"
|
||||
sentry = { version = "0.31.5", optional = true }
|
||||
sentry-anyhow = { version = "0.38.1", optional = true }
|
||||
|
||||
# Core
|
||||
revolt-result = { version = "0.8.2", path = "../result", optional = true }
|
||||
revolt-result = { version = "0.8.9", path = "../result", optional = true }
|
||||
|
||||
@@ -7,3 +7,6 @@ host = "127.0.0.1"
|
||||
port = 5672
|
||||
username = "rabbituser"
|
||||
password = "rabbitpass"
|
||||
|
||||
[features]
|
||||
webhooks_enabled = true
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
production = false
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
# Defaults to the container name specified in self-hosted
|
||||
@@ -20,6 +22,8 @@ january = "http://local.revolt.chat/january"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
[hosts.livekit]
|
||||
|
||||
[rabbit]
|
||||
host = "rabbit"
|
||||
port = 5672
|
||||
@@ -52,6 +56,10 @@ authifier_shield_key = ""
|
||||
voso_legacy_token = ""
|
||||
# Whether services are behind the Cloudflare network
|
||||
trust_cloudflare = false
|
||||
# easypwned endpoint
|
||||
easypwned = ""
|
||||
# Tenor API Key
|
||||
tenor_key = ""
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
@@ -62,19 +70,34 @@ hcaptcha_sitekey = ""
|
||||
# Maximum concurrent connections (to proxy server)
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[api.livekit]
|
||||
# How long to ring devices for when calling in dms/groups, in seconds
|
||||
call_ring_duration = 30
|
||||
|
||||
[api.livekit.nodes]
|
||||
|
||||
[api.users]
|
||||
|
||||
[pushd]
|
||||
# this changes the names of the queues to not overlap
|
||||
# prod/beta if they happen to be on the same exchange/instance.
|
||||
# Usually they have to be, so that messages sent from one or the other get sent to everyone
|
||||
production = true
|
||||
|
||||
# Changes how many users are processed in each chunk when resolving role/everyone mentions.
|
||||
# Increasing this will resolve mentions faster, but will consume more memory while resolving.
|
||||
mass_mention_chunk_size = 200
|
||||
|
||||
# none of these should need changing
|
||||
exchange = "revolt.notifications"
|
||||
message_queue = "notifications.origin.message"
|
||||
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
|
||||
fr_received_queue = "notifications.ingest.fr_received" # friend request received
|
||||
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
|
||||
ack_queue = "notifications.process.ack" # updates badges for apple devices
|
||||
mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions
|
||||
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
|
||||
fr_received_queue = "notifications.ingest.fr_received" # friend request received
|
||||
dm_call_queue = "notifications.ingest.dm_call" # friend request received
|
||||
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
|
||||
ack_queue = "notifications.process.ack" # updates badges for apple devices
|
||||
|
||||
|
||||
[pushd.vapid]
|
||||
queue = "notifications.outbound.vapid"
|
||||
@@ -171,9 +194,13 @@ default_bucket = "revolt-uploads"
|
||||
|
||||
|
||||
[features]
|
||||
# Bucket to upload to by default
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
# Enable push notifications for mass pings (everyone, online, roles)
|
||||
# When false this will still ping in-client but will not send notifications from pushd
|
||||
mass_mentions_send_notifications = true
|
||||
# Can role/everyone pings be used at all
|
||||
mass_mentions_enabled = true
|
||||
|
||||
[features.limits]
|
||||
|
||||
@@ -211,6 +238,18 @@ message_attachments = 5
|
||||
# Maximum number of servers the user can create/join
|
||||
servers = 50
|
||||
|
||||
# Maximum audio frequency (Hz) in voice calls
|
||||
voice_quality = 16000
|
||||
|
||||
# Whether the user can use video streams in voice calls
|
||||
video = true
|
||||
|
||||
# Mamimum resolution (width, height) of video streams in voice calls
|
||||
video_resolution = [1080, 720]
|
||||
|
||||
# Minimum and maximum aspect ratio of video streams in voice calls
|
||||
video_aspect_ratio = [0.3, 2.5]
|
||||
|
||||
[features.limits.new_user.file_upload_size_limit]
|
||||
# Maximum file size limits (in bytes)
|
||||
attachments = 20_000_000
|
||||
@@ -238,6 +277,18 @@ message_attachments = 5
|
||||
# Maximum number of servers the user can create/join
|
||||
servers = 100
|
||||
|
||||
# Maximum audio frequency (Hz) in voice calls
|
||||
voice_quality = 16000
|
||||
|
||||
# Whether the user can use video streams in voice calls
|
||||
video = true
|
||||
|
||||
# Mamimum resolution (width, height) of video streams in voice calls
|
||||
video_resolution = [1080, 720]
|
||||
|
||||
# Minimum and maximum aspect ratio of video streams in voice calls
|
||||
video_aspect_ratio = [0.3, 2.5]
|
||||
|
||||
[features.limits.default.file_upload_size_limit]
|
||||
# Maximum file size limits (in bytes)
|
||||
attachments = 20_000_000
|
||||
@@ -256,6 +307,9 @@ process_message_delay_limit = 5
|
||||
# Configuration for Sentry error reporting
|
||||
api = ""
|
||||
events = ""
|
||||
voice_ingress = ""
|
||||
files = ""
|
||||
proxy = ""
|
||||
pushd = ""
|
||||
crond = ""
|
||||
gifbox = ""
|
||||
@@ -6,9 +6,12 @@ use futures_locks::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
pub use sentry::{capture_error, capture_message, Level};
|
||||
#[cfg(feature = "anyhow")]
|
||||
pub use sentry_anyhow::capture_anyhow;
|
||||
|
||||
#[cfg(feature = "report-macros")]
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! report_error {
|
||||
( $expr: expr, $error: ident $( $tt:tt )? ) => {
|
||||
@@ -23,7 +26,7 @@ macro_rules! report_error {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "report-macros")]
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! capture_internal_error {
|
||||
( $expr: expr ) => {
|
||||
@@ -34,7 +37,7 @@ macro_rules! capture_internal_error {
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "report-macros")]
|
||||
#[cfg(all(feature = "report-macros", feature = "sentry"))]
|
||||
#[macro_export]
|
||||
macro_rules! report_internal_error {
|
||||
( $expr: expr ) => {
|
||||
@@ -59,6 +62,9 @@ static CONFIG_SEARCH_PATHS: [&str; 3] = [
|
||||
"/Revolt.toml",
|
||||
];
|
||||
|
||||
/// Path to search for test overrides
|
||||
static TEST_OVERRIDE_PATH: &str = "Revolt.test-overrides.toml";
|
||||
|
||||
/// Configuration builder
|
||||
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
RwLock::new({
|
||||
@@ -72,6 +78,20 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
include_str!("../Revolt.test.toml"),
|
||||
FileFormat::Toml,
|
||||
));
|
||||
|
||||
// recursively search upwards for an overrides file (if there is one)
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
let mut path = Some(cwd.as_path());
|
||||
while let Some(current_path) = path {
|
||||
let target_path = current_path.join(TEST_OVERRIDE_PATH);
|
||||
if target_path.exists() {
|
||||
builder = builder
|
||||
.add_source(File::new(target_path.to_str().unwrap(), FileFormat::Toml));
|
||||
}
|
||||
|
||||
path = current_path.parent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for path in CONFIG_SEARCH_PATHS {
|
||||
@@ -105,8 +125,7 @@ pub struct Hosts {
|
||||
pub events: String,
|
||||
pub autumn: String,
|
||||
pub january: String,
|
||||
pub voso_legacy: String,
|
||||
pub voso_legacy_ws: String,
|
||||
pub livekit: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -123,6 +142,7 @@ pub struct ApiSmtp {
|
||||
pub reply_to: Option<String>,
|
||||
pub port: Option<i32>,
|
||||
pub use_tls: Option<bool>,
|
||||
pub use_starttls: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -168,6 +188,8 @@ pub struct ApiSecurity {
|
||||
pub voso_legacy_token: String,
|
||||
pub captcha: ApiSecurityCaptcha,
|
||||
pub trust_cloudflare: bool,
|
||||
pub easypwned: String,
|
||||
pub tenor_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -175,19 +197,50 @@ pub struct ApiWorkers {
|
||||
pub max_concurrent_connections: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiLiveKit {
|
||||
pub call_ring_duration: usize,
|
||||
pub nodes: HashMap<String, LiveKitNode>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct LiveKitNode {
|
||||
pub url: String,
|
||||
pub lat: f64,
|
||||
pub lon: f64,
|
||||
pub key: String,
|
||||
pub secret: String,
|
||||
|
||||
// whether to hide the node in the nodes list
|
||||
#[serde(default)]
|
||||
pub private: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiUsers {
|
||||
pub early_adopter_cutoff: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Api {
|
||||
pub registration: ApiRegistration,
|
||||
pub smtp: ApiSmtp,
|
||||
pub security: ApiSecurity,
|
||||
pub workers: ApiWorkers,
|
||||
pub livekit: ApiLiveKit,
|
||||
pub users: ApiUsers,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Pushd {
|
||||
pub production: bool,
|
||||
pub exchange: String,
|
||||
pub mass_mention_chunk_size: usize,
|
||||
|
||||
// Queues
|
||||
pub message_queue: String,
|
||||
pub mass_mention_queue: String,
|
||||
pub dm_call_queue: String,
|
||||
pub fr_accepted_queue: String,
|
||||
pub fr_received_queue: String,
|
||||
pub generic_queue: String,
|
||||
@@ -214,6 +267,14 @@ impl Pushd {
|
||||
self.get_routing_key(self.message_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_mass_mention_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.mass_mention_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_dm_call_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.dm_call_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_fr_accepted_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.fr_accepted_queue.clone())
|
||||
}
|
||||
@@ -281,6 +342,10 @@ pub struct FeaturesLimits {
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
pub voice_quality: u32,
|
||||
pub video: bool,
|
||||
pub video_resolution: [u32; 2],
|
||||
pub video_aspect_ratio: [f32; 2],
|
||||
|
||||
pub file_upload_size_limit: HashMap<String, usize>,
|
||||
}
|
||||
@@ -314,6 +379,9 @@ impl Default for FeaturesAdvanced {
|
||||
pub struct Features {
|
||||
pub limits: FeaturesLimitsCollection,
|
||||
pub webhooks_enabled: bool,
|
||||
pub mass_mentions_send_notifications: bool,
|
||||
pub mass_mentions_enabled: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub advanced: FeaturesAdvanced,
|
||||
}
|
||||
@@ -322,9 +390,12 @@ pub struct Features {
|
||||
pub struct Sentry {
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
pub voice_ingress: String,
|
||||
pub files: String,
|
||||
pub proxy: String,
|
||||
pub pushd: String,
|
||||
pub crond: String,
|
||||
pub gifbox: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -337,6 +408,7 @@ pub struct Settings {
|
||||
pub files: Files,
|
||||
pub features: Features,
|
||||
pub sentry: Sentry,
|
||||
pub production: bool,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -364,10 +436,23 @@ pub async fn read() -> Config {
|
||||
|
||||
#[cached(time = 30)]
|
||||
pub async fn config() -> Settings {
|
||||
read().await.try_deserialize::<Settings>().unwrap()
|
||||
let mut config = read().await.try_deserialize::<Settings>().unwrap();
|
||||
|
||||
// inject REDIS_URI for redis-kiss library
|
||||
if std::env::var("REDIS_URL").is_err() {
|
||||
std::env::set_var("REDIS_URI", config.database.redis.clone());
|
||||
}
|
||||
|
||||
// auto-detect production nodes
|
||||
if config.hosts.api.contains("https") && config.hosts.api.contains("revolt.chat") {
|
||||
config.production = true;
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
/// Configure logging and common Rust variables
|
||||
#[cfg(feature = "sentry")]
|
||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
@@ -377,12 +462,6 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
|
||||
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
|
||||
}
|
||||
|
||||
if std::env::var("REDIS_URL").is_err() {
|
||||
// Configure redis-kiss library
|
||||
let config = config().await;
|
||||
std::env::set_var("REDIS_URI", config.database.redis);
|
||||
}
|
||||
|
||||
pretty_env_logger::init();
|
||||
log::info!("Starting {release}");
|
||||
|
||||
@@ -399,6 +478,7 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
#[macro_export]
|
||||
macro_rules! configure {
|
||||
($application: ident) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.8.2"
|
||||
version = "0.8.9"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -10,12 +10,12 @@ description = "Revolt Backend: Database Implementation"
|
||||
|
||||
[features]
|
||||
# Databases
|
||||
mongodb = ["dep:mongodb", "bson"]
|
||||
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
|
||||
|
||||
# ... Other
|
||||
tasks = ["isahc", "linkify", "url-escape"]
|
||||
async-std-runtime = ["async-std"]
|
||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
||||
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
|
||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
|
||||
axum-impl = ["axum"]
|
||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||
|
||||
@@ -24,18 +24,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.8.2", path = "../config", features = [
|
||||
revolt-config = { version = "0.8.9", path = "../config", features = [
|
||||
"report-macros",
|
||||
] }
|
||||
revolt-result = { version = "0.8.2", path = "../result" }
|
||||
revolt-models = { version = "0.8.2", path = "../models", features = [
|
||||
revolt-result = { version = "0.8.9", path = "../result" }
|
||||
revolt-models = { version = "0.8.9", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.8.2", path = "../presence" }
|
||||
revolt-permissions = { version = "0.8.2", path = "../permissions", features = [
|
||||
revolt-presence = { version = "0.8.9", path = "../presence" }
|
||||
revolt-permissions = { version = "0.8.9", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
revolt-parser = { version = "0.8.9", path = "../parser" }
|
||||
|
||||
# Utility
|
||||
log = "0.4"
|
||||
@@ -64,7 +65,7 @@ redis-kiss = { version = "0.1.4" }
|
||||
|
||||
# Database
|
||||
bson = { optional = true, version = "2.1.0" }
|
||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
||||
mongodb = { optional = true, version = "3.1.0" }
|
||||
|
||||
# Database Migration
|
||||
unicode-segmentation = "1.10.1"
|
||||
@@ -90,13 +91,13 @@ rocket = { version = "0.5.1", default-features = false, features = [
|
||||
revolt_okapi = { version = "0.9.1", 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 = { version = "1.0.9", features = ["rocket_impl"] }
|
||||
authifier = { version = "1.0.15" }
|
||||
|
||||
# RabbitMQ
|
||||
amqprs = { version = "1.7.0" }
|
||||
|
||||
# Voice
|
||||
livekit-api = "0.4.4"
|
||||
livekit-protocol = "0.4.0"
|
||||
livekit-runtime = { version = "0.3.1", features = ["tokio"] }
|
||||
|
||||
@@ -3,18 +3,21 @@
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Member",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "Member",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0002"
|
||||
},
|
||||
{
|
||||
@@ -23,6 +26,9 @@
|
||||
"channel_type": "Group",
|
||||
"name": "My Group",
|
||||
"owner": "__ID:0__",
|
||||
"recipients": ["__ID:0__", "__ID:1__"]
|
||||
"recipients": [
|
||||
"__ID:0__",
|
||||
"__ID:1__"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -3,18 +3,21 @@
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:0__",
|
||||
"username": "Owner",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:1__",
|
||||
"username": "Moderator",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
"_object_type": "User",
|
||||
"_id": "__ID:2__",
|
||||
"username": "User",
|
||||
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
|
||||
"discriminator": "0001"
|
||||
},
|
||||
{
|
||||
@@ -39,7 +42,9 @@
|
||||
"_id": "__ID:4__",
|
||||
"owner": "__ID:0__",
|
||||
"name": "Server",
|
||||
"channels": ["__ID:3__"],
|
||||
"channels": [
|
||||
"__ID:3__"
|
||||
],
|
||||
"roles": {
|
||||
"__ID:5__": {
|
||||
"name": "Moderator",
|
||||
@@ -47,7 +52,7 @@
|
||||
"a": 545270208,
|
||||
"d": 0
|
||||
},
|
||||
"rank": 3
|
||||
"rank": 1
|
||||
},
|
||||
"__ID:6__": {
|
||||
"name": "Owner",
|
||||
@@ -66,7 +71,9 @@
|
||||
"user": "__ID:0__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": ["__ID:6__"],
|
||||
"roles": [
|
||||
"__ID:6__"
|
||||
],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
@@ -75,7 +82,9 @@
|
||||
"user": "__ID:1__",
|
||||
"server": "__ID:4__"
|
||||
},
|
||||
"roles": ["__ID:5__"],
|
||||
"roles": [
|
||||
"__ID:5__"
|
||||
],
|
||||
"joined_at": 1698318340195
|
||||
},
|
||||
{
|
||||
@@ -86,4 +95,4 @@
|
||||
},
|
||||
"joined_at": 1698318340195
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -2,7 +2,8 @@ use std::collections::HashSet;
|
||||
|
||||
use crate::events::rabbit::*;
|
||||
use crate::User;
|
||||
use amqprs::channel::BasicPublishArguments;
|
||||
use amqprs::channel::{BasicPublishArguments, ExchangeDeclareArguments};
|
||||
use amqprs::connection::OpenConnectionArguments;
|
||||
use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError};
|
||||
use amqprs::{BasicProperties, FieldTable};
|
||||
use revolt_models::v0::PushNotification;
|
||||
@@ -25,6 +26,35 @@ impl AMQP {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_auto() -> AMQP {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let connection = Connection::open(&OpenConnectionArguments::new(
|
||||
&config.rabbit.host,
|
||||
config.rabbit.port,
|
||||
&config.rabbit.username,
|
||||
&config.rabbit.password,
|
||||
))
|
||||
.await
|
||||
.expect("Failed to connect to RabbitMQ");
|
||||
|
||||
let channel = connection
|
||||
.open_channel(None)
|
||||
.await
|
||||
.expect("Failed to open RabbitMQ channel");
|
||||
|
||||
channel
|
||||
.exchange_declare(
|
||||
ExchangeDeclareArguments::new(&config.pushd.exchange, "direct")
|
||||
.durable(true)
|
||||
.finish(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to declare exchange");
|
||||
|
||||
AMQP::new(connection, channel)
|
||||
}
|
||||
|
||||
pub async fn friend_request_accepted(
|
||||
&self,
|
||||
accepted_request_user: &User,
|
||||
@@ -170,6 +200,38 @@ impl AMQP {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn mass_mention_message_sent(
|
||||
&self,
|
||||
server_id: String,
|
||||
payload: Vec<PushNotification>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = MassMessageSentPayload {
|
||||
notifications: payload,
|
||||
server_id,
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
let routing_key = config.pushd.get_mass_mention_routing_key();
|
||||
|
||||
debug!(
|
||||
"Sending mass mention payload on channel {}: {}",
|
||||
routing_key, payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(&config.pushd.exchange, routing_key.as_str()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn ack_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
@@ -208,4 +270,50 @@ impl AMQP {
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// # DM Call Update
|
||||
/// Used to send an update about a DM call, eg. start or end of a call.
|
||||
/// Recipients can be used to narrow the scope of recipients, otherwise all recipients will be notified.
|
||||
/// `ended` refers to the ringing period, not necessarily the call itself.
|
||||
pub async fn dm_call_updated(
|
||||
&self,
|
||||
initiator_id: &str,
|
||||
channel_id: &str,
|
||||
started_at: Option<&str>,
|
||||
ended: bool,
|
||||
recipients: Option<Vec<String>>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = InternalDmCallPayload {
|
||||
payload: DmCallPayload {
|
||||
initiator_id: initiator_id.to_string(),
|
||||
channel_id: channel_id.to_string(),
|
||||
started_at: started_at.map(|f| f.to_string()),
|
||||
ended,
|
||||
},
|
||||
recipients,
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending dm call update payload on channel {}: {}",
|
||||
config.pushd.get_dm_call_routing_key(),
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_dm_call_routing_key(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use authifier::config::Captcha;
|
||||
use authifier::config::EmailVerificationConfig;
|
||||
use authifier::config::PasswordScanning;
|
||||
use authifier::config::ResolveIp;
|
||||
use authifier::config::SMTPSettings;
|
||||
use authifier::config::Shield;
|
||||
@@ -12,6 +14,7 @@ use authifier::Authifier;
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use self::mongodb::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
@@ -24,17 +27,20 @@ pub enum DatabaseInfo {
|
||||
/// Use the mock database
|
||||
Reference,
|
||||
/// Connect to MongoDB
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDb { uri: String, database_name: String },
|
||||
/// Use existing MongoDB connection
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDbFromClient(::mongodb::Client, String),
|
||||
}
|
||||
|
||||
/// Database
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Database {
|
||||
/// Mock database
|
||||
Reference(ReferenceDb),
|
||||
/// MongoDB database
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDb(MongoDb),
|
||||
}
|
||||
|
||||
@@ -44,7 +50,7 @@ impl DatabaseInfo {
|
||||
pub async fn connect(self) -> Result<Database, String> {
|
||||
let config = config().await;
|
||||
|
||||
Ok(match self {
|
||||
match self {
|
||||
DatabaseInfo::Auto => {
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
DatabaseInfo::Test(format!(
|
||||
@@ -52,16 +58,20 @@ impl DatabaseInfo {
|
||||
rand::thread_rng().gen_range(1_000_000..10_000_000)
|
||||
))
|
||||
.connect()
|
||||
.await?
|
||||
.await
|
||||
} else if !config.database.mongodb.is_empty() {
|
||||
DatabaseInfo::MongoDb {
|
||||
#[cfg(feature = "mongodb")]
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name: "revolt".to_string(),
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
.await;
|
||||
|
||||
#[cfg(not(feature = "mongodb"))]
|
||||
return Err("MongoDB not enabled.".to_string())
|
||||
} else {
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
DatabaseInfo::Reference.connect().await
|
||||
}
|
||||
}
|
||||
DatabaseInfo::Test(database_name) => {
|
||||
@@ -69,30 +79,36 @@ impl DatabaseInfo {
|
||||
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
|
||||
.as_str()
|
||||
{
|
||||
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
|
||||
"REFERENCE" => DatabaseInfo::Reference.connect().await,
|
||||
"MONGODB" => {
|
||||
DatabaseInfo::MongoDb {
|
||||
#[cfg(feature = "mongodb")]
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri: config.database.mongodb,
|
||||
database_name,
|
||||
}
|
||||
.connect()
|
||||
.await?
|
||||
.await;
|
||||
|
||||
#[cfg(not(feature = "mongodb"))]
|
||||
return Err("MongoDB not enabled.".to_string())
|
||||
}
|
||||
_ => 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 } => {
|
||||
let client = ::mongodb::Client::with_uri_str(uri)
|
||||
.await
|
||||
.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) => {
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
Ok(Database::MongoDb(MongoDb(client, database_name)))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +118,13 @@ impl Database {
|
||||
let config = config().await;
|
||||
|
||||
let mut auth_config = authifier::Config {
|
||||
password_scanning: if config.api.security.easypwned.is_empty() {
|
||||
Default::default()
|
||||
} else {
|
||||
PasswordScanning::EasyPwned {
|
||||
endpoint: config.api.security.easypwned,
|
||||
}
|
||||
},
|
||||
email_verification: if !config.api.smtp.host.is_empty() {
|
||||
EmailVerificationConfig::Enabled {
|
||||
smtp: SMTPSettings {
|
||||
@@ -118,28 +141,69 @@ impl Database {
|
||||
),
|
||||
port: config.api.smtp.port,
|
||||
use_tls: config.api.smtp.use_tls,
|
||||
use_starttls: config.api.smtp.use_starttls,
|
||||
},
|
||||
expiry: Default::default(),
|
||||
templates: Templates {
|
||||
verify: Template {
|
||||
title: "Verify your Revolt account.".into(),
|
||||
text: include_str!("../../templates/verify.txt").into(),
|
||||
url: format!("{}/login/verify/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/verify.html").into()),
|
||||
},
|
||||
reset: Template {
|
||||
title: "Reset your Revolt password.".into(),
|
||||
text: include_str!("../../templates/reset.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/reset.html").into()),
|
||||
},
|
||||
deletion: Template {
|
||||
title: "Confirm account deletion.".into(),
|
||||
text: include_str!("../../templates/deletion.txt").into(),
|
||||
url: format!("{}/delete/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/deletion.html").into()),
|
||||
},
|
||||
welcome: None,
|
||||
templates: if config.production {
|
||||
Templates {
|
||||
verify: Template {
|
||||
title: "Verify your Revolt account.".into(),
|
||||
text: include_str!("../../templates/verify.txt").into(),
|
||||
url: format!("{}/login/verify/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/verify.html").into()),
|
||||
},
|
||||
reset: Template {
|
||||
title: "Reset your Revolt password.".into(),
|
||||
text: include_str!("../../templates/reset.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/reset.html").into()),
|
||||
},
|
||||
reset_existing: Template {
|
||||
title: "You already have a Revolt account, reset your password."
|
||||
.into(),
|
||||
text: include_str!("../../templates/reset-existing.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: Some(
|
||||
include_str!("../../templates/reset-existing.html").into(),
|
||||
),
|
||||
},
|
||||
deletion: Template {
|
||||
title: "Confirm account deletion.".into(),
|
||||
text: include_str!("../../templates/deletion.txt").into(),
|
||||
url: format!("{}/delete/", config.hosts.app),
|
||||
html: Some(include_str!("../../templates/deletion.html").into()),
|
||||
},
|
||||
welcome: None,
|
||||
}
|
||||
} else {
|
||||
Templates {
|
||||
verify: Template {
|
||||
title: "Verify your account.".into(),
|
||||
text: include_str!("../../templates/verify.whitelabel.txt").into(),
|
||||
url: format!("{}/login/verify/", config.hosts.app),
|
||||
html: None,
|
||||
},
|
||||
reset: Template {
|
||||
title: "Reset your password.".into(),
|
||||
text: include_str!("../../templates/reset.whitelabel.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: None,
|
||||
},
|
||||
reset_existing: Template {
|
||||
title: "Reset your password.".into(),
|
||||
text: include_str!("../../templates/reset.whitelabel.txt").into(),
|
||||
url: format!("{}/login/reset/", config.hosts.app),
|
||||
html: None,
|
||||
},
|
||||
deletion: Template {
|
||||
title: "Confirm account deletion.".into(),
|
||||
text: include_str!("../../templates/deletion.whitelabel.txt")
|
||||
.into(),
|
||||
url: format!("{}/delete/", config.hosts.app),
|
||||
html: None,
|
||||
},
|
||||
welcome: None,
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@@ -170,12 +234,16 @@ impl Database {
|
||||
Authifier {
|
||||
database: match self {
|
||||
Database::Reference(_) => Default::default(),
|
||||
#[cfg(feature = "mongodb")]
|
||||
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
|
||||
authifier::database::MongoDb(client.database("revolt")),
|
||||
),
|
||||
},
|
||||
config: auth_config,
|
||||
#[cfg(feature = "tasks")]
|
||||
event_channel: Some(crate::tasks::authifier_relay::sender()),
|
||||
#[cfg(not(feature = "tasks"))]
|
||||
event_channel: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
database_derived!(
|
||||
#[cfg(feature = "mongodb")]
|
||||
/// MongoDB implementation
|
||||
#[derive(Debug)]
|
||||
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
||||
);
|
||||
|
||||
@@ -31,17 +31,17 @@ impl MongoDb {
|
||||
}
|
||||
|
||||
/// Get a collection by its name
|
||||
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
|
||||
pub fn col<T: Send + Sync>(&self, collection: &str) -> mongodb::Collection<T> {
|
||||
self.db().collection(collection)
|
||||
}
|
||||
|
||||
/// Insert one document into a collection
|
||||
pub async fn insert_one<T: Serialize>(
|
||||
pub async fn insert_one<T: Serialize + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
document: T,
|
||||
) -> Result<InsertOneResult> {
|
||||
self.col::<T>(collection).insert_one(document, None).await
|
||||
self.col::<T>(collection).insert_one(document).await
|
||||
}
|
||||
|
||||
/// Count documents by projection
|
||||
@@ -51,7 +51,7 @@ impl MongoDb {
|
||||
projection: Document,
|
||||
) -> Result<u64> {
|
||||
self.col::<Document>(collection)
|
||||
.count_documents(projection, None)
|
||||
.count_documents(projection)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ impl MongoDb {
|
||||
{
|
||||
Ok(self
|
||||
.col::<T>(collection)
|
||||
.find(projection, options)
|
||||
.find(projection)
|
||||
.with_options(options)
|
||||
.await?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
@@ -101,7 +102,8 @@ impl MongoDb {
|
||||
O: Into<Option<FindOneOptions>>,
|
||||
{
|
||||
self.col::<T>(collection)
|
||||
.find_one(projection, options)
|
||||
.find_one(projection)
|
||||
.with_options(options)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -165,7 +167,7 @@ impl MongoDb {
|
||||
};
|
||||
|
||||
self.col::<Document>(collection)
|
||||
.update_one(projection, query, None)
|
||||
.update_one(projection, query)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -200,7 +202,7 @@ impl MongoDb {
|
||||
projection: Document,
|
||||
) -> Result<DeleteResult> {
|
||||
self.col::<Document>(collection)
|
||||
.delete_one(projection, None)
|
||||
.delete_one(projection)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
|
||||
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
|
||||
UserSettings, Webhook,
|
||||
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
|
||||
User, UserSettings, Webhook,
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ReferenceDb {
|
||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
@@ -21,6 +21,7 @@ database_derived!(
|
||||
pub file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
|
||||
pub files: Arc<Mutex<HashMap<String, File>>>,
|
||||
pub messages: Arc<Mutex<HashMap<String, Message>>>,
|
||||
pub policy_changes: Arc<Mutex<HashMap<String, PolicyChange>>>,
|
||||
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
|
||||
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
||||
|
||||
@@ -3,10 +3,7 @@ use revolt_result::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage,
|
||||
FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message,
|
||||
PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser,
|
||||
PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, Webhook,
|
||||
AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook
|
||||
};
|
||||
|
||||
use crate::Database;
|
||||
@@ -20,16 +17,33 @@ pub enum Ping {
|
||||
}
|
||||
|
||||
/// Fields provided in Ready payload
|
||||
#[derive(PartialEq)]
|
||||
pub enum ReadyPayloadFields {
|
||||
Users,
|
||||
Servers,
|
||||
Channels,
|
||||
Members,
|
||||
Emoji,
|
||||
#[derive(PartialEq, Debug, Clone, Deserialize)]
|
||||
pub struct ReadyPayloadFields {
|
||||
pub users: bool,
|
||||
pub servers: bool,
|
||||
pub channels: bool,
|
||||
pub members: bool,
|
||||
pub emojis: bool,
|
||||
pub voice_states: bool,
|
||||
pub user_settings: Vec<String>,
|
||||
pub channel_unreads: bool,
|
||||
pub policy_changes: bool,
|
||||
}
|
||||
|
||||
UserSettings(Vec<String>),
|
||||
ChannelUnreads,
|
||||
impl Default for ReadyPayloadFields {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
users: true,
|
||||
servers: true,
|
||||
channels: true,
|
||||
members: true,
|
||||
emojis: true,
|
||||
voice_states: true,
|
||||
user_settings: Vec::new(),
|
||||
channel_unreads: false,
|
||||
policy_changes: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol Events
|
||||
@@ -57,11 +71,16 @@ pub enum EventV1 {
|
||||
members: Option<Vec<Member>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voice_states: Option<Vec<ChannelVoiceState>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_settings: Option<UserSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
policy_changes: Option<Vec<PolicyChange>>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
@@ -120,6 +139,7 @@ pub enum EventV1 {
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
emojis: Vec<Emoji>,
|
||||
voice_states: Vec<ChannelVoiceState>
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
@@ -142,7 +162,13 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// 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
|
||||
ServerMemberLeave {
|
||||
@@ -163,6 +189,9 @@ pub enum EventV1 {
|
||||
/// Server role deleted
|
||||
ServerRoleDelete { id: String, role_id: String },
|
||||
|
||||
/// Server roles ranks updated
|
||||
ServerRoleRanksUpdate { id: String, ranks: Vec<String> },
|
||||
|
||||
/// Update existing user
|
||||
UserUpdate {
|
||||
id: String,
|
||||
@@ -243,6 +272,31 @@ pub enum EventV1 {
|
||||
|
||||
/// Auth events
|
||||
Auth(AuthifierEvent),
|
||||
|
||||
/// Voice events
|
||||
VoiceChannelJoin {
|
||||
id: String,
|
||||
state: UserVoiceState,
|
||||
},
|
||||
VoiceChannelLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
},
|
||||
VoiceChannelMove {
|
||||
user: String,
|
||||
from: String,
|
||||
to: String,
|
||||
state: UserVoiceState
|
||||
},
|
||||
UserVoiceStateUpdate {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
data: PartialUserVoiceState,
|
||||
},
|
||||
UserMoveVoiceChannel {
|
||||
node: String,
|
||||
token: String
|
||||
}
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
|
||||
@@ -11,6 +11,12 @@ pub struct MessageSentPayload {
|
||||
pub users: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MassMessageSentPayload {
|
||||
pub notifications: Vec<PushNotification>,
|
||||
pub server_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct FRAcceptedPayload {
|
||||
pub accepted_user: User,
|
||||
@@ -31,6 +37,20 @@ pub struct GenericPayload {
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DmCallPayload {
|
||||
pub initiator_id: String,
|
||||
pub channel_id: String,
|
||||
pub started_at: Option<String>,
|
||||
pub ended: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct InternalDmCallPayload {
|
||||
pub payload: DmCallPayload,
|
||||
pub recipients: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -40,6 +60,7 @@ pub enum PayloadKind {
|
||||
FRReceived(FRReceivedPayload),
|
||||
BadgeUpdate(usize),
|
||||
Generic(GenericPayload),
|
||||
DmCallStartEnd(DmCallPayload),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use serde::Deserialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use super::client::Ping;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClientMessage {
|
||||
Authenticate { token: String },
|
||||
|
||||
@@ -25,6 +25,9 @@ pub use mongodb;
|
||||
#[macro_use]
|
||||
extern crate bson;
|
||||
|
||||
#[cfg(not(feature = "async-std-runtime"))]
|
||||
compile_error!("async-std-runtime feature must be enabled.");
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! query {
|
||||
@@ -103,11 +106,15 @@ pub mod util;
|
||||
pub use models::*;
|
||||
|
||||
pub mod events;
|
||||
#[cfg(feature = "tasks")]
|
||||
pub mod tasks;
|
||||
|
||||
mod amqp;
|
||||
pub use amqp::amqp::AMQP;
|
||||
|
||||
pub mod voice;
|
||||
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ impl AbstractMigrations for MongoDb {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self) {
|
||||
self.db().drop(None).await.ok();
|
||||
self.db().drop().await.ok();
|
||||
}
|
||||
|
||||
/// Migrate the database
|
||||
@@ -18,7 +18,7 @@ impl AbstractMigrations for MongoDb {
|
||||
info!("Migrating the database.");
|
||||
|
||||
let list = self
|
||||
.list_database_names(None, None)
|
||||
.list_database_names()
|
||||
.await
|
||||
.expect("Failed to fetch database names.");
|
||||
|
||||
|
||||
@@ -8,278 +8,258 @@ pub async fn create_database(db: &MongoDb) {
|
||||
info!("Creating database.");
|
||||
let db = db.db();
|
||||
|
||||
db.create_collection("accounts", None)
|
||||
db.create_collection("accounts")
|
||||
.await
|
||||
.expect("Failed to create accounts collection.");
|
||||
|
||||
db.create_collection("users", None)
|
||||
db.create_collection("users")
|
||||
.await
|
||||
.expect("Failed to create users collection.");
|
||||
|
||||
db.create_collection("channels", None)
|
||||
db.create_collection("channels")
|
||||
.await
|
||||
.expect("Failed to create channels collection.");
|
||||
|
||||
db.create_collection("messages", None)
|
||||
db.create_collection("messages")
|
||||
.await
|
||||
.expect("Failed to create messages collection.");
|
||||
|
||||
db.create_collection("servers", None)
|
||||
db.create_collection("servers")
|
||||
.await
|
||||
.expect("Failed to create servers collection.");
|
||||
|
||||
db.create_collection("server_members", None)
|
||||
db.create_collection("server_members")
|
||||
.await
|
||||
.expect("Failed to create server_members collection.");
|
||||
|
||||
db.create_collection("server_bans", None)
|
||||
db.create_collection("server_bans")
|
||||
.await
|
||||
.expect("Failed to create server_bans collection.");
|
||||
|
||||
db.create_collection("channel_invites", None)
|
||||
db.create_collection("channel_invites")
|
||||
.await
|
||||
.expect("Failed to create channel_invites collection.");
|
||||
|
||||
db.create_collection("channel_unreads", None)
|
||||
db.create_collection("channel_unreads")
|
||||
.await
|
||||
.expect("Failed to create channel_unreads collection.");
|
||||
|
||||
db.create_collection("channel_webhooks", None)
|
||||
db.create_collection("channel_webhooks")
|
||||
.await
|
||||
.expect("Failed to create channel_webhooks collection.");
|
||||
|
||||
db.create_collection("migrations", None)
|
||||
db.create_collection("migrations")
|
||||
.await
|
||||
.expect("Failed to create migrations collection.");
|
||||
|
||||
db.create_collection("attachments", None)
|
||||
db.create_collection("attachments")
|
||||
.await
|
||||
.expect("Failed to create attachments collection.");
|
||||
|
||||
db.create_collection("attachment_hashes", None)
|
||||
db.create_collection("attachment_hashes")
|
||||
.await
|
||||
.expect("Failed to create attachment_hashes collection.");
|
||||
|
||||
db.create_collection("user_settings", None)
|
||||
db.create_collection("user_settings")
|
||||
.await
|
||||
.expect("Failed to create user_settings collection.");
|
||||
|
||||
db.create_collection("safety_reports", None)
|
||||
db.create_collection("policy_changes")
|
||||
.await
|
||||
.expect("Failed to create policy_changes collection.");
|
||||
|
||||
db.create_collection("safety_reports")
|
||||
.await
|
||||
.expect("Failed to create safety_reports collection.");
|
||||
|
||||
db.create_collection("safety_snapshots", None)
|
||||
db.create_collection("safety_snapshots")
|
||||
.await
|
||||
.expect("Failed to create safety_snapshots collection.");
|
||||
|
||||
db.create_collection("safety_strikes", None)
|
||||
db.create_collection("safety_strikes")
|
||||
.await
|
||||
.expect("Failed to create safety_strikes collection.");
|
||||
|
||||
db.create_collection("bots", None)
|
||||
db.create_collection("bots")
|
||||
.await
|
||||
.expect("Failed to create bots collection.");
|
||||
|
||||
db.create_collection("ratelimit_events", None)
|
||||
db.create_collection("ratelimit_events")
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events collection.");
|
||||
|
||||
db.create_collection(
|
||||
"pubsub",
|
||||
CreateCollectionOptions::builder()
|
||||
.capped(true)
|
||||
.size(1_000_000)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create pubsub collection.");
|
||||
db.create_collection("pubsub")
|
||||
.with_options(
|
||||
CreateCollectionOptions::builder()
|
||||
.capped(true)
|
||||
.size(1_000_000)
|
||||
.build(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create pubsub collection.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "users",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32
|
||||
},
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "users",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
"name": "username",
|
||||
"unique": false,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"username": 1_i32,
|
||||
"discriminator": 1_i32
|
||||
},
|
||||
"name": "username_discriminator",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2_i32
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create username index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "messages",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"content": "text"
|
||||
},
|
||||
"name": "content"
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "messages",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"content": "text"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"_id": 1_i32
|
||||
},
|
||||
"name": "channel_id_compound"
|
||||
"name": "content"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"_id": 1_i32
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
"name": "channel_id_compound"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
"name": "channel_pinned_compound"
|
||||
"name": "author"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"pinned": 1_i32
|
||||
},
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
"name": "channel_pinned_compound"
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create message index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "channel_unreads",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.channel": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "compound_id"
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "channel_unreads",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.channel": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
"name": "compound_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create channel_unreads index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "server_members",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.server": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "compound_id"
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "server_members",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id.server": 1_i32,
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
"name": "compound_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"_id.user": 1_i32,
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create server_members index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "attachments",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"hash": 1_i32
|
||||
},
|
||||
"name": "hash"
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "attachments",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"hash": 1_i32
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"used_for.id": 1_i32
|
||||
},
|
||||
"name": "used_for_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
"name": "hash"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"used_for.id": 1_i32
|
||||
},
|
||||
"name": "used_for_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create attachments index.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "attachment_hashes",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"processed_hash": 1_i32
|
||||
},
|
||||
"name": "processed_hash"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "attachment_hashes",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"processed_hash": 1_i32
|
||||
},
|
||||
"name": "processed_hash"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create attachment_hashes index.");
|
||||
|
||||
db.collection("migrations")
|
||||
.insert_one(
|
||||
doc! {
|
||||
"_id": 0_i32,
|
||||
"revision": LATEST_REVISION
|
||||
},
|
||||
None,
|
||||
)
|
||||
.insert_one(doc! {
|
||||
"_id": 0_i32,
|
||||
"revision": LATEST_REVISION
|
||||
})
|
||||
.await
|
||||
.expect("Failed to save migration info.");
|
||||
|
||||
db.run_command(
|
||||
doc! {
|
||||
"createIndexes": "ratelimit_events",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id": 1_i32,
|
||||
"target_id": 1_i32,
|
||||
"event_type": 1_i32,
|
||||
},
|
||||
"name": "compound_key"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "ratelimit_events",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"_id": 1_i32,
|
||||
"target_id": 1_i32,
|
||||
"event_type": 1_i32,
|
||||
},
|
||||
"name": "compound_key"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ impl Invite {
|
||||
creator: creator.id.clone(),
|
||||
channel: id.clone(),
|
||||
}),
|
||||
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
|
||||
Channel::TextChannel { id, server, .. } => {
|
||||
Ok(Invite::Server {
|
||||
code,
|
||||
creator: creator.id.clone(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::Invite;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -24,12 +24,9 @@ impl AbstractChannelInvites for MongoDb {
|
||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
|
||||
Ok(self
|
||||
.col::<Invite>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"server": server_id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"server": server_id,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::ChannelUnread;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
@@ -26,6 +27,14 @@ pub trait AbstractChannelUnreads: Sync + Send {
|
||||
message_ids: &[String],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Add a mention.
|
||||
async fn add_mention_to_many_unreads<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_ids: &[String],
|
||||
message_ids: &[String],
|
||||
) -> Result<()>;
|
||||
|
||||
/// Fetch all unreads with mentions for a user.
|
||||
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ impl AbstractChannelUnreads for MongoDb {
|
||||
"last_id": message_id
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_options(
|
||||
FindOneAndUpdateOptions::builder()
|
||||
.upsert(true)
|
||||
.return_document(ReturnDocument::After)
|
||||
@@ -51,15 +53,12 @@ impl AbstractChannelUnreads for MongoDb {
|
||||
let current_time = Ulid::new().to_string();
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.channel": {
|
||||
"$in": channel_ids
|
||||
},
|
||||
"_id.user": user_id
|
||||
.delete_many(doc! {
|
||||
"_id.channel": {
|
||||
"$in": channel_ids
|
||||
},
|
||||
None,
|
||||
)
|
||||
"_id.user": user_id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", COL))?;
|
||||
|
||||
@@ -77,7 +76,6 @@ impl AbstractChannelUnreads for MongoDb {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Document>>(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -104,13 +102,42 @@ impl AbstractChannelUnreads for MongoDb {
|
||||
}
|
||||
}
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
)
|
||||
.with_options(UpdateOptions::builder().upsert(true).build())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Add a mention to multiple users.
|
||||
async fn add_mention_to_many_unreads<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_ids: &[String],
|
||||
message_ids: &[String],
|
||||
) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
doc! {
|
||||
"_id.channel": channel_id,
|
||||
"_id.user": {
|
||||
"$in": user_ids
|
||||
},
|
||||
},
|
||||
doc! {
|
||||
"$push": {
|
||||
"mentions": {
|
||||
"$each": message_ids
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_options(UpdateOptions::builder().upsert(true).build())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
}
|
||||
|
||||
/// Fetch all channel unreads for a user.
|
||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
||||
query!(
|
||||
|
||||
@@ -78,6 +78,38 @@ impl AbstractChannelUnreads for ReferenceDb {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a mention to multiple users.
|
||||
async fn add_mention_to_many_unreads<'a>(
|
||||
&self,
|
||||
channel_id: &str,
|
||||
user_ids: &[String],
|
||||
message_ids: &[String],
|
||||
) -> Result<()> {
|
||||
let mut unreads = self.channel_unreads.lock().await;
|
||||
|
||||
for user_id in user_ids {
|
||||
let key = ChannelCompositeKey {
|
||||
channel: channel_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
};
|
||||
|
||||
if let Some(unread) = unreads.get_mut(&key) {
|
||||
unread.mentions.replace(message_ids.to_vec());
|
||||
} else {
|
||||
unreads.insert(
|
||||
key.clone(),
|
||||
ChannelUnread {
|
||||
id: key,
|
||||
last_id: None,
|
||||
mentions: Some(message_ids.to_vec()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
||||
let unreads = self.channel_unreads.lock().await;
|
||||
Ok(unreads
|
||||
|
||||
@@ -142,7 +142,7 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_webhook(&webhook).await.unwrap();
|
||||
webhook.create(&db).await.unwrap();
|
||||
|
||||
let mut updated_webhook = webhook.clone();
|
||||
updated_webhook
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -24,12 +24,9 @@ impl AbstractWebhooks for MongoDb {
|
||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
||||
Ok(self
|
||||
.col::<Webhook>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"channel_id": channel_id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"channel_id": channel_id,
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
#![allow(deprecated)]
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::{self, MessageAuthor};
|
||||
@@ -8,10 +9,12 @@ use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1, tasks::ack::AckEvent, Database, File, IntoDocumentPath, PartialServer,
|
||||
Server, SystemMessage, User, AMQP,
|
||||
events::client::EventV1, Database, File, PartialServer, Server, SystemMessage, User, AMQP,
|
||||
};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
use crate::IntoDocumentPath;
|
||||
|
||||
auto_derived!(
|
||||
#[serde(tag = "channel_type")]
|
||||
pub enum Channel {
|
||||
@@ -103,38 +106,18 @@ auto_derived!(
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
|
||||
/// Voice Information for when this channel is also a voice channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voice: Option<VoiceInformation>,
|
||||
},
|
||||
/// Voice channel belonging to a server
|
||||
VoiceChannel {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
}
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Channel description
|
||||
description: Option<String>,
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
#[derive(Default)]
|
||||
pub struct VoiceInformation {
|
||||
/// Maximium amount of users allowed in the voice channel at once
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_users: Option<usize>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -161,6 +144,8 @@ auto_derived!(
|
||||
pub default_permissions: Option<OverrideField>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_message_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub voice: Option<VoiceInformation>,
|
||||
}
|
||||
|
||||
/// Optional fields on channel object
|
||||
@@ -168,6 +153,7 @@ auto_derived!(
|
||||
Description,
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
Voice,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -219,16 +205,19 @@ impl Channel {
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
voice: data.voice.map(|voice| voice.into()),
|
||||
},
|
||||
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
|
||||
v0::LegacyServerChannelType::Voice => Channel::TextChannel {
|
||||
id: id.clone(),
|
||||
server: server.id.to_owned(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
icon: None,
|
||||
last_message_id: None,
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
voice: Some(data.voice.unwrap_or_default().into()),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -269,16 +258,24 @@ impl Channel {
|
||||
}));
|
||||
}
|
||||
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
|
||||
let icon = if let Some(icon_id) = data.icon {
|
||||
Some(File::use_channel_icon(db, &icon_id, &id, &owner_id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let recipients = data.users.into_iter().collect::<Vec<String>>();
|
||||
let channel = Channel::Group {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
id,
|
||||
|
||||
name: data.name,
|
||||
owner: owner_id,
|
||||
description: data.description,
|
||||
recipients: recipients.clone(),
|
||||
|
||||
icon: None,
|
||||
icon,
|
||||
last_message_id: None,
|
||||
|
||||
permissions: None,
|
||||
@@ -320,13 +317,10 @@ impl Channel {
|
||||
|
||||
db.insert_channel(&channel).await?;
|
||||
|
||||
match &channel {
|
||||
Channel::DirectMessage { .. } => {
|
||||
let event = EventV1::ChannelCreate(channel.clone().into());
|
||||
event.clone().private(user_a.id.clone()).await;
|
||||
event.private(user_b.id.clone()).await;
|
||||
}
|
||||
_ => {}
|
||||
if let Channel::DirectMessage { .. } = &channel {
|
||||
let event = EventV1::ChannelCreate(channel.clone().into());
|
||||
event.clone().private(user_a.id.clone()).await;
|
||||
event.private(user_b.id.clone()).await;
|
||||
};
|
||||
|
||||
Ok(channel)
|
||||
@@ -424,8 +418,28 @@ impl Channel {
|
||||
Channel::DirectMessage { id, .. }
|
||||
| Channel::Group { id, .. }
|
||||
| Channel::SavedMessages { id, .. }
|
||||
| Channel::TextChannel { id, .. }
|
||||
| Channel::VoiceChannel { id, .. } => id,
|
||||
| Channel::TextChannel { id, .. } => id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone this channel's server id
|
||||
pub fn server(&self) -> Option<&str> {
|
||||
match self {
|
||||
Channel::TextChannel { server, .. } => Some(server),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets this channel's voice information
|
||||
pub fn voice(&self) -> Option<Cow<VoiceInformation>> {
|
||||
match self {
|
||||
Self::DirectMessage { .. } | Self::Group { .. } => {
|
||||
Some(Cow::Owned(VoiceInformation::default()))
|
||||
}
|
||||
Self::TextChannel {
|
||||
voice: Some(voice), ..
|
||||
} => Some(Cow::Borrowed(voice)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,12 +456,6 @@ impl Channel {
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
db.set_channel_role_permission(id, role_id, permissions)
|
||||
.await?;
|
||||
@@ -494,7 +502,7 @@ impl Channel {
|
||||
clear: remove.into_iter().map(|v| v.into()).collect(),
|
||||
}
|
||||
.p(match self {
|
||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
|
||||
Self::TextChannel { server, .. } => server.clone(),
|
||||
_ => id,
|
||||
})
|
||||
.await;
|
||||
@@ -506,17 +514,13 @@ impl Channel {
|
||||
pub fn remove_field(&mut self, field: &FieldsChannel) {
|
||||
match field {
|
||||
FieldsChannel::Description => match self {
|
||||
Self::Group { description, .. }
|
||||
| Self::TextChannel { description, .. }
|
||||
| Self::VoiceChannel { description, .. } => {
|
||||
Self::Group { description, .. } | Self::TextChannel { description, .. } => {
|
||||
description.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Icon => match self {
|
||||
Self::Group { icon, .. }
|
||||
| Self::TextChannel { icon, .. }
|
||||
| Self::VoiceChannel { icon, .. } => {
|
||||
Self::Group { icon, .. } | Self::TextChannel { icon, .. } => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
@@ -525,15 +529,17 @@ impl Channel {
|
||||
Self::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
} => {
|
||||
default_permissions.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Voice => match self {
|
||||
Self::TextChannel { voice, .. } => {
|
||||
voice.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,6 +551,7 @@ impl Channel {
|
||||
}
|
||||
|
||||
/// Apply partial channel to channel
|
||||
#[allow(deprecated)]
|
||||
pub fn apply_options(&mut self, partial: PartialChannel) {
|
||||
match self {
|
||||
Self::SavedMessages { .. } => {}
|
||||
@@ -593,15 +600,7 @@ impl Channel {
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
voice,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
@@ -627,6 +626,10 @@ impl Channel {
|
||||
if let Some(v) = partial.default_permissions {
|
||||
default_permissions.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.voice {
|
||||
voice.replace(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -641,10 +644,11 @@ impl Channel {
|
||||
.private(user.to_string())
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "tasks")]
|
||||
crate::tasks::ack::queue_ack(
|
||||
self.id().to_string(),
|
||||
user.to_string(),
|
||||
AckEvent::AckMessage {
|
||||
crate::tasks::ack::AckEvent::AckMessage {
|
||||
id: message.to_string(),
|
||||
},
|
||||
)
|
||||
@@ -761,12 +765,14 @@ impl Channel {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
impl IntoDocumentPath for FieldsChannel {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsChannel::Description => "description",
|
||||
FieldsChannel::Icon => "icon",
|
||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
||||
FieldsChannel::Voice => "voice",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -23,14 +23,11 @@ impl AbstractChannels for MongoDb {
|
||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
||||
Ok(self
|
||||
.col::<Channel>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("fetch", "channels"))?
|
||||
.filter_map(|s| async {
|
||||
@@ -119,7 +116,6 @@ impl AbstractChannels for MongoDb {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -141,7 +137,6 @@ impl AbstractChannels for MongoDb {
|
||||
"role_permissions.".to_owned() + role: permissions
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -179,7 +174,6 @@ impl AbstractChannels for MongoDb {
|
||||
"recipients": user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -190,7 +184,7 @@ impl AbstractChannels for MongoDb {
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let id = channel.id().to_string();
|
||||
let server_id = match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Channel::TextChannel { server, .. } => {
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
@@ -253,7 +247,6 @@ impl AbstractChannels for MongoDb {
|
||||
"_id": server.id
|
||||
},
|
||||
update,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", "servers"))?;
|
||||
@@ -274,23 +267,17 @@ impl MongoDb {
|
||||
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
|
||||
// Delete all invites to these channels.
|
||||
self.col::<Document>("channel_invites")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.delete_many(doc! {
|
||||
"channel": &id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
|
||||
|
||||
// Delete unread message objects on channels.
|
||||
self.col::<Document>("channel_unreads")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.delete_many(doc! {
|
||||
"_id.channel": &id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channel_unreads"))
|
||||
.map(|_| ())?;
|
||||
@@ -299,12 +286,9 @@ impl MongoDb {
|
||||
|
||||
// Delete all webhooks on this channel.
|
||||
self.col::<Document>("webhooks")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": &id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.delete_many(doc! {
|
||||
"channel": &id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "webhooks"))
|
||||
.map(|_| ())
|
||||
|
||||
@@ -94,9 +94,6 @@ impl AbstractChannels for ReferenceDb {
|
||||
match &mut channel {
|
||||
Channel::TextChannel {
|
||||
role_permissions, ..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
role_permissions, ..
|
||||
} => {
|
||||
if role_permissions.get(role_id).is_some() {
|
||||
role_permissions.remove(role_id);
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@ impl AbstractEmojis for MongoDb {
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
|
||||
@@ -28,7 +28,7 @@ auto_derived_partial!(
|
||||
/// Size of this file (in bytes)
|
||||
pub size: isize,
|
||||
},
|
||||
"PartialFile"
|
||||
"PartialFileHash"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::FileHash;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ impl AbstractAttachmentHashes for MongoDb {
|
||||
"iv": nonce
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::File;
|
||||
|
||||
use super::FileUsedFor;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -110,7 +110,6 @@ impl AbstractAttachments for MongoDb {
|
||||
"uploader_id": uploader_id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", COL))?;
|
||||
@@ -130,7 +129,6 @@ impl AbstractAttachments for MongoDb {
|
||||
"reported": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -149,7 +147,6 @@ impl AbstractAttachments for MongoDb {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -170,7 +167,6 @@ impl AbstractAttachments for MongoDb {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -193,7 +189,6 @@ impl MongoDb {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
|
||||
@@ -5,20 +5,25 @@ use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0::{
|
||||
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
|
||||
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
||||
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text,
|
||||
};
|
||||
use revolt_permissions::{ChannelPermission, PermissionValue};
|
||||
use revolt_result::Result;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
|
||||
use revolt_result::{ErrorType, Result};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1,
|
||||
tasks::{self, ack::AckEvent},
|
||||
util::{bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey},
|
||||
util::{
|
||||
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
|
||||
permissions::DatabasePermissionQuery,
|
||||
},
|
||||
Channel, Database, Emoji, File, User, AMQP,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tasks")]
|
||||
use crate::tasks::{self, ack::AckEvent};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Message
|
||||
pub struct Message {
|
||||
@@ -53,6 +58,9 @@ auto_derived_partial!(
|
||||
/// Array of user ids mentioned in this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mentions: Option<Vec<String>>,
|
||||
/// Array of role ids mentioned in this message
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role_mentions: Option<Vec<String>>,
|
||||
/// Array of message ids this message is replying to
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub replies: Option<Vec<String>>,
|
||||
@@ -71,7 +79,7 @@ auto_derived_partial!(
|
||||
|
||||
/// Bitfield of message flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
pub flags: Option<u32>,
|
||||
},
|
||||
"PartialMessage"
|
||||
);
|
||||
@@ -106,6 +114,11 @@ auto_derived!(
|
||||
MessagePinned { id: String, by: String },
|
||||
#[serde(rename = "message_unpinned")]
|
||||
MessageUnpinned { id: String, by: String },
|
||||
#[serde(rename = "call_started")]
|
||||
CallStarted {
|
||||
by: String,
|
||||
finished_at: Option<Timestamp>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Name and / or avatar override information
|
||||
@@ -199,6 +212,30 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
pub struct MessageFlagsValue(pub u32);
|
||||
|
||||
impl MessageFlagsValue {
|
||||
pub fn has(&self, flag: MessageFlags) -> bool {
|
||||
self.has_value(flag as u32)
|
||||
}
|
||||
pub fn has_value(&self, bit: u32) -> bool {
|
||||
let mask = 1 << bit;
|
||||
self.0 & mask == mask
|
||||
}
|
||||
|
||||
pub fn set(&mut self, flag: MessageFlags, toggle: bool) -> &mut Self {
|
||||
self.set_value(flag as u32, toggle)
|
||||
}
|
||||
pub fn set_value(&mut self, bit: u32, toggle: bool) -> &mut Self {
|
||||
if toggle {
|
||||
self.0 |= 1 << bit;
|
||||
} else {
|
||||
self.0 &= !(1 << bit);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for Message {
|
||||
fn default() -> Self {
|
||||
@@ -214,6 +251,7 @@ impl Default for Message {
|
||||
edited: None,
|
||||
embeds: None,
|
||||
mentions: None,
|
||||
role_mentions: None,
|
||||
replies: None,
|
||||
reactions: Default::default(),
|
||||
interactions: Default::default(),
|
||||
@@ -255,20 +293,48 @@ impl Message {
|
||||
.map_err(|_| create_error!(InvalidOperation))?;
|
||||
|
||||
// Check the message is not empty
|
||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
||||
if (data.content.as_ref().is_none_or(|v| v.is_empty()))
|
||||
&& (data.attachments.as_ref().is_none_or(|v| v.is_empty()))
|
||||
&& (data.embeds.as_ref().is_none_or(|v| v.is_empty()))
|
||||
{
|
||||
return Err(create_error!(EmptyMessage));
|
||||
}
|
||||
|
||||
// Ensure flags are either not set or have permissible values
|
||||
if let Some(flags) = &data.flags {
|
||||
if flags != &0 && flags != &1 {
|
||||
let allow_mass_mentions = allow_mentions && config.features.mass_mentions_enabled;
|
||||
|
||||
let mut mentions_everyone = false;
|
||||
let mut mentions_online = false;
|
||||
let mut suppress_notifications = false;
|
||||
|
||||
if let Some(raw_flags) = &data.flags {
|
||||
if raw_flags > &7 {
|
||||
// quick path to failure: bigger than all the bits combined
|
||||
return Err(create_error!(InvalidProperty));
|
||||
}
|
||||
|
||||
// First step of mass mention resolution
|
||||
let flags = MessageFlagsValue(*raw_flags);
|
||||
suppress_notifications = flags.has(MessageFlags::SuppressNotifications);
|
||||
mentions_everyone = allow_mentions && flags.has(MessageFlags::MentionsEveryone);
|
||||
mentions_online = allow_mentions && flags.has(MessageFlags::MentionsOnline);
|
||||
|
||||
// Not a bot, and attempting to set mention flags
|
||||
if user.as_ref().is_some_and(|u| u.bot.as_ref().is_none())
|
||||
&& (mentions_everyone || mentions_online)
|
||||
{
|
||||
return Err(create_error!(IsNotBot));
|
||||
}
|
||||
|
||||
if mentions_everyone && mentions_online {
|
||||
return Err(create_error!(InvalidFlagValue));
|
||||
}
|
||||
}
|
||||
|
||||
let server_id = match channel {
|
||||
Channel::TextChannel { ref server, .. } => Some(server.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Ensure restrict_reactions is not specified without reactions list
|
||||
if let Some(interactions) = &data.interactions {
|
||||
if interactions.restrict_reactions {
|
||||
@@ -302,18 +368,76 @@ impl Message {
|
||||
.unwrap_or_default(),
|
||||
author: author_id,
|
||||
webhook: webhook.map(|w| w.into()),
|
||||
flags: data.flags.map(|v| v as i32),
|
||||
flags: data.flags,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Parse mentions in message.
|
||||
let mut mentions = HashSet::new();
|
||||
if allow_mentions {
|
||||
if let Some(content) = &data.content {
|
||||
for capture in RE_MENTION.captures_iter(content) {
|
||||
if let Some(mention) = capture.get(1) {
|
||||
mentions.insert(mention.as_str().to_string());
|
||||
}
|
||||
|
||||
let mut message_mentions = if let Some(raw_content) = &data.content {
|
||||
revolt_parser::parse_message(raw_content)
|
||||
} else {
|
||||
revolt_parser::MessageResults::default()
|
||||
};
|
||||
|
||||
message_mentions.mentions_everyone |= mentions_everyone;
|
||||
message_mentions.mentions_online |= mentions_online;
|
||||
|
||||
let revolt_parser::MessageResults {
|
||||
mut user_mentions,
|
||||
mut role_mentions,
|
||||
mut mentions_everyone,
|
||||
mut mentions_online,
|
||||
} = message_mentions;
|
||||
|
||||
if allow_mass_mentions && server_id.is_some() && !role_mentions.is_empty() {
|
||||
let server_data = db
|
||||
.fetch_server(server_id.unwrap().as_str())
|
||||
.await
|
||||
.expect("Failed to fetch server");
|
||||
|
||||
role_mentions.retain(|role_id| server_data.roles.contains_key(role_id));
|
||||
}
|
||||
|
||||
// Validate the user can perform a mass mention
|
||||
if !config.features.mass_mentions_enabled
|
||||
&& (mentions_everyone || mentions_online || !role_mentions.is_empty())
|
||||
{
|
||||
mentions_everyone = false;
|
||||
mentions_online = false;
|
||||
role_mentions.clear();
|
||||
} else if mentions_everyone || mentions_online || !role_mentions.is_empty() {
|
||||
debug!(
|
||||
"Mentioned everyone: {}, mentioned online: {}, mentioned roles: {:?}",
|
||||
mentions_everyone, mentions_online, &role_mentions
|
||||
);
|
||||
if let Some(user) = match author {
|
||||
MessageAuthor::User(user) => Some(Ok(user)),
|
||||
MessageAuthor::System { .. } => Some(Err(())), // DISALLOWED
|
||||
MessageAuthor::Webhook(..) => None, // Bypass check
|
||||
} {
|
||||
if user.is_err() {
|
||||
return Err(create_error!(InvalidProperty));
|
||||
}
|
||||
let owned_user: User = user.unwrap().to_owned().into();
|
||||
|
||||
let mut query = DatabasePermissionQuery::new(db, &owned_user).channel(&channel);
|
||||
let perms = calculate_channel_permissions(&mut query).await;
|
||||
|
||||
if (mentions_everyone || mentions_online)
|
||||
&& !perms.has_channel_permission(ChannelPermission::MentionEveryone)
|
||||
{
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: ChannelPermission::MentionEveryone.to_string()
|
||||
}));
|
||||
}
|
||||
|
||||
if !role_mentions.is_empty()
|
||||
&& !perms.has_channel_permission(ChannelPermission::MentionRoles)
|
||||
{
|
||||
return Err(create_error!(MissingPermission {
|
||||
permission: ChannelPermission::MentionRoles.to_string()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -327,38 +451,56 @@ impl Message {
|
||||
}));
|
||||
}
|
||||
|
||||
for ReplyIntent { id, mention } in entries {
|
||||
let message = db.fetch_message(&id).await?;
|
||||
for ReplyIntent {
|
||||
id,
|
||||
mention,
|
||||
fail_if_not_exists,
|
||||
} in entries
|
||||
{
|
||||
match db.fetch_message(&id).await {
|
||||
// Referenced message exists
|
||||
Ok(message) => {
|
||||
if mention && allow_mentions {
|
||||
user_mentions.insert(message.author.to_owned());
|
||||
}
|
||||
|
||||
if mention && allow_mentions {
|
||||
mentions.insert(message.author.to_owned());
|
||||
replies.insert(message.id);
|
||||
}
|
||||
// If the referenced message doesn't exist and fail_if_not_exists
|
||||
// is set to false, send the message without the reply.
|
||||
Err(e) => {
|
||||
if !matches!(e.error_type, ErrorType::NotFound)
|
||||
|| fail_if_not_exists.unwrap_or(true)
|
||||
{
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replies.insert(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the mentions go to users in the channel/server
|
||||
if !mentions.is_empty() {
|
||||
if !user_mentions.is_empty() {
|
||||
#[allow(deprecated)]
|
||||
match channel {
|
||||
Channel::DirectMessage { ref recipients, .. }
|
||||
| Channel::Group { ref recipients, .. } => {
|
||||
let recipients_hash: HashSet<&String, RandomState> =
|
||||
HashSet::from_iter(recipients);
|
||||
mentions.retain(|m| recipients_hash.contains(m));
|
||||
let recipients_hash = HashSet::<&String, RandomState>::from_iter(recipients);
|
||||
user_mentions.retain(|m| recipients_hash.contains(m));
|
||||
role_mentions.clear();
|
||||
}
|
||||
Channel::TextChannel { ref server, .. }
|
||||
| Channel::VoiceChannel { ref server, .. } => {
|
||||
let mentions_vec = Vec::from_iter(mentions.iter().cloned());
|
||||
Channel::TextChannel { ref server, .. }=> {
|
||||
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
|
||||
|
||||
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
|
||||
if let Ok(valid_members) = valid_members {
|
||||
let valid_mentions: HashSet<&String, RandomState> =
|
||||
HashSet::from_iter(valid_members.iter().map(|m| &m.id.user));
|
||||
let valid_mentions = HashSet::<&String, RandomState>::from_iter(
|
||||
valid_members.iter().map(|m| &m.id.user),
|
||||
);
|
||||
|
||||
mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
|
||||
user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
|
||||
|
||||
if !mentions.is_empty() {
|
||||
if !user_mentions.is_empty() {
|
||||
// if there are still mentions, drill down to a channel-level
|
||||
let member_channel_view_perms =
|
||||
BulkDatabasePermissionQuery::from_server_id(db, server)
|
||||
@@ -368,7 +510,7 @@ impl Message {
|
||||
.members_can_see_channel()
|
||||
.await;
|
||||
|
||||
mentions
|
||||
user_mentions
|
||||
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
|
||||
}
|
||||
} else {
|
||||
@@ -377,13 +519,21 @@ impl Message {
|
||||
}
|
||||
}
|
||||
Channel::SavedMessages { .. } => {
|
||||
mentions.clear();
|
||||
user_mentions.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !mentions.is_empty() {
|
||||
message.mentions.replace(mentions.into_iter().collect());
|
||||
if !user_mentions.is_empty() {
|
||||
message
|
||||
.mentions
|
||||
.replace(user_mentions.into_iter().collect());
|
||||
}
|
||||
|
||||
if !role_mentions.is_empty() {
|
||||
message
|
||||
.role_mentions
|
||||
.replace(role_mentions.into_iter().collect());
|
||||
}
|
||||
|
||||
if !replies.is_empty() {
|
||||
@@ -392,6 +542,15 @@ impl Message {
|
||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
||||
}
|
||||
|
||||
// Calculate final message flags
|
||||
let mut flag_value = MessageFlagsValue(0);
|
||||
flag_value
|
||||
.set(MessageFlags::SuppressNotifications, suppress_notifications)
|
||||
.set(MessageFlags::MentionsEveryone, mentions_everyone)
|
||||
.set(MessageFlags::MentionsOnline, mentions_online);
|
||||
|
||||
message.flags = Some(flag_value.0);
|
||||
|
||||
// Add attachments to message.
|
||||
let mut attachments = vec![];
|
||||
if data
|
||||
@@ -462,15 +621,22 @@ impl Message {
|
||||
.await;
|
||||
|
||||
// Update last_message_id
|
||||
#[cfg(feature = "tasks")]
|
||||
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
|
||||
|
||||
// Add mentions for affected users
|
||||
#[cfg(feature = "tasks")]
|
||||
if !mentions_elsewhere {
|
||||
if let Some(mentions) = &self.mentions {
|
||||
tasks::ack::queue_message(
|
||||
self.channel.to_string(),
|
||||
AckEvent::ProcessMessage {
|
||||
messages: vec![(None, self.clone(), mentions.clone(), true)],
|
||||
messages: vec![(
|
||||
None,
|
||||
self.clone(),
|
||||
mentions.clone(),
|
||||
self.has_suppressed_notifications(),
|
||||
)],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -478,6 +644,7 @@ impl Message {
|
||||
}
|
||||
|
||||
// Generate embeds
|
||||
#[cfg(feature = "tasks")]
|
||||
if generate_embeds {
|
||||
if let Some(content) = &self.content {
|
||||
tasks::process_embeds::queue(
|
||||
@@ -497,7 +664,7 @@ impl Message {
|
||||
pub async fn send(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
amqp: Option<&AMQP>, // this is optional mostly for tests.
|
||||
_amqp: Option<&AMQP>, // this is optional mostly for tests.
|
||||
author: MessageAuthor<'_>,
|
||||
user: Option<v0::User>,
|
||||
member: Option<v0::Member>,
|
||||
@@ -514,8 +681,11 @@ impl Message {
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !self.has_suppressed_notifications() {
|
||||
if !self.has_suppressed_notifications()
|
||||
&& (self.mentions.is_some() || self.contains_mass_push_mention())
|
||||
{
|
||||
// send Push notifications
|
||||
#[cfg(feature = "tasks")]
|
||||
tasks::ack::queue_message(
|
||||
self.channel.to_string(),
|
||||
AckEvent::ProcessMessage {
|
||||
@@ -537,7 +707,7 @@ impl Message {
|
||||
}
|
||||
_ => vec![],
|
||||
},
|
||||
self.has_suppressed_notifications(),
|
||||
false, // branch already dictates this
|
||||
)],
|
||||
},
|
||||
)
|
||||
@@ -574,13 +744,24 @@ impl Message {
|
||||
/// Whether this message has suppressed notifications
|
||||
pub fn has_suppressed_notifications(&self) -> bool {
|
||||
if let Some(flags) = self.flags {
|
||||
flags & MessageFlags::SuppressNotifications as i32
|
||||
== MessageFlags::SuppressNotifications as i32
|
||||
flags & MessageFlags::SuppressNotifications as u32
|
||||
== MessageFlags::SuppressNotifications as u32
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_mass_push_mention(&self) -> bool {
|
||||
let ping = if let Some(flags) = self.flags {
|
||||
let flags = MessageFlagsValue(flags);
|
||||
flags.has(MessageFlags::MentionsEveryone)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
ping || self.role_mentions.is_some()
|
||||
}
|
||||
|
||||
/// Update message data
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
@@ -615,7 +796,7 @@ impl Message {
|
||||
query: MessageQuery,
|
||||
perspective: &User,
|
||||
include_users: Option<bool>,
|
||||
server_id: Option<String>,
|
||||
server_id: Option<&str>,
|
||||
) -> Result<BulkMessageResponse> {
|
||||
let messages: Vec<v0::Message> = db
|
||||
.fetch_messages(query)
|
||||
@@ -658,6 +839,7 @@ impl Message {
|
||||
v0::SystemMessage::MessageUnpinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
v0::SystemMessage::CallStarted { by, .. } => users.push(by.clone()),
|
||||
}
|
||||
}
|
||||
users
|
||||
@@ -672,7 +854,7 @@ impl Message {
|
||||
users,
|
||||
members: if let Some(server_id) = server_id {
|
||||
Some(
|
||||
db.fetch_members(&server_id, &user_ids)
|
||||
db.fetch_members(server_id, &user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -228,7 +228,6 @@ impl AbstractMessages for MongoDb {
|
||||
"_id": id
|
||||
},
|
||||
query,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -247,7 +246,6 @@ impl AbstractMessages for MongoDb {
|
||||
format!("reactions.{emoji}"): user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -266,7 +264,6 @@ impl AbstractMessages for MongoDb {
|
||||
format!("reactions.{emoji}"): user
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -285,7 +282,6 @@ impl AbstractMessages for MongoDb {
|
||||
format!("reactions.{emoji}"): 1
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -300,15 +296,12 @@ impl AbstractMessages for MongoDb {
|
||||
/// Delete messages from a channel by their ids and corresponding channel id
|
||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(
|
||||
doc! {
|
||||
"channel": channel,
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.delete_many(doc! {
|
||||
"channel": channel,
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("delete_many", COL))
|
||||
@@ -362,7 +355,6 @@ impl MongoDb {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_many", "attachments"))?;
|
||||
@@ -370,7 +362,7 @@ impl MongoDb {
|
||||
|
||||
// And then delete said messages.
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(projection, None)
|
||||
.delete_many(projection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("delete_many", COL))
|
||||
|
||||
@@ -8,6 +8,7 @@ mod emojis;
|
||||
mod file_hashes;
|
||||
mod files;
|
||||
mod messages;
|
||||
mod policy_changes;
|
||||
mod ratelimit_events;
|
||||
mod safety_reports;
|
||||
mod safety_snapshots;
|
||||
@@ -27,6 +28,7 @@ pub use emojis::*;
|
||||
pub use file_hashes::*;
|
||||
pub use files::*;
|
||||
pub use messages::*;
|
||||
pub use policy_changes::*;
|
||||
pub use ratelimit_events::*;
|
||||
pub use safety_reports::*;
|
||||
pub use safety_snapshots::*;
|
||||
@@ -36,7 +38,10 @@ pub use servers::*;
|
||||
pub use user_settings::*;
|
||||
pub use users::*;
|
||||
|
||||
use crate::{Database, MongoDb, ReferenceDb};
|
||||
use crate::{Database, ReferenceDb};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
use crate::MongoDb;
|
||||
|
||||
pub trait AbstractDatabase:
|
||||
Sync
|
||||
@@ -51,6 +56,7 @@ pub trait AbstractDatabase:
|
||||
+ file_hashes::AbstractAttachmentHashes
|
||||
+ files::AbstractAttachments
|
||||
+ messages::AbstractMessages
|
||||
+ policy_changes::AbstractPolicyChange
|
||||
+ ratelimit_events::AbstractRatelimitEvents
|
||||
+ safety_reports::AbstractReport
|
||||
+ safety_snapshots::AbstractSnapshot
|
||||
@@ -63,6 +69,8 @@ pub trait AbstractDatabase:
|
||||
}
|
||||
|
||||
impl AbstractDatabase for ReferenceDb {}
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
impl AbstractDatabase for MongoDb {}
|
||||
|
||||
impl std::ops::Deref for Database {
|
||||
@@ -71,6 +79,7 @@ impl std::ops::Deref for Database {
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match &self {
|
||||
Database::Reference(dummy) => dummy,
|
||||
#[cfg(feature = "mongodb")]
|
||||
Database::MongoDb(mongo) => mongo,
|
||||
}
|
||||
}
|
||||
|
||||
5
crates/core/database/src/models/policy_changes/mod.rs
Normal file
5
crates/core/database/src/models/policy_changes/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
20
crates/core/database/src/models/policy_changes/model.rs
Normal file
20
crates/core/database/src/models/policy_changes/model.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
|
||||
auto_derived!(
|
||||
/// Platform policy change
|
||||
pub struct PolicyChange {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// Time at which this policy was created
|
||||
pub created_time: Timestamp,
|
||||
/// Time at which this policy is effective
|
||||
pub effective_time: Timestamp,
|
||||
|
||||
/// Message shown to users
|
||||
pub description: String,
|
||||
/// URL with details about changes
|
||||
pub url: String,
|
||||
}
|
||||
);
|
||||
16
crates/core/database/src/models/policy_changes/ops.rs
Normal file
16
crates/core/database/src/models/policy_changes/ops.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::PolicyChange;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractPolicyChange: Sync + Send {
|
||||
/// Fetch all policy changes
|
||||
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>>;
|
||||
|
||||
/// Acknowledge policy changes
|
||||
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use bson::to_bson;
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::PolicyChange;
|
||||
use crate::User;
|
||||
|
||||
use super::AbstractPolicyChange;
|
||||
|
||||
static COL: &str = "policy_changes";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractPolicyChange for MongoDb {
|
||||
/// Fetch all policy changes
|
||||
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>> {
|
||||
query!(self, find, COL, doc! {})
|
||||
}
|
||||
|
||||
/// Acknowledge policy changes
|
||||
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()> {
|
||||
let latest_policy = self
|
||||
.fetch_policy_changes()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|policy| policy.created_time)
|
||||
.max()
|
||||
.unwrap_or(Timestamp::UNIX_EPOCH);
|
||||
|
||||
self.col::<User>("users")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": user_id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"last_acknowledged_policy_change": to_bson(&latest_policy)
|
||||
.map_err(|_| create_database_error!("to_bson", "timestamp"))?
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::PolicyChange;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractPolicyChange;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractPolicyChange for ReferenceDb {
|
||||
/// Fetch all policy changes
|
||||
async fn fetch_policy_changes(&self) -> Result<Vec<PolicyChange>> {
|
||||
let policy_changes = self.policy_changes.lock().await;
|
||||
Ok(policy_changes.values().cloned().collect())
|
||||
}
|
||||
|
||||
/// Acknowledge policy changes
|
||||
async fn acknowledge_policy_changes(&self, user_id: &str) -> Result<()> {
|
||||
let mut users = self.users.lock().await;
|
||||
let user = users.get_mut(user_id).expect("user doesn't exist");
|
||||
user.last_acknowledged_policy_change = self
|
||||
.fetch_policy_changes()
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|policy| policy.created_time)
|
||||
.max()
|
||||
.unwrap_or(Timestamp::UNIX_EPOCH);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -23,16 +23,13 @@ impl AbstractRatelimitEvents for MongoDb {
|
||||
count: usize,
|
||||
) -> Result<bool> {
|
||||
self.col::<RatelimitEvent>(COL)
|
||||
.count_documents(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
|
||||
},
|
||||
"target_id": target_id,
|
||||
"event_type": event_type.to_string()
|
||||
.count_documents(doc! {
|
||||
"_id": {
|
||||
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
|
||||
},
|
||||
None,
|
||||
)
|
||||
"target_id": target_id,
|
||||
"event_type": event_type.to_string()
|
||||
})
|
||||
.await
|
||||
.map(|c| c as usize >= count)
|
||||
.map_err(|_| create_database_error!("count_documents", COL))
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::Report;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -85,8 +85,8 @@ impl SnapshotContent {
|
||||
Ok((
|
||||
SnapshotContent::Message {
|
||||
message,
|
||||
prior_context: prior_context.into_iter().map(Into::into).collect(),
|
||||
leading_context: leading_context.into_iter().map(Into::into).collect(),
|
||||
prior_context: prior_context.into_iter().collect(),
|
||||
leading_context: leading_context.into_iter().collect(),
|
||||
},
|
||||
files,
|
||||
))
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::Snapshot;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ auto_derived!(
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl ServerBan {
|
||||
/// Create ban
|
||||
pub async fn create(
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{MemberCompositeKey, ServerBan};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{create_error, Result};
|
||||
use crate::voice::get_channel_voice_state;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
|
||||
Server, SystemMessage, User,
|
||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel,
|
||||
Database, File, Server, SystemMessage, User,
|
||||
};
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_true(x: &bool) -> bool {
|
||||
*x
|
||||
}
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server Member
|
||||
pub struct Member {
|
||||
@@ -30,6 +39,17 @@ auto_derived_partial!(
|
||||
/// Timestamp this member is timed out until
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeout: Option<Timestamp>,
|
||||
|
||||
/// Whether the member is server-wide voice muted
|
||||
#[serde(skip_serializing_if = "is_true", default = "default_true")]
|
||||
pub can_publish: bool,
|
||||
/// Whether the member is server-wide voice deafened
|
||||
#[serde(skip_serializing_if = "is_true", default = "default_true")]
|
||||
pub can_receive: bool,
|
||||
|
||||
// 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"
|
||||
);
|
||||
@@ -50,6 +70,9 @@ auto_derived!(
|
||||
Avatar,
|
||||
Roles,
|
||||
Timeout,
|
||||
CanReceive,
|
||||
CanPublish,
|
||||
JoinedAt,
|
||||
}
|
||||
|
||||
/// Member removal intention
|
||||
@@ -69,6 +92,8 @@ impl Default for Member {
|
||||
avatar: None,
|
||||
roles: vec![],
|
||||
timeout: None,
|
||||
can_publish: true,
|
||||
can_receive: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +115,7 @@ impl Member {
|
||||
return Err(create_error!(AlreadyInServer));
|
||||
}
|
||||
|
||||
let member = Member {
|
||||
let mut member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
@@ -98,7 +123,9 @@ impl Member {
|
||||
..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 mut channels = channels.unwrap_or_default();
|
||||
@@ -121,9 +148,18 @@ impl Member {
|
||||
|
||||
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
|
||||
|
||||
let mut voice_states = Vec::new();
|
||||
|
||||
for channel in &channels {
|
||||
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
|
||||
voice_states.push(voice_state)
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::ServerMemberJoin {
|
||||
id: server.id.clone(),
|
||||
user: user.id.clone(),
|
||||
member: member.clone().into(),
|
||||
}
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
@@ -137,6 +173,7 @@ impl Member {
|
||||
.map(|channel| channel.into())
|
||||
.collect(),
|
||||
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
|
||||
voice_states
|
||||
}
|
||||
.private(user.id.clone())
|
||||
.await;
|
||||
@@ -159,7 +196,7 @@ impl Member {
|
||||
}
|
||||
|
||||
/// Update member data
|
||||
pub async fn update<'a>(
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialMember,
|
||||
@@ -186,10 +223,13 @@ impl Member {
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsMember) {
|
||||
match field {
|
||||
FieldsMember::JoinedAt => (),
|
||||
FieldsMember::Avatar => self.avatar = None,
|
||||
FieldsMember::Nickname => self.nickname = None,
|
||||
FieldsMember::Roles => self.roles.clear(),
|
||||
FieldsMember::Timeout => self.timeout = None,
|
||||
FieldsMember::CanReceive => self.can_receive = true,
|
||||
FieldsMember::CanPublish => self.can_publish = true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +264,7 @@ impl Member {
|
||||
intention: RemovalIntention,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
db.delete_member(&self.id).await?;
|
||||
db.soft_delete_member(&self.id).await?;
|
||||
|
||||
EventV1::ServerMemberLeave {
|
||||
id: self.id.server.to_string(),
|
||||
@@ -260,3 +300,74 @@ impl Member {
|
||||
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,26 +1,109 @@
|
||||
#[cfg(feature = "mongodb")]
|
||||
use ::mongodb::{ClientSession, SessionCursor};
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum ChunkedServerMembersGenerator {
|
||||
#[cfg(feature = "mongodb")]
|
||||
MongoDb {
|
||||
session: ClientSession,
|
||||
cursor: Option<SessionCursor<Member>>,
|
||||
},
|
||||
|
||||
Reference {
|
||||
offset: i32,
|
||||
data: Option<Vec<Member>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ChunkedServerMembersGenerator {
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub fn new_mongo(session: ClientSession, cursor: SessionCursor<Member>) -> Self {
|
||||
ChunkedServerMembersGenerator::MongoDb {
|
||||
session,
|
||||
cursor: Some(cursor),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_reference(data: Vec<Member>) -> Self {
|
||||
ChunkedServerMembersGenerator::Reference {
|
||||
offset: 0,
|
||||
data: Some(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn next(&mut self) -> Option<Member> {
|
||||
match self {
|
||||
#[cfg(feature = "mongodb")]
|
||||
ChunkedServerMembersGenerator::MongoDb { session, cursor } => {
|
||||
if let Some(cursor) = cursor {
|
||||
let value = cursor.next(session).await;
|
||||
value.map(|val| val.expect("Failed to fetch the next member"))
|
||||
} else {
|
||||
warn!("Attempted to access a (MongoDb) server member generator without first setting a cursor");
|
||||
None
|
||||
}
|
||||
}
|
||||
ChunkedServerMembersGenerator::Reference { offset, data } => {
|
||||
if let Some(data) = data {
|
||||
if data.len() as i32 >= *offset {
|
||||
None
|
||||
} else {
|
||||
let resp = &data[*offset as usize];
|
||||
*offset += 1;
|
||||
Some(resp.clone())
|
||||
}
|
||||
} else {
|
||||
warn!("Attempted to access a (Reference) server member generator without first providing data");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractServerMembers: Sync + Send {
|
||||
/// Insert a new server member into the database
|
||||
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
|
||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
||||
|
||||
/// 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
|
||||
async fn fetch_all_members_chunked(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> Result<ChunkedServerMembersGenerator>;
|
||||
|
||||
async fn fetch_all_members_with_roles(
|
||||
&self,
|
||||
server_id: &str,
|
||||
roles: &[String],
|
||||
) -> Result<Vec<Member>>;
|
||||
|
||||
async fn fetch_all_members_with_roles_chunked(
|
||||
&self,
|
||||
server_id: &str,
|
||||
roles: &[String],
|
||||
) -> Result<ChunkedServerMembersGenerator>;
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>>;
|
||||
|
||||
/// 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
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
|
||||
@@ -36,6 +119,14 @@ pub trait AbstractServerMembers: Sync + Send {
|
||||
remove: Vec<FieldsMember>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
/// Marks a user as no longer a member of a server, while retaining the database value.
|
||||
/// 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,18 +1,54 @@
|
||||
use bson::Document;
|
||||
use futures::StreamExt;
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use mongodb::options::ReadConcern;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractServerMembers;
|
||||
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
|
||||
|
||||
static COL: &str = "server_members";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractServerMembers for MongoDb {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
||||
query!(self, insert_one, COL, &member).map(|_| ())
|
||||
/// Insert a new server member (or use the existing member if one is found)
|
||||
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
|
||||
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
|
||||
@@ -23,22 +59,21 @@ impl AbstractServerMembers for MongoDb {
|
||||
COL,
|
||||
doc! {
|
||||
"_id.server": server_id,
|
||||
"_id.user": user_id
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// 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
|
||||
.col::<Member>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id.server": server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
@@ -52,16 +87,102 @@ impl AbstractServerMembers for MongoDb {
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
/// Fetch all members in a server as a generator.
|
||||
/// Uses config key pushd.mass_mention_chunk_size as the batch size.
|
||||
async fn fetch_all_members_chunked(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> Result<ChunkedServerMembersGenerator> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let mut session = self
|
||||
.start_session()
|
||||
.await
|
||||
.map_err(|_| create_database_error!("start_session", COL))?;
|
||||
|
||||
session
|
||||
.start_transaction()
|
||||
.read_concern(ReadConcern::snapshot())
|
||||
.await
|
||||
.map_err(|_| create_database_error!("start_transaction", COL))?;
|
||||
|
||||
let cursor = self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.server": server_id
|
||||
})
|
||||
.session(&mut session)
|
||||
.batch_size(config.pushd.mass_mention_chunk_size as u32)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?;
|
||||
|
||||
Ok(ChunkedServerMembersGenerator::new_mongo(session, cursor))
|
||||
}
|
||||
|
||||
async fn fetch_all_members_with_roles(
|
||||
&self,
|
||||
server_id: &str,
|
||||
roles: &[String],
|
||||
) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id.user": user_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"_id.server": server_id,
|
||||
"roles": {"$in": roles}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn fetch_all_members_with_roles_chunked(
|
||||
&self,
|
||||
server_id: &str,
|
||||
roles: &[String],
|
||||
) -> Result<ChunkedServerMembersGenerator> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let mut session = self
|
||||
.start_session()
|
||||
.await
|
||||
.map_err(|_| create_database_error!("start_session", COL))?;
|
||||
|
||||
session
|
||||
.start_transaction()
|
||||
.read_concern(ReadConcern::snapshot())
|
||||
.await
|
||||
.map_err(|_| create_database_error!("start_transaction", COL))?;
|
||||
|
||||
let cursor = self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.server": server_id,
|
||||
"roles": {"$in": roles}
|
||||
})
|
||||
.session(&mut session)
|
||||
.batch_size(config.pushd.mass_mention_chunk_size as u32)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?;
|
||||
|
||||
return Ok(ChunkedServerMembersGenerator::new_mongo(session, cursor));
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
@@ -76,18 +197,16 @@ impl AbstractServerMembers for MongoDb {
|
||||
}
|
||||
|
||||
/// 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
|
||||
.col::<Member>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id.server": server_id,
|
||||
"_id.user": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false},
|
||||
"_id.user": {
|
||||
"$in": ids
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
@@ -104,12 +223,10 @@ impl AbstractServerMembers for MongoDb {
|
||||
/// Fetch member count of a server
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
|
||||
self.col::<Member>(COL)
|
||||
.count_documents(
|
||||
doc! {
|
||||
"_id.server": server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.count_documents(doc! {
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map(|c| c as usize)
|
||||
.map_err(|_| create_database_error!("count_documents", COL))
|
||||
@@ -118,12 +235,10 @@ impl AbstractServerMembers for MongoDb {
|
||||
/// Fetch server count of a user
|
||||
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
|
||||
self.col::<Member>(COL)
|
||||
.count_documents(
|
||||
doc! {
|
||||
"_id.user": user_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.count_documents(doc! {
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map(|c| c as usize)
|
||||
.map_err(|_| create_database_error!("count_documents", COL))
|
||||
@@ -151,8 +266,42 @@ impl AbstractServerMembers for MongoDb {
|
||||
.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
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
delete_one,
|
||||
@@ -164,15 +313,31 @@ impl AbstractServerMembers for MongoDb {
|
||||
)
|
||||
.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 {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsMember::JoinedAt => "joined_at",
|
||||
FieldsMember::Avatar => "avatar",
|
||||
FieldsMember::Nickname => "nickname",
|
||||
FieldsMember::Roles => "roles",
|
||||
FieldsMember::Timeout => "timeout",
|
||||
FieldsMember::CanPublish => "is_publishing",
|
||||
FieldsMember::CanReceive => "is_receiving",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@ use revolt_result::Result;
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
|
||||
use super::AbstractServerMembers;
|
||||
use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractServerMembers for ReferenceDb {
|
||||
/// 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;
|
||||
if server_members.contains_key(&member.id) {
|
||||
Err(create_database_error!("insert", "member"))
|
||||
} else {
|
||||
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
|
||||
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;
|
||||
Ok(server_members
|
||||
.values()
|
||||
@@ -40,8 +40,72 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch all members in a server as an iterator
|
||||
async fn fetch_all_members_chunked(
|
||||
&self,
|
||||
server_id: &str,
|
||||
) -> Result<ChunkedServerMembersGenerator> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
|
||||
let members = server_members
|
||||
.clone()
|
||||
.into_values()
|
||||
.filter(move |member| member.id.server == server_id)
|
||||
.collect();
|
||||
|
||||
// this is inefficient as shit but its the reference db so its fine
|
||||
Ok(ChunkedServerMembersGenerator::new_reference(members))
|
||||
}
|
||||
|
||||
/// Fetch all members that have any of the roles given
|
||||
async fn fetch_all_members_with_roles(
|
||||
&self,
|
||||
server_id: &str,
|
||||
roles: &[String],
|
||||
) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
|
||||
Ok(server_members
|
||||
.clone()
|
||||
.into_values()
|
||||
.filter(|member| {
|
||||
member.id.server == server_id
|
||||
&& !member
|
||||
.roles
|
||||
.iter()
|
||||
.filter(|p| roles.contains(*p))
|
||||
.collect::<Vec<&String>>()
|
||||
.is_empty()
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn fetch_all_members_with_roles_chunked(
|
||||
&self,
|
||||
server_id: &str,
|
||||
roles: &[String],
|
||||
) -> Result<ChunkedServerMembersGenerator> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
|
||||
let resp = server_members
|
||||
.clone()
|
||||
.into_values()
|
||||
.filter(|member| {
|
||||
member.id.server == server_id
|
||||
&& !member
|
||||
.roles
|
||||
.iter()
|
||||
.filter(|p| roles.contains(*p))
|
||||
.collect::<Vec<&String>>()
|
||||
.is_empty()
|
||||
})
|
||||
.collect();
|
||||
|
||||
return Ok(ChunkedServerMembersGenerator::new_reference(resp));
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
@@ -51,7 +115,7 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// 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;
|
||||
Ok(ids
|
||||
.iter()
|
||||
@@ -105,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
|
||||
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;
|
||||
if server_members.remove(id).is_some() {
|
||||
Ok(())
|
||||
@@ -114,4 +196,8 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_dangling_members(&self) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ impl Server {
|
||||
}
|
||||
|
||||
/// Update server data
|
||||
pub async fn update<'a>(
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialServer,
|
||||
@@ -228,6 +228,13 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ordered roles list
|
||||
pub fn ordered_roles(&self) -> Vec<(String, Role)> {
|
||||
let mut ordered_roles = self.roles.clone().into_iter().collect::<Vec<_>>();
|
||||
ordered_roles.sort_by(|(_, role_a), (_, role_b)| role_a.rank.cmp(&role_b.rank));
|
||||
ordered_roles
|
||||
}
|
||||
|
||||
/// Set role permission on a server
|
||||
pub async fn set_role_permission(
|
||||
&mut self,
|
||||
@@ -253,6 +260,37 @@ impl Server {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Reorders the server's roles rankings
|
||||
pub async fn set_role_ordering(&mut self, db: &Database, new_order: Vec<String>) -> Result<()> {
|
||||
// New order must always contain every role
|
||||
debug_assert_eq!(self.roles.len(), new_order.len());
|
||||
|
||||
// Set the role's ranks to the positions in the vec
|
||||
for (rank, id) in new_order.iter().enumerate() {
|
||||
self.roles.get_mut(id).unwrap().rank = rank as i64;
|
||||
}
|
||||
|
||||
db.update_server(
|
||||
&self.id,
|
||||
&PartialServer {
|
||||
roles: Some(self.roles.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
Vec::new(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Publish bulk update event
|
||||
EventV1::ServerRoleRanksUpdate {
|
||||
id: self.id.clone(),
|
||||
ranks: new_order,
|
||||
}
|
||||
.p(self.id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Role {
|
||||
@@ -285,7 +323,7 @@ impl Role {
|
||||
}
|
||||
|
||||
/// Update server data
|
||||
pub async fn update<'a>(
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
server_id: &str,
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -25,14 +25,11 @@ impl AbstractServers for MongoDb {
|
||||
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
|
||||
Ok(self
|
||||
.col::<Server>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", "servers"))?
|
||||
.filter_map(|s| async {
|
||||
@@ -84,7 +81,6 @@ impl AbstractServers for MongoDb {
|
||||
.map_err(|_| create_database_error!("to_document", "role"))?
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -125,7 +121,6 @@ impl AbstractServers for MongoDb {
|
||||
"roles": &role_id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_many", "server_members"))?;
|
||||
@@ -140,7 +135,6 @@ impl AbstractServers for MongoDb {
|
||||
"role_permissions.".to_owned() + role_id: 1_i32
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", "channels"))?;
|
||||
@@ -155,7 +149,6 @@ impl AbstractServers for MongoDb {
|
||||
"roles.".to_owned() + role_id: 1_i32
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
@@ -188,12 +181,9 @@ impl MongoDb {
|
||||
// Find all channels
|
||||
let channels: Vec<String> = self
|
||||
.col::<Document>("channels")
|
||||
.find(
|
||||
doc! {
|
||||
"server": server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.find(doc! {
|
||||
"server": server_id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", "channels"))?
|
||||
.filter_map(|s| async {
|
||||
@@ -225,19 +215,15 @@ impl MongoDb {
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_many", "emojis"))?;
|
||||
|
||||
// Delete all channels.
|
||||
self.col::<Document>("channels")
|
||||
.delete_many(
|
||||
doc! {
|
||||
"server": &server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.delete_many(doc! {
|
||||
"server": &server_id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", "channels"))?;
|
||||
|
||||
@@ -248,12 +234,9 @@ impl MongoDb {
|
||||
// Delete members and bans.
|
||||
for with in &["server_members", "server_bans"] {
|
||||
self.col::<Document>(with)
|
||||
.delete_many(
|
||||
doc! {
|
||||
"_id.server": &server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.delete_many(doc! {
|
||||
"_id.server": &server_id
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", with))?;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use revolt_result::Result;
|
||||
|
||||
use crate::UserSettings;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ impl AbstractUserSettings for MongoDb {
|
||||
doc! {
|
||||
"$set": set
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
)
|
||||
.with_options(UpdateOptions::builder().upsert(true).build())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "user_settings"))
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||
use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts};
|
||||
|
||||
use revolt_result::{create_error, Error, Result};
|
||||
|
||||
use crate::{Database, User};
|
||||
|
||||
#[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;
|
||||
|
||||
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()) {
|
||||
let bot = db.fetch_bot_by_token(bot_token).await?;
|
||||
db.fetch_user(&bot.id).await
|
||||
|
||||
@@ -7,11 +7,5 @@ mod rocket;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod schema;
|
||||
|
||||
#[cfg(feature = "axum-impl")]
|
||||
pub use self::axum::*;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::rocket::*;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::schema::*;
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
|
||||
@@ -3,11 +3,12 @@ use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP};
|
||||
|
||||
use authifier::config::{EmailVerificationConfig, Template};
|
||||
use futures::future::join_all;
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0::{self, UserFlags};
|
||||
use revolt_models::v0::{self, UserBadges, UserFlags};
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde_json::json;
|
||||
@@ -56,6 +57,8 @@ auto_derived_partial!(
|
||||
/// Time until user is unsuspended
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub suspended_until: Option<Timestamp>,
|
||||
/// Last acknowledged policy change
|
||||
pub last_acknowledged_policy_change: Timestamp,
|
||||
},
|
||||
"PartialUser"
|
||||
);
|
||||
@@ -177,6 +180,7 @@ impl Default for User {
|
||||
privileged: Default::default(),
|
||||
bot: Default::default(),
|
||||
suspended_until: Default::default(),
|
||||
last_acknowledged_policy_change: Timestamp::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,6 +203,7 @@ impl User {
|
||||
id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()),
|
||||
discriminator: User::find_discriminator(db, &username, None).await?,
|
||||
username,
|
||||
last_acknowledged_policy_change: Timestamp::now_utc(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -348,15 +353,13 @@ impl User {
|
||||
) -> Result<Vec<v0::User>> {
|
||||
let online_ids = filter_online(ids).await;
|
||||
|
||||
Ok(db
|
||||
.fetch_users(ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|user| {
|
||||
Ok(
|
||||
join_all(db.fetch_users(ids).await?.into_iter().map(|user| async {
|
||||
let is_online = online_ids.contains(&user.id);
|
||||
user.into_known(perspective, is_online)
|
||||
})
|
||||
.collect())
|
||||
user.into_known(perspective, is_online).await
|
||||
}))
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
/// Find a free discriminator for a given username
|
||||
@@ -637,7 +640,7 @@ impl User {
|
||||
}
|
||||
|
||||
/// Update user data
|
||||
pub async fn update<'a>(
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialUser,
|
||||
@@ -807,4 +810,18 @@ impl User {
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Gets the user's badges along with calculating any dynamic badges
|
||||
pub async fn get_badges(&self) -> u32 {
|
||||
let config = config().await;
|
||||
let badges = self.badges.unwrap_or_default() as u32;
|
||||
|
||||
if let Some(cutoff) = config.api.users.early_adopter_cutoff {
|
||||
if Ulid::from_string(&self.id).unwrap().timestamp_ms() < cutoff {
|
||||
return badges + UserBadges::EarlyAdopter as u32;
|
||||
};
|
||||
};
|
||||
|
||||
badges
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user