mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-08 03:56:54 +00:00
Compare commits
152 Commits
fix/hosepi
...
feat/admin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cebf6e35b | ||
|
|
db6cd1a014 | ||
|
|
35a11f7916 | ||
|
|
feb936a278 | ||
|
|
f10aaa5bbe | ||
|
|
0e8f220dfb | ||
|
|
cf4b9ba2ca | ||
|
|
08fb35c3f9 | ||
|
|
a22378c35c | ||
|
|
164be6a2da | ||
|
|
9217c6e49b | ||
|
|
23ba7d8fd8 | ||
|
|
6ece5cde3b | ||
|
|
7c2098c611 | ||
|
|
01c7d925b0 | ||
|
|
a15a542f43 | ||
|
|
34dffa9425 | ||
|
|
a386e84c7d | ||
|
|
abdba5ce93 | ||
|
|
ffab2369ab | ||
|
|
d27917b824 | ||
|
|
a7af24b38d | ||
|
|
0af376c26b | ||
|
|
aa907e28c3 | ||
|
|
9fb344a433 | ||
|
|
948fb97e89 | ||
|
|
05bceb6d4a | ||
|
|
e48d92c635 | ||
|
|
3f4df765eb | ||
|
|
a3d390c085 | ||
|
|
c70459b10c | ||
|
|
db4b26251f | ||
|
|
bebfe34922 | ||
|
|
5b769b60de | ||
|
|
0896e68882 | ||
|
|
65acc64034 | ||
|
|
bd987bf72a | ||
|
|
93a00ffbf8 | ||
|
|
7937179db7 | ||
|
|
2d308e03d5 | ||
|
|
b38499a05b | ||
|
|
4815429952 | ||
|
|
0d9ae508d9 | ||
|
|
03b52655ff | ||
|
|
5b1985381a | ||
|
|
018afaf38f | ||
|
|
af0d8aad14 | ||
|
|
acbc087982 | ||
|
|
2871632382 | ||
|
|
494c8b7cab | ||
|
|
26a8692677 | ||
|
|
298742dbad | ||
|
|
6c920de03a | ||
|
|
c902077cf5 | ||
|
|
19ee535f45 | ||
|
|
ee4575470b | ||
|
|
6cfee1f601 | ||
|
|
ab9b8ccfca | ||
|
|
7647cfc8d9 | ||
|
|
8157e1f6e9 | ||
|
|
fcb8091cd7 | ||
|
|
260036488d | ||
|
|
1100eaf46f | ||
|
|
d52e84c5d3 | ||
|
|
d76a71141f | ||
|
|
6f3441cf4a | ||
|
|
23ad135983 | ||
|
|
d46c7f7f3c | ||
|
|
0719985ac5 | ||
|
|
ab5bd47a39 | ||
|
|
9fd7128f80 | ||
|
|
df276ac40b | ||
|
|
356491e934 | ||
|
|
21d82018cf | ||
|
|
6b41db984b | ||
|
|
5378cd22b4 | ||
|
|
841985d3b9 | ||
|
|
2d8b7481bc | ||
|
|
8bd5542e71 | ||
|
|
279f5d5fd7 | ||
|
|
e93769786c | ||
|
|
ed4fd5ebfe | ||
|
|
89171e9bd0 | ||
|
|
533cc5835a | ||
|
|
80b749d6fc | ||
|
|
057f2bb8b3 | ||
|
|
3675ff1a1f | ||
|
|
74ce10162c | ||
|
|
9a67cb139c | ||
|
|
5e98a01624 | ||
|
|
9fe7de709a | ||
|
|
5b9a909923 | ||
|
|
913946cfa1 | ||
|
|
01e0c30596 | ||
|
|
1bb12b416a | ||
|
|
5e577358ac | ||
|
|
144e939c6b | ||
|
|
61fd13629f | ||
|
|
6e78f0b9c0 | ||
|
|
26faa18d6e | ||
|
|
a152d825a2 | ||
|
|
85ad261cc3 | ||
|
|
47336a7940 | ||
|
|
4ea8f90c0d | ||
|
|
fb8fe16557 | ||
|
|
f2c056a151 | ||
|
|
f30b729ca9 | ||
|
|
f81e3291bd | ||
|
|
1e80916b65 | ||
|
|
8814bf4c23 | ||
|
|
5466ada9ae | ||
|
|
f0e513ccae | ||
|
|
4d4b0dd864 | ||
|
|
f181edc8f2 | ||
|
|
ccda6f5c53 | ||
|
|
52ed5100c2 | ||
|
|
c2d4369e16 | ||
|
|
91783b9066 | ||
|
|
735d644e04 | ||
|
|
d1e72cee42 | ||
|
|
6107f242fd | ||
|
|
a5cd08a655 | ||
|
|
98c7b1b5a5 | ||
|
|
ec22deb2cd | ||
|
|
ecb94e278c | ||
|
|
1a9d5b4b65 | ||
|
|
abfb3925fd | ||
|
|
2a73d82250 | ||
|
|
edfa97db10 | ||
|
|
cf2cedcee1 | ||
|
|
b830631bd2 | ||
|
|
3fa0abf47f | ||
|
|
5191bd16b2 | ||
|
|
2547fc6cd3 | ||
|
|
5701b5c18c | ||
|
|
dc4438bc3c | ||
|
|
f2fc1ee00c | ||
|
|
ab525699bd | ||
|
|
d56135e0cb | ||
|
|
52c0d2f266 | ||
|
|
49c6289580 | ||
|
|
b0b728fb0d | ||
|
|
d629e89304 | ||
|
|
f857ca6926 | ||
|
|
73ce052e1a | ||
|
|
4fd71c98d6 | ||
|
|
96f046bc76 | ||
|
|
6ecfd14b07 | ||
|
|
bf4530563a | ||
|
|
ff36d71ad5 | ||
|
|
f8eb324543 | ||
|
|
03db5b1ddf |
24
.github/pull_request_template.md
vendored
Normal file
24
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<!-- Describe your changes -->
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## How was this PR tested?
|
||||
|
||||
<!-- What did you do to test your changes? -->
|
||||
|
||||
- [ ] Test A
|
||||
- [ ] Test B
|
||||
|
||||
## Checklist:
|
||||
|
||||
- [ ] I have carefully read [the contributing guidelines](https://developers.stoat.chat/developing/contrib/)
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have made corresponding changes to the documentation if applicable
|
||||
- [ ] I have no unrelated changes in the PR
|
||||
- [ ] I have confirmed that any new dependencies are strictly necessary
|
||||
- [ ] I have written tests for new code (if applicable)
|
||||
- [ ] I have followed naming conventions/patterns in the surrounding code
|
||||
|
||||
## Please declare, if any, LLM usage involved in creating this PR
|
||||
|
||||
...
|
||||
46
.github/workflows/docker-cleanup.yaml
vendored
Normal file
46
.github/workflows/docker-cleanup.yaml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Docker PR Image Cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: docker-cleanup-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package:
|
||||
- base
|
||||
- api
|
||||
- events
|
||||
- file-server
|
||||
- proxy
|
||||
- gifbox
|
||||
- crond
|
||||
- pushd
|
||||
- voice-ingress
|
||||
steps:
|
||||
- env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ORG: stoatchat
|
||||
PACKAGE: ${{ matrix.package }}
|
||||
TAG: pr-${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh api --paginate \
|
||||
"/orgs/${ORG}/packages/container/${PACKAGE}/versions" \
|
||||
--jq ".[] | select(.metadata.container.tags | index(\"${TAG}\")) | .id" \
|
||||
| while read -r id; do
|
||||
gh api -X DELETE "/orgs/${ORG}/packages/container/${PACKAGE}/versions/${id}"
|
||||
done
|
||||
39
.github/workflows/docker.yaml
vendored
39
.github/workflows/docker.yaml
vendored
@@ -5,8 +5,6 @@ on:
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -14,14 +12,14 @@ permissions:
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
group: docker-test-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
base:
|
||||
name: Test base image build
|
||||
name: Test base image build (fork)
|
||||
runs-on: arc-runner-set
|
||||
if: github.event_name == 'pull_request'
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
@@ -42,7 +40,7 @@ jobs:
|
||||
|
||||
publish:
|
||||
runs-on: arc-runner-set
|
||||
if: github.event_name != 'pull_request'
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
|
||||
name: Publish Docker images
|
||||
steps:
|
||||
# Configure build environment
|
||||
@@ -59,6 +57,15 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Determine base image tag
|
||||
id: base
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
echo "tag=pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tag=latest" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Build the image
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v4
|
||||
@@ -66,7 +73,9 @@ jobs:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
tags: ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
cache-from: type=gha,scope=buildx-base-multi-arch
|
||||
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
|
||||
|
||||
# stoatchat/api
|
||||
- name: Docker meta
|
||||
@@ -84,7 +93,7 @@ jobs:
|
||||
file: crates/delta/Dockerfile
|
||||
tags: ${{ steps.meta-delta.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-delta.outputs.labels }}
|
||||
|
||||
# stoatchat/events
|
||||
@@ -103,7 +112,7 @@ jobs:
|
||||
file: crates/bonfire/Dockerfile
|
||||
tags: ${{ steps.meta-bonfire.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-bonfire.outputs.labels }}
|
||||
|
||||
# stoatchat/file-server
|
||||
@@ -122,7 +131,7 @@ jobs:
|
||||
file: crates/services/autumn/Dockerfile
|
||||
tags: ${{ steps.meta-autumn.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-autumn.outputs.labels }}
|
||||
|
||||
# stoatchat/proxy
|
||||
@@ -141,7 +150,7 @@ jobs:
|
||||
file: crates/services/january/Dockerfile
|
||||
tags: ${{ steps.meta-january.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-january.outputs.labels }}
|
||||
|
||||
# stoatchat/gifbox
|
||||
@@ -160,7 +169,7 @@ jobs:
|
||||
file: crates/services/gifbox/Dockerfile
|
||||
tags: ${{ steps.meta-gifbox.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-gifbox.outputs.labels }}
|
||||
|
||||
# stoatchat/crond
|
||||
@@ -179,7 +188,7 @@ jobs:
|
||||
file: crates/daemons/crond/Dockerfile
|
||||
tags: ${{ steps.meta-crond.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-crond.outputs.labels }}
|
||||
|
||||
# stoatchat/pushd
|
||||
@@ -198,7 +207,7 @@ jobs:
|
||||
file: crates/daemons/pushd/Dockerfile
|
||||
tags: ${{ steps.meta-pushd.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-pushd.outputs.labels }}
|
||||
|
||||
# stoatchat/voice-ingress
|
||||
@@ -217,5 +226,5 @@ jobs:
|
||||
file: crates/daemons/voice-ingress/Dockerfile
|
||||
tags: ${{ steps.meta-voice-ingress.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-voice-ingress.outputs.labels }}
|
||||
|
||||
6
.github/workflows/git-town.yml
vendored
6
.github/workflows/git-town.yml
vendored
@@ -1,3 +1,6 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: Git Town
|
||||
|
||||
on:
|
||||
@@ -8,7 +11,7 @@ jobs:
|
||||
name: Display the branch stack
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
if: ${{ !startsWith(github.head_ref, 'release-please--') }}
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !startsWith(github.head_ref, 'release-please--') }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -17,3 +20,4 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: stoatchat/action-git-town@4bc5c942e4603bffa0806b51d5fe5f0bc5deb0ac
|
||||
continue-on-error: true
|
||||
2
.github/workflows/publish-crates.yml
vendored
2
.github/workflows/publish-crates.yml
vendored
@@ -21,4 +21,6 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: mise publish --workspace
|
||||
|
||||
3
.github/workflows/release-webhook.yml
vendored
3
.github/workflows/release-webhook.yml
vendored
@@ -1,3 +1,6 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: Release Webhook
|
||||
|
||||
on:
|
||||
|
||||
30
.github/workflows/renovate.yml
vendored
Normal file
30
.github/workflows/renovate.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: Renovate
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0/15 * * * *'
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_STOAT_RELEASE_APP_ID }}
|
||||
private-key: ${{ secrets.GH_STOAT_RELEASE_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
|
||||
with:
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
- name: Self-hosted Renovate
|
||||
uses: renovatebot/github-action@v46.1.14
|
||||
with:
|
||||
token: '${{ steps.app-token.outputs.token }}'
|
||||
env:
|
||||
RENOVATE_PLATFORM_COMMIT: 'enabled'
|
||||
RENOVATE_REPOSITORIES: '${{ github.repository }}'
|
||||
4
.github/workflows/rust.yaml
vendored
4
.github/workflows/rust.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
group: rust-build-test-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -37,6 +37,7 @@ jobs:
|
||||
- name: Reference Test
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
continue-on-error: ${{ github.ref_name == 'main' }}
|
||||
run: |
|
||||
mise test
|
||||
|
||||
@@ -44,6 +45,7 @@ jobs:
|
||||
env:
|
||||
TEST_DB: MONGODB
|
||||
MONGODB: mongodb://localhost
|
||||
continue-on-error: ${{ github.ref_name == 'main' }}
|
||||
run: |
|
||||
mise test
|
||||
|
||||
|
||||
5
.github/workflows/validate-pr-title.yml
vendored
5
.github/workflows/validate-pr-title.yml
vendored
@@ -1,3 +1,6 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: "Lint PR"
|
||||
|
||||
on:
|
||||
@@ -15,6 +18,6 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@v6
|
||||
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -7,7 +7,7 @@ gh = "2.25.0"
|
||||
rust = "1.92.0"
|
||||
"cargo:cargo-nextest" = "0.9.122"
|
||||
|
||||
"github:git-town/git-town" = "22.4.0"
|
||||
"github:git-town/git-town" = "22.7.1"
|
||||
|
||||
[settings]
|
||||
experimental = true
|
||||
@@ -16,4 +16,13 @@ idiomatic_version_file_enable_tools = ["rust"]
|
||||
[tasks.start]
|
||||
description = "Run all services"
|
||||
depends = ["docker:start", "build"]
|
||||
wait_for = ["docker:start", "build"]
|
||||
run = [{ task = "service:*" }]
|
||||
|
||||
[env]
|
||||
BUILDER = "cargo"
|
||||
DOCKER_NETWORK_NAME = "stoatchat_default"
|
||||
DATABASE_PORT = "27017"
|
||||
RABBIT_PORT = "5672"
|
||||
REDIS_PORT = "6379"
|
||||
_.file = { path = ".env", tools = true }
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
#MISE description="Build project"
|
||||
set -e
|
||||
|
||||
cargo build "$@"
|
||||
${BUILDER} build "$@"
|
||||
|
||||
@@ -3,3 +3,11 @@
|
||||
set -e
|
||||
|
||||
docker compose up -d
|
||||
|
||||
docker run \
|
||||
--network=${DOCKER_NETWORK_NAME} \
|
||||
--name wait \
|
||||
--rm dokku/wait -c \
|
||||
rabbit:${RABBIT_PORT},\
|
||||
database:${DATABASE_PORT},\
|
||||
redis:${REDIS_PORT}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "0.11.5"
|
||||
".": "0.13.7"
|
||||
}
|
||||
139
CHANGELOG.md
139
CHANGELOG.md
@@ -1,5 +1,144 @@
|
||||
# Changelog
|
||||
|
||||
## [0.13.7](https://github.com/stoatchat/stoatchat/compare/v0.13.6...v0.13.7) (2026-05-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* sanitize emoji input to handle variation selectors ([#774](https://github.com/stoatchat/stoatchat/issues/774)) ([2d308e0](https://github.com/stoatchat/stoatchat/commit/2d308e03d58c19f27b5b4d65dc2a15ef20b56190))
|
||||
* update mention count badge for channel acks ([#769](https://github.com/stoatchat/stoatchat/issues/769)) ([0d9ae50](https://github.com/stoatchat/stoatchat/commit/0d9ae508d9d2199f0e408b8ca634d20489be6f61))
|
||||
|
||||
## [0.13.6](https://github.com/stoatchat/stoatchat/compare/v0.13.5...v0.13.6) (2026-05-18)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Update FCM payload for android notifications ([#766](https://github.com/stoatchat/stoatchat/issues/766)) ([acbc087](https://github.com/stoatchat/stoatchat/commit/acbc087982e9aeb05cabc5ab4c9b1291f67490ad))
|
||||
* user slowmode events ([#760](https://github.com/stoatchat/stoatchat/issues/760)) ([af0d8aa](https://github.com/stoatchat/stoatchat/commit/af0d8aad14dc68d88159d0e1c714077d362e21e4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* include `minio` region as tests need it ([#761](https://github.com/stoatchat/stoatchat/issues/761)) ([298742d](https://github.com/stoatchat/stoatchat/commit/298742dbad4eafae356f976c56b9db23904b0c3a))
|
||||
* set env var for publishing crates ([#768](https://github.com/stoatchat/stoatchat/issues/768)) ([018afaf](https://github.com/stoatchat/stoatchat/commit/018afaf38f6330d92dad2a68b640c0cb3f6b639a))
|
||||
* Use proper headers to determine IP when not behind cloudflare ([#764](https://github.com/stoatchat/stoatchat/issues/764)) ([494c8b7](https://github.com/stoatchat/stoatchat/commit/494c8b7cabaae2a51039a7a5b559d5e2e5279554))
|
||||
* voice ingress crashing due to new Result in AMQP::new_auto() ([#765](https://github.com/stoatchat/stoatchat/issues/765)) ([2871632](https://github.com/stoatchat/stoatchat/commit/2871632382395cb20cbe0047c542d3ac31ff3f03))
|
||||
|
||||
|
||||
### Miscellaneous Chores
|
||||
|
||||
* switch to lapin ([#767](https://github.com/stoatchat/stoatchat/issues/767)) ([5b19853](https://github.com/stoatchat/stoatchat/commit/5b1985381ae829a92c80a19e91a414cd9dc4de93))
|
||||
|
||||
## [0.13.5](https://github.com/stoatchat/stoatchat/compare/v0.13.4...v0.13.5) (2026-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* dont panic on hash missing when deleting files ([#755](https://github.com/stoatchat/stoatchat/issues/755)) ([c902077](https://github.com/stoatchat/stoatchat/commit/c902077cf51076fee11712eb732dc8a8f786fc4b))
|
||||
|
||||
## [0.13.4](https://github.com/stoatchat/stoatchat/compare/v0.13.3...v0.13.4) (2026-05-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add TLS feature to livekit-api crate ([#753](https://github.com/stoatchat/stoatchat/issues/753)) ([6cfee1f](https://github.com/stoatchat/stoatchat/commit/6cfee1f601c1e084df7c8f1e7a5e8a560d1dd514))
|
||||
|
||||
## [0.13.3](https://github.com/stoatchat/stoatchat/compare/v0.13.2...v0.13.3) (2026-05-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't automatically set up rabbitmq in delta ([#749](https://github.com/stoatchat/stoatchat/issues/749)) ([7647cfc](https://github.com/stoatchat/stoatchat/commit/7647cfc8d93aba99f5faef13eb3d970097540d76))
|
||||
* don't declare queues which seem to cause the backend to crash in prod ([7647cfc](https://github.com/stoatchat/stoatchat/commit/7647cfc8d93aba99f5faef13eb3d970097540d76))
|
||||
|
||||
## [0.13.2](https://github.com/stoatchat/stoatchat/compare/v0.13.1...v0.13.2) (2026-05-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update default exchange to `revolt.default` ([#746](https://github.com/stoatchat/stoatchat/issues/746)) ([fcb8091](https://github.com/stoatchat/stoatchat/commit/fcb8091cd7a00d7f26c798daa33aae4b923b2a8b))
|
||||
|
||||
## [0.13.1](https://github.com/stoatchat/stoatchat/compare/v0.13.0...v0.13.1) (2026-05-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* amqprs startup bug ([#744](https://github.com/stoatchat/stoatchat/issues/744)) ([1100eaf](https://github.com/stoatchat/stoatchat/commit/1100eaf46f849f2509ae01ac497556ca33bde778))
|
||||
|
||||
## [0.13.0](https://github.com/stoatchat/stoatchat/compare/v0.12.1...v0.13.0) (2026-05-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add embed support for YouTube Shorts ([#734](https://github.com/stoatchat/stoatchat/issues/734)) ([d46c7f7](https://github.com/stoatchat/stoatchat/commit/d46c7f7f3c04524c0639c3e0a122626f8e0b3bf7))
|
||||
* add emoji rename endpoint ([#714](https://github.com/stoatchat/stoatchat/issues/714)) ([23ad135](https://github.com/stoatchat/stoatchat/commit/23ad1359834bb7d07a460b8678d6a6ebffc73eb0))
|
||||
* add legal links to root payload ([#733](https://github.com/stoatchat/stoatchat/issues/733)) ([21d8201](https://github.com/stoatchat/stoatchat/commit/21d82018cf84ab0fdd10613d254b9562aea8eea3))
|
||||
* add role icon support ([#724](https://github.com/stoatchat/stoatchat/issues/724)) ([841985d](https://github.com/stoatchat/stoatchat/commit/841985d3b994df1c6eefab2fc7ecbd77ab22c493))
|
||||
* Add webhook endpoints for editing and deleting messages ([#682](https://github.com/stoatchat/stoatchat/issues/682)) ([6f3441c](https://github.com/stoatchat/stoatchat/commit/6f3441cf4acac2a8e6e1bf07a279a153b80f7956))
|
||||
* automatically sanitise usernames on create/update ([#689](https://github.com/stoatchat/stoatchat/issues/689)) ([e937697](https://github.com/stoatchat/stoatchat/commit/e93769786c7669485a659ee471630740d3cea702))
|
||||
* blacklist private ip ranges and add january domain blocklist ([#731](https://github.com/stoatchat/stoatchat/issues/731)) ([6b41db9](https://github.com/stoatchat/stoatchat/commit/6b41db984bb491b2e58324309cc70d8c14e0b814))
|
||||
* Rewrite acks ([#741](https://github.com/stoatchat/stoatchat/issues/741)) ([ab5bd47](https://github.com/stoatchat/stoatchat/commit/ab5bd47a39ee889de0b5ae6e7b560620853daead))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add new_user_hours to configuration limits ([#729](https://github.com/stoatchat/stoatchat/issues/729)) ([279f5d5](https://github.com/stoatchat/stoatchat/commit/279f5d5fd7af2df55902c706859ec07f569cdb1e))
|
||||
* add reconnection policy to Redis subscriber to prevent ghost state ([#708](https://github.com/stoatchat/stoatchat/issues/708)) ([057f2bb](https://github.com/stoatchat/stoatchat/commit/057f2bb8b359f8b942741a30ff54eeb8fbe3e0b1))
|
||||
* docker compose file had personal url in it ([#742](https://github.com/stoatchat/stoatchat/issues/742)) ([0719985](https://github.com/stoatchat/stoatchat/commit/0719985ac5636590f91e6f9ec4b68f3eded70c13))
|
||||
* don't strip ICC from exif ([#735](https://github.com/stoatchat/stoatchat/issues/735)) ([d76a711](https://github.com/stoatchat/stoatchat/commit/d76a71141f3e508f6308ba52fa28eaeb56fb3438))
|
||||
* dont send notification in fcm ([#721](https://github.com/stoatchat/stoatchat/issues/721)) ([89171e9](https://github.com/stoatchat/stoatchat/commit/89171e9bd0f15711157e78c6eec0fe7b480de93a))
|
||||
* encode filenames in redirects ([#737](https://github.com/stoatchat/stoatchat/issues/737)) ([9fd7128](https://github.com/stoatchat/stoatchat/commit/9fd7128f800badbd184baf943d4f799e601201e4))
|
||||
* january ip redirects & domain resolver ([#738](https://github.com/stoatchat/stoatchat/issues/738)) ([356491e](https://github.com/stoatchat/stoatchat/commit/356491e934b274f9e895df883dd63ef0b3123510))
|
||||
* update message length validation to remove upper limit ([#723](https://github.com/stoatchat/stoatchat/issues/723)) ([ed4fd5e](https://github.com/stoatchat/stoatchat/commit/ed4fd5ebfe6d0ea534a0898da4afdc1f4e2cd6c5))
|
||||
* use correct response for NoEffect errors ([#732](https://github.com/stoatchat/stoatchat/issues/732)) ([5378cd2](https://github.com/stoatchat/stoatchat/commit/5378cd22b4c7d85f44c31a6af0dda00941b80d5c))
|
||||
|
||||
## [0.12.1](https://github.com/stoatchat/stoatchat/compare/v0.12.0...v0.12.1) (2026-04-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add migration to update existing files to be animated ([#705](https://github.com/stoatchat/stoatchat/issues/705)) ([f2c056a](https://github.com/stoatchat/stoatchat/commit/f2c056a1515be493b195f3f5db5886c2ddf36700))
|
||||
* don't send self dm notifications ([#706](https://github.com/stoatchat/stoatchat/issues/706)) ([f30b729](https://github.com/stoatchat/stoatchat/commit/f30b729ca90d0be6853c57ab4935694e5e59ae56))
|
||||
* mise start + missing docker image ([#564](https://github.com/stoatchat/stoatchat/issues/564)) ([fb8fe16](https://github.com/stoatchat/stoatchat/commit/fb8fe1655776791421284a6a093e86f0320c258a))
|
||||
* test failure due to wrong assertion ([#707](https://github.com/stoatchat/stoatchat/issues/707)) ([f81e329](https://github.com/stoatchat/stoatchat/commit/f81e3291bdd57af9ceedb2987b111acc7051d69c))
|
||||
|
||||
## [0.12.0](https://github.com/stoatchat/stoatchat/compare/v0.11.5...v0.12.0) (2026-03-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add bug report template for issue tracking ([#627](https://github.com/stoatchat/stoatchat/issues/627)) ([f777e28](https://github.com/stoatchat/stoatchat/commit/f777e2863c6ca50057c8b5d0a5be14915d287724))
|
||||
* Add slowmode functionality to text channels ([#680](https://github.com/stoatchat/stoatchat/issues/680)) ([6107f24](https://github.com/stoatchat/stoatchat/commit/6107f242fd3ebaff71a15f9a16330ffbcb4f2d7b))
|
||||
* Allow restricting server creation to specific users ([#685](https://github.com/stoatchat/stoatchat/issues/685)) ([edfa97d](https://github.com/stoatchat/stoatchat/commit/edfa97db108c9c81828547f98a1db5315cb5ba4a))
|
||||
* compute thumbhash for images ([#596](https://github.com/stoatchat/stoatchat/issues/596)) ([c2d4369](https://github.com/stoatchat/stoatchat/commit/c2d4369e160f32d79bce0a0b0f14677f89de3669))
|
||||
* Detect animation in image files for fetch_preview ([#574](https://github.com/stoatchat/stoatchat/issues/574)) ([3fa0abf](https://github.com/stoatchat/stoatchat/commit/3fa0abf47f5f42ddd8ee041fe4c44fbc5ba800c1))
|
||||
* expose global and user limits in root API response ([#644](https://github.com/stoatchat/stoatchat/issues/644)) ([0b522eb](https://github.com/stoatchat/stoatchat/commit/0b522ebddc17f2e3f792ff5e2347793e9849fa23))
|
||||
* implement time based message sweep on user ban ([#670](https://github.com/stoatchat/stoatchat/issues/670)) ([98c7b1b](https://github.com/stoatchat/stoatchat/commit/98c7b1b5a5b9fdac5c0ab83be10f0e23114dbfc9))
|
||||
* load config from env vars ([#576](https://github.com/stoatchat/stoatchat/issues/576)) ([5191bd1](https://github.com/stoatchat/stoatchat/commit/5191bd16b2a905b8409838e34eb0baca96f08580))
|
||||
* parse message push notification content and replace internal formatting ([#693](https://github.com/stoatchat/stoatchat/issues/693)) ([d1e72ce](https://github.com/stoatchat/stoatchat/commit/d1e72cee42c54e16f4e49af569897528b10a28ca))
|
||||
* Transfer ownership ([#396](https://github.com/stoatchat/stoatchat/issues/396)) ([735d644](https://github.com/stoatchat/stoatchat/commit/735d644e043793cb86e74aab5b88bb4b8bc17ba2))
|
||||
* update livekit ([#698](https://github.com/stoatchat/stoatchat/issues/698)) ([f181edc](https://github.com/stoatchat/stoatchat/commit/f181edc8f2ff3ce4b6d48938dfc73931ecfa2279))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add flag for disabling events instead of commenting them out ([#695](https://github.com/stoatchat/stoatchat/issues/695)) ([a5cd08a](https://github.com/stoatchat/stoatchat/commit/a5cd08a655dece4269f3ac84fa2387ae356709a5))
|
||||
* add masquerade permission to default direct message settings ([#665](https://github.com/stoatchat/stoatchat/issues/665)) ([ab52569](https://github.com/stoatchat/stoatchat/commit/ab525699bd6663333f0e9fed6d2455e482e6a09f))
|
||||
* Check for appropriate permission for removing other users avatar ([#657](https://github.com/stoatchat/stoatchat/issues/657)) ([d56135e](https://github.com/stoatchat/stoatchat/commit/d56135e0cbc713884c9378832952f7ad490fa315))
|
||||
* default video resolution is a non-existent size ([#601](https://github.com/stoatchat/stoatchat/issues/601)) ([0698e11](https://github.com/stoatchat/stoatchat/commit/0698e115e8d003d615e468c4fb9654e6bbc9107f)), closes [#588](https://github.com/stoatchat/stoatchat/issues/588)
|
||||
* **docs:** Update GitHub links ([#647](https://github.com/stoatchat/stoatchat/issues/647)) ([b830631](https://github.com/stoatchat/stoatchat/commit/b830631bd25a546844b7bdd30386084bb365e4de))
|
||||
* don't use a bitop for OR ([#676](https://github.com/stoatchat/stoatchat/issues/676)) ([5701b5c](https://github.com/stoatchat/stoatchat/commit/5701b5c18c513f796af365169ceaea372a22638c))
|
||||
* Fix typo for p256dh in vapid notification flow ([#622](https://github.com/stoatchat/stoatchat/issues/622)) ([a80ad1c](https://github.com/stoatchat/stoatchat/commit/a80ad1cbe58b8af5e45751e51d94d93c1cea1c9f))
|
||||
* improve generated openapi.json ([#584](https://github.com/stoatchat/stoatchat/issues/584)) ([52ed510](https://github.com/stoatchat/stoatchat/commit/52ed5100c2446e0b261085639e123e7e124cab2c))
|
||||
* no node state set on channel creation ([#653](https://github.com/stoatchat/stoatchat/issues/653)) ([24d0d2b](https://github.com/stoatchat/stoatchat/commit/24d0d2b7266f6f8a692d0a52704acfecf517674c))
|
||||
* only show first line on commit messages ([#696](https://github.com/stoatchat/stoatchat/issues/696)) ([91783b9](https://github.com/stoatchat/stoatchat/commit/91783b906697fc85305dee683f7c15dda55f0c50))
|
||||
* pass &str to Reference ([#697](https://github.com/stoatchat/stoatchat/issues/697)) ([ccda6f5](https://github.com/stoatchat/stoatchat/commit/ccda6f5c53ee043705f7ff6b5f6c393f020781de))
|
||||
* redis_url vs redis_uri in config ([#666](https://github.com/stoatchat/stoatchat/issues/666)) ([b0b728f](https://github.com/stoatchat/stoatchat/commit/b0b728fb0dbc9ee28360301de1c3ea501bbbff1d))
|
||||
* replace some links and Revolt mentions to current Stoat ([#515](https://github.com/stoatchat/stoatchat/issues/515)) ([d629e89](https://github.com/stoatchat/stoatchat/commit/d629e89304be2f0011e189293b278f07d346aa7d))
|
||||
* send push notifications for DM and group messages ([#660](https://github.com/stoatchat/stoatchat/issues/660)) ([52c0d2f](https://github.com/stoatchat/stoatchat/commit/52c0d2f266b76d8975bba2d5e75c62bb30149c45))
|
||||
* store server id in redis and in room metadata to be able to delete voice state in all scenarios ([#656](https://github.com/stoatchat/stoatchat/issues/656)) ([49c6289](https://github.com/stoatchat/stoatchat/commit/49c628958070e4f0a5edc764d3b48158589219d9))
|
||||
* uname is missing from crond ([#675](https://github.com/stoatchat/stoatchat/issues/675)) ([dc4438b](https://github.com/stoatchat/stoatchat/commit/dc4438bc3c7b2cad8d442b3cd438afb9ed566a5e))
|
||||
|
||||
## [0.11.5](https://github.com/stoatchat/stoatchat/compare/v0.11.4...v0.11.5) (2026-02-17)
|
||||
|
||||
|
||||
|
||||
5433
Cargo.lock
generated
5433
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
174
Cargo.toml
174
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
resolver = "3"
|
||||
|
||||
members = [
|
||||
"crates/delta",
|
||||
@@ -20,24 +20,132 @@ lto = true
|
||||
[workspace.dependencies]
|
||||
# Async
|
||||
async-trait = "0.1.89"
|
||||
tokio = { version = "1.49.0", features = ["macros", "rt"] }
|
||||
tokio = "1.49.0"
|
||||
async-channel = "2.3.1"
|
||||
futures = "0.3.32"
|
||||
async-tungstenite = "0.17.0"
|
||||
futures-locks = "0.7.1"
|
||||
async-lock = "2.8.0"
|
||||
async-recursion = "1.0.4"
|
||||
tokio-util = { version = "0.7.18" }
|
||||
tokio-stream = "0.1.18"
|
||||
|
||||
# Error Handling
|
||||
anyhow = "1.0.100"
|
||||
thiserror = "2.0.18"
|
||||
sentry = "0.31.5"
|
||||
sentry-anyhow = "0.38.1"
|
||||
|
||||
# Other Utilities
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
# Data Validation
|
||||
regex = "1.12.3"
|
||||
validator = "0.16"
|
||||
|
||||
# Data Types
|
||||
uuid = "1.19.0"
|
||||
ulid = "1.2.1"
|
||||
nanoid = "0.4.0"
|
||||
typenum = "1.17.0"
|
||||
num_enum = "0.6.1"
|
||||
bitfield = "0.13.2"
|
||||
|
||||
# Time
|
||||
chrono = "0.4.15"
|
||||
iso8601-timestamp = "0.2.10"
|
||||
|
||||
# Data Collections
|
||||
lru = "0.16.3"
|
||||
indexmap = "2.13.1"
|
||||
dashmap = "5.2.0"
|
||||
moka = "0.12.8"
|
||||
lru_time_cache = "0.11.11"
|
||||
deadqueue = "0.2.4"
|
||||
|
||||
# Web scraping
|
||||
scraper = "0.20.0"
|
||||
encoding_rs = "0.8.34"
|
||||
|
||||
# Mail
|
||||
lettre = "0.10.0-alpha.4"
|
||||
handlebars = "4.3.0"
|
||||
|
||||
# HTTP Requests
|
||||
reqwest = "0.13.2"
|
||||
isahc = "1.7"
|
||||
|
||||
# Notifications
|
||||
fcm_v1 = "0.3.0"
|
||||
web-push = "0.10.0"
|
||||
revolt_a2 = "0.10"
|
||||
|
||||
# Parsing
|
||||
logos = "0.15"
|
||||
|
||||
# SVG rendering
|
||||
usvg = "0.44.0"
|
||||
resvg = "0.44.0"
|
||||
tiny-skia = "0.11.4"
|
||||
|
||||
# Logging
|
||||
log = "0.4.29"
|
||||
pretty_env_logger = "0.4.0"
|
||||
|
||||
# Redis
|
||||
redis-kiss = { version = "0.1.4", default-features = false }
|
||||
fred = "8.0.1"
|
||||
|
||||
# Serialisation
|
||||
bincode = "1.3.3"
|
||||
serde_json = "1.0.79"
|
||||
rmp-serde = "1.0.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
strum_macros = "0.26.4"
|
||||
|
||||
# MongoDB
|
||||
bson = { version = "2.1.0" }
|
||||
mongodb = { version = "3.1.0" }
|
||||
|
||||
# S3
|
||||
aws-config = "1.5.5"
|
||||
aws-sdk-s3 = "1.46.0"
|
||||
|
||||
# Axum (HTTP server)
|
||||
axum-macros = "0.4.1"
|
||||
axum_typed_multipart = "0.12.1"
|
||||
axum = { version = "0.7.5", features = ["multipart"] }
|
||||
tower-http = { version = "0.5.2", features = ["cors", "trace"] }
|
||||
axum = "0.7.5"
|
||||
axum-extra = "0.9"
|
||||
tower-http = "0.5.2"
|
||||
|
||||
# Rocket (HTTP server)
|
||||
rocket = "0.5.1"
|
||||
rocket_empty = "0.1.1"
|
||||
revolt_rocket_okapi = "0.10.0"
|
||||
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" }
|
||||
rocket_authifier = "1.0.16"
|
||||
rocket_prometheus = "0.10.0-rc.3"
|
||||
|
||||
# Spec Generation
|
||||
utoipa = "4.2.3"
|
||||
revolt_okapi = "0.9.1"
|
||||
schemars = "0.8.8"
|
||||
utoipa-scalar = "0.1.0"
|
||||
|
||||
# Image Processing
|
||||
jxl-oxide = { version = "0.12.5", features = ["image"] }
|
||||
image = "0.25.9"
|
||||
jxl-oxide = "0.12.5"
|
||||
sha2 = "0.10.8"
|
||||
kamadak-exif = "0.5.4"
|
||||
webp = "0.3.0"
|
||||
image = "0.25.2" # avif encode requires dav1d system library: features = ["avif-native"]
|
||||
thumbhash = "0.1.0"
|
||||
lcms2 = "6.1.1" # for color profile processing
|
||||
|
||||
# File processing
|
||||
revolt_clamav-client = "0.1.5"
|
||||
simdutf8 = "0.1.4"
|
||||
|
||||
# Content type processing
|
||||
infer = "0.16.0"
|
||||
ffprobe = "0.4.0"
|
||||
imagesize = "0.13.0"
|
||||
|
||||
# OpenTelemetry
|
||||
tracing = "0.1.44"
|
||||
@@ -47,4 +155,52 @@ tracing-subscriber = { version = "0.3.22", features = [
|
||||
opentelemetry = { version = "0.31.0", features = ["logs"] }
|
||||
opentelemetry_sdk = { version = "0.31.0", features = ["logs"] }
|
||||
opentelemetry-otlp = { version = "0.31.0", features = ["logs"] }
|
||||
opentelemetry-appender-tracing = { version = "0.31.1" }
|
||||
opentelemetry-appender-tracing = "0.31.1"
|
||||
|
||||
# RabbitMQ
|
||||
lapin = "4.7.1"
|
||||
|
||||
# Voice
|
||||
livekit-api = "=0.4.23"
|
||||
livekit-protocol = "=0.7.7"
|
||||
livekit-runtime = "0.4.0"
|
||||
|
||||
# Other Utilities
|
||||
once_cell = "1.9.0"
|
||||
config = "0.13.3"
|
||||
cached = "0.44.0"
|
||||
rand = "0.8.5"
|
||||
base64 = "0.21.3"
|
||||
decancer = "3.3.3"
|
||||
linkify = "0.8.1"
|
||||
url-escape = "0.1.1"
|
||||
revolt_optional_struct = "0.2.0"
|
||||
unicode-segmentation = "1.10.1"
|
||||
querystring = "1.1.0"
|
||||
tempfile = "3.12.0"
|
||||
aes-gcm = "0.10.3"
|
||||
auto_ops = "0.3.0"
|
||||
url = "2.2.2"
|
||||
impl_ops = "0.1.1"
|
||||
lazy_static = "1.5.0"
|
||||
mime = "0.3.17"
|
||||
totp-lite = "2.0.0"
|
||||
rust-argon2 = "1.0.0"
|
||||
base32 = "0.4.0"
|
||||
sha1 = "0.10.6"
|
||||
futures-lite = "2.6.1"
|
||||
|
||||
# Build Dependencies
|
||||
vergen = "7.5.0"
|
||||
|
||||
# Local packages
|
||||
revolt-coalesced = { version = "0.13.7", path = "crates/core/coalesced" }
|
||||
revolt-config = { version = "0.13.7", path = "crates/core/config" }
|
||||
revolt-database = { version = "0.13.7", path = "crates/core/database" }
|
||||
revolt-files = { version = "0.13.7", path = "crates/core/files" }
|
||||
revolt-models = { version = "0.13.7", path = "crates/core/models" }
|
||||
revolt-parser = { version = "0.13.7", path = "crates/core/parser" }
|
||||
revolt-permissions = { version = "0.13.7", path = "crates/core/permissions" }
|
||||
revolt-presence = { version = "0.13.7", path = "crates/core/presence" }
|
||||
revolt-ratelimits = { version = "0.13.7", path = "crates/core/ratelimits" }
|
||||
revolt-result = { version = "0.13.7", path = "crates/core/result" }
|
||||
|
||||
85
README.md
85
README.md
@@ -2,14 +2,14 @@
|
||||
<h1>
|
||||
Stoat Backend
|
||||
|
||||
[](https://github.com/revoltchat/backend/stargazers)
|
||||
[](https://github.com/revoltchat/backend/network/members)
|
||||
[](https://github.com/revoltchat/backend/pulls)
|
||||
[](https://github.com/revoltchat/backend/issues)
|
||||
[](https://github.com/revoltchat/backend/graphs/contributors)
|
||||
[](https://github.com/revoltchat/backend/blob/main/LICENSE)
|
||||
[](https://github.com/stoatchat/stoatchat/stargazers)
|
||||
[](https://github.com/stoatchat/stoatchat/network/members)
|
||||
[](https://github.com/stoatchat/stoatchat/pulls)
|
||||
[](https://github.com/stoatchat/stoatchat/issues)
|
||||
[](https://github.com/stoatchat/stoatchat/graphs/contributors)
|
||||
[](https://github.com/stoatchat/stoatchat/blob/main/LICENSE)
|
||||
</h1>
|
||||
The services and libraries that power the Revolt service.<br/>
|
||||
The services and libraries that power the Stoat service.<br/>
|
||||
<br/>
|
||||
|
||||
| Crate | Path | Description | |
|
||||
@@ -39,7 +39,7 @@ Rust 1.86.0 or higher.
|
||||
|
||||
## Development Guide
|
||||
|
||||
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
|
||||
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.stoat.chat/developing/contrib/) and the [technical documentation for this project](https://developers.stoat.chat/).
|
||||
|
||||
Before getting started, you'll want to install:
|
||||
|
||||
@@ -70,11 +70,20 @@ As a heads-up, the development environment uses the following ports:
|
||||
Now you can clone and build the project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/revoltchat/backend revolt-backend
|
||||
cd revolt-backend
|
||||
git clone https://github.com/stoatchat/stoatchat stoat-backend
|
||||
cd stoat-backend
|
||||
mise install
|
||||
mise build
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> You can override `BUILDER` in your `.env` file to run cargo with mold if you installed it:
|
||||
>
|
||||
> ```bash
|
||||
> # .env
|
||||
> BUILDER = "mold --run cargo"
|
||||
> ```
|
||||
|
||||
A default configuration `Revolt.toml` is present in this project that is suited for development.
|
||||
|
||||
If you'd like to change anything, create a `Revolt.overrides.toml` file and specify relevant variables.
|
||||
@@ -111,7 +120,7 @@ If you'd like to change anything, create a `Revolt.overrides.toml` file and spec
|
||||
> - "14672:15672"
|
||||
> ```
|
||||
>
|
||||
> And corresponding Revolt configuration:
|
||||
> With the corresponding Revolt configuration:
|
||||
>
|
||||
> ```toml
|
||||
> # Revolt.overrides.toml
|
||||
@@ -123,53 +132,41 @@ If you'd like to change anything, create a `Revolt.overrides.toml` file and spec
|
||||
> [rabbit]
|
||||
> port = 14072
|
||||
> ```
|
||||
>
|
||||
> And mise configuration
|
||||
>
|
||||
> ```bash
|
||||
> #.env
|
||||
> DATABASE_PORT = "14017"
|
||||
> RABBIT_PORT = "14072"
|
||||
> REDIS_PORT = "14079"
|
||||
> ```
|
||||
|
||||
Then continue:
|
||||
|
||||
```bash
|
||||
# start other necessary services
|
||||
docker compose up -d
|
||||
cp livekit.example.yml livekit.yml
|
||||
|
||||
# run everything together
|
||||
./scripts/start.sh
|
||||
# .. or individually
|
||||
# run the API server
|
||||
cargo run --bin revolt-delta
|
||||
# run the events server
|
||||
cargo run --bin revolt-bonfire
|
||||
# run the file server
|
||||
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
|
||||
|
||||
# hint:
|
||||
# mold -run <cargo build, cargo run, etc...>
|
||||
# mold -run ./scripts/start.sh
|
||||
mise start
|
||||
```
|
||||
|
||||
You can start a web client by doing the following:
|
||||
You can start a web client by doing the following in another terminal:
|
||||
|
||||
```bash
|
||||
# if you do not have yarn yet and have a modern Node.js:
|
||||
corepack enable
|
||||
|
||||
# clone the web client and run it:
|
||||
git clone --recursive https://github.com/revoltchat/revite
|
||||
cd revite
|
||||
yarn
|
||||
yarn build:deps
|
||||
echo "VITE_API_URL=http://local.revolt.chat:14702" > .env.local
|
||||
yarn dev --port 14701
|
||||
git clone --recursive https://github.com/stoatchat/for-web stoat-web
|
||||
cd stoat-web
|
||||
# refer to stoat-web/README.md for startup, creating an account and loging in
|
||||
```
|
||||
|
||||
Then go to http://local.revolt.chat:14701 to create an account/login.
|
||||
|
||||
When signing up, go to http://localhost:14080 to find confirmation/password reset emails.
|
||||
|
||||
To stop all services, hit (CTRL + c) in the terminal you ran `mise start` and run `mise docker:stop`
|
||||
|
||||
|
||||
## Deployment Guide
|
||||
|
||||
### Cutting new crate releases
|
||||
@@ -198,14 +195,14 @@ Tag and push a new release by running:
|
||||
just release
|
||||
```
|
||||
|
||||
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/revoltchat/backend/releases/new) to create a changelog.
|
||||
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/stoatchat/stoatchat/releases/new) to create a changelog.
|
||||
|
||||
## Testing
|
||||
|
||||
First, start the required services:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.db.yml up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Now run tests for whichever database:
|
||||
@@ -217,6 +214,6 @@ TEST_DB=MONGODB cargo nextest run
|
||||
|
||||
## License
|
||||
|
||||
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
|
||||
The Stoat backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/stoatchat/stoatchat/blob/main/LICENSE).
|
||||
|
||||
**Individual crates may supply their own licenses!**
|
||||
|
||||
@@ -15,7 +15,7 @@ host = "127.0.0.1"
|
||||
[hosts]
|
||||
# Web locations of various services
|
||||
# Defaults assume all services are reverse-proxied
|
||||
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
|
||||
# See https://github.com/stoatchat/self-hosted/blob/main/Caddyfile
|
||||
#
|
||||
# Remember to change these to https/wss where appropriate in production!
|
||||
app = "http://local.revolt.chat:14701"
|
||||
|
||||
19
compose.yml
19
compose.yml
@@ -8,10 +8,20 @@ services:
|
||||
# MongoDB
|
||||
database:
|
||||
image: mongo
|
||||
command: mongod --replSet rs0
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./.data/db:/data/db
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'127.0.0.1:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 65536
|
||||
@@ -19,11 +29,12 @@ services:
|
||||
|
||||
# MinIO
|
||||
minio:
|
||||
image: minio/minio
|
||||
command: server /data
|
||||
image: firstfinger/minio:latest
|
||||
#command: server /data
|
||||
environment:
|
||||
MINIO_ROOT_USER: minioautumn
|
||||
MINIO_ROOT_PASSWORD: minioautumn
|
||||
MINIO_REGION: minio
|
||||
volumes:
|
||||
- ./.data/minio:/data
|
||||
ports:
|
||||
@@ -70,8 +81,8 @@ services:
|
||||
MAILDEV_INCOMING_PASS: smtp
|
||||
|
||||
livekit:
|
||||
image: ghcr.io/stoatchat/livekit-server:v1.9.9
|
||||
image: ghcr.io/stoatchat/livekit-server:v1.9.13
|
||||
command: --config /etc/livekit.yml
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./livekit.yml:/etc/livekit.yml
|
||||
- ./livekit.yml:/etc/livekit.yml
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.11.5"
|
||||
version = "0.13.7"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
@@ -9,42 +9,38 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
# util
|
||||
log = "*"
|
||||
sentry = "0.31.5"
|
||||
lru = "0.7.6"
|
||||
ulid = "0.5.0"
|
||||
once_cell = "1.9.0"
|
||||
redis-kiss = "0.1.4"
|
||||
lru_time_cache = "0.11.11"
|
||||
async-channel = "2.3.1"
|
||||
log = { workspace = true }
|
||||
sentry = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
|
||||
lru_time_cache = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
regex = "1.11.1"
|
||||
querystring = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
||||
# serde
|
||||
bincode = "1.3.3"
|
||||
serde_json = "1.0.79"
|
||||
rmp-serde = "1.0.0"
|
||||
serde = "1.0.136"
|
||||
bincode = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
rmp-serde = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
# async
|
||||
futures = "0.3.21"
|
||||
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
|
||||
async-std = { version = "1.8.0", features = [
|
||||
"tokio1",
|
||||
"tokio02",
|
||||
"attributes",
|
||||
] }
|
||||
futures = { workspace = true }
|
||||
async-tungstenite = { workspace = true, features = ["tokio-runtime"] }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["compat"] }
|
||||
|
||||
# core
|
||||
authifier = { version = "1.0.16" }
|
||||
revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database", features = ["voice"] }
|
||||
revolt-permissions = { path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
revolt-result = { workspace = true }
|
||||
revolt-models = { workspace = true }
|
||||
revolt-config = { workspace = true }
|
||||
revolt-database = { workspace = true, features = ["voice"] }
|
||||
revolt-permissions = { workspace = true }
|
||||
revolt-presence = { workspace = true, features = ["redis-is-patched"] }
|
||||
|
||||
# redis
|
||||
fred = { version = "8.0.1", features = ["subscriber-client"] }
|
||||
fred = { workspace = true, features = ["subscriber-client"] }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use futures::future::join_all;
|
||||
use redis_kiss::AsyncCommands;
|
||||
use revolt_database::{
|
||||
events::client::{EventV1, ReadyPayloadFields},
|
||||
util::permissions::DatabasePermissionQuery,
|
||||
voice::get_channel_voice_state,
|
||||
voice::{get_channel_voice_state, UserVoiceChannel},
|
||||
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
@@ -167,7 +168,9 @@ impl State {
|
||||
| Channel::TextChannel { voice: Some(_), .. }
|
||||
)
|
||||
}) {
|
||||
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
|
||||
if let Ok(Some(voice_state)) =
|
||||
get_channel_voice_state(&UserVoiceChannel::from_channel(channel)).await
|
||||
{
|
||||
if let Some(server) = channel.server() {
|
||||
let set = voice_state_server_members
|
||||
.entry(server.to_string())
|
||||
@@ -398,29 +401,33 @@ impl State {
|
||||
}
|
||||
|
||||
/// Push presence change to the user and all associated server topics
|
||||
pub async fn broadcast_presence_change(&self, _target: bool) {
|
||||
// disabled events
|
||||
// if if let Some(status) = &self.cache.users.get(&self.cache.user_id).unwrap().status {
|
||||
// status.presence != Some(Presence::Invisible)
|
||||
// } else {
|
||||
// true
|
||||
// } {
|
||||
// let event = EventV1::UserUpdate {
|
||||
// id: self.cache.user_id.clone(),
|
||||
// data: v0::PartialUser {
|
||||
// online: Some(target),
|
||||
// ..Default::default()
|
||||
// },
|
||||
// clear: vec![],
|
||||
// event_id: Some(ulid::Ulid::new().to_string()),
|
||||
// };
|
||||
pub async fn broadcast_presence_change(&self, target: bool) {
|
||||
let config = revolt_config::config().await;
|
||||
if config.disable_events_dont_use {
|
||||
return;
|
||||
}
|
||||
|
||||
// for server in self.cache.servers.keys() {
|
||||
// event.clone().p(server.clone()).await;
|
||||
// }
|
||||
if if let Some(status) = &self.cache.users.get(&self.cache.user_id).unwrap().status {
|
||||
status.presence != Some(Presence::Invisible)
|
||||
} else {
|
||||
true
|
||||
} {
|
||||
let event = EventV1::UserUpdate {
|
||||
id: self.cache.user_id.clone(),
|
||||
data: v0::PartialUser {
|
||||
online: Some(target),
|
||||
..Default::default()
|
||||
},
|
||||
clear: vec![],
|
||||
event_id: Some(ulid::Ulid::new().to_string()),
|
||||
};
|
||||
|
||||
// event.p(self.cache.user_id.clone()).await;
|
||||
// }
|
||||
for server in self.cache.servers.keys() {
|
||||
event.clone().p(server.clone()).await;
|
||||
}
|
||||
|
||||
event.p(self.cache.user_id.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming event for protocol version 1
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
collections::{HashMap, HashSet}, num::NonZeroUsize, sync::Arc, time::Duration
|
||||
};
|
||||
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use lru::LruCache;
|
||||
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
|
||||
use revolt_database::{Channel, Member, Server, User};
|
||||
@@ -57,7 +55,7 @@ impl Default for Cache {
|
||||
members: Default::default(),
|
||||
servers: Default::default(),
|
||||
|
||||
seen_events: LruCache::new(20),
|
||||
seen_events: LruCache::new(NonZeroUsize::new(20).unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::env;
|
||||
|
||||
use async_std::net::TcpListener;
|
||||
use tokio::net::TcpListener;
|
||||
use revolt_presence::clear_region;
|
||||
|
||||
#[macro_use]
|
||||
@@ -12,7 +12,7 @@ pub mod events;
|
||||
mod database;
|
||||
mod websocket;
|
||||
|
||||
#[async_std::main]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Configure requirements for Bonfire.
|
||||
revolt_config::configure!(events);
|
||||
@@ -33,7 +33,7 @@ async fn main() {
|
||||
|
||||
// Start accepting new connections and spawn a client for each connection.
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
async_std::task::spawn(async move {
|
||||
tokio::task::spawn(async move {
|
||||
info!("User connected from {addr:?}");
|
||||
websocket::client(database::get_db(), stream, addr).await;
|
||||
info!("User disconnected from {addr:?}");
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
|
||||
|
||||
use async_tungstenite::WebSocketStream;
|
||||
use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::RedisErrorKind,
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
types::{ReconnectPolicy, RedisConfig},
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
@@ -13,7 +12,7 @@ use futures::{
|
||||
stream::{SplitSink, SplitStream},
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
};
|
||||
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
||||
use redis_kiss::{get_connection, AsyncCommands, PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
||||
use revolt_config::report_internal_error;
|
||||
use revolt_database::{
|
||||
events::{client::EventV1, server::ClientMessage},
|
||||
@@ -22,19 +21,21 @@ use revolt_database::{
|
||||
};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
|
||||
use async_std::{
|
||||
use tokio::{
|
||||
net::TcpStream,
|
||||
sync::{Mutex, RwLock},
|
||||
task::spawn,
|
||||
};
|
||||
use tokio_util::compat::{TokioAsyncReadCompatExt, Compat};
|
||||
use revolt_result::create_error;
|
||||
use sentry::Level;
|
||||
|
||||
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
|
||||
use crate::events::state::{State, SubscriptionStateChange};
|
||||
use revolt_models::v0;
|
||||
|
||||
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
|
||||
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
|
||||
type WsReader = SplitStream<WebSocketStream<Compat<TcpStream>>>;
|
||||
type WsWriter = SplitSink<WebSocketStream<Compat<TcpStream>>, async_tungstenite::tungstenite::Message>;
|
||||
|
||||
/// Start a new WebSocket client worker given access to the database,
|
||||
/// the relevant TCP stream and the remote address of the client.
|
||||
@@ -44,7 +45,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
// e.g. wss://example.com?format=json&version=1
|
||||
let (sender, receiver) = oneshot::channel();
|
||||
let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
|
||||
stream,
|
||||
stream.compat(),
|
||||
WebsocketHandshakeCallback::from(sender),
|
||||
None,
|
||||
)
|
||||
@@ -128,6 +129,14 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
return;
|
||||
}
|
||||
|
||||
let slowmodes = fetch_user_slowmodes(&user_id).await.unwrap_or_default();
|
||||
if !slowmodes.is_empty() {
|
||||
let event = EventV1::UserSlowmodes { slowmodes };
|
||||
if report_internal_error!(write.send(config.encode(&event)).await).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
|
||||
@@ -225,9 +234,9 @@ async fn listener(
|
||||
.unwrap_or(REDIS_URI.to_string());
|
||||
|
||||
let redis_config = RedisConfig::from_url(&url).unwrap();
|
||||
let subscriber = match report_internal_error!(
|
||||
fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
) {
|
||||
let mut builder = fred::types::Builder::from_config(redis_config);
|
||||
builder.set_policy(ReconnectPolicy::new_exponential(8, 100, 30_000, 2));
|
||||
let subscriber = match report_internal_error!(builder.build_subscriber_client()) {
|
||||
Ok(subscriber) => subscriber,
|
||||
Err(_) => return,
|
||||
};
|
||||
@@ -236,16 +245,21 @@ async fn listener(
|
||||
return;
|
||||
}
|
||||
|
||||
// Let Fred automatically re-subscribe to tracked channels on reconnect.
|
||||
subscriber.manage_subscriptions();
|
||||
|
||||
// Handle Redis connection dropping
|
||||
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
||||
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
|
||||
subscriber.on_error(move |err| {
|
||||
warn!("Redis subscriber error: {:?}", err);
|
||||
if let RedisErrorKind::Canceled = err.kind() {
|
||||
let clean_up_s = clean_up_s.clone();
|
||||
spawn(async move {
|
||||
clean_up_s.lock().await.send(()).await.ok();
|
||||
});
|
||||
}
|
||||
// Transient errors (IO, timeout) are handled by the reconnect policy.
|
||||
|
||||
Ok(())
|
||||
});
|
||||
@@ -341,22 +355,20 @@ async fn listener(
|
||||
break 'out;
|
||||
};
|
||||
|
||||
if let EventV1::Auth(auth) = &event {
|
||||
if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
|
||||
if &state.session_id == session_id {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else if let AuthifierEvent::DeleteAllSessions {
|
||||
exclude_session_id, ..
|
||||
} = auth
|
||||
{
|
||||
if let Some(excluded) = exclude_session_id {
|
||||
if &state.session_id != excluded {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else {
|
||||
if let EventV1::DeleteSession { session_id, .. } = &event {
|
||||
if &state.session_id == session_id {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else if let EventV1::DeleteAllSessions {
|
||||
exclude_session_id, ..
|
||||
} = &event
|
||||
{
|
||||
if let Some(excluded) = exclude_session_id {
|
||||
if &state.session_id != excluded {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else {
|
||||
event = EventV1::Logout;
|
||||
}
|
||||
} else {
|
||||
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
|
||||
@@ -428,6 +440,8 @@ async fn worker(
|
||||
mut read: WsReader,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let revolt_config = revolt_config::config().await;
|
||||
|
||||
loop {
|
||||
let t1 = read.try_next().fuse();
|
||||
let t2 = kill_signal_r.recv().fuse();
|
||||
@@ -463,31 +477,38 @@ async fn worker(
|
||||
};
|
||||
|
||||
match payload {
|
||||
// disabled events
|
||||
// ClientMessage::BeginTyping { channel } => {
|
||||
// if !subscribed.read().await.contains(&channel) {
|
||||
// continue;
|
||||
// }
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
if revolt_config.disable_events_dont_use {
|
||||
continue;
|
||||
}
|
||||
|
||||
// EventV1::ChannelStartTyping {
|
||||
// id: channel.clone(),
|
||||
// user: user_id.clone(),
|
||||
// }
|
||||
// .p(channel.clone())
|
||||
// .await;
|
||||
// }
|
||||
// ClientMessage::EndTyping { channel } => {
|
||||
// if !subscribed.read().await.contains(&channel) {
|
||||
// continue;
|
||||
// }
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// EventV1::ChannelStopTyping {
|
||||
// id: channel.clone(),
|
||||
// user: user_id.clone(),
|
||||
// }
|
||||
// .p(channel.clone())
|
||||
// .await;
|
||||
// }
|
||||
EventV1::ChannelStartTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if revolt_config.disable_events_dont_use {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EventV1::ChannelStopTyping {
|
||||
id: channel.clone(),
|
||||
user: user_id.clone(),
|
||||
}
|
||||
.p(channel.clone())
|
||||
.await;
|
||||
}
|
||||
ClientMessage::Subscribe { server_id } => {
|
||||
let mut servers = active_servers.lock().await;
|
||||
let has_item = servers.contains_key(&server_id);
|
||||
@@ -514,3 +535,42 @@ async fn worker(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_user_slowmodes(user_id: &str) -> Option<Vec<v0::ChannelSlowmode>> {
|
||||
let mut conn = get_connection().await.ok()?.into_inner();
|
||||
let idx_key = format!("slowmode_idx:{}", user_id);
|
||||
|
||||
let channel_ids: Vec<String> = conn.smembers(&idx_key).await.unwrap_or_default();
|
||||
if channel_ids.is_empty() {
|
||||
return Some(vec![]);
|
||||
}
|
||||
|
||||
// Bulk fetch all TTLs in one round trip
|
||||
let mut pipe = redis_kiss::redis::pipe();
|
||||
for channel_id in &channel_ids {
|
||||
pipe.ttl(format!("slowmode:{}:{}", user_id, channel_id));
|
||||
}
|
||||
let ttls: Vec<i64> = pipe.query_async(&mut conn).await.unwrap_or_default();
|
||||
|
||||
// Partition into alive/expired in one pass
|
||||
let mut slowmodes = vec![];
|
||||
let mut expired = vec![];
|
||||
for (channel_id, ttl) in channel_ids.iter().zip(ttls.iter()) {
|
||||
if *ttl > 0 {
|
||||
slowmodes.push(v0::ChannelSlowmode {
|
||||
channel_id: channel_id.clone(),
|
||||
duration: *ttl as u64,
|
||||
retry_after: *ttl as u64,
|
||||
});
|
||||
} else {
|
||||
expired.push(channel_id.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Bulk remove all expired members in one SREM call
|
||||
if !expired.is_empty() {
|
||||
conn.srem::<_, _, ()>(&idx_key, expired).await.ok();
|
||||
}
|
||||
|
||||
Some(slowmodes)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-coalesced"
|
||||
version = "0.11.5"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
|
||||
@@ -15,12 +15,12 @@ cache = ["dep:lru"]
|
||||
default = ["tokio"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.47.0", features = ["sync"], optional = true }
|
||||
indexmap = { version = "2.13.0", optional = true }
|
||||
lru = { version = "0.16.3", optional = true }
|
||||
tokio = { workspace = true, features = ["sync"], optional = true }
|
||||
indexmap = { workspace = true, optional = true }
|
||||
lru = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.47.0", features = [
|
||||
tokio = { workspace = true, features = [
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.11.5"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -13,29 +13,28 @@ repository = "https://github.com/stoatchat/stoatchat"
|
||||
anyhow = ["dep:sentry-anyhow"]
|
||||
report-macros = ["revolt-result"]
|
||||
sentry = ["dep:sentry"]
|
||||
test = ["async-std"]
|
||||
default = ["test", "sentry"]
|
||||
test = ["tokio"]
|
||||
default = ["sentry"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
config = "0.13.3"
|
||||
cached = "0.44.0"
|
||||
once_cell = "1.18.0"
|
||||
config = { workspace = true }
|
||||
cached = { workspace = true }
|
||||
|
||||
# Serde
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde = { workspace = true }
|
||||
|
||||
# Async
|
||||
futures-locks = "0.7.1"
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
futures-locks = { workspace = true }
|
||||
tokio = { workspace = true, optional = true }
|
||||
|
||||
# Logging
|
||||
log = "0.4.14"
|
||||
pretty_env_logger = "0.4.0"
|
||||
log = { workspace = true }
|
||||
pretty_env_logger = { workspace = true }
|
||||
|
||||
# Sentry
|
||||
sentry = { version = "0.31.5", optional = true }
|
||||
sentry-anyhow = { version = "0.38.1", optional = true }
|
||||
sentry = { workspace = true, optional = true }
|
||||
sentry-anyhow = { workspace = true, optional = true }
|
||||
|
||||
# Core
|
||||
revolt-result = { version = "0.11.5", path = "../result", optional = true }
|
||||
revolt-result = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
environment = "test"
|
||||
|
||||
[database]
|
||||
mongodb = "mongodb://localhost"
|
||||
redis = "redis://localhost/"
|
||||
@@ -10,3 +12,12 @@ password = "rabbitpass"
|
||||
|
||||
[features]
|
||||
webhooks_enabled = true
|
||||
|
||||
[api.smtp]
|
||||
host = "localhost"
|
||||
username = "smtp"
|
||||
password = "smtp"
|
||||
from_address = "development@stoat.chat"
|
||||
reply_to = "support@stoat.chat"
|
||||
port = 14025
|
||||
use_tls = false
|
||||
@@ -1,4 +1,6 @@
|
||||
production = false
|
||||
disable_events_dont_use = false
|
||||
environment = "dev"
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
@@ -11,7 +13,7 @@ redis = "redis://redis/"
|
||||
[hosts]
|
||||
# Web locations of various services
|
||||
# Defaults assume all services are reverse-proxied
|
||||
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
|
||||
# See https://github.com/stoatchat/self-hosted/blob/main/Caddyfile
|
||||
#
|
||||
# Remember to change these to https/wss where appropriate in production!
|
||||
app = "http://local.revolt.chat"
|
||||
@@ -29,12 +31,16 @@ host = "rabbit"
|
||||
port = 5672
|
||||
username = "rabbituser"
|
||||
password = "rabbitpass"
|
||||
default_exchange = "revolt.default"
|
||||
|
||||
[rabbit.queues]
|
||||
acks = "internal.ack"
|
||||
|
||||
[api]
|
||||
|
||||
[api.registration]
|
||||
# Whether an invite should be required for registration
|
||||
# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
|
||||
# See https://github.com/stoatchat/self-hosted#making-your-instance-invite-only
|
||||
invite_only = false
|
||||
|
||||
[api.smtp]
|
||||
@@ -48,6 +54,10 @@ from_address = "noreply@example.com"
|
||||
# port = 587
|
||||
# use_tls = true
|
||||
|
||||
[api.smtp.expiry]
|
||||
expire_verification = 604800 # 3600 * 24 * 7
|
||||
expire_password_reset = 86400 # 3600 * 24
|
||||
expire_account_deletion = 86400 # 3600 * 24
|
||||
|
||||
[api.security]
|
||||
# Authifier Shield API key
|
||||
@@ -60,12 +70,19 @@ trust_cloudflare = false
|
||||
easypwned = ""
|
||||
# Tenor API Key
|
||||
tenor_key = ""
|
||||
# admin api machine keys
|
||||
# admin_keys = ["key_1", "key_2"]
|
||||
admin_keys = []
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
hcaptcha_key = ""
|
||||
hcaptcha_sitekey = ""
|
||||
|
||||
[api.security.shield]
|
||||
host = ""
|
||||
key = ""
|
||||
|
||||
[api.workers]
|
||||
# Maximum concurrent connections (to proxy server)
|
||||
max_concurrent_connections = 50
|
||||
@@ -77,6 +94,8 @@ call_ring_duration = 30
|
||||
[api.livekit.nodes]
|
||||
|
||||
[api.users]
|
||||
# Minimum allowed length of usernames
|
||||
min_username_length = 2
|
||||
|
||||
[pushd]
|
||||
# this changes the names of the queues to not overlap
|
||||
@@ -88,6 +107,11 @@ production = true
|
||||
# Increasing this will resolve mentions faster, but will consume more memory while resolving.
|
||||
mass_mention_chunk_size = 200
|
||||
|
||||
# How long pushd will cache resolved names for rendered message notifications.
|
||||
# Increasing this will result in lower database usage, but may result in a situation where a user/channel/role name changes
|
||||
# and the notifications still resolve to the old name.
|
||||
render_cache_time = 60
|
||||
|
||||
# none of these should need changing
|
||||
exchange = "revolt.notifications"
|
||||
message_queue = "notifications.origin.message"
|
||||
@@ -124,6 +148,8 @@ pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
[january]
|
||||
blocked_domains = []
|
||||
|
||||
[files]
|
||||
# Encryption key for stored files
|
||||
@@ -196,11 +222,14 @@ default_bucket = "revolt-uploads"
|
||||
[features]
|
||||
# Feature gate options
|
||||
webhooks_enabled = false
|
||||
admin_api_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
|
||||
# Show the admin api in the OpenAPI spec
|
||||
admin_api_show_spec = false
|
||||
|
||||
[features.limits]
|
||||
|
||||
@@ -220,6 +249,10 @@ new_user_hours = 72
|
||||
# (should be greater than any one file upload limit)
|
||||
body_limit_size = 20_000_000
|
||||
|
||||
# If any userids are entered here, only those users will be able to create servers.
|
||||
# Leave empty to allow all users to create servers
|
||||
restrict_server_creation = []
|
||||
|
||||
[features.limits.new_user]
|
||||
# Limits imposed on new users
|
||||
|
||||
@@ -303,6 +336,12 @@ emojis = 500_000
|
||||
# default: 5
|
||||
process_message_delay_limit = 5
|
||||
|
||||
[features.legal_links]
|
||||
# URLs for legal documents
|
||||
terms_of_service = ""
|
||||
privacy_policy = ""
|
||||
guidelines = ""
|
||||
|
||||
[sentry]
|
||||
# Configuration for Sentry error reporting
|
||||
api = ""
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::{collections::HashMap, path::Path};
|
||||
#[cfg(feature = "test")]
|
||||
use std::sync::OnceLock;
|
||||
use std::{collections::HashMap, path::Path, sync::LazyLock};
|
||||
|
||||
use cached::proc_macro::cached;
|
||||
use config::{Config, File, FileFormat};
|
||||
use config::{Config, Environment, File, FileFormat};
|
||||
use futures_locks::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
@@ -66,13 +67,28 @@ static CONFIG_SEARCH_PATHS: [&str; 3] = [
|
||||
static TEST_OVERRIDE_PATH: &str = "Revolt.test-overrides.toml";
|
||||
|
||||
/// Configuration builder
|
||||
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
static CONFIG_BUILDER: LazyLock<RwLock<Config>> = LazyLock::new(|| {
|
||||
RwLock::new({
|
||||
let mut builder = Config::builder().add_source(File::from_str(
|
||||
include_str!("../Revolt.toml"),
|
||||
FileFormat::Toml,
|
||||
));
|
||||
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let mut cwd: Option<&Path> = Some(&cwd);
|
||||
|
||||
while let Some(path) = cwd {
|
||||
for config_path in CONFIG_SEARCH_PATHS {
|
||||
let config_path = path.join(config_path);
|
||||
if config_path.exists() {
|
||||
builder = builder
|
||||
.add_source(File::new(config_path.to_str().unwrap(), FileFormat::Toml));
|
||||
}
|
||||
}
|
||||
|
||||
cwd = path.parent();
|
||||
}
|
||||
|
||||
if std::env::var("TEST_DB").is_ok() {
|
||||
builder = builder.add_source(File::from_str(
|
||||
include_str!("../Revolt.test.toml"),
|
||||
@@ -94,20 +110,7 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
}
|
||||
}
|
||||
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let mut cwd: Option<&Path> = Some(&cwd);
|
||||
|
||||
while let Some(path) = cwd {
|
||||
for config_path in CONFIG_SEARCH_PATHS {
|
||||
let config_path = path.join(config_path);
|
||||
if config_path.exists() {
|
||||
builder = builder
|
||||
.add_source(File::new(config_path.to_str().unwrap(), FileFormat::Toml));
|
||||
}
|
||||
}
|
||||
|
||||
cwd = path.parent();
|
||||
}
|
||||
builder = builder.add_source(Environment::with_prefix("REVOLT").separator("__"));
|
||||
|
||||
builder.build().unwrap()
|
||||
})
|
||||
@@ -120,12 +123,19 @@ pub struct Database {
|
||||
pub redis_pubsub: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct RabbitQueues {
|
||||
pub acks: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Rabbit {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub default_exchange: String,
|
||||
pub queues: RabbitQueues,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -153,6 +163,18 @@ pub struct ApiSmtp {
|
||||
pub port: Option<i32>,
|
||||
pub use_tls: Option<bool>,
|
||||
pub use_starttls: Option<bool>,
|
||||
pub expiry: EmailExpiry,
|
||||
}
|
||||
|
||||
/// Email expiration config
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct EmailExpiry {
|
||||
/// How long email verification codes should last for (in seconds)
|
||||
pub expire_verification: i64,
|
||||
/// How long password reset codes should last for (in seconds)
|
||||
pub expire_password_reset: i64,
|
||||
/// How long account deletion codes should last for (in seconds)
|
||||
pub expire_account_deletion: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -192,14 +214,21 @@ pub struct ApiSecurityCaptcha {
|
||||
pub hcaptcha_sitekey: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurityShield {
|
||||
pub host: String,
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiSecurity {
|
||||
pub authifier_shield_key: String,
|
||||
pub shield: ApiSecurityShield,
|
||||
pub voso_legacy_token: String,
|
||||
pub captcha: ApiSecurityCaptcha,
|
||||
pub trust_cloudflare: bool,
|
||||
pub easypwned: String,
|
||||
pub tenor_key: String,
|
||||
pub admin_keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -229,6 +258,7 @@ pub struct LiveKitNode {
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiUsers {
|
||||
pub early_adopter_cutoff: Option<u64>,
|
||||
pub min_username_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -246,6 +276,7 @@ pub struct Pushd {
|
||||
pub production: bool,
|
||||
pub exchange: String,
|
||||
pub mass_mention_chunk_size: usize,
|
||||
pub render_cache_time: usize,
|
||||
|
||||
// Queues
|
||||
pub message_queue: String,
|
||||
@@ -298,6 +329,11 @@ impl Pushd {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct January {
|
||||
pub blocked_domains: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FilesLimit {
|
||||
pub min_file_size: usize,
|
||||
@@ -342,6 +378,8 @@ pub struct GlobalLimits {
|
||||
pub new_user_hours: usize,
|
||||
|
||||
pub body_limit_size: usize,
|
||||
|
||||
pub restrict_server_creation: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -371,6 +409,16 @@ pub struct FeaturesLimitsCollection {
|
||||
pub roles: HashMap<String, FeaturesLimits>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct LegalLinks {
|
||||
/// Terms of Service URL
|
||||
pub terms_of_service: String,
|
||||
/// Privacy Policy URL
|
||||
pub privacy_policy: String,
|
||||
/// Guidelines URL
|
||||
pub guidelines: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct FeaturesAdvanced {
|
||||
#[serde(default)]
|
||||
@@ -388,9 +436,12 @@ impl Default for FeaturesAdvanced {
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct Features {
|
||||
pub limits: FeaturesLimitsCollection,
|
||||
pub legal_links: LegalLinks,
|
||||
pub webhooks_enabled: bool,
|
||||
pub mass_mentions_send_notifications: bool,
|
||||
pub mass_mentions_enabled: bool,
|
||||
pub admin_api_enabled: bool,
|
||||
pub admin_api_show_spec: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub advanced: FeaturesAdvanced,
|
||||
@@ -415,10 +466,13 @@ pub struct Settings {
|
||||
pub hosts: Hosts,
|
||||
pub api: Api,
|
||||
pub pushd: Pushd,
|
||||
pub january: January,
|
||||
pub files: Files,
|
||||
pub features: Features,
|
||||
pub sentry: Sentry,
|
||||
pub production: bool,
|
||||
pub environment: String,
|
||||
pub disable_events_dont_use: bool,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -444,23 +498,52 @@ pub async fn read() -> Config {
|
||||
CONFIG_BUILDER.read().await.clone()
|
||||
}
|
||||
|
||||
#[cached(time = 30)]
|
||||
pub async fn config() -> Settings {
|
||||
pub async fn config_no_cache() -> Settings {
|
||||
let mut config = read().await.try_deserialize::<Settings>().unwrap();
|
||||
|
||||
// inject REDIS_URI for redis-kiss library
|
||||
if std::env::var("REDIS_URL").is_err() {
|
||||
if std::env::var("REDIS_URI").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") {
|
||||
if config.hosts.api.contains("https")
|
||||
&& (config.hosts.api.contains("revolt.chat") || config.hosts.api.contains("stoat.chat"))
|
||||
{
|
||||
config.production = true;
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
#[cached(time = 30)]
|
||||
pub async fn config() -> Settings {
|
||||
#[cfg(feature = "test")]
|
||||
if let Some(overwrites) = CONFIG_OVERWRITES.get() {
|
||||
return overwrites.clone();
|
||||
}
|
||||
|
||||
config_no_cache().await
|
||||
}
|
||||
|
||||
#[cfg(feature = "test")]
|
||||
static CONFIG_OVERWRITES: OnceLock<Settings> = OnceLock::new();
|
||||
|
||||
/// Modify the config values for a test, this can only be called once
|
||||
///
|
||||
/// This will also fail if two or more tests are running in the same process and both try to modify the config,
|
||||
/// This could happen if tests where run under `cargo test` instead of `nextest`.
|
||||
#[cfg(feature = "test")]
|
||||
pub async fn overwrite_config(f: impl FnOnce(&mut Settings)) {
|
||||
let mut config = config_no_cache().await;
|
||||
|
||||
f(&mut config);
|
||||
|
||||
CONFIG_OVERWRITES.set(config).expect(
|
||||
"Cannot overwrite config multiple times, make sure you are running tests through nextest.",
|
||||
);
|
||||
}
|
||||
|
||||
/// Configure logging and common Rust variables
|
||||
#[cfg(feature = "sentry")]
|
||||
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
|
||||
@@ -506,7 +589,7 @@ macro_rules! configure {
|
||||
mod tests {
|
||||
use crate::init;
|
||||
|
||||
#[async_std::test]
|
||||
#[tokio::test]
|
||||
async fn it_works() {
|
||||
init().await;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.11.5"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
@@ -11,101 +11,104 @@ repository = "https://github.com/stoatchat/stoatchat"
|
||||
|
||||
[features]
|
||||
# Databases
|
||||
mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
|
||||
mongodb = ["dep:mongodb", "bson"]
|
||||
|
||||
# ... Other
|
||||
tasks = ["isahc", "linkify", "url-escape"]
|
||||
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
|
||||
tokio-runtime = ["tokio"]
|
||||
rocket-impl = [
|
||||
"rocket",
|
||||
"schemars",
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi",
|
||||
"authifier/rocket_impl",
|
||||
]
|
||||
axum-impl = ["axum", "revolt-result/axum"]
|
||||
axum-impl = ["axum", "revolt-result/axum", "utoipa"]
|
||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||
voice = ["livekit-api", "livekit-protocol", "livekit-runtime"]
|
||||
|
||||
# Default Features
|
||||
default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
default = ["mongodb", "tokio-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.11.5", path = "../config", features = [
|
||||
"report-macros",
|
||||
] }
|
||||
revolt-result = { version = "0.11.5", path = "../result" }
|
||||
revolt-models = { version = "0.11.5", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.11.5", path = "../presence" }
|
||||
revolt-permissions = { version = "0.11.5", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
revolt-parser = { version = "0.11.5", path = "../parser" }
|
||||
revolt-config = { workspace = true, features = ["report-macros"] }
|
||||
revolt-result = { workspace = true }
|
||||
revolt-models = { workspace = true, features = ["validator"] }
|
||||
revolt-presence = { workspace = true }
|
||||
revolt-permissions = { workspace = true, features = ["serde", "bson"] }
|
||||
revolt-parser = { workspace = true }
|
||||
revolt-coalesced = { workspace = true }
|
||||
|
||||
# Utility
|
||||
log = "0.4"
|
||||
lru = "0.11.0"
|
||||
rand = "0.8.5"
|
||||
ulid = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
base64 = "0.21.3"
|
||||
once_cell = "1.17"
|
||||
indexmap = "1.9.1"
|
||||
decancer = "1.6.2"
|
||||
deadqueue = "0.2.4"
|
||||
linkify = { optional = true, version = "0.8.1" }
|
||||
url-escape = { optional = true, version = "0.1.1" }
|
||||
validator = { version = "0.16", features = ["derive"] }
|
||||
isahc = { optional = true, version = "1.7", features = ["json"] }
|
||||
log = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
ulid = { workspace = true }
|
||||
nanoid = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
decancer = { workspace = true }
|
||||
deadqueue = { workspace = true }
|
||||
linkify = { workspace = true, optional = true }
|
||||
url-escape = { workspace = true, optional = true }
|
||||
validator = { workspace = true, features = ["derive"] }
|
||||
isahc = { workspace = true, features = ["json"], optional = true }
|
||||
base32 = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
|
||||
# Serialisation
|
||||
serde_json = "1"
|
||||
revolt_optional_struct = "0.2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
|
||||
serde_json = { workspace = true }
|
||||
revolt_optional_struct = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
iso8601-timestamp = { workspace = true, features = ["serde", "bson"] }
|
||||
|
||||
# Events
|
||||
redis-kiss = { version = "0.1.4" }
|
||||
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
|
||||
|
||||
# Database
|
||||
bson = { optional = true, version = "2.1.0" }
|
||||
mongodb = { optional = true, version = "3.1.0" }
|
||||
bson = { workspace = true, optional = true }
|
||||
mongodb = { workspace = true, optional = true }
|
||||
|
||||
# Database Migration
|
||||
unicode-segmentation = "1.10.1"
|
||||
regex = "1"
|
||||
unicode-segmentation = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
||||
# Async Language Features
|
||||
futures = "0.3.19"
|
||||
async-lock = "2.8.0"
|
||||
async-trait = "0.1.51"
|
||||
async-recursion = "1.0.4"
|
||||
futures = { workspace = true }
|
||||
async-lock = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
async-recursion = { workspace = true }
|
||||
|
||||
# Async
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
tokio = { workspace = true, optional = true }
|
||||
tokio-stream = { workspace = true }
|
||||
|
||||
# Axum Impl
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { workspace = true, optional = true }
|
||||
utoipa = { workspace = true, features = ["axum_extras"], optional = true }
|
||||
|
||||
# Rocket Impl
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
rocket = { version = "0.5.1", default-features = false, features = [
|
||||
"json",
|
||||
], optional = true }
|
||||
revolt_okapi = { version = "0.9.1", optional = true }
|
||||
revolt_rocket_okapi = { version = "0.10.0", optional = true }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0.16" }
|
||||
schemars = { workspace = true, optional = true }
|
||||
rocket = { workspace = true, features = ["json"], optional = true }
|
||||
revolt_okapi = { workspace = true, optional = true }
|
||||
revolt_rocket_okapi = { workspace = true, optional = true }
|
||||
|
||||
# RabbitMQ
|
||||
amqprs = { version = "1.7.0" }
|
||||
lapin = { workspace = true, features = ["tokio"] }
|
||||
|
||||
# Voice
|
||||
livekit-api = { version = "0.4.4", optional = true }
|
||||
livekit-protocol = { version = "0.4.0", optional = true }
|
||||
livekit-runtime = { version = "0.3.1", features = ["tokio"], optional = true }
|
||||
livekit-api = { workspace = true, features = ["rustls-tls-native-roots"], optional = true }
|
||||
livekit-protocol = { workspace = true, optional = true }
|
||||
livekit-runtime = { workspace = true, features = ["tokio"], optional = true }
|
||||
|
||||
# Security
|
||||
totp-lite = { workspace = true }
|
||||
rust-argon2 = { workspace = true }
|
||||
|
||||
# Email
|
||||
lettre = { workspace = true }
|
||||
handlebars = { workspace = true }
|
||||
|
||||
# Web Requests
|
||||
reqwest = { workspace = true, features = ["json", "form"] }
|
||||
|
||||
100005
crates/core/database/assets/pwned100k.txt
Normal file
100005
crates/core/database/assets/pwned100k.txt
Normal file
File diff suppressed because it is too large
Load Diff
555
crates/core/database/assets/revolt_source_list.txt
Normal file
555
crates/core/database/assets/revolt_source_list.txt
Normal file
@@ -0,0 +1,555 @@
|
||||
this is an assorted list of known disposable email providers
|
||||
|
||||
#region nobody will ever have an email @example.com so we can safely block it
|
||||
----
|
||||
example.com
|
||||
|
||||
#region list provided by michenriksen at https://gist.github.com/michenriksen/8710649
|
||||
----
|
||||
0815.ru
|
||||
0wnd.net
|
||||
0wnd.org
|
||||
10minutemail.co.za
|
||||
10minutemail.com
|
||||
123-m.com
|
||||
1fsdfdsfsdf.tk
|
||||
1pad.de
|
||||
20minutemail.com
|
||||
21cn.com
|
||||
2fdgdfgdfgdf.tk
|
||||
2prong.com
|
||||
30minutemail.com
|
||||
33mail.com
|
||||
3trtretgfrfe.tk
|
||||
4gfdsgfdgfd.tk
|
||||
4warding.com
|
||||
5ghgfhfghfgh.tk
|
||||
6hjgjhgkilkj.tk
|
||||
6paq.com
|
||||
7tags.com
|
||||
9ox.net
|
||||
a-bc.net
|
||||
agedmail.com
|
||||
ama-trade.de
|
||||
amilegit.com
|
||||
amiri.net
|
||||
amiriindustries.com
|
||||
anonmails.de
|
||||
anonymbox.com
|
||||
antichef.com
|
||||
antichef.net
|
||||
antireg.ru
|
||||
antispam.de
|
||||
antispammail.de
|
||||
armyspy.com
|
||||
artman-conception.com
|
||||
azmeil.tk
|
||||
baxomale.ht.cx
|
||||
beefmilk.com
|
||||
bigstring.com
|
||||
binkmail.com
|
||||
bio-muesli.net
|
||||
bobmail.info
|
||||
bodhi.lawlita.com
|
||||
bofthew.com
|
||||
bootybay.de
|
||||
boun.cr
|
||||
bouncr.com
|
||||
breakthru.com
|
||||
brefmail.com
|
||||
bsnow.net
|
||||
bspamfree.org
|
||||
bugmenot.com
|
||||
bund.us
|
||||
burstmail.info
|
||||
buymoreplays.com
|
||||
byom.de
|
||||
c2.hu
|
||||
casualdx.com
|
||||
cek.pm
|
||||
centermail.com
|
||||
centermail.net
|
||||
chammy.info
|
||||
childsavetrust.org
|
||||
chogmail.com
|
||||
choicemail1.com
|
||||
clixser.com
|
||||
cmail.net
|
||||
cmail.org
|
||||
coldemail.info
|
||||
cool.fr.nf
|
||||
courriel.fr.nf
|
||||
courrieltemporaire.com
|
||||
crapmail.org
|
||||
cust.in
|
||||
cuvox.de
|
||||
d3p.dk
|
||||
dacoolest.com
|
||||
dandikmail.com
|
||||
dayrep.com
|
||||
dcemail.com
|
||||
deadaddress.com
|
||||
deadspam.com
|
||||
delikkt.de
|
||||
despam.it
|
||||
despammed.com
|
||||
devnullmail.com
|
||||
dfgh.net
|
||||
digitalsanctuary.com
|
||||
dingbone.com
|
||||
disposableaddress.com
|
||||
disposableemailaddresses.com
|
||||
disposableinbox.com
|
||||
dispose.it
|
||||
dispostable.com
|
||||
dodgeit.com
|
||||
dodgit.com
|
||||
donemail.ru
|
||||
dontreg.com
|
||||
dontsendmespam.de
|
||||
drdrb.net
|
||||
dump-email.info
|
||||
dumpandjunk.com
|
||||
dumpyemail.com
|
||||
e-mail.com
|
||||
e-mail.org
|
||||
e4ward.com
|
||||
easytrashmail.com
|
||||
einmalmail.de
|
||||
einrot.com
|
||||
eintagsmail.de
|
||||
emailgo.de
|
||||
emailias.com
|
||||
emaillime.com
|
||||
emailsensei.com
|
||||
emailtemporanea.com
|
||||
emailtemporanea.net
|
||||
emailtemporar.ro
|
||||
emailtemporario.com.br
|
||||
emailthe.net
|
||||
emailtmp.com
|
||||
emailwarden.com
|
||||
emailx.at.hm
|
||||
emailxfer.com
|
||||
emeil.in
|
||||
emeil.ir
|
||||
emz.net
|
||||
ero-tube.org
|
||||
evopo.com
|
||||
explodemail.com
|
||||
eyepaste.com
|
||||
fakeinbox.com
|
||||
fakeinformation.com
|
||||
fansworldwide.de
|
||||
fantasymail.de
|
||||
fightallspam.com
|
||||
filzmail.com
|
||||
fivemail.de
|
||||
fleckens.hu
|
||||
frapmail.com
|
||||
friendlymail.co.uk
|
||||
fuckingduh.com
|
||||
fudgerub.com
|
||||
fyii.de
|
||||
garliclife.com
|
||||
gehensiemirnichtaufdensack.de
|
||||
get2mail.fr
|
||||
getairmail.com
|
||||
getmails.eu
|
||||
getonemail.com
|
||||
giantmail.de
|
||||
girlsundertheinfluence.com
|
||||
gishpuppy.com
|
||||
gmial.com
|
||||
goemailgo.com
|
||||
gotmail.net
|
||||
gotmail.org
|
||||
gotti.otherinbox.com
|
||||
great-host.in
|
||||
greensloth.com
|
||||
grr.la
|
||||
gsrv.co.uk
|
||||
guerillamail.biz
|
||||
guerillamail.com
|
||||
guerrillamail.biz
|
||||
guerrillamail.com
|
||||
guerrillamail.de
|
||||
guerrillamail.info
|
||||
guerrillamail.net
|
||||
guerrillamail.org
|
||||
guerrillamailblock.com
|
||||
gustr.com
|
||||
harakirimail.com
|
||||
hat-geld.de
|
||||
hatespam.org
|
||||
herp.in
|
||||
hidemail.de
|
||||
hidzz.com
|
||||
hmamail.com
|
||||
hopemail.biz
|
||||
ieh-mail.de
|
||||
ikbenspamvrij.nl
|
||||
imails.info
|
||||
inbax.tk
|
||||
inbox.si
|
||||
inboxalias.com
|
||||
inboxclean.com
|
||||
inboxclean.org
|
||||
instant-mail.de
|
||||
ip6.li
|
||||
irish2me.com
|
||||
iwi.net
|
||||
jetable.com
|
||||
jetable.fr.nf
|
||||
jetable.net
|
||||
jetable.org
|
||||
jnxjn.com
|
||||
jourrapide.com
|
||||
jsrsolutions.com
|
||||
kasmail.com
|
||||
kaspop.com
|
||||
killmail.com
|
||||
killmail.net
|
||||
klassmaster.com
|
||||
klzlk.com
|
||||
koszmail.pl
|
||||
kurzepost.de
|
||||
lawlita.com
|
||||
letthemeatspam.com
|
||||
lhsdv.com
|
||||
lifebyfood.com
|
||||
link2mail.net
|
||||
litedrop.com
|
||||
lol.ovpn.to
|
||||
lolfreak.net
|
||||
lookugly.com
|
||||
lortemail.dk
|
||||
lr78.com
|
||||
lroid.com
|
||||
lukop.dk
|
||||
m21.cc
|
||||
mail-filter.com
|
||||
mail-temporaire.fr
|
||||
mail.mezimages.net
|
||||
mail1a.de
|
||||
mail21.cc
|
||||
mail2rss.org
|
||||
mail333.com
|
||||
mailbidon.com
|
||||
mailbiz.biz
|
||||
mailblocks.com
|
||||
mailbucket.org
|
||||
mailcat.biz
|
||||
mailcatch.com
|
||||
mailde.de
|
||||
mailde.info
|
||||
maildrop.cc
|
||||
maileimer.de
|
||||
mailexpire.com
|
||||
mailfa.tk
|
||||
mailforspam.com
|
||||
mailfreeonline.com
|
||||
mailguard.me
|
||||
mailin8r.com
|
||||
mailinater.com
|
||||
mailinator.com
|
||||
mailinator.net
|
||||
mailinator.org
|
||||
mailinator2.com
|
||||
mailincubator.com
|
||||
mailismagic.com
|
||||
mailme.lv
|
||||
mailme24.com
|
||||
mailmetrash.com
|
||||
mailmoat.com
|
||||
mailms.com
|
||||
mailnesia.com
|
||||
mailnull.com
|
||||
mailorg.org
|
||||
mailpick.biz
|
||||
mailrock.biz
|
||||
mailscrap.com
|
||||
mailshell.com
|
||||
mailsiphon.com
|
||||
mailtemp.info
|
||||
mailtome.de
|
||||
mailtothis.com
|
||||
mailtrash.net
|
||||
mailtv.net
|
||||
mailtv.tv
|
||||
mailzilla.com
|
||||
makemetheking.com
|
||||
manybrain.com
|
||||
mbx.cc
|
||||
mega.zik.dj
|
||||
meinspamschutz.de
|
||||
meltmail.com
|
||||
messagebeamer.de
|
||||
mezimages.net
|
||||
ministry-of-silly-walks.de
|
||||
mintemail.com
|
||||
misterpinball.de
|
||||
moncourrier.fr.nf
|
||||
monemail.fr.nf
|
||||
monmail.fr.nf
|
||||
monumentmail.com
|
||||
mt2009.com
|
||||
mt2014.com
|
||||
mycleaninbox.net
|
||||
mymail-in.net
|
||||
mypacks.net
|
||||
mypartyclip.de
|
||||
myphantomemail.com
|
||||
mysamp.de
|
||||
mytempemail.com
|
||||
mytempmail.com
|
||||
mytrashmail.com
|
||||
nabuma.com
|
||||
neomailbox.com
|
||||
nepwk.com
|
||||
nervmich.net
|
||||
nervtmich.net
|
||||
netmails.com
|
||||
netmails.net
|
||||
neverbox.com
|
||||
nice-4u.com
|
||||
nincsmail.hu
|
||||
nnh.com
|
||||
no-spam.ws
|
||||
noblepioneer.com
|
||||
nomail.pw
|
||||
nomail.xl.cx
|
||||
nomail2me.com
|
||||
nomorespamemails.com
|
||||
nospam.ze.tc
|
||||
nospam4.us
|
||||
nospamfor.us
|
||||
nospammail.net
|
||||
notmailinator.com
|
||||
nowhere.org
|
||||
nowmymail.com
|
||||
nurfuerspam.de
|
||||
nus.edu.sg
|
||||
objectmail.com
|
||||
obobbo.com
|
||||
odnorazovoe.ru
|
||||
oneoffemail.com
|
||||
onewaymail.com
|
||||
onlatedotcom.info
|
||||
online.ms
|
||||
ordinaryamerican.net
|
||||
otherinbox.com
|
||||
ovpn.to
|
||||
owlpic.com
|
||||
pancakemail.com
|
||||
pcusers.otherinbox.com
|
||||
pjjkp.com
|
||||
plexolan.de
|
||||
poczta.onet.pl
|
||||
politikerclub.de
|
||||
poofy.org
|
||||
pookmail.com
|
||||
privacy.net
|
||||
privatdemail.net
|
||||
proxymail.eu
|
||||
prtnx.com
|
||||
putthisinyourspamdatabase.com
|
||||
putthisinyourspamdatabase.com
|
||||
quickinbox.com
|
||||
rcpt.at
|
||||
reallymymail.com
|
||||
realtyalerts.ca
|
||||
recode.me
|
||||
recursor.net
|
||||
reliable-mail.com
|
||||
rhyta.com
|
||||
rmqkr.net
|
||||
royal.net
|
||||
rtrtr.com
|
||||
s0ny.net
|
||||
safersignup.de
|
||||
safetymail.info
|
||||
safetypost.de
|
||||
saynotospams.com
|
||||
schafmail.de
|
||||
schrott-email.de
|
||||
secretemail.de
|
||||
secure-mail.biz
|
||||
senseless-entertainment.com
|
||||
services391.com
|
||||
sharklasers.com
|
||||
shieldemail.com
|
||||
shiftmail.com
|
||||
shitmail.me
|
||||
shitware.nl
|
||||
shmeriously.com
|
||||
shortmail.net
|
||||
sinnlos-mail.de
|
||||
slapsfromlastnight.com
|
||||
slaskpost.se
|
||||
smashmail.de
|
||||
smellfear.com
|
||||
snakemail.com
|
||||
sneakemail.com
|
||||
sneakmail.de
|
||||
snkmail.com
|
||||
sofimail.com
|
||||
solvemail.info
|
||||
sogetthis.com
|
||||
soodonims.com
|
||||
spam4.me
|
||||
spamail.de
|
||||
spamarrest.com
|
||||
spambob.net
|
||||
spambog.ru
|
||||
spambox.us
|
||||
spamcannon.com
|
||||
spamcannon.net
|
||||
spamcon.org
|
||||
spamcorptastic.com
|
||||
spamcowboy.com
|
||||
spamcowboy.net
|
||||
spamcowboy.org
|
||||
spamday.com
|
||||
spamex.com
|
||||
spamfree.eu
|
||||
spamfree24.com
|
||||
spamfree24.de
|
||||
spamfree24.org
|
||||
spamgoes.in
|
||||
spamgourmet.com
|
||||
spamgourmet.net
|
||||
spamgourmet.org
|
||||
spamherelots.com
|
||||
spamherelots.com
|
||||
spamhereplease.com
|
||||
spamhereplease.com
|
||||
spamhole.com
|
||||
spamify.com
|
||||
spaml.de
|
||||
spammotel.com
|
||||
spamobox.com
|
||||
spamslicer.com
|
||||
spamspot.com
|
||||
spamthis.co.uk
|
||||
spamtroll.net
|
||||
speed.1s.fr
|
||||
spoofmail.de
|
||||
stuffmail.de
|
||||
super-auswahl.de
|
||||
supergreatmail.com
|
||||
supermailer.jp
|
||||
superrito.com
|
||||
superstachel.de
|
||||
suremail.info
|
||||
talkinator.com
|
||||
teewars.org
|
||||
teleworm.com
|
||||
teleworm.us
|
||||
temp-mail.org
|
||||
temp-mail.ru
|
||||
tempe-mail.com
|
||||
tempemail.co.za
|
||||
tempemail.com
|
||||
tempemail.net
|
||||
tempemail.net
|
||||
tempinbox.co.uk
|
||||
tempinbox.com
|
||||
tempmail.eu
|
||||
tempmaildemo.com
|
||||
tempmailer.com
|
||||
tempmailer.de
|
||||
tempomail.fr
|
||||
temporaryemail.net
|
||||
temporaryforwarding.com
|
||||
temporaryinbox.com
|
||||
temporarymailaddress.com
|
||||
tempthe.net
|
||||
thankyou2010.com
|
||||
thc.st
|
||||
thelimestones.com
|
||||
thisisnotmyrealemail.com
|
||||
thismail.net
|
||||
throwawayemailaddress.com
|
||||
tilien.com
|
||||
tittbit.in
|
||||
tizi.com
|
||||
tmailinator.com
|
||||
toomail.biz
|
||||
topranklist.de
|
||||
tradermail.info
|
||||
trash-mail.at
|
||||
trash-mail.com
|
||||
trash-mail.de
|
||||
trash2009.com
|
||||
trashdevil.com
|
||||
trashemail.de
|
||||
trashmail.at
|
||||
trashmail.com
|
||||
trashmail.de
|
||||
trashmail.me
|
||||
trashmail.net
|
||||
trashmail.org
|
||||
trashymail.com
|
||||
trialmail.de
|
||||
trillianpro.com
|
||||
twinmail.de
|
||||
tyldd.com
|
||||
uggsrock.com
|
||||
umail.net
|
||||
uroid.com
|
||||
us.af
|
||||
venompen.com
|
||||
veryrealemail.com
|
||||
viditag.com
|
||||
viralplays.com
|
||||
vpn.st
|
||||
vsimcard.com
|
||||
vubby.com
|
||||
wasteland.rfc822.org
|
||||
webemail.me
|
||||
weg-werf-email.de
|
||||
wegwerf-emails.de
|
||||
wegwerfadresse.de
|
||||
wegwerfemail.com
|
||||
wegwerfemail.de
|
||||
wegwerfmail.de
|
||||
wegwerfmail.info
|
||||
wegwerfmail.net
|
||||
wegwerfmail.org
|
||||
wh4f.org
|
||||
whyspam.me
|
||||
willhackforfood.biz
|
||||
willselfdestruct.com
|
||||
winemaven.info
|
||||
wronghead.com
|
||||
www.e4ward.com
|
||||
www.mailinator.com
|
||||
wwwnew.eu
|
||||
x.ip6.li
|
||||
xagloo.com
|
||||
xemaps.com
|
||||
xents.com
|
||||
xmaily.com
|
||||
xoxy.net
|
||||
yep.it
|
||||
yogamaven.com
|
||||
yopmail.com
|
||||
yopmail.fr
|
||||
yopmail.net
|
||||
yourdomain.com
|
||||
yuurok.com
|
||||
z1p.biz
|
||||
za.com
|
||||
zehnminuten.de
|
||||
zehnminutenmail.de
|
||||
zippymail.info
|
||||
zoemail.net
|
||||
zomg.info
|
||||
|
||||
#region public emails provided by mail.tm
|
||||
----
|
||||
trythe.net
|
||||
leadwizzer.com
|
||||
metalunits.com
|
||||
scpulse.com
|
||||
@@ -1,58 +1,77 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::events::rabbit::*;
|
||||
use crate::User;
|
||||
use amqprs::channel::{BasicPublishArguments, ExchangeDeclareArguments};
|
||||
use amqprs::connection::OpenConnectionArguments;
|
||||
use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError};
|
||||
use amqprs::{BasicProperties, FieldTable};
|
||||
use lapin::{
|
||||
options::BasicPublishOptions,
|
||||
protocol::basic::AMQPProperties,
|
||||
types::{AMQPValue, FieldTable},
|
||||
Channel, Connection, ConnectionProperties, Error as AMQPError,
|
||||
};
|
||||
use revolt_models::v0::PushNotification;
|
||||
use revolt_presence::filter_online;
|
||||
use revolt_result::Result;
|
||||
|
||||
use serde_json::to_string;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AMQP {
|
||||
friend_request_accepted: Arc<Channel>,
|
||||
friend_request_received: Arc<Channel>,
|
||||
generic_message: Arc<Channel>,
|
||||
message_sent: Arc<Channel>,
|
||||
mass_mention_message_sent: Arc<Channel>,
|
||||
ack_notification_message: Arc<Channel>,
|
||||
dm_call_updated: Arc<Channel>,
|
||||
process_ack: Arc<Channel>,
|
||||
#[allow(unused)]
|
||||
connection: Connection,
|
||||
channel: Channel,
|
||||
connection: Arc<Connection>,
|
||||
}
|
||||
|
||||
impl AMQP {
|
||||
pub fn new(connection: Connection, channel: Channel) -> AMQP {
|
||||
AMQP {
|
||||
pub async fn new(connection: Arc<Connection>) -> Self {
|
||||
Self {
|
||||
friend_request_accepted: Self::create_channel(&connection).await,
|
||||
friend_request_received: Self::create_channel(&connection).await,
|
||||
generic_message: Self::create_channel(&connection).await,
|
||||
message_sent: Self::create_channel(&connection).await,
|
||||
mass_mention_message_sent: Self::create_channel(&connection).await,
|
||||
ack_notification_message: Self::create_channel(&connection).await,
|
||||
dm_call_updated: Self::create_channel(&connection).await,
|
||||
process_ack: Self::create_channel(&connection).await,
|
||||
connection,
|
||||
channel,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn new_auto() -> AMQP {
|
||||
pub async fn new_auto() -> Self {
|
||||
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(),
|
||||
let connection = Arc::new(
|
||||
Connection::connect(
|
||||
&format!(
|
||||
"amqp://{}:{}@{}:{}",
|
||||
&config.rabbit.username,
|
||||
&config.rabbit.password,
|
||||
&config.rabbit.host,
|
||||
&config.rabbit.port,
|
||||
),
|
||||
ConnectionProperties::default(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to declare exchange");
|
||||
.expect("Failed to connect to RabbitMQ"),
|
||||
);
|
||||
|
||||
AMQP::new(connection, channel)
|
||||
Self::new(connection).await
|
||||
}
|
||||
|
||||
async fn create_channel(connection: &Connection) -> Arc<Channel> {
|
||||
Arc::new(
|
||||
connection
|
||||
.create_channel()
|
||||
.await
|
||||
.expect("Failed to create channel"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn friend_request_accepted(
|
||||
@@ -72,19 +91,20 @@ impl AMQP {
|
||||
config.pushd.get_fr_accepted_routing_key(),
|
||||
payload
|
||||
);
|
||||
self.channel
|
||||
|
||||
self.friend_request_accepted
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_fr_accepted_routing_key(),
|
||||
),
|
||||
config.pushd.exchange.clone().into(),
|
||||
config.pushd.get_fr_accepted_routing_key().into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn friend_request_received(
|
||||
@@ -105,19 +125,19 @@ impl AMQP {
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
self.friend_request_received
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_fr_received_routing_key(),
|
||||
),
|
||||
config.pushd.exchange.clone().into(),
|
||||
config.pushd.get_fr_received_routing_key().into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn generic_message(
|
||||
@@ -142,19 +162,19 @@ impl AMQP {
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
self.generic_message
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_generic_routing_key(),
|
||||
),
|
||||
config.pushd.exchange.clone().into(),
|
||||
config.pushd.get_generic_routing_key().into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn message_sent(
|
||||
@@ -185,19 +205,19 @@ impl AMQP {
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
self.message_sent
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(
|
||||
&config.pushd.exchange,
|
||||
&config.pushd.get_message_routing_key(),
|
||||
),
|
||||
config.pushd.exchange.clone().into(),
|
||||
config.pushd.get_message_routing_key().into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mass_mention_message_sent(
|
||||
@@ -220,19 +240,24 @@ impl AMQP {
|
||||
routing_key, payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
self.mass_mention_message_sent
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(&config.pushd.exchange, routing_key.as_str()),
|
||||
config.pushd.exchange.clone().into(),
|
||||
routing_key.into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ack_message(
|
||||
/// # Sends an ack to pushd to update badges on iPhones.
|
||||
/// Not to be confused with the process_ack function, which handles sending all acks to crond for processing.
|
||||
pub async fn ack_notification_message(
|
||||
&self,
|
||||
user_id: String,
|
||||
channel_id: String,
|
||||
@@ -252,23 +277,25 @@ impl AMQP {
|
||||
config.pushd.ack_queue, payload
|
||||
);
|
||||
|
||||
let mut headers = FieldTable::new();
|
||||
let mut headers = FieldTable::default();
|
||||
headers.insert(
|
||||
"x-deduplication-header".try_into().unwrap(),
|
||||
format!("{}-{}", &user_id, &channel_id).into(),
|
||||
"x-deduplication-header".into(),
|
||||
AMQPValue::LongString(format!("{}-{}", &user_id, &channel_id).into()),
|
||||
);
|
||||
|
||||
self.channel
|
||||
self.ack_notification_message
|
||||
.basic_publish(
|
||||
BasicProperties::default()
|
||||
.with_content_type("application/json")
|
||||
.with_persistence(true)
|
||||
//.with_headers(headers)
|
||||
.finish(),
|
||||
payload.into(),
|
||||
BasicPublishArguments::new(&config.pushd.exchange, &config.pushd.ack_queue),
|
||||
config.pushd.exchange.clone().into(),
|
||||
config.pushd.ack_queue.into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # DM Call Update
|
||||
@@ -302,18 +329,54 @@ impl AMQP {
|
||||
payload
|
||||
);
|
||||
|
||||
self.channel
|
||||
self.dm_call_updated
|
||||
.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(),
|
||||
),
|
||||
config.pushd.exchange.clone().into(),
|
||||
config.pushd.get_dm_call_routing_key().into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # Send an ack to crond for processing
|
||||
pub async fn process_ack(
|
||||
&self,
|
||||
user_id: &str,
|
||||
channel_id: Option<&str>,
|
||||
server_id: Option<&str>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = AckEventPayload {
|
||||
user_id: user_id.to_string(),
|
||||
channel_id: channel_id.map(|value| value.to_string()),
|
||||
server_id: server_id.map(|value| value.to_string()),
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
info!(
|
||||
"Sending ack processor event on exchange {}, channel {}: {}",
|
||||
config.rabbit.default_exchange, config.rabbit.queues.acks, payload
|
||||
);
|
||||
|
||||
self.process_ack
|
||||
.basic_publish(
|
||||
config.rabbit.default_exchange.clone().into(),
|
||||
config.rabbit.queues.acks.into(),
|
||||
BasicPublishOptions::default(),
|
||||
payload.as_bytes(),
|
||||
AMQPProperties::default()
|
||||
.with_content_type("application/json".into())
|
||||
.with_delivery_mode(2),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,9 @@
|
||||
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;
|
||||
use authifier::config::Template;
|
||||
use authifier::config::Templates;
|
||||
use authifier::config::EmailExpiryConfig;
|
||||
use authifier::Authifier;
|
||||
use rand::Rng;
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use self::mongodb::*;
|
||||
@@ -112,143 +103,3 @@ impl DatabaseInfo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
/// Create an Authifier reference
|
||||
pub async fn to_authifier(self) -> Authifier {
|
||||
let config = config().await;
|
||||
|
||||
let mut auth_config = authifier::Config {
|
||||
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 {
|
||||
from: config.api.smtp.from_address,
|
||||
host: config.api.smtp.host,
|
||||
username: config.api.smtp.username,
|
||||
password: config.api.smtp.password,
|
||||
reply_to: Some(
|
||||
config
|
||||
.api
|
||||
.smtp
|
||||
.reply_to
|
||||
.unwrap_or("support@stoat.chat".into()),
|
||||
),
|
||||
port: config.api.smtp.port,
|
||||
use_tls: config.api.smtp.use_tls,
|
||||
use_starttls: config.api.smtp.use_starttls,
|
||||
},
|
||||
expiry: EmailExpiryConfig {
|
||||
expire_verification: 3600 * 24 * 7,
|
||||
expire_password_reset: 3600 * 24,
|
||||
expire_account_deletion: 3600 * 24,
|
||||
},
|
||||
templates: if config.production {
|
||||
Templates {
|
||||
verify: Template {
|
||||
title: "Verify your Stoat 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 Stoat 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 Stoat 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 {
|
||||
EmailVerificationConfig::Disabled
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
auth_config.invite_only = config.api.registration.invite_only;
|
||||
|
||||
if !config.api.security.captcha.hcaptcha_key.is_empty() {
|
||||
auth_config.captcha = Captcha::HCaptcha {
|
||||
secret: config.api.security.captcha.hcaptcha_key,
|
||||
};
|
||||
}
|
||||
|
||||
if !config.api.security.authifier_shield_key.is_empty() {
|
||||
auth_config.shield = Shield::Enabled {
|
||||
api_key: config.api.security.authifier_shield_key,
|
||||
strict: false,
|
||||
};
|
||||
}
|
||||
|
||||
if config.api.security.trust_cloudflare {
|
||||
auth_config.resolve_ip = ResolveIp::Cloudflare;
|
||||
}
|
||||
|
||||
Authifier {
|
||||
database: match self {
|
||||
Database::Reference(_) => Default::default(),
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{
|
||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
|
||||
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
|
||||
User, UserSettings, Webhook,
|
||||
AdminAuditItem, AdminCase, AdminComment, AdminStrike, AdminToken, AdminUser, Bot, Channel,
|
||||
ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member, MemberCompositeKey,
|
||||
Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot, User, UserSettings,
|
||||
Webhook, Account, AccountInvite, Session, MFATicket
|
||||
};
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ReferenceDb {
|
||||
pub admin_audits: Arc<Mutex<BTreeMap<String, AdminAuditItem>>>,
|
||||
pub admin_cases: Arc<Mutex<HashMap<String, AdminCase>>>,
|
||||
pub admin_comments: Arc<Mutex<HashMap<String, AdminComment>>>,
|
||||
pub admin_strikes: Arc<Mutex<HashMap<String, AdminStrike>>>,
|
||||
pub admin_tokens: Arc<Mutex<HashMap<String, AdminToken>>>,
|
||||
pub admin_users: Arc<Mutex<HashMap<String, AdminUser>>>,
|
||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
|
||||
@@ -30,5 +40,9 @@ database_derived!(
|
||||
pub servers: Arc<Mutex<HashMap<String, Server>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
|
||||
pub accounts: Arc<Mutex<HashMap<String, Account>>>,
|
||||
pub account_invites: Arc<Mutex<HashMap<String, AccountInvite>>>,
|
||||
pub sessions: Arc<Mutex<HashMap<String, Session>>>,
|
||||
pub tickets: Arc<Mutex<HashMap<String, MFATicket>>>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use authifier::AuthifierEvent;
|
||||
use revolt_result::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
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
|
||||
AppendMessage, Channel, ChannelSlowmode, ChannelUnread, ChannelVoiceState, Emoji,
|
||||
FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser,
|
||||
FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialEmoji,
|
||||
PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState,
|
||||
PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings,
|
||||
UserVoiceState, Webhook,
|
||||
};
|
||||
|
||||
use crate::Database;
|
||||
use crate::{Account, Database, Session};
|
||||
|
||||
/// Ping Packet
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
@@ -51,9 +55,13 @@ impl Default for ReadyPayloadFields {
|
||||
#[serde(tag = "type")]
|
||||
pub enum EventV1 {
|
||||
/// Multiple events
|
||||
Bulk { v: Vec<EventV1> },
|
||||
Bulk {
|
||||
v: Vec<EventV1>,
|
||||
},
|
||||
/// Error event
|
||||
Error { data: Error },
|
||||
Error {
|
||||
data: Error,
|
||||
},
|
||||
|
||||
/// Successfully authenticated
|
||||
Authenticated,
|
||||
@@ -84,7 +92,9 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping },
|
||||
Pong {
|
||||
data: Ping,
|
||||
},
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
@@ -105,7 +115,10 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Delete message
|
||||
MessageDelete { id: String, channel: String },
|
||||
MessageDelete {
|
||||
id: String,
|
||||
channel: String,
|
||||
},
|
||||
|
||||
/// New reaction to a message
|
||||
MessageReact {
|
||||
@@ -131,7 +144,10 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Bulk delete messages
|
||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
||||
BulkMessageDelete {
|
||||
channel: String,
|
||||
ids: Vec<String>,
|
||||
},
|
||||
|
||||
/// New server
|
||||
ServerCreate {
|
||||
@@ -139,7 +155,7 @@ pub enum EventV1 {
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
emojis: Vec<Emoji>,
|
||||
voice_states: Vec<ChannelVoiceState>
|
||||
voice_states: Vec<ChannelVoiceState>,
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
@@ -151,7 +167,9 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Delete server
|
||||
ServerDelete { id: String },
|
||||
ServerDelete {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Update existing server member
|
||||
ServerMemberUpdate {
|
||||
@@ -187,10 +205,16 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Server role deleted
|
||||
ServerRoleDelete { id: String, role_id: String },
|
||||
ServerRoleDelete {
|
||||
id: String,
|
||||
role_id: String,
|
||||
},
|
||||
|
||||
/// Server roles ranks updated
|
||||
ServerRoleRanksUpdate { id: String, ranks: Vec<String> },
|
||||
ServerRoleRanksUpdate {
|
||||
id: String,
|
||||
ranks: Vec<String>,
|
||||
},
|
||||
|
||||
/// Update existing user
|
||||
UserUpdate {
|
||||
@@ -202,9 +226,15 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Relationship with another user changed
|
||||
UserRelationship { id: String, user: User },
|
||||
UserRelationship {
|
||||
id: String,
|
||||
user: User,
|
||||
},
|
||||
/// Settings updated remotely
|
||||
UserSettingsUpdate { id: String, update: UserSettings },
|
||||
UserSettingsUpdate {
|
||||
id: String,
|
||||
update: UserSettings,
|
||||
},
|
||||
|
||||
/// User has been platform banned or deleted their account
|
||||
///
|
||||
@@ -215,12 +245,23 @@ pub enum EventV1 {
|
||||
/// - Server Memberships
|
||||
///
|
||||
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
||||
UserPlatformWipe { user_id: String, flags: i32 },
|
||||
UserPlatformWipe {
|
||||
user_id: String,
|
||||
flags: i32,
|
||||
},
|
||||
/// New emoji
|
||||
EmojiCreate(Emoji),
|
||||
|
||||
/// Update existing emoji
|
||||
EmojiUpdate {
|
||||
id: String,
|
||||
data: PartialEmoji,
|
||||
},
|
||||
|
||||
/// Delete emoji
|
||||
EmojiDelete { id: String },
|
||||
EmojiDelete {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// New report
|
||||
ReportCreate(Report),
|
||||
@@ -236,19 +277,43 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Delete channel
|
||||
ChannelDelete { id: String },
|
||||
ChannelDelete {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Channel wiped
|
||||
///
|
||||
/// Clients should remove all associated data:
|
||||
/// - Messages
|
||||
/// - Unreads
|
||||
/// - Voice States
|
||||
ChannelWipe {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// User joins a group
|
||||
ChannelGroupJoin { id: String, user: String },
|
||||
ChannelGroupJoin {
|
||||
id: String,
|
||||
user: String,
|
||||
},
|
||||
|
||||
/// User leaves a group
|
||||
ChannelGroupLeave { id: String, user: String },
|
||||
ChannelGroupLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
},
|
||||
|
||||
/// User started typing in a channel
|
||||
ChannelStartTyping { id: String, user: String },
|
||||
ChannelStartTyping {
|
||||
id: String,
|
||||
user: String,
|
||||
},
|
||||
|
||||
/// User stopped typing in a channel
|
||||
ChannelStopTyping { id: String, user: String },
|
||||
ChannelStopTyping {
|
||||
id: String,
|
||||
user: String,
|
||||
},
|
||||
|
||||
/// User acknowledged message in channel
|
||||
ChannelAck {
|
||||
@@ -268,10 +333,25 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Delete webhook
|
||||
WebhookDelete { id: String },
|
||||
WebhookDelete {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Auth events
|
||||
Auth(AuthifierEvent),
|
||||
CreateAccount {
|
||||
account: Account,
|
||||
},
|
||||
CreateSession {
|
||||
session: Session,
|
||||
},
|
||||
DeleteSession {
|
||||
user_id: String,
|
||||
session_id: String,
|
||||
},
|
||||
DeleteAllSessions {
|
||||
user_id: String,
|
||||
exclude_session_id: Option<String>,
|
||||
},
|
||||
|
||||
/// Voice events
|
||||
VoiceChannelJoin {
|
||||
@@ -286,7 +366,7 @@ pub enum EventV1 {
|
||||
user: String,
|
||||
from: String,
|
||||
to: String,
|
||||
state: UserVoiceState
|
||||
state: UserVoiceState,
|
||||
},
|
||||
UserVoiceStateUpdate {
|
||||
id: String,
|
||||
@@ -298,7 +378,11 @@ pub enum EventV1 {
|
||||
from: String,
|
||||
to: String,
|
||||
token: String,
|
||||
}
|
||||
},
|
||||
/// User's active slowmodes
|
||||
UserSlowmodes {
|
||||
slowmodes: Vec<ChannelSlowmode>,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
|
||||
@@ -78,3 +78,11 @@ pub struct AckPayload {
|
||||
pub channel_id: String,
|
||||
pub message_id: String,
|
||||
}
|
||||
|
||||
/// This is not the same as the AckPayload above, as the state for this event is stored in redis to allow for state updates while the event is queued.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct AckEventPayload {
|
||||
pub user_id: String,
|
||||
pub channel_id: Option<String>,
|
||||
pub server_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ pub use mongodb;
|
||||
#[macro_use]
|
||||
extern crate bson;
|
||||
|
||||
#[cfg(not(feature = "async-std-runtime"))]
|
||||
compile_error!("async-std-runtime feature must be enabled.");
|
||||
#[cfg(not(feature = "tokio-runtime"))]
|
||||
compile_error!("tokio-runtime feature must be enabled.");
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
5
crates/core/database/src/models/account_invites/mod.rs
Normal file
5
crates/core/database/src/models/account_invites/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
25
crates/core/database/src/models/account_invites/model.rs
Normal file
25
crates/core/database/src/models/account_invites/model.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::{if_false, Database};
|
||||
use revolt_result::Result;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Account invite ticket
|
||||
pub struct AccountInvite {
|
||||
/// Invite code
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Whether this invite ticket has been used
|
||||
#[serde(skip_serializing_if = "if_false", default)]
|
||||
pub used: bool,
|
||||
/// User ID that this invite was claimed by
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub claimed_by: Option<String>,
|
||||
},
|
||||
"PartialAccountInvite"
|
||||
);
|
||||
|
||||
impl AccountInvite {
|
||||
/// Save model
|
||||
pub async fn save(&self, db: &Database) -> Result<()> {
|
||||
db.save_account_invite(self).await
|
||||
}
|
||||
}
|
||||
16
crates/core/database/src/models/account_invites/ops.rs
Normal file
16
crates/core/database/src/models/account_invites/ops.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AccountInvite;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAccountInvites: Sync + Send {
|
||||
/// Find invite by id
|
||||
async fn fetch_account_invite(&self, id: &str) -> Result<AccountInvite>;
|
||||
|
||||
/// Save invite
|
||||
async fn save_account_invite(&self, invite: &AccountInvite) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use crate::{AbstractAccountInvites, AccountInvite, MongoDb};
|
||||
use bson::to_document;
|
||||
use mongodb::options::UpdateOptions;
|
||||
use revolt_result::Result;
|
||||
|
||||
const COL: &str = "account_invites";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAccountInvites for MongoDb {
|
||||
/// Find invite by id
|
||||
async fn fetch_account_invite(&self, id: &str) -> Result<AccountInvite> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(InvalidInvite))
|
||||
}
|
||||
|
||||
/// Save invite
|
||||
async fn save_account_invite(&self, invite: &AccountInvite) -> Result<()> {
|
||||
self.col::<AccountInvite>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &invite.id
|
||||
},
|
||||
doc! {
|
||||
"$set": to_document(invite).map_err(|_| create_database_error!("to_document", COL))?,
|
||||
},
|
||||
)
|
||||
.with_options(UpdateOptions::builder().upsert(true).build())
|
||||
.await
|
||||
.map_err(|_| create_database_error!("upsert_one", COL))
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use crate::{AbstractAccountInvites, AccountInvite, ReferenceDb};
|
||||
use revolt_result::Result;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAccountInvites for ReferenceDb {
|
||||
/// Find invite by id
|
||||
async fn fetch_account_invite(&self, id: &str) -> Result<AccountInvite> {
|
||||
let invites = self.account_invites.lock().await;
|
||||
invites
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(InvalidInvite))
|
||||
}
|
||||
|
||||
/// Save invite
|
||||
async fn save_account_invite(&self, invite: &AccountInvite) -> Result<()> {
|
||||
let mut invites = self.account_invites.lock().await;
|
||||
invites.insert(invite.id.to_string(), invite.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
22
crates/core/database/src/models/accounts/axum.rs
Normal file
22
crates/core/database/src/models/accounts/axum.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts};
|
||||
|
||||
use revolt_result::{Error, Result};
|
||||
|
||||
use crate::{Account, Database, Session};
|
||||
|
||||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for Account
|
||||
where
|
||||
Database: FromRef<S>,
|
||||
S: Send + Sync
|
||||
{
|
||||
type Rejection = Error;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
|
||||
let db = Database::from_ref(state);
|
||||
|
||||
let session = Session::from_request_parts(parts, state).await?;
|
||||
|
||||
db.fetch_account(&session.user_id).await
|
||||
}
|
||||
}
|
||||
11
crates/core/database/src/models/accounts/mod.rs
Normal file
11
crates/core/database/src/models/accounts/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
#[cfg(feature = "axum-impl")]
|
||||
mod axum;
|
||||
mod model;
|
||||
mod ops;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod rocket;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod schema;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
656
crates/core/database/src/models/accounts/model.rs
Normal file
656
crates/core/database/src/models/accounts/model.rs
Normal file
@@ -0,0 +1,656 @@
|
||||
use iso8601_timestamp::{Duration, Timestamp};
|
||||
|
||||
use nanoid::nanoid;
|
||||
use revolt_config::config;
|
||||
use revolt_result::Result;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1,
|
||||
util::{
|
||||
email::{email_templates, normalise_email, send_email},
|
||||
password::hash_password,
|
||||
},
|
||||
Database, MFATicket, Session,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Account model
|
||||
pub struct Account {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// User's email
|
||||
pub email: String,
|
||||
|
||||
/// Normalised email
|
||||
///
|
||||
/// (see https://github.com/insertish/authifier/#how-does-authifier-work)
|
||||
pub email_normalised: String,
|
||||
|
||||
/// Argon2 hashed password
|
||||
pub password: String,
|
||||
|
||||
/// Whether the account is disabled
|
||||
#[serde(default)]
|
||||
pub disabled: bool,
|
||||
|
||||
/// Email verification status
|
||||
pub verification: EmailVerification,
|
||||
|
||||
/// Password reset information
|
||||
pub password_reset: Option<PasswordReset>,
|
||||
|
||||
/// Account deletion information
|
||||
pub deletion: Option<DeletionInfo>,
|
||||
|
||||
/// Account lockout
|
||||
pub lockout: Option<Lockout>,
|
||||
|
||||
/// Multi-factor authentication information
|
||||
pub mfa: MultiFactorAuthentication,
|
||||
},
|
||||
"PartialAccount"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Email verification status
|
||||
#[serde(tag = "status")]
|
||||
pub enum EmailVerification {
|
||||
/// Account is verified
|
||||
Verified,
|
||||
/// Pending email verification
|
||||
Pending { token: String, expiry: Timestamp },
|
||||
/// Moving to a new email
|
||||
Moving {
|
||||
new_email: String,
|
||||
token: String,
|
||||
expiry: Timestamp,
|
||||
},
|
||||
}
|
||||
|
||||
/// Password reset information
|
||||
pub struct PasswordReset {
|
||||
/// Token required to change password
|
||||
pub token: String,
|
||||
/// Time at which this token expires
|
||||
pub expiry: Timestamp,
|
||||
}
|
||||
|
||||
/// Account deletion information
|
||||
#[serde(tag = "status")]
|
||||
pub enum DeletionInfo {
|
||||
/// The user must confirm deletion by email
|
||||
WaitingForVerification { token: String, expiry: Timestamp },
|
||||
/// The account is scheduled for deletion
|
||||
Scheduled { after: Timestamp },
|
||||
/// This account was deleted
|
||||
Deleted,
|
||||
}
|
||||
|
||||
/// Lockout information
|
||||
pub struct Lockout {
|
||||
/// Attempt counter
|
||||
pub attempts: i32,
|
||||
/// Time at which this lockout expires
|
||||
pub expiry: Option<Timestamp>,
|
||||
}
|
||||
|
||||
/// MFA configuration
|
||||
#[derive(Default)]
|
||||
pub struct MultiFactorAuthentication {
|
||||
/// Allow password-less email OTP login
|
||||
/// (1-Factor)
|
||||
// #[serde(skip_serializing_if = "is_false", default)]
|
||||
// pub enable_email_otp: bool,
|
||||
|
||||
/// Allow trusted handover
|
||||
/// (1-Factor)
|
||||
// #[serde(skip_serializing_if = "is_false", default)]
|
||||
// pub enable_trusted_handover: bool,
|
||||
|
||||
/// Allow email MFA
|
||||
/// (2-Factor)
|
||||
// #[serde(skip_serializing_if = "is_false", default)]
|
||||
// pub enable_email_mfa: bool,
|
||||
|
||||
/// TOTP MFA token, enabled if present
|
||||
/// (2-Factor)
|
||||
#[serde(skip_serializing_if = "Totp::is_empty", default)]
|
||||
pub totp_token: Totp,
|
||||
|
||||
/// Security Key MFA token, enabled if present
|
||||
/// (2-Factor)
|
||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||
// pub security_key_token: Option<String>,
|
||||
|
||||
/// Recovery codes
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub recovery_codes: Vec<String>,
|
||||
}
|
||||
|
||||
/// MFA method
|
||||
#[derive(Hash)]
|
||||
pub enum MFAMethod {
|
||||
Password,
|
||||
Recovery,
|
||||
Totp,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum Totp {
|
||||
/// Disabled
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Waiting for user activation
|
||||
Pending { secret: String },
|
||||
/// Required on account
|
||||
Enabled { secret: String },
|
||||
}
|
||||
);
|
||||
|
||||
impl MultiFactorAuthentication {
|
||||
// Check whether MFA is in-use
|
||||
pub fn is_active(&self) -> bool {
|
||||
matches!(self.totp_token, Totp::Enabled { .. })
|
||||
}
|
||||
|
||||
// Check whether there are still usable recovery codes
|
||||
pub fn has_recovery(&self) -> bool {
|
||||
!self.recovery_codes.is_empty()
|
||||
}
|
||||
|
||||
// Get available MFA methods
|
||||
pub fn get_methods(&self) -> Vec<MFAMethod> {
|
||||
if let Totp::Enabled { .. } = self.totp_token {
|
||||
let mut methods = vec![MFAMethod::Totp];
|
||||
|
||||
if self.has_recovery() {
|
||||
methods.push(MFAMethod::Recovery);
|
||||
}
|
||||
|
||||
methods
|
||||
} else {
|
||||
vec![MFAMethod::Password]
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new recovery codes
|
||||
pub fn generate_recovery_codes(&mut self) {
|
||||
static ALPHABET: [char; 32] = [
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||
'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
|
||||
let mut codes = vec![];
|
||||
for _ in 1..=10 {
|
||||
codes.push(format!(
|
||||
"{}-{}",
|
||||
nanoid!(5, &ALPHABET),
|
||||
nanoid!(5, &ALPHABET)
|
||||
));
|
||||
}
|
||||
|
||||
self.recovery_codes = codes;
|
||||
}
|
||||
|
||||
// Generate new TOTP secret
|
||||
pub fn generate_new_totp_secret(&mut self) -> Result<String> {
|
||||
if let Totp::Enabled { .. } = self.totp_token {
|
||||
return Err(create_error!(OperationFailed));
|
||||
}
|
||||
|
||||
let secret: [u8; 10] = rand::random();
|
||||
let secret = base32::encode(base32::Alphabet::RFC4648 { padding: false }, &secret);
|
||||
|
||||
self.totp_token = Totp::Pending {
|
||||
secret: secret.clone(),
|
||||
};
|
||||
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
/// Enable TOTP using a given MFA response
|
||||
pub fn enable_totp(&mut self, response: v0::MFAResponse) -> Result<()> {
|
||||
if let v0::MFAResponse::Totp { totp_code } = response {
|
||||
let code = self.totp_token.generate_code()?;
|
||||
|
||||
if code == totp_code {
|
||||
let mut totp = Totp::Disabled;
|
||||
std::mem::swap(&mut totp, &mut self.totp_token);
|
||||
|
||||
if let Totp::Pending { secret } = totp {
|
||||
self.totp_token = Totp::Enabled { secret };
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(OperationFailed))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(InvalidToken))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(InvalidToken))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Totp {
|
||||
/// Whether TOTP information is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
matches!(self, Totp::Disabled)
|
||||
}
|
||||
|
||||
/// Whether TOTP is disabled
|
||||
pub fn is_disabled(&self) -> bool {
|
||||
!matches!(self, Totp::Enabled { .. })
|
||||
}
|
||||
|
||||
// Generate a TOTP code from secret
|
||||
pub fn generate_code(&self) -> Result<String> {
|
||||
if let Totp::Enabled { secret } | Totp::Pending { secret } = &self {
|
||||
let seconds: u64 = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
Ok(totp_lite::totp_custom::<totp_lite::Sha1>(
|
||||
totp_lite::DEFAULT_STEP,
|
||||
6,
|
||||
&base32::decode(base32::Alphabet::RFC4648 { padding: false }, secret)
|
||||
.expect("valid base32 secret"),
|
||||
seconds,
|
||||
))
|
||||
} else {
|
||||
Err(create_error!(OperationFailed))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// Save model
|
||||
pub async fn save(&self, db: &Database) -> Result<()> {
|
||||
db.save_account(self).await
|
||||
}
|
||||
|
||||
/// Create a new account
|
||||
pub async fn new(
|
||||
db: &Database,
|
||||
email: String,
|
||||
plaintext_password: String,
|
||||
verify_email: bool,
|
||||
) -> Result<Account> {
|
||||
// Get a normalised representation of the user's email
|
||||
let email_normalised = normalise_email(email.clone());
|
||||
|
||||
// Try to find an existing account
|
||||
if let Some(mut account) = db
|
||||
.fetch_account_by_normalised_email(&email_normalised)
|
||||
.await?
|
||||
{
|
||||
// Resend account verification or send password reset
|
||||
if let EmailVerification::Pending { .. } = &account.verification {
|
||||
account.start_email_verification(db).await?;
|
||||
} else {
|
||||
account.start_password_reset(db, true).await?;
|
||||
}
|
||||
|
||||
Ok(account)
|
||||
} else {
|
||||
// Hash the user's password
|
||||
let password = hash_password(plaintext_password)?;
|
||||
|
||||
// Create a new account
|
||||
let mut account = Account {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
|
||||
email,
|
||||
email_normalised,
|
||||
password,
|
||||
|
||||
disabled: false,
|
||||
verification: EmailVerification::Verified,
|
||||
password_reset: None,
|
||||
deletion: None,
|
||||
lockout: None,
|
||||
|
||||
mfa: Default::default(),
|
||||
};
|
||||
|
||||
// Send email verification
|
||||
if verify_email {
|
||||
account.start_email_verification(db).await?;
|
||||
} else {
|
||||
account.save(db).await?;
|
||||
}
|
||||
|
||||
// Create and push event
|
||||
EventV1::CreateAccount {
|
||||
account: account.clone(),
|
||||
}
|
||||
.global()
|
||||
.await;
|
||||
|
||||
Ok(account)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new session
|
||||
pub async fn create_session(&self, db: &Database, name: String) -> Result<Session> {
|
||||
let config = config().await;
|
||||
|
||||
let session = Session {
|
||||
id: ulid::Ulid::new().to_string(),
|
||||
token: nanoid!(64),
|
||||
|
||||
user_id: self.id.clone(),
|
||||
name,
|
||||
|
||||
last_seen: Timestamp::now_utc(),
|
||||
|
||||
origin: Some(config.environment),
|
||||
subscription: None,
|
||||
};
|
||||
|
||||
// Save to database
|
||||
db.save_session(&session).await?;
|
||||
|
||||
// Create and push event
|
||||
EventV1::CreateSession {
|
||||
session: session.clone(),
|
||||
}
|
||||
.global()
|
||||
.await;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Send account verification email
|
||||
pub async fn start_email_verification(&mut self, db: &Database) -> Result<()> {
|
||||
let config = config().await;
|
||||
|
||||
if !config.api.smtp.host.is_empty() {
|
||||
let templates = email_templates().await;
|
||||
|
||||
let token = nanoid!(32);
|
||||
let url = format!("{}{}", templates.verify.url, token);
|
||||
|
||||
send_email(
|
||||
&config.api.smtp,
|
||||
self.email.clone(),
|
||||
&templates.verify,
|
||||
json!({
|
||||
"email": self.email.clone(),
|
||||
"url": url
|
||||
}),
|
||||
)?;
|
||||
|
||||
self.verification = EmailVerification::Pending {
|
||||
token,
|
||||
expiry: Timestamp::now_utc()
|
||||
.checked_add(Duration::seconds(
|
||||
config.api.smtp.expiry.expire_verification,
|
||||
))
|
||||
.unwrap(),
|
||||
};
|
||||
} else {
|
||||
self.verification = EmailVerification::Verified;
|
||||
}
|
||||
|
||||
self.save(db).await
|
||||
}
|
||||
|
||||
/// Send account verification to new email
|
||||
pub async fn start_email_move(&mut self, db: &Database, new_email: String) -> Result<()> {
|
||||
// This method should and will never be called on an unverified account,
|
||||
// but just validate this just in case.
|
||||
if let EmailVerification::Pending { .. } = self.verification {
|
||||
return Err(create_error!(UnverifiedAccount));
|
||||
}
|
||||
|
||||
let config = config().await;
|
||||
|
||||
if !config.api.smtp.host.is_empty() {
|
||||
let templates = email_templates().await;
|
||||
|
||||
let token = nanoid!(32);
|
||||
let url = format!("{}{}", templates.verify.url, token);
|
||||
|
||||
send_email(
|
||||
&config.api.smtp,
|
||||
new_email.clone(),
|
||||
&templates.verify,
|
||||
json!({
|
||||
"email": self.email.clone(),
|
||||
"url": url
|
||||
}),
|
||||
)?;
|
||||
|
||||
self.verification = EmailVerification::Moving {
|
||||
new_email,
|
||||
token,
|
||||
expiry: Timestamp::now_utc()
|
||||
.checked_add(Duration::seconds(
|
||||
config.api.smtp.expiry.expire_verification,
|
||||
))
|
||||
.unwrap(),
|
||||
};
|
||||
} else {
|
||||
self.email_normalised = normalise_email(new_email.clone());
|
||||
self.email = new_email;
|
||||
}
|
||||
|
||||
self.save(db).await
|
||||
}
|
||||
|
||||
/// Send password reset email
|
||||
pub async fn start_password_reset(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
existing_account: bool,
|
||||
) -> Result<()> {
|
||||
let config = config().await;
|
||||
|
||||
if !config.api.smtp.host.is_empty() {
|
||||
let templates = email_templates().await;
|
||||
|
||||
let template = if existing_account {
|
||||
&templates.reset_existing
|
||||
} else {
|
||||
&templates.reset
|
||||
};
|
||||
|
||||
let token = nanoid!(32);
|
||||
let url = format!("{}{}", template.url, token);
|
||||
|
||||
send_email(
|
||||
&config.api.smtp,
|
||||
self.email.clone(),
|
||||
template,
|
||||
json!({
|
||||
"email": self.email.clone(),
|
||||
"url": url
|
||||
}),
|
||||
)?;
|
||||
|
||||
self.password_reset = Some(PasswordReset {
|
||||
token,
|
||||
expiry: Timestamp::now_utc()
|
||||
.checked_add(Duration::seconds(
|
||||
config.api.smtp.expiry.expire_password_reset,
|
||||
))
|
||||
.unwrap(),
|
||||
});
|
||||
} else {
|
||||
return Err(create_error!(OperationFailed));
|
||||
}
|
||||
|
||||
self.save(db).await
|
||||
}
|
||||
|
||||
/// Begin account deletion process by sending confirmation email
|
||||
///
|
||||
/// If email verification is not on, the account will be marked for deletion instantly
|
||||
pub async fn start_account_deletion(&mut self, db: &Database) -> Result<()> {
|
||||
let config = config().await;
|
||||
|
||||
if !config.api.smtp.host.is_empty() {
|
||||
let templates = email_templates().await;
|
||||
|
||||
let token = nanoid!(32);
|
||||
let url = format!("{}{}", templates.deletion.url, token);
|
||||
|
||||
send_email(
|
||||
&config.api.smtp,
|
||||
self.email.clone(),
|
||||
&templates.deletion,
|
||||
json!({
|
||||
"email": self.email.clone(),
|
||||
"url": url
|
||||
}),
|
||||
)?;
|
||||
|
||||
self.deletion = Some(DeletionInfo::WaitingForVerification {
|
||||
token,
|
||||
expiry: Timestamp::now_utc()
|
||||
.checked_add(Duration::seconds(
|
||||
config.api.smtp.expiry.expire_password_reset,
|
||||
))
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
self.save(db).await
|
||||
} else {
|
||||
self.schedule_deletion(db).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a user's password is correct
|
||||
pub fn verify_password(&self, plaintext_password: &str) -> Result<()> {
|
||||
argon2::verify_encoded(&self.password, plaintext_password.as_bytes())
|
||||
.map(|v| {
|
||||
if v {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(InvalidCredentials))
|
||||
}
|
||||
})
|
||||
// To prevent user enumeration, we should ignore
|
||||
// the error and pretend the password is wrong.
|
||||
.map_err(|_| create_error!(InvalidCredentials))?
|
||||
}
|
||||
|
||||
/// Validate an MFA response
|
||||
pub async fn consume_mfa_response(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
response: v0::MFAResponse,
|
||||
ticket: Option<MFATicket>,
|
||||
) -> Result<()> {
|
||||
let allowed_methods = self.mfa.get_methods();
|
||||
|
||||
match response {
|
||||
v0::MFAResponse::Password { password } => {
|
||||
if allowed_methods.contains(&MFAMethod::Password) {
|
||||
self.verify_password(&password)
|
||||
} else {
|
||||
Err(create_error!(DisallowedMFAMethod))
|
||||
}
|
||||
}
|
||||
v0::MFAResponse::Totp { totp_code } => {
|
||||
if allowed_methods.contains(&MFAMethod::Totp) {
|
||||
if let Totp::Enabled { .. } = &self.mfa.totp_token {
|
||||
// Use TOTP code at generation if applicable
|
||||
if let Some(ticket) = ticket {
|
||||
if let Some(code) = ticket.last_totp_code {
|
||||
if code == totp_code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise read current TOTP token
|
||||
if self.mfa.totp_token.generate_code()? == totp_code {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(InvalidToken))
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(DisallowedMFAMethod))
|
||||
}
|
||||
}
|
||||
v0::MFAResponse::Recovery { recovery_code } => {
|
||||
if allowed_methods.contains(&MFAMethod::Recovery) {
|
||||
if let Some(index) = self
|
||||
.mfa
|
||||
.recovery_codes
|
||||
.iter()
|
||||
.position(|x| x == &recovery_code)
|
||||
{
|
||||
self.mfa.recovery_codes.remove(index);
|
||||
self.save(db).await
|
||||
} else {
|
||||
Err(create_error!(InvalidToken))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(DisallowedMFAMethod))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete all sessions for an account
|
||||
pub async fn delete_all_sessions(
|
||||
&self,
|
||||
db: &Database,
|
||||
exclude_session_id: Option<String>,
|
||||
) -> Result<()> {
|
||||
db.delete_all_sessions(&self.id, exclude_session_id.clone())
|
||||
.await?;
|
||||
|
||||
// Create and push event
|
||||
EventV1::DeleteAllSessions {
|
||||
user_id: self.id.clone(),
|
||||
exclude_session_id,
|
||||
}
|
||||
.private(self.id.clone())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disable an account
|
||||
pub async fn disable(&mut self, db: &Database) -> Result<()> {
|
||||
self.disabled = true;
|
||||
self.delete_all_sessions(db, None).await?;
|
||||
self.save(db).await
|
||||
}
|
||||
|
||||
/// Schedule an account for deletion
|
||||
pub async fn schedule_deletion(&mut self, db: &Database) -> Result<()> {
|
||||
self.deletion = Some(DeletionInfo::Scheduled {
|
||||
after: Timestamp::now_utc()
|
||||
.checked_add(Duration::weeks(1))
|
||||
.unwrap(),
|
||||
});
|
||||
|
||||
self.disable(db).await
|
||||
}
|
||||
|
||||
/// Removes all information from the account and marks it as fully deleted
|
||||
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
|
||||
self.email = format!("Deleted User {}", &self.id);
|
||||
self.email_normalised = format!("Deleted User {}", &self.id);
|
||||
self.deletion = Some(DeletionInfo::Deleted);
|
||||
|
||||
self.save(db).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
34
crates/core/database/src/models/accounts/ops.rs
Normal file
34
crates/core/database/src/models/accounts/ops.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Account;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAccounts: Sync + Send {
|
||||
/// Find account by id
|
||||
async fn fetch_account(&self, id: &str) -> Result<Account>;
|
||||
|
||||
/// Find account by normalised email
|
||||
async fn fetch_account_by_normalised_email(
|
||||
&self,
|
||||
normalised_email: &str,
|
||||
) -> Result<Option<Account>>;
|
||||
|
||||
/// Find account with active pending email verification
|
||||
async fn fetch_account_with_email_verification(&self, token: &str) -> Result<Account>;
|
||||
|
||||
/// Find account with active password reset
|
||||
async fn fetch_account_with_password_reset(&self, token: &str) -> Result<Account>;
|
||||
|
||||
/// Find account with active deletion token
|
||||
async fn fetch_account_with_deletion_token(&self, token: &str) -> Result<Account>;
|
||||
|
||||
/// Find accounts which are due to be deleted
|
||||
async fn fetch_accounts_due_for_deletion(&self) -> Result<Vec<Account>>;
|
||||
|
||||
// Save account
|
||||
async fn save_account(&self, account: &Account) -> Result<()>;
|
||||
}
|
||||
118
crates/core/database/src/models/accounts/ops/mongodb.rs
Normal file
118
crates/core/database/src/models/accounts/ops/mongodb.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use crate::{AbstractAccounts, Account, MongoDb};
|
||||
use bson::{to_bson, to_document};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use mongodb::options::{Collation, CollationStrength, FindOneOptions, UpdateOptions};
|
||||
use revolt_result::Result;
|
||||
|
||||
const COL: &str = "accounts";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAccounts for MongoDb {
|
||||
/// Find account by id
|
||||
async fn fetch_account(&self, id: &str) -> Result<Account> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(UnknownUser))
|
||||
}
|
||||
|
||||
/// Find account by normalised email
|
||||
async fn fetch_account_by_normalised_email(
|
||||
&self,
|
||||
normalised_email: &str,
|
||||
) -> Result<Option<Account>> {
|
||||
query!(
|
||||
self,
|
||||
find_one_with_options,
|
||||
COL,
|
||||
doc! {
|
||||
"email_normalised": normalised_email
|
||||
},
|
||||
FindOneOptions::builder()
|
||||
.collation(
|
||||
Collation::builder()
|
||||
.locale("en")
|
||||
.strength(CollationStrength::Secondary)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
/// Find account with active pending email verification
|
||||
async fn fetch_account_with_email_verification(&self, token: &str) -> Result<Account> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"verification.token": token,
|
||||
"verification.expiry": {
|
||||
"$gte": to_bson(&Timestamp::now_utc()).unwrap()
|
||||
}
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(InvalidToken))
|
||||
}
|
||||
|
||||
/// Find account with active password reset
|
||||
async fn fetch_account_with_password_reset(&self, token: &str) -> Result<Account> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"password_reset.token": token,
|
||||
"password_reset.expiry": {
|
||||
"$gte": to_bson(&Timestamp::now_utc()).unwrap()
|
||||
}
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(InvalidToken))
|
||||
}
|
||||
|
||||
/// Find account with active deletion token
|
||||
async fn fetch_account_with_deletion_token(&self, token: &str) -> Result<Account> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"deletion.token": token,
|
||||
"deletion.expiry": {
|
||||
"$gte": to_bson(&Timestamp::now_utc()).unwrap()
|
||||
}
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(InvalidToken))
|
||||
}
|
||||
|
||||
/// Find accounts which are due to be deleted
|
||||
async fn fetch_accounts_due_for_deletion(&self) -> Result<Vec<Account>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"deletion.status": "Scheduled",
|
||||
"deletion.after": {
|
||||
"$lte": to_bson(&Timestamp::now_utc()).unwrap()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Save account
|
||||
async fn save_account(&self, account: &Account) -> Result<()> {
|
||||
self.col::<Account>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &account.id
|
||||
},
|
||||
doc! {
|
||||
"$set": to_document(account).map_err(|_| create_database_error!("to_document", COL))?
|
||||
},
|
||||
)
|
||||
.with_options(UpdateOptions::builder().upsert(true).build())
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find_one", COL))
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
99
crates/core/database/src/models/accounts/ops/reference.rs
Normal file
99
crates/core/database/src/models/accounts/ops/reference.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::{AbstractAccounts, Account, DeletionInfo, EmailVerification, ReferenceDb};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAccounts for ReferenceDb {
|
||||
/// Find account by id
|
||||
async fn fetch_account(&self, id: &str) -> Result<Account> {
|
||||
let accounts = self.accounts.lock().await;
|
||||
accounts
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(UnknownUser))
|
||||
}
|
||||
|
||||
/// Find account by normalised email
|
||||
async fn fetch_account_by_normalised_email(
|
||||
&self,
|
||||
normalised_email: &str,
|
||||
) -> Result<Option<Account>> {
|
||||
let accounts = self.accounts.lock().await;
|
||||
Ok(accounts
|
||||
.values()
|
||||
.find(|account| account.email_normalised == normalised_email)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
/// Find account with active pending email verification
|
||||
async fn fetch_account_with_email_verification(&self, token_to_match: &str) -> Result<Account> {
|
||||
let accounts = self.accounts.lock().await;
|
||||
accounts
|
||||
.values()
|
||||
.find(|account| match &account.verification {
|
||||
EmailVerification::Pending { token, .. }
|
||||
| EmailVerification::Moving { token, .. } => token == token_to_match,
|
||||
_ => false,
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(InvalidToken))
|
||||
}
|
||||
|
||||
/// Find account with active password reset
|
||||
async fn fetch_account_with_password_reset(&self, token: &str) -> Result<Account> {
|
||||
let accounts = self.accounts.lock().await;
|
||||
accounts
|
||||
.values()
|
||||
.find(|account| {
|
||||
if let Some(reset) = &account.password_reset {
|
||||
reset.token == token
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(InvalidToken))
|
||||
}
|
||||
|
||||
/// Find account with active deletion token
|
||||
async fn fetch_account_with_deletion_token(&self, token_to_match: &str) -> Result<Account> {
|
||||
let accounts = self.accounts.lock().await;
|
||||
accounts
|
||||
.values()
|
||||
.find(|account| {
|
||||
if let Some(DeletionInfo::WaitingForVerification { token, .. }) = &account.deletion
|
||||
{
|
||||
token == token_to_match
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(InvalidToken))
|
||||
}
|
||||
|
||||
/// Find accounts which are due to be deleted
|
||||
async fn fetch_accounts_due_for_deletion(&self) -> Result<Vec<Account>> {
|
||||
let now = Timestamp::now_utc();
|
||||
let accounts = self.accounts.lock().await;
|
||||
|
||||
Ok(accounts
|
||||
.values()
|
||||
.filter(|account| {
|
||||
if let Some(DeletionInfo::Scheduled { after }) = &account.deletion {
|
||||
after <= &now
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
// Save account
|
||||
async fn save_account(&self, account: &Account) -> Result<()> {
|
||||
let mut accounts = self.accounts.lock().await;
|
||||
accounts.insert(account.id.to_string(), account.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
32
crates/core/database/src/models/accounts/rocket.rs
Normal file
32
crates/core/database/src/models/accounts/rocket.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use crate::{Account, Database, Session};
|
||||
use revolt_result::Error;
|
||||
use rocket::{
|
||||
http::Status,
|
||||
request::{FromRequest, Outcome},
|
||||
Request,
|
||||
};
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Account {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
match request.guard::<Session>().await {
|
||||
Outcome::Success(session) => {
|
||||
if let Ok(account) = request
|
||||
.rocket()
|
||||
.state::<Database>()
|
||||
.expect("`Database`")
|
||||
.fetch_account(&session.user_id)
|
||||
.await
|
||||
{
|
||||
Outcome::Success(account)
|
||||
} else {
|
||||
Outcome::Error((Status::InternalServerError, create_error!(InternalError)))
|
||||
}
|
||||
}
|
||||
Outcome::Forward(_) => unreachable!(),
|
||||
Outcome::Error(err) => Outcome::Error(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
32
crates/core/database/src/models/accounts/schema.rs
Normal file
32
crates/core/database/src/models/accounts/schema.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
|
||||
use revolt_rocket_okapi::{
|
||||
gen::OpenApiGenerator,
|
||||
request::{OpenApiFromRequest, RequestHeaderInput},
|
||||
};
|
||||
|
||||
use crate::Account;
|
||||
|
||||
|
||||
impl<'r> OpenApiFromRequest<'r> for Account {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
_name: String,
|
||||
_required: bool,
|
||||
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
||||
let mut requirements = schemars::Map::new();
|
||||
requirements.insert("Session Token".to_owned(), vec![]);
|
||||
|
||||
Ok(RequestHeaderInput::Security(
|
||||
"Session Token".to_owned(),
|
||||
SecurityScheme {
|
||||
data: SecuritySchemeData::ApiKey {
|
||||
name: "x-session-token".to_owned(),
|
||||
location: "header".to_owned(),
|
||||
},
|
||||
description: Some("Used to authenticate as a user.".to_owned()),
|
||||
extensions: schemars::Map::new(),
|
||||
},
|
||||
requirements,
|
||||
))
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/admin_audits/mod.rs
Normal file
5
crates/core/database/src/models/admin_audits/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
45
crates/core/database/src/models/admin_audits/models.rs
Normal file
45
crates/core/database/src/models/admin_audits/models.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_models::v0::AdminAuditItemActions;
|
||||
|
||||
use crate::util::basic::transform_optional_string;
|
||||
|
||||
auto_derived! {
|
||||
pub struct AdminAuditItem {
|
||||
/// The audit item ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The moderator who performed the action
|
||||
pub mod_id: String,
|
||||
/// The action performed (previously 'permission'). Should be one of v0::AdminAuditActionAction
|
||||
pub action: String,
|
||||
/// The relevant case ID, if applicable
|
||||
pub case_id: Option<String>,
|
||||
/// The object the action was taken against, if applicable
|
||||
pub target_id: Option<String>,
|
||||
/// The context of the action, if applicable (eg. search phrases)
|
||||
pub context: Option<String>,
|
||||
/// When the action occurred, in iso8601.
|
||||
pub timestamp: String
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminAuditItem {
|
||||
pub fn new(
|
||||
mod_id: &str,
|
||||
action: AdminAuditItemActions,
|
||||
case_id: Option<&str>,
|
||||
target_id: Option<&str>,
|
||||
context: Option<&str>,
|
||||
) -> AdminAuditItem {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminAuditItem {
|
||||
id,
|
||||
mod_id: mod_id.to_string(),
|
||||
action: action.to_string(),
|
||||
case_id: transform_optional_string(case_id),
|
||||
target_id: transform_optional_string(target_id),
|
||||
context: transform_optional_string(context),
|
||||
timestamp: Timestamp::now_utc().format_short().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
17
crates/core/database/src/models/admin_audits/ops.rs
Normal file
17
crates/core/database/src/models/admin_audits/ops.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_audits::AdminAuditItem;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminAudits: Sync + Send {
|
||||
async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()>;
|
||||
|
||||
async fn admin_audit_fetch(
|
||||
&self,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminAuditItem>>;
|
||||
}
|
||||
48
crates/core/database/src/models/admin_audits/ops/mongodb.rs
Normal file
48
crates/core/database/src/models/admin_audits/ops/mongodb.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use tokio_stream::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AdminAuditItem;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractAdminAudits;
|
||||
|
||||
static COL: &str = "admin_audits";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminAudits for MongoDb {
|
||||
async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()> {
|
||||
query!(self, insert_one, COL, audit).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_audit_fetch(
|
||||
&self,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminAuditItem>> {
|
||||
if let Some(before) = before_id {
|
||||
Ok(self
|
||||
.col::<AdminAuditItem>(COL)
|
||||
.find(doc! {
|
||||
"_id": { "$lt": before}
|
||||
})
|
||||
.sort(doc! {"_id": -1})
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|f| f.ok())
|
||||
.collect()
|
||||
.await)
|
||||
} else {
|
||||
Ok(self
|
||||
.col::<AdminAuditItem>(COL)
|
||||
.find(doc! {})
|
||||
.sort(doc! {"_id": -1})
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|f| f.ok())
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AdminAuditItem;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractAdminAudits;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminAudits for ReferenceDb {
|
||||
async fn admin_audit_insert(&self, audit: AdminAuditItem) -> Result<()> {
|
||||
let mut admin_audits = self.admin_audits.lock().await;
|
||||
if admin_audits.contains_key(&audit.id) {
|
||||
Err(create_database_error!("insert", "admin_audits"))
|
||||
} else {
|
||||
admin_audits.insert(audit.id.to_string(), audit.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_audit_fetch(
|
||||
&self,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminAuditItem>> {
|
||||
let admin_audits = self.admin_audits.lock().await;
|
||||
if let Some(before) = before_id {
|
||||
Ok(admin_audits
|
||||
.iter()
|
||||
.rev()
|
||||
.skip_while(|(id, _)| id.as_str() <= before)
|
||||
.by_ref()
|
||||
.take(limit as usize)
|
||||
.map(|(_, item)| item.clone())
|
||||
.collect())
|
||||
} else {
|
||||
Ok(admin_audits
|
||||
.iter()
|
||||
.rev()
|
||||
.by_ref()
|
||||
.take(limit as usize)
|
||||
.map(|(_, item)| item.clone())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/admin_cases/mod.rs
Normal file
5
crates/core/database/src/models/admin_cases/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
53
crates/core/database/src/models/admin_cases/models.rs
Normal file
53
crates/core/database/src/models/admin_cases/models.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
auto_derived_partial! {
|
||||
pub struct AdminCase {
|
||||
/// The case ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The case Short ID
|
||||
pub short_id: String,
|
||||
|
||||
/// The owner of the case
|
||||
pub owner_id: String,
|
||||
/// The title of the case
|
||||
pub title: String,
|
||||
/// The status of the case (open/closed)
|
||||
pub status: String,
|
||||
/// When the case was closed, in iso8601
|
||||
pub closed_at: Option<String>,
|
||||
/// The tags for the case
|
||||
pub tags: Vec<String>,
|
||||
},
|
||||
"PartialAdminCase"
|
||||
}
|
||||
|
||||
impl AdminCase {
|
||||
pub fn new(owner_id: &str, title: &str) -> AdminCase {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
let short_id = id.clone().split_off(id.len() - 7);
|
||||
|
||||
AdminCase {
|
||||
id,
|
||||
short_id,
|
||||
owner_id: owner_id.to_string(),
|
||||
title: title.to_string(),
|
||||
status: "Open".to_string(),
|
||||
closed_at: None,
|
||||
tags: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_tags(&self, other: &[String]) -> Vec<String> {
|
||||
let mut resp: Vec<String> = vec![];
|
||||
// Shitty combining chain; itll only ever be like 5 items
|
||||
resp.extend(self.tags.clone());
|
||||
resp.extend(other.iter().filter_map(|p| {
|
||||
if !self.tags.contains(p) {
|
||||
Some(p.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
||||
30
crates/core/database/src/models/admin_cases/ops.rs
Normal file
30
crates/core/database/src/models/admin_cases/ops.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_cases::{AdminCase, PartialAdminCase};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminCases: Sync + Send {
|
||||
async fn admin_case_create(&self, case: AdminCase) -> Result<()>;
|
||||
|
||||
async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()>;
|
||||
|
||||
async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()>;
|
||||
|
||||
async fn admin_case_fetch(&self, case_id: &str) -> Result<AdminCase>;
|
||||
|
||||
async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result<AdminCase>;
|
||||
|
||||
/// title is fuzzy, the rest of the arguments are direct matches
|
||||
/// before_id and limit are for paginating
|
||||
async fn admin_case_search(
|
||||
&self,
|
||||
title: Option<&str>,
|
||||
status: Option<&str>,
|
||||
owner_id: Option<&str>,
|
||||
tags: Option<Vec<String>>,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminCase>>;
|
||||
}
|
||||
85
crates/core/database/src/models/admin_cases/ops/mongodb.rs
Normal file
85
crates/core/database/src/models/admin_cases/ops/mongodb.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use tokio_stream::StreamExt;
|
||||
use bson::Document;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminCase, PartialAdminCase};
|
||||
|
||||
use super::AbstractAdminCases;
|
||||
|
||||
static COL: &str = "admin_cases";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminCases for MongoDb {
|
||||
async fn admin_case_create(&self, case: AdminCase) -> Result<()> {
|
||||
query!(self, insert_one, COL, case).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()> {
|
||||
self.col::<Document>("safety_reports")
|
||||
.update_one(
|
||||
doc! {"_id": { "$regex": format!("{}$", report_id)}},
|
||||
doc! {"case_id": case_id},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("safety_reports", "update_one"))
|
||||
}
|
||||
|
||||
async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, case_id, partial, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_case_fetch(&self, case_id: &str) -> Result<AdminCase> {
|
||||
query!(self, find_one_by_id, COL, case_id)?
|
||||
.ok_or_else(|| create_database_error!("find_one", COL))
|
||||
}
|
||||
|
||||
async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result<AdminCase> {
|
||||
query!(self, find_one, COL, doc! {"short_id": short_id})?
|
||||
.ok_or_else(|| create_database_error!("find_one", COL))
|
||||
}
|
||||
|
||||
/// title is fuzzy, the rest of the arguments are direct matches
|
||||
async fn admin_case_search(
|
||||
&self,
|
||||
title: Option<&str>,
|
||||
status: Option<&str>,
|
||||
owner_id: Option<&str>,
|
||||
tags: Option<Vec<String>>,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminCase>> {
|
||||
let mut query = Document::new();
|
||||
|
||||
if let Some(title) = title {
|
||||
query.insert("$text", doc! {"$search": title});
|
||||
}
|
||||
|
||||
if let Some(status) = status {
|
||||
query.insert("status", status);
|
||||
}
|
||||
|
||||
if let Some(owner) = owner_id {
|
||||
query.insert("owner_id", owner);
|
||||
}
|
||||
|
||||
if let Some(tags) = tags {
|
||||
query.insert("tags", doc! {"$elemMatch": {"$in": tags}});
|
||||
}
|
||||
|
||||
if let Some(before) = before_id {
|
||||
query.insert("_id", doc! {"$lt": before});
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.col::<AdminCase>(COL)
|
||||
.find(query)
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|f| f.ok())
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
}
|
||||
112
crates/core/database/src/models/admin_cases/ops/reference.rs
Normal file
112
crates/core/database/src/models/admin_cases/ops/reference.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminCase, PartialAdminCase};
|
||||
|
||||
use super::AbstractAdminCases;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminCases for ReferenceDb {
|
||||
async fn admin_case_create(&self, case: AdminCase) -> Result<()> {
|
||||
let mut admin_cases = self.admin_cases.lock().await;
|
||||
if admin_cases.contains_key(&case.id) {
|
||||
Err(create_database_error!("insert", "admin_cases"))
|
||||
} else {
|
||||
admin_cases.insert(case.id.to_string(), case.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_case_assign_report(&self, case_id: &str, report_id: &str) -> Result<()> {
|
||||
let mut reports = self.safety_reports.lock().await;
|
||||
if let Some(report) = reports.get_mut(report_id) {
|
||||
report.case_id = Some(case_id.to_string());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_case_edit(&self, case_id: &str, partial: &PartialAdminCase) -> Result<()> {
|
||||
let mut admin_cases = self.admin_cases.lock().await;
|
||||
if let Some(case) = admin_cases.get_mut(case_id) {
|
||||
case.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_case_fetch(&self, case_id: &str) -> Result<AdminCase> {
|
||||
let admin_cases = self.admin_cases.lock().await;
|
||||
if let Some(case) = admin_cases.get(case_id) {
|
||||
Ok(case.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_case_fetch_from_shorthand(&self, short_id: &str) -> Result<AdminCase> {
|
||||
let admin_cases = self.admin_cases.lock().await;
|
||||
if let Some((_, case)) = admin_cases.iter().find(|(_, c)| c.short_id == short_id) {
|
||||
Ok(case.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// title is fuzzy, the rest of the arguments are direct matches
|
||||
/// before_id and limit are for paginating
|
||||
async fn admin_case_search(
|
||||
&self,
|
||||
title: Option<&str>,
|
||||
status: Option<&str>,
|
||||
owner_id: Option<&str>,
|
||||
tags: Option<Vec<String>>,
|
||||
before_id: Option<&str>,
|
||||
limit: i64,
|
||||
) -> Result<Vec<AdminCase>> {
|
||||
let admin_cases = self.admin_cases.lock().await;
|
||||
Ok(admin_cases
|
||||
.iter()
|
||||
.filter(|(_, case)| {
|
||||
if let Some(title) = title {
|
||||
case.title.to_lowercase().contains(title)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.filter(|(_, case)| {
|
||||
if let Some(status) = status {
|
||||
case.status == status
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.filter(|(_, case)| {
|
||||
if let Some(owner) = owner_id {
|
||||
case.owner_id == owner
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.filter(|(_, case)| {
|
||||
if let Some(tags) = &tags {
|
||||
tags.iter().filter(|p| case.tags.contains(p)).count() > 0
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.skip_while(|(id, _)| {
|
||||
if let Some(before) = before_id {
|
||||
id.as_str() <= before
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.by_ref()
|
||||
.take(limit as usize)
|
||||
.map(|(_, case)| case.clone())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/admin_comments/mod.rs
Normal file
5
crates/core/database/src/models/admin_comments/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
95
crates/core/database/src/models/admin_comments/models.rs
Normal file
95
crates/core/database/src/models/admin_comments/models.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use revolt_models::v0;
|
||||
|
||||
auto_derived_partial! {
|
||||
pub struct AdminComment {
|
||||
/// The comment ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The Object this comment is attached to
|
||||
pub object_id: String,
|
||||
/// The ID of the case this comment is attached to, if applicable
|
||||
pub case_id: Option<String>,
|
||||
/// The author ID
|
||||
pub user_id: String,
|
||||
/// When the comment was edited, if applicable, in iso8601
|
||||
pub edited_at: Option<String>,
|
||||
/// The content, if not a system event
|
||||
pub content: Option<String>,
|
||||
/// The system event that occurred, if applicable
|
||||
pub system_message: Option<String>,
|
||||
/// The system message target
|
||||
pub system_message_target: Option<String>,
|
||||
/// The system message raw context
|
||||
pub system_message_context: Option<String>,
|
||||
},
|
||||
"PartialAdminComment"
|
||||
}
|
||||
|
||||
impl AdminComment {
|
||||
pub fn new(
|
||||
object_id: &str,
|
||||
user_id: &str,
|
||||
content: &str,
|
||||
case_id: Option<&str>,
|
||||
) -> AdminComment {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminComment {
|
||||
id,
|
||||
object_id: object_id.to_string(),
|
||||
case_id: case_id.map(|c| c.to_string()),
|
||||
user_id: user_id.to_string(),
|
||||
edited_at: None,
|
||||
content: Some(content.to_string()),
|
||||
system_message: None,
|
||||
system_message_context: None,
|
||||
system_message_target: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_system_message(
|
||||
object_id: &str,
|
||||
user_id: &str,
|
||||
case_id: &str,
|
||||
system_message_kind: v0::AdminAuditItemActions,
|
||||
context: Option<&str>,
|
||||
target: &str,
|
||||
) -> AdminComment {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminComment {
|
||||
id,
|
||||
object_id: object_id.to_string(),
|
||||
case_id: Some(case_id.to_string()),
|
||||
user_id: user_id.to_string(),
|
||||
edited_at: None,
|
||||
content: None,
|
||||
system_message: Some(serde_json::to_string(&system_message_kind).unwrap()), // if this explodes i'll eat my hat.
|
||||
system_message_context: context.map(|c| c.to_string()),
|
||||
system_message_target: Some(target.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit the comment, updating the edited_at time as well
|
||||
pub fn edit(&mut self, content: &str) {
|
||||
self.content = Some(content.to_string());
|
||||
self.edited_at = Some(
|
||||
iso8601_timestamp::Timestamp::now_utc()
|
||||
.format_short()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialAdminComment {
|
||||
pub fn new() -> PartialAdminComment {
|
||||
PartialAdminComment::default()
|
||||
}
|
||||
/// Edit the comment, updating the edited_at time as well
|
||||
pub fn edit(&mut self, content: &str) {
|
||||
self.content = Some(content.to_string());
|
||||
self.edited_at = Some(
|
||||
iso8601_timestamp::Timestamp::now_utc()
|
||||
.format_short()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
crates/core/database/src/models/admin_comments/ops.rs
Normal file
28
crates/core/database/src/models/admin_comments/ops.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{models::admin_comments::AdminComment, PartialAdminComment};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminComments: Sync + Send {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()>;
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment>;
|
||||
|
||||
/// Fetch all comments related to the case. This includes comments made on other objects.
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>>;
|
||||
|
||||
/// Fetch all comments on an object
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>>;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminComment, PartialAdminComment};
|
||||
|
||||
use super::AbstractAdminComments;
|
||||
|
||||
static COL: &str = "admin_comments";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminComments for MongoDb {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> {
|
||||
query!(self, insert_one, COL, comment).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
comment_id,
|
||||
partial,
|
||||
vec![],
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment> {
|
||||
query!(self, find_one, COL, doc! {"_id": id})?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>> {
|
||||
query!(self, find, COL, doc! {"case_id": case_id})
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>> {
|
||||
query!(self, find, COL, doc! {"object_id": object_id})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminComment, PartialAdminComment};
|
||||
|
||||
use super::AbstractAdminComments;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminComments for ReferenceDb {
|
||||
async fn admin_comment_insert(&self, comment: AdminComment) -> Result<()> {
|
||||
let mut admin_comments = self.admin_comments.lock().await;
|
||||
if admin_comments.contains_key(&comment.id) {
|
||||
Err(create_database_error!("insert", "admin_comments"))
|
||||
} else {
|
||||
admin_comments.insert(comment.id.to_string(), comment.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_comment_update(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
partial: &PartialAdminComment,
|
||||
) -> Result<()> {
|
||||
let mut admin_comments = self.admin_comments.lock().await;
|
||||
if let Some(comment) = admin_comments.get_mut(comment_id) {
|
||||
comment.apply_options(partial.clone());
|
||||
comment.edited_at = Some(Timestamp::now_utc().format().to_string());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch(&self, id: &str) -> Result<AdminComment> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
admin_comments
|
||||
.get(id)
|
||||
.map_or(Err(create_error!(NotFound)), |ac| Ok(ac.clone()))
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_related_case(&self, case_id: &str) -> Result<Vec<AdminComment>> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
Ok(admin_comments
|
||||
.iter()
|
||||
.filter_map(|(_, c)| {
|
||||
if c.case_id.as_ref().is_some_and(|c| c == case_id) {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn admin_comment_fetch_object_comments(
|
||||
&self,
|
||||
object_id: &str,
|
||||
) -> Result<Vec<AdminComment>> {
|
||||
let admin_comments = self.admin_comments.lock().await;
|
||||
Ok(admin_comments
|
||||
.iter()
|
||||
.filter_map(|(_, c)| {
|
||||
if c.object_id == object_id {
|
||||
Some(c.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ auto_derived!(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[async_std::test]
|
||||
#[tokio::test]
|
||||
async fn migrate() {
|
||||
database_test!(|db| async move {
|
||||
// Initialise the database
|
||||
|
||||
@@ -28,6 +28,20 @@ impl AbstractMigrations for MongoDb {
|
||||
init::create_database(self).await;
|
||||
}
|
||||
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
if config.features.admin_api_enabled {
|
||||
let db = self.db();
|
||||
let colls = db
|
||||
.list_collection_names()
|
||||
.await
|
||||
.expect("Failed to fetch collection names.");
|
||||
if !colls.iter().any(|x| x == "admin_audits") {
|
||||
info!("You've enabled the admin api for the first time. Setting up database...");
|
||||
init::create_admin_database(&db).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ pub async fn create_database(db: &MongoDb) {
|
||||
info!("Creating database.");
|
||||
let db = db.db();
|
||||
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
db.create_collection("accounts")
|
||||
.await
|
||||
.expect("Failed to create accounts collection.");
|
||||
@@ -98,6 +100,18 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create pubsub collection.");
|
||||
|
||||
db.create_collection("sessions")
|
||||
.await
|
||||
.expect("Failed to create sessions collection.");
|
||||
|
||||
db.create_collection("account_invites")
|
||||
.await
|
||||
.expect("Failed to create account_invites collection.");
|
||||
|
||||
db.create_collection("mfa_tickets")
|
||||
.await
|
||||
.expect("Failed to create mfa_tickets collection.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "users",
|
||||
"indexes": [
|
||||
@@ -263,5 +277,219 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create ratelimit_events index.");
|
||||
|
||||
// Only create these tables if the admin api is enabled
|
||||
if config.features.admin_api_enabled {
|
||||
create_admin_database(&db).await;
|
||||
}
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "accounts",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"email": 1
|
||||
},
|
||||
"name": "email",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"email_normalised": 1
|
||||
},
|
||||
"name": "email_normalised",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"verification.token": 1
|
||||
},
|
||||
"name": "email_verification"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"password_reset.token": 1
|
||||
},
|
||||
"name": "password_reset"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"deletion.token": 1
|
||||
},
|
||||
"name": "account_deletion"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "sessions",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"token": 1
|
||||
},
|
||||
"name": "token",
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"user_id": 1
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "mfa_tickets",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"token": 1
|
||||
},
|
||||
"name": "token",
|
||||
"unique": true
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
info!("Created database.");
|
||||
}
|
||||
|
||||
pub async fn create_admin_database(db: &mongodb::Database) {
|
||||
db.create_collection("admin_audits")
|
||||
.await
|
||||
.expect("Failed to create admin_audits collection.");
|
||||
|
||||
db.create_collection("admin_comments")
|
||||
.await
|
||||
.expect("Failed to create admin_comments collection.");
|
||||
|
||||
db.create_collection("admin_cases")
|
||||
.await
|
||||
.expect("Failed to create admin_cases collection.");
|
||||
|
||||
db.create_collection("admin_strikes")
|
||||
.await
|
||||
.expect("Failed to create admin_strikes collection.");
|
||||
|
||||
db.create_collection("admin_tokens")
|
||||
.await
|
||||
.expect("Failed to create admin_tokens collection.");
|
||||
|
||||
db.create_collection("admin_users")
|
||||
.await
|
||||
.expect("Failed to create admin_users collection.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "admin_comments",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"case_id": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "case_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"object_id": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "object_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create admin_comments index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "admin_cases",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"id": 1_i32,
|
||||
},
|
||||
"unique": true,
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"short_id": 1_i32,
|
||||
},
|
||||
"unique": true,
|
||||
"name": "short_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create admin_cases index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "admin_users",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"email": 1_i32,
|
||||
},
|
||||
"unique": true,
|
||||
"name": "case_id"
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create admin_comments index.");
|
||||
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "admin_audits",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"mod_id": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "mod_id"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"target_id": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "target_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create admin_audits index.");
|
||||
|
||||
// indexes on regular data that are only needed for admin queries
|
||||
db.run_command(doc! {
|
||||
"createIndexes": "server_members",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"joined_at": 1_i32,
|
||||
},
|
||||
"unique": false,
|
||||
"name": "joined_at"
|
||||
},
|
||||
]
|
||||
})
|
||||
.await
|
||||
.expect("Failed to create server_members::joined_at index.");
|
||||
|
||||
info!("Created admin database.");
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use iso8601_timestamp::Timestamp;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_permissions::{ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -25,7 +26,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 50; // MUST BE +1 to last migration
|
||||
pub const LATEST_REVISION: i32 = 51; // MUST BE +1 to last migration
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -451,7 +452,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
warn!("This is a destructive operation and will wipe existing permission data (excl. defaults for SendMessage).");
|
||||
warn!("Taking a backup is advised.");
|
||||
warn!("Continuing in 10 seconds...");
|
||||
async_std::task::sleep(Duration::from_secs(10)).await;
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
|
||||
let servers = db.col::<Document>("servers");
|
||||
let mut cursor = servers.find(doc! {}).await.unwrap();
|
||||
@@ -573,20 +574,145 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
if revision <= 15 {
|
||||
info!("Running migration [revision 15 / 04-06-2022]: Migrate Authifier to latest version.");
|
||||
|
||||
let db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
|
||||
db.run_migration(authifier::Migration::M2022_06_03EnsureUpToSpec)
|
||||
if !db
|
||||
.db()
|
||||
.collection::<Document>("mfa_tickets")
|
||||
.list_index_names()
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap_or_default()
|
||||
.contains(&"token".to_owned())
|
||||
{
|
||||
// Make sure all collections exist
|
||||
let list = db.db().list_collection_names().await.unwrap();
|
||||
let collections = ["accounts", "sessions", "invites", "mfa_tickets"];
|
||||
|
||||
for name in collections {
|
||||
if !list.contains(&name.to_string()) {
|
||||
db.db().create_collection(name).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup index for `accounts`
|
||||
let col = db.db().collection::<Document>("accounts");
|
||||
col.drop_indexes().await.unwrap();
|
||||
|
||||
db.db()
|
||||
.run_command(doc! {
|
||||
"createIndexes": "accounts",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"email": 1
|
||||
},
|
||||
"name": "email",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"email_normalised": 1
|
||||
},
|
||||
"name": "email_normalised",
|
||||
"unique": true,
|
||||
"collation": {
|
||||
"locale": "en",
|
||||
"strength": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"verification.token": 1
|
||||
},
|
||||
"name": "email_verification"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"password_reset.token": 1
|
||||
},
|
||||
"name": "password_reset"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Setup index for `sessions`
|
||||
let col = db.db().collection::<Document>("sessions");
|
||||
col.drop_indexes().await.unwrap();
|
||||
|
||||
db.db()
|
||||
.run_command(doc! {
|
||||
"createIndexes": "sessions",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"token": 1
|
||||
},
|
||||
"name": "token",
|
||||
"unique": true
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"user_id": 1
|
||||
},
|
||||
"name": "user_id"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Setup index for `mfa_tickets`
|
||||
let col = db.db().collection::<Document>("mfa_tickets");
|
||||
col.drop_indexes().await.unwrap();
|
||||
|
||||
db.db()
|
||||
.run_command(doc! {
|
||||
"createIndexes": "mfa_tickets",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"token": 1
|
||||
},
|
||||
"name": "token",
|
||||
"unique": true
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if revision <= 16 {
|
||||
info!("Running migration [revision 16 / 07-07-2022]: Add `emojis` collection and Authifier migration.");
|
||||
|
||||
let authifier_db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
|
||||
authifier_db
|
||||
.run_migration(authifier::Migration::M2022_06_09AddIndexForDeletion)
|
||||
if !db
|
||||
.db()
|
||||
.collection::<Document>("accounts")
|
||||
.list_index_names()
|
||||
.await
|
||||
.unwrap();
|
||||
.expect("list of index names")
|
||||
.contains(&"account_deletion".to_owned())
|
||||
{
|
||||
db.db()
|
||||
.run_command(doc! {
|
||||
"createIndexes": "accounts",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"deletion.token": 1
|
||||
},
|
||||
"name": "account_deletion"
|
||||
}
|
||||
]
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
db.db()
|
||||
.create_collection("emojis")
|
||||
@@ -1085,7 +1211,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
enum Channel {
|
||||
Group { owner: String },
|
||||
TextChannel { server: String },
|
||||
VoiceChannel { server: String }
|
||||
VoiceChannel { server: String },
|
||||
}
|
||||
|
||||
let webhooks = db
|
||||
@@ -1099,7 +1225,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.await;
|
||||
|
||||
for webhook in webhooks {
|
||||
match db.col::<Channel>("channels").find_one(doc! { "_id": &webhook.channel_id }).await.unwrap() {
|
||||
match db
|
||||
.col::<Channel>("channels")
|
||||
.find_one(doc! { "_id": &webhook.channel_id })
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
Some(channel) => {
|
||||
let creator_id = match channel {
|
||||
Channel::Group { owner, .. } => owner,
|
||||
@@ -1141,10 +1272,51 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
"Running migration [revision 32 / 12-05-2025]: (Authifier) Add last_seen to sessions."
|
||||
);
|
||||
|
||||
let db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
|
||||
db.run_migration(authifier::Migration::M2025_02_20AddLastSeenToSession)
|
||||
.await
|
||||
.unwrap();
|
||||
loop {
|
||||
#[derive(Deserialize)]
|
||||
struct SessionId {
|
||||
_id: String,
|
||||
}
|
||||
|
||||
let sessions: Vec<SessionId> = db
|
||||
.db()
|
||||
.collection("sessions")
|
||||
.find(doc! {
|
||||
"$or": [
|
||||
{ "last_seen": { "$exists": false } },
|
||||
{ "last_seen": "1970-01-01T00:00:00.000Z" }
|
||||
]
|
||||
})
|
||||
.limit(50_000) // about 400 batches for 2 million
|
||||
.await
|
||||
.expect("Failed to create cursor for sessions!")
|
||||
.map(|doc| doc.expect("id and username"))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
if sessions.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for session in sessions {
|
||||
let timestamp = iso8601_timestamp::Timestamp::from(Ulid::from_string(&session._id).unwrap().datetime());
|
||||
|
||||
db.db()
|
||||
.collection::<Document>("sessions")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": &session._id.to_string(),
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"last_seen": timestamp.format().to_string()
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update a session.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if revision <= 40 {
|
||||
@@ -1240,7 +1412,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
"channel_type": "TextChannel",
|
||||
"voice": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update voice channels");
|
||||
@@ -1292,10 +1464,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
let mut doc = doc! {};
|
||||
|
||||
for id in server.roles.keys() {
|
||||
doc.insert(
|
||||
format!("roles.{id}._id"),
|
||||
id,
|
||||
);
|
||||
doc.insert(format!("roles.{id}._id"), id);
|
||||
}
|
||||
|
||||
db.db()
|
||||
@@ -1306,6 +1475,21 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
}
|
||||
};
|
||||
|
||||
if revision <= 50 {
|
||||
info!("Running migration [revision 50 / 13-04-2026]: Rename invites collection to account_invites");
|
||||
|
||||
db.db()
|
||||
.client()
|
||||
.database("admin")
|
||||
.run_command(doc! {
|
||||
"renameCollection": "revolt.invites",
|
||||
"to": "revolt.account_invites",
|
||||
"dropTarget": true
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
LATEST_REVISION.max(revision)
|
||||
}
|
||||
|
||||
5
crates/core/database/src/models/admin_strikes/mod.rs
Normal file
5
crates/core/database/src/models/admin_strikes/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
47
crates/core/database/src/models/admin_strikes/models.rs
Normal file
47
crates/core/database/src/models/admin_strikes/models.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::util::basic::transform_optional_string;
|
||||
|
||||
auto_derived_partial! {
|
||||
pub struct AdminStrike {
|
||||
/// The strike ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The object receiving the strike (user/server)
|
||||
pub target_id: String,
|
||||
/// The moderator who gave the strike
|
||||
pub mod_id: String,
|
||||
/// The case the strike was made under
|
||||
pub case_id: Option<String>,
|
||||
/// Action associated with the strike (eg. suspension/ban)
|
||||
pub associated_action: Option<String>,
|
||||
/// Has the strike been removed
|
||||
pub overruled: bool,
|
||||
/// The user-facing reason for the strike
|
||||
pub reason: String,
|
||||
/// Internal context for the strike
|
||||
pub mod_context: Option<String>,
|
||||
},
|
||||
"PartialAdminStrike"
|
||||
}
|
||||
|
||||
impl AdminStrike {
|
||||
pub fn new(
|
||||
target_id: &str,
|
||||
mod_id: &str,
|
||||
case_id: Option<&str>,
|
||||
associated_action: Option<&str>,
|
||||
reason: &str,
|
||||
mod_context: Option<&str>,
|
||||
) -> AdminStrike {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminStrike {
|
||||
id,
|
||||
target_id: target_id.to_string(),
|
||||
mod_id: mod_id.to_string(),
|
||||
case_id: transform_optional_string(case_id),
|
||||
associated_action: transform_optional_string(associated_action),
|
||||
overruled: false,
|
||||
reason: reason.to_string(),
|
||||
mod_context: transform_optional_string(mod_context),
|
||||
}
|
||||
}
|
||||
}
|
||||
17
crates/core/database/src/models/admin_strikes/ops.rs
Normal file
17
crates/core/database/src/models/admin_strikes/ops.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_strikes::models::{AdminStrike, PartialAdminStrike};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminStrikes: Sync + Send {
|
||||
async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()>;
|
||||
|
||||
async fn admin_strike_update(&self, strike_id: &str, partial: PartialAdminStrike)
|
||||
-> Result<()>;
|
||||
|
||||
async fn admin_strike_get(&self, strike_id: &str) -> Result<AdminStrike>;
|
||||
|
||||
async fn admin_strike_get_user(&self, user_id: &str) -> Result<Vec<AdminStrike>>;
|
||||
}
|
||||
41
crates/core/database/src/models/admin_strikes/ops/mongodb.rs
Normal file
41
crates/core/database/src/models/admin_strikes/ops/mongodb.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminStrike, PartialAdminStrike};
|
||||
|
||||
use super::AbstractAdminStrikes;
|
||||
|
||||
static COL: &str = "admin_strikes";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminStrikes for MongoDb {
|
||||
async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()> {
|
||||
query!(self, insert_one, COL, strike).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_strike_update(
|
||||
&self,
|
||||
strike_id: &str,
|
||||
partial: PartialAdminStrike,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
strike_id,
|
||||
partial,
|
||||
vec![],
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_strike_get(&self, strike_id: &str) -> Result<AdminStrike> {
|
||||
query!(self, find_one_by_id, COL, strike_id)?
|
||||
.ok_or_else(|| create_database_error!("find_one", COL))?
|
||||
}
|
||||
|
||||
async fn admin_strike_get_user(&self, user_id: &str) -> Result<Vec<AdminStrike>> {
|
||||
query!(self, find, COL, doc! {"target_id": user_id})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminStrike, PartialAdminStrike};
|
||||
|
||||
use super::AbstractAdminStrikes;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminStrikes for ReferenceDb {
|
||||
async fn admin_strike_insert(&self, strike: AdminStrike) -> Result<()> {
|
||||
let mut admin_strikes = self.admin_strikes.lock().await;
|
||||
if admin_strikes.contains_key(&strike.id) {
|
||||
Err(create_database_error!("insert", "admin_strikes"))
|
||||
} else {
|
||||
admin_strikes.insert(strike.id.to_string(), strike.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_strike_update(
|
||||
&self,
|
||||
strike_id: &str,
|
||||
partial: PartialAdminStrike,
|
||||
) -> Result<()> {
|
||||
let mut admin_strikes = self.admin_strikes.lock().await;
|
||||
if let Some(strike) = admin_strikes.get_mut(strike_id) {
|
||||
strike.apply_options(partial);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_strike_get(&self, strike_id: &str) -> Result<AdminStrike> {
|
||||
let admin_strikes = self.admin_strikes.lock().await;
|
||||
if let Some(strike) = admin_strikes.get(strike_id) {
|
||||
Ok(strike.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_strike_get_user(&self, user_id: &str) -> Result<Vec<AdminStrike>> {
|
||||
let admin_strikes = self.admin_strikes.lock().await;
|
||||
Ok(admin_strikes
|
||||
.iter()
|
||||
.filter(|(_, strike)| strike.target_id == user_id)
|
||||
.map(|(_, strike)| strike.clone())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
12
crates/core/database/src/models/admin_tokens/mod.rs
Normal file
12
crates/core/database/src/models/admin_tokens/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod rocket;
|
||||
mod schema;
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::rocket::*;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::schema::*;
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
71
crates/core/database/src/models/admin_tokens/models.rs
Normal file
71
crates/core/database/src/models/admin_tokens/models.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AdminUser, Database};
|
||||
|
||||
auto_derived! {
|
||||
pub struct AdminToken {
|
||||
/// The token ID
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The user this token is attached to
|
||||
pub user_id: String,
|
||||
/// The token itself
|
||||
pub token: String,
|
||||
/// The expiry timestamp for this token, in iso6801
|
||||
pub expiry: String
|
||||
}
|
||||
|
||||
/// This struct is used to validate machine tokens when doing machine to machine communication.
|
||||
pub struct AdminMachineToken {
|
||||
/// Placeholder field.
|
||||
pub on_behalf_of: AdminUser
|
||||
}
|
||||
|
||||
pub enum AdminAuthorization {
|
||||
AdminUser(AdminUser),
|
||||
AdminMachine(AdminMachineToken),
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminToken {
|
||||
pub fn new(user_id: &str, expiry: Timestamp) -> AdminToken {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
let token = nanoid::nanoid!(64);
|
||||
AdminToken {
|
||||
id,
|
||||
user_id: user_id.to_string(),
|
||||
token,
|
||||
expiry: expiry.format_short().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminMachineToken {
|
||||
pub async fn new_from_email(email: &str, db: &Database) -> Result<AdminMachineToken> {
|
||||
println!("{}", email);
|
||||
if email == "example@example.com" {
|
||||
// This is basically just a workaround to make the first user.
|
||||
let user_count = db.admin_user_count().await?;
|
||||
println!("{}", user_count);
|
||||
if user_count == 0 {
|
||||
return Ok(AdminMachineToken {
|
||||
on_behalf_of: AdminUser {
|
||||
id: "00".to_string(),
|
||||
platform_user_id: "".to_string(),
|
||||
email: "example@example.com".to_string(),
|
||||
active: true,
|
||||
permissions: u64::MAX,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
let user = db.admin_user_fetch_email(email).await?;
|
||||
Ok(AdminMachineToken { on_behalf_of: user })
|
||||
}
|
||||
|
||||
pub async fn new_from_id(user_id: &str, db: &Database) -> Result<AdminMachineToken> {
|
||||
let user = db.admin_user_fetch(user_id).await?;
|
||||
Ok(AdminMachineToken { on_behalf_of: user })
|
||||
}
|
||||
}
|
||||
16
crates/core/database/src/models/admin_tokens/ops.rs
Normal file
16
crates/core/database/src/models/admin_tokens/ops.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_tokens::AdminToken;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminTokens: Sync + Send {
|
||||
async fn admin_token_create(&self, token: AdminToken) -> Result<()>;
|
||||
|
||||
async fn admin_token_revoke(&self, token_id: &str) -> Result<()>;
|
||||
|
||||
async fn admin_token_authenticate(&self, token: &str) -> Result<AdminToken>;
|
||||
|
||||
async fn admin_token_fetch(&self, id: &str) -> Result<AdminToken>;
|
||||
}
|
||||
34
crates/core/database/src/models/admin_tokens/ops/mongodb.rs
Normal file
34
crates/core/database/src/models/admin_tokens/ops/mongodb.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AdminToken;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractAdminTokens;
|
||||
|
||||
static COL: &str = "admin_tokens";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminTokens for MongoDb {
|
||||
async fn admin_token_create(&self, token: AdminToken) -> Result<()> {
|
||||
query!(self, insert_one, COL, token).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_token_revoke(&self, token_id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, token_id).map(|k| {
|
||||
if k.deleted_count > 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
})?
|
||||
}
|
||||
|
||||
async fn admin_token_authenticate(&self, token: &str) -> Result<AdminToken> {
|
||||
query!(self, find_one, COL, doc! {"token": token})?
|
||||
.ok_or_else(|| create_error!(InvalidCredentials))
|
||||
}
|
||||
|
||||
async fn admin_token_fetch(&self, id: &str) -> Result<AdminToken> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::AdminToken;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractAdminTokens;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminTokens for ReferenceDb {
|
||||
async fn admin_token_create(&self, token: AdminToken) -> Result<()> {
|
||||
let mut admin_tokens = self.admin_tokens.lock().await;
|
||||
if admin_tokens.contains_key(&token.id) {
|
||||
Err(create_database_error!("insert", "admin_tokens"))
|
||||
} else {
|
||||
admin_tokens.insert(token.id.to_string(), token.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_token_revoke(&self, token_id: &str) -> Result<()> {
|
||||
let mut admin_tokens = self.admin_tokens.lock().await;
|
||||
if admin_tokens.remove(token_id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_token_authenticate(&self, token: &str) -> Result<AdminToken> {
|
||||
let admin_tokens = self.admin_tokens.lock().await;
|
||||
let result = admin_tokens
|
||||
.iter()
|
||||
.filter_map(|(_, t)| {
|
||||
if t.token == token {
|
||||
Some(t.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.next();
|
||||
|
||||
Ok(result.ok_or_else(|| create_error!(NotFound))?)
|
||||
}
|
||||
|
||||
async fn admin_token_fetch(&self, id: &str) -> Result<AdminToken> {
|
||||
let admin_tokens = self.admin_tokens.lock().await;
|
||||
let result = admin_tokens.iter().find(|tok| tok.0 == id);
|
||||
|
||||
Ok(result
|
||||
.map(|t| t.1.clone())
|
||||
.ok_or_else(|| create_error!(NotFound))?)
|
||||
}
|
||||
}
|
||||
146
crates/core/database/src/models/admin_tokens/rocket.rs
Normal file
146
crates/core/database/src/models/admin_tokens/rocket.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::{capture_internal_error, report_error};
|
||||
use revolt_result::{Error, Result};
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, FromRequest, Outcome, Request};
|
||||
|
||||
use crate::{AdminAuthorization, AdminMachineToken, AdminUser, Database};
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminMachineToken {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let user: &Option<AdminMachineToken> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
if let Some(on_behalf_of) = request
|
||||
.headers()
|
||||
.get("x-admin-on-behalf-of")
|
||||
.next()
|
||||
.map(|x| x.to_string())
|
||||
{
|
||||
if let Some(token) = request
|
||||
.headers()
|
||||
.get("x-admin-machine")
|
||||
.next()
|
||||
.map(|x| x.to_string())
|
||||
{
|
||||
let config = revolt_config::config().await;
|
||||
let token = token.to_string();
|
||||
if config.api.security.admin_keys.contains(&token) {
|
||||
let resp: Result<AdminMachineToken> = if on_behalf_of.contains("@") {
|
||||
AdminMachineToken::new_from_email(&on_behalf_of, db).await
|
||||
} else {
|
||||
AdminMachineToken::new_from_id(&on_behalf_of, db).await
|
||||
};
|
||||
if let Ok(resp) = resp {
|
||||
return Some(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(user) = user {
|
||||
Outcome::Success(user.clone())
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(InvalidSession)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminAuthorization {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let machine: &Option<AdminMachineToken> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
if let Some(token) = request
|
||||
.headers()
|
||||
.get("x-admin-machine")
|
||||
.next()
|
||||
.map(|x| x.to_string())
|
||||
{
|
||||
if let Some(on_behalf_of) = request
|
||||
.headers()
|
||||
.get("x-admin-on-behalf-of")
|
||||
.next()
|
||||
.map(|x| x.to_string())
|
||||
{
|
||||
let config = revolt_config::config().await;
|
||||
let token = token.to_string();
|
||||
if config.api.security.admin_keys.contains(&token) {
|
||||
let resp: Result<AdminMachineToken> = if on_behalf_of.contains("@") {
|
||||
AdminMachineToken::new_from_email(&on_behalf_of, db).await
|
||||
} else {
|
||||
AdminMachineToken::new_from_id(&on_behalf_of, db).await
|
||||
};
|
||||
if let Ok(resp) = resp {
|
||||
return Some(resp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(machine) = machine {
|
||||
if !machine.on_behalf_of.active {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(LockedOut)))
|
||||
} else {
|
||||
Outcome::Success(AdminAuthorization::AdminMachine(machine.clone()))
|
||||
}
|
||||
} else {
|
||||
let user: &Option<AdminUser> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
let token = request
|
||||
.headers()
|
||||
.get("x-admin-user")
|
||||
.next()
|
||||
.map(|x| x.to_string());
|
||||
|
||||
if let Some(token) = token {
|
||||
if let Ok(admin_token) = db.admin_token_authenticate(&token).await {
|
||||
if let Some(expiry) = Timestamp::parse(&admin_token.expiry) {
|
||||
if expiry < Timestamp::now_utc() {
|
||||
if let Err(e) = db.admin_token_revoke(&admin_token.id).await {
|
||||
capture_internal_error!(e);
|
||||
}
|
||||
return None;
|
||||
} else if let Ok(user) =
|
||||
db.admin_user_fetch(&admin_token.user_id).await
|
||||
{
|
||||
if !user.active {
|
||||
return None;
|
||||
}
|
||||
return Some(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(user) = user {
|
||||
if !user.active {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(LockedOut)))
|
||||
} else {
|
||||
Outcome::Success(AdminAuthorization::AdminUser(user.clone()))
|
||||
}
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(InvalidSession)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
crates/core/database/src/models/admin_tokens/schema.rs
Normal file
59
crates/core/database/src/models/admin_tokens/schema.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
|
||||
use revolt_rocket_okapi::{
|
||||
gen::OpenApiGenerator,
|
||||
request::{OpenApiFromRequest, RequestHeaderInput},
|
||||
};
|
||||
|
||||
use crate::{AdminAuthorization, AdminMachineToken};
|
||||
|
||||
impl OpenApiFromRequest<'_> for AdminAuthorization {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
_name: String,
|
||||
_required: bool,
|
||||
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
||||
let mut requirements = schemars::Map::new();
|
||||
requirements.insert("Admin Token".to_owned(), vec![]);
|
||||
|
||||
Ok(RequestHeaderInput::Security(
|
||||
"Admin Token".to_owned(),
|
||||
SecurityScheme {
|
||||
data: SecuritySchemeData::ApiKey {
|
||||
name: "x-admin-user".to_owned(),
|
||||
location: "header".to_owned(),
|
||||
},
|
||||
description: Some("Used to authenticate as an admin user.
|
||||
Can instead use an x-admin-machine token with an x-on-behalf-of containing the userid or email of
|
||||
the user the machine is performing the action for.".to_owned()),
|
||||
extensions: schemars::Map::new(),
|
||||
},
|
||||
requirements,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenApiFromRequest<'_> for AdminMachineToken {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
_name: String,
|
||||
_required: bool,
|
||||
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
|
||||
let mut requirements = schemars::Map::new();
|
||||
requirements.insert("Admin Machine Token".to_owned(), vec![]);
|
||||
|
||||
Ok(RequestHeaderInput::Security(
|
||||
"Admin Machine Token".to_owned(),
|
||||
SecurityScheme {
|
||||
data: SecuritySchemeData::ApiKey {
|
||||
name: "x-admin-machine".to_owned(),
|
||||
location: "header".to_owned(),
|
||||
},
|
||||
description: Some("Used by machines to authenticate on behalf of a user.
|
||||
Machines are trusted devices that authenticate as other users via providing the x-on-behalf-of header
|
||||
with an admin user's ID or email.".to_owned()),
|
||||
extensions: schemars::Map::new(),
|
||||
},
|
||||
requirements,
|
||||
))
|
||||
}
|
||||
}
|
||||
9
crates/core/database/src/models/admin_users/mod.rs
Normal file
9
crates/core/database/src/models/admin_users/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod models;
|
||||
mod ops;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod rocket;
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::rocket::*;
|
||||
pub use models::*;
|
||||
pub use ops::*;
|
||||
69
crates/core/database/src/models/admin_users/models.rs
Normal file
69
crates/core/database/src/models/admin_users/models.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
auto_derived_partial! {
|
||||
pub struct AdminUser {
|
||||
/// The ID of the user
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// The user's revolt ID.
|
||||
pub platform_user_id: String,
|
||||
/// The user's email
|
||||
pub email: String,
|
||||
/// Whether the user is active or not (ie. can they use the api)
|
||||
pub active: bool,
|
||||
/// The permissions of the user
|
||||
pub permissions: u64,
|
||||
},
|
||||
"PartialAdminUser"
|
||||
}
|
||||
|
||||
impl AdminUser {
|
||||
pub fn new(email: &str, platform_user_id: &str, permissions: u64) -> AdminUser {
|
||||
let id = ulid::Ulid::new().to_string();
|
||||
AdminUser {
|
||||
id,
|
||||
platform_user_id: platform_user_id.to_string(),
|
||||
email: email.to_string(),
|
||||
active: true,
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_id(id: &str, db: &Database) -> Result<AdminUser> {
|
||||
return db.admin_user_fetch(id).await;
|
||||
}
|
||||
|
||||
pub async fn find_by_email(email: &str, db: &Database) -> Result<AdminUser> {
|
||||
return db.admin_user_fetch_email(email).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AdminUserPermissionFlagsValue(pub u64);
|
||||
|
||||
impl AdminUserPermissionFlagsValue {
|
||||
pub fn has(&self, flag: revolt_models::v0::AdminUserPermissionFlags) -> bool {
|
||||
self.has_value(flag as u64)
|
||||
}
|
||||
pub fn has_value(&self, bit: u64) -> bool {
|
||||
let mask = 1 << bit;
|
||||
self.0 & mask == mask
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&mut self,
|
||||
flag: revolt_models::v0::AdminUserPermissionFlags,
|
||||
toggle: bool,
|
||||
) -> &mut Self {
|
||||
self.set_value(flag as u64, toggle)
|
||||
}
|
||||
pub fn set_value(&mut self, bit: u64, toggle: bool) -> &mut Self {
|
||||
if toggle {
|
||||
self.0 |= 1 << bit;
|
||||
} else {
|
||||
self.0 &= !(1 << bit);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
20
crates/core/database/src/models/admin_users/ops.rs
Normal file
20
crates/core/database/src/models/admin_users/ops.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::models::admin_users::models::{AdminUser, PartialAdminUser};
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAdminUsers: Sync + Send {
|
||||
async fn admin_user_insert(&self, user: AdminUser) -> Result<()>;
|
||||
|
||||
async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()>;
|
||||
|
||||
async fn admin_user_fetch(&self, user_id: &str) -> Result<AdminUser>;
|
||||
|
||||
async fn admin_user_fetch_email(&self, email: &str) -> Result<AdminUser>;
|
||||
|
||||
async fn admin_user_list(&self) -> Result<Vec<AdminUser>>;
|
||||
|
||||
async fn admin_user_count(&self) -> Result<u16>;
|
||||
}
|
||||
35
crates/core/database/src/models/admin_users/ops/mongodb.rs
Normal file
35
crates/core/database/src/models/admin_users/ops/mongodb.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AdminUser, PartialAdminUser};
|
||||
|
||||
use super::AbstractAdminUsers;
|
||||
|
||||
static COL: &str = "admin_users";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminUsers for MongoDb {
|
||||
async fn admin_user_insert(&self, user: AdminUser) -> Result<()> {
|
||||
query!(self, insert_one, COL, user).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, user_id, partial, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
async fn admin_user_fetch(&self, user_id: &str) -> Result<AdminUser> {
|
||||
query!(self, find_one_by_id, COL, user_id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
async fn admin_user_fetch_email(&self, email: &str) -> Result<AdminUser> {
|
||||
query!(self, find_one, COL, doc! {"email": email})?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
async fn admin_user_list(&self) -> Result<Vec<AdminUser>> {
|
||||
query!(self, find, COL, doc! {})
|
||||
}
|
||||
|
||||
async fn admin_user_count(&self) -> Result<u16> {
|
||||
query!(self, count_documents, COL, doc! {}).map(|resp| resp as u16)
|
||||
}
|
||||
}
|
||||
56
crates/core/database/src/models/admin_users/ops/reference.rs
Normal file
56
crates/core/database/src/models/admin_users/ops/reference.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AdminUser, PartialAdminUser};
|
||||
|
||||
use super::AbstractAdminUsers;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAdminUsers for ReferenceDb {
|
||||
async fn admin_user_insert(&self, user: AdminUser) -> Result<()> {
|
||||
let mut admin_users = self.admin_users.lock().await;
|
||||
if admin_users.contains_key(&user.id) {
|
||||
Err(create_database_error!("insert", "admin_users"))
|
||||
} else {
|
||||
admin_users.insert(user.id.to_string(), user.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_user_update(&self, user_id: &str, partial: PartialAdminUser) -> Result<()> {
|
||||
let mut admin_users = self.admin_users.lock().await;
|
||||
if let Some(existing_user) = admin_users.get_mut(user_id) {
|
||||
existing_user.apply_options(partial);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_user_fetch(&self, user_id: &str) -> Result<AdminUser> {
|
||||
let admin_users = self.admin_users.lock().await;
|
||||
if let Some(user) = admin_users.get(user_id) {
|
||||
Ok(user.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_user_fetch_email(&self, email: &str) -> Result<AdminUser> {
|
||||
let admin_users = self.admin_users.lock().await;
|
||||
if let Some((_, user)) = admin_users.iter().filter(|(_, p)| p.email == email).next() {
|
||||
Ok(user.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn admin_user_list(&self) -> Result<Vec<AdminUser>> {
|
||||
let admin_users = self.admin_users.lock().await;
|
||||
Ok(admin_users.iter().map(|(_, u)| u).cloned().collect())
|
||||
}
|
||||
|
||||
async fn admin_user_count(&self) -> Result<u16> {
|
||||
Ok(self.admin_users.lock().await.len() as u16)
|
||||
}
|
||||
}
|
||||
43
crates/core/database/src/models/admin_users/rocket.rs
Normal file
43
crates/core/database/src/models/admin_users/rocket.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, FromRequest, Outcome, Request};
|
||||
use revolt_result::Error;
|
||||
use crate::{AdminUser, Database};
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminUser {
|
||||
type Error = Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let user: &Option<AdminUser> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
let token = request
|
||||
.headers()
|
||||
.get("x-admin-user")
|
||||
.next()
|
||||
.map(|x| x.to_string());
|
||||
|
||||
if let Some(token) = token {
|
||||
if let Ok(admin_token) = db.admin_token_authenticate(&token).await {
|
||||
if let Ok(user) = db.admin_user_fetch(&admin_token.user_id).await {
|
||||
return Some(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(user) = user {
|
||||
if !user.active {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(LockedOut)))
|
||||
} else {
|
||||
Outcome::Success(user.clone())
|
||||
}
|
||||
} else {
|
||||
Outcome::Error((Status::Unauthorized, create_error!(InvalidCredentials)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ impl Bot {
|
||||
mod tests {
|
||||
use crate::{Bot, FieldsBot, PartialBot, User};
|
||||
|
||||
#[async_std::test]
|
||||
#[tokio::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let owner = User::create(&db, "Owner".to_string(), None, None)
|
||||
|
||||
@@ -12,7 +12,14 @@ static COL: &str = "channel_invites";
|
||||
impl AbstractChannelInvites for MongoDb {
|
||||
/// Insert a new invite into the database
|
||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
||||
query!(self, insert_one, COL, &invite).map(|_| ())
|
||||
self.insert_one(COL, &invite).await.or_else(|e| {
|
||||
if e.to_string().contains("duplicate") {
|
||||
return Err(create_error!(InviteExists));
|
||||
} else {
|
||||
return Err(create_database_error!("insert_one", COL));
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch an invite by the code
|
||||
|
||||
@@ -128,7 +128,7 @@ impl Webhook {
|
||||
mod tests {
|
||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
||||
|
||||
#[async_std::test]
|
||||
#[tokio::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let webhook_id = "webhook";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#![allow(deprecated)]
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use redis_kiss::get_connection;
|
||||
use revolt_config::config;
|
||||
use revolt_models::v0::{self, MessageAuthor};
|
||||
use revolt_permissions::OverrideField;
|
||||
@@ -110,6 +111,10 @@ auto_derived!(
|
||||
/// Voice Information for when this channel is also a voice channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voice: Option<VoiceInformation>,
|
||||
|
||||
/// The channel's slowmode delay in seconds
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
slowmode: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -146,6 +151,8 @@ auto_derived!(
|
||||
pub last_message_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub voice: Option<VoiceInformation>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slowmode: Option<u64>,
|
||||
}
|
||||
|
||||
/// Optional fields on channel object
|
||||
@@ -206,6 +213,7 @@ impl Channel {
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
voice: data.voice.map(|voice| voice.into()),
|
||||
slowmode: None,
|
||||
},
|
||||
v0::LegacyServerChannelType::Voice => Channel::TextChannel {
|
||||
id: id.clone(),
|
||||
@@ -218,6 +226,7 @@ impl Channel {
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
voice: Some(data.voice.unwrap_or_default().into()),
|
||||
slowmode: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -399,7 +408,10 @@ impl Channel {
|
||||
/// Check whether has a user as a recipient
|
||||
pub fn contains_user(&self, user_id: &str) -> bool {
|
||||
match self {
|
||||
Channel::Group { recipients, .. } => recipients.contains(&String::from(user_id)),
|
||||
Channel::Group { recipients, .. } | Channel::DirectMessage { recipients, .. } => {
|
||||
recipients.iter().any(|recipient| recipient == user_id)
|
||||
}
|
||||
Channel::SavedMessages { user, .. } => user == user_id,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -407,7 +419,9 @@ impl Channel {
|
||||
/// Get list of recipients
|
||||
pub fn users(&self) -> Result<Vec<String>> {
|
||||
match self {
|
||||
Channel::Group { recipients, .. } => Ok(recipients.to_owned()),
|
||||
Channel::Group { recipients, .. } | Channel::DirectMessage { recipients, .. } => {
|
||||
Ok(recipients.to_owned())
|
||||
}
|
||||
_ => Err(create_error!(NotFound)),
|
||||
}
|
||||
}
|
||||
@@ -635,7 +649,7 @@ impl Channel {
|
||||
}
|
||||
|
||||
/// Acknowledge a message
|
||||
pub async fn ack(&self, user: &str, message: &str) -> Result<()> {
|
||||
pub async fn ack(&self, user: &str, message: &str, amqp: &AMQP) -> Result<()> {
|
||||
EventV1::ChannelAck {
|
||||
id: self.id().to_string(),
|
||||
user: user.to_string(),
|
||||
@@ -644,17 +658,7 @@ impl Channel {
|
||||
.private(user.to_string())
|
||||
.await;
|
||||
|
||||
#[cfg(feature = "tasks")]
|
||||
crate::tasks::ack::queue_ack(
|
||||
self.id().to_string(),
|
||||
user.to_string(),
|
||||
crate::tasks::ack::AckEvent::AckMessage {
|
||||
id: message.to_string(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
crate::util::acker::ack_channel(user, self.id(), message, amqp).await
|
||||
}
|
||||
|
||||
/// Remove user from a group
|
||||
@@ -763,6 +767,13 @@ impl Channel {
|
||||
// - channels list / categories list on server
|
||||
db.delete_channel(self).await
|
||||
}
|
||||
|
||||
/// Wipe a channel's messages
|
||||
pub async fn wipe(&self, db: &Database) -> Result<()> {
|
||||
let id = self.id().to_string();
|
||||
EventV1::ChannelWipe { id: id.clone() }.p(id).await;
|
||||
db.wipe_channel(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
@@ -783,7 +794,7 @@ mod tests {
|
||||
|
||||
use crate::{fixture, util::permissions::DatabasePermissionQuery};
|
||||
|
||||
#[async_std::test]
|
||||
#[tokio::test]
|
||||
async fn permissions_group_channel() {
|
||||
database_test!(|db| async move {
|
||||
fixture!(db, "group_with_members",
|
||||
@@ -809,7 +820,7 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
#[tokio::test]
|
||||
async fn permissions_text_channel() {
|
||||
database_test!(|db| async move {
|
||||
fixture!(db, "server_with_roles",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
||||
use crate::{Channel, FieldsChannel, PartialChannel, revolt_result::Result, util::ChunkedDatabaseGenerator};
|
||||
use revolt_permissions::OverrideField;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
@@ -19,6 +19,9 @@ pub trait AbstractChannels: Sync + Send {
|
||||
/// Fetch all direct messages for a user
|
||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>>;
|
||||
|
||||
// Fetch all group dms for a user
|
||||
async fn find_group_message_channels(&self, user_id: &str) -> Result<ChunkedDatabaseGenerator<Channel>>;
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel>;
|
||||
|
||||
@@ -47,6 +50,10 @@ pub trait AbstractChannels: Sync + Send {
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
||||
|
||||
// Remove a user from all specified groups
|
||||
async fn remove_user_from_groups(&self, channel_ids: Vec<String>, user_id: &str) -> Result<()>;
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
|
||||
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>; // Wipe a channel's messages
|
||||
async fn wipe_channel(&self, channel: &Channel) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::AbstractChannels;
|
||||
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
||||
use crate::{AbstractServers, Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel, util::ChunkedDatabaseGenerator};
|
||||
use bson::{Bson, Document};
|
||||
use futures::StreamExt;
|
||||
use mongodb::options::ReadConcern;
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
|
||||
@@ -69,6 +70,32 @@ impl AbstractChannels for MongoDb {
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch all group dms for a user
|
||||
async fn find_group_message_channels(&self, user_id: &str) -> Result<ChunkedDatabaseGenerator<Channel>> {
|
||||
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(COL)
|
||||
.find(doc! {
|
||||
"channel_type": "Group",
|
||||
"recipients": user_id
|
||||
})
|
||||
.session(&mut session)
|
||||
.batch_size(100)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?;
|
||||
|
||||
Ok(ChunkedDatabaseGenerator::new_mongo(session, cursor))
|
||||
}
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
query!(
|
||||
@@ -180,13 +207,29 @@ impl AbstractChannels for MongoDb {
|
||||
.map_err(|_| create_database_error!("update_one", "channels"))
|
||||
}
|
||||
|
||||
// Remove a user from all specified groups
|
||||
async fn remove_user_from_groups(&self, channel_ids: Vec<String>, user_id: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
doc! {
|
||||
"_id": { "$in": channel_ids },
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"recipients": user_id
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
}
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let id = channel.id().to_string();
|
||||
let server_id = match channel {
|
||||
Channel::TextChannel { server, .. } => {
|
||||
Some(server)
|
||||
}
|
||||
Channel::TextChannel { server, .. } => Some(server),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -261,6 +304,23 @@ impl AbstractChannels for MongoDb {
|
||||
// Delete the channel itself
|
||||
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
|
||||
}
|
||||
|
||||
// Wipe a channel's messages
|
||||
async fn wipe_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let id = channel.id().to_string();
|
||||
|
||||
// Delete invites and unreads.
|
||||
self.delete_associated_channel_objects(Bson::String(id.to_string()))
|
||||
.await?;
|
||||
|
||||
// Delete messages.
|
||||
self.delete_bulk_messages(doc! {
|
||||
"channel": &id
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MongoDb {
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::hash_map::Entry;
|
||||
|
||||
use super::AbstractChannels;
|
||||
use crate::ReferenceDb;
|
||||
use crate::util::ChunkedDatabaseGenerator;
|
||||
use crate::{Channel, FieldsChannel, PartialChannel};
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
@@ -51,6 +52,23 @@ impl AbstractChannels for ReferenceDb {
|
||||
.collect())
|
||||
}
|
||||
|
||||
// Fetch all group dms for a user
|
||||
async fn find_group_message_channels(&self, user_id: &str) -> Result<ChunkedDatabaseGenerator<Channel>> {
|
||||
let channels = self.channels.lock().await;
|
||||
let groups = channels
|
||||
.values()
|
||||
.filter(|channel| match channel {
|
||||
Channel::Group { recipients, .. } => {
|
||||
recipients.iter().any(|recipient| recipient == user_id)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Ok(ChunkedDatabaseGenerator::new_reference(groups))
|
||||
}
|
||||
|
||||
// Fetch saved messages channel
|
||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
||||
let channels = self.channels.lock().await;
|
||||
@@ -131,9 +149,9 @@ impl AbstractChannels for ReferenceDb {
|
||||
// Remove a user from a group
|
||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
if let Some(channel_data) = channels.get_mut(channel) {
|
||||
if channel_data.users()?.contains(&String::from(user)) {
|
||||
channel_data.users()?.retain(|x| x != user);
|
||||
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(channel) {
|
||||
if let Some(index) = recipients.iter().position(|recipient| recipient == user) {
|
||||
recipients.remove(index);
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(create_error!(NotFound));
|
||||
@@ -142,6 +160,19 @@ impl AbstractChannels for ReferenceDb {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
|
||||
// Remove a user from all specified groups
|
||||
async fn remove_user_from_groups(&self, channel_ids: Vec<String>, user_id: &str) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
|
||||
for channel_id in channel_ids {
|
||||
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(&channel_id) {
|
||||
recipients.retain(|recipient| recipient != user_id);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Delete a channel
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut channels = self.channels.lock().await;
|
||||
@@ -151,4 +182,10 @@ impl AbstractChannels for ReferenceDb {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn wipe_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
messages.retain(|_, message| message.channel != channel.id());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashSet;
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use revolt_models::v0;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
@@ -11,7 +12,7 @@ use crate::Database;
|
||||
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
include_str!("unicode_emoji.txt")
|
||||
.split('\n')
|
||||
.map(|x| x.into())
|
||||
.map(|x| x.replace('\u{FE0F}', ""))
|
||||
.collect()
|
||||
});
|
||||
|
||||
@@ -41,6 +42,12 @@ auto_derived!(
|
||||
Server { id: String },
|
||||
Detached,
|
||||
}
|
||||
|
||||
/// Partial representation of an emoji
|
||||
pub struct PartialEmoji {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
@@ -75,13 +82,34 @@ impl Emoji {
|
||||
db.detach_emoji(&self).await
|
||||
}
|
||||
|
||||
/// Update an emoji
|
||||
pub async fn update(&mut self, db: &Database, partial: PartialEmoji) -> Result<()> {
|
||||
if let Some(name) = partial.name.clone() {
|
||||
self.name = name;
|
||||
}
|
||||
|
||||
db.update_emoji(&self.id, &partial).await?;
|
||||
|
||||
EventV1::EmojiUpdate {
|
||||
id: self.id.clone(),
|
||||
data: v0::PartialEmoji {
|
||||
name: partial.name.clone(),
|
||||
},
|
||||
}
|
||||
.p(self.parent().to_string())
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether we can use a given emoji
|
||||
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
||||
if Ulid::from_str(emoji).is_ok() {
|
||||
db.fetch_emoji(emoji).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
|
||||
let sanitized_emoji = emoji.replace('\u{FE0F}', "");
|
||||
Ok(PERMISSIBLE_EMOJIS.contains(&sanitized_emoji))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::{Emoji, PartialEmoji};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
mod mongodb;
|
||||
@@ -20,6 +20,9 @@ pub trait AbstractEmojis: Sync + Send {
|
||||
/// Fetch emoji by their parent ids
|
||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
|
||||
|
||||
/// Update emoji with new information
|
||||
async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()>;
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bson::Document;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::{Emoji, PartialEmoji};
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractEmojis;
|
||||
@@ -46,6 +46,11 @@ impl AbstractEmojis for MongoDb {
|
||||
)
|
||||
}
|
||||
|
||||
/// Update emoji with new information
|
||||
async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, emoji_id, partial, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Emoji;
|
||||
use crate::{Emoji, PartialEmoji};
|
||||
use crate::EmojiParent;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
@@ -54,6 +54,19 @@ impl AbstractEmojis for ReferenceDb {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Update emoji with new information
|
||||
async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()> {
|
||||
let mut emojis = self.emojis.lock().await;
|
||||
if let Some(emoji) = emojis.get_mut(emoji_id) {
|
||||
if let Some(name) = partial.name.clone() {
|
||||
emoji.name = name;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Detach an emoji by its id
|
||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
||||
let mut emojis = self.emojis.lock().await;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user