diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 00000000..867978dd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -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.
diff --git a/.github/workflows/git-town.yml b/.github/workflows/git-town.yml
index 03b9d9a0..678b60c1 100644
--- a/.github/workflows/git-town.yml
+++ b/.github/workflows/git-town.yml
@@ -1,15 +1,18 @@
+# DO NOT EDIT DIRECTLY IN REPOSITORY
+# Managed in Terraform templates
+
name: Git Town
on:
pull_request:
- branches:
- - '**'
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
@@ -17,3 +20,4 @@ jobs:
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: stoatchat/action-git-town@4bc5c942e4603bffa0806b51d5fe5f0bc5deb0ac
+ continue-on-error: true
\ No newline at end of file
diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml
index 7e68e112..63d9a88e 100644
--- a/.github/workflows/release-please.yml
+++ b/.github/workflows/release-please.yml
@@ -45,8 +45,9 @@ jobs:
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 '${{ steps.rp.outputs.prs }}' | jq -r '.[0].number')
+ PR_NUMBER=$(echo "$RP_PR_OUTPUT_JSON" | jq -r '.[0].number')
gh pr checkout "$PR_NUMBER"
cargo update -w
diff --git a/.github/workflows/release-webhook.yml b/.github/workflows/release-webhook.yml
index 58a1d419..3996b1f8 100644
--- a/.github/workflows/release-webhook.yml
+++ b/.github/workflows/release-webhook.yml
@@ -1,3 +1,6 @@
+# DO NOT EDIT DIRECTLY IN REPOSITORY
+# Managed in Terraform templates
+
name: Release Webhook
on:
@@ -12,8 +15,12 @@ jobs:
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/${{ github.repository }}/releases/tag/${{ github.event.release.tag_name }}"
- curl -X POST "${{ secrets.STOAT_WEBHOOK_UPDATES_URL }}" \
+ RELEASE_URL="https://github.com/${REPOSITORY}/releases/tag/${TAG_NAME}"
+ curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
- -d "{\"content\": \"$RELEASE_URL\"}"
+ -d "{\"content\": \"$RELEASE_URL\"}"
\ No newline at end of file
diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml
index 6af09303..1374d58c 100644
--- a/.github/workflows/validate-pr-title.yml
+++ b/.github/workflows/validate-pr-title.yml
@@ -1,3 +1,6 @@
+# DO NOT EDIT DIRECTLY IN REPOSITORY
+# Managed in Terraform templates
+
name: "Lint PR"
on:
@@ -15,6 +18,6 @@ jobs:
permissions:
pull-requests: read
steps:
- - uses: amannn/action-semantic-pull-request@v6
+ - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 7bff16bc..53d4a3f2 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.9.4"
+ ".": "0.12.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 22da6e66..7153a56f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,118 @@
# Changelog
+## [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)
diff --git a/Cargo.lock b/Cargo.lock
index 958eb695..4489f555 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -475,9 +475,9 @@ dependencies = [
[[package]]
name = "authifier"
-version = "1.0.15"
+version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1a03b810b342b4c584cdc1cfc40d1ec763b1d653ef4086f7494740f35d9e1f0"
+checksum = "1d6163caecbb985d91406c2c6dc7710c46b38e6461d93d5e843b2f0eac43910c"
dependencies = [
"async-std",
"async-trait",
@@ -5525,9 +5525,9 @@ dependencies = [
[[package]]
name = "opentelemetry-otlp"
-version = "0.31.0"
+version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf"
+checksum = "1f69cd6acbb9af919df949cd1ec9e5e7fdc2ef15d234b6b795aaa525cc02f71f"
dependencies = [
"http 1.3.1",
"opentelemetry",
@@ -6767,9 +6767,9 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.11.2"
+version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
@@ -6779,9 +6779,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.10"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
@@ -6907,7 +6907,7 @@ dependencies = [
[[package]]
name = "revolt-autumn"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"axum",
"axum-macros",
@@ -6937,6 +6937,7 @@ dependencies = [
"simdutf8",
"strum_macros",
"tempfile",
+ "thumbhash",
"tokio 1.49.0",
"tower-http 0.5.2",
"tracing",
@@ -6949,7 +6950,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"async-channel 2.5.0",
"async-std",
@@ -6980,7 +6981,7 @@ dependencies = [
[[package]]
name = "revolt-coalesced"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"indexmap 2.13.0",
"lru 0.16.3",
@@ -6989,7 +6990,7 @@ dependencies = [
[[package]]
name = "revolt-config"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"async-std",
"cached",
@@ -7006,7 +7007,7 @@ dependencies = [
[[package]]
name = "revolt-crond"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"log",
"revolt-config",
@@ -7018,7 +7019,7 @@ dependencies = [
[[package]]
name = "revolt-database"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"amqprs",
"async-lock 2.8.0",
@@ -7068,7 +7069,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"amqprs",
"async-channel 1.9.0",
@@ -7118,7 +7119,7 @@ dependencies = [
[[package]]
name = "revolt-files"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"aes-gcm",
"anyhow",
@@ -7146,7 +7147,7 @@ dependencies = [
[[package]]
name = "revolt-gifbox"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"axum",
"axum-extra",
@@ -7169,7 +7170,7 @@ dependencies = [
[[package]]
name = "revolt-january"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"async-recursion",
"axum",
@@ -7197,7 +7198,7 @@ dependencies = [
[[package]]
name = "revolt-models"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"indexmap 1.9.3",
"iso8601-timestamp",
@@ -7216,14 +7217,14 @@ dependencies = [
[[package]]
name = "revolt-parser"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"logos",
]
[[package]]
name = "revolt-permissions"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"async-std",
"async-trait",
@@ -7238,7 +7239,7 @@ dependencies = [
[[package]]
name = "revolt-presence"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"async-std",
"log",
@@ -7250,7 +7251,7 @@ dependencies = [
[[package]]
name = "revolt-pushd"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"amqprs",
"anyhow",
@@ -7262,9 +7263,12 @@ dependencies = [
"iso8601-timestamp",
"log",
"pretty_env_logger",
+ "redis-kiss",
+ "regex",
"revolt-config",
"revolt-database",
"revolt-models",
+ "revolt-parser",
"revolt-presence",
"revolt-result",
"revolt_a2",
@@ -7278,7 +7282,7 @@ dependencies = [
[[package]]
name = "revolt-ratelimits"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"async-trait",
"authifier",
@@ -7295,7 +7299,7 @@ dependencies = [
[[package]]
name = "revolt-result"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"axum",
"log",
@@ -7311,7 +7315,7 @@ dependencies = [
[[package]]
name = "revolt-voice-ingress"
-version = "0.9.4"
+version = "0.12.0"
dependencies = [
"amqprs",
"async-std",
@@ -7524,9 +7528,9 @@ dependencies = [
[[package]]
name = "rocket_authifier"
-version = "1.0.15"
+version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1f98a5be96ac4144ee1adc809c1594e8212032b8a08d9bc4cef70735dfd976e"
+checksum = "4a4e9660d198ea0165dc2694e7620d8fe199e923b39bec4d12b7159da5a4a6cd"
dependencies = [
"authifier",
"iso8601-timestamp",
@@ -8945,6 +8949,12 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "thumbhash"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b7726e0245a7331bd0c9a1fb4fd99fd695bcd478ca569f0eda2ff2cb14e7a00"
+
[[package]]
name = "tiff"
version = "0.10.3"
@@ -9220,9 +9230,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "tonic"
-version = "0.14.2"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203"
+checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -9241,9 +9251,9 @@ dependencies = [
[[package]]
name = "tonic-prost"
-version = "0.14.2"
+version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67"
+checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
dependencies = [
"bytes 1.10.1",
"prost 0.14.3",
diff --git a/Dockerfile b/Dockerfile
index 86011e56..aca233ec 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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
diff --git a/Dockerfile.useCurrentArch b/Dockerfile.useCurrentArch
index 82929690..9bcce8ac 100644
--- a/Dockerfile.useCurrentArch
+++ b/Dockerfile.useCurrentArch
@@ -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
diff --git a/README.md b/README.md
index 14605969..386a0453 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,15 @@
- 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)
-The services and libraries that power the Revolt service.
+The services and libraries that power the Stoat service.
| Crate | Path | Description | |
@@ -39,7 +39,7 @@ Rust 1.86.0 or higher.
## Development Guide
-Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
+Before contributing, make yourself familiar with [our contribution guidelines](https://developers.stoat.chat/developing/contrib/) and the [technical documentation for this project](https://developers.stoat.chat/).
Before getting started, you'll want to install:
@@ -70,8 +70,9 @@ As a heads-up, the development environment uses the following ports:
Now you can clone and build the project:
```bash
-git clone https://github.com/revoltchat/backend revolt-backend
-cd revolt-backend
+git clone https://github.com/stoatchat/stoatchat stoat-backend
+cd stoat-backend
+mise install
mise build
```
@@ -142,9 +143,6 @@ Then continue:
# start other necessary services
docker compose up -d
-# run everything together
-./scripts/start.sh
-# .. or individually
# run the API server
cargo run --bin revolt-delta
# run the events server
@@ -170,16 +168,11 @@ You can start a web client by doing the following:
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.
## Deployment Guide
@@ -210,7 +203,7 @@ 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
@@ -229,6 +222,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!**
diff --git a/Revolt.toml b/Revolt.toml
index 0a824e2d..7398ed53 100644
--- a/Revolt.toml
+++ b/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"
diff --git a/compose.yml b/compose.yml
index e215efcf..490fbfa4 100644
--- a/compose.yml
+++ b/compose.yml
@@ -73,7 +73,7 @@ services:
# LiveKit
livekit:
- image: ghcr.io/stoatchat/livekit-server:v1.9.9
+ image: ghcr.io/stoatchat/livekit-server:v1.9.13
command: --config /etc/livekit.yml
network_mode: "host"
volumes:
diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml
index ca0ce9bb..e4481b1c 100644
--- a/crates/bonfire/Cargo.toml
+++ b/crates/bonfire/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
-version = "0.9.4"
+version = "0.12.0"
license = "AGPL-3.0-or-later"
edition = "2021"
publish = false
@@ -38,7 +38,7 @@ async-std = { version = "1.8.0", features = [
] }
# core
-authifier = { version = "1.0.15" }
+authifier = { version = "1.0.16" }
revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
diff --git a/crates/bonfire/src/events/impl.rs b/crates/bonfire/src/events/impl.rs
index e0e9ac71..12113537 100644
--- a/crates/bonfire/src/events/impl.rs
+++ b/crates/bonfire/src/events/impl.rs
@@ -4,7 +4,7 @@ use futures::future::join_all;
use revolt_database::{
events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery,
- voice::get_channel_voice_state,
+ voice::{get_channel_voice_state, UserVoiceChannel},
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
};
use revolt_models::v0;
@@ -167,9 +167,13 @@ impl State {
| Channel::TextChannel { voice: Some(_), .. }
)
}) {
- if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
+ if let Ok(Some(voice_state)) =
+ get_channel_voice_state(&UserVoiceChannel::from_channel(channel)).await
+ {
if let Some(server) = channel.server() {
- let set = voice_state_server_members.entry(server.to_string()).or_default();
+ let set = voice_state_server_members
+ .entry(server.to_string())
+ .or_default();
for participant in &voice_state.participants {
user_ids.insert(participant.id.clone());
@@ -397,6 +401,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 {
diff --git a/crates/bonfire/src/websocket.rs b/crates/bonfire/src/websocket.rs
index 913245e9..ec4202dc 100644
--- a/crates/bonfire/src/websocket.rs
+++ b/crates/bonfire/src/websocket.rs
@@ -218,7 +218,13 @@ async fn listener(
kill_signal_r: async_channel::Receiver<()>,
write: &Mutex
,
) {
- let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
+ 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 subscriber = match report_internal_error!(
fred::types::Builder::from_config(redis_config).build_subscriber_client()
) {
@@ -422,6 +428,8 @@ async fn worker(
mut read: WsReader,
write: &Mutex,
) {
+ let revolt_config = revolt_config::config().await;
+
loop {
let t1 = read.try_next().fuse();
let t2 = kill_signal_r.recv().fuse();
@@ -458,6 +466,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 +482,10 @@ async fn worker(
.await;
}
ClientMessage::EndTyping { channel } => {
+ if revolt_config.disable_events_dont_use {
+ continue;
+ }
+
if !subscribed.read().await.contains(&channel) {
continue;
}
diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml
index aac2b95b..4b589c0d 100644
--- a/crates/core/coalesced/Cargo.toml
+++ b/crates/core/coalesced/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "revolt-coalesced"
-version = "0.9.4"
+version = "0.12.0"
edition = "2021"
license = "MIT"
authors = ["Paul Makles ", "Zomatree "]
diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml
index e91ce5e0..ad1ee469 100644
--- a/crates/core/config/Cargo.toml
+++ b/crates/core/config/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
-version = "0.9.4"
+version = "0.12.0"
edition = "2021"
license = "MIT"
authors = ["Paul Makles "]
@@ -38,4 +38,4 @@ sentry = { version = "0.31.5", optional = true }
sentry-anyhow = { version = "0.38.1", optional = true }
# Core
-revolt-result = { version = "0.9.4", path = "../result", optional = true }
+revolt-result = { version = "0.12.0", path = "../result", optional = true }
diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml
index 9813a27f..3bb33111 100644
--- a/crates/core/config/Revolt.toml
+++ b/crates/core/config/Revolt.toml
@@ -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"
@@ -34,7 +35,7 @@ password = "rabbitpass"
[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]
@@ -88,6 +89,11 @@ production = true
# Increasing this will resolve mentions faster, but will consume more memory while resolving.
mass_mention_chunk_size = 200
+# How long pushd will cache resolved names for rendered message notifications.
+# Increasing this will result in lower database usage, but may result in a situation where a user/channel/role name changes
+# and the notifications still resolve to the old name.
+render_cache_time = 60
+
# none of these should need changing
exchange = "revolt.notifications"
message_queue = "notifications.origin.message"
@@ -220,6 +226,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
@@ -244,7 +254,7 @@ voice_quality = 16000
# Whether the user can use video streams in voice calls
video = true
-# Mamimum resolution (width, height) of video streams in voice calls
+# 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
@@ -283,8 +293,8 @@ voice_quality = 16000
# Whether the user can use video streams in voice calls
video = true
-# Mamimum resolution (width, height) of video streams in voice calls
-video_resolution = [1080, 720]
+# 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]
@@ -312,4 +322,4 @@ files = ""
proxy = ""
pushd = ""
crond = ""
-gifbox = ""
\ No newline at end of file
+gifbox = ""
diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs
index 065658dc..65df3446 100644
--- a/crates/core/config/src/lib.rs
+++ b/crates/core/config/src/lib.rs
@@ -1,7 +1,7 @@
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;
@@ -109,6 +109,8 @@ static CONFIG_BUILDER: Lazy> = Lazy::new(|| {
cwd = path.parent();
}
+ builder = builder.add_source(Environment::with_prefix("REVOLT").separator("__"));
+
builder.build().unwrap()
})
});
@@ -117,6 +119,7 @@ static CONFIG_BUILDER: Lazy> = Lazy::new(|| {
pub struct Database {
pub mongodb: String,
pub redis: String,
+ pub redis_pubsub: Option,
}
#[derive(Deserialize, Debug, Clone)]
@@ -245,6 +248,7 @@ pub struct Pushd {
pub production: bool,
pub exchange: String,
pub mass_mention_chunk_size: usize,
+ pub render_cache_time: usize,
// Queues
pub message_queue: String,
@@ -341,6 +345,8 @@ pub struct GlobalLimits {
pub new_user_hours: usize,
pub body_limit_size: usize,
+
+ pub restrict_server_creation: Vec,
}
#[derive(Deserialize, Debug, Clone)]
@@ -418,6 +424,7 @@ pub struct Settings {
pub features: Features,
pub sentry: Sentry,
pub production: bool,
+ pub disable_events_dont_use: bool,
}
impl Settings {
@@ -448,12 +455,14 @@ pub async fn config() -> Settings {
let mut config = read().await.try_deserialize::().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;
}
diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml
index 979d4a5d..05a66179 100644
--- a/crates/core/database/Cargo.toml
+++ b/crates/core/database/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
-version = "0.9.4"
+version = "0.12.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles "]
@@ -32,19 +32,19 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
-revolt-config = { version = "0.9.4", path = "../config", features = [
+revolt-config = { version = "0.12.0", path = "../config", features = [
"report-macros",
] }
-revolt-result = { version = "0.9.4", path = "../result" }
-revolt-models = { version = "0.9.4", path = "../models", features = [
+revolt-result = { version = "0.12.0", path = "../result" }
+revolt-models = { version = "0.12.0", path = "../models", features = [
"validator",
] }
-revolt-presence = { version = "0.9.4", path = "../presence" }
-revolt-permissions = { version = "0.9.4", path = "../permissions", features = [
+revolt-presence = { version = "0.12.0", path = "../presence" }
+revolt-permissions = { version = "0.12.0", path = "../permissions", features = [
"serde",
"bson",
] }
-revolt-parser = { version = "0.9.4", path = "../parser" }
+revolt-parser = { version = "0.12.0", path = "../parser" }
# Utility
log = "0.4"
@@ -100,7 +100,7 @@ revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
# Authifier
-authifier = { version = "1.0.15" }
+authifier = { version = "1.0.16" }
# RabbitMQ
amqprs = { version = "1.7.0" }
diff --git a/crates/core/database/src/drivers/mod.rs b/crates/core/database/src/drivers/mod.rs
index 9618ee13..df9a56ef 100644
--- a/crates/core/database/src/drivers/mod.rs
+++ b/crates/core/database/src/drivers/mod.rs
@@ -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;
@@ -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),
diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs
index d8a485b9..025b7934 100644
--- a/crates/core/database/src/models/channels/model.rs
+++ b/crates/core/database/src/models/channels/model.rs
@@ -110,6 +110,10 @@ auto_derived!(
/// Voice Information for when this channel is also a voice channel
#[serde(skip_serializing_if = "Option::is_none")]
voice: Option,
+
+ /// The channel's slowmode delay in seconds
+ #[serde(skip_serializing_if = "Option::is_none")]
+ slowmode: Option,
},
}
@@ -146,6 +150,8 @@ auto_derived!(
pub last_message_id: Option,
#[serde(skip_serializing_if = "Option::is_none")]
pub voice: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub slowmode: Option,
}
/// Optional fields on channel object
@@ -206,6 +212,7 @@ impl Channel {
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
voice: data.voice.map(|voice| voice.into()),
+ slowmode: None
},
v0::LegacyServerChannelType::Voice => Channel::TextChannel {
id: id.clone(),
@@ -218,6 +225,7 @@ impl Channel {
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
voice: Some(data.voice.unwrap_or_default().into()),
+ slowmode: None
},
};
diff --git a/crates/core/database/src/models/file_hashes/model.rs b/crates/core/database/src/models/file_hashes/model.rs
index cec7a39d..b25abb12 100644
--- a/crates/core/database/src/models/file_hashes/model.rs
+++ b/crates/core/database/src/models/file_hashes/model.rs
@@ -45,7 +45,9 @@ auto_derived!(
Image {
width: isize,
height: isize,
- // animated: bool // TODO: https://docs.rs/image/latest/image/trait.AnimationDecoder.html for APNG support
+ thumbhash: Option>,
+ #[serde(default)]
+ animated: bool,
},
/// File is a video with specific dimensions
Video { width: isize, height: isize },
diff --git a/crates/core/database/src/models/messages/model.rs b/crates/core/database/src/models/messages/model.rs
index 11bc2eac..bacb3c53 100644
--- a/crates/core/database/src/models/messages/model.rs
+++ b/crates/core/database/src/models/messages/model.rs
@@ -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;
@@ -388,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() {
@@ -497,7 +498,7 @@ impl Message {
user_mentions.retain(|m| recipients_hash.contains(m));
role_mentions.clear();
}
- Channel::TextChannel { 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;
@@ -687,8 +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")]
@@ -699,7 +705,7 @@ impl Message {
Some(
PushNotification::from(
self.clone().into_model(user, member),
- Some(author),
+ Some(author.clone()),
channel.to_owned().into(),
)
.await,
@@ -707,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()
}
@@ -1029,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
diff --git a/crates/core/database/src/models/messages/ops.rs b/crates/core/database/src/models/messages/ops.rs
index 4d54e83c..6ebdf4c3 100644
--- a/crates/core/database/src/models/messages/ops.rs
+++ b/crates/core/database/src/models/messages/ops.rs
@@ -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>>;
}
diff --git a/crates/core/database/src/models/messages/ops/mongodb.rs b/crates/core/database/src/models/messages/ops/mongodb.rs
index 449e71ef..2a79ed5b 100644
--- a/crates/core/database/src/models/messages/ops/mongodb.rs
+++ b/crates/core/database/src/models/messages/ops/mongodb.rs
@@ -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>> {
+ 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::::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::::new(),
+ "in": { "$setUnion": ["$$value", "$$this"] }
+ }
+ }
+ }
+ },
+ ];
+
+ #[derive(serde::Deserialize)]
+ struct AggregatedChannel {
+ #[serde(rename = "_id")]
+ channel: String,
+ message_ids: Vec,
+ #[serde(default)]
+ attachment_ids: Vec,
+ }
+
+ let mut cursor = self
+ .col::(COL)
+ .aggregate(pipeline)
+ .await
+ .map_err(|_| create_database_error!("aggregate", COL))?
+ .with_type::();
+
+ let mut deleted_messages: HashMap> = HashMap::new();
+ let mut attachment_ids: HashSet = 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::("attachments")
+ .update_many(
+ doc! {
+ "_id": {
+ "$in": attachment_ids.into_iter().collect::>()
+ }
+ },
+ doc! {
+ "$set": {
+ "deleted": true
+ }
+ },
+ )
+ .await
+ .map_err(|_| create_database_error!("update_many", "attachments"))?;
+ }
+
+ self.col::(COL)
+ .delete_many(filter)
+ .await
+ .map_err(|_| create_database_error!("delete_many", COL))?;
+
+ Ok(deleted_messages)
+ }
}
impl IntoDocumentPath for FieldsMessage {
diff --git a/crates/core/database/src/models/messages/ops/reference.rs b/crates/core/database/src/models/messages/ops/reference.rs
index ae5b43f0..6f30de6f 100644
--- a/crates/core/database/src/models/messages/ops/reference.rs
+++ b/crates/core/database/src/models/messages/ops/reference.rs
@@ -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;
@@ -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>> {
+ let threshold_ulid = Ulid::from_datetime(since).to_string();
+ let mut deleted_messages: HashMap> = HashMap::new();
+ let mut attachment_ids: Vec = 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)
+ }
}
diff --git a/crates/core/database/src/models/server_members/model.rs b/crates/core/database/src/models/server_members/model.rs
index 0062a270..e242c62f 100644
--- a/crates/core/database/src/models/server_members/model.rs
+++ b/crates/core/database/src/models/server_members/model.rs
@@ -3,8 +3,8 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use crate::{
- events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel,
- Database, File, Server, SystemMessage, User,
+ events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
+ Server, SystemMessage, User,
};
fn default_true() -> bool {
@@ -45,7 +45,6 @@ auto_derived_partial!(
/// 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
@@ -153,7 +152,11 @@ impl Member {
#[cfg(feature = "voice")]
for channel in &channels {
- if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(channel).await {
+ if let Ok(Some(voice_state)) = crate::voice::get_channel_voice_state(
+ &crate::voice::UserVoiceChannel::from_channel(channel),
+ )
+ .await
+ {
voice_states.push(voice_state)
}
}
@@ -175,7 +178,7 @@ impl Member {
.map(|channel| channel.into())
.collect(),
emojis: emojis.into_iter().map(|emoji| emoji.into()).collect(),
- voice_states
+ voice_states,
}
.private(user.id.clone())
.await;
@@ -225,14 +228,14 @@ impl Member {
pub fn remove_field(&mut self, field: &FieldsMember) {
match field {
- FieldsMember::JoinedAt => {},
+ 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 => {},
+ FieldsMember::VoiceChannel => {}
}
}
diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs
index aafb2313..2e637795 100644
--- a/crates/core/database/src/util/bridge/v0.rs
+++ b/crates/core/database/src/util/bridge/v0.rs
@@ -190,6 +190,7 @@ impl From for Channel {
role_permissions,
nsfw,
voice,
+ slowmode
} => Channel::TextChannel {
id,
server,
@@ -201,6 +202,7 @@ impl From for Channel {
role_permissions,
nsfw,
voice: voice.map(|voice| voice.into()),
+ slowmode
},
}
}
@@ -254,6 +256,7 @@ impl From for crate::Channel {
role_permissions,
nsfw,
voice,
+ slowmode
} => crate::Channel::TextChannel {
id,
server,
@@ -265,6 +268,7 @@ impl From for crate::Channel {
role_permissions,
nsfw,
voice: voice.map(|voice| voice.into()),
+ slowmode
},
}
}
@@ -283,7 +287,8 @@ impl From 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())
+ voice: value.voice.map(|voice| voice.into()),
+ slowmode: value.slowmode,
}
}
}
@@ -301,7 +306,8 @@ impl From 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())
+ voice: value.voice.map(|voice| voice.into()),
+ slowmode: value.slowmode
}
}
}
@@ -406,9 +412,16 @@ impl From 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,
@@ -424,9 +437,16 @@ impl From 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,
@@ -515,7 +535,9 @@ impl From 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 }
+ crate::SystemMessage::CallStarted { by, finished_at } => {
+ Self::CallStarted { by, finished_at }
+ }
}
}
}
@@ -1382,7 +1404,7 @@ impl From for crate::FieldsMessage {
impl From for crate::VoiceInformation {
fn from(value: VoiceInformation) -> Self {
crate::VoiceInformation {
- max_users: value.max_users
+ max_users: value.max_users,
}
}
}
@@ -1390,7 +1412,7 @@ impl From for crate::VoiceInformation {
impl From for VoiceInformation {
fn from(value: crate::VoiceInformation) -> Self {
VoiceInformation {
- max_users: value.max_users
+ max_users: value.max_users,
}
}
-}
\ No newline at end of file
+}
diff --git a/crates/core/database/src/voice/mod.rs b/crates/core/database/src/voice/mod.rs
index 56009a53..836de92a 100644
--- a/crates/core/database/src/voice/mod.rs
+++ b/crates/core/database/src/voice/mod.rs
@@ -1,3 +1,5 @@
+use std::fmt::{Display, Write};
+
use crate::{
events::client::EventV1,
models::{Channel, User},
@@ -6,7 +8,11 @@ use crate::{
};
use iso8601_timestamp::{Duration, Timestamp};
use livekit_protocol::ParticipantPermission;
-use redis_kiss::{get_connection as _get_connection, redis::Pipeline, AsyncCommands, Conn};
+use redis_kiss::{
+ get_connection as _get_connection,
+ redis::{FromRedisValue, Pipeline, RedisError, RedisWrite, ToRedisArgs, Value},
+ AsyncCommands, Conn,
+};
use revolt_config::FeaturesLimits;
use revolt_models::v0::{self, PartialUserVoiceState, UserVoiceState};
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue};
@@ -16,48 +22,60 @@ mod voice_client;
pub use voice_client::VoiceClient;
async fn get_connection() -> Result {
- _get_connection().await.map_err(|_| create_error!(InternalError))
+ _get_connection()
+ .await
+ .map_err(|_| create_error!(InternalError))
}
-pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
+pub async fn raise_if_in_voice(user: &User, channel: &UserVoiceChannel) -> Result<()> {
let mut conn = get_connection().await?;
- if user.bot.is_some()
- // bots can be in as many voice channels as it wants so we just check if its already connected to the one its trying to connect to
- && conn.sismember(format!("vc:{}", &user.id), channel_id)
+ if user.bot.is_some() {
+ // bots can be in as many voice channels as it wants so we just check if its already connected to the one its trying to connect to
+ if conn
+ .sismember(format!("vc:{}", &user.id), channel)
.await
.to_internal_error()?
- {
- Err(create_error!(AlreadyConnected))
+ {
+ return Err(create_error!(AlreadyConnected));
+ };
} else if conn
.scard::<_, u32>(format!("vc:{}", &user.id)) // check if the current vc set is empty
.await
.to_internal_error()?
> 0
{
- Err(create_error!(AlreadyConnected))
- } else {
- Ok(())
- }
+ return Err(create_error!(AlreadyConnected));
+ };
+
+ Ok(())
}
-pub async fn set_channel_node(channel: &str, node: &str) -> Result<()> {
+pub async fn set_channel_node(channel_id: &str, node: &str) -> Result<()> {
get_connection()
.await?
- .set(format!("node:{channel}"), node)
+ .set(format!("node:{channel_id}"), node)
.await
.to_internal_error()
}
-pub async fn get_channel_node(channel: &str) -> Result