mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-06 03:06:04 +00:00
Compare commits
198 Commits
feat/error
...
v0.13.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
279f5d5fd7 | ||
|
|
e93769786c | ||
|
|
ed4fd5ebfe | ||
|
|
89171e9bd0 | ||
|
|
057f2bb8b3 | ||
|
|
3675ff1a1f | ||
|
|
144e939c6b | ||
|
|
61fd13629f | ||
|
|
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 | ||
|
|
24d0d2b726 | ||
|
|
f777e2863c | ||
|
|
a80ad1cbe5 | ||
|
|
0b522ebddc | ||
|
|
6bd045eb79 | ||
|
|
0698e115e8 | ||
|
|
8e3765df4c | ||
|
|
1c98ead695 | ||
|
|
8849d03cd2 | ||
|
|
a75e4eabfc | ||
|
|
74f0c537e6 | ||
|
|
1f8ea963ad | ||
|
|
7c7f4f24de | ||
|
|
a11986ba1a | ||
|
|
2e87696c83 | ||
|
|
94cb916231 | ||
|
|
95c7dbc793 | ||
|
|
ba798490e2 | ||
|
|
5132270f2e | ||
|
|
4a0957b5ec | ||
|
|
1bfb2a7214 | ||
|
|
84483cee7a | ||
|
|
4bfd79c42c | ||
|
|
413aa04dca | ||
|
|
5d3e65e076 | ||
|
|
98da8a28a0 | ||
|
|
84b57dee57 | ||
|
|
1fef495d5e | ||
|
|
310d82819d | ||
|
|
5611dab86f | ||
|
|
01ded209c6 | ||
|
|
a1a21252d0 | ||
|
|
0dc5442498 | ||
|
|
9fb55f74dc | ||
|
|
24cfc33907 | ||
|
|
ce2a4eccbd | ||
|
|
ff68679bad | ||
|
|
9bdb7d7feb | ||
|
|
3c51fad74d | ||
|
|
4344419cdb | ||
|
|
65efea020d | ||
|
|
b9360b4d88 | ||
|
|
12c187a386 | ||
|
|
2846f09c45 | ||
|
|
50835f08c8 | ||
|
|
c413fbcb58 | ||
|
|
c674a9fd4e | ||
|
|
b2da2a8587 | ||
|
|
d0ea88f1f8 | ||
|
|
92b466a5f0 | ||
|
|
aeeafebefc | ||
|
|
bf0048a052 | ||
|
|
b4ba02218f | ||
|
|
d13609c372 | ||
|
|
58fb269450 | ||
|
|
303e52b476 | ||
|
|
868894f500 | ||
|
|
67d49b461f | ||
|
|
f89ee5a370 | ||
|
|
2afea56e56 | ||
|
|
a5387b64af | ||
|
|
0b178fc791 | ||
|
|
5d27a91e90 | ||
|
|
e2016a2ab4 | ||
|
|
924c7f5b18 | ||
|
|
22e46194ab | ||
|
|
ffdb2b6900 | ||
|
|
a7b870c397 | ||
|
|
b48eb28fc2 | ||
|
|
dc9c82aa4e | ||
|
|
24fedf8c4d | ||
|
|
dda20f44ae | ||
|
|
0b7c132ace | ||
|
|
e006cdd051 | ||
|
|
ac2971a78d | ||
|
|
b7c2ca84ab | ||
|
|
82b492e529 | ||
|
|
d567155f12 | ||
|
|
27ea7345ea | ||
|
|
5b534242c4 | ||
|
|
e36fc9738b | ||
|
|
657a3f08e5 | ||
|
|
af78ac0586 | ||
|
|
d65c1a1ab3 | ||
|
|
4fb99e3bd0 | ||
|
|
f0a83abcfa | ||
|
|
6f1c715b8c | ||
|
|
38dd4d1079 | ||
|
|
154204742d | ||
|
|
db55998546 | ||
|
|
5885e067a6 | ||
|
|
a92152d86d | ||
|
|
b5cd5e30ef | ||
|
|
b0c977b324 | ||
|
|
bfe4018e43 | ||
|
|
cc7a7962a8 | ||
|
|
fb4011084d | ||
|
|
3a3415915f | ||
|
|
964884a5de | ||
|
|
db57706794 | ||
|
|
a186732ca5 | ||
|
|
e635bc23ec | ||
|
|
3cb7da95e3 | ||
|
|
dea0f675dd | ||
|
|
480f210ce8 | ||
|
|
caa8607468 | ||
|
|
32e6600272 | ||
|
|
5335124306 | ||
|
|
1561481eb4 | ||
|
|
8f9015a6ff |
147
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
147
.github/ISSUE_TEMPLATE/bug.yml
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
name: Bug Report
|
||||
description: Report a bug in Stoat — please fill out every required field to help us triage quickly.
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
- unconfirmed
|
||||
|
||||
body:
|
||||
# ── Thank you section ────────────────────────────────────────────────────────
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for taking the time to report a bug for Stoat!**
|
||||
|
||||
Please follow the instructions below and provide as much detail as possible to help us understand and reproduce the issue.
|
||||
|
||||
# ── Pre-flight checks ────────────────────────────────────────────────────────
|
||||
- type: checkboxes
|
||||
id: preflight
|
||||
attributes:
|
||||
label: Pre-flight checklist
|
||||
description: Please confirm all of the following before submitting.
|
||||
options:
|
||||
- label: I am on the latest available version of Stoat.
|
||||
required: true
|
||||
- label: I searched existing issues and did not find a duplicate.
|
||||
required: true
|
||||
- label: This is a bug report, not a support question or feature request.
|
||||
required: true
|
||||
|
||||
# ── Bug description ──────────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: >
|
||||
What happened? What did you expect to happen instead?
|
||||
Keep this brief — detailed steps go in the next field.
|
||||
placeholder: "Example: When I open a DM thread, new messages from the other person do not appear unless I reload the page."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Steps to reproduce ───────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: >
|
||||
Provide a numbered list of every step needed to trigger the bug.
|
||||
The clearer this is, the faster we can fix it.
|
||||
placeholder: |
|
||||
1. Log in to Stoat on the web client.
|
||||
2. Open a direct message conversation.
|
||||
3. Ask the other person to send a message.
|
||||
4. Observe that the message does not appear without reloading.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Expected behavior ────────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What should have happened?
|
||||
placeholder: "New messages should appear in real time without a page reload."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Client / platform ────────────────────────────────────────────────────────
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Client / platform
|
||||
description: Which Stoat client are you using?
|
||||
options:
|
||||
- Web (browser)
|
||||
- Stoat for Desktop
|
||||
- Android
|
||||
- iOS
|
||||
- Self-hosted server
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Client version ───────────────────────────────────────────────────────────
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Client version
|
||||
description: >
|
||||
Find this in **Settings**. For the web client, include the build
|
||||
number shown in the footer or About page.
|
||||
placeholder: "e.g. Stoat for Desktop 1.3.0 or 0.2.1 (2025-10-10)"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── OS / browser details ─────────────────────────────────────────────────────
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: OS / browser details
|
||||
description: Your operating system and, for web bugs, your browser and its version.
|
||||
placeholder: "e.g. Windows 11, Chrome 121 or macOS 14.3, Stoat for Desktop 1.3.0 or Android 14"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Reproducibility ──────────────────────────────────────────────────────────
|
||||
- type: dropdown
|
||||
id: reproducibility
|
||||
attributes:
|
||||
label: Reproducibility
|
||||
description: How consistently does this bug occur?
|
||||
options:
|
||||
- Always
|
||||
- Usually
|
||||
- Rarely
|
||||
- Only once
|
||||
- Unknown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ── Screenshots or video ─────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots or video
|
||||
description: Paste images or drag-and-drop a screen recording here. GitHub accepts common image and video formats.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
# ── Additional context ───────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: >
|
||||
Anything else that might help: workarounds you found, links to related issues,
|
||||
self-hosted configuration details (Redis, MongoDB, LiveKit, SMTP), network environment, etc.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
# ── Closing note ─────────────────────────────────────────────────────────────
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
---
|
||||
A maintainer will review your report. Please watch for follow-up questions — issues
|
||||
that go unanswered are harder to resolve and may be closed after 30 days of inactivity.
|
||||
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
|
||||
|
||||
...
|
||||
37
.github/workflows/book.yml
vendored
37
.github/workflows/book.yml
vendored
@@ -1,37 +0,0 @@
|
||||
name: Build documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # To push a branch
|
||||
pages: write # To push to a GitHub Pages site
|
||||
id-token: write # To update the deployment status
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install latest mdbook
|
||||
run: |
|
||||
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
|
||||
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
|
||||
mkdir mdbook
|
||||
curl -sSL $url | tar -xz --directory=./mdbook
|
||||
echo `pwd`/mdbook >> $GITHUB_PATH
|
||||
- name: Build Book
|
||||
run: |
|
||||
cd doc
|
||||
mdbook build
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "doc/book"
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
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
|
||||
119
.github/workflows/docker.yaml
vendored
119
.github/workflows/docker.yaml
vendored
@@ -5,20 +5,21 @@ on:
|
||||
tags:
|
||||
- "*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
base:
|
||||
name: Test base image build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
name: Test base image build (fork)
|
||||
runs-on: arc-runner-set
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
@@ -38,8 +39,8 @@ jobs:
|
||||
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
|
||||
|
||||
publish:
|
||||
runs-on: self-hosted
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: arc-runner-set
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
|
||||
name: Publish Docker images
|
||||
steps:
|
||||
# Configure build environment
|
||||
@@ -49,13 +50,6 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# Authenticate with Docker Hub and GHCR
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -63,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
|
||||
@@ -70,16 +73,17 @@ 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
|
||||
|
||||
# revoltchat/server
|
||||
# stoatchat/api
|
||||
- name: Docker meta
|
||||
id: meta-delta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/server
|
||||
ghcr.io/revoltchat/server
|
||||
ghcr.io/stoatchat/api
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -89,17 +93,16 @@ 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 }}
|
||||
|
||||
# revoltchat/bonfire
|
||||
# stoatchat/events
|
||||
- name: Docker meta
|
||||
id: meta-bonfire
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/bonfire
|
||||
ghcr.io/revoltchat/bonfire
|
||||
ghcr.io/stoatchat/events
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -109,17 +112,16 @@ 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 }}
|
||||
|
||||
# revoltchat/autumn
|
||||
# stoatchat/file-server
|
||||
- name: Docker meta
|
||||
id: meta-autumn
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/autumn
|
||||
ghcr.io/revoltchat/autumn
|
||||
ghcr.io/stoatchat/file-server
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -129,17 +131,16 @@ 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 }}
|
||||
|
||||
# revoltchat/january
|
||||
# stoatchat/proxy
|
||||
- name: Docker meta
|
||||
id: meta-january
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/january
|
||||
ghcr.io/revoltchat/january
|
||||
ghcr.io/stoatchat/proxy
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -149,17 +150,35 @@ 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 }}
|
||||
|
||||
# revoltchat/crond
|
||||
# stoatchat/gifbox
|
||||
- name: Docker meta
|
||||
id: meta-gifbox
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/gifbox
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/services/gifbox/Dockerfile
|
||||
tags: ${{ steps.meta-gifbox.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-gifbox.outputs.labels }}
|
||||
|
||||
# stoatchat/crond
|
||||
- name: Docker meta
|
||||
id: meta-crond
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/crond
|
||||
ghcr.io/revoltchat/crond
|
||||
ghcr.io/stoatchat/crond
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -169,17 +188,16 @@ 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 }}
|
||||
|
||||
# revoltchat/pushd
|
||||
# stoatchat/pushd
|
||||
- name: Docker meta
|
||||
id: meta-pushd
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
docker.io/revoltchat/pushd
|
||||
ghcr.io/revoltchat/pushd
|
||||
ghcr.io/stoatchat/pushd
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -189,5 +207,24 @@ 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
|
||||
- name: Docker meta
|
||||
id: meta-voice-ingress
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stoatchat/voice-ingress
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: crates/daemons/voice-ingress/Dockerfile
|
||||
tags: ${{ steps.meta-voice-ingress.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
|
||||
labels: ${{ steps.meta-voice-ingress.outputs.labels }}
|
||||
|
||||
23
.github/workflows/docs-test.yml
vendored
Normal file
23
.github/workflows/docs-test.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Documentation (test)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-deploy:
|
||||
name: Test deployment
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./docs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: mise docs:build
|
||||
48
.github/workflows/docs.yml
vendored
Normal file
48
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docusaurus
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./docs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- run: mise docs:build
|
||||
|
||||
- name: Upload Build Artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./docs/build
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
needs: build
|
||||
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
23
.github/workflows/git-town.yml
vendored
Normal file
23
.github/workflows/git-town.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: Git Town
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
git-town:
|
||||
name: Display the branch stack
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !startsWith(github.head_ref, 'release-please--') }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: stoatchat/action-git-town@4bc5c942e4603bffa0806b51d5fe5f0bc5deb0ac
|
||||
continue-on-error: true
|
||||
26
.github/workflows/publish-crates.yml
vendored
Normal file
26
.github/workflows/publish-crates.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Publish Crates
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish Crates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: mise publish --workspace
|
||||
63
.github/workflows/release-please.yml
vendored
Normal file
63
.github/workflows/release-please.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Release Please
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main] # updates/opens the release PR when commits land on main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: release-please
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
name: Release Please
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_created: ${{ steps.rp.outputs.release_created }}
|
||||
tag_name: ${{ steps.rp.outputs.tag_name }}
|
||||
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 }}
|
||||
- id: rp
|
||||
uses: googleapis/release-please-action@v4
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
config-file: release-please-config.json
|
||||
|
||||
- name: Install latest stable
|
||||
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
- name: Update Cargo.lock
|
||||
if: ${{ steps.rp.outputs.prs_created == 'true' || steps.rp.outputs.prs_updated == 'true' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
RP_PR_OUTPUT_JSON: ${{ steps.rp.outputs.prs }}
|
||||
run: |
|
||||
PR_NUMBER=$(echo "$RP_PR_OUTPUT_JSON" | jq -r '.[0].number')
|
||||
gh pr checkout "$PR_NUMBER"
|
||||
|
||||
cargo update -w
|
||||
|
||||
if git diff --quiet Cargo.lock; then
|
||||
echo "No changes to Cargo.lock"
|
||||
else
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add Cargo.lock
|
||||
git commit -s -m "chore: update Cargo.lock"
|
||||
git push
|
||||
fi
|
||||
26
.github/workflows/release-webhook.yml
vendored
Normal file
26
.github/workflows/release-webhook.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: Release Webhook
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
release-webhook:
|
||||
name: Send Release Webhook
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Send release notification webhook
|
||||
env:
|
||||
TAG_NAME: ${{ github.event.release.tag_name }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
WEBHOOK_URL: ${{ secrets.STOAT_WEBHOOK_UPDATES_URL }}
|
||||
run: |
|
||||
RELEASE_URL="https://github.com/${REPOSITORY}/releases/tag/${TAG_NAME}"
|
||||
curl -X POST "$WEBHOOK_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"content\": \"$RELEASE_URL\"}"
|
||||
75
.github/workflows/rust.yaml
vendored
75
.github/workflows/rust.yaml
vendored
@@ -2,16 +2,12 @@ name: Rust build, test, and generate specification
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/rust.yaml"
|
||||
- ".vscode/**"
|
||||
- "doc/**"
|
||||
- ".gitignore"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -19,63 +15,60 @@ env:
|
||||
jobs:
|
||||
check:
|
||||
name: Rust project
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: arc-runner-set
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install latest stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- name: Install cargo-nextest
|
||||
uses: baptiste0928/cargo-install@v1
|
||||
persist-credentials: false
|
||||
|
||||
# Using our own runners for now:
|
||||
# - name: Free up disk space
|
||||
# run: |
|
||||
# sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
|
||||
with:
|
||||
crate: cargo-nextest
|
||||
locked: true
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
||||
- name: Run services in background
|
||||
run: |
|
||||
docker compose -f compose.yml up -d
|
||||
|
||||
- name: Run cargo test
|
||||
- run: mise build
|
||||
- run: mise docker:start
|
||||
|
||||
- name: Reference Test
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
continue-on-error: ${{ github.ref_name == 'main' }}
|
||||
run: |
|
||||
cargo nextest run
|
||||
mise test
|
||||
|
||||
- name: Run cargo test (with MongoDB)
|
||||
- name: MongoDB Test
|
||||
env:
|
||||
TEST_DB: MONGODB
|
||||
MONGODB: mongodb://localhost
|
||||
continue-on-error: ${{ github.ref_name == 'main' }}
|
||||
run: |
|
||||
cargo nextest run
|
||||
mise test
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
env:
|
||||
TEST_DB: REFERENCE
|
||||
run: |
|
||||
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
|
||||
mise build --bin revolt-delta && (mise service:api &)
|
||||
|
||||
- name: Wait for API to go up
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: nev7n/wait_for_response@v1
|
||||
uses: nev7n/wait_for_response@7fef3c1a6e8939d0b09062f14fec50d3c5d15fa1 # v1.0.1
|
||||
with:
|
||||
url: "http://localhost:14702/"
|
||||
|
||||
- name: Checkout API repository
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: revoltchat/api
|
||||
repository: stoatchat/javascript-client-api
|
||||
path: api
|
||||
token: ${{ secrets.PAT }}
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY_JAVASCRIPT_CLIENT_API }}
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
@@ -83,10 +76,10 @@ jobs:
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'main'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
||||
with:
|
||||
cwd: "api"
|
||||
add: "*.json"
|
||||
author_name: Revolt CI
|
||||
author_email: revolt-ci@users.noreply.github.com
|
||||
author_name: Stoat CI
|
||||
author_email: stoat-ci@users.noreply.github.com
|
||||
message: "chore: generate OpenAPI specification"
|
||||
|
||||
54
.github/workflows/triage_issue.yml
vendored
54
.github/workflows/triage_issue.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: Add Issue to Board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add issue to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
79
.github/workflows/triage_pr.yml
vendored
79
.github/workflows/triage_pr.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectV2(number: 5) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
mutation (
|
||||
$project: ID!
|
||||
$item: ID!
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: {
|
||||
singleSelectOptionId: $status_value
|
||||
}
|
||||
}) {
|
||||
projectV2Item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
23
.github/workflows/validate-pr-title.yml
vendored
Normal file
23
.github/workflows/validate-pr-title.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# DO NOT EDIT DIRECTLY IN REPOSITORY
|
||||
# Managed in Terraform templates
|
||||
|
||||
name: "Lint PR"
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
main:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,4 +11,5 @@ venv/
|
||||
.vercel
|
||||
.DS_Store
|
||||
|
||||
livekit.yml
|
||||
.idea
|
||||
28
.mise/config.toml
Normal file
28
.mise/config.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[tools]
|
||||
node = "25.4.0"
|
||||
pnpm = "10.28.1"
|
||||
|
||||
gh = "2.25.0"
|
||||
|
||||
rust = "1.92.0"
|
||||
"cargo:cargo-nextest" = "0.9.122"
|
||||
|
||||
"github:git-town/git-town" = "22.4.0"
|
||||
|
||||
[settings]
|
||||
experimental = true
|
||||
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 }
|
||||
5
.mise/tasks/build
Executable file
5
.mise/tasks/build
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Build project"
|
||||
set -e
|
||||
|
||||
${BUILDER} build "$@"
|
||||
5
.mise/tasks/check
Executable file
5
.mise/tasks/check
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Check project with clippy"
|
||||
set -e
|
||||
|
||||
cargo clippy
|
||||
13
.mise/tasks/docker/start
Executable file
13
.mise/tasks/docker/start
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Start Docker containers"
|
||||
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}
|
||||
5
.mise/tasks/docker/stop
Executable file
5
.mise/tasks/docker/stop
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Stop Docker containers"
|
||||
set -e
|
||||
|
||||
docker compose down
|
||||
7
.mise/tasks/docs/_default
Executable file
7
.mise/tasks/docs/_default
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Start the Stoat Developers website"
|
||||
#MISE depends=["docs:install"]
|
||||
#MISE dir="{{config_root}}/docs"
|
||||
set -e
|
||||
|
||||
pnpm build
|
||||
7
.mise/tasks/docs/build
Executable file
7
.mise/tasks/docs/build
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Build the Stoat Developers website"
|
||||
#MISE depends=["docs:install"]
|
||||
#MISE dir="{{config_root}}/docs"
|
||||
set -e
|
||||
|
||||
pnpm build
|
||||
6
.mise/tasks/docs/install
Executable file
6
.mise/tasks/docs/install
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Install dependencies for docs site"
|
||||
#MISE dir="{{config_root}}/docs"
|
||||
set -e
|
||||
|
||||
pnpm i --frozen-lockfile
|
||||
5
.mise/tasks/publish
Executable file
5
.mise/tasks/publish
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Publish project"
|
||||
set -e
|
||||
|
||||
cargo publish "$@"
|
||||
5
.mise/tasks/service/api
Executable file
5
.mise/tasks/service/api
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run API server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-delta
|
||||
5
.mise/tasks/service/crond
Executable file
5
.mise/tasks/service/crond
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run cron daemon"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-crond
|
||||
5
.mise/tasks/service/events
Executable file
5
.mise/tasks/service/events
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run events server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-bonfire
|
||||
5
.mise/tasks/service/files
Executable file
5
.mise/tasks/service/files
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run file server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-autumn
|
||||
5
.mise/tasks/service/gifbox
Executable file
5
.mise/tasks/service/gifbox
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run GIF proxy server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-gifbox
|
||||
5
.mise/tasks/service/proxy
Executable file
5
.mise/tasks/service/proxy
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run proxy server"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-january
|
||||
5
.mise/tasks/service/pushd
Executable file
5
.mise/tasks/service/pushd
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Run push daemon"
|
||||
set -e
|
||||
|
||||
cargo run --bin revolt-pushd
|
||||
8
.mise/tasks/test
Executable file
8
.mise/tasks/test
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
#MISE description="Test project"
|
||||
set -e
|
||||
|
||||
: "${TEST_DB:=REFERENCE}"
|
||||
export TEST_DB
|
||||
|
||||
cargo nextest run
|
||||
3
.release-please-manifest.json
Normal file
3
.release-please-manifest.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "0.13.7"
|
||||
}
|
||||
288
CHANGELOG.md
Normal file
288
CHANGELOG.md
Normal file
@@ -0,0 +1,288 @@
|
||||
# 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)
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* disable user update events ([#593](https://github.com/stoatchat/stoatchat/issues/593)) ([1c98ead](https://github.com/stoatchat/stoatchat/commit/1c98ead69579b4700be0b51c9020bb8402336cc6))
|
||||
|
||||
## [0.11.4](https://github.com/stoatchat/stoatchat/compare/v0.11.3...v0.11.4) (2026-02-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add separate config option for redis events replica url ([#590](https://github.com/stoatchat/stoatchat/issues/590)) ([a75e4ea](https://github.com/stoatchat/stoatchat/commit/a75e4eabfc4b34aba7620c82ba77558a32d9e10a))
|
||||
|
||||
## [0.11.3](https://github.com/stoatchat/stoatchat/compare/v0.11.2...v0.11.3) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cut presence traffic too while we engineer a new events architecture ([#561](https://github.com/stoatchat/stoatchat/issues/561)) ([1f8ea96](https://github.com/stoatchat/stoatchat/commit/1f8ea963ad742f693f405e6438f1c343c81e6579))
|
||||
|
||||
## [0.11.2](https://github.com/stoatchat/stoatchat/compare/v0.11.1...v0.11.2) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cut events traffic while we engineer a new events architecture ([#559](https://github.com/stoatchat/stoatchat/issues/559)) ([a11986b](https://github.com/stoatchat/stoatchat/commit/a11986ba1ad16b672ff1080913a684567d88adbb))
|
||||
|
||||
## [0.11.1](https://github.com/stoatchat/stoatchat/compare/v0.11.0...v0.11.1) (2026-02-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bots in multiple voice channel logic ([#544](https://github.com/stoatchat/stoatchat/issues/544)) ([94cb916](https://github.com/stoatchat/stoatchat/commit/94cb916231b9b8befb2e94065917ff40815bec52))
|
||||
|
||||
## [0.11.0](https://github.com/stoatchat/stoatchat/compare/v0.10.3...v0.11.0) (2026-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* appeal to the almighty Spamhaus ([#524](https://github.com/stoatchat/stoatchat/issues/524)) ([5132270](https://github.com/stoatchat/stoatchat/commit/5132270f2edd6df25ce414daa42ed1b2aa6fa7a9))
|
||||
|
||||
## [0.10.3](https://github.com/stoatchat/stoatchat/compare/v0.10.2...v0.10.3) (2026-02-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* update `Revolt` -> `Stoat` in email titles/desc. ([#508](https://github.com/stoatchat/stoatchat/issues/508)) ([84483ce](https://github.com/stoatchat/stoatchat/commit/84483cee7af3e5dfa16f7fe13e334c4d9f5abd60))
|
||||
|
||||
## [0.10.2](https://github.com/stoatchat/stoatchat/compare/v0.10.1...v0.10.2) (2026-01-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* thumbnailification requires rgb8/rgba8 ([#505](https://github.com/stoatchat/stoatchat/issues/505)) ([413aa04](https://github.com/stoatchat/stoatchat/commit/413aa04dcaf8bff3935ed1e5f31432e11a03ce6f))
|
||||
|
||||
## [0.10.1](https://github.com/stoatchat/stoatchat/compare/v0.10.0...v0.10.1) (2026-01-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use Rust 1.92.0 for Docker build ([#503](https://github.com/stoatchat/stoatchat/issues/503)) ([98da8a2](https://github.com/stoatchat/stoatchat/commit/98da8a28a0aa2fee4e8eee1d86bd7c49e3187477))
|
||||
|
||||
## [0.10.0](https://github.com/stoatchat/stoatchat/compare/v0.9.4...v0.10.0) (2026-01-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow kicking members from voice channels ([#495](https://github.com/stoatchat/stoatchat/issues/495)) ([0dc5442](https://github.com/stoatchat/stoatchat/commit/0dc544249825a49c793309edee5ec1838458a6da))
|
||||
* repository architecture for files crate w. added tests ([#498](https://github.com/stoatchat/stoatchat/issues/498)) ([01ded20](https://github.com/stoatchat/stoatchat/commit/01ded209c62208fc906d6aab9b08c04e860e10ef))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* expose ratelimit headers via cors ([#496](https://github.com/stoatchat/stoatchat/issues/496)) ([a1a2125](https://github.com/stoatchat/stoatchat/commit/a1a21252d0ad58937e41f16e5fb86f96bebd2a51))
|
||||
|
||||
## [0.9.4](https://github.com/stoatchat/stoatchat/compare/v0.9.3...v0.9.4) (2026-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* checkout repo. before bumping lock ([#490](https://github.com/stoatchat/stoatchat/issues/490)) ([b2da2a8](https://github.com/stoatchat/stoatchat/commit/b2da2a858787853be43136fd526a0bd72baf78ef))
|
||||
* persist credentials for git repo ([#492](https://github.com/stoatchat/stoatchat/issues/492)) ([c674a9f](https://github.com/stoatchat/stoatchat/commit/c674a9fd4e0abbd51569870e4b38074d4a1de03c))
|
||||
|
||||
## [0.9.3](https://github.com/stoatchat/stoatchat/compare/v0.9.2...v0.9.3) (2026-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* pipeline fixes ([#487](https://github.com/stoatchat/stoatchat/issues/487)) ([aeeafeb](https://github.com/stoatchat/stoatchat/commit/aeeafebefc36a43a656cf797c9251ca50292733c))
|
||||
|
||||
## [0.9.2](https://github.com/stoatchat/stoatchat/compare/v0.9.1...v0.9.2) (2026-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* disable publish for services ([#485](https://github.com/stoatchat/stoatchat/issues/485)) ([d13609c](https://github.com/stoatchat/stoatchat/commit/d13609c37279d6a40445dcd99564e5c3dd03bac1))
|
||||
|
||||
## [0.9.1](https://github.com/stoatchat/stoatchat/compare/v0.9.0...v0.9.1) (2026-01-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ci:** pipeline fixes (marked as fix to force release) ([#483](https://github.com/stoatchat/stoatchat/issues/483)) ([303e52b](https://github.com/stoatchat/stoatchat/commit/303e52b476585eea81c33837f1b01506ce387684))
|
||||
|
||||
## [0.9.0](https://github.com/stoatchat/stoatchat/compare/v0.8.8...v0.9.0) (2026-01-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add id field to role ([#470](https://github.com/stoatchat/stoatchat/issues/470)) ([2afea56](https://github.com/stoatchat/stoatchat/commit/2afea56e56017f02de98e67316b4457568ad5b26))
|
||||
* add ratelimits to gifbox ([1542047](https://github.com/stoatchat/stoatchat/commit/154204742d21cbeff6e2577b00f50b495ea44631))
|
||||
* include groups and dms in fetch mutuals ([caa8607](https://github.com/stoatchat/stoatchat/commit/caa86074680d46223cebc20f41e9c91c41ec825d))
|
||||
* include member payload in ServerMemberJoin event ([480f210](https://github.com/stoatchat/stoatchat/commit/480f210ce85271e13d1dac58a5dae08de108579d))
|
||||
* initial work on tenor gif searching ([b0c977b](https://github.com/stoatchat/stoatchat/commit/b0c977b324b8144c1152589546eb8fec5954c3e7))
|
||||
* make message lexer use unowned string ([1561481](https://github.com/stoatchat/stoatchat/commit/1561481eb4cdc0f385fbf0a81e4950408050e11f))
|
||||
* ready payload field customisation ([db57706](https://github.com/stoatchat/stoatchat/commit/db577067948f13e830b5fb773034e9713a1abaff))
|
||||
* require auth for search ([b5cd5e3](https://github.com/stoatchat/stoatchat/commit/b5cd5e30ef7d5e56e8964fb7c543965fa6bf5a4a))
|
||||
* trending and categories routes ([5885e06](https://github.com/stoatchat/stoatchat/commit/5885e067a627b8fff1c8ce2bf9e852ff8cf3f07a))
|
||||
* voice chats v2 ([#414](https://github.com/stoatchat/stoatchat/issues/414)) ([d567155](https://github.com/stoatchat/stoatchat/commit/d567155f124e4da74115b1a8f810062f7c6559d9))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add license to revolt-parser ([5335124](https://github.com/stoatchat/stoatchat/commit/53351243064cac8d499dd74284be73928fa78a43))
|
||||
* allow for disabling default features ([65fbd36](https://github.com/stoatchat/stoatchat/commit/65fbd3662462aed1333b79e59155fa6377e83fcc))
|
||||
* apple music to use original url instead of metadata url ([bfe4018](https://github.com/stoatchat/stoatchat/commit/bfe4018e436a4075bae780dd4d35a9b58315e12f))
|
||||
* apply uname fix to january and autumn ([8f9015a](https://github.com/stoatchat/stoatchat/commit/8f9015a6ff181d208d9269ab8691bd417d39811a))
|
||||
* **ci:** publish images under stoatchat and remove docker hub ([d65c1a1](https://github.com/stoatchat/stoatchat/commit/d65c1a1ab3bdc7e5684b03f280af77d881661a3d))
|
||||
* correct miniz_oxide in lockfile ([#478](https://github.com/stoatchat/stoatchat/issues/478)) ([5d27a91](https://github.com/stoatchat/stoatchat/commit/5d27a91e901dd2ea3e860aeaed8468db6c5f3214))
|
||||
* correct shebang for try-tag-and-release ([050ba16](https://github.com/stoatchat/stoatchat/commit/050ba16d4adad5d0fb247867aa3e94e3d42bd12d))
|
||||
* correct string_cache in lockfile ([#479](https://github.com/stoatchat/stoatchat/issues/479)) ([0b178fc](https://github.com/stoatchat/stoatchat/commit/0b178fc791583064bf9ca94b1d39b42d021e1d79))
|
||||
* don't remove timeouts when a member leaves a server ([#409](https://github.com/stoatchat/stoatchat/issues/409)) ([e635bc2](https://github.com/stoatchat/stoatchat/commit/e635bc23ec857d648d5705e1a3875d7bc3402b0d))
|
||||
* don't update the same field while trying to remove it ([f4ee35f](https://github.com/stoatchat/stoatchat/commit/f4ee35fb093ca49f0a64ff4b17fd61587df28145)), closes [#392](https://github.com/stoatchat/stoatchat/issues/392)
|
||||
* github webhook incorrect payload and formatting ([#468](https://github.com/stoatchat/stoatchat/issues/468)) ([dc9c82a](https://github.com/stoatchat/stoatchat/commit/dc9c82aa4e9667ea6639256c65ac8de37a24d1f7))
|
||||
* implement Serialize to ClientMessage ([dea0f67](https://github.com/stoatchat/stoatchat/commit/dea0f675dde7a63c7a59b38d469f878b7a8a3af4))
|
||||
* newly created roles should be ranked the lowest ([947eb15](https://github.com/stoatchat/stoatchat/commit/947eb15771ed6785b3dcd16c354c03ded5e4cbe0))
|
||||
* permit empty `remove` array in edit requests ([6ad3da5](https://github.com/stoatchat/stoatchat/commit/6ad3da5f35f989a2e7d8e29718b98374248e76af))
|
||||
* preserve order of replies in message ([#447](https://github.com/stoatchat/stoatchat/issues/447)) ([657a3f0](https://github.com/stoatchat/stoatchat/commit/657a3f08e5d652814bbf0647e089ed9ebb139bbf))
|
||||
* prevent timing out members which have TimeoutMembers permission ([e36fc97](https://github.com/stoatchat/stoatchat/commit/e36fc9738bac0de4f3fcbccba521f1e3754f7ae7))
|
||||
* relax settings name regex ([3a34159](https://github.com/stoatchat/stoatchat/commit/3a3415915f0d0fdce1499d47a2b7fa097f5946ea))
|
||||
* remove authentication tag bytes from attachment download ([32e6600](https://github.com/stoatchat/stoatchat/commit/32e6600272b885c595c094f0bc69459250220dcb))
|
||||
* rename openapi operation ids ([6048587](https://github.com/stoatchat/stoatchat/commit/6048587d348fbca0dc3a9b47690c56df8fece576)), closes [#406](https://github.com/stoatchat/stoatchat/issues/406)
|
||||
* respond with 201 if no body in requests ([#465](https://github.com/stoatchat/stoatchat/issues/465)) ([24fedf8](https://github.com/stoatchat/stoatchat/commit/24fedf8c4d9cd3160bdec97aa451520f8beaa739))
|
||||
* swap to using reqwest for query building ([38dd4d1](https://github.com/stoatchat/stoatchat/commit/38dd4d10797b3e6e397fc219e818f379bdff19f2))
|
||||
* use `trust_cloudflare` config value instead of env var ([cc7a796](https://github.com/stoatchat/stoatchat/commit/cc7a7962a882e1627fcd0bc75858a017415e8cfc))
|
||||
* use our own result types instead of tenors types ([a92152d](https://github.com/stoatchat/stoatchat/commit/a92152d86da136997817e797c7af8e38731cdde8))
|
||||
5356
Cargo.lock
generated
5356
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
191
Cargo.toml
191
Cargo.toml
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
resolver = "3"
|
||||
|
||||
members = [
|
||||
"crates/delta",
|
||||
@@ -14,9 +14,190 @@ redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/rev
|
||||
#authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
|
||||
#rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
|
||||
|
||||
# I'm 99% sure this is overloading the GitHub worker
|
||||
# hence builds have been failing since, let's just
|
||||
# disable it for now. In the future, we could use this
|
||||
# if we were rolling our own CI (that is now).
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[workspace.dependencies]
|
||||
# Async
|
||||
async-trait = "0.1.89"
|
||||
tokio = "1.49.0"
|
||||
async-channel = "2.3.1"
|
||||
futures = "0.3.32"
|
||||
async-std = "1.8.0"
|
||||
async-tungstenite = "0.17.0"
|
||||
futures-locks = "0.7.1"
|
||||
async-lock = "2.8.0"
|
||||
async-recursion = "1.0.4"
|
||||
|
||||
# Error Handling
|
||||
anyhow = "1.0.100"
|
||||
thiserror = "2.0.18"
|
||||
sentry = "0.31.5"
|
||||
sentry-anyhow = "0.38.1"
|
||||
|
||||
# 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"
|
||||
|
||||
# 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 = "0.1.4"
|
||||
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 = "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 = "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"
|
||||
tracing-subscriber = { version = "0.3.22", features = [
|
||||
"env-filter",
|
||||
] } # consider https://crates.io/crates/better-tracing
|
||||
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 = "0.31.1"
|
||||
|
||||
# Authifier
|
||||
authifier = "1.0.16"
|
||||
|
||||
# RabbitMQ
|
||||
lapin = "4.7.1"
|
||||
|
||||
# Voice
|
||||
livekit-api = "0.4.4"
|
||||
livekit-protocol = "0.7.4"
|
||||
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"
|
||||
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" }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.86.0-slim-bookworm
|
||||
FROM --platform="${BUILDPLATFORM}" rust:1.92.0-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
@@ -27,10 +27,14 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
|
||||
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
|
||||
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
||||
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
|
||||
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
|
||||
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
|
||||
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM rust:1.86.0-slim-bookworm
|
||||
FROM rust:1.92.0-slim-bookworm
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
@@ -23,10 +23,14 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
|
||||
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
|
||||
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||
COPY crates/services/january/Cargo.toml ./crates/services/january/
|
||||
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
|
||||
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
|
||||
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
|
||||
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
|
||||
97
README.md
97
README.md
@@ -1,15 +1,15 @@
|
||||
<div align="center">
|
||||
<h1>
|
||||
Revolt Backend
|
||||
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 | |
|
||||
@@ -21,9 +21,11 @@ The services and libraries that power the Revolt service.<br/>
|
||||
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |     |
|
||||
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |     |
|
||||
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |     |
|
||||
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service |     |
|
||||
| `delta` | [crates/delta](crates/delta) | REST API server |  |
|
||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |  |
|
||||
| `services/january` | [crates/services/january](crates/services/january) | Proxy server |  |
|
||||
| `services/gifbox` | [crates/services/gifbox](crates/services/gifbox) | Tenor proxy server |  |
|
||||
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server |  |
|
||||
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server |  |
|
||||
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server |  |
|
||||
@@ -35,22 +37,19 @@ The services and libraries that power the Revolt service.<br/>
|
||||
|
||||
Rust 1.86.0 or higher.
|
||||
|
||||
> [!CAUTION]
|
||||
> The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341). This is currently solved by build time options but we are looking for a proper fix.
|
||||
|
||||
## 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:
|
||||
|
||||
- Rust toolchain (rustup recommended)
|
||||
- mise
|
||||
- Docker
|
||||
- Git
|
||||
- mold (optional, faster compilation)
|
||||
|
||||
> A **default.nix** is available for Nix users!
|
||||
> Just run `nix-shell` and continue.
|
||||
> Run `nix-shell` to activate mise.
|
||||
|
||||
As a heads-up, the development environment uses the following ports:
|
||||
|
||||
@@ -66,15 +65,25 @@ As a heads-up, the development environment uses the following ports:
|
||||
| `crates/bonfire` | 14703 |
|
||||
| `crates/services/autumn` | 14704 |
|
||||
| `crates/services/january` | 14705 |
|
||||
| `crates/services/gifbox` | 14706 |
|
||||
|
||||
Now you can clone and build the project:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/revoltchat/backend revolt-backend
|
||||
cd revolt-backend
|
||||
cargo build
|
||||
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,51 +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 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
|
||||
@@ -196,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:
|
||||
@@ -215,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!**
|
||||
|
||||
19
Revolt.toml
19
Revolt.toml
@@ -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"
|
||||
@@ -26,6 +26,11 @@ january = "http://local.revolt.chat:14705"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
# Public urls for livekit nodes
|
||||
# each entry here should have a corresponding entry under `api.livekit.nodes`
|
||||
[hosts.livekit]
|
||||
worldwide = "ws://local.revolt.chat:14706"
|
||||
|
||||
[api]
|
||||
|
||||
[api.smtp]
|
||||
@@ -40,6 +45,18 @@ port = 14025
|
||||
use_tls = false
|
||||
use_starttls = false
|
||||
|
||||
[api.livekit]
|
||||
|
||||
# Config for livekit nodes
|
||||
# Make sure to change the secret when deploying
|
||||
# The key and secret should match the values livekit is using
|
||||
[api.livekit.nodes.worldwide]
|
||||
url = "http://livekit"
|
||||
lat = 0.0
|
||||
lon = 0.0
|
||||
key = "worldwide"
|
||||
secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0"
|
||||
|
||||
[files.s3]
|
||||
# S3 protocol endpoint
|
||||
endpoint = "http://127.0.0.1:14009"
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# Code Style Guide
|
||||
|
||||
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
|
||||
|
||||
## Writing Style
|
||||
|
||||
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
|
||||
|
||||
## `core/database` crate
|
||||
|
||||
w.r.t. `model.rs` files
|
||||
|
||||
- All struct definitions must be commented.
|
||||
```rust
|
||||
/// Server
|
||||
pub struct Server {
|
||||
/// Name of the server
|
||||
pub name: String,
|
||||
```
|
||||
- Struct definitions should not include derives unless necessary (if additional traits such as Hash are required) and instead use `auto_derived!` and `auto_derived_partial!`.
|
||||
```rust
|
||||
auto_derived_partial!(
|
||||
/// Server
|
||||
pub struct Server { .. },
|
||||
"PartialServer"
|
||||
);
|
||||
```
|
||||
- `auto_derived!` macro accepts multiple entries and should be used as such:
|
||||
|
||||
```rust
|
||||
auto_derived!(
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsServer { .. }
|
||||
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsRole { .. }
|
||||
);
|
||||
```
|
||||
|
||||
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
|
||||
```rust
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
```
|
||||
- `impl` blocks may be defined below the struct definitions and should be ordered in the same order of definition. Methods in the block must follow the same guidelines as traits where-in: methods are ordered in terms of CRUD, there are empty line breaks, and methods are commented.
|
||||
|
||||
w.r.t. `ops` module for models
|
||||
|
||||
- All traits must use a the name format `AbstractPlural` where Plural is the plural form of the collection. e.g. Servers
|
||||
- Traits defined must follow these guidelines:
|
||||
|
||||
- Methods are ordered in terms of CRUD, create-read-update-delete ordering.
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait AbstractServerMembers: Sync + Send {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()>;
|
||||
|
||||
/// Fetch a server member by their id
|
||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
||||
|
||||
/// Update information for a server member
|
||||
async fn update_member(&self, .. ) -> Result<()>;
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
- There should be an empty line break between each method declaration.
|
||||
- All methods must have an appropriate comment.
|
||||
|
||||
- When implementing the trait defined in `ops.rs` with each driver, the method declaration style should be the same for ease of searching: same ordering, same comments, same line breaks.
|
||||
30
compose.yml
30
compose.yml
@@ -8,18 +8,33 @@ 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
|
||||
hard: 65536
|
||||
|
||||
# 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:
|
||||
@@ -40,7 +55,7 @@ services:
|
||||
|
||||
# Rabbit
|
||||
rabbit:
|
||||
image: rabbitmq:3-management
|
||||
image: rabbitmq:4-management
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: rabbituser
|
||||
RABBITMQ_DEFAULT_PASS: rabbitpass
|
||||
@@ -55,7 +70,7 @@ services:
|
||||
|
||||
# Mock SMTP server
|
||||
maildev:
|
||||
image: soulteary/maildev
|
||||
image: maildev/maildev
|
||||
ports:
|
||||
- "14025:25"
|
||||
- "14080:8080"
|
||||
@@ -64,3 +79,10 @@ services:
|
||||
MAILDEV_WEB_PORT: 8080
|
||||
MAILDEV_INCOMING_USER: smtp
|
||||
MAILDEV_INCOMING_PASS: smtp
|
||||
|
||||
livekit:
|
||||
image: ghcr.io/stoatchat/livekit-server:v1.9.13
|
||||
command: --config /etc/livekit.yml
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./livekit.yml:/etc/livekit.yml
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "0.8.8"
|
||||
version = "0.13.7"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[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 }
|
||||
lru_time_cache = { workspace = true }
|
||||
async-channel = { workspace = true }
|
||||
|
||||
# parsing
|
||||
querystring = "1.1.0"
|
||||
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 = ["async-std-runtime"] }
|
||||
async-std = { workspace = true }
|
||||
|
||||
# core
|
||||
authifier = { version = "1.0.15" }
|
||||
revolt-result = { path = "../core/result" }
|
||||
revolt-models = { path = "../core/models" }
|
||||
revolt-config = { path = "../core/config" }
|
||||
revolt-database = { path = "../core/database" }
|
||||
revolt-permissions = { version = "0.8.8", path = "../core/permissions" }
|
||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||
authifier = { workspace = true }
|
||||
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,5 +1,5 @@
|
||||
# Build Stage
|
||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||
FROM ghcr.io/stoatchat/base:latest AS builder
|
||||
FROM debian:12 AS debian
|
||||
|
||||
# Bundle Stage
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
use async_tungstenite::tungstenite::{handshake, Message};
|
||||
use futures::channel::oneshot::Sender;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use revolt_database::events::client::ReadyPayloadFields;
|
||||
use revolt_result::{create_error, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// matches either a single word ie "users" or a key and value ie "settings[notifications]"
|
||||
static READY_PAYLOAD_FIELD_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap());
|
||||
|
||||
/// Enumeration of supported protocol formats
|
||||
#[derive(Debug)]
|
||||
pub enum ProtocolFormat {
|
||||
@@ -17,6 +23,7 @@ pub struct ProtocolConfiguration {
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
ready_payload_fields: ReadyPayloadFields,
|
||||
}
|
||||
|
||||
impl ProtocolConfiguration {
|
||||
@@ -25,11 +32,13 @@ impl ProtocolConfiguration {
|
||||
protocol_version: i32,
|
||||
format: ProtocolFormat,
|
||||
session_token: Option<String>,
|
||||
ready_payload_fields: ReadyPayloadFields,
|
||||
) -> Self {
|
||||
Self {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
ready_payload_fields,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +95,8 @@ impl ProtocolConfiguration {
|
||||
}
|
||||
|
||||
/// Get ready payload fields
|
||||
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
|
||||
vec![
|
||||
ReadyPayloadFields::Users,
|
||||
ReadyPayloadFields::Servers,
|
||||
ReadyPayloadFields::Channels,
|
||||
ReadyPayloadFields::Members,
|
||||
ReadyPayloadFields::Emoji,
|
||||
]
|
||||
pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields {
|
||||
&self.ready_payload_fields
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +127,23 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
let mut protocol_version = 1;
|
||||
let mut format = ProtocolFormat::Json;
|
||||
let mut session_token = None;
|
||||
let mut ready_payload_fields = if params.iter().any(|(k, _)| *k == "ready") {
|
||||
// If they pass the ready field, set all fields to false
|
||||
|
||||
ReadyPayloadFields {
|
||||
users: false,
|
||||
servers: false,
|
||||
channels: false,
|
||||
members: false,
|
||||
emojis: false,
|
||||
voice_states: false,
|
||||
user_settings: Vec::new(),
|
||||
channel_unreads: false,
|
||||
policy_changes: false,
|
||||
}
|
||||
} else {
|
||||
ReadyPayloadFields::default()
|
||||
};
|
||||
|
||||
// Parse and map parameters from key-value to known variables.
|
||||
for (key, value) in params {
|
||||
@@ -139,6 +159,31 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
_ => {}
|
||||
},
|
||||
"token" => session_token = Some(value.into()),
|
||||
"ready" => {
|
||||
// Re-enable all the fields the client specifies
|
||||
if let Some(captures) = READY_PAYLOAD_FIELD_REGEX.captures(value) {
|
||||
if let Some(field) = captures.get(0) {
|
||||
match field.as_str() {
|
||||
"users" => ready_payload_fields.users = true,
|
||||
"servers" => ready_payload_fields.servers = true,
|
||||
"channels" => ready_payload_fields.channels = true,
|
||||
"members" => ready_payload_fields.members = true,
|
||||
"emojis" => ready_payload_fields.emojis = true,
|
||||
"voice_states" => ready_payload_fields.voice_states = true,
|
||||
"channel_unreads" => ready_payload_fields.channel_unreads = true,
|
||||
"user_settings" => {
|
||||
if let Some(subkey) = captures.get(1) {
|
||||
ready_payload_fields
|
||||
.user_settings
|
||||
.push(subkey.as_str().to_string());
|
||||
}
|
||||
}
|
||||
"policy_changes" => ready_payload_fields.policy_changes = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -151,6 +196,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
|
||||
protocol_version,
|
||||
format,
|
||||
session_token,
|
||||
ready_payload_fields,
|
||||
})
|
||||
.is_ok()
|
||||
{
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use std::collections::HashSet;
|
||||
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, UserVoiceChannel},
|
||||
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||
};
|
||||
use revolt_models::v0;
|
||||
@@ -17,8 +19,9 @@ use super::state::{Cache, State};
|
||||
impl Cache {
|
||||
/// Check whether the current user can view a channel
|
||||
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
|
||||
#[allow(deprecated)]
|
||||
match &channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Channel::TextChannel { server, .. } => {
|
||||
let member = self.members.get(server);
|
||||
let server = self.servers.get(server);
|
||||
let mut query =
|
||||
@@ -95,21 +98,23 @@ impl State {
|
||||
pub async fn generate_ready_payload(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
fields: Vec<ReadyPayloadFields>,
|
||||
fields: &ReadyPayloadFields,
|
||||
) -> Result<EventV1> {
|
||||
let user = self.clone_user();
|
||||
self.cache.is_bot = user.bot.is_some();
|
||||
|
||||
// Fetch pending policy changes.
|
||||
let policy_changes = if user.bot.is_some() {
|
||||
vec![]
|
||||
let policy_changes = if user.bot.is_some() || !fields.policy_changes {
|
||||
None
|
||||
} else {
|
||||
db.fetch_policy_changes()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
Some(
|
||||
db.fetch_policy_changes()
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
|
||||
// Find all relationships to the user.
|
||||
@@ -120,12 +125,7 @@ impl State {
|
||||
.unwrap_or_default();
|
||||
|
||||
// Fetch all memberships with their corresponding servers.
|
||||
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
|
||||
self.cache.members = members
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id.server.clone(), x))
|
||||
.collect();
|
||||
let mut members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
|
||||
|
||||
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
|
||||
let servers = db.fetch_servers(&server_ids).await?;
|
||||
@@ -154,6 +154,55 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
let voice_states = if fields.voice_states {
|
||||
let mut voice_state_server_members: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
|
||||
// fetch voice states for all the channels we can see
|
||||
let mut voice_states = Vec::new();
|
||||
|
||||
for channel in channels.iter().filter(|c| {
|
||||
matches!(
|
||||
c,
|
||||
Channel::DirectMessage { .. }
|
||||
| Channel::Group { .. }
|
||||
| Channel::TextChannel { voice: Some(_), .. }
|
||||
)
|
||||
}) {
|
||||
if let Ok(Some(voice_state)) =
|
||||
get_channel_voice_state(&UserVoiceChannel::from_channel(channel)).await
|
||||
{
|
||||
if let Some(server) = channel.server() {
|
||||
let set = voice_state_server_members
|
||||
.entry(server.to_string())
|
||||
.or_default();
|
||||
|
||||
for participant in &voice_state.participants {
|
||||
user_ids.insert(participant.id.clone());
|
||||
set.insert(participant.id.clone());
|
||||
}
|
||||
} else {
|
||||
for participant in &voice_state.participants {
|
||||
user_ids.insert(participant.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
voice_states.push(voice_state);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all the members for for the participants who are in a server
|
||||
for (server, user_ids) in voice_state_server_members {
|
||||
let user_ids = user_ids.into_iter().collect::<Vec<_>>();
|
||||
let voice_members = db.fetch_members(&server, &user_ids).await?;
|
||||
|
||||
members.extend(voice_members);
|
||||
}
|
||||
|
||||
Some(voice_states)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch presence data for known users.
|
||||
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
|
||||
|
||||
@@ -167,8 +216,14 @@ impl State {
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.cache.members = members
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|x| (x.id.server.clone(), x))
|
||||
.collect();
|
||||
|
||||
// Fetch customisations.
|
||||
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
|
||||
let emojis = if fields.emojis {
|
||||
Some(
|
||||
db.fetch_emoji_by_parent_ids(
|
||||
&servers
|
||||
@@ -176,25 +231,34 @@ impl State {
|
||||
.map(|x| x.id.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
)
|
||||
.await?,
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|emoji| emoji.into())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch user settings
|
||||
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
|
||||
.iter()
|
||||
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
|
||||
{
|
||||
Some(db.fetch_user_settings(&user.id, keys).await?)
|
||||
let user_settings = if !fields.user_settings.is_empty() {
|
||||
Some(
|
||||
db.fetch_user_settings(&user.id, &fields.user_settings)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Fetch channel unreads
|
||||
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
|
||||
Some(db.fetch_unreads(&user.id).await?)
|
||||
let channel_unreads = if fields.channel_unreads {
|
||||
Some(
|
||||
db.fetch_unreads(&user.id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|unread| unread.into())
|
||||
.collect(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -241,30 +305,27 @@ impl State {
|
||||
}
|
||||
|
||||
Ok(EventV1::Ready {
|
||||
users: if fields.contains(&ReadyPayloadFields::Users) {
|
||||
Some(users)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
servers: if fields.contains(&ReadyPayloadFields::Servers) {
|
||||
users: if fields.users { Some(users) } else { None },
|
||||
servers: if fields.servers {
|
||||
Some(servers.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
channels: if fields.contains(&ReadyPayloadFields::Channels) {
|
||||
channels: if fields.channels {
|
||||
Some(channels.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
members: if fields.contains(&ReadyPayloadFields::Members) {
|
||||
members: if fields.members {
|
||||
Some(members.into_iter().map(Into::into).collect())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
voice_states,
|
||||
|
||||
emojis,
|
||||
user_settings,
|
||||
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||
channel_unreads,
|
||||
|
||||
policy_changes,
|
||||
})
|
||||
@@ -279,19 +340,14 @@ impl State {
|
||||
|
||||
let id = &id.to_string();
|
||||
for (channel_id, channel) in &self.cache.channels {
|
||||
match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
if server == id {
|
||||
channel_ids.insert(channel_id.clone());
|
||||
if channel.server() == Some(id) {
|
||||
channel_ids.insert(channel_id.clone());
|
||||
|
||||
if self.cache.can_view_channel(db, channel).await {
|
||||
added_channels.push(channel_id.clone());
|
||||
} else {
|
||||
removed_channels.push(channel_id.clone());
|
||||
}
|
||||
}
|
||||
if self.cache.can_view_channel(db, channel).await {
|
||||
added_channels.push(channel_id.clone());
|
||||
} else {
|
||||
removed_channels.push(channel_id.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,6 +402,11 @@ impl State {
|
||||
|
||||
/// Push presence change to the user and all associated server topics
|
||||
pub async fn broadcast_presence_change(&self, target: bool) {
|
||||
let config = revolt_config::config().await;
|
||||
if config.disable_events_dont_use {
|
||||
return;
|
||||
}
|
||||
|
||||
if if let Some(status) = &self.cache.users.get(&self.cache.user_id).unwrap().status {
|
||||
status.presence != Some(Presence::Invisible)
|
||||
} else {
|
||||
@@ -459,6 +520,7 @@ impl State {
|
||||
server,
|
||||
channels,
|
||||
emojis: _,
|
||||
voice_states: _,
|
||||
} => {
|
||||
self.insert_subscription(id.clone()).await;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
collections::{HashMap, HashSet}, num::NonZeroUsize, sync::Arc, time::Duration
|
||||
};
|
||||
|
||||
use async_std::sync::{Mutex, RwLock};
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use authifier::AuthifierEvent;
|
||||
use fred::{
|
||||
error::RedisErrorKind,
|
||||
interfaces::{ClientLike, EventInterface, PubsubInterface},
|
||||
types::RedisConfig,
|
||||
types::{ReconnectPolicy, RedisConfig},
|
||||
};
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
@@ -13,7 +13,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},
|
||||
@@ -32,6 +32,7 @@ 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>;
|
||||
@@ -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;
|
||||
|
||||
@@ -218,10 +227,16 @@ async fn listener(
|
||||
kill_signal_r: async_channel::Receiver<()>,
|
||||
write: &Mutex<WsWriter>,
|
||||
) {
|
||||
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
||||
let subscriber = match report_internal_error!(
|
||||
fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||
) {
|
||||
let stoat_config = revolt_config::config().await;
|
||||
let url = stoat_config
|
||||
.database
|
||||
.redis_pubsub
|
||||
.unwrap_or(REDIS_URI.to_string());
|
||||
|
||||
let redis_config = RedisConfig::from_url(&url).unwrap();
|
||||
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,
|
||||
};
|
||||
@@ -230,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(())
|
||||
});
|
||||
@@ -422,6 +442,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();
|
||||
@@ -458,6 +480,10 @@ async fn worker(
|
||||
|
||||
match payload {
|
||||
ClientMessage::BeginTyping { channel } => {
|
||||
if revolt_config.disable_events_dont_use {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
@@ -470,6 +496,10 @@ async fn worker(
|
||||
.await;
|
||||
}
|
||||
ClientMessage::EndTyping { channel } => {
|
||||
if revolt_config.disable_events_dont_use {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !subscribed.read().await.contains(&channel) {
|
||||
continue;
|
||||
}
|
||||
@@ -507,3 +537,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)
|
||||
}
|
||||
|
||||
28
crates/core/coalesced/Cargo.toml
Normal file
28
crates/core/coalesced/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "revolt-coalesced"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
|
||||
description = "Revolt Backend: Coalescion service"
|
||||
repository = "https://github.com/stoatchat/stoatchat"
|
||||
|
||||
[features]
|
||||
tokio = ["dep:tokio"]
|
||||
queue = ["dep:indexmap"]
|
||||
cache = ["dep:lru"]
|
||||
|
||||
default = ["tokio"]
|
||||
|
||||
[dependencies]
|
||||
tokio = { workspace = true, features = ["sync"], optional = true }
|
||||
indexmap = { workspace = true, optional = true }
|
||||
lru = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = [
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"time",
|
||||
] }
|
||||
9
crates/core/coalesced/LICENSE
Normal file
9
crates/core/coalesced/LICENSE
Normal file
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Pawel Makles
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
24
crates/core/coalesced/src/config.rs
Normal file
24
crates/core/coalesced/src/config.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
/// Config values for [`CoalescionService`].
|
||||
pub struct CoalescionServiceConfig {
|
||||
/// How many tasks are running at once
|
||||
pub max_concurrent: Option<usize>,
|
||||
/// Whether to queue tasks once `max_concurrent` is reached
|
||||
#[cfg(feature = "queue")]
|
||||
pub queue_requests: bool,
|
||||
/// Max amount of tasks in the buffer queue
|
||||
#[cfg(feature = "queue")]
|
||||
pub max_queue: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for CoalescionServiceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_concurrent: Some(100),
|
||||
#[cfg(feature = "queue")]
|
||||
queue_requests: true,
|
||||
#[cfg(feature = "queue")]
|
||||
max_queue: Some(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
27
crates/core/coalesced/src/error.rs
Normal file
27
crates/core/coalesced/src/error.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||
/// Coalescion service error.
|
||||
pub enum Error {
|
||||
/// Failed to receive the actions return from the channel for unknown reason
|
||||
RecvError,
|
||||
/// Reached the `max_concurrent` amount of actions running at once and could not queue the action
|
||||
MaxConcurrent,
|
||||
/// Reached the `max_queue` amount of actions in the queue
|
||||
MaxQueue,
|
||||
/// Failed to downcast the type to the current type being returned, this will be most likely an ID collision
|
||||
DowncastError,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::RecvError => write!(f, "Unable to receive data from the channel"),
|
||||
Error::MaxConcurrent => write!(f, "Max number of tasks running at once"),
|
||||
Error::MaxQueue => write!(f, "Max number of tasks in queue"),
|
||||
Error::DowncastError => write!(f, "Failed to downcast type, possible key collision with different types")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
39
crates/core/coalesced/src/lib.rs
Normal file
39
crates/core/coalesced/src/lib.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! # Coalesced
|
||||
//!
|
||||
//! Coalescion service to group, caching and queue duplicate actions.
|
||||
//! useful for deduplicating web requests, database lookups and other similar resource
|
||||
//! intensive or rate-limited actions.
|
||||
//!
|
||||
//! ## Features
|
||||
//! - `tokio`: Uses tokio for the async backend, this is currently the only backend.
|
||||
//! - `queue`: Whether to support queueing requests to only allow X amount of actions running at once.
|
||||
//! - `cache`: Whether to cache the actions results for future actions with the same id, uses an LRU cache internally.
|
||||
//!
|
||||
//! [`CoalescionService`] uses both [`Arc`] and [`RwLock`] internally and can be cheaply cloned to
|
||||
//! use in your codebase.
|
||||
//!
|
||||
//! It is common practice to wrap the service and in your own which delegates the executions to ensure all ids are tracked in one location across your codebase.
|
||||
//!
|
||||
//! All values are stored using [`Any`] and must be [`'static`] + [`Send`] + [`Sync`], if there is an id mismatch
|
||||
//! and a type is wrong the library will return an error, values returned from the service are also
|
||||
//! wrapped in an [`Arc`] as they are shared to each duplicate action.
|
||||
//!
|
||||
//! ## Example:
|
||||
//! ```rs
|
||||
//! use revolt_coalesced::CoalescionService;
|
||||
//!
|
||||
//! let service = CoalescionService::new();
|
||||
//!
|
||||
//! let user_id = "my_user_id";
|
||||
//! let user = service.execute(user_id, || async move {
|
||||
//! database.fetch_user(user_id).await.unwrap()
|
||||
//! }).await;
|
||||
//! ```
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
mod service;
|
||||
|
||||
pub use config::CoalescionServiceConfig;
|
||||
pub use error::Error;
|
||||
pub use service::CoalescionService;
|
||||
208
crates/core/coalesced/src/service.rs
Normal file
208
crates/core/coalesced/src/service.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc};
|
||||
|
||||
use tokio::sync::{
|
||||
watch::{channel as watch_channel, Receiver},
|
||||
RwLock,
|
||||
};
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
use lru::LruCache;
|
||||
|
||||
#[cfg(feature = "queue")]
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::{CoalescionServiceConfig, Error};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// # Coalescion service
|
||||
///
|
||||
/// See module description for example usage.
|
||||
pub struct CoalescionService<Id: Hash + Clone + Eq> {
|
||||
config: Arc<CoalescionServiceConfig>,
|
||||
watchers: Arc<RwLock<HashMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
|
||||
#[cfg(feature = "queue")]
|
||||
queue: Arc<RwLock<IndexMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
|
||||
#[cfg(feature = "cache")]
|
||||
cache: Option<Arc<tokio::sync::Mutex<LruCache<Id, Arc<dyn Any + Send + Sync>>>>>,
|
||||
}
|
||||
|
||||
impl<Id: Hash + Clone + Eq> CoalescionService<Id> {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn from_config(config: CoalescionServiceConfig) -> Self {
|
||||
Self {
|
||||
config: Arc::new(config),
|
||||
watchers: Arc::new(RwLock::new(HashMap::new())),
|
||||
#[cfg(feature = "queue")]
|
||||
queue: Arc::new(RwLock::new(IndexMap::new())),
|
||||
#[cfg(feature = "cache")]
|
||||
cache: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
pub fn from_cache(
|
||||
config: CoalescionServiceConfig,
|
||||
cache: LruCache<Id, Arc<dyn Any + Send + Sync>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cache: Some(Arc::new(Mutex::new(cache))),
|
||||
..Self::from_config(config)
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for<Value: Any + Send + Sync>(
|
||||
&self,
|
||||
mut receiver: Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>,
|
||||
) -> Result<Arc<Value>, Error> {
|
||||
receiver
|
||||
.wait_for(|v| v.is_some())
|
||||
.await
|
||||
.map_err(|_| Error::RecvError)
|
||||
.and_then(|r| r.clone().unwrap())
|
||||
.and_then(|arc| Arc::downcast(arc).map_err(|_| Error::DowncastError))
|
||||
}
|
||||
|
||||
async fn insert_and_execute<
|
||||
Value: Send + Sync + 'static,
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = Value>,
|
||||
>(
|
||||
&self,
|
||||
id: Id,
|
||||
func: F,
|
||||
) -> Result<Arc<Value>, Error> {
|
||||
let (send, recv) = watch_channel(None);
|
||||
|
||||
self.watchers.write().await.insert(id.clone(), recv);
|
||||
|
||||
let value = Ok(Arc::new(func().await));
|
||||
|
||||
send.send_modify(|opt| {
|
||||
opt.replace(value.clone().map(|v| v as Arc<dyn Any + Send + Sync>));
|
||||
});
|
||||
|
||||
#[cfg(feature = "cache")]
|
||||
if let Some(cache) = self.cache.as_ref() {
|
||||
if let Ok(value) = &value {
|
||||
cache.lock().await.push(id.clone(), value.clone());
|
||||
}
|
||||
};
|
||||
|
||||
self.watchers.write().await.remove(&id);
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
/// Coalesces an function, the actual function may not run if one with the same id is already running,
|
||||
/// queued to be ran, or cached, the id should be globally unique for this specific action.
|
||||
pub async fn execute<
|
||||
Value: Send + Sync + 'static,
|
||||
F: FnOnce() -> Fut,
|
||||
Fut: Future<Output = Value>,
|
||||
>(
|
||||
&self,
|
||||
id: Id,
|
||||
func: F,
|
||||
) -> Result<Arc<Value>, Error> {
|
||||
#[cfg(feature = "cache")]
|
||||
if let Some(cache) = self.cache.as_ref() {
|
||||
if let Some(value) = cache.lock().await.get(&id) {
|
||||
return Arc::downcast::<Value>(value.clone()).map_err(|_| Error::DowncastError);
|
||||
}
|
||||
};
|
||||
|
||||
let (receiver, length) = {
|
||||
let watchers = self.watchers.read().await;
|
||||
let length = watchers.len();
|
||||
|
||||
(watchers.get(&id).cloned(), length)
|
||||
};
|
||||
|
||||
if let Some(receiver) = receiver {
|
||||
self.wait_for(receiver).await
|
||||
} else {
|
||||
match self.config.max_concurrent {
|
||||
Some(max_concurrent) if length >= max_concurrent => {
|
||||
#[cfg(feature = "queue")]
|
||||
if self.config.queue_requests {
|
||||
let (receiver, length) = {
|
||||
let queue = self.queue.read().await;
|
||||
|
||||
(queue.get(&id).cloned(), queue.len())
|
||||
};
|
||||
|
||||
if let Some(receiver) = receiver {
|
||||
return self.wait_for(receiver).await;
|
||||
} else {
|
||||
if self
|
||||
.config
|
||||
.max_queue
|
||||
.is_some_and(|max_queue| max_queue >= length)
|
||||
{
|
||||
return Err(Error::MaxQueue);
|
||||
};
|
||||
|
||||
let (send, recv) = watch_channel(None);
|
||||
|
||||
self.queue.write().await.insert(id.clone(), recv);
|
||||
|
||||
loop {
|
||||
let length = self.watchers.read().await.len();
|
||||
|
||||
if length < max_concurrent {
|
||||
let first_key = {
|
||||
let queue = self.queue.read().await;
|
||||
queue.first().map(|v| v.0).cloned()
|
||||
};
|
||||
|
||||
if first_key == Some(id.clone()) {
|
||||
self.queue.write().await.shift_remove(&id);
|
||||
|
||||
let response = self.insert_and_execute(id, func).await;
|
||||
|
||||
send.send_modify(|opt| {
|
||||
opt.replace(
|
||||
response
|
||||
.clone()
|
||||
.map(|v| v as Arc<dyn Any + Send + Sync>),
|
||||
);
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(Error::MaxConcurrent)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "queue"))]
|
||||
Err(Error::MaxConcurrent)
|
||||
}
|
||||
_ => self.insert_and_execute(id, func).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the amount of currently running tasks
|
||||
pub async fn current_task_count(&self) -> usize {
|
||||
self.watchers.read().await.len()
|
||||
}
|
||||
|
||||
#[cfg(feature = "queue")]
|
||||
/// Fetches the current length of the queue
|
||||
pub async fn current_queue_len(&self) -> usize {
|
||||
self.queue.read().await.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Id: Hash + Clone + Eq> Default for CoalescionService<Id> {
|
||||
fn default() -> Self {
|
||||
Self::from_config(CoalescionServiceConfig::default())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
[package]
|
||||
name = "revolt-config"
|
||||
version = "0.8.8"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Configuration"
|
||||
repository = "https://github.com/stoatchat/stoatchat"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -17,24 +18,24 @@ default = ["test", "sentry"]
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
config = "0.13.3"
|
||||
cached = "0.44.0"
|
||||
once_cell = "1.18.0"
|
||||
config = { workspace = true }
|
||||
cached = { workspace = true }
|
||||
once_cell = { 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 }
|
||||
async-std = { workspace = true, features = ["attributes"], 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.8.8", path = "../result", optional = true }
|
||||
revolt-result = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
production = false
|
||||
disable_events_dont_use = false
|
||||
|
||||
[database]
|
||||
# MongoDB connection URL
|
||||
@@ -11,7 +12,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"
|
||||
@@ -22,17 +23,23 @@ january = "http://local.revolt.chat/january"
|
||||
voso_legacy = ""
|
||||
voso_legacy_ws = ""
|
||||
|
||||
[hosts.livekit]
|
||||
|
||||
[rabbit]
|
||||
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]
|
||||
@@ -56,6 +63,8 @@ voso_legacy_token = ""
|
||||
trust_cloudflare = false
|
||||
# easypwned endpoint
|
||||
easypwned = ""
|
||||
# Tenor API Key
|
||||
tenor_key = ""
|
||||
|
||||
[api.security.captcha]
|
||||
# hCaptcha configuration
|
||||
@@ -66,8 +75,15 @@ hcaptcha_sitekey = ""
|
||||
# Maximum concurrent connections (to proxy server)
|
||||
max_concurrent_connections = 50
|
||||
|
||||
[api.users]
|
||||
[api.livekit]
|
||||
# How long to ring devices for when calling in dms/groups, in seconds
|
||||
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
|
||||
@@ -79,12 +95,18 @@ 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"
|
||||
mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions
|
||||
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
|
||||
fr_received_queue = "notifications.ingest.fr_received" # friend request received
|
||||
dm_call_queue = "notifications.ingest.dm_call" # direct message voice call
|
||||
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
|
||||
ack_queue = "notifications.process.ack" # updates badges for apple devices
|
||||
|
||||
@@ -114,6 +136,8 @@ pkcs8 = ""
|
||||
key_id = ""
|
||||
team_id = ""
|
||||
|
||||
[january]
|
||||
blocked_domains = []
|
||||
|
||||
[files]
|
||||
# Encryption key for stored files
|
||||
@@ -210,6 +234,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
|
||||
|
||||
@@ -228,6 +256,18 @@ message_attachments = 5
|
||||
# Maximum number of servers the user can create/join
|
||||
servers = 50
|
||||
|
||||
# Maximum audio frequency (Hz) in voice calls
|
||||
voice_quality = 16000
|
||||
|
||||
# Whether the user can use video streams in voice calls
|
||||
video = true
|
||||
|
||||
# Maximum resolution (width, height) of video streams in voice calls
|
||||
video_resolution = [1080, 720]
|
||||
|
||||
# Minimum and maximum aspect ratio of video streams in voice calls
|
||||
video_aspect_ratio = [0.3, 2.5]
|
||||
|
||||
[features.limits.new_user.file_upload_size_limit]
|
||||
# Maximum file size limits (in bytes)
|
||||
attachments = 20_000_000
|
||||
@@ -255,6 +295,18 @@ message_attachments = 5
|
||||
# Maximum number of servers the user can create/join
|
||||
servers = 100
|
||||
|
||||
# Maximum audio frequency (Hz) in voice calls
|
||||
voice_quality = 16000
|
||||
|
||||
# Whether the user can use video streams in voice calls
|
||||
video = true
|
||||
|
||||
# Maximum resolution (width, height) of video streams in voice calls
|
||||
video_resolution = [1280, 720]
|
||||
|
||||
# Minimum and maximum aspect ratio of video streams in voice calls
|
||||
video_aspect_ratio = [0.3, 2.5]
|
||||
|
||||
[features.limits.default.file_upload_size_limit]
|
||||
# Maximum file size limits (in bytes)
|
||||
attachments = 20_000_000
|
||||
@@ -269,11 +321,19 @@ 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 = ""
|
||||
events = ""
|
||||
voice_ingress = ""
|
||||
files = ""
|
||||
proxy = ""
|
||||
pushd = ""
|
||||
crond = ""
|
||||
gifbox = ""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
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;
|
||||
@@ -94,12 +94,23 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
}
|
||||
}
|
||||
|
||||
for path in CONFIG_SEARCH_PATHS {
|
||||
if std::path::Path::new(path).exists() {
|
||||
builder = builder.add_source(File::new(path, 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();
|
||||
}
|
||||
|
||||
builder = builder.add_source(Environment::with_prefix("REVOLT").separator("__"));
|
||||
|
||||
builder.build().unwrap()
|
||||
})
|
||||
});
|
||||
@@ -108,6 +119,12 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||
pub struct Database {
|
||||
pub mongodb: String,
|
||||
pub redis: String,
|
||||
pub redis_pubsub: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct RabbitQueues {
|
||||
pub acks: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -116,6 +133,8 @@ pub struct Rabbit {
|
||||
pub port: u16,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub default_exchange: String,
|
||||
pub queues: RabbitQueues,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -125,8 +144,7 @@ pub struct Hosts {
|
||||
pub events: String,
|
||||
pub autumn: String,
|
||||
pub january: String,
|
||||
pub voso_legacy: String,
|
||||
pub voso_legacy_ws: String,
|
||||
pub livekit: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -190,6 +208,7 @@ pub struct ApiSecurity {
|
||||
pub captcha: ApiSecurityCaptcha,
|
||||
pub trust_cloudflare: bool,
|
||||
pub easypwned: String,
|
||||
pub tenor_key: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -197,9 +216,29 @@ pub struct ApiWorkers {
|
||||
pub max_concurrent_connections: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiLiveKit {
|
||||
pub call_ring_duration: usize,
|
||||
pub nodes: HashMap<String, LiveKitNode>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct LiveKitNode {
|
||||
pub url: String,
|
||||
pub lat: f64,
|
||||
pub lon: f64,
|
||||
pub key: String,
|
||||
pub secret: String,
|
||||
|
||||
// whether to hide the node in the nodes list
|
||||
#[serde(default)]
|
||||
pub private: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ApiUsers {
|
||||
pub early_adopter_cutoff: Option<u64>,
|
||||
pub min_username_length: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -208,6 +247,7 @@ pub struct Api {
|
||||
pub smtp: ApiSmtp,
|
||||
pub security: ApiSecurity,
|
||||
pub workers: ApiWorkers,
|
||||
pub livekit: ApiLiveKit,
|
||||
pub users: ApiUsers,
|
||||
}
|
||||
|
||||
@@ -216,10 +256,12 @@ 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,
|
||||
pub mass_mention_queue: String,
|
||||
pub dm_call_queue: String,
|
||||
pub fr_accepted_queue: String,
|
||||
pub fr_received_queue: String,
|
||||
pub generic_queue: String,
|
||||
@@ -250,6 +292,10 @@ impl Pushd {
|
||||
self.get_routing_key(self.mass_mention_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_dm_call_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.dm_call_queue.clone())
|
||||
}
|
||||
|
||||
pub fn get_fr_accepted_routing_key(&self) -> String {
|
||||
self.get_routing_key(self.fr_accepted_queue.clone())
|
||||
}
|
||||
@@ -263,6 +309,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,
|
||||
@@ -307,6 +358,8 @@ pub struct GlobalLimits {
|
||||
pub new_user_hours: usize,
|
||||
|
||||
pub body_limit_size: usize,
|
||||
|
||||
pub restrict_server_creation: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -317,6 +370,10 @@ pub struct FeaturesLimits {
|
||||
pub message_length: usize,
|
||||
pub message_attachments: usize,
|
||||
pub servers: usize,
|
||||
pub voice_quality: u32,
|
||||
pub video: bool,
|
||||
pub video_resolution: [u32; 2],
|
||||
pub video_aspect_ratio: [f32; 2],
|
||||
|
||||
pub file_upload_size_limit: HashMap<String, usize>,
|
||||
}
|
||||
@@ -332,6 +389,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)]
|
||||
@@ -349,6 +416,7 @@ 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,
|
||||
@@ -361,10 +429,12 @@ pub struct Features {
|
||||
pub struct Sentry {
|
||||
pub api: String,
|
||||
pub events: String,
|
||||
pub voice_ingress: String,
|
||||
pub files: String,
|
||||
pub proxy: String,
|
||||
pub pushd: String,
|
||||
pub crond: String,
|
||||
pub gifbox: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
@@ -374,10 +444,12 @@ 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 disable_events_dont_use: bool,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
@@ -408,12 +480,14 @@ pub async fn config() -> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.8.8"
|
||||
version = "0.13.7"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <me@insrt.uk>"]
|
||||
description = "Revolt Backend: Database Implementation"
|
||||
repository = "https://github.com/stoatchat/stoatchat"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -15,84 +16,88 @@ mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
|
||||
# ... Other
|
||||
tasks = ["isahc", "linkify", "url-escape"]
|
||||
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
|
||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
|
||||
axum-impl = ["axum"]
|
||||
rocket-impl = [
|
||||
"rocket",
|
||||
"schemars",
|
||||
"revolt_okapi",
|
||||
"revolt_rocket_okapi",
|
||||
"authifier/rocket_impl",
|
||||
]
|
||||
axum-impl = ["axum", "revolt-result/axum"]
|
||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||
voice = ["livekit-api", "livekit-protocol", "livekit-runtime"]
|
||||
|
||||
# Default Features
|
||||
default = ["mongodb", "async-std-runtime", "tasks"]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-config = { version = "0.8.8", path = "../config", features = [
|
||||
"report-macros",
|
||||
] }
|
||||
revolt-result = { version = "0.8.8", path = "../result" }
|
||||
revolt-models = { version = "0.8.8", path = "../models", features = [
|
||||
"validator",
|
||||
] }
|
||||
revolt-presence = { version = "0.8.8", path = "../presence" }
|
||||
revolt-permissions = { version = "0.8.8", path = "../permissions", features = [
|
||||
"serde",
|
||||
"bson",
|
||||
] }
|
||||
revolt-parser = { version = "0.8.8", 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 }
|
||||
|
||||
# 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 }
|
||||
|
||||
# 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 }
|
||||
async-std = { workspace = true, features = ["attributes"], optional = true }
|
||||
|
||||
# Axum Impl
|
||||
axum = { version = "0.7.5", optional = true }
|
||||
axum = { workspace = true, 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 }
|
||||
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 }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0.15" }
|
||||
authifier = { workspace = true }
|
||||
|
||||
# RabbitMQ
|
||||
amqprs = { version = "1.7.0" }
|
||||
lapin = { workspace = true, features = ["tokio"] }
|
||||
|
||||
# Voice
|
||||
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 }
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
],
|
||||
"roles": {
|
||||
"__ID:5__": {
|
||||
"_id": "__ID:5__",
|
||||
"name": "Moderator",
|
||||
"permissions": {
|
||||
"a": 545270208,
|
||||
@@ -55,6 +56,7 @@
|
||||
"rank": 1
|
||||
},
|
||||
"__ID:6__": {
|
||||
"_id": "__ID:6__",
|
||||
"name": "Owner",
|
||||
"permissions": {
|
||||
"a": 0,
|
||||
|
||||
@@ -1,30 +1,79 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::events::rabbit::*;
|
||||
use crate::User;
|
||||
use amqprs::channel::BasicPublishArguments;
|
||||
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() -> Self {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
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 connect to RabbitMQ"),
|
||||
);
|
||||
|
||||
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(
|
||||
&self,
|
||||
accepted_request_user: &User,
|
||||
@@ -42,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(
|
||||
@@ -75,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(
|
||||
@@ -112,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(
|
||||
@@ -155,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(
|
||||
@@ -190,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,
|
||||
@@ -222,22 +277,106 @@ 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
|
||||
/// Used to send an update about a DM call, eg. start or end of a call.
|
||||
/// Recipients can be used to narrow the scope of recipients, otherwise all recipients will be notified.
|
||||
/// `ended` refers to the ringing period, not necessarily the call itself.
|
||||
pub async fn dm_call_updated(
|
||||
&self,
|
||||
initiator_id: &str,
|
||||
channel_id: &str,
|
||||
started_at: Option<&str>,
|
||||
ended: bool,
|
||||
recipients: Option<Vec<String>>,
|
||||
) -> Result<(), AMQPError> {
|
||||
let config = revolt_config::config().await;
|
||||
|
||||
let payload = InternalDmCallPayload {
|
||||
payload: DmCallPayload {
|
||||
initiator_id: initiator_id.to_string(),
|
||||
channel_id: channel_id.to_string(),
|
||||
started_at: started_at.map(|f| f.to_string()),
|
||||
ended,
|
||||
},
|
||||
recipients,
|
||||
};
|
||||
let payload = to_string(&payload).unwrap();
|
||||
|
||||
debug!(
|
||||
"Sending dm call update payload on channel {}: {}",
|
||||
config.pushd.get_dm_call_routing_key(),
|
||||
payload
|
||||
);
|
||||
|
||||
self.dm_call_updated
|
||||
.basic_publish(
|
||||
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?;
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -35,7 +36,7 @@ pub enum DatabaseInfo {
|
||||
}
|
||||
|
||||
/// Database
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Database {
|
||||
/// Mock database
|
||||
Reference(ReferenceDb),
|
||||
@@ -69,7 +70,7 @@ impl DatabaseInfo {
|
||||
.await;
|
||||
|
||||
#[cfg(not(feature = "mongodb"))]
|
||||
return Err("MongoDB not enabled.".to_string())
|
||||
return Err("MongoDB not enabled.".to_string());
|
||||
} else {
|
||||
DatabaseInfo::Reference.connect().await
|
||||
}
|
||||
@@ -90,7 +91,7 @@ impl DatabaseInfo {
|
||||
.await;
|
||||
|
||||
#[cfg(not(feature = "mongodb"))]
|
||||
return Err("MongoDB not enabled.".to_string())
|
||||
return Err("MongoDB not enabled.".to_string());
|
||||
}
|
||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
||||
}
|
||||
@@ -137,29 +138,33 @@ impl Database {
|
||||
.api
|
||||
.smtp
|
||||
.reply_to
|
||||
.unwrap_or("support@revolt.chat".into()),
|
||||
.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: Default::default(),
|
||||
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 Revolt account.".into(),
|
||||
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 Revolt password.".into(),
|
||||
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 Revolt account, reset your password."
|
||||
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),
|
||||
|
||||
@@ -11,6 +11,7 @@ use serde::Serialize;
|
||||
|
||||
database_derived!(
|
||||
/// MongoDB implementation
|
||||
#[derive(Debug)]
|
||||
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
||||
);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ReferenceDb {
|
||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
||||
|
||||
@@ -3,10 +3,12 @@ use revolt_result::Error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use revolt_models::v0::{
|
||||
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage,
|
||||
FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message,
|
||||
PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser,
|
||||
PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, 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;
|
||||
@@ -20,16 +22,33 @@ pub enum Ping {
|
||||
}
|
||||
|
||||
/// Fields provided in Ready payload
|
||||
#[derive(PartialEq)]
|
||||
pub enum ReadyPayloadFields {
|
||||
Users,
|
||||
Servers,
|
||||
Channels,
|
||||
Members,
|
||||
Emoji,
|
||||
#[derive(PartialEq, Debug, Clone, Deserialize)]
|
||||
pub struct ReadyPayloadFields {
|
||||
pub users: bool,
|
||||
pub servers: bool,
|
||||
pub channels: bool,
|
||||
pub members: bool,
|
||||
pub emojis: bool,
|
||||
pub voice_states: bool,
|
||||
pub user_settings: Vec<String>,
|
||||
pub channel_unreads: bool,
|
||||
pub policy_changes: bool,
|
||||
}
|
||||
|
||||
UserSettings(Vec<String>),
|
||||
ChannelUnreads,
|
||||
impl Default for ReadyPayloadFields {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
users: true,
|
||||
servers: true,
|
||||
channels: true,
|
||||
members: true,
|
||||
emojis: true,
|
||||
voice_states: true,
|
||||
user_settings: Vec::new(),
|
||||
channel_unreads: false,
|
||||
policy_changes: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol Events
|
||||
@@ -37,9 +56,13 @@ pub enum 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,
|
||||
@@ -57,17 +80,22 @@ pub enum EventV1 {
|
||||
members: Option<Vec<Member>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
emojis: Option<Vec<Emoji>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voice_states: Option<Vec<ChannelVoiceState>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
user_settings: Option<UserSettings>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||
|
||||
policy_changes: Vec<PolicyChange>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
policy_changes: Option<Vec<PolicyChange>>,
|
||||
},
|
||||
|
||||
/// Ping response
|
||||
Pong { data: Ping },
|
||||
Pong {
|
||||
data: Ping,
|
||||
},
|
||||
/// New message
|
||||
Message(Message),
|
||||
|
||||
@@ -88,7 +116,10 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Delete message
|
||||
MessageDelete { id: String, channel: String },
|
||||
MessageDelete {
|
||||
id: String,
|
||||
channel: String,
|
||||
},
|
||||
|
||||
/// New reaction to a message
|
||||
MessageReact {
|
||||
@@ -114,7 +145,10 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Bulk delete messages
|
||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
||||
BulkMessageDelete {
|
||||
channel: String,
|
||||
ids: Vec<String>,
|
||||
},
|
||||
|
||||
/// New server
|
||||
ServerCreate {
|
||||
@@ -122,6 +156,7 @@ pub enum EventV1 {
|
||||
server: Server,
|
||||
channels: Vec<Channel>,
|
||||
emojis: Vec<Emoji>,
|
||||
voice_states: Vec<ChannelVoiceState>,
|
||||
},
|
||||
|
||||
/// Update existing server
|
||||
@@ -133,7 +168,9 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Delete server
|
||||
ServerDelete { id: String },
|
||||
ServerDelete {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Update existing server member
|
||||
ServerMemberUpdate {
|
||||
@@ -144,7 +181,13 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// User joins server
|
||||
ServerMemberJoin { id: String, user: String },
|
||||
ServerMemberJoin {
|
||||
id: String,
|
||||
// Deprecated: use member.id.user
|
||||
#[deprecated = "Use member.id.user instead"]
|
||||
user: String,
|
||||
member: Member,
|
||||
},
|
||||
|
||||
/// User left server
|
||||
ServerMemberLeave {
|
||||
@@ -163,10 +206,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 {
|
||||
@@ -178,9 +227,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
|
||||
///
|
||||
@@ -191,12 +246,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),
|
||||
@@ -212,19 +278,33 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Delete channel
|
||||
ChannelDelete { id: String },
|
||||
ChannelDelete {
|
||||
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 {
|
||||
@@ -244,10 +324,43 @@ pub enum EventV1 {
|
||||
},
|
||||
|
||||
/// Delete webhook
|
||||
WebhookDelete { id: String },
|
||||
WebhookDelete {
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Auth events
|
||||
Auth(AuthifierEvent),
|
||||
|
||||
/// Voice events
|
||||
VoiceChannelJoin {
|
||||
id: String,
|
||||
state: UserVoiceState,
|
||||
},
|
||||
VoiceChannelLeave {
|
||||
id: String,
|
||||
user: String,
|
||||
},
|
||||
VoiceChannelMove {
|
||||
user: String,
|
||||
from: String,
|
||||
to: String,
|
||||
state: UserVoiceState,
|
||||
},
|
||||
UserVoiceStateUpdate {
|
||||
id: String,
|
||||
channel_id: String,
|
||||
data: PartialUserVoiceState,
|
||||
},
|
||||
UserMoveVoiceChannel {
|
||||
node: String,
|
||||
from: String,
|
||||
to: String,
|
||||
token: String,
|
||||
},
|
||||
/// User's active slowmodes
|
||||
UserSlowmodes {
|
||||
slowmodes: Vec<ChannelSlowmode>,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventV1 {
|
||||
|
||||
@@ -37,6 +37,20 @@ pub struct GenericPayload {
|
||||
pub user: User,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DmCallPayload {
|
||||
pub initiator_id: String,
|
||||
pub channel_id: String,
|
||||
pub started_at: Option<String>,
|
||||
pub ended: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct InternalDmCallPayload {
|
||||
pub payload: DmCallPayload,
|
||||
pub recipients: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", content = "data")]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
@@ -46,6 +60,7 @@ pub enum PayloadKind {
|
||||
FRReceived(FRReceivedPayload),
|
||||
BadgeUpdate(usize),
|
||||
Generic(GenericPayload),
|
||||
DmCallStartEnd(DmCallPayload),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -63,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>,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use serde::Deserialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use super::client::Ping;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClientMessage {
|
||||
Authenticate { token: String },
|
||||
|
||||
@@ -112,6 +112,10 @@ pub mod tasks;
|
||||
mod amqp;
|
||||
pub use amqp::amqp::AMQP;
|
||||
|
||||
#[cfg(feature = "voice")]
|
||||
pub mod voice;
|
||||
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
|
||||
@@ -9,14 +9,13 @@ use crate::{
|
||||
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
||||
options::FindOptions,
|
||||
},
|
||||
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, User, DISCRIMINATOR_SEARCH_SPACE,
|
||||
AbstractServers, Invite, MongoDb, User, DISCRIMINATOR_SEARCH_SPACE,
|
||||
};
|
||||
use bson::{oid::ObjectId, to_bson};
|
||||
use futures::StreamExt;
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use rand::seq::SliceRandom;
|
||||
use revolt_permissions::DEFAULT_WEBHOOK_PERMISSIONS;
|
||||
use revolt_result::{Error, ErrorType};
|
||||
use revolt_permissions::{ChannelPermission, DEFAULT_WEBHOOK_PERMISSIONS};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
@@ -26,7 +25,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 42; // MUST BE +1 to last migration
|
||||
pub const LATEST_REVISION: i32 = 50; // MUST BE +1 to last migration
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -914,6 +913,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
}
|
||||
|
||||
if revision <= 26 {
|
||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||
info!("Running migration [revision 26 / 15-05-2024]: fix invites being incorrectly serialized with wrong enum tagging.");
|
||||
|
||||
auto_derived!(
|
||||
@@ -1080,6 +1080,14 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
channel_id: String,
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
enum Channel {
|
||||
Group { owner: String },
|
||||
TextChannel { server: String },
|
||||
VoiceChannel { server: String }
|
||||
}
|
||||
|
||||
let webhooks = db
|
||||
.db()
|
||||
.collection::<WebhookShell>("channel_webhooks")
|
||||
@@ -1091,8 +1099,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.await;
|
||||
|
||||
for webhook in webhooks {
|
||||
match db.fetch_channel(&webhook.channel_id).await {
|
||||
Ok(channel) => {
|
||||
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,
|
||||
Channel::TextChannel { server, .. }
|
||||
@@ -1100,7 +1108,6 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
let server = db.fetch_server(&server).await.expect("server");
|
||||
server.owner
|
||||
}
|
||||
_ => unreachable!("not server or group channel!"),
|
||||
};
|
||||
|
||||
db.db()
|
||||
@@ -1118,17 +1125,13 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.await
|
||||
.expect("update webhook");
|
||||
}
|
||||
Err(Error {
|
||||
error_type: ErrorType::NotFound,
|
||||
..
|
||||
}) => {
|
||||
None => {
|
||||
db.db()
|
||||
.collection::<WebhookShell>("channel_webhooks")
|
||||
.delete_one(doc! { "_id": webhook._id })
|
||||
.await
|
||||
.expect("failed to delete invalid webhook");
|
||||
}
|
||||
Err(err) => panic!("{err:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1169,9 +1172,9 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.expect("failed to update users");
|
||||
}
|
||||
|
||||
if revision <= 41 {
|
||||
if revision <= 43 {
|
||||
info!(
|
||||
"Running migration [revision 41 / 05-06-2025]: convert role ranks to uniform numbers."
|
||||
"Running migration [revision 43 / 05-06-2025]: convert role ranks to uniform numbers."
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@@ -1226,6 +1229,83 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
}
|
||||
}
|
||||
|
||||
if revision <= 46 {
|
||||
info!("Running migration [revision 46 / 29-04-2025]: Convert all `VoiceChannel`'s into `TextChannel`");
|
||||
|
||||
db.col::<Document>("channels")
|
||||
.update_many(
|
||||
doc! { "channel_type": "VoiceChannel" },
|
||||
doc! {
|
||||
"$set": {
|
||||
"channel_type": "TextChannel",
|
||||
"voice": {}
|
||||
}
|
||||
}
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update voice channels");
|
||||
};
|
||||
|
||||
if revision <= 48 {
|
||||
info!("Running migration [revision 48 / 22-10-2025]: Add Video + Listen to default permissions");
|
||||
|
||||
db.col::<Document>("servers")
|
||||
.update_many(
|
||||
doc! { },
|
||||
doc! {
|
||||
"$bit": {
|
||||
"default_permissions": {
|
||||
"or": (ChannelPermission::Video + ChannelPermission::Speak + ChannelPermission::Listen) as i64
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update default_permissions");
|
||||
};
|
||||
|
||||
if revision <= 49 {
|
||||
info!("Running migration [revision 49 / 12-12-2025]: Add _id key to roles");
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct Server {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
#[serde(default = "HashMap::<String, Document>::new")]
|
||||
pub roles: HashMap<String, Document>,
|
||||
}
|
||||
|
||||
let mut servers = db
|
||||
.db()
|
||||
.collection::<Server>("servers")
|
||||
.find(doc! {
|
||||
"roles": {
|
||||
"$exists": true,
|
||||
"$ne": {}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.map(|res| res.expect("Failed to decode Server { id, roles }"));
|
||||
|
||||
while let Some(server) = servers.next().await {
|
||||
let mut doc = doc! {};
|
||||
|
||||
for id in server.roles.keys() {
|
||||
doc.insert(
|
||||
format!("roles.{id}._id"),
|
||||
id,
|
||||
);
|
||||
}
|
||||
|
||||
db.db()
|
||||
.collection::<Server>("servers")
|
||||
.update_one(doc! { "_id": &server.id }, doc! { "$set": doc })
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
};
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
LATEST_REVISION.max(revision)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ impl Invite {
|
||||
creator: creator.id.clone(),
|
||||
channel: id.clone(),
|
||||
}),
|
||||
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
|
||||
Channel::TextChannel { id, server, .. } => {
|
||||
Ok(Invite::Server {
|
||||
code,
|
||||
creator: creator.id.clone(),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
#![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;
|
||||
@@ -8,8 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
events::client::EventV1, Database, File, PartialServer,
|
||||
Server, SystemMessage, User, AMQP,
|
||||
events::client::EventV1, Database, File, PartialServer, Server, SystemMessage, User, AMQP,
|
||||
};
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
@@ -106,38 +107,22 @@ auto_derived!(
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
|
||||
/// Voice Information for when this channel is also a voice channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
voice: Option<VoiceInformation>,
|
||||
|
||||
/// The channel's slowmode delay in seconds
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
slowmode: Option<u64>,
|
||||
},
|
||||
/// Voice channel belonging to a server
|
||||
VoiceChannel {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
/// Id of the server this channel belongs to
|
||||
server: String,
|
||||
}
|
||||
|
||||
/// Display name of the channel
|
||||
name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Channel description
|
||||
description: Option<String>,
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
icon: Option<File>,
|
||||
|
||||
/// Default permissions assigned to users in this channel
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
default_permissions: Option<OverrideField>,
|
||||
/// Permissions assigned based on role to this channel
|
||||
#[serde(
|
||||
default = "HashMap::<String, OverrideField>::new",
|
||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
||||
)]
|
||||
role_permissions: HashMap<String, OverrideField>,
|
||||
|
||||
/// Whether this channel is marked as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
nsfw: bool,
|
||||
},
|
||||
#[derive(Default)]
|
||||
pub struct VoiceInformation {
|
||||
/// Maximium amount of users allowed in the voice channel at once
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_users: Option<usize>,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -164,6 +149,10 @@ auto_derived!(
|
||||
pub default_permissions: Option<OverrideField>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub last_message_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub voice: Option<VoiceInformation>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub slowmode: Option<u64>,
|
||||
}
|
||||
|
||||
/// Optional fields on channel object
|
||||
@@ -171,6 +160,7 @@ auto_derived!(
|
||||
Description,
|
||||
Icon,
|
||||
DefaultPermissions,
|
||||
Voice,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -222,16 +212,21 @@ impl Channel {
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
voice: data.voice.map(|voice| voice.into()),
|
||||
slowmode: None,
|
||||
},
|
||||
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
|
||||
v0::LegacyServerChannelType::Voice => Channel::TextChannel {
|
||||
id: id.clone(),
|
||||
server: server.id.to_owned(),
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
icon: None,
|
||||
last_message_id: None,
|
||||
default_permissions: None,
|
||||
role_permissions: HashMap::new(),
|
||||
nsfw: data.nsfw.unwrap_or(false),
|
||||
voice: Some(data.voice.unwrap_or_default().into()),
|
||||
slowmode: None,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -432,8 +427,28 @@ impl Channel {
|
||||
Channel::DirectMessage { id, .. }
|
||||
| Channel::Group { id, .. }
|
||||
| Channel::SavedMessages { id, .. }
|
||||
| Channel::TextChannel { id, .. }
|
||||
| Channel::VoiceChannel { id, .. } => id,
|
||||
| Channel::TextChannel { id, .. } => id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone this channel's server id
|
||||
pub fn server(&self) -> Option<&str> {
|
||||
match self {
|
||||
Channel::TextChannel { server, .. } => Some(server),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets this channel's voice information
|
||||
pub fn voice(&self) -> Option<Cow<VoiceInformation>> {
|
||||
match self {
|
||||
Self::DirectMessage { .. } | Self::Group { .. } => {
|
||||
Some(Cow::Owned(VoiceInformation::default()))
|
||||
}
|
||||
Self::TextChannel {
|
||||
voice: Some(voice), ..
|
||||
} => Some(Cow::Borrowed(voice)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,12 +465,6 @@ impl Channel {
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
role_permissions,
|
||||
..
|
||||
} => {
|
||||
db.set_channel_role_permission(id, role_id, permissions)
|
||||
.await?;
|
||||
@@ -502,7 +511,7 @@ impl Channel {
|
||||
clear: remove.into_iter().map(|v| v.into()).collect(),
|
||||
}
|
||||
.p(match self {
|
||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
|
||||
Self::TextChannel { server, .. } => server.clone(),
|
||||
_ => id,
|
||||
})
|
||||
.await;
|
||||
@@ -514,17 +523,13 @@ impl Channel {
|
||||
pub fn remove_field(&mut self, field: &FieldsChannel) {
|
||||
match field {
|
||||
FieldsChannel::Description => match self {
|
||||
Self::Group { description, .. }
|
||||
| Self::TextChannel { description, .. }
|
||||
| Self::VoiceChannel { description, .. } => {
|
||||
Self::Group { description, .. } | Self::TextChannel { description, .. } => {
|
||||
description.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Icon => match self {
|
||||
Self::Group { icon, .. }
|
||||
| Self::TextChannel { icon, .. }
|
||||
| Self::VoiceChannel { icon, .. } => {
|
||||
Self::Group { icon, .. } | Self::TextChannel { icon, .. } => {
|
||||
icon.take();
|
||||
}
|
||||
_ => {}
|
||||
@@ -533,15 +538,17 @@ impl Channel {
|
||||
Self::TextChannel {
|
||||
default_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
default_permissions,
|
||||
..
|
||||
} => {
|
||||
default_permissions.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
FieldsChannel::Voice => match self {
|
||||
Self::TextChannel { voice, .. } => {
|
||||
voice.take();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,6 +560,7 @@ impl Channel {
|
||||
}
|
||||
|
||||
/// Apply partial channel to channel
|
||||
#[allow(deprecated)]
|
||||
pub fn apply_options(&mut self, partial: PartialChannel) {
|
||||
match self {
|
||||
Self::SavedMessages { .. } => {}
|
||||
@@ -601,15 +609,7 @@ impl Channel {
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
..
|
||||
}
|
||||
| Self::VoiceChannel {
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
nsfw,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
voice,
|
||||
..
|
||||
} => {
|
||||
if let Some(v) = partial.name {
|
||||
@@ -635,12 +635,16 @@ impl Channel {
|
||||
if let Some(v) = partial.default_permissions {
|
||||
default_permissions.replace(v);
|
||||
}
|
||||
|
||||
if let Some(v) = partial.voice {
|
||||
voice.replace(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
@@ -649,17 +653,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
|
||||
@@ -777,6 +771,7 @@ impl IntoDocumentPath for FieldsChannel {
|
||||
FieldsChannel::Description => "description",
|
||||
FieldsChannel::Icon => "icon",
|
||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
||||
FieldsChannel::Voice => "voice",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ impl AbstractChannels for MongoDb {
|
||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||
let id = channel.id().to_string();
|
||||
let server_id = match channel {
|
||||
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||
Channel::TextChannel { server, .. } => {
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
|
||||
@@ -94,9 +94,6 @@ impl AbstractChannels for ReferenceDb {
|
||||
match &mut channel {
|
||||
Channel::TextChannel {
|
||||
role_permissions, ..
|
||||
}
|
||||
| Channel::VoiceChannel {
|
||||
role_permissions, ..
|
||||
} => {
|
||||
if role_permissions.get(role_id).is_some() {
|
||||
role_permissions.remove(role_id);
|
||||
|
||||
@@ -2,6 +2,7 @@ use 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
@@ -45,7 +45,8 @@ auto_derived!(
|
||||
Image {
|
||||
width: isize,
|
||||
height: isize,
|
||||
// animated: bool // TODO: https://docs.rs/image/latest/image/trait.AnimationDecoder.html for APNG support
|
||||
thumbhash: Option<Vec<u8>>,
|
||||
animated: Option<bool>,
|
||||
},
|
||||
/// File is a video with specific dimensions
|
||||
Video { width: isize, height: isize },
|
||||
|
||||
@@ -17,6 +17,12 @@ pub trait AbstractAttachmentHashes: Sync + Send {
|
||||
/// Update an attachment hash nonce value.
|
||||
async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()>;
|
||||
|
||||
/// Updates the attachments animated metadata value.
|
||||
///
|
||||
/// The primary use for this is to update the metadata for existing uploaded files, this
|
||||
/// can only be used for images.
|
||||
async fn set_attachment_hash_animated(&self, hash: &str, animated: bool) -> Result<()>;
|
||||
|
||||
/// Delete attachment hash by id.
|
||||
async fn delete_attachment_hash(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,29 @@ impl AbstractAttachmentHashes for MongoDb {
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Updates the attachments animated metadata value.
|
||||
///
|
||||
/// The primary use for this is to update the metadata for existing uploaded files, this
|
||||
/// can only be used for images.
|
||||
async fn set_attachment_hash_animated(&self, hash: &str, animated: bool) -> Result<()> {
|
||||
self.col::<FileHash>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": hash,
|
||||
"metadata.type": "Image",
|
||||
"metadata.animated": { "$exists": false },
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"metadata.animated": animated
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Delete attachment hash by id.
|
||||
async fn delete_attachment_hash(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::FileHash;
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FileHash, Metadata, ReferenceDb};
|
||||
|
||||
use super::AbstractAttachmentHashes;
|
||||
|
||||
@@ -39,6 +38,29 @@ impl AbstractAttachmentHashes for ReferenceDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the attachments animated metadata value.
|
||||
///
|
||||
/// The primary use for this is to update the metadata for existing uploaded files, this
|
||||
/// can only be used for images.
|
||||
async fn set_attachment_hash_animated(&self, hash: &str, animated: bool) -> Result<()> {
|
||||
let mut hashes = self.file_hashes.lock().await;
|
||||
if let Some(FileHash {
|
||||
metadata:
|
||||
Metadata::Image {
|
||||
animated: Some(animated_metadata),
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = hashes.get_mut(hash)
|
||||
{
|
||||
*animated_metadata = animated;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete attachment hash by id.
|
||||
async fn delete_attachment_hash(&self, id: &str) -> Result<()> {
|
||||
let mut file_hashes = self.file_hashes.lock().await;
|
||||
|
||||
@@ -70,6 +70,7 @@ auto_derived!(
|
||||
LegacyGroupIcon,
|
||||
ChannelIcon,
|
||||
ServerIcon,
|
||||
RoleIcon,
|
||||
}
|
||||
|
||||
/// Information about what the file was used for
|
||||
@@ -239,4 +240,23 @@ impl File {
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a role icon
|
||||
pub async fn use_role_icon(
|
||||
db: &Database,
|
||||
id: &str,
|
||||
parent: &str,
|
||||
uploader_id: &str,
|
||||
) -> Result<File> {
|
||||
db.find_and_use_attachment(
|
||||
id,
|
||||
"icons",
|
||||
FileUsedFor {
|
||||
id: parent.to_owned(),
|
||||
object_type: FileUsedForType::RoleIcon,
|
||||
},
|
||||
uploader_id.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::{collections::HashSet, hash::RandomState};
|
||||
|
||||
use indexmap::{IndexMap, IndexSet};
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
@@ -9,6 +7,8 @@ use revolt_models::v0::{
|
||||
};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
|
||||
use revolt_result::{ErrorType, Result};
|
||||
use std::time::SystemTime;
|
||||
use std::{collections::HashSet, hash::RandomState};
|
||||
use ulid::Ulid;
|
||||
use validator::Validate;
|
||||
|
||||
@@ -114,6 +114,11 @@ auto_derived!(
|
||||
MessagePinned { id: String, by: String },
|
||||
#[serde(rename = "message_unpinned")]
|
||||
MessageUnpinned { id: String, by: String },
|
||||
#[serde(rename = "call_started")]
|
||||
CallStarted {
|
||||
by: String,
|
||||
finished_at: Option<Timestamp>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Name and / or avatar override information
|
||||
@@ -326,9 +331,7 @@ impl Message {
|
||||
}
|
||||
|
||||
let server_id = match channel {
|
||||
Channel::TextChannel { ref server, .. } | Channel::VoiceChannel { ref server, .. } => {
|
||||
Some(server.clone())
|
||||
}
|
||||
Channel::TextChannel { ref server, .. } => Some(server.clone()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -385,6 +388,7 @@ impl Message {
|
||||
mut role_mentions,
|
||||
mut mentions_everyone,
|
||||
mut mentions_online,
|
||||
..
|
||||
} = message_mentions;
|
||||
|
||||
if allow_mass_mentions && server_id.is_some() && !role_mentions.is_empty() {
|
||||
@@ -440,7 +444,8 @@ impl Message {
|
||||
}
|
||||
|
||||
// Verify replies are valid.
|
||||
let mut replies = HashSet::new();
|
||||
let mut replies = Vec::new();
|
||||
|
||||
if let Some(entries) = data.replies {
|
||||
if entries.len() > config.features.limits.global.message_replies {
|
||||
return Err(create_error!(TooManyReplies {
|
||||
@@ -448,6 +453,8 @@ impl Message {
|
||||
}));
|
||||
}
|
||||
|
||||
replies.reserve(entries.len());
|
||||
|
||||
for ReplyIntent {
|
||||
id,
|
||||
mention,
|
||||
@@ -461,7 +468,12 @@ impl Message {
|
||||
user_mentions.insert(message.author.to_owned());
|
||||
}
|
||||
|
||||
replies.insert(message.id);
|
||||
// This is O(n^2), but this is faster than a HashSet
|
||||
// when n < 20; as long as the message_replies limit
|
||||
// is reasonable, this will be fast.
|
||||
if !replies.contains(&message.id) {
|
||||
replies.push(message.id);
|
||||
}
|
||||
}
|
||||
// If the referenced message doesn't exist and fail_if_not_exists
|
||||
// is set to false, send the message without the reply.
|
||||
@@ -478,6 +490,7 @@ impl Message {
|
||||
|
||||
// Validate the mentions go to users in the channel/server
|
||||
if !user_mentions.is_empty() {
|
||||
#[allow(deprecated)]
|
||||
match channel {
|
||||
Channel::DirectMessage { ref recipients, .. }
|
||||
| Channel::Group { ref recipients, .. } => {
|
||||
@@ -485,8 +498,7 @@ impl Message {
|
||||
user_mentions.retain(|m| recipients_hash.contains(m));
|
||||
role_mentions.clear();
|
||||
}
|
||||
Channel::TextChannel { ref server, .. }
|
||||
| Channel::VoiceChannel { ref server, .. } => {
|
||||
Channel::TextChannel { ref server, .. } => {
|
||||
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned());
|
||||
|
||||
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
|
||||
@@ -534,9 +546,7 @@ impl Message {
|
||||
}
|
||||
|
||||
if !replies.is_empty() {
|
||||
message
|
||||
.replies
|
||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
||||
message.replies.replace(replies);
|
||||
}
|
||||
|
||||
// Calculate final message flags
|
||||
@@ -678,9 +688,13 @@ impl Message {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let is_dm_or_group = matches!(
|
||||
channel,
|
||||
Channel::DirectMessage { .. } | Channel::Group { .. }
|
||||
);
|
||||
|
||||
if !self.has_suppressed_notifications()
|
||||
&& (self.mentions.is_some() || self.contains_mass_push_mention())
|
||||
&& (is_dm_or_group || self.mentions.is_some() || self.contains_mass_push_mention())
|
||||
{
|
||||
// send Push notifications
|
||||
#[cfg(feature = "tasks")]
|
||||
@@ -691,7 +705,7 @@ impl Message {
|
||||
Some(
|
||||
PushNotification::from(
|
||||
self.clone().into_model(user, member),
|
||||
Some(author),
|
||||
Some(author.clone()),
|
||||
channel.to_owned().into(),
|
||||
)
|
||||
.await,
|
||||
@@ -699,7 +713,11 @@ impl Message {
|
||||
self.clone(),
|
||||
match channel {
|
||||
Channel::DirectMessage { recipients, .. }
|
||||
| Channel::Group { recipients, .. } => recipients.clone(),
|
||||
| Channel::Group { recipients, .. } => recipients
|
||||
.iter()
|
||||
.filter(|uid| *uid != author.id())
|
||||
.cloned()
|
||||
.collect(),
|
||||
Channel::TextChannel { .. } => {
|
||||
self.mentions.clone().unwrap_or_default()
|
||||
}
|
||||
@@ -794,7 +812,7 @@ impl Message {
|
||||
query: MessageQuery,
|
||||
perspective: &User,
|
||||
include_users: Option<bool>,
|
||||
server_id: Option<String>,
|
||||
server_id: Option<&str>,
|
||||
) -> Result<BulkMessageResponse> {
|
||||
let messages: Vec<v0::Message> = db
|
||||
.fetch_messages(query)
|
||||
@@ -837,6 +855,7 @@ impl Message {
|
||||
v0::SystemMessage::MessageUnpinned { by, .. } => {
|
||||
users.push(by.clone());
|
||||
}
|
||||
v0::SystemMessage::CallStarted { by, .. } => users.push(by.clone()),
|
||||
}
|
||||
}
|
||||
users
|
||||
@@ -851,7 +870,7 @@ impl Message {
|
||||
users,
|
||||
members: if let Some(server_id) = server_id {
|
||||
Some(
|
||||
db.fetch_members(&server_id, &user_ids)
|
||||
db.fetch_members(server_id, &user_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
@@ -1020,6 +1039,31 @@ impl Message {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bulk delete messages by an author since a given time
|
||||
pub async fn bulk_delete_by_author_since(
|
||||
db: &Database,
|
||||
channels: &[String],
|
||||
author: &str,
|
||||
since: SystemTime,
|
||||
) -> Result<()> {
|
||||
let deleted_groups = db
|
||||
.delete_messages_by_author_since(channels, author, since)
|
||||
.await?;
|
||||
|
||||
for (channel_id, message_ids) in deleted_groups {
|
||||
if !message_ids.is_empty() {
|
||||
EventV1::BulkMessageDelete {
|
||||
channel: channel_id.clone(),
|
||||
ids: message_ids,
|
||||
}
|
||||
.p(channel_id)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a reaction from a message
|
||||
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
|
||||
// Check if it actually exists
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||
@@ -40,4 +42,12 @@ pub trait AbstractMessages: Sync + Send {
|
||||
|
||||
/// Delete messages from a channel by their ids and corresponding channel id
|
||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>;
|
||||
|
||||
/// Delete all messages from a specific author in a server from a certain ULID onwards
|
||||
async fn delete_messages_by_author_since(
|
||||
&self,
|
||||
channels: &[String],
|
||||
author: &str,
|
||||
since: SystemTime
|
||||
) -> Result<HashMap<String, Vec<String>>>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use bson::{to_bson, Document};
|
||||
use futures::try_join;
|
||||
use futures::StreamExt;
|
||||
use mongodb::options::FindOptions;
|
||||
use revolt_models::v0::MessageSort;
|
||||
use revolt_result::Result;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::SystemTime;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{
|
||||
AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath, Message, MessageQuery,
|
||||
@@ -306,6 +310,112 @@ impl AbstractMessages for MongoDb {
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("delete_many", COL))
|
||||
}
|
||||
|
||||
/// Delete all messages from a specific author in a server from a certain ULID onwards
|
||||
async fn delete_messages_by_author_since(
|
||||
&self,
|
||||
channels: &[String],
|
||||
author: &str,
|
||||
since: SystemTime,
|
||||
) -> Result<HashMap<String, Vec<String>>> {
|
||||
let threshold_ulid = Ulid::from_datetime(since).to_string();
|
||||
|
||||
let filter = doc! {
|
||||
"author": author,
|
||||
"channel": { "$in": channels },
|
||||
"_id": { "$gte": &threshold_ulid }
|
||||
};
|
||||
|
||||
let pipeline = vec![
|
||||
doc! { "$match": filter.clone() },
|
||||
doc! {
|
||||
"$project": {
|
||||
"channel": 1_i32,
|
||||
"message_id": "$_id",
|
||||
"attachment_ids": {
|
||||
"$map": {
|
||||
"input": { "$ifNull": ["$attachments", Vec::<bson::Bson>::new()] },
|
||||
"as": "a",
|
||||
"in": "$$a._id"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$group": {
|
||||
"_id": "$channel",
|
||||
"message_ids": { "$push": "$message_id" },
|
||||
"attachment_ids_nested": { "$push": "$attachment_ids" }
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$project": {
|
||||
"message_ids": 1_i32,
|
||||
"attachment_ids": {
|
||||
"$reduce": {
|
||||
"input": "$attachment_ids_nested",
|
||||
"initialValue": Vec::<bson::Bson>::new(),
|
||||
"in": { "$setUnion": ["$$value", "$$this"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct AggregatedChannel {
|
||||
#[serde(rename = "_id")]
|
||||
channel: String,
|
||||
message_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
attachment_ids: Vec<String>,
|
||||
}
|
||||
|
||||
let mut cursor = self
|
||||
.col::<Document>(COL)
|
||||
.aggregate(pipeline)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("aggregate", COL))?
|
||||
.with_type::<AggregatedChannel>();
|
||||
|
||||
let mut deleted_messages: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut attachment_ids: HashSet<String> = HashSet::new();
|
||||
|
||||
while let Some(result) = cursor.next().await {
|
||||
if let Ok(item) = result {
|
||||
for id in item.attachment_ids {
|
||||
attachment_ids.insert(id);
|
||||
}
|
||||
deleted_messages.insert(item.channel, item.message_ids);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark attachments as deleted before deleting messages
|
||||
if !attachment_ids.is_empty() {
|
||||
self.col::<Document>("attachments")
|
||||
.update_many(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": attachment_ids.into_iter().collect::<Vec<String>>()
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_many", "attachments"))?;
|
||||
}
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(filter)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("delete_many", COL))?;
|
||||
|
||||
Ok(deleted_messages)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsMessage {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use futures::future::try_join_all;
|
||||
use indexmap::IndexSet;
|
||||
use revolt_result::Result;
|
||||
|
||||
use std::time::SystemTime;
|
||||
use ulid::Ulid;
|
||||
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
||||
|
||||
use super::AbstractMessages;
|
||||
@@ -247,7 +249,7 @@ impl AbstractMessages for ReferenceDb {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message) = messages.get_mut(id) {
|
||||
if let Some(users) = message.reactions.get_mut(emoji) {
|
||||
users.remove(&user.to_string());
|
||||
users.swap_remove(&user.to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -260,7 +262,7 @@ impl AbstractMessages for ReferenceDb {
|
||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
||||
let mut messages = self.messages.lock().await;
|
||||
if let Some(message) = messages.get_mut(id) {
|
||||
message.reactions.remove(emoji);
|
||||
message.reactions.swap_remove(emoji);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
@@ -286,4 +288,63 @@ impl AbstractMessages for ReferenceDb {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all messages from a specific author in a list of channels from a certain ULID onwards
|
||||
async fn delete_messages_by_author_since(
|
||||
&self,
|
||||
channels: &[String],
|
||||
author: &str,
|
||||
since: SystemTime
|
||||
) -> Result<HashMap<String, Vec<String>>> {
|
||||
let threshold_ulid = Ulid::from_datetime(since).to_string();
|
||||
let mut deleted_messages: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut attachment_ids: Vec<String> = Vec::new();
|
||||
|
||||
let messages = self.messages.lock().await;
|
||||
|
||||
// First pass: collect attachment IDs and message IDs to delete
|
||||
for (id, message) in messages.iter() {
|
||||
let should_delete = message.author == author
|
||||
&& channels.contains(&message.channel)
|
||||
&& id.as_str() >= threshold_ulid.as_str();
|
||||
|
||||
if should_delete {
|
||||
// Collect attachment IDs
|
||||
if let Some(attachments) = &message.attachments {
|
||||
for attachment in attachments {
|
||||
attachment_ids.push(attachment.id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
deleted_messages
|
||||
.entry(message.channel.clone())
|
||||
.or_default()
|
||||
.push(id.clone());
|
||||
}
|
||||
}
|
||||
drop(messages);
|
||||
|
||||
// Mark attachments as deleted
|
||||
if !attachment_ids.is_empty() {
|
||||
let mut files = self.files.lock().await;
|
||||
for attachment_id in attachment_ids {
|
||||
if let Some(file) = files.get_mut(&attachment_id) {
|
||||
file.deleted = Some(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the messages
|
||||
self.messages
|
||||
.lock()
|
||||
.await
|
||||
.retain(|id, message| {
|
||||
let should_keep = !(message.author == author
|
||||
&& channels.contains(&message.channel)
|
||||
&& id.as_str() >= threshold_ulid.as_str());
|
||||
should_keep
|
||||
});
|
||||
|
||||
Ok(deleted_messages)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,14 @@ use crate::{
|
||||
Server, SystemMessage, User,
|
||||
};
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_true(x: &bool) -> bool {
|
||||
*x
|
||||
}
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server Member
|
||||
pub struct Member {
|
||||
@@ -30,6 +38,16 @@ auto_derived_partial!(
|
||||
/// Timestamp this member is timed out until
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeout: Option<Timestamp>,
|
||||
|
||||
/// Whether the member is server-wide voice muted
|
||||
#[serde(skip_serializing_if = "is_true", default = "default_true")]
|
||||
pub can_publish: bool,
|
||||
/// Whether the member is server-wide voice deafened
|
||||
#[serde(skip_serializing_if = "is_true", default = "default_true")]
|
||||
pub can_receive: bool,
|
||||
// This value only exists in the database, not the models.
|
||||
// If it is not-None, the database layer should return None to member fetching queries.
|
||||
// pub pending_deletion_at: Option<Timestamp>
|
||||
},
|
||||
"PartialMember"
|
||||
);
|
||||
@@ -50,6 +68,10 @@ auto_derived!(
|
||||
Avatar,
|
||||
Roles,
|
||||
Timeout,
|
||||
CanReceive,
|
||||
CanPublish,
|
||||
JoinedAt,
|
||||
VoiceChannel,
|
||||
}
|
||||
|
||||
/// Member removal intention
|
||||
@@ -69,6 +91,8 @@ impl Default for Member {
|
||||
avatar: None,
|
||||
roles: vec![],
|
||||
timeout: None,
|
||||
can_publish: true,
|
||||
can_receive: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +114,7 @@ impl Member {
|
||||
return Err(create_error!(AlreadyInServer));
|
||||
}
|
||||
|
||||
let member = Member {
|
||||
let mut member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: server.id.to_string(),
|
||||
user: user.id.to_string(),
|
||||
@@ -98,7 +122,9 @@ impl Member {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_member(&member).await?;
|
||||
if let Some(updated) = db.insert_or_merge_member(&member).await? {
|
||||
member = updated;
|
||||
}
|
||||
|
||||
let should_fetch = channels.is_none();
|
||||
let mut channels = channels.unwrap_or_default();
|
||||
@@ -121,9 +147,24 @@ impl Member {
|
||||
|
||||
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut voice_states = Vec::new();
|
||||
|
||||
#[cfg(feature = "voice")]
|
||||
for channel in &channels {
|
||||
if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(
|
||||
&crate::voice::UserVoiceChannel::from_channel(channel),
|
||||
)
|
||||
.await
|
||||
{
|
||||
voice_states.push(voice_state)
|
||||
}
|
||||
}
|
||||
|
||||
EventV1::ServerMemberJoin {
|
||||
id: server.id.clone(),
|
||||
user: user.id.clone(),
|
||||
member: member.clone().into(),
|
||||
}
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
@@ -137,6 +178,7 @@ impl Member {
|
||||
.map(|channel| channel.into())
|
||||
.collect(),
|
||||
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
|
||||
voice_states,
|
||||
}
|
||||
.private(user.id.clone())
|
||||
.await;
|
||||
@@ -186,10 +228,14 @@ impl Member {
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsMember) {
|
||||
match field {
|
||||
FieldsMember::JoinedAt => {}
|
||||
FieldsMember::Avatar => self.avatar = None,
|
||||
FieldsMember::Nickname => self.nickname = None,
|
||||
FieldsMember::Roles => self.roles.clear(),
|
||||
FieldsMember::Timeout => self.timeout = None,
|
||||
FieldsMember::CanReceive => self.can_receive = true,
|
||||
FieldsMember::CanPublish => self.can_publish = true,
|
||||
FieldsMember::VoiceChannel => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +270,7 @@ impl Member {
|
||||
intention: RemovalIntention,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
db.delete_member(&self.id).await?;
|
||||
db.soft_delete_member(&self.id).await?;
|
||||
|
||||
EventV1::ServerMemberLeave {
|
||||
id: self.id.server.to_string(),
|
||||
@@ -260,3 +306,74 @@ impl Member {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use iso8601_timestamp::{Duration, Timestamp};
|
||||
use revolt_models::v0::DataCreateServer;
|
||||
|
||||
use crate::{Member, PartialMember, RemovalIntention, Server, User};
|
||||
|
||||
#[async_std::test]
|
||||
async fn muted_member_rejoin() {
|
||||
database_test!(|db| async move {
|
||||
match db {
|
||||
crate::Database::Reference(_) => return,
|
||||
crate::Database::MongoDb(_) => (),
|
||||
}
|
||||
let owner = User::create(&db, "Server Owner".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let kickable_user = User::create(&db, "Member".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let server = Server::create(
|
||||
&db,
|
||||
DataCreateServer {
|
||||
name: "Server".to_string(),
|
||||
description: None,
|
||||
nsfw: None,
|
||||
},
|
||||
&owner,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
Member::create(&db, &server, &owner, None).await.unwrap();
|
||||
let mut kickable_member = Member::create(&db, &server, &kickable_user, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
kickable_member
|
||||
.update(
|
||||
&db,
|
||||
PartialMember {
|
||||
timeout: Some(Timestamp::now_utc() + Duration::minutes(5)),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(kickable_member.in_timeout());
|
||||
|
||||
kickable_member
|
||||
.remove(&db, &server, RemovalIntention::Kick, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let kickable_member = Member::create(&db, &server, &kickable_user, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
assert!(kickable_member.in_timeout())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +73,13 @@ impl ChunkedServerMembersGenerator {
|
||||
#[async_trait]
|
||||
pub trait AbstractServerMembers: Sync + Send {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()>;
|
||||
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>>;
|
||||
|
||||
/// Fetch a server member by their id
|
||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
|
||||
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch all members in a server as an iterator
|
||||
async fn fetch_all_members_chunked(
|
||||
@@ -100,10 +100,10 @@ pub trait AbstractServerMembers: Sync + Send {
|
||||
) -> Result<ChunkedServerMembersGenerator>;
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>;
|
||||
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch member count of a server
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
|
||||
@@ -119,6 +119,14 @@ pub trait AbstractServerMembers: Sync + Send {
|
||||
remove: Vec<FieldsMember>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
/// Marks a user as no longer a member of a server, while retaining the database value.
|
||||
/// This is used to keep information such as timeouts in place, but will remove information such as join date and applied roles.
|
||||
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
|
||||
/// Forcibly delete a server member by their id.
|
||||
/// This will cancel any pending timeouts or other longer term actions, and they will not be reapplied on rejoin.
|
||||
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
|
||||
/// Fetch all members who have been marked for deletion.
|
||||
async fn remove_dangling_members(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use bson::Document;
|
||||
use futures::StreamExt;
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use mongodb::options::ReadConcern;
|
||||
use revolt_result::Result;
|
||||
|
||||
@@ -11,9 +13,42 @@ static COL: &str = "server_members";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractServerMembers for MongoDb {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
||||
query!(self, insert_one, COL, &member).map(|_| ())
|
||||
/// Insert a new server member (or use the existing member if one is found)
|
||||
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
|
||||
let existing: Result<Option<Document>> = query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.server": &member.id.server,
|
||||
"_id.user": &member.id.user,
|
||||
"pending_deletion_at": {"$exists": true}
|
||||
}
|
||||
);
|
||||
// Update the existing record if it exist, otherwise make a new record
|
||||
if existing.is_ok_and(|x| x.is_some()) {
|
||||
self.col::<Member>(COL)
|
||||
.find_one_and_update(
|
||||
doc! {
|
||||
"_id.server": &member.id.server,
|
||||
"_id.user": &member.id.user,
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"joined_at": member.joined_at.duration_since(Timestamp::UNIX_EPOCH).whole_seconds(),
|
||||
},
|
||||
"$unset": {
|
||||
"pending_deletion_at": ""
|
||||
}
|
||||
},
|
||||
)
|
||||
.return_document(mongodb::options::ReturnDocument::After)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
} else {
|
||||
query!(self, insert_one, COL, &member).map(|_| ())?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a server member by their id
|
||||
@@ -24,18 +59,20 @@ impl AbstractServerMembers for MongoDb {
|
||||
COL,
|
||||
doc! {
|
||||
"_id.server": server_id,
|
||||
"_id.user": user_id
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.server": server_id
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
@@ -139,11 +176,12 @@ impl AbstractServerMembers for MongoDb {
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.user": user_id
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
@@ -159,11 +197,12 @@ impl AbstractServerMembers for MongoDb {
|
||||
}
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
||||
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(doc! {
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false},
|
||||
"_id.user": {
|
||||
"$in": ids
|
||||
}
|
||||
@@ -185,7 +224,8 @@ impl AbstractServerMembers for MongoDb {
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
|
||||
self.col::<Member>(COL)
|
||||
.count_documents(doc! {
|
||||
"_id.server": server_id
|
||||
"_id.server": server_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map(|c| c as usize)
|
||||
@@ -196,7 +236,8 @@ impl AbstractServerMembers for MongoDb {
|
||||
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
|
||||
self.col::<Member>(COL)
|
||||
.count_documents(doc! {
|
||||
"_id.user": user_id
|
||||
"_id.user": user_id,
|
||||
"pending_deletion_at": {"$exists": false}
|
||||
})
|
||||
.await
|
||||
.map(|c| c as usize)
|
||||
@@ -225,8 +266,42 @@ impl AbstractServerMembers for MongoDb {
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Marks a member for deletion.
|
||||
/// This will remove the record if the user has no pending actions (eg. timeout),
|
||||
/// otherwise will slate the record for deletion by revolt_crond once the actions expire.
|
||||
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
let member = self.fetch_member(&id.server, &id.user).await;
|
||||
if let Ok(member) = member {
|
||||
if member.in_timeout() {
|
||||
self.col::<Document>(COL)
|
||||
.update_many(
|
||||
doc! {
|
||||
"_id.server": &id.server,
|
||||
"_id.user": &id.user,
|
||||
},
|
||||
doc! {
|
||||
"$set": {"pending_deletion_at": format!("{}", member.timeout.unwrap().format())},
|
||||
"$unset": {
|
||||
"joined_at": "",
|
||||
"avatar": "",
|
||||
"nickname": "",
|
||||
"roles": ""
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_many", COL))
|
||||
} else {
|
||||
self.force_delete_member(id).await
|
||||
}
|
||||
} else {
|
||||
Err(create_database_error!("fetch_member", COL))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
delete_one,
|
||||
@@ -238,15 +313,32 @@ impl AbstractServerMembers for MongoDb {
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
async fn remove_dangling_members(&self) -> Result<()> {
|
||||
let now = Timestamp::now_utc();
|
||||
let date = bson::to_bson(&now).expect("Failed to serialize timestamp");
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.delete_many(doc! {
|
||||
"pending_deletion_at": {"$lt": date}
|
||||
})
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("count_documents", COL))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsMember {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsMember::Avatar => "avatar",
|
||||
FieldsMember::Nickname => "nickname",
|
||||
FieldsMember::Roles => "roles",
|
||||
FieldsMember::Timeout => "timeout",
|
||||
})
|
||||
match self {
|
||||
FieldsMember::JoinedAt => Some("joined_at"),
|
||||
FieldsMember::Avatar => Some("avatar"),
|
||||
FieldsMember::Nickname => Some("nickname"),
|
||||
FieldsMember::Roles => Some("roles"),
|
||||
FieldsMember::Timeout => Some("timeout"),
|
||||
FieldsMember::CanPublish => Some("can_publish"),
|
||||
FieldsMember::CanReceive => Some("can_receive"),
|
||||
FieldsMember::VoiceChannel => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ use super::{AbstractServerMembers, ChunkedServerMembersGenerator};
|
||||
#[async_trait]
|
||||
impl AbstractServerMembers for ReferenceDb {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
||||
async fn insert_or_merge_member(&self, member: &Member) -> Result<Option<Member>> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
if server_members.contains_key(&member.id) {
|
||||
Err(create_database_error!("insert", "member"))
|
||||
} else {
|
||||
server_members.insert(member.id.clone(), member.clone());
|
||||
Ok(())
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_members(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
@@ -105,7 +105,7 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
async fn fetch_all_memberships(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
@@ -115,7 +115,7 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
||||
async fn fetch_members(&self, server_id: &str, ids: &[String]) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(ids
|
||||
.iter()
|
||||
@@ -169,8 +169,26 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
}
|
||||
}
|
||||
|
||||
/// Soft delete a member
|
||||
async fn soft_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
|
||||
let member = server_members.get_mut(id);
|
||||
if let Some(member) = member {
|
||||
if member.in_timeout() {
|
||||
panic!("Soft deletion is not implemented.")
|
||||
} else if server_members.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
async fn force_delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
if server_members.remove(id).is_some() {
|
||||
Ok(())
|
||||
@@ -178,4 +196,8 @@ impl AbstractServerMembers for ReferenceDb {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
async fn remove_dangling_members(&self) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,9 @@ auto_derived_partial!(
|
||||
auto_derived_partial!(
|
||||
/// Role
|
||||
pub struct Role {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Role name
|
||||
pub name: String,
|
||||
/// Permissions available to this role
|
||||
@@ -83,6 +86,9 @@ auto_derived_partial!(
|
||||
/// Ranking of this role
|
||||
#[serde(default)]
|
||||
pub rank: i64,
|
||||
/// Custom icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<File>,
|
||||
},
|
||||
"PartialRole"
|
||||
);
|
||||
@@ -126,6 +132,7 @@ auto_derived!(
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsRole {
|
||||
Colour,
|
||||
Icon,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -246,7 +253,6 @@ impl Server {
|
||||
role.update(
|
||||
db,
|
||||
&self.id,
|
||||
role_id,
|
||||
PartialRole {
|
||||
permissions: Some(permissions),
|
||||
..Default::default()
|
||||
@@ -297,29 +303,41 @@ impl Role {
|
||||
/// Into optional struct
|
||||
pub fn into_optional(self) -> PartialRole {
|
||||
PartialRole {
|
||||
id: Some(self.id),
|
||||
name: Some(self.name),
|
||||
permissions: Some(self.permissions),
|
||||
colour: self.colour,
|
||||
hoist: Some(self.hoist),
|
||||
rank: Some(self.rank),
|
||||
icon: self.icon,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a role
|
||||
pub async fn create(&self, db: &Database, server_id: &str) -> Result<String> {
|
||||
let role_id = Ulid::new().to_string();
|
||||
db.insert_role(server_id, &role_id, self).await?;
|
||||
pub async fn create(db: &Database, server: &Server, name: String) -> Result<Self> {
|
||||
let role = Role {
|
||||
id: Ulid::new().to_string(),
|
||||
name,
|
||||
// Rank of the new role should be below the lowest role
|
||||
rank: server.roles.len() as i64,
|
||||
colour: None,
|
||||
hoist: false,
|
||||
permissions: Default::default(),
|
||||
icon: None,
|
||||
};
|
||||
|
||||
db.insert_role(&server.id, &role).await?;
|
||||
|
||||
EventV1::ServerRoleUpdate {
|
||||
id: server_id.to_string(),
|
||||
role_id: role_id.to_string(),
|
||||
data: self.clone().into_optional().into(),
|
||||
id: server.id.clone(),
|
||||
role_id: role.id.clone(),
|
||||
data: role.clone().into_optional().into(),
|
||||
clear: vec![],
|
||||
}
|
||||
.p(server_id.to_string())
|
||||
.p(server.id.clone())
|
||||
.await;
|
||||
|
||||
Ok(role_id)
|
||||
Ok(role)
|
||||
}
|
||||
|
||||
/// Update server data
|
||||
@@ -327,7 +345,6 @@ impl Role {
|
||||
&mut self,
|
||||
db: &Database,
|
||||
server_id: &str,
|
||||
role_id: &str,
|
||||
partial: PartialRole,
|
||||
remove: Vec<FieldsRole>,
|
||||
) -> Result<()> {
|
||||
@@ -337,14 +354,14 @@ impl Role {
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_role(server_id, role_id, &partial, remove.clone())
|
||||
db.update_role(server_id, &self.id, &partial, remove.clone())
|
||||
.await?;
|
||||
|
||||
EventV1::ServerRoleUpdate {
|
||||
id: server_id.to_string(),
|
||||
role_id: role_id.to_string(),
|
||||
role_id: self.id.clone(),
|
||||
data: partial.into(),
|
||||
clear: vec![],
|
||||
clear: remove.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
.p(server_id.to_string())
|
||||
.await;
|
||||
@@ -356,19 +373,20 @@ impl Role {
|
||||
pub fn remove_field(&mut self, field: &FieldsRole) {
|
||||
match field {
|
||||
FieldsRole::Colour => self.colour = None,
|
||||
FieldsRole::Icon => self.icon = None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a role
|
||||
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
|
||||
pub async fn delete(self, db: &Database, server_id: &str) -> Result<()> {
|
||||
EventV1::ServerRoleDelete {
|
||||
id: server_id.to_string(),
|
||||
role_id: role_id.to_string(),
|
||||
role_id: self.id.clone(),
|
||||
}
|
||||
.p(server_id.to_string())
|
||||
.await;
|
||||
|
||||
db.delete_role(server_id, role_id).await
|
||||
db.delete_role(server_id, &self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ pub trait AbstractServers: Sync + Send {
|
||||
async fn delete_server(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Insert a new role into server object
|
||||
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()>;
|
||||
async fn insert_role(&self, server_id: &str, role: &Role) -> Result<()>;
|
||||
|
||||
/// Update an existing role on a server
|
||||
async fn update_role(
|
||||
|
||||
@@ -69,7 +69,7 @@ impl AbstractServers for MongoDb {
|
||||
}
|
||||
|
||||
/// Insert a new role into server object
|
||||
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
|
||||
async fn insert_role(&self, server_id: &str, role: &Role) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
@@ -77,7 +77,7 @@ impl AbstractServers for MongoDb {
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"roles.".to_owned() + role_id: to_document(role)
|
||||
"roles.".to_owned() + role.id.as_str(): to_document(role)
|
||||
.map_err(|_| create_database_error!("to_document", "role"))?
|
||||
}
|
||||
},
|
||||
@@ -172,6 +172,7 @@ impl IntoDocumentPath for FieldsRole {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsRole::Colour => "colour",
|
||||
FieldsRole::Icon => "icon",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +72,10 @@ impl AbstractServers for ReferenceDb {
|
||||
}
|
||||
|
||||
/// Insert a new role into server object
|
||||
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
|
||||
async fn insert_role(&self, server_id: &str, role: &Role) -> Result<()> {
|
||||
let mut servers = self.servers.lock().await;
|
||||
if let Some(server) = servers.get_mut(server_id) {
|
||||
server.roles.insert(role_id.to_string(), role.clone());
|
||||
server.roles.insert(role.id.clone(), role.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||
use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts};
|
||||
|
||||
use revolt_result::{create_error, Error, Result};
|
||||
|
||||
use crate::{Database, User};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FromRequestParts<Database> for User {
|
||||
impl<S> FromRequestParts<S> for User
|
||||
where
|
||||
Database: FromRef<S>,
|
||||
S: Send + Sync
|
||||
{
|
||||
type Rejection = Error;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> {
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<User> {
|
||||
let db = Database::from_ref(state);
|
||||
|
||||
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
|
||||
let bot = db.fetch_bot_by_token(bot_token).await?;
|
||||
db.fetch_user(&bot.id).await
|
||||
|
||||
@@ -7,6 +7,7 @@ use futures::future::join_all;
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::seq::SliceRandom;
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use revolt_config::{config, FeaturesLimits};
|
||||
use revolt_models::v0::{self, UserBadges, UserFlags};
|
||||
use revolt_presence::filter_online;
|
||||
@@ -163,6 +164,13 @@ pub static DISCRIMINATOR_SEARCH_SPACE: Lazy<HashSet<String>> = Lazy::new(|| {
|
||||
set.into_iter().collect()
|
||||
});
|
||||
|
||||
static BLOCKED_USERNAME_PATTERNS: Lazy<Regex> = Lazy::new(|| {
|
||||
RegexBuilder::new("`{3}|(discord|rvlt|guilded|stt)\\.gg|(revolt|stoat)\\.chat|https?:\\/\\/")
|
||||
.case_insensitive(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for User {
|
||||
fn default() -> Self {
|
||||
@@ -198,11 +206,13 @@ impl User {
|
||||
I: Into<Option<String>>,
|
||||
D: Into<Option<PartialUser>>,
|
||||
{
|
||||
let username = User::validate_username(username)?;
|
||||
let new_username = User::sanitise_username(&username).await?;
|
||||
User::validate_username(&new_username)?;
|
||||
|
||||
let mut user = User {
|
||||
id: account_id.into().unwrap_or_else(|| Ulid::new().to_string()),
|
||||
discriminator: User::find_discriminator(db, &username, None).await?,
|
||||
username,
|
||||
discriminator: User::find_discriminator(db, &new_username, None).await?,
|
||||
username: new_username.clone(),
|
||||
last_acknowledged_policy_change: Timestamp::now_utc(),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -278,39 +288,40 @@ impl User {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitise and validate a username can be used
|
||||
pub fn validate_username(username: String) -> Result<String> {
|
||||
// Copy the username for validation
|
||||
/// Validate a username
|
||||
///
|
||||
/// This will check if the username is a blocked name or contains a blocked pattern.
|
||||
fn validate_username(username: &str) -> Result<()> {
|
||||
let username_lowercase = username.to_lowercase();
|
||||
|
||||
// Block homoglyphs
|
||||
if decancer::cure(&username_lowercase).into_str() != username_lowercase {
|
||||
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt", "stoat"];
|
||||
|
||||
if BLOCKED_USERNAMES.contains(&username_lowercase.as_str())
|
||||
|| BLOCKED_USERNAME_PATTERNS.is_match(username)
|
||||
{
|
||||
return Err(create_error!(InvalidUsername));
|
||||
}
|
||||
|
||||
// Ensure the username itself isn't blocked
|
||||
const BLOCKED_USERNAMES: &[&str] = &["admin", "revolt"];
|
||||
Ok(())
|
||||
}
|
||||
|
||||
for username in BLOCKED_USERNAMES {
|
||||
if username_lowercase == *username {
|
||||
return Err(create_error!(InvalidUsername));
|
||||
}
|
||||
}
|
||||
/// Sanitise a username
|
||||
///
|
||||
/// This will clean up Unicode homoglyphs and pad to the min username length with underscores.
|
||||
async fn sanitise_username(username: &str) -> Result<String> {
|
||||
let options = decancer::Options::default().retain_capitalization();
|
||||
let mut username = decancer::cure(username, options)
|
||||
.map_err(|_| create_error!(InvalidUsername))?
|
||||
.to_string();
|
||||
|
||||
// Ensure none of the following substrings show up in the username
|
||||
const BLOCKED_SUBSTRINGS: &[&str] = &[
|
||||
"```",
|
||||
"discord.gg",
|
||||
"rvlt.gg",
|
||||
"guilded.gg",
|
||||
"https://",
|
||||
"http://",
|
||||
];
|
||||
|
||||
for substr in BLOCKED_SUBSTRINGS {
|
||||
if username_lowercase.contains(substr) {
|
||||
return Err(create_error!(InvalidUsername));
|
||||
}
|
||||
let config = revolt_config::config().await;
|
||||
let username_length_diff = config
|
||||
.api
|
||||
.users
|
||||
.min_username_length
|
||||
.saturating_sub(username.len());
|
||||
if username_length_diff > 0 {
|
||||
username.push_str(&"_".repeat(username_length_diff))
|
||||
}
|
||||
|
||||
Ok(username)
|
||||
@@ -416,12 +427,14 @@ impl User {
|
||||
|
||||
/// Update a user's username
|
||||
pub async fn update_username(&mut self, db: &Database, username: String) -> Result<()> {
|
||||
let username = User::validate_username(username)?;
|
||||
if self.username.to_lowercase() == username.to_lowercase() {
|
||||
let new_username = User::sanitise_username(&username).await?;
|
||||
User::validate_username(&new_username)?;
|
||||
|
||||
if self.username.to_lowercase() == new_username.to_lowercase() {
|
||||
self.update(
|
||||
db,
|
||||
PartialUser {
|
||||
username: Some(username),
|
||||
username: Some(new_username),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
@@ -434,12 +447,12 @@ impl User {
|
||||
discriminator: Some(
|
||||
User::find_discriminator(
|
||||
db,
|
||||
&username,
|
||||
&new_username,
|
||||
Some((self.discriminator.to_string(), self.id.clone())),
|
||||
)
|
||||
.await?,
|
||||
),
|
||||
username: Some(username),
|
||||
username: Some(new_username),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
@@ -825,3 +838,109 @@ impl User {
|
||||
badges
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::User;
|
||||
|
||||
#[test]
|
||||
fn username_validation_blocked_names() {
|
||||
let username_admin = "Admin";
|
||||
let username_revolt = "Revolt";
|
||||
let username_stoat = "Stoat";
|
||||
let username_allowed = "Allowed";
|
||||
|
||||
assert!(User::validate_username(username_admin).is_err());
|
||||
assert!(User::validate_username(username_revolt).is_err());
|
||||
assert!(User::validate_username(username_stoat).is_err());
|
||||
assert!(User::validate_username(username_allowed).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn username_validation_blocked_patterns() {
|
||||
let username_grave = "```_test";
|
||||
let username_discord = "discord.gg_test";
|
||||
let username_rvlt = "rvlt.gg_test";
|
||||
let username_guilded = "guilded.gg_test";
|
||||
let username_stt = "stt.gg_test";
|
||||
let username_revolt = "revolt.chat_test";
|
||||
let username_stoat = "stoat.chat_test";
|
||||
let username_http = "http://_test";
|
||||
let username_https = "https://_test";
|
||||
|
||||
assert!(User::validate_username(username_grave).is_err());
|
||||
assert!(User::validate_username(username_discord).is_err());
|
||||
assert!(User::validate_username(username_rvlt).is_err());
|
||||
assert!(User::validate_username(username_guilded).is_err());
|
||||
assert!(User::validate_username(username_stt).is_err());
|
||||
assert!(User::validate_username(username_revolt).is_err());
|
||||
assert!(User::validate_username(username_stoat).is_err());
|
||||
assert!(User::validate_username(username_http).is_err());
|
||||
assert!(User::validate_username(username_https).is_err());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn username_sanitisation_clean() {
|
||||
let username_clean = "Test";
|
||||
|
||||
let username_clean_sanitised = User::sanitise_username(username_clean).await;
|
||||
|
||||
assert!(username_clean_sanitised.is_ok());
|
||||
assert_eq!(username_clean, username_clean_sanitised.unwrap());
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn username_sanitisation_homoglyphs() {
|
||||
let username_homoglyphs = "𝔽𝕌Ňℕy";
|
||||
|
||||
let username_homoglyphs_sanitised =
|
||||
User::sanitise_username(username_homoglyphs).await.unwrap();
|
||||
|
||||
assert_ne!(username_homoglyphs, username_homoglyphs_sanitised);
|
||||
assert_eq!("funny", username_homoglyphs_sanitised);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn username_sanitisation_padding() {
|
||||
let username_padding = "a";
|
||||
|
||||
let username = User::sanitise_username(username_padding).await.unwrap();
|
||||
|
||||
assert_eq!("a_", username);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn create_user() {
|
||||
use revolt_result::Result;
|
||||
|
||||
database_test!(|db| async move {
|
||||
let mut created_clean = User::create(&db, "Test".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!("Test", created_clean.username);
|
||||
|
||||
created_clean
|
||||
.update_username(&db, "Test2".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!("Test2", created_clean.username);
|
||||
|
||||
let created_invalid_result: Result<_> =
|
||||
User::create(&db, "stoat.chat".to_string(), None, None).await;
|
||||
|
||||
assert!(created_invalid_result.is_err());
|
||||
|
||||
let mut updated_invalid = User::create(&db, "Test".to_string(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let updated_invalid_update_result = updated_invalid
|
||||
.update_username(&db, "http://test".to_string())
|
||||
.await;
|
||||
|
||||
assert!(updated_invalid_update_result.is_err());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use validator::HasLen;
|
||||
use revolt_result::Result;
|
||||
|
||||
use super::DelayedTask;
|
||||
use crate::Channel::{TextChannel, VoiceChannel};
|
||||
use crate::Channel::TextChannel;
|
||||
|
||||
/// Enumeration of possible events
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
@@ -105,7 +105,11 @@ pub async fn handle_ack_event(
|
||||
|
||||
if mentions_acked > 0 {
|
||||
if let Err(err) = amqp
|
||||
.ack_message(user.to_string(), channel.to_string(), id.to_owned())
|
||||
.ack_notification_message(
|
||||
user.to_string(),
|
||||
channel.to_string(),
|
||||
id.to_owned(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
revolt_config::capture_error(&err);
|
||||
@@ -191,17 +195,12 @@ pub async fn handle_ack_event(
|
||||
.await
|
||||
.expect("Failed to fetch channel from db");
|
||||
|
||||
match channel {
|
||||
TextChannel { server, .. } | VoiceChannel { server, .. } => {
|
||||
if let Err(err) =
|
||||
amqp.mass_mention_message_sent(server, mass_mentions).await
|
||||
{
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("Unknown channel type when sending mass mention event");
|
||||
if let TextChannel { server, .. } = channel {
|
||||
if let Err(err) = amqp.mass_mention_message_sent(server, mass_mentions).await {
|
||||
revolt_config::capture_error(&err);
|
||||
}
|
||||
} else {
|
||||
panic!("Unknown channel type when sending mass mention event");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
77
crates/core/database/src/util/acker.rs
Normal file
77
crates/core/database/src/util/acker.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use redis_kiss::{get_connection, AsyncCommands};
|
||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||
use revolt_result::{Result, ToRevoltError};
|
||||
|
||||
use crate::{events::client::EventV1, Channel, Database, Server, User, AMQP};
|
||||
|
||||
pub async fn ack_channel(user: &str, channel: &str, message: &str, amqp: &AMQP) -> Result<()> {
|
||||
let mut redis = get_connection()
|
||||
.await
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
let old: Option<String> = redis
|
||||
.getset(format!("acker:{user}+{channel}"), message)
|
||||
.await
|
||||
.to_internal_error()?;
|
||||
|
||||
if old.is_none() || old.unwrap() == message {
|
||||
amqp.process_ack(user, Some(channel), None)
|
||||
.await
|
||||
.to_internal_error()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ack_server(user: &User, server: &Server, db: &Database, amqp: &AMQP) -> Result<()> {
|
||||
let mut redis = get_connection()
|
||||
.await
|
||||
.map_err(|_| create_error!(InternalError))?;
|
||||
|
||||
let channels = db.fetch_channels(&server.channels).await?;
|
||||
let query = crate::util::permissions::DatabasePermissionQuery::new(db, user).server(server);
|
||||
|
||||
for channel in channels {
|
||||
let channel_id = channel.id();
|
||||
let mut q = query.clone().channel(&channel);
|
||||
|
||||
if calculate_channel_permissions(&mut q)
|
||||
.await
|
||||
.has_channel_permission(ChannelPermission::ViewChannel)
|
||||
{
|
||||
let channel_last_msg = match &channel {
|
||||
Channel::TextChannel {
|
||||
last_message_id, ..
|
||||
} => last_message_id,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
.clone();
|
||||
|
||||
if let Some(channel_last_msg) = channel_last_msg {
|
||||
let old: Option<String> = redis
|
||||
.getset(
|
||||
format!("acker:{}+{}", user.id, channel_id),
|
||||
&channel_last_msg,
|
||||
)
|
||||
.await
|
||||
.to_internal_error()?;
|
||||
|
||||
if old.is_none() || old.unwrap() == channel_last_msg {
|
||||
amqp.process_ack(&user.id, Some(channel_id), Some(&server.id))
|
||||
.await
|
||||
.to_internal_error()?;
|
||||
|
||||
EventV1::ChannelAck {
|
||||
id: channel_id.to_string(),
|
||||
user: user.id.clone(),
|
||||
message_id: channel_last_msg,
|
||||
}
|
||||
.private(user.id.clone())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -143,6 +143,7 @@ impl From<crate::FieldsWebhook> for FieldsWebhook {
|
||||
}
|
||||
|
||||
impl From<crate::Channel> for Channel {
|
||||
#[allow(deprecated)]
|
||||
fn from(value: crate::Channel) -> Self {
|
||||
match value {
|
||||
crate::Channel::SavedMessages { id, user } => Channel::SavedMessages { id, user },
|
||||
@@ -188,6 +189,8 @@ impl From<crate::Channel> for Channel {
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
voice,
|
||||
slowmode,
|
||||
} => Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
@@ -198,31 +201,15 @@ impl From<crate::Channel> for Channel {
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
},
|
||||
crate::Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
} => Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon: icon.map(|file| file.into()),
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
voice: voice.map(|voice| voice.into()),
|
||||
slowmode,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Channel> for crate::Channel {
|
||||
#[allow(deprecated)]
|
||||
fn from(value: Channel) -> crate::Channel {
|
||||
match value {
|
||||
Channel::SavedMessages { id, user } => crate::Channel::SavedMessages { id, user },
|
||||
@@ -268,6 +255,8 @@ impl From<Channel> for crate::Channel {
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
voice,
|
||||
slowmode,
|
||||
} => crate::Channel::TextChannel {
|
||||
id,
|
||||
server,
|
||||
@@ -278,25 +267,8 @@ impl From<Channel> for crate::Channel {
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
},
|
||||
Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon,
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
} => crate::Channel::VoiceChannel {
|
||||
id,
|
||||
server,
|
||||
name,
|
||||
description,
|
||||
icon: icon.map(|file| file.into()),
|
||||
default_permissions,
|
||||
role_permissions,
|
||||
nsfw,
|
||||
voice: voice.map(|voice| voice.into()),
|
||||
slowmode,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -315,6 +287,8 @@ impl From<crate::PartialChannel> for PartialChannel {
|
||||
role_permissions: value.role_permissions,
|
||||
default_permissions: value.default_permissions,
|
||||
last_message_id: value.last_message_id,
|
||||
voice: value.voice.map(|voice| voice.into()),
|
||||
slowmode: value.slowmode,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,6 +306,8 @@ impl From<PartialChannel> for crate::PartialChannel {
|
||||
role_permissions: value.role_permissions,
|
||||
default_permissions: value.default_permissions,
|
||||
last_message_id: value.last_message_id,
|
||||
voice: value.voice.map(|voice| voice.into()),
|
||||
slowmode: value.slowmode,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,6 +318,7 @@ impl From<FieldsChannel> for crate::FieldsChannel {
|
||||
FieldsChannel::Description => crate::FieldsChannel::Description,
|
||||
FieldsChannel::Icon => crate::FieldsChannel::Icon,
|
||||
FieldsChannel::DefaultPermissions => crate::FieldsChannel::DefaultPermissions,
|
||||
FieldsChannel::Voice => crate::FieldsChannel::Voice,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +329,7 @@ impl From<crate::FieldsChannel> for FieldsChannel {
|
||||
crate::FieldsChannel::Description => FieldsChannel::Description,
|
||||
crate::FieldsChannel::Icon => FieldsChannel::Icon,
|
||||
crate::FieldsChannel::DefaultPermissions => FieldsChannel::DefaultPermissions,
|
||||
crate::FieldsChannel::Voice => FieldsChannel::Voice,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,9 +412,16 @@ impl From<crate::Metadata> for Metadata {
|
||||
match value {
|
||||
crate::Metadata::File => Metadata::File,
|
||||
crate::Metadata::Text => Metadata::Text,
|
||||
crate::Metadata::Image { width, height } => Metadata::Image {
|
||||
crate::Metadata::Image {
|
||||
width,
|
||||
height,
|
||||
thumbhash,
|
||||
animated,
|
||||
} => Metadata::Image {
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
thumbhash,
|
||||
animated,
|
||||
},
|
||||
crate::Metadata::Video { width, height } => Metadata::Video {
|
||||
width: width as usize,
|
||||
@@ -452,9 +437,16 @@ impl From<Metadata> for crate::Metadata {
|
||||
match value {
|
||||
Metadata::File => crate::Metadata::File,
|
||||
Metadata::Text => crate::Metadata::Text,
|
||||
Metadata::Image { width, height } => crate::Metadata::Image {
|
||||
Metadata::Image {
|
||||
width,
|
||||
height,
|
||||
thumbhash,
|
||||
animated,
|
||||
} => crate::Metadata::Image {
|
||||
width: width as isize,
|
||||
height: height as isize,
|
||||
thumbhash,
|
||||
animated,
|
||||
},
|
||||
Metadata::Video { width, height } => crate::Metadata::Video {
|
||||
width: width as isize,
|
||||
@@ -543,6 +535,9 @@ impl From<crate::SystemMessage> for SystemMessage {
|
||||
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
|
||||
crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by },
|
||||
crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by },
|
||||
crate::SystemMessage::CallStarted { by, finished_at } => {
|
||||
Self::CallStarted { by, finished_at }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -639,6 +634,8 @@ impl From<crate::Member> for Member {
|
||||
avatar: value.avatar.map(|f| f.into()),
|
||||
roles: value.roles,
|
||||
timeout: value.timeout,
|
||||
can_publish: value.can_publish,
|
||||
can_receive: value.can_receive,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,6 +649,8 @@ impl From<Member> for crate::Member {
|
||||
avatar: value.avatar.map(|f| f.into()),
|
||||
roles: value.roles,
|
||||
timeout: value.timeout,
|
||||
can_publish: value.can_publish,
|
||||
can_receive: value.can_receive,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -665,6 +664,8 @@ impl From<crate::PartialMember> for PartialMember {
|
||||
avatar: value.avatar.map(|f| f.into()),
|
||||
roles: value.roles,
|
||||
timeout: value.timeout,
|
||||
can_publish: value.can_publish,
|
||||
can_receive: value.can_receive,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,6 +679,8 @@ impl From<PartialMember> for crate::PartialMember {
|
||||
avatar: value.avatar.map(|f| f.into()),
|
||||
roles: value.roles,
|
||||
timeout: value.timeout,
|
||||
can_publish: value.can_publish,
|
||||
can_receive: value.can_receive,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -707,6 +710,10 @@ impl From<crate::FieldsMember> for FieldsMember {
|
||||
crate::FieldsMember::Nickname => FieldsMember::Nickname,
|
||||
crate::FieldsMember::Roles => FieldsMember::Roles,
|
||||
crate::FieldsMember::Timeout => FieldsMember::Timeout,
|
||||
crate::FieldsMember::CanReceive => FieldsMember::CanReceive,
|
||||
crate::FieldsMember::CanPublish => FieldsMember::CanPublish,
|
||||
crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt,
|
||||
crate::FieldsMember::VoiceChannel => FieldsMember::VoiceChannel,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -718,6 +725,10 @@ impl From<FieldsMember> for crate::FieldsMember {
|
||||
FieldsMember::Nickname => crate::FieldsMember::Nickname,
|
||||
FieldsMember::Roles => crate::FieldsMember::Roles,
|
||||
FieldsMember::Timeout => crate::FieldsMember::Timeout,
|
||||
FieldsMember::CanReceive => crate::FieldsMember::CanReceive,
|
||||
FieldsMember::CanPublish => crate::FieldsMember::CanPublish,
|
||||
FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt,
|
||||
FieldsMember::VoiceChannel => crate::FieldsMember::VoiceChannel,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -909,11 +920,13 @@ impl From<SystemMessageChannels> for crate::SystemMessageChannels {
|
||||
impl From<crate::Role> for Role {
|
||||
fn from(value: crate::Role) -> Self {
|
||||
Role {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
permissions: value.permissions,
|
||||
colour: value.colour,
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -921,11 +934,13 @@ impl From<crate::Role> for Role {
|
||||
impl From<Role> for crate::Role {
|
||||
fn from(value: Role) -> crate::Role {
|
||||
crate::Role {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
permissions: value.permissions,
|
||||
colour: value.colour,
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -933,11 +948,13 @@ impl From<Role> for crate::Role {
|
||||
impl From<crate::PartialRole> for PartialRole {
|
||||
fn from(value: crate::PartialRole) -> Self {
|
||||
PartialRole {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
permissions: value.permissions,
|
||||
colour: value.colour,
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -945,11 +962,13 @@ impl From<crate::PartialRole> for PartialRole {
|
||||
impl From<PartialRole> for crate::PartialRole {
|
||||
fn from(value: PartialRole) -> crate::PartialRole {
|
||||
crate::PartialRole {
|
||||
id: value.id,
|
||||
name: value.name,
|
||||
permissions: value.permissions,
|
||||
colour: value.colour,
|
||||
hoist: value.hoist,
|
||||
rank: value.rank,
|
||||
icon: value.icon.map(|f| f.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -958,6 +977,7 @@ impl From<crate::FieldsRole> for FieldsRole {
|
||||
fn from(value: crate::FieldsRole) -> Self {
|
||||
match value {
|
||||
crate::FieldsRole::Colour => FieldsRole::Colour,
|
||||
crate::FieldsRole::Icon => FieldsRole::Icon,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -966,6 +986,7 @@ impl From<FieldsRole> for crate::FieldsRole {
|
||||
fn from(value: FieldsRole) -> Self {
|
||||
match value {
|
||||
FieldsRole::Colour => crate::FieldsRole::Colour,
|
||||
FieldsRole::Icon => crate::FieldsRole::Icon,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1385,3 +1406,19 @@ impl From<FieldsMessage> for crate::FieldsMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VoiceInformation> for crate::VoiceInformation {
|
||||
fn from(value: VoiceInformation) -> Self {
|
||||
crate::VoiceInformation {
|
||||
max_users: value.max_users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::VoiceInformation> for VoiceInformation {
|
||||
fn from(value: crate::VoiceInformation) -> Self {
|
||||
VoiceInformation {
|
||||
max_users: value.max_users,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user