Compare commits

..
83 Commits
Author SHA1 Message Date
izzy 4789dd4847 fix: include voice-ingress in build 2025-09-30 14:55:31 -05:00
izzy f91d3bbfdd merge: branch 'main' into feat/livekit 2025-09-30 14:52:24 -05:00
Zomatree 5b50532d15 feat: track join time 2025-09-20 05:26:58 +01:00
Zomatree 938480ccec Signed-off-by: Angelo Kontaxis <me@zomatree.live> 2025-09-20 01:58:57 +01:00
Zomatree f422e8e917 Merge remote-tracking branch 'origin' into feat/livekit 2025-09-07 05:39:51 +01:00
Zomatree 1632e3dacf feat: allow users to specify who is notified when starting a call 2025-09-07 05:01:14 +01:00
Zomatree cc9a68b9c5 chore: cleanup 2025-09-07 02:27:50 +01:00
IAmTomahawkx 77a23239ea feat: Send push notifications for dm call start/end 2025-09-06 15:14:48 -07:00
Zomatree d03e6be2eb fix: update migration 2025-08-19 18:56:17 +01:00
Zomatree 0a4d77ffd3 fix: remove VoiceChannel channel type
fix: calculate user overwrites correctly
fix: dont include personal info in livekit user metadata
fix: revoke video permissons on denied publish
fix: add video to default permissions
2025-08-19 17:37:09 +01:00
Zomatree c6f121f585 Merge remote-tracking branch 'origin/main' into feat/livekit 2025-08-18 07:44:52 +01:00
Zomatree 96ddeb79d2 fix: dont set max participants in livekit 2025-08-18 07:28:09 +01:00
Zomatree a36b138978 fix: move call system message to daemon,
check max participants when creating a token to avoid giving tokens but erroring when attempting to join,
check if the channel actually supports voice
2025-08-18 04:58:06 +01:00
Zomatree 860c47fbad feat: - track call length
- move voice and video limits to config
- seperate VoiceInformation into model and db model
- fix build scripts
2025-08-12 01:48:57 +01:00
Zomatree a770d5822a chore: cleanup code 2025-08-04 03:20:08 +01:00
Zomatree 10c9682bba feat: ability to force disconnect existing voice connection 2025-08-04 03:19:53 +01:00
Zomatree eb48975428 chore: bump livekit dep 2025-07-01 20:22:53 +01:00
Zomatree 28ed85a779 Merge branch 'main' into feat/livekit 2025-07-01 20:22:34 +01:00
Zomatree 95ca735b7b fix: get_voice_state has incorrect unique_key and allowed_sources should be lowercase 2025-06-08 02:33:35 +01:00
Zomatree 901ac56154 fix: remove node from channel voice state 2025-06-05 17:26:37 +01:00
Zomatree 3dbf3bb7a0 feat: private livekit nodes 2025-06-05 14:14:19 +01:00
Zomatree 45df5724b3 Merge remote-tracking branch 'origin/main' into livekit 2025-06-02 02:20:04 +01:00
Zomatree ea1c165d80 Merge remote-tracking branch 'origin/main' into livekit 2025-06-02 02:11:51 +01:00
Zomatree 48e0b48ea9 fix: update branch from main 2025-05-01 16:58:35 +01:00
Zomatree 61038ddcf1 Merge remote-tracking branch 'origin/main' into livekit 2025-05-01 16:42:13 +01:00
Zomatree b96f07e615 fix: update branch from main 2025-04-29 15:52:03 +01:00
Zomatree d830fb0911 Merge branch 'main' into livekit 2025-04-29 15:47:16 +01:00
Zomatree cc9ff9a632 fix: add back missing early adopter badge 2025-04-29 15:40:41 +01:00
Zomatree 02f18705f4 fix: dont leak invisible presence to others 2025-04-29 15:40:10 +01:00
Zomatree a841df3b36 fix: show full branch name on github webhook messages 2025-04-29 15:40:10 +01:00
Tom 2a36f5912d feat: Add Mass Mentions to the backend (#394)
* feat: create base of push daemon

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* Add outbound senders

* Make web_push send to rabbit instead (temp stuff)

* feat: stability and friend requests

* make vapid fr stuff not suck

* swap naming of queue

* move pushd into daemons folder

* fix cargo file for move into daemons folder

* feat: probably working fcm push notifs

* comment out fcm webpush stuff since the config keys dont exist

* fix fcm, name queues according to their prod status and configure routing keys

* add pushd to docker

* mix: Remove old code, add stuff to pushd

* fix: lockfile

* feat: update rocket to 5.0.1

* fix: fix queues and ack bugs

* Move rabbit messsage processing into ack queue

* chore: update readme

* chore: optimizations for ack database hits

* pushd flowchart

* misc: update flowchart

* exit dependancy hell

* add rocket_impl flag to authifier

* make the tests file of delta actually compile

* fix: don't silence every push message

* fix: don't silence all messages

* add debug logging for sending data to rabbit from message events

* validate mentions at a server membership level

* put back that import that was actually important

* minor fix to lockfile

* update delta authifier

* feat: proper permissions for push notifications

* add unit test for mention sanitization

* remove local file dependancy on authifier

* update ports to proper defaults

* fixTM the node bindings

* Theoretically configure docker releases for pushd

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* declare exchange in pushd and delta

* fix createbuckets script

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: reference db implementation

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: remove finally redundant code

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: changes

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: other changes

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: make channel name return result

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* Add role mention parsing

* feat: update to mongo 3.1, add member generator.

* integrate mass mentions into pushd

* patch redis-rs with updated versions

* feat: chunk role mentions

* move permission bits to 37/38 to avoid livekit conflict

* change role mention format to <%id>

* fix the lockfile from merge

* fix: PR change requests

* feat: add tests

* fix: i am a dumbass

* fix: tests, again

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-04-29 15:40:09 +01:00
izzy a110a087be ci: downgrade lockfile 2025-04-29 15:39:12 +01:00
izzy 21b9164ee7 chore: bump version to 0.8.4
chore: enable webhooks in test builds
2025-04-29 15:39:12 +01:00
ShaksterNano 2f3d1f0c7e feat: add option to send message with missing replies
Signed-off-by: ShaksterNano <54268387+shaksternano@users.noreply.github.com>
2025-04-29 15:39:12 +01:00
Builderb 62204fe50b feat: allow bots to manage emojis (#407)
* Remove bot check on emoji create

Signed-off-by: Builderb <builderbgamer@gmail.com>

* Remove bot check on emoji delete

Signed-off-by: Builderb <builderbgamer@gmail.com>

---------

Signed-off-by: Builderb <builderbgamer@gmail.com>
2025-04-29 15:39:12 +01:00
Bradlee Barnes 773b890012 fix: remove SavedMessages and Group due to given permissions
Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>
2025-04-29 15:39:12 +01:00
Bradlee Barnes 9c3b2fb364 fix: allow message pinning and unpinning for DM-like channels
Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>
2025-04-29 15:39:11 +01:00
Nils Ponsard 5fe19d9815 feat: allow to set an icon when creating a group
Signed-off-by: Nils Ponsard <nilsponsard@gmail.com>
2025-04-29 15:39:11 +01:00
phazeschift c1c5b0fd29 test: add delete server channel test
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-04-29 15:39:11 +01:00
phazeschift 6737100b4b test: add utilities for common setup
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-04-29 15:39:11 +01:00
phazeschift 6f0e80eef9 test: Add tests for invite/fetch
Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>
2025-04-29 15:39:11 +01:00
IAmTomahawkx bfc6f50ee5 chore: ignore python venv and dev script for user generation
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-04-29 15:39:11 +01:00
Paul Makles 7abbad700c fix: include production default value for config 2025-04-29 15:39:11 +01:00
Paul Makles a671ba6308 fix: don't bump the lockfile version 2025-04-29 15:39:11 +01:00
Paul Makles 1d385ef2bc chore: bump version to 0.8.3 2025-04-29 15:39:11 +01:00
Paul Makles 6558e9f7c4 feat: add company information to email footers 2025-04-29 15:39:10 +01:00
Paul Makles 92f4b44cb3 chore: clarify cargo deny / tomls 2025-04-29 15:39:10 +01:00
Paul Makles 370442bf4e feat: add crond container definitions 2025-04-29 15:39:10 +01:00
Paul Makles d54c5e80fa chore: fix version references 2025-04-29 15:39:10 +01:00
Paul Makles a42746c55b feat: file deletion implementation 2025-04-29 15:39:10 +01:00
Paul Makles 9a98c53aff feat: init crond crate 2025-04-29 15:38:13 +01:00
Paul Makles 4ea08a189d fix: match new error type for status code 2025-04-29 15:37:42 +01:00
Paul Makles ae15a0b4c7 fix: don't allow users to time themselves out
closes #376
2025-04-29 15:37:42 +01:00
Paul Makles 9e5fbec9b0 chore: disable LTO because it likely overloads GitHub worker 2025-04-29 15:36:56 +01:00
Paul Makles 0b02c84f04 chore: bump version to 0.8.2 2025-04-29 15:36:56 +01:00
Paul Makles 8bfe3ec6b0 fix: ensure ratelimiter adjusts for version prefix 2025-04-29 15:36:56 +01:00
Paul Makles a7345a7b90 chore: mount API at /0.8/ as well 2025-04-29 15:36:56 +01:00
Paul Makles 12854d0f1b fix: change permission check for fetching channel webhooks 2025-04-29 15:36:56 +01:00
Paul Makles 9a2a7901e7 fix: ensure limit is always at least 1 when sent to database 2025-04-29 15:36:56 +01:00
Paul Makles e8cb0bb88a docs: add some notes on overrides; fix defaults 2025-04-29 15:36:55 +01:00
Paul Makles 343b90b9af refactor: add error messages for Rabbit issues 2025-04-29 15:36:55 +01:00
Paul Makles a4246f39e4 chore: add pushd to debug image script 2025-04-29 15:36:55 +01:00
Zomatree 38380db6e7 refactor: Remove VoiceChannel 2025-04-29 15:05:18 +01:00
Zomatree 091aa4705b fix: use existing node if someone is already in the voice channel 2025-03-25 20:37:06 +00:00
Zomatree 8b5e085232 feat: being moved between voice channels 2025-03-01 22:09:40 +00:00
Zomatree 9c2cc8736c fix: respond with the url when joining a voice channel 2025-02-17 02:11:32 +00:00
Zomatree 951128032b fix: expose list of nodes 2025-02-17 01:48:34 +00:00
Zomatree 367ac887f4 feat: support multiple voice nodes 2025-02-17 01:39:00 +00:00
Zomatree 060b4c43f4 feat: make Channel::server return a reference 2025-02-07 21:35:51 +00:00
Zomatree 2593a4272b chore: switch to ids for parameter 2025-02-02 13:46:45 +00:00
Zomatree 742eb3bb7d fix: remove local testing values from config 2025-01-28 16:06:58 +00:00
Zomatree 41a6de67cd fix: remove locked rocket version 2025-01-28 15:59:58 +00:00
Zomatree c8ae34ba73 feat: implement missing permission syncs 2025-01-28 15:55:21 +00:00
Zomatree c55b5bf75f fix: include voice states in servercreate event
feat: call started system message in dms
2025-01-27 20:46:29 +00:00
Zomatree 9de87a01ab Merge remote-tracking branch 'origin/main' into livekit 2025-01-26 20:36:40 +00:00
Zomatree 2f7294705e feat(livekit): push unfinished code 2024-10-13 20:23:00 +01:00
Zomatree 120ca449b8 feat(livekit): more permission handling 2024-07-23 04:07:34 +01:00
Zomatree ffbc899792 split voice ops into its own library 2024-05-13 14:09:08 +01:00
Zomatree 675c7d6c0d fix redis key inconsistancy 2024-04-24 02:29:31 +01:00
Zomatree 67569f932e more stuff 2024-04-24 02:23:52 +01:00
Zomatree 7a4421089c track current state 2024-04-11 01:07:03 +01:00
Zomatree ae87bc0b67 move voice states to global 2024-04-09 22:36:48 +01:00
Zomatree ad0dcad497 initial livekit support
fix up code

undo changes to compose file

add back .env.example
2024-04-09 21:21:52 +01:00
109 changed files with 787 additions and 31901 deletions
+37
View File
@@ -0,0 +1,37 @@
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
+30 -37
View File
@@ -5,6 +5,8 @@ on:
tags:
- "*"
pull_request:
branches:
- "main"
paths:
- "Dockerfile"
@@ -12,10 +14,6 @@ permissions:
contents: read
packages: write
concurrency:
group: ${{ github.head_ref }}
cancel-in-progress: true
jobs:
base:
name: Test base image build
@@ -51,6 +49,13 @@ 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:
@@ -67,13 +72,14 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest
# stoatchat/api
# revoltchat/server
- name: Docker meta
id: meta-delta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/api
docker.io/revoltchat/server
ghcr.io/revoltchat/server
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -86,13 +92,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-delta.outputs.labels }}
# stoatchat/events
# revoltchat/bonfire
- name: Docker meta
id: meta-bonfire
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/events
docker.io/revoltchat/bonfire
ghcr.io/revoltchat/bonfire
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -105,13 +112,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-bonfire.outputs.labels }}
# stoatchat/file-server
# revoltchat/autumn
- name: Docker meta
id: meta-autumn
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/file-server
docker.io/revoltchat/autumn
ghcr.io/revoltchat/autumn
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -124,13 +132,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-autumn.outputs.labels }}
# stoatchat/proxy
# revoltchat/january
- name: Docker meta
id: meta-january
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/proxy
docker.io/revoltchat/january
ghcr.io/revoltchat/january
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -143,13 +152,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-january.outputs.labels }}
# stoatchat/gifbox
# revoltchat/gifbox
- name: Docker meta
id: meta-gifbox
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/gifbox
docker.io/revoltchat/gifbox
ghcr.io/revoltchat/gifbox
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -162,13 +172,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-gifbox.outputs.labels }}
# stoatchat/crond
# revoltchat/crond
- name: Docker meta
id: meta-crond
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/crond
docker.io/revoltchat/crond
ghcr.io/revoltchat/crond
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -181,13 +192,14 @@ jobs:
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta-crond.outputs.labels }}
# stoatchat/pushd
# revoltchat/pushd
- name: Docker meta
id: meta-pushd
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/pushd
docker.io/revoltchat/pushd
ghcr.io/revoltchat/pushd
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -199,22 +211,3 @@ jobs:
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
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:latest
labels: ${{ steps.meta-voice-ingress.outputs.labels }}
-29
View File
@@ -1,29 +0,0 @@
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
- uses: pnpm/action-setup@v4
with:
package_json_file: ./docs/package.json
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
cache-dependency-path: ./docs/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
- name: Test build website
run: pnpm run build
-54
View File
@@ -1,54 +0,0 @@
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
- uses: pnpm/action-setup@v4
with:
package_json_file: ./docs/package.json
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: pnpm
cache-dependency-path: ./docs/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install
- name: Build website
run: pnpm run 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
+12 -12
View File
@@ -2,12 +2,16 @@ name: Rust build, test, and generate specification
on:
push:
branches: [main]
paths-ignore:
- ".github/**"
- "!.github/workflows/rust.yaml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request:
concurrency:
group: ${{ github.head_ref }}
cancel-in-progress: true
branches: [main]
env:
CARGO_TERM_COLOR: always
@@ -18,10 +22,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Free up disk space
run: |
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
- name: Install latest stable
uses: actions-rs/toolchain@v1
with:
@@ -32,7 +32,7 @@ jobs:
uses: baptiste0928/cargo-install@v1
with:
crate: cargo-nextest
args: --locked
locked: true
- name: Run cargo build
uses: actions-rs/cargo@v1
@@ -73,9 +73,9 @@ jobs:
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: actions/checkout@v3
with:
repository: stoatchat/javascript-client-api
repository: revoltchat/api
path: api
ssh-key: ${{ secrets.DEPLOY_KEY_JAVASCRIPT_CLIENT_API }}
token: ${{ secrets.PAT }}
- name: Download OpenAPI specification
if: github.event_name != 'pull_request' && github.ref_name == 'main'
+54
View File
@@ -0,0 +1,54 @@
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
View File
@@ -0,0 +1,79 @@
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
-20
View File
@@ -1,20 +0,0 @@
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@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+2 -1
View File
@@ -12,4 +12,5 @@ venv/
.DS_Store
livekit.yml
.idea
.idea
start
Generated
+6 -130
View File
@@ -443,25 +443,22 @@ dependencies = [
[[package]]
name = "authifier"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a03b810b342b4c584cdc1cfc40d1ec763b1d653ef4086f7494740f35d9e1f0"
dependencies = [
"async-std",
"async-trait",
"base32",
"base64 0.22.1",
"bson",
"chrono",
"form_urlencoded",
"futures",
"handlebars",
"iso8601-timestamp",
"jsonwebtoken",
"lazy_static",
"lettre",
"log",
"mime",
"mongodb",
"nanoid",
"oauth2-types",
"rand 0.8.5",
"regex",
"reqwest 0.11.27",
@@ -472,9 +469,7 @@ dependencies = [
"schemars 0.8.22",
"serde",
"serde_json",
"serde_urlencoded",
"sha1",
"sha2",
"totp-lite",
"ulid 0.5.0",
"validator 0.15.0",
@@ -4034,11 +4029,9 @@ checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
dependencies = [
"base64 0.22.1",
"js-sys",
"pem 3.0.5",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]]
@@ -4059,7 +4052,7 @@ dependencies = [
"p256 0.13.2",
"p384",
"rand 0.8.5",
"rsa 0.7.2",
"rsa",
"serde",
"serde_json",
"spki 0.6.0",
@@ -4251,15 +4244,6 @@ dependencies = [
"log",
]
[[package]]
name = "language-tags"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
dependencies = [
"serde",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -4672,47 +4656,6 @@ dependencies = [
"tendril",
]
[[package]]
name = "mas-iana"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41af7e8eb3584b648c563a1b97b8f60c7f3dcd7ea0ded525050418d90c5200"
dependencies = [
"schemars 0.8.22",
"serde",
]
[[package]]
name = "mas-jose"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb6a1c99221601a1e9ef284efaa6db5985389c839c299d71d5a4c2933ba88eda"
dependencies = [
"base64ct",
"chrono",
"digest",
"ecdsa 0.16.9",
"elliptic-curve 0.13.8",
"generic-array 0.14.7",
"hmac",
"k256",
"mas-iana",
"p256 0.13.2",
"p384",
"rand 0.8.5",
"rsa 0.9.9",
"schemars 0.8.22",
"sec1 0.7.3",
"serde",
"serde_json",
"serde_with",
"sha2",
"signature 2.2.0",
"thiserror 1.0.69",
"tracing",
"url",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
@@ -5203,26 +5146,6 @@ dependencies = [
"libc",
]
[[package]]
name = "oauth2-types"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db9baa46cfc1969e04a7f8c31ee6718a8b4cb52ef31d7d91772f878052d872e1"
dependencies = [
"chrono",
"data-encoding",
"http 1.3.1",
"language-tags",
"mas-iana",
"mas-jose",
"serde",
"serde_json",
"serde_with",
"sha2",
"thiserror 1.0.69",
"url",
]
[[package]]
name = "object"
version = "0.36.7"
@@ -5717,17 +5640,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der 0.7.10",
"pkcs8 0.10.2",
"spki 0.7.3",
]
[[package]]
name = "pkcs8"
version = "0.9.0"
@@ -6688,7 +6600,6 @@ name = "revolt-config"
version = "0.8.9"
dependencies = [
"async-std",
"authifier",
"cached",
"config",
"futures-locks",
@@ -6994,7 +6905,6 @@ dependencies = [
"revolt_rocket_okapi",
"rocket",
"schemars 0.8.22",
"sentry",
"serde",
"serde_json",
"utoipa",
@@ -7216,11 +7126,11 @@ dependencies = [
[[package]]
name = "rocket_authifier"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1f98a5be96ac4144ee1adc809c1594e8212032b8a08d9bc4cef70735dfd976e"
dependencies = [
"async-std",
"authifier",
"iso8601-timestamp",
"mongodb",
"revolt_okapi",
"revolt_rocket_okapi",
"rocket",
@@ -7339,7 +7249,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"pkcs1 0.4.1",
"pkcs1",
"pkcs8 0.9.0",
"rand_core 0.6.4",
"signature 1.6.4",
@@ -7348,26 +7258,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rsa"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid 0.9.6",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1 0.7.5",
"pkcs8 0.10.2",
"rand_core 0.6.4",
"signature 2.2.0",
"spki 0.7.3",
"subtle",
"zeroize",
]
[[package]]
name = "rust-argon2"
version = "1.0.1"
@@ -7605,13 +7495,11 @@ version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
dependencies = [
"chrono",
"dyn-clone",
"indexmap 1.9.3",
"schemars_derive",
"serde",
"serde_json",
"url",
]
[[package]]
@@ -8203,18 +8091,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simple_asn1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror 2.0.16",
"time",
]
[[package]]
name = "simplecss"
version = "0.2.2"
+1 -1
View File
@@ -54,7 +54,7 @@ use_starttls = false
url = "http://livekit"
lat = 0.0
lon = 0.0
key = "worldwide"
key = "worldwide_key"
secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0"
[files.s3]
+73
View File
@@ -0,0 +1,73 @@
# 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.
-7
View File
@@ -64,10 +64,3 @@ services:
MAILDEV_WEB_PORT: 8080
MAILDEV_INCOMING_USER: smtp
MAILDEV_INCOMING_PASS: smtp
livekit:
image: ghcr.io/stoatchat/livekit-server:v1.9.9
command: --config /etc/livekit.yml
network_mode: "host"
volumes:
- ./livekit.yml:/etc/livekit.yml
+1 -1
View File
@@ -41,7 +41,7 @@ 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", features = ["voice"] }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.9", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage
+29 -53
View File
@@ -1,4 +1,4 @@
use std::collections::{HashMap, HashSet};
use std::collections::HashSet;
use futures::future::join_all;
use revolt_database::{
@@ -124,7 +124,12 @@ impl State {
.unwrap_or_default();
// Fetch all memberships with their corresponding servers.
let mut members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
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 server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
let servers = db.fetch_servers(&server_ids).await?;
@@ -153,51 +158,6 @@ 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(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;
@@ -211,12 +171,6 @@ impl State {
)
.await?;
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
// Fetch customisations.
let emojis = if fields.emojis {
Some(
@@ -258,6 +212,28 @@ impl State {
None
};
let voice_states = if fields.voice_states {
// fetch voice states for all the channels we can see
let mut voice_states = Vec::new();
for channel in channels.iter().filter(|c| {
matches!(
c,
Channel::DirectMessage { .. }
| Channel::Group { .. }
| Channel::TextChannel { voice: Some(_), .. }
)
}) {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
voice_states.push(voice_state)
}
}
Some(voice_states)
} else {
None
};
// Copy data into local state cache.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
self.cache
+14 -1
View File
@@ -1,7 +1,9 @@
use std::env;
use std::{env, sync::Arc};
use async_std::net::TcpListener;
use revolt_presence::clear_region;
use once_cell::sync::OnceCell;
use revolt_database::voice::VoiceClient;
#[macro_use]
extern crate log;
@@ -12,6 +14,15 @@ pub mod events;
mod database;
mod websocket;
pub static VOICE_CLIENT: OnceCell<Arc<VoiceClient>> = OnceCell::new();
pub fn get_voice_client() -> Arc<VoiceClient> {
VOICE_CLIENT
.get()
.expect("get_voice_client called before set")
.clone()
}
#[async_std::main]
async fn main() {
// Configure requirements for Bonfire.
@@ -24,6 +35,8 @@ async fn main() {
clear_region(None).await;
}
VOICE_CLIENT.set(Arc::new(VoiceClient::from_revolt_config().await)).unwrap();
// Setup a TCP listener to accept WebSocket connections on.
// By default, we bind to port 14703 on all interfaces.
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into());
-3
View File
@@ -38,6 +38,3 @@ sentry-anyhow = { version = "0.38.1", optional = true }
# Core
revolt-result = { version = "0.8.9", path = "../result", optional = true }
# Authifier
authifier = "1.0.15"
+1 -3
View File
@@ -74,8 +74,6 @@ max_concurrent_connections = 50
# How long to ring devices for when calling in dms/groups, in seconds
call_ring_duration = 30
[api.sso]
[api.livekit.nodes]
[api.users]
@@ -96,7 +94,7 @@ 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
dm_call_queue = "notifications.ingest.dm_call" # friend request received
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices
-2
View File
@@ -5,7 +5,6 @@ use config::{Config, File, FileFormat};
use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
use authifier::config::SSO;
#[cfg(feature = "sentry")]
pub use sentry::{capture_error, capture_message, Level};
@@ -230,7 +229,6 @@ pub struct Api {
pub workers: ApiWorkers,
pub livekit: ApiLiveKit,
pub users: ApiUsers,
pub sso: SSO,
}
#[derive(Deserialize, Debug, Clone)]
+3 -4
View File
@@ -18,7 +18,6 @@ async-std-runtime = ["async-std", "authifier/async-std-runtime"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi", "authifier/rocket_impl"]
axum-impl = ["axum"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
voice = ["livekit-api", "livekit-protocol", "livekit-runtime"]
# Default Features
default = ["mongodb", "async-std-runtime", "tasks"]
@@ -99,6 +98,6 @@ authifier = { version = "1.0.15" }
amqprs = { version = "1.7.0" }
# Voice
livekit-api = { version = "0.4.4", optional = true}
livekit-protocol = { version = "0.4.0", optional = true }
livekit-runtime = { version = "0.3.1", features = ["tokio"], optional = true }
livekit-api = "0.4.4"
livekit-protocol = "0.4.0"
livekit-runtime = { version = "0.3.1", features = ["tokio"] }
-3
View File
@@ -209,8 +209,6 @@ impl Database {
} else {
EmailVerificationConfig::Disabled
},
sso: config.api.sso.clone(),
server_url: Some(config.hosts.api.parse().expect("Failed to parse API host url.")),
..Default::default()
};
@@ -246,7 +244,6 @@ impl Database {
event_channel: Some(crate::tasks::authifier_relay::sender()),
#[cfg(not(feature = "tasks"))]
event_channel: None,
..Default::default()
}
}
}
+1 -3
View File
@@ -295,9 +295,7 @@ pub enum EventV1 {
},
UserMoveVoiceChannel {
node: String,
from: String,
to: String,
token: String,
token: String
}
}
-1
View File
@@ -112,7 +112,6 @@ pub mod tasks;
mod amqp;
pub use amqp::amqp::AMQP;
#[cfg(feature = "voice")]
pub mod voice;
@@ -25,7 +25,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 49; // MUST BE +1 to last migration
pub const LATEST_REVISION: i32 = 48; // MUST BE +1 to last migration
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -1246,8 +1246,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to update voice channels");
};
if revision <= 48 {
info!("Running migration [revision 48 / 22-10-2025]: Add Video + Listen to default permissions");
if revision <= 47 {
info!("Running migration [revision 47 / 29-04-2025]: Add Video to default permissions");
db.col::<Document>("servers")
.update_many(
@@ -1255,7 +1255,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! {
"$bit": {
"default_permissions": {
"or": (ChannelPermission::Video + ChannelPermission::Speak + ChannelPermission::Listen) as i64
"or": ChannelPermission::Video as i64
},
}
}
@@ -443,8 +443,7 @@ impl Message {
}
// Verify replies are valid.
let mut replies = Vec::new();
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > config.features.limits.global.message_replies {
return Err(create_error!(TooManyReplies {
@@ -452,8 +451,6 @@ impl Message {
}));
}
replies.reserve(entries.len());
for ReplyIntent {
id,
mention,
@@ -467,12 +464,7 @@ impl Message {
user_mentions.insert(message.author.to_owned());
}
// 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);
}
replies.insert(message.id);
}
// If the referenced message doesn't exist and fail_if_not_exists
// is set to false, send the message without the reply.
@@ -545,7 +537,9 @@ impl Message {
}
if !replies.is_empty() {
message.replies.replace(replies);
message
.replies
.replace(replies.into_iter().collect::<Vec<String>>());
}
// Calculate final message flags
@@ -1,6 +1,7 @@
use iso8601_timestamp::Timestamp;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use crate::voice::get_channel_voice_state;
use crate::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel,
@@ -147,12 +148,10 @@ 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(channel).await {
if let Ok(Some(voice_state)) = get_channel_voice_state(channel).await {
voice_states.push(voice_state)
}
}
@@ -336,8 +336,8 @@ impl IntoDocumentPath for FieldsMember {
FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout",
FieldsMember::CanPublish => "can_publish",
FieldsMember::CanReceive => "can_receive",
FieldsMember::CanPublish => "is_publishing",
FieldsMember::CanReceive => "is_receiving",
})
}
}
+6 -40
View File
@@ -16,7 +16,7 @@ mod voice_client;
pub use voice_client::VoiceClient;
async fn get_connection() -> Result<Conn> {
_get_connection().await.map_err(|_| create_error!(InternalError))
_get_connection().await.to_internal_error()
}
pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
@@ -35,7 +35,7 @@ pub async fn raise_if_in_voice(user: &User, channel_id: &str) -> Result<()> {
.to_internal_error()?
> 0
{
Err(create_error!(AlreadyConnected))
Err(create_error!(NotConnected))
} else {
Ok(())
}
@@ -412,9 +412,7 @@ pub async fn sync_voice_permissions(
server: Option<&Server>,
role_id: Option<&str>,
) -> Result<()> {
let Some(node) = get_channel_node(channel.id()).await? else {
return Ok(());
};
let node = get_channel_node(channel.id()).await?.unwrap();
for user_id in get_voice_channel_members(channel.id())
.await?
@@ -456,9 +454,9 @@ pub async fn sync_user_voice_permissions(
.as_ref()
.is_none_or(|member| member.roles.iter().any(|r| r == role_id))
}) {
let Some(voice_state) = get_voice_state(channel_id, server_id, &user.id).await? else {
return Ok(());
};
let voice_state = get_voice_state(channel_id, server_id, &user.id)
.await?
.unwrap();
let mut query = DatabasePermissionQuery::new(db, user)
.channel(channel)
@@ -564,35 +562,3 @@ pub async fn get_call_notification_recipients(
.await
.to_internal_error()
}
pub async fn remove_user_from_voice_channels(db: &Database, voice_client: &VoiceClient, user_id: &str) -> Result<()> {
for channel_id in get_user_voice_channels(user_id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, user_id).await?;
};
Ok(())
}
pub async fn remove_user_from_voice_channel(db: &Database, voice_client: &VoiceClient, channel_id: &str, user_id: &str) -> Result<()> {
if let Some(node) = get_channel_node(channel_id).await? {
let _ = voice_client.remove_user(&node, user_id, channel_id).await;
}
let channel = Reference::from_unchecked(channel_id).as_channel(db).await?;
delete_voice_state(channel_id, channel.server(), user_id).await?;
Ok(())
}
pub async fn delete_voice_channel(voice_client: &VoiceClient, channel_id: &str, server_id: Option<&str>) -> Result<()> {
if let Some(users) = get_voice_channel_members(channel_id).await? {
let node = get_channel_node(channel_id).await?.unwrap();
voice_client.delete_room(&node, channel_id).await?;
delete_channel_voice_state(channel_id, server_id, &users).await?;
};
Ok(())
}
+3 -3
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Stoat Logo" class="w-24" src="https://stoat.chat/favicon.svg" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
@@ -107,7 +107,7 @@
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Stoat<br>
Sent from Revolt<br>
Made in Europe
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -1,37 +1,38 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img alt="Stoat Logo" class="ax-center my-10 w-24" src="https://stoat.chat/favicon.svg" />
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Account Deletion</h1>
<p>
You requested to have your account deleted, if you did not perform
this action please take measures to secure your account immediately.
</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Account Deletion</h1>
<p>
You requested to have your account deleted, if you did not perform
this action please take measures to secure your account immediately.
</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Stoat<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
</body>
</html>
+1 -1
View File
@@ -3,7 +3,7 @@ You requested to have your account deleted, if you did not perform this action p
Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Stoat
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
@@ -4,6 +4,6 @@ Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Stoat or Revolt Platforms Ltd.
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.stoat.chat/faq.html
https://developers.revolt.chat/faq.html
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img class="w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
<img class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
@@ -120,7 +120,7 @@
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Stoat<br>
Sent from Revolt<br>
Made in Europe
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -9,8 +9,7 @@
<div class="container">
<img
class="ax-center my-10 w-24"
src="https://stoat.chat/favicon.svg"
alt="Stoat Logo"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Password Reset</h1>
@@ -26,7 +25,7 @@
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Stoat<br />
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
@@ -8,7 +8,7 @@ password on it, click below to continue.
Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Stoat
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
+3 -3
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img class="w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
<img class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
@@ -104,7 +104,7 @@
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Stoat<br>
Sent from Revolt<br>
Made in Europe
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -1,34 +1,34 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img class="ax-center my-10 w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" />
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Password Reset</h1>
<p>You requested a password reset, click below to continue.</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Reset</a>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Password Reset</h1>
<p>You requested a password reset, click below to continue.</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Reset</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Stoat<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
</body>
</html>
+1 -1
View File
@@ -3,7 +3,7 @@ You requested a password reset, if you did not perform this action you can safel
Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Stoat
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img class="w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
@@ -134,7 +134,7 @@
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Stoat<br>
Sent from Revolt<br>
Made in Europe
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -1,45 +1,46 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img class="ax-center my-10 w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" />
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Account Suspended</h1>
<p>Your account has been suspended, for one or more reasons:</p>
<ul>
{{list}}
</ul>
<p style="display:{{duration_display}}">
You will be able to use your account again in {{duration}} days.
</p>
<p>
Further violations may result in a permanent ban depending on
severity, please abide by the
<a href="https://revolt.chat/aup">Acceptable Usage Policy</a>.
</p>
<p>Ban evasion is prohibited and will be dealt with accordingly.</p>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Account Suspended</h1>
<p>Your account has been suspended, for one or more reasons:</p>
<ul>
{{list}}
</ul>
<p style="display:{{duration_display}}">
You will be able to use your account again in {{duration}} days.
</p>
<p>
Further violations may result in a permanent ban depending on
severity, please abide by the
<a href="https://revolt.chat/aup">Acceptable Usage Policy</a>.
</p>
<p>Ban evasion is prohibited and will be dealt with accordingly.</p>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Stoat<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
</body>
</html>
@@ -8,7 +8,7 @@ Further violations may result in a permanent ban depending on severity, please a
Ban evasion is prohibited and will be dealt with accordingly.
This email is intended for {{email}}
Sent by Stoat
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
+3 -3
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.6.0 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
@@ -42,7 +42,7 @@
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img class="w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
@@ -104,7 +104,7 @@
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Stoat<br>
Sent from Revolt<br>
Made in Europe
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
@@ -1,34 +1,35 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img class="ax-center my-10 w-24" src="https://stoat.chat/favicon.svg" alt="Stoat Logo" />
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Almost there!</h1>
<p>To complete your sign up, we just need to verify your email.</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Almost there!</h1>
<p>To complete your sign up, we just need to verify your email.</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Stoat<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
</body>
</html>
+1 -1
View File
@@ -4,7 +4,7 @@ To complete your sign up, we just need to verify your email.
Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Stoat
Sent by Revolt
Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
+1 -4
View File
@@ -15,9 +15,8 @@ utoipa = ["dep:utoipa"]
rocket = ["dep:rocket", "dep:serde_json"]
axum = ["dep:axum", "dep:serde_json"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
sentry = ["dep:sentry"]
default = ["serde", "sentry"]
default = ["serde"]
[dependencies]
# Serialisation
@@ -37,5 +36,3 @@ revolt_okapi = { version = "0.9.1", optional = true }
log = "0.4"
# Axum
axum = { version = "0.7.5", optional = true }
sentry = { version = "0.31.5", optional = true }
-1
View File
@@ -62,7 +62,6 @@ impl IntoResponse for Error {
ErrorType::NotPrivileged => StatusCode::FORBIDDEN,
ErrorType::CannotGiveMissingPermissions => StatusCode::FORBIDDEN,
ErrorType::NotOwner => StatusCode::FORBIDDEN,
ErrorType::IsElevated => StatusCode::FORBIDDEN,
ErrorType::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
+2 -7
View File
@@ -139,7 +139,6 @@ pub enum ErrorType {
NotPrivileged,
CannotGiveMissingPermissions,
NotOwner,
IsElevated,
// ? General errors
DatabaseError {
@@ -227,17 +226,13 @@ pub trait ToRevoltError<T> {
fn to_internal_error(self) -> Result<T, Error>;
}
impl<T, E: std::fmt::Debug + std::error::Error> ToRevoltError<T> for Result<T, E> {
impl<T, E: std::fmt::Debug> ToRevoltError<T> for Result<T, E> {
#[track_caller]
fn to_internal_error(self) -> Result<T, Error> {
let loc = Location::caller();
self
.map_err(|e| {
log::error!("{e:?}");
#[cfg(feature = "sentry")]
sentry::capture_error(&e);
.map_err(|_| {
Error {
error_type: ErrorType::InternalError,
location: format!("{}:{}:{}", loc.file(), loc.line(), loc.column())
-1
View File
@@ -69,7 +69,6 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotPrivileged => Status::Forbidden,
ErrorType::CannotGiveMissingPermissions => Status::Forbidden,
ErrorType::NotOwner => Status::Forbidden,
ErrorType::IsElevated => Status::Forbidden,
ErrorType::DatabaseError { .. } => Status::InternalServerError,
ErrorType::InternalError => Status::InternalServerError,
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage
+1 -1
View File
@@ -36,7 +36,7 @@ async-std = { version = "1.8.0", features = [
revolt-result = { path = "../../core/result" }
revolt-models = { path = "../../core/models" }
revolt-config = { path = "../../core/config" }
revolt-database = { path = "../../core/database", features = ["voice"] }
revolt-database = { path = "../../core/database" }
revolt-permissions = { path = "../../core/permissions" }
# Voice
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage
+116 -134
View File
@@ -1,3 +1,4 @@
use chrono::DateTime;
use livekit_api::{access_token::TokenVerifier, webhooks::WebhookReceiver};
use livekit_protocol::TrackType;
use revolt_database::{
@@ -5,15 +6,18 @@ use revolt_database::{
iso8601_timestamp::{Duration, Timestamp},
util::reference::Reference,
voice::{
create_voice_state, delete_voice_state,
get_user_moved_from_voice, get_user_moved_to_voice,
create_voice_state, delete_voice_state, get_call_notification_recipients,
get_user_moved_from_voice, get_user_moved_to_voice, get_voice_channel_members,
set_channel_call_started_system_message, take_channel_call_started_system_message,
update_voice_state_tracks, VoiceClient,
},
Database, AMQP,
Database, PartialMessage, SystemMessage, AMQP,
};
use revolt_models::v0;
use revolt_result::{Result, ToRevoltError};
use rocket::{post, State};
use rocket_empty::EmptyResponse;
use ulid::Ulid;
use crate::guard::AuthHeader;
@@ -21,7 +25,7 @@ use crate::guard::AuthHeader;
pub async fn ingress(
db: &State<Database>,
voice_client: &State<VoiceClient>,
_amqp: &State<AMQP>,
amqp: &State<AMQP>,
node: &str,
auth_header: AuthHeader<'_>,
body: &str,
@@ -86,51 +90,50 @@ pub async fn ingress(
.await;
};
// TODO: fix `num_participants` being incorrect sometimes see (#457)
// First user who joined - send call started system message.
// if event.room.as_ref().unwrap().num_participants == 1 {
// let user = Reference::from_unchecked(user_id).as_user(db).await?;
if event.room.as_ref().unwrap().num_participants == 1 {
let user = Reference::from_unchecked(user_id).as_user(db).await?;
// let message_id =
// Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap())
// .to_string();
let message_id =
Ulid::from_datetime(DateTime::from_timestamp_secs(event.created_at).unwrap())
.to_string();
// let mut call_started_message = SystemMessage::CallStarted {
// by: user_id.to_string(),
// finished_at: None,
// }
// .into_message(channel.id().to_string());
let mut call_started_message = SystemMessage::CallStarted {
by: user_id.to_string(),
finished_at: None,
}
.into_message(channel.id().to_string());
// call_started_message.id = message_id;
call_started_message.id = message_id;
// set_channel_call_started_system_message(channel.id(), &call_started_message.id)
// .await?;
set_channel_call_started_system_message(channel.id(), &call_started_message.id)
.await?;
// call_started_message
// .send(
// db,
// Some(amqp),
// v0::MessageAuthor::System {
// username: &user.username,
// avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
// },
// None,
// None,
// &channel,
// false,
// )
// .await?;
call_started_message
.send(
db,
Some(amqp),
v0::MessageAuthor::System {
username: &user.username,
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
},
None,
None,
&channel,
false,
)
.await?;
// let recipients = get_call_notification_recipients(&channel_id, &user_id).await?;
// let now = joined_at.format_short().to_string();
let recipients = get_call_notification_recipients(&channel_id, &user_id).await?;
let now = joined_at.format_short().to_string();
// if let Err(e) = amqp
// .dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients)
// .await
// {
// revolt_config::capture_error(&e);
// }
// }
if let Err(e) = amqp
.dm_call_updated(&user.id, channel.id(), Some(&now), false, recipients)
.await
{
revolt_config::capture_error(&e);
}
}
}
// User left a channel
"participant_left" => {
@@ -154,52 +157,50 @@ pub async fn ingress(
.await;
};
// See above for why this is commented out
// Update CallStarted system message if everyone has left with the end time
let members = get_voice_channel_members(channel_id).await?;
// // Update CallStarted system message if everyone has left with the end time
// let members = get_voice_channel_members(channel_id).await?;
if members.is_none_or(|m| m.is_empty()) {
// The channel is empty so send out an "end" message for ringing
if let Err(e) = amqp
.dm_call_updated(user_id, channel_id, None, true, None)
.await
{
revolt_config::capture_internal_error!(&e);
}
// if members.is_none_or(|m| m.is_empty()) {
// // The channel is empty so send out an "end" message for ringing
// if let Err(e) = amqp
// .dm_call_updated(user_id, channel_id, None, true, None)
// .await
// {
// revolt_config::capture_internal_error!(&e);
// }
if let Some(system_message_id) =
take_channel_call_started_system_message(channel_id).await?
{
// Could have been deleted
if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
.as_message(db)
.await
{
if let Some(SystemMessage::CallStarted { finished_at, .. }) =
&mut message.system
{
*finished_at = Some(Timestamp::now_utc());
// if let Some(system_message_id) =
// take_channel_call_started_system_message(channel_id).await?
// {
// // Could have been deleted
// if let Ok(mut message) = Reference::from_unchecked(&system_message_id)
// .as_message(db)
// .await
// {
// if let Some(SystemMessage::CallStarted { finished_at, .. }) =
// &mut message.system
// {
// *finished_at = Some(Timestamp::now_utc());
// message
// .update(
// db,
// PartialMessage {
// system: message.system.clone(),
// ..Default::default()
// },
// Vec::new(),
// )
// .await?;
// } else {
// log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
// }
// };
// };
// }
message
.update(
db,
PartialMessage {
system: message.system.clone(),
..Default::default()
},
Vec::new(),
)
.await?;
} else {
log::error!("Broken State: Call started message ID ({}) does not contain a CallStarted system message.", &message.id)
}
};
};
}
}
// Audio/video track was started/stopped/unmuted/muted
"track_published" | "track_unpublished" | "track_unmuted" | "track_muted" => {
// Audio/video track was started/stopped
"track_published" | "track_unpublished" => {
let channel_id = channel_id.to_internal_error()?;
let user_id = user_id.to_internal_error()?;
let track = event.track.as_ref().to_internal_error()?;
@@ -213,59 +214,40 @@ pub async fn ingress(
// forbid any size which goes over the limit and also limit the aspect ratio to stop people from making too tall or too wide and bypassing the limit.
// TODO: figure out how to track audio stream quality
if event.event == "track_published" {
let mut disconnect = false;
if event.event == "track_published"
&& (track.r#type == TrackType::Data as i32
|| (track.r#type == TrackType::Video as i32
&& (user_limits.video_resolution[0] != 0
&& user_limits.video_resolution[1] != 0
&& track.width * track.height
> user_limits.video_resolution[0]
* user_limits.video_resolution[1])
|| (user_limits.video_aspect_ratio[0]
!= user_limits.video_aspect_ratio[1]
&& !(user_limits.video_aspect_ratio[0]
..=user_limits.video_aspect_ratio[1])
.contains(&(track.width as f32 / track.height as f32)))))
{
voice_client.remove_user(node, user_id, channel_id).await?;
delete_voice_state(channel_id, channel.server(), user_id).await?;
} else {
let partial = update_voice_state_tracks(
channel_id,
channel.server(),
user_id,
event.event == "track_published", // to avoid duplicating this entire case twice
track.source,
)
.await?;
if track.r#type == TrackType::Data as i32 {
log::debug!("User published data");
disconnect = true;
};
if track.r#type == TrackType::Video as i32 {
if user_limits.video_resolution[0] != 0
&& user_limits.video_resolution[1] != 0
&& track.width * track.height
> user_limits.video_resolution[0] * user_limits.video_resolution[1]
{
log::debug!("User published video with out of bounds resolution");
disconnect = true;
};
if user_limits.video_aspect_ratio[0] != user_limits.video_aspect_ratio[1]
&& !(user_limits.video_aspect_ratio[0]..=user_limits.video_aspect_ratio[1])
.contains(&(track.width as f32 / track.height as f32))
{
log::debug!("User published video with out of bounds aspect ratio");
disconnect = true;
};
};
if disconnect {
log::debug!("Removing user {user_id} from channel {channel_id} {event:?} due to forbidden track.");
let _ = voice_client.remove_user(node, user_id, channel_id).await;
delete_voice_state(channel_id, channel.server(), user_id).await?;
return Ok(EmptyResponse);
};
};
let partial = update_voice_state_tracks(
channel_id,
channel.server(),
user_id,
event.event == "track_published" || event.event == "track_unmuted", // to avoid duplicating this entire case twice
track.source,
)
.await?;
EventV1::UserVoiceStateUpdate {
id: user_id.clone(),
channel_id: channel_id.clone(),
data: partial,
EventV1::UserVoiceStateUpdate {
id: user_id.clone(),
channel_id: channel_id.clone(),
data: partial,
}
.p(channel_id.clone())
.await;
}
.p(channel_id.clone())
.await;
}
_ => {}
};
-1
View File
@@ -72,7 +72,6 @@ revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database", features = [
"rocket-impl",
"redis-is-patched",
"voice",
] }
revolt-models = { path = "../core/models", features = [
"schemas",
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage
+9 -2
View File
@@ -1,4 +1,4 @@
use revolt_database::{Database, User, util::reference::Reference, voice::{VoiceClient, remove_user_from_voice_channels}};
use revolt_database::{util::reference::Reference, voice::{delete_voice_state, get_channel_node, get_user_voice_channels, VoiceClient}, Database, User};
use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
@@ -21,7 +21,14 @@ pub async fn delete_bot(
bot.delete(db).await?;
remove_user_from_voice_channels(db, voice_client, &bot.id).await?;
for channel_id in get_user_voice_channels(&bot.id).await? {
let node = get_channel_node(&channel_id).await?.unwrap();
let channel = Reference::from_unchecked(&channel_id).as_channel(db).await?;
voice_client.remove_user(&node, &bot.id, &channel_id).await?;
delete_voice_state(&channel_id, channel.server(), &bot.id).await?;
}
Ok(EmptyResponse)
}
@@ -1,7 +1,9 @@
use revolt_database::{
AMQP, Channel, Database, PartialChannel, User, util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{
VoiceClient, delete_voice_channel, is_in_voice_channel, remove_user_from_voice_channel
}
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{
delete_channel_voice_state, delete_voice_state, get_channel_node, get_voice_channel_members, is_in_voice_channel, VoiceClient
},
Channel, Database, PartialChannel, User, AMQP,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -55,14 +57,26 @@ pub async fn delete(
.await?;
if is_in_voice_channel(&user.id, channel.id()).await? {
remove_user_from_voice_channel(db, voice_client, channel.id(), &user.id).await?;
let node = get_channel_node(channel.id()).await?.unwrap();
voice_client
.remove_user(&node, &user.id, channel.id())
.await?;
delete_voice_state(channel.id(), None, &user.id).await?;
};
}
Channel::TextChannel { .. } => {
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
channel.delete(db).await?;
delete_voice_channel(voice_client, channel.id(), channel.server()).await?;
if let Some(users) = get_voice_channel_members(channel.id()).await? {
let node = get_channel_node(channel.id()).await?.unwrap();
voice_client.delete_room(&node, channel.id()).await?;
delete_channel_voice_state(channel.id(), channel.server(), &users).await?;
};
}
};
@@ -1,7 +1,5 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{delete_voice_channel, VoiceClient},
Channel, Database, File, PartialChannel, SystemMessage, User, AMQP,
util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{delete_channel_voice_state, get_channel_node, get_voice_channel_members, VoiceClient}, Channel, Database, File, PartialChannel, SystemMessage, User, AMQP
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -255,12 +253,21 @@ pub async fn edit(
.update(
db,
partial,
data.remove.into_iter().map(|f| f.into()).collect(),
data.remove
.into_iter()
.map(|f| f.into())
.collect(),
)
.await?;
if channel.voice().is_none() {
delete_voice_channel(voice_client, channel.id(), channel.server()).await?;
if let Some(users) = get_voice_channel_members(channel.id()).await? {
let node = get_channel_node(channel.id()).await?.unwrap();
voice_client.delete_room(&node, channel.id()).await?;
delete_channel_voice_state(channel.id(), channel.server(), &users).await?;
};
}
Ok(Json(channel.into()))
@@ -1,8 +1,4 @@
use revolt_database::{
util::reference::Reference,
voice::{is_in_voice_channel, remove_user_from_voice_channel, VoiceClient},
Channel, Database, User, AMQP,
};
use revolt_database::{util::reference::Reference, voice::{delete_voice_state, get_channel_node, is_in_voice_channel, VoiceClient}, Channel, Database, User, AMQP};
use revolt_permissions::ChannelPermission;
use revolt_result::{create_error, Result};
@@ -28,10 +24,7 @@ pub async fn remove_member(
let channel = target.as_channel(db).await?;
if let Channel::Group {
owner, recipients, ..
} = &channel
{
if let Channel::Group { owner, recipients, .. } = &channel {
if &user.id != owner {
return Err(create_error!(MissingPermission {
permission: ChannelPermission::ManageChannel.to_string()
@@ -51,11 +44,14 @@ pub async fn remove_member(
.remove_user_from_group(db, amqp, &member, Some(&user.id), false)
.await?;
} else {
return Err(create_error!(InvalidOperation));
return Err(create_error!(InvalidOperation))
};
if is_in_voice_channel(&member.id, channel.id()).await? {
remove_user_from_voice_channel(db, voice_client, channel.id(), &member.id).await?;
if is_in_voice_channel(&user.id, channel.id()).await? {
let node = get_channel_node(channel.id()).await?.unwrap();
voice_client.remove_user(&node, &user.id, channel.id()).await?;
delete_voice_state(channel.id(), None, &user.id).await?;
};
Ok(EmptyResponse)
@@ -79,15 +79,15 @@ pub async fn call(
// should only ever loop once but just to cover our backs.
for channel_id in get_user_voice_channels(&user.id).await? {
if let Some(node) = get_channel_node(&channel_id).await? {
// if this errors its just a mismatching state - ignore and proceed to still delete our state
let _ = voice_client.remove_user(&node, &user.id, &channel_id).await;
};
let node = get_channel_node(&channel_id).await?.unwrap();
let channel = Reference::from_unchecked(&channel_id)
.as_channel(db)
.await?;
voice_client
.remove_user(&node, &user.id, &channel_id)
.await?;
delete_voice_state(&channel_id, channel.server(), &user.id).await?;
}
} else {
@@ -103,7 +103,7 @@ pub async fn call(
log::debug!("Created room {}", room.name);
if let Some(recipients) = recipients {
if room.num_participants == 0 && !recipients.is_empty() {
if room.num_participants == 0 {
set_call_notification_recipients(channel.id(), &user.id, &recipients).await?;
}
}
-4
View File
@@ -36,7 +36,6 @@ pub fn mount(config: Settings, mut rocket: Rocket<Build>) -> Rocket<Build> {
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/auth/sso" => rocket_authifier::routes::sso::routes(),
"/onboard" => onboard::routes(),
"/policy" => policy::routes(),
"/push" => push::routes(),
@@ -58,7 +57,6 @@ pub fn mount(config: Settings, mut rocket: Rocket<Build>) -> Rocket<Build> {
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/auth/sso" => rocket_authifier::routes::sso::routes(),
"/onboard" => onboard::routes(),
"/policy" => policy::routes(),
"/push" => push::routes(),
@@ -81,7 +79,6 @@ pub fn mount(config: Settings, mut rocket: Rocket<Build>) -> Rocket<Build> {
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/auth/sso" => rocket_authifier::routes::sso::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes(),
@@ -102,7 +99,6 @@ pub fn mount(config: Settings, mut rocket: Rocket<Build>) -> Rocket<Build> {
"/auth/account" => rocket_authifier::routes::account::routes(),
"/auth/session" => rocket_authifier::routes::session::routes(),
"/auth/mfa" => rocket_authifier::routes::mfa::routes(),
"/auth/sso" => rocket_authifier::routes::sso::routes(),
"/onboard" => onboard::routes(),
"/push" => push::routes(),
"/sync" => sync::routes()
@@ -2,14 +2,12 @@ use revolt_database::{Database, User};
use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// # Acknowledge Policy Changes
///
/// Accept/acknowledge changes to platform policy.
#[openapi(tag = "Policy")]
#[post("/acknowledge")]
pub async fn acknowledge_policy_changes(db: &State<Database>, user: User) -> Result<EmptyResponse> {
db.acknowledge_policy_changes(&user.id).await?;
Ok(EmptyResponse)
pub async fn acknowledge_policy_changes(db: &State<Database>, user: User) -> Result<()> {
db.acknowledge_policy_changes(&user.id).await
}
@@ -1,7 +1,6 @@
use revolt_database::{events::client::EventV1, Database, Report, Snapshot, SnapshotContent, User};
use revolt_models::v0::{ReportStatus, ReportedContent};
use revolt_result::{create_error, Result};
use rocket_empty::EmptyResponse;
use serde::Deserialize;
use ulid::Ulid;
use validator::Validate;
@@ -28,7 +27,7 @@ pub async fn report_content(
db: &State<Database>,
user: User,
data: Json<DataReportContent>,
) -> Result<EmptyResponse> {
) -> Result<()> {
let data = data.into_inner();
data.validate().map_err(|error| {
create_error!(FailedValidation {
@@ -130,5 +129,5 @@ pub async fn report_content(
EventV1::ReportCreate(report.into()).global().await;
Ok(EmptyResponse)
Ok(())
}
@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{get_user_voice_channel_in_server, remove_user_from_voice_channel, VoiceClient},
voice::{delete_voice_state, get_channel_node, get_user_voice_channel_in_server, VoiceClient},
Database, RemovalIntention, ServerBan, User,
};
use revolt_models::v0;
@@ -58,8 +58,12 @@ pub async fn ban(
.await?;
// If the member is in a voice channel while banned kick them from the voice channel
if let Some(channel_id) = get_user_voice_channel_in_server(&target.id, &server.id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, &target.id).await?;
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
let node = get_channel_node(&channel_id).await?.unwrap();
voice_client.remove_user(&node, &user.id, &channel_id).await?;
delete_voice_state(&channel_id, Some(&server.id), &user.id).await?
}
}
+3 -15
View File
@@ -51,10 +51,6 @@ pub async fn edit(
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
let permissions = calculate_server_permissions(&mut query).await;
// Fetch target permissions
let mut target_query = DatabasePermissionQuery::new(db, &target_user).server(&server).member(&member);
let target_permissions = calculate_server_permissions(&mut target_query).await;
// Check permissions in server
if data.nickname.is_some() || data.remove.contains(&v0::FieldsMember::Nickname) {
if user.id == member.id.user {
@@ -77,14 +73,8 @@ pub async fn edit(
}
if data.timeout.is_some() || data.remove.contains(&v0::FieldsMember::Timeout) {
if data.timeout.is_some() {
if member.id.user == user.id {
return Err(create_error!(CannotTimeoutYourself));
}
if target_permissions.has_channel_permission(ChannelPermission::TimeoutMembers) {
return Err(create_error!(IsElevated))
}
if data.timeout.is_some() && member.id.user == user.id {
return Err(create_error!(CannotTimeoutYourself));
}
permissions.throw_if_lacking_channel_permission(ChannelPermission::TimeoutMembers)?;
@@ -227,11 +217,9 @@ pub async fn edit(
EventV1::UserMoveVoiceChannel {
node: new_node,
from: channel,
to: new_voice_channel.id().to_string(),
token,
}
.private(target_user.id.clone())
.p_user(target_user.id.clone(), db)
.await;
};
} else if can_publish.is_some() || can_receive.is_some() || remove_contains_voice {
@@ -1,6 +1,6 @@
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
voice::{get_user_voice_channel_in_server, remove_user_from_voice_channel, VoiceClient},
voice::{delete_voice_state, get_channel_node, get_user_voice_channel_in_server, VoiceClient},
Database, RemovalIntention, User,
};
use revolt_permissions::{calculate_server_permissions, ChannelPermission};
@@ -46,8 +46,12 @@ pub async fn kick(
.remove(db, &server, RemovalIntention::Kick, false)
.await?;
if let Some(channel_id) = get_user_voice_channel_in_server(&target.id, &server.id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, &target.id).await?;
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
let node = get_channel_node(&channel_id).await?.unwrap();
voice_client.remove_user(&node, &user.id, &channel_id).await?;
delete_voice_state(&channel_id, Some(&server.id), &user.id).await?;
};
Ok(EmptyResponse)
@@ -1,9 +1,6 @@
use revolt_database::{
util::reference::Reference,
voice::{
delete_voice_channel, get_user_voice_channel_in_server, remove_user_from_voice_channel,
VoiceClient,
},
voice::{delete_channel_voice_state, delete_voice_state, get_channel_node, get_user_voice_channel_in_server, get_voice_channel_members, VoiceClient},
Database, RemovalIntention, User,
};
use revolt_models::v0;
@@ -29,13 +26,25 @@ pub async fn delete(
if server.owner == user.id {
for channel_id in &server.channels {
delete_voice_channel(voice_client, channel_id, Some(&server.id)).await?;
if let Some(users) = get_voice_channel_members(channel_id).await? {
let node = get_channel_node(channel_id).await?.unwrap();
voice_client.delete_room(&node, channel_id).await?;
delete_channel_voice_state(channel_id, Some(&server.id), &users).await?;
};
}
server.delete(db).await
} else {
if let Some(channel_id) = get_user_voice_channel_in_server(&user.id, &server.id).await? {
remove_user_from_voice_channel(db, voice_client, &channel_id, &user.id).await?;
if server.channels.iter().any(|c| c == &channel_id) {
let node = get_channel_node(&channel_id).await?.unwrap();
voice_client.remove_user(&node, &user.id, &channel_id).await?;
delete_voice_state(&channel_id, Some(&server.id), &user.id).await?;
}
};
member
-1
View File
@@ -104,7 +104,6 @@ impl TestHarness {
mfa: Default::default(),
password_reset: None,
verification: EmailVerification::Verified,
id_providers: Default::default(),
};
self.authifier
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
# Bundle Stage
FROM gcr.io/distroless/cc-debian12:nonroot
+1 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/stoatchat/base:latest AS builder
FROM ghcr.io/revoltchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage
+1
View File
@@ -0,0 +1 @@
book
+6
View File
@@ -0,0 +1,6 @@
[book]
authors = []
language = "en"
multilingual = false
src = "src"
title = "Revolt Backend"
+8
View File
@@ -0,0 +1,8 @@
# Summary
- [Introduction](./hello.md)
- [Project Structure]()
- [Creating new API features](./new_features.md)
- [Testing]()
- [Writing a new database test]()
- [Writing a new API test]()
+5
View File
@@ -0,0 +1,5 @@
# Revolt Backend
Welcome to the developer documentation for the Revolt backend.
This is very much incomplete and needs more work!
@@ -1,4 +1,4 @@
# Adding new API features
# New API features
New API features must be documented where appropriate, this document aims to cover everywhere you need to update for new features.
-20
View File
@@ -1,20 +0,0 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
-41
View File
@@ -1,41 +0,0 @@
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
## Installation
```bash
yarn
```
## Local Development
```bash
yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
## Build
```bash
yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
## Deployment
Using SSH:
```bash
USE_SSH=true yarn deploy
```
Not using SSH:
```bash
GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
-11
View File
@@ -1,11 +0,0 @@
{
pkgs ? import <nixpkgs> { },
}:
with pkgs;
pkgs.mkShell {
buildInputs = [
nodejs
nodejs.pkgs.pnpm
];
}
-10
View File
@@ -1,10 +0,0 @@
{
"label": "API",
"position": 2,
"link": {
"type": "generated-index",
"description": "Connecting and consuming the Stoat API"
},
"collapsed": false,
"collapsible": true
}
@@ -1,15 +0,0 @@
# Authentication
To authenticate with the API, you must first acquire a bot token or user token:
- **Bot:** create one from user settings in the client
- **User:** copy one from client or authenticate through API
Then you may provide these through either:
| Type | Header |
| :---: | :---------------: |
| Bot | `X-Bot-Token` |
| User | `X-Session-Token` |
When dealing with an authenticated route.
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 74 KiB

-49
View File
@@ -1,49 +0,0 @@
# Permissions
Stoat's permission system works by sequentially applying allows then denies.
## Flow Chart
Below are the high-level steps taken to determine both server and channel permissions (click to view).
<a href="./permission hierarchy.svg" target="_blank">
<img src="./permission_hierarchy.svg" style={{background: "white", padding: "1em", borderRadius: "1em"}} />
</a>
If you are looking to implement permissions in a library, I highly recommend reading either the Stoat JavaScript SDK or the backend source code since all the routines are well commented and should be relatively easy to understand.
## Values
The following permissions are currently allocated:
| Name | Value | Bitwise | Description |
| --------------------- | :-----------: | :-------: | ------------------------------------------------------- |
| `ManageChannel` | `1` | `1 << 0` | Manage the channel or channels on the server |
| `ManageServer` | `2` | `1 << 1` | Manage the server |
| `ManagePermissions` | `4` | `1 << 2` | Manage permissions on servers or channels |
| `ManageRole` | `8` | `1 << 3` | Manage roles on server |
| `ManageCustomisation` | `16` | `1 << 4` | Manage emoji on servers |
| `KickMembers` | `64` | `1 << 6` | Kick other members below their ranking |
| `BanMembers` | `128` | `1 << 7` | Ban other members below their ranking |
| `TimeoutMembers` | `256` | `1 << 8` | Timeout other members below their ranking |
| `AssignRoles` | `512` | `1 << 9` | Assign roles to members below their ranking |
| `ChangeNickname` | `1024` | `1 << 10` | Change own nickname |
| `ManageNicknames` | `2048` | `1 << 11` | Change or remove other's nicknames below their ranking |
| `ChangeAvatar` | `4096` | `1 << 12` | Change own avatar |
| `RemoveAvatars` | `8192` | `1 << 13` | Remove other's avatars below their ranking |
| `ViewChannel` | `1048576` | `1 << 20` | View a channel |
| `ReadMessageHistory` | `2097152` | `1 << 21` | Read a channel's past message history |
| `SendMessage` | `4194304` | `1 << 22` | Send a message in a channel |
| `ManageMessages` | `8388608` | `1 << 23` | Delete messages in a channel |
| `ManageWebhooks` | `16777216` | `1 << 24` | Manage webhook entries on a channel |
| `InviteOthers` | `33554432` | `1 << 25` | Create invites to this channel |
| `SendEmbeds` | `67108864` | `1 << 26` | Send embedded content in this channel |
| `UploadFiles` | `134217728` | `1 << 27` | Send attachments and media in this channel |
| `Masquerade` | `268435456` | `1 << 28` | Masquerade messages using custom nickname and avatar |
| `React` | `536870912` | `1 << 29` | React to messages with emojis |
| `Connect` | `1073741824` | `1 << 30` | Connect to a voice channel |
| `Speak` | `2147483648` | `1 << 31` | Speak in a voice call |
| `Video` | `4294967296` | `1 << 32` | Share video in a voice call |
| `MuteMembers` | `8589934592` | `1 << 33` | Mute other members with lower ranking in a voice call |
| `DeafenMembers` | `17179869184` | `1 << 34` | Deafen other members with lower ranking in a voice call |
| `MoveMembers` | `34359738368` | `1 << 35` | Move members between voice channels |
-49
View File
@@ -1,49 +0,0 @@
# Rate Limits
Stoat uses a fixed-window ratelimiting algorithm:
- You are given a set amount of calls per each named bucket.
- Any calls past this limit will result in 429 errors.
- Buckets are replenished after 10 seconds from initial request.
## Buckets
There are distinct buckets that you may be calling against, none of these affect each other and can be used up independently of one another.
| Method | Path | Limit |
| -------: | --------------------------- | :---: |
| | `/users` | 20 |
| `PATCH` | `/users/:id` | 2 |
| | `/users/:id/default_avatar` | 255 |
| | `/bots` | 10 |
| | `/channels` | 15 |
| `POST` | `/channels/:id/messages` | 10 |
| | `/servers` | 5 |
| | `/auth` | 3 |
| `DELETE` | `/auth` | 255 |
| | `/safety` | 15 |
| | `/safety/report` | 3 |
| | `/swagger` | 100 |
| | `/*` | 20 |
## Headers
There are multiple headers you can use to figure out when you can and cannot send requests, and to determine when you can next send a request.
| Header | Type | Description |
| ------------------------- | :------: | ------------------------------------------------ |
| `X-RateLimit-Limit` | `number` | Maximum number of calls allowed for this bucket. |
| `X-RateLimit-Bucket` | `string` | Unique identifier for this bucket. |
| `X-RateLimit-Remaining` | `number` | Remaining number of calls left for this bucket. |
| `X-RateLimit-Reset-After` | `number` | Milliseconds left until calls are replenished. |
## Rate Limited Response
When you receive `429 Too Many Requests`, you will also receive a JSON body with the schema:
```typescript
interface Response {
// Milliseconds until calls are replenished
retry_after: number;
}
```
@@ -1,47 +0,0 @@
# Uploading Files
File uploads work by first sending a file to the server and then using the ID provided.
You can find out what kinds of files you can upload by visiting [the API documentation](https://cdn.stoatusercontent.com/scalar).
To upload a file, pick the desired tag then send a **POST** to `{endpoint}/{tag}` along with a `multipart/form-data` body with one field `file` that contains the file you wish to upload.
You must specify session/bot authentication token as with any other API route.
You will receive the following JSON response:
```json
{
"id": "0"
}
```
You can use the ID wherever a file is required in the API.
Code sample in JavaScript using Fetch API:
```js
const body = new FormData();
body.append("file", file);
const data = await fetch(`${endpoint}/${tag}`, {
method: "POST",
body,
headers: {
"X-Session-Token": "...", // or X-Bot-Token
},
}).then((res) => res.json());
// use data.id
```
## Differences from old Autumn
If you are migrating from old Autumn, the following key points are important:
- There are only two paths that serve a unique image, the preview version of it (if available) and the original image.
- You should not specify any query parameters under any circumstance, the preview route will serve the optimal size for the content type.
- Preview routes for banners, emojis, backgrounds, and attachments will redirect to the original file where the file is not an image or the image is animated.
- If you are currently using logic to replace the URL path to start/stop animations, you should use the following templates: (NB. this only applies to avatars and icons)
- Non-animated file: `/{tag}/{file_id}`
- Animated file: `/{tag}/{file_id}/{file_name}` or `/{tag}/{file_id}/original` (if name unavailable)
-23
View File
@@ -1,23 +0,0 @@
---
sidebar_position: 1
---
# Endpoints
**We are moving stuff around currently following the rebrand, guidance will follow soon!**
You can connect to the API on the following URLs:
| URL | Release | Description |
| ----------------------------- | :--------: | --------------------------- |
| `https://api.revolt.chat` | Production | Primary API endpoint |
| `https://app.revolt.chat/api` | Production | API endpoint for old client |
| `https://revolt.chat/api` | Staging | API endpoint for new client |
You can connect to the events server on the following URLs:
| URL | Release | Description |
| ------------------------------ | :--------: | ------------------------------ |
| `wss://ws.revolt.chat` | Production | Primary events endpoint |
| `wss://app.revolt.chat/events` | Production | Events endpoint for old client |
| `wss://revolt.chat/events` | Staging | Events endpoint for new client |
@@ -1,10 +0,0 @@
{
"label": "Events",
"position": 2,
"link": {
"type": "generated-index",
"description": "Connecting and using the Stoat gateway"
},
"collapsed": false,
"collapsible": true
}
@@ -1,57 +0,0 @@
# Establishing a connection
To get started, you should have:
- A WebSocket URL, which is found from the API root.
- A valid session or bot token.
You may authenticate in one of two ways:
- Include credentials in the connection URL, see [Query Parameters](#query-parameters).
- Sending an [Authenticate](./protocol.md#authenticate) event to the server.
You should listen out for [Error](./protocol.md#error) events to find out if your credentials are incorrect or if something goes wrong here.
After authenticating, the server will respond with [Authenticated](./protocol.md#authenticated) then it will send a [Ready](./protocol.md#ready) event containing useful data.
The server will now start sending relevant events as they come in.
You should [Ping](./protocol.md#ping) the server every 10 to 30 seconds to prevent your connection being dropped.
Bots receive all events, normal users do not receive UserUpdate events fanned out through servers by default, [read more here](./protocol.md#subscribe).
## Query Parameters
The Bonfire service supports additional query parameters:
| Parameter | Description | Values | Required |
| --------- | ----------------------------------------------------------------------- | -------------------------- | -------- |
| `version` | Describes the protocol version in use. | `1` | No † |
| `format` | In what format to send packets, default is JSON. | `json`, `msgpack` | No |
| `token` | token for authenticating the connecting user. | Session or bot token | No |
| `ready` | Fields to include in the `Ready` event payload, by default all are sent | [See Below](#ready-fields) | No |
`version` may become compulsary in the future, please set it to `1` if you can.
### Ready Fields
The ready query parameter can be passed multiple times to specify multiple fields, supported fields are:
| Value | Description |
| ----------------- | --------------------------------------------------------------------------------------------------------------- |
| `users` | Includes all users you have a relation with. |
| `servers` | Includes all servers you are in. |
| `channels` | Includes all channels you have access to. |
| `members` | Includes all members you have a relation with. |
| `emojis` | Includes all emojis you have access to. |
| `user_settings` | Specify which settings to pre-fetch, specify a setting by setting the value to `user_settings[<setting_name>]`. |
| `channel_unreads` | Includes all channel unreads you have. |
| `policy_changes` | Includes all new policy changes you should be aware of, this is not sent to bots. |
For example:
```
?ready=users&ready=servers&ready=user_settings[ordering]
```
You may specify these in the connection URL: `wss://stoat.chat/events?version=1&format=json`.
-589
View File
@@ -1,589 +0,0 @@
# Events
This page documents various incoming and outgoing events.
**Help Wanted:** we should adopt [AsyncAPI](https://www.asyncapi.com) to properly document the protocol!
## Client to Server
### Authenticate
Authenticate with the server.
```json
{
"type": "Authenticate",
"token": "{token}"
}
```
### BeginTyping
Tell other users that you have begun typing in a channel.
Must be in the specified channel or nothing will happen.
```json
{
"type": "BeginTyping",
"channel": "{channel_id}"
}
```
### EndTyping
Tell other users that you have stopped typing in a channel.
Must be in the specified channel or nothing will happen.
```json
{
"type": "EndTyping",
"channel": "{channel_id}"
}
```
### Ping
Ping the server, you can specify a timestamp that you'll receive back.
```json
{
"type": "Ping",
"data": 0
}
```
### Subscribe
Subscribe to a server's UserUpdate events.
```json
{
"type": "Subscribe",
"server_id": "{server_id}"
}
```
Implementation notes:
- Subscriptions automatically expire within 15 minutes.
- A client may have up to 5 active subscriptions.
- This has no effect on bot sessions.
- This event should only be sent **iff** app/client is in focus.
- You should aim to send this event at most every 10 minutes per server.
## Server to Client
### Error
An error occurred which meant you couldn't authenticate.
```json
{
"type": "Error",
"error": "{error_id}"
}
```
The `{error_id}` can be one of the following:
- `LabelMe`: uncategorised error
- `InternalError`: the server ran into an issue
- `InvalidSession`: authentication details are incorrect
- `OnboardingNotFinished`: user has not chosen a username
- `AlreadyAuthenticated`: this connection is already authenticated
### Authenticated
The server has authenticated your connection and you will shortly start receiving data.
```json
{
"type": "Authenticated"
}
```
### Logged Out
The current user session has been invalidated or the bot token has been reset.
```json
{
"type": "Logout"
}
```
Your connection will be closed shortly after.
### Bulk
Several events have been sent, process each item of `v` as its own event.
```json
{
"type": "Bulk",
"v": [...]
}
```
### Pong
Ping response from the server.
```json
{
"type": "Pong",
"data": 0
}
```
### Ready
Data for use by client, data structures match the API specification.
```json
{
"type": "Ready",
"users"?: [{..}],
"servers"?: [{..}],
"channels"?: [{..}],
"members"?: [{..}],
"emojis"?: [{..}],
"user_settings"?: [{..}],
"channel_unreads"?: [{..}],
"policy_changes"?: [{..}],
}
```
### Message
Message received, the event object has the same schema as the Message object in the API with the addition of an event type.
```json
{
"type": "Message",
[..]
}
```
### MessageUpdate
Message edited or otherwise updated.
```json
{
"type": "MessageUpdate",
"id": "{message_id}",
"channel": "{channel_id}",
"data": {..}
}
```
- `data` field contains a partial Message object.
### MessageAppend
Message has data being appended to it.
```json
{
"type": "MessageAppend",
"id": "{message_id}",
"channel": "{channel_id}",
"append": {
"embeds"?: [...]
}
}
```
### MessageDelete
Message has been deleted.
```json
{
"type": "MessageDelete",
"id": "{message_id}",
"channel": "{channel_id}"
}
```
### MessageReact
A reaction has been added to a message.
```json
{
"type": "MessageReact",
"id": "{message_id}",
"channel_id": "{channel_id}",
"user_id": "{user_id}",
"emoji_id": "{emoji_id}"
}
```
### MessageUnreact
A reaction has been removed from a message.
```json
{
"type": "MessageUnreact",
"id": "{message_id}",
"channel_id": "{channel_id}",
"user_id": "{user_id}",
"emoji_id": "{emoji_id}"
}
```
### MessageRemoveReaction
A certain reaction has been removed from the message.
```json
{
"type": "MessageRemoveReaction",
"id": "{message_id}",
"channel_id": "{channel_id}",
"emoji_id": "{emoji_id}"
}
```
### ChannelCreate
Channel created, the event object has the same schema as the Channel object in the API with the addition of an event type.
```json
{
"type": "ChannelCreate",
[..]
}
```
### ChannelUpdate
Channel details updated.
```json
{
"type": "ChannelUpdate",
"id": "{channel_id}",
"data": {..},
"clear": ["{field}", ...]
}
```
- `data` field contains a partial Channel object.
- `{field}` is a field to remove, one of:
- `Icon`
- `Description`
### ChannelDelete
Channel has been deleted.
```json
{
"type": "ChannelDelete",
"id": "{channel_id}"
}
```
### ChannelGroupJoin
A user has joined the group.
```json
{
"type": "ChannelGroupJoin",
"id": "{channel_id}",
"user": "{user_id}"
}
```
### ChannelGroupLeave
A user has left the group.
```json
{
"type": "ChannelGroupLeave",
"id": "{channel_id}",
"user": "{user_id}"
}
```
### ChannelStartTyping
A user has started typing in this channel.
```json
{
"type": "ChannelStartTyping",
"id": "{channel_id}",
"user": "{user_id}"
}
```
### ChannelStopTyping
A user has stopped typing in this channel.
```json
{
"type": "ChannelStopTyping",
"id": "{channel_id}",
"user": "{user_id}"
}
```
### ChannelAck
You have acknowledged new messages in this channel up to this message ID.
```json
{
"type": "ChannelAck",
"id": "{channel_id}",
"user": "{user_id}",
"message_id": "{message_id}"
}
```
### ServerCreate
Server created, the event object has the same schema as the SERVER object in the API with the addition of an event type.
```json
{
"type": "ServerCreate",
[..]
}
```
### ServerUpdate
Server details updated.
```json
{
"type": "ServerUpdate",
"id": "{server_id}",
"data": {..},
"clear": ["{field}", ...]
}
```
- `data` field contains a partial Server object.
- `{field}` is a field to remove, one of:
- `Icon`
- `Banner`
- `Description`
### ServerDelete
Server has been deleted.
```json
{
"type": "ServerDelete",
"id": "{server_id}"
}
```
### ServerMemberUpdate
Server member details updated.
```json
{
"type": "ServerMemberUpdate",
"id": {
"server": "{server_id}",
"user": "{user_id}"
},
"data": {..},
"clear": ["{field}", ...]
}
```
- `data` field contains a partial Server Member object.
- `{field}` is a field to remove, one of:
- `Nickname`
- `Avatar`
### ServerMemberJoin
A user has joined the server.
```json
{
"type": "ServerMemberJoin",
"id": "{server_id}",
"user": "{user_id}",
"member": {..}
}
```
- `member` field contains a Member object.
### ServerMemberLeave
A user has left the server.
```json
{
"type": "ServerMemberLeave",
"id": "{server_id}",
"user": "{user_id}"
}
```
### ServerRoleUpdate
Server role has been updated or created.
```json
{
"type": "ServerRoleUpdate",
"id": "{server_id}",
"role_id": "{role_id}",
"data": {..},
"clear": ["{field}", ...]
}
```
- `data` field contains a partial Server Role object.
- `clear` is a field to remove, one of:
- `Colour`
### ServerRoleDelete
Server role has been deleted.
```json
{
"type": "ServerRoleDelete",
"id": "{server_id}",
"role_id": "{role_id}"
}
```
### UserUpdate
User has been updated.
```json
{
"type": "UserUpdate",
"id": "{user_id}",
"data": {..},
"clear": ["{field}", ...]
}
```
- `data` field contains a partial User object.
- `clear` is a field to remove, one of:
- `ProfileContent`
- `ProfileBackground`
- `StatusText`
- `Avatar`
- `DisplayName`
### UserRelationship
Your relationship with another user has changed.
```json
{
"type": "UserRelationship",
"id": "{your_user_id}",
"user": "{..}",
"status": "{status}"
}
```
- `user` field contains a User object.
- `status` field matches Relationship Status in API.
### UserPlatformWipe
User has been platform banned or deleted their account.
Clients should remove the following associated data:
- Messages
- DM Channels
- Relationships
- Server Memberships
User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
```json
{
"type": "UserPlatformWipe",
"user_id": "{user_id}",
"flags": "{user_flags}"
}
```
### EmojiCreate
Emoji created, the event object has the same schema as the Emoji object in the API with the addition of an event type.
```json
{
"type": "EmojiCreate",
[..]
}
```
### EmojiDelete
Emoji has been deleted.
```json
{
"type": "EmojiDelete",
"id": "{emoji_id}"
}
```
### Auth
Forwarded events from [Authifier](https://github.com/authifier/authifier), currently only session deletion events are forwarded.
```json
{
"type": "Auth",
"event_type": "{event_type}",
[..]
}
```
Event type may be defined as one of the following with the additional properties:
#### DeleteSession
A session has been deleted.
```json
{
"event_type": "DeleteSession",
"user_id": "{user_id}",
"session_id": "{session_id}"
}
```
#### DeleteAllSessions
All sessions for this account have been deleted, optionally excluding a given ID.
```json
{
"event_type": "DeleteAllSessions",
"user_id": "{user_id}",
"exclude_session_id": "{session_id}"
}
```
-147
View File
@@ -1,147 +0,0 @@
# Plugin API
:::danger
This page documents the old Revite Plugin API (manifest v1), it will be replaced in the new client.
:::
:::warning
The Plugin API is very powerful. **Tread carefully.**
**Zero guarantees or sandboxes are provided.** Your code is run as-is.
:::
This document details the very experimental plugin API available in [Revite](https://github.com/revoltchat/revite).
This is more or less a proof of concept but can be used to achieve some simple client modifications.
## Plugin Manifest
Below is the specification for revision 1 of the plugin API. The `format` parameter is not currently enforced but you should set it to `1` to avoid future breakage.
````typescript
type Plugin = {
/**
* Plugin Format Revision
*/
format: 1;
/**
* Semver Version String
*
* This is the version of the plugin.
*/
version: string;
/**
* Plugin Namespace
*
* This will usually be the author's name.
*/
namespace: string;
/**
* Plugin Id
*
* This should be a valid URL slug, e.g. cool-plugin.
*/
id: string;
/**
* Entrypoint
*
* Valid Javascript code. It must be a function which returns a object.
*
* ```typescript
* function (state: State) {
* return {
* onUnload: () => {}
* }
* }
* ```
*/
entrypoint: string;
/**
* Whether this plugin is enabled.
*
* @default true
*/
enabled?: boolean;
};
````
An example plugin:
```javascript
{
format: 1,
version: "0.0.1",
namespace: "insert",
id: "my-plugin",
entrypoint: `(state) => {
console.log('[my-plugin] Plugin init!');
return {
onUnload: () => console.log('[my-plugin] bye!')
}
}`
}
```
## Using the Plugin API
To begin, you can load plugins using the global plugin manager at `state.plugins`.
Open the developer console and run:
```javascript
state.plugins.load({ ... });
// ...where [...] is your plugin manifest as described above.
```
## Plugin API
A plugin's entrypoint is required to return an object which is referred to as the **instance**:
```typescript
interface Instance {
onUnload?: () => void;
}
```
The Plugin API (`state.plugins`) exposes the following methods:
```typescript
interface PluginAPI {
/**
* Add a plugin
* @param plugin Plugin Manifest
*/
add(plugin: Plugin);
/**
* Remove a plugin
* @param namespace Plugin Namespace
* @param id Plugin Id
*/
remove(namespace: string, id: string);
/**
* Load a plugin
* @param namespace Plugin Namespace
* @param id Plugin Id
*/
load(namespace: string, id: string);
/**
* Unload a plugin
* @param namespace Plugin Namespace
* @param id Plugin Id
*/
unload(namespace: string, id: string);
/**
* Reset everything
*/
reset();
}
```
-8
View File
@@ -1,8 +0,0 @@
# Libraries
The following libraries are provided by the Stoat team:
- [Javascript SDK](https://github.com/stoatchat/javascript-client-sdk)
- [Python SDK](https://github.com/stoatchat/python-client-sdk)
You can find a host of [community created libraries here](https://github.com/stoatchat/awesome-stoat#-api-libraries).
@@ -1,22 +0,0 @@
# Stoat Migration Guide
:::warning
This is not yet finished.
:::
## Endpoint changes
| Service | Old URL | New URL |
| ---------- | ----------------------------------- | --------------------------------------- |
| **API** | `https://api.revolt.chat` | `https://stoat.chat/api` |
| | `https://app.revolt.chat/api` | `https://stoat.chat/api` |
| | `https://revolt.chat/api` | No equivalent |
| **Events** | `wss://ws.revolt.chat` | `wss://stoat.chat/events` |
| | `wss://app.revolt.chat/events` | `wss://stoat.chat/events` |
| | `wss://revolt.chat/events` | No equivalent |
| **Files** | `https://autumn.revolt.chat` | `https://cdn.stoatusercontent.com` |
| | `https://cdn.revoltusercontent.com` | `https://cdn.stoatusercontent.com` |
| **Proxy** | `https://jan.revolt.chat` | `https://external.stoatusercontent.com` |
| **Voice** | `https://vortex.revolt.chat` | Superseded by Voice Chats v2 |
@@ -1,10 +0,0 @@
{
"label": "Backend",
"position": 10,
"link": {
"type": "generated-index",
"description": "Building the Stoat software"
},
"collapsed": false,
"collapsible": true
}
-45
View File
@@ -1,45 +0,0 @@
---
sidebar_position: 2
---
# Contribution Guide
This is the contribution guide for developers wanting to help out with Stoat.
## Repository Lifecycle
### Making Commits
- Sign-off your commits ([Git flag](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff)), [read here about DCO obligations](https://developercertificate.org/).
- Sign commits where possible, [learn more about that here](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits).
- Prefer to use [Conventional Commit style](https://www.conventionalcommits.org/en/v1.0.0-beta.2/).
- If present, e.g. `prettier`, `cargo fmt`, use the formatter.
- Try to keep each PR bound to a single feature or change, multiple bug fixes may be fine in some cases.
This is to avoid your PR getting stuck due to parts of it having conflicts or other issues.
### Merging Pull Requests
All PR titles must use use [Conventional Commit style](https://www.conventionalcommits.org/en/v1.0.0-beta.2/) and will be squash merged!
## What can I help with?
Stuff is currently being moved around, for the mean time, come ask in the development server: https://stt.gg/API
Also typically `help wanted` labels are available on repo issues!
<!-- The main project board can serve as a helpful starting point:
1. If you are new to the code base or are looking for issues we really need help with, look at ["What can I help with?"](https://github.com/orgs/projects/3/views/11)
2. Issue Board ["Free Issues"](https://github.com/orgs/projects/3/views/1): issues that anyone can pick up and are generally free to work on
3. Issue Board ["Todo"](https://github.com/orgs/projects/3/views/1): these are issues that are probably fine to pick up, but please ask first since a lot of these tend to be complicated and potentially already planned
4. Working on new issues and fixes: ideally you should run new features by us, most fixes are probably going to be alright though, we wouldn't want to reject any PRs that we don't deem suitable after work has already been done. If it's a fix, make sure to make an issue for it first, if it's a new feature, it may be better suited in [Feature Suggestions](https://github.com/discussions/categories/feature-suggestions)
Any issues marked with "Future Work" or with a milestone greater than the current milestone are out of bounds and should not be worked on since it's likely that the team already has a plan in place, any work you may do may conflict with prior ideas, and your work may potentially be rejected if it does fit the criteria exactly. In general, these issues are just postponed to reduce long term technical debt, i.e. allow current issues to be handled. -->
## Project Guidance
Please read the additional relevant guidance on:
- [Developing for Backend](https://github.com/stoatchat/backend?tab=readme-ov-file#development-guide) (contrib guide TBA)
- [Contributing to Frontend](https://stoatchat.github.io/for-web/contribution-guide.html)
- [Contributing to Android](https://stoatchat.github.io/for-android/contributing/guidelines/)
-109
View File
@@ -1,109 +0,0 @@
---
sidebar_position: 4
---
# 📖 Frequently Asked Questions
This page includes several frequently asked questions and explanations.
All of these answers are written from the perspective of the project owner.
**Last Update:** 10th May 2024
<details>
<summary>Why another project?</summary>
I think this is best explained with a bit of history:
- Stoat (formerly Revolt) started as a passion project
- It grew way beyond any of our expectations
- We might as well keep going since there is an interest in this space
Beyond that:
- Stoat has been a great learning experience, including development, management, and running the infrastructure for a large project. Stoat has also taught me a lot about different concepts and programming languages, and really, that's how developers learn. We make cool projects to try to better how we work, it doesn't matter if someone has done it before as long as you can attempt to do the same. Reinventing the wheel is part of the process.
- At the time, there was also a relative void of competition in this specific genre of chat platforms. There were Guilded, Discord, and Matrix but these are all either closed-source or cater to a different audience.
PS. I've had a few people say, 'why not just contribute to X?', the answer is quite simple, I just didn't know about any of these projects (i.e. Matrix).
</details>
<details>
<summary>How are we funded?</summary>
Stoat is entirely funded through donations, we have amassed a significant amount of money from donations alone. (financial transparency reports coming soon :tm:)
The month-to-month income of Stoat covers our operational costs and leaves enough spare to cover yearly expenses and the occassional one-time expense, such as for additional hardware.
We have monetisation plans lined up for the future, however it is not our intention to paywall existing features, instead where possible we intend to pass down costs such as for file storage or voice bandwidth.
</details>
<details>
<summary>'X' feature when?</summary>
Please take a look at [our roadmap on GitHub](https://op.stoatinternal.com/projects/all-of-revolt/gantt?query_id=53).
</details>
<details>
<summary>Does Stoat have federation?</summary>
As of right now, Stoat does not feature any federation and **it is not in our feature roadmap**.
However, this does not necessarily mean federation is off the table, possible avenues are:
- Implement our own federation protocol
- Implement a promising up and coming federation protocol, polyproto
- Implement the Matrix protocol (unlikely, obtuse and unstable)
- Implement the XMPP protocol (battle-tested and stable)
Any federation that is implemented MUST exercise caution in:
- Preventing spam and abuse: moderators should be able to block malicious actors
- Protecting user data: users should be able to redact all of their information and messages
</details>
<details>
<summary>What can I do with Stoat and how do I self-host?</summary>
In general:
- The Stoat branding is used to represent the platform, stoat.chat.
- You may use the branding as-is to promote the platform and your community on the platform.
- You should not use the branding in order to appear as if you are associated with the Stoat team.
- Please make explicit distinctions between Stoat (the platform, "stoat.chat") and the Stoat software.
- The Stoat software is provided unbranded and only associated by name.
If you have any concerns or questions, please liase with us at [contact@stoat.chat](mailto:contact@stoat.chat).
As a third-party platform:
- You **must** provide correct attribution in line with our software licenses:
If you are using official images (GitHub Packages / Docker Hub), attribution is included.
If you are modifying the software and using it in production, you must publish the changes to the source publicly in line with AGPLv3. (In addition to providing attribution back to the original project.)
- You are **solely responsible** for whatever happens on your third party instance, we provide no warranty or liability for what happens on 3rd party instances.
- You **must not** appear to associate with Stoat / stoat.chat unless if granted explicit written permission. In regards to custom clients, provide a warning of any potential risks or clear it with us.
- You **may not** use any of the Stoat branding or brand assets to advertise or promote your third party instance.
You can self-host Stoat by:
- Using [Docker Compose and our recommended guide](https://github.com/stoatchat/self-hosted).
- Building individual components yourself from the [source code](https://github.com/stoatchat).
</details>
<details>
<summary>Can you verify my server/bot?</summary>
Currently, you can only apply to verify servers given that you have a valid reason to believe verification is necessary for your community. Verification is intended to provide protection for server owners from copy cats and to provide authenticity to users as such we are not just giving it out to anyone because that would defeat the purpose.
However if you would like to get a server verified, you should satisfy one of the following criteria:
- Official community for a well-established open source project
- Official community for any other well-established product, service, or person
- Large and active distinct pre-existing community
Distinct means the community is unique and well-known (& has an active presence) off platform. This means we are not currently verifying generic servers that centre around a topic unless if it meets one of the first two criteria. Though in special circumstances, well known on platform communities may also be considered.
Server verification also comes with a vanity invite, so please have one ready if you want to apply. To apply, drop an email at [contact@stoat.chat](mailto:contact@stoat.chat).
We also periodically prune verification from servers that have fallen into disrepair and / or otherwise are no longer active.
</details>
For questions about the Stoat platform, you may want to go to our knowledge base:
- [What badges can I get?](https://support.stoat.chat/kb/account/badges)
- [How old do I have to be to use Stoat?](https://support.stoat.chat/kb/safety/minimum-age-guidelines)
- [Are there any restrictions on servers being on Discover?](https://support.stoat.chat/kb/safety/discover-guidelines)
- [(... and more)](https://support.stoat.chat)
-30
View File
@@ -1,30 +0,0 @@
---
sidebar_position: 3
---
# 💖 Helping Stoat!
You can contribute to Stoat in a variety of ways:
### 1. Feedback
The easiest, but most important, way to contribute to Stoat is to voice your opinion and give us feedback.
We want to hear what you think and appreciate and await your feature suggestions, bug reports and general opinions on everything Stoat has to offer.
Typically, you can open issues on the relevant [GitHub repositories](https://github.com/stoatchat).
<!-- Within the Stoat app, you can navigate to the [**feedback tab**](https://app.revolt.chat/settings/feedback), or you can open an issue on the relevant [GitHub repo](https://github.com/revoltchat). -->
### 2. Translate
Stoat is used by users all around the world; as such, it's more accessible if the user interface is available in a variety of languages.
You can contribute translations through [**Weblate**](https://translate.stoat.chat/engage/revolt/).
### 3. Donate
Stoat is not backed by a big company, is not currently monetised (for example, via a subscription service) and does not serve you advertisements; as such, Stoat currently relies entirely on donations.
You can learn more about donating [here](https://wiki.revolt.chat/notes/project/financial-support/) - if you want to make a larger donation, please consult me first.
### 4. Join the project
We are a small team and always appreciate more help! [Check out roles we're looking for here!](https://outline.stoatinternal.com/s/454dd0eb-44b5-41f7-b1d1-b6accec577a0)
-17
View File
@@ -1,17 +0,0 @@
---
sidebar_position: 1
---
# 🤗 Introduction
Welcome to Stoat's developer documentation. Everything you need to contribute to Stoat, build apps or bots, or to learn more about the project can be found here.
Learn more about:
- [Contributing as a developer](./developing/contrib)
- [Other ways to help out](./help)
You may also be interested in the:
- [Frontend Book](https://stoatchat.github.io/for-web)
- [Android Book](https://stoatchat.github.io/for-android)
-182
View File
@@ -1,182 +0,0 @@
import { themes as prismThemes } from 'prism-react-renderer';
import type { Config } from '@docusaurus/types';
import type * as Preset from '@docusaurus/preset-classic';
import type { ScalarOptions } from '@scalar/docusaurus';
const config: Config = {
title: 'Stoat Developers',
tagline: 'Developer documentation for Stoat',
favicon: 'https://stoat.chat/favicon.svg',
future: {
v4: true,
},
url: 'https://developers.stoat.chat',
baseUrl: '/',
organizationName: 'stoatchat',
projectName: 'stoatchat',
onBrokenLinks: 'throw',
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
[
'classic',
{
docs: {
routeBasePath: '/',
sidebarPath: './sidebars.ts',
editUrl:
'https://github.com/stoatchat/stoatchat/tree/main/docs/',
},
} satisfies Preset.Options,
],
],
plugins: [
[
'@scalar/docusaurus',
{
label: 'API Reference',
route: '/api-reference',
showNavLink: true,
configuration: {
url: 'https://stoat.chat/api/openapi.json',
},
} as ScalarOptions,
],
[
'@docusaurus/plugin-client-redirects',
{
fromExtensions: ['html', 'htm'],
redirects: [
// legacy docs website (stoatchat/developer-wiki)
{
from: '/developers/api/reference.html',
to: '/api-reference',
},
{
from: '/contrib.html',
to: '/developing/contrib',
},
{
from: '/contrib',
to: '/developing/contrib',
},
],
}
],
],
themeConfig: {
// image: 'img/docusaurus-social-card.jpg',
colorMode: {
respectPrefersColorScheme: true,
},
navbar: {
title: 'Stoat Developers',
logo: {
alt: 'Stoat',
src: 'https://stoat.chat/favicon.svg',
},
items: [
{
type: 'doc',
docId: 'index',
label: 'Docs'
},
{
href: 'https://github.com/stoatchat',
label: 'GitHub',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [
{
title: 'Developers',
items: [
{
label: 'Source Code',
href: 'https://github.com/stoatchat'
},
{
label: 'Help Translate',
href: 'https://translate.stoat.chat'
},
],
},
{
title: 'Team',
items: [
{
label: 'About',
href: 'https://stoat.chat/about'
},
{
label: 'Blog and Changelogs',
href: 'https://stoat.chat/updates'
},
{
label: 'Contact',
href: 'https://support.stoat.chat'
},
],
},
{
title: 'Stoat on Socials',
items: [
{
label: 'Bluesky',
href: 'https://bsky.app/profile/stoat.chat'
},
{
label: 'Reddit',
href: 'https://reddit.com/r/stoatchat'
},
{
label: 'Stoat Server',
href: 'https://stt.gg/Testers'
},
],
},
{
title: 'Legal',
items: [
{
label: 'Community Guidelines',
href: 'https://stoat.chat/legal/community-guidelines'
},
{
label: 'Terms of Service',
href: 'https://stoat.chat/legal/terms'
},
{
label: 'Privacy Policy',
href: 'https://stoat.chat/legal/privacy'
},
{
label: 'Imprint',
href: 'https://stoat.chat/legal/imprint'
},
],
},
],
copyright: `© Revolt Platforms Ltd, ${new Date().getFullYear()}`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
} satisfies Preset.ThemeConfig,
};
export default config;
-18000
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More