Compare commits

..
Author SHA1 Message Date
Zomatree 6ba48a0197 fix: add Speak and Listen to default permissions 2025-10-22 18:19:02 +01:00
Zomatree 47002ec81c fix: dont break on user not still being in channel and force disconnecting 2025-10-20 00:29:56 +01:00
Zomatree 6ab264eb36 fix: return correct error if user is already in a voice channel 2025-10-19 23:25:35 +01:00
Zomatree 59af536e29 refactor: seperate out disconnect logic 2025-10-12 02:02:36 +01:00
Zomatree 5c7eec68d0 chore: add debug logging to voice ingress 2025-10-12 01:12:08 +01:00
Zomatree 94d7320e70 fix(ci): add voice-ingress to compose 2025-10-11 23:46:13 +01:00
Zomatree a42ddcbbfa Merge branch 'main' into feat/livekit 2025-10-11 23:09:07 +01:00
Zomatree 530264f747 chore: add error logging to internal errors 2025-10-11 22:22:11 +01:00
Zomatree ae1d4dd264 fix: dont presume channel has a node 2025-10-09 01:32:17 +01:00
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
238 changed files with 2079 additions and 33970 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
+2 -4
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
-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: stoatchat/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: "stoatchat"){
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: "stoatchat"){
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
+193 -312
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -11,9 +11,8 @@ members = [
[patch.crates-io]
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
iso8601-timestamp = { path = "../iso8601-timestamp" }
#authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
#rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
# I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just
+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"] }
+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());
+1 -1
View File
@@ -15,7 +15,7 @@ use crate::{CoalescionServiceConfig, Error};
#[derive(Debug, Clone)]
#[allow(clippy::type_complexity)]
/// Coalescion service
/// # Coalescion service
///
/// See module description for example usage.
pub struct CoalescionService<Id: Hash + Clone + Eq> {
+1 -1
View File
@@ -94,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
+10 -11
View File
@@ -15,10 +15,9 @@ mongodb = ["dep:mongodb", "bson", "authifier/database-mongodb"]
# ... Other
tasks = ["isahc", "linkify", "url-escape"]
async-std-runtime = ["async-std", "authifier/async-std-runtime"]
rocket-impl = ["rocket"]
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"]
@@ -47,7 +46,7 @@ ulid = "1.0.0"
nanoid = "0.4.0"
base64 = "0.21.3"
once_cell = "1.17"
indexmap = "2.12.0"
indexmap = "1.9.1"
decancer = "1.6.2"
deadqueue = "0.2.4"
linkify = { optional = true, version = "0.8.1" }
@@ -59,7 +58,7 @@ isahc = { optional = true, version = "1.7", features = ["json"] }
serde_json = "1"
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.4.0", features = ["serde", "bson"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
# Events
redis-kiss = { version = "0.1.4" }
@@ -82,15 +81,15 @@ async-recursion = "1.0.4"
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
# Axum Impl
axum = { version = "0.8.6", optional = true }
axum = { version = "0.7.5", optional = true }
# Rocket Impl
schemars = { version = "0.8.8", optional = true }
rocket = { version = "0.5.1", default-features = false, features = [
"json",
], optional = true }
# Openapi Schema
utoipa = { version = "5.4.0", optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
# Authifier
authifier = { version = "1.0.15" }
@@ -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"] }
+1 -3
View File
@@ -295,9 +295,7 @@ pub enum EventV1 {
},
UserMoveVoiceChannel {
node: String,
from: String,
to: String,
token: String,
token: String
}
}
+3 -8
View File
@@ -25,10 +25,6 @@ pub use mongodb;
#[macro_use]
extern crate bson;
#[cfg(feature = "utoipa")]
#[macro_use]
extern crate utoipa;
#[cfg(not(feature = "async-std-runtime"))]
compile_error!("async-std-runtime feature must be enabled.");
@@ -64,7 +60,7 @@ macro_rules! database_derived {
macro_rules! auto_derived {
( $( $item:item )+ ) => {
$(
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
$item
)+
};
@@ -72,8 +68,8 @@ macro_rules! auto_derived {
macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => {
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, PartialEq)]
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
#[optional_name = $name]
#[opt_skip_serializing_none]
#[opt_some_priority]
@@ -116,7 +112,6 @@ pub mod tasks;
mod amqp;
pub use amqp::amqp::AMQP;
#[cfg(feature = "voice")]
pub mod voice;
@@ -14,7 +14,7 @@ auto_derived!(
}
/// Composite primary key consisting of channel and user id
#[derive(Hash, Eq)]
#[derive(Hash)]
pub struct ChannelCompositeKey {
/// Channel Id
pub channel: String,
@@ -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
@@ -247,7 +247,7 @@ impl AbstractMessages for ReferenceDb {
let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) {
if let Some(users) = message.reactions.get_mut(emoji) {
users.swap_remove(&user.to_string());
users.remove(&user.to_string());
}
Ok(())
@@ -260,7 +260,7 @@ impl AbstractMessages for ReferenceDb {
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
let mut messages = self.messages.lock().await;
if let Some(message) = messages.get_mut(id) {
message.reactions.swap_remove(emoji);
message.reactions.remove(emoji);
Ok(())
} else {
Err(create_error!(NotFound))
@@ -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,
@@ -55,7 +56,7 @@ auto_derived_partial!(
auto_derived!(
/// Composite primary key consisting of server and user id
#[derive(Hash, Default, Eq)]
#[derive(Hash, Default)]
pub struct MemberCompositeKey {
/// Server Id
pub server: String,
@@ -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",
})
}
}
@@ -4,6 +4,7 @@ use revolt_result::{create_error, Error, Result};
use crate::{Database, User};
#[async_trait::async_trait]
impl<S> FromRequestParts<S> for User
where
Database: FromRef<S>,
@@ -4,6 +4,8 @@ mod model;
mod ops;
#[cfg(feature = "rocket-impl")]
mod rocket;
#[cfg(feature = "rocket-impl")]
mod schema;
pub use model::*;
pub use ops::*;
@@ -15,7 +15,7 @@ use serde_json::json;
use ulid::Ulid;
auto_derived_partial!(
/// User
/// # User
pub struct User {
/// Unique Id
#[serde(rename = "_id")]
@@ -0,0 +1,31 @@
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
};
use crate::User;
impl OpenApiFromRequest<'_> for User {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
let mut requirements = schemars::Map::new();
requirements.insert("Session Token".to_owned(), vec![]);
Ok(RequestHeaderInput::Security(
"Session Token".to_owned(),
SecurityScheme {
data: SecuritySchemeData::ApiKey {
name: "x-session-token".to_owned(),
location: "header".to_owned(),
},
description: Some("Used to authenticate as a user.".to_owned()),
extensions: schemars::Map::new(),
},
requirements,
))
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ use super::DelayedTask;
use crate::Channel::TextChannel;
/// Enumeration of possible events
#[derive(Debug, PartialEq)]
#[derive(Debug, Eq, PartialEq)]
pub enum AckEvent {
/// Add mentions for a channel
ProcessMessage {
+49 -47
View File
@@ -10,16 +10,16 @@ use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
#[cfg_attr(feature = "utoipa", into_params(names("Idempotency-Key"), parameter_in=Header))]
pub struct IdempotencyKey(String);
pub struct IdempotencyKey {
key: String,
}
static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
impl IdempotencyKey {
pub fn unchecked_from_string(key: String) -> Self {
Self(key)
Self { key }
}
// Backwards compatibility.
@@ -32,56 +32,56 @@ impl IdempotencyKey {
}
cache.put(v.clone(), ());
self.0 = v;
self.key = v;
}
Ok(())
}
pub fn into_key(self) -> String {
self.0
self.key
}
}
// #[cfg(feature = "rocket-impl")]
// use revolt_rocket_okapi::{
// gen::OpenApiGenerator,
// request::{OpenApiFromRequest, RequestHeaderInput},
// revolt_okapi::openapi3::{Parameter, ParameterValue},
// };
#[cfg(feature = "rocket-impl")]
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
revolt_okapi::openapi3::{Parameter, ParameterValue},
};
// #[cfg(feature = "rocket-impl")]
// use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
#[cfg(feature = "rocket-impl")]
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
// #[cfg(feature = "rocket-impl")]
// impl OpenApiFromRequest<'_> for IdempotencyKey {
// fn from_request_input(
// _gen: &mut OpenApiGenerator,
// _name: String,
// _required: bool,
// ) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
// Ok(RequestHeaderInput::Parameter(Parameter {
// name: "Idempotency-Key".to_string(),
// description: Some("Unique key to prevent duplicate requests".to_string()),
// allow_empty_value: false,
// required: false,
// deprecated: false,
// extensions: schemars::Map::new(),
// location: "header".to_string(),
// value: ParameterValue::Schema {
// allow_reserved: false,
// example: None,
// examples: None,
// explode: None,
// style: None,
// schema: SchemaObject {
// instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
// ..Default::default()
// },
// },
// }))
// }
// }
#[cfg(feature = "rocket-impl")]
impl OpenApiFromRequest<'_> for IdempotencyKey {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::Parameter(Parameter {
name: "Idempotency-Key".to_string(),
description: Some("Unique key to prevent duplicate requests".to_string()),
allow_empty_value: false,
required: false,
deprecated: false,
extensions: schemars::Map::new(),
location: "header".to_string(),
value: ParameterValue::Schema {
allow_reserved: false,
example: None,
examples: None,
explode: None,
style: None,
schema: SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
..Default::default()
},
},
}))
}
}
#[cfg(feature = "rocket-impl")]
use rocket::{
@@ -110,16 +110,18 @@ impl<'r> FromRequest<'r> for IdempotencyKey {
));
}
let idempotency = IdempotencyKey(key);
let idempotency = IdempotencyKey { key };
let mut cache = TOKEN_CACHE.lock().await;
if cache.get(&idempotency.0).is_some() {
if cache.get(&idempotency.key).is_some() {
return Outcome::Error((Status::Conflict, create_error!(DuplicateNonce)));
}
cache.put(idempotency.0.clone(), ());
cache.put(idempotency.key.clone(), ());
return Outcome::Success(idempotency);
}
Outcome::Success(IdempotencyKey(ulid::Ulid::new().to_string()))
Outcome::Success(IdempotencyKey {
key: ulid::Ulid::new().to_string(),
})
}
}
-2
View File
@@ -5,7 +5,5 @@ pub mod idempotency;
pub mod permissions;
pub mod reference;
pub mod test_fixtures;
#[cfg(feature = "utoipa")]
pub mod utoipa;
pub use funcs::*;
@@ -3,6 +3,11 @@ use std::str::FromStr;
use revolt_result::Result;
#[cfg(feature = "rocket-impl")]
use rocket::request::FromParam;
#[cfg(feature = "rocket-impl")]
use schemars::{
schema::{InstanceType, Schema, SchemaObject, SingleOrVec},
JsonSchema,
};
use crate::{
Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook,
@@ -107,3 +112,17 @@ impl<'r> FromParam<'r> for Reference<'r> {
Ok(Reference::from_unchecked(param))
}
}
#[cfg(feature = "rocket-impl")]
impl<'a> JsonSchema for Reference<'a> {
fn schema_name() -> String {
"Id".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
..Default::default()
})
}
}
-49
View File
@@ -1,49 +0,0 @@
use utoipa::{
openapi::{
schema::SchemaType,
security::{ApiKey, ApiKeyValue, SecurityScheme},
ObjectBuilder, OpenApi, RefOr, Schema, Type,
},
Modify, PartialSchema, ToSchema,
};
use crate::util::reference::Reference;
pub struct TokenSecurity;
impl Modify for TokenSecurity {
fn modify(&self, openapi: &mut OpenApi) {
let components = openapi.components.get_or_insert_default();
components.add_security_scheme(
"Session-Token",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new(
"X-Session-Token".to_string(),
))),
);
components.add_security_scheme(
"Bot-Token",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Bot-Ticket".to_string()))),
);
}
}
impl ToSchema for Reference<'_> {
fn name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("Reference")
}
}
impl PartialSchema for Reference<'_> {
fn schema() -> RefOr<Schema> {
RefOr::T(
ObjectBuilder::new()
.description(Some("An id referencing a stoat model."))
.schema_type(SchemaType::Type(Type::String))
.examples(["01FD58YK5W7QRV5H3D64KTQYX3"])
.build()
.into(),
)
}
}
+1 -33
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<()> {
@@ -564,35 +564,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.
+8 -7
View File
@@ -10,22 +10,22 @@ description = "Revolt Backend: API Models"
[features]
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
schemas = ["dep:schemars", "revolt-permissions/schemas"]
utoipa = ["dep:utoipa"]
validator = ["dep:validator"]
rocket = ["dep:rocket"]
partials = ["dep:revolt_optional_struct", "serde", "utoipa"]
partials = ["dep:revolt_optional_struct", "serde", "schemas", "utoipa"]
default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.8.9", path = "../config" }
revolt-permissions = { version = "0.8.9", path = "../permissions", features = [
"utoipa",
] }
revolt-permissions = { version = "0.8.9", path = "../permissions" }
# Utility
regex = "1.11"
indexmap = "2.12.0"
indexmap = "1.9.3"
once_cell = "1.17.1"
num_enum = "0.6.1"
@@ -35,10 +35,11 @@ rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
# Serialisation
revolt_optional_struct = { version = "0.2.0", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
iso8601-timestamp = { version = "0.4.0", features = ["utoipa", "bson"] }
iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
# Spec Generation
utoipa = { version = "5.4.0", features = ["indexmap"], optional = true }
schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] }
utoipa = { version = "4.2.3", optional = true }
# Validation
validator = { version = "0.16.0", optional = true, features = ["derive"] }
+20 -4
View File
@@ -2,6 +2,10 @@
#[macro_use]
extern crate serde;
#[cfg(feature = "schemas")]
#[macro_use]
extern crate schemars;
#[cfg(feature = "utoipa")]
#[macro_use]
extern crate utoipa;
@@ -17,8 +21,9 @@ macro_rules! auto_derived {
( $( $item:item )+ ) => {
$(
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq)]
$item
)+
};
@@ -27,8 +32,19 @@ macro_rules! auto_derived {
#[cfg(feature = "partials")]
macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => {
#[derive(OptionalStruct, Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
#[optional_derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Default)]
#[derive(
OptionalStruct, Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema,
)]
#[optional_derive(
Debug,
Clone,
Eq,
PartialEq,
Serialize,
Deserialize,
JsonSchema,
Default
)]
#[optional_name = $name]
#[opt_skip_serializing_none]
#[opt_some_priority]
@@ -39,7 +55,7 @@ macro_rules! auto_derived_partial {
#[cfg(not(feature = "partials"))]
macro_rules! auto_derived_partial {
( $item:item, $name:expr ) => {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
$item
};
}
@@ -86,10 +86,10 @@ auto_derived!(
/// Information for the webhook
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct CreateWebhookBody {
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
#[validate(length(min = 1, max = 32))]
pub name: String,
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))]
#[validate(length(min = 1, max = 128))]
pub avatar: Option<String>,
}
);
-1
View File
@@ -271,7 +271,6 @@ auto_derived!(
/// Options when deleting a channel
#[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
-78
View File
@@ -1,78 +0,0 @@
auto_derived!(
/// hCaptcha Configuration
pub struct CaptchaFeature {
/// Whether captcha is enabled
pub enabled: bool,
/// Client key used for solving captcha
pub key: String,
}
/// Generic Service Configuration
pub struct Feature {
/// Whether the service is enabled
pub enabled: bool,
/// URL pointing to the service
pub url: String,
}
/// # Information about a livekit node
pub struct VoiceNode {
pub name: String,
pub lat: f64,
pub lon: f64,
pub public_url: String,
}
/// # Voice Server Configuration
pub struct VoiceFeature {
/// Whether voice is enabled
pub enabled: bool,
/// All livekit nodes
pub nodes: Vec<VoiceNode>,
}
/// Feature Configuration
pub struct RevoltFeatures {
/// hCaptcha configuration
pub captcha: CaptchaFeature,
/// Whether email verification is enabled
pub email: bool,
/// Whether this server is invite only
pub invite_only: bool,
/// File server service configuration
pub autumn: Feature,
/// Proxy service configuration
pub january: Feature,
/// Voice server configuration
pub livekit: VoiceFeature,
}
/// Build Information
pub struct BuildInformation {
/// Commit Hash
pub commit_sha: String,
/// Commit Timestamp
pub commit_timestamp: String,
/// Git Semver
pub semver: String,
/// Git Origin URL
pub origin_url: String,
/// Build Timestamp
pub timestamp: String,
}
/// Server Configuration
pub struct RevoltConfig {
/// Revolt API Version
pub revolt: String,
/// Features enabled on this Revolt node
pub features: RevoltFeatures,
/// WebSocket URL
pub ws: String,
/// URL pointing to the client serving this node
pub app: String,
/// Web Push VAPID public key
pub vapid: String,
/// Build information
pub build: BuildInformation,
}
);
+1 -1
View File
@@ -46,7 +46,7 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataCreateEmoji {
/// Server name
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32), regex = "RE_EMOJI"))]
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
pub name: String,
/// Parent information
pub parent: EmojiParent,
+4 -6
View File
@@ -141,17 +141,17 @@ auto_derived!(
pub struct Masquerade {
/// Replace the display name shown on this message
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
#[validate(length(min = 1, max = 32))]
pub name: Option<String>,
/// Replace the avatar shown on this message (URL to image file)
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 256)))]
#[validate(length(min = 1, max = 256))]
pub avatar: Option<String>,
/// Replace the display role colour shown on this message
///
/// Must have `ManageRole` permission to use
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 128), regex = "RE_COLOUR"))]
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
pub colour: Option<String>,
}
@@ -281,7 +281,6 @@ auto_derived!(
/// Options for querying messages
#[cfg_attr(feature = "validator", derive(Validate))]
#[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsQueryMessages {
/// Maximum number of messages to fetch
///
@@ -354,13 +353,12 @@ auto_derived!(
)]
pub struct OptionsBulkDelete {
/// Message IDs
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 100)))]
#[validate(length(min = 1, max = 100))]
pub ids: Vec<String>,
}
/// Options for removing reaction
#[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsUnreact {
/// Remove a specific user's reaction
pub user_id: Option<String>,
-2
View File
@@ -3,7 +3,6 @@ mod channel_invites;
mod channel_unreads;
mod channel_webhooks;
mod channels;
mod config;
mod embeds;
mod emojis;
mod files;
@@ -21,7 +20,6 @@ pub use channel_invites::*;
pub use channel_unreads::*;
pub use channel_webhooks::*;
pub use channels::*;
pub use config::*;
pub use embeds::*;
pub use emojis::*;
pub use files::*;
@@ -116,7 +116,6 @@ auto_derived!(
/// Options for fetching all members
#[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsFetchAllMembers {
/// Whether to exclude offline users
pub exclude_offline: Option<bool>,
-2
View File
@@ -198,7 +198,6 @@ auto_derived!(
/// Options when fetching server
#[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsFetchServer {
/// Whether to include channels
pub include_channels: Option<bool>,
@@ -285,7 +284,6 @@ auto_derived!(
/// Options when leaving a server
#[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsServerDelete {
/// Whether to not send a leave message
pub leave_silently: Option<bool>,
@@ -17,7 +17,6 @@ auto_derived!(
/// Additional options for inserting settings
#[cfg_attr(feature = "rocket", derive(FromForm))]
#[cfg_attr(feature = "utoipa", derive(IntoParams))]
pub struct OptionsSetSettings {
/// Timestamp of settings change.
///
+2 -2
View File
@@ -142,7 +142,7 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct UserStatus {
/// Custom status text
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 128)))]
#[validate(length(min = 0, max = 128))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub text: Option<String>,
/// Current presence option
@@ -155,7 +155,7 @@ auto_derived!(
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct UserProfile {
/// Text content on user's profile
#[cfg_attr(feature = "validator", validate(length(min = 0, max = 2000)))]
#[validate(length(min = 0, max = 2000))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub content: Option<String>,
/// Background visible on user's profile
+3 -1
View File
@@ -11,8 +11,10 @@ description = "Revolt Backend: Permission Logic"
[features]
bson = ["dep:bson"]
serde = ["dep:serde"]
schemas = ["dep:schemars"]
try-from-primitive = ["dep:num_enum"]
[dev-dependencies]
# Async
async-std = { version = "1.8.0", features = ["attributes"] }
@@ -34,4 +36,4 @@ serde = { version = "1", features = ["derive"], optional = true }
bson = { version = "2.1.0", optional = true }
# Spec Generation
utoipa = { version = "5.4.0", optional = true }
schemars = { version = "0.8.8", optional = true }
+7 -7
View File
@@ -1,10 +1,10 @@
#[cfg(feature = "utoipa")]
use utoipa::ToSchema;
#[cfg(feature = "schemas")]
use schemars::JsonSchema;
/// Representation of a single permission override
#[derive(Debug, Clone, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
pub struct Override {
/// Allow bit flags
pub allow: u64,
@@ -15,7 +15,7 @@ pub struct Override {
/// Data permissions Field - contains both allow and deny
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
pub struct DataPermissionsField {
pub permissions: Override,
}
@@ -23,7 +23,7 @@ pub struct DataPermissionsField {
/// Data permissions Value - contains allow
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
pub struct DataPermissionsValue {
pub permissions: u64,
}
@@ -31,7 +31,7 @@ pub struct DataPermissionsValue {
/// Data permissions Poly - can contain either Value or Field
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "serde", serde(untagged))]
pub enum DataPermissionPoly {
Value {
@@ -48,7 +48,7 @@ pub enum DataPermissionPoly {
/// as it appears on models and in the database
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
pub struct OverrideField {
/// Allow bit flags
pub a: i64,
+4 -6
View File
@@ -4,22 +4,20 @@ version = "0.8.9"
edition = "2024"
[features]
rocket = [
"dep:rocket",
"revolt-database/rocket-impl",
]
rocket = ["dep:rocket", "dep:revolt_rocket_okapi", "revolt-database/rocket-impl"]
axum = ["dep:axum", "revolt-database/axum-impl"]
default = ["rocket", "axum"]
[dependencies]
revolt-database = { version = "0.8.9", path = "../database" }
revolt-database = { version = "0.8.9", path = "../database"}
revolt-result = { version = "0.8.9", path = "../result" }
revolt-config = { version = "0.8.9", path = "../config" }
rocket = { version = "0.5.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
axum = { version = "0.8.6", optional = true, features = ["macros"] }
axum = { version = "0.7.5", optional = true, features = ["macros"] }
serde = { version = "1", features = ["derive"] }
authifier = { version = "1.0.15" }
+3
View File
@@ -1,5 +1,6 @@
use std::net::SocketAddr;
use async_trait::async_trait;
use axum::{
Json, RequestPartsExt, Router,
body::Body,
@@ -43,6 +44,7 @@ async fn to_real_ip(parts: &Parts) -> String {
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for Ratelimiter
where
Database: FromRef<S>,
@@ -80,6 +82,7 @@ where
}
}
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for RatelimitInformation
where
Database: FromRef<S>,
+13
View File
@@ -8,6 +8,9 @@ use rocket::serde::json::Json;
use rocket::{Data, Request, Response, State};
use revolt_config::config;
use revolt_rocket_okapi::r#gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use authifier::models::Session;
use crate::ratelimiter::RequestKind;
@@ -75,6 +78,16 @@ impl<'r> FromRequest<'r> for Ratelimiter {
}
}
impl OpenApiFromRequest<'_> for Ratelimiter {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::None)
}
}
/// Attach ratelimiter to the Rocket application
pub struct RatelimitFairing;
+8 -6
View File
@@ -10,12 +10,13 @@ description = "Revolt Backend: Result and Error types"
[features]
serde = ["dep:serde"]
schemas = ["dep:schemars"]
utoipa = ["dep:utoipa"]
rocket = ["dep:rocket", "dep:serde_json"]
axum = ["dep:axum", "dep:serde_json"]
sentry = ["dep:sentry"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
default = ["serde", "sentry"]
default = ["serde"]
[dependencies]
# Serialisation
@@ -23,14 +24,15 @@ serde_json = { version = "1", optional = true }
serde = { version = "1", features = ["derive"], optional = true }
# Spec Generation
utoipa = { version = "5.4.0", optional = true }
schemars = { version = "0.8.8", optional = true }
utoipa = { version = "4.2.3", optional = true }
# Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
# utilities
log = "0.4"
# Axum
axum = { version = "0.8.6", optional = true }
sentry = { version = "0.31.5", optional = true }
axum = { version = "0.7.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,
+14 -9
View File
@@ -5,10 +5,13 @@ use std::fmt::Display;
#[macro_use]
extern crate serde;
#[cfg(feature = "schemas")]
#[macro_use]
extern crate schemars;
#[cfg(feature = "utoipa")]
mod utoipa_impl;
#[cfg(feature = "utoipa")]
pub use crate::utoipa_impl::*;
#[macro_use]
extern crate utoipa;
#[cfg(feature = "rocket")]
pub mod rocket;
@@ -16,12 +19,16 @@ pub mod rocket;
#[cfg(feature = "axum")]
pub mod axum;
#[cfg(feature = "okapi")]
pub mod okapi;
/// Result type with custom Error
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Error information
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone)]
pub struct Error {
/// Type of error and additional information
@@ -43,7 +50,8 @@ impl std::error::Error for Error {}
/// Possible error types
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone)]
pub enum ErrorType {
/// This error was not labeled :(
@@ -131,7 +139,6 @@ pub enum ErrorType {
NotPrivileged,
CannotGiveMissingPermissions,
NotOwner,
IsElevated,
// ? General errors
DatabaseError {
@@ -219,7 +226,7 @@ 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();
@@ -227,8 +234,6 @@ impl<T, E: std::fmt::Debug + std::error::Error> ToRevoltError<T> for Result<T, E
self
.map_err(|e| {
log::error!("{e:?}");
#[cfg(feature = "sentry")]
sentry::capture_error(&e);
Error {
error_type: ErrorType::InternalError,
+49
View File
@@ -0,0 +1,49 @@
use revolt_okapi::openapi3::SchemaObject;
use revolt_rocket_okapi::revolt_okapi::openapi3;
use schemars::schema::Schema;
use crate::Error;
impl revolt_rocket_okapi::response::OpenApiResponderInner for Error {
fn responses(
gen: &mut revolt_rocket_okapi::gen::OpenApiGenerator,
) -> std::result::Result<openapi3::Responses, revolt_rocket_okapi::OpenApiError> {
let mut content = revolt_okapi::Map::new();
let settings = schemars::gen::SchemaSettings::default().with(|s| {
s.option_nullable = true;
s.option_add_null_type = false;
s.definitions_path = "#/components/schemas/".to_string();
});
let mut schema_generator = settings.into_generator();
let schema = schema_generator.root_schema_for::<Error>();
let definitions = gen.schema_generator().definitions_mut();
for (key, value) in schema.definitions {
definitions.insert(key, value);
}
definitions.insert("Error".to_string(), Schema::Object(schema.schema));
content.insert(
"application/json".to_string(),
openapi3::MediaType {
schema: Some(SchemaObject {
reference: Some("#/components/schemas/Error".to_string()),
..Default::default()
}),
..Default::default()
},
);
Ok(openapi3::Responses {
default: Some(openapi3::RefOr::Object(openapi3::Response {
content,
description: "An error occurred.".to_string(),
..Default::default()
})),
..Default::default()
})
}
}
-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,
-52
View File
@@ -1,52 +0,0 @@
use utoipa::{Modify, openapi::{Content, OpenApi, Ref, RefOr, ResponseBuilder}};
pub struct ErrorAddon;
impl Modify for ErrorAddon {
fn modify(&self, utoipa: &mut OpenApi) {
let response = ResponseBuilder::new()
.description("An error occurred")
.content(
"application/json",
Content::new(Some(RefOr::Ref(Ref::from_schema_name("Error")))),
)
.build();
for path in utoipa.paths.paths.values_mut() {
if let Some(route) = path.get.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
if let Some(route) = path.delete.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
if let Some(route) = path.patch.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
if let Some(route) = path.post.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
if let Some(route) = path.put.as_mut() {
route
.responses
.responses
.insert("default".to_string(), RefOr::T(response.clone()));
};
}
}
}
+1 -1
View File
@@ -38,5 +38,5 @@ pretty_env_logger = "0.4.0"
serde_json = "1"
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.4.0", features = ["serde", "bson"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
base64 = "0.22.1"
+4 -4
View File
@@ -36,13 +36,13 @@ 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
livekit-api = "=0.4.4"
livekit-protocol = "=0.4.0"
livekit-runtime = { version = "=0.3.1", features = ["tokio"] }
livekit-api = "0.4.4"
livekit-protocol = "0.4.0"
livekit-runtime = { version = "0.3.1", features = ["tokio"] }
# RabbitMQ
amqprs = { version = "1.7.0" }
+85 -84
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()?;
@@ -254,7 +255,7 @@ pub async fn ingress(
channel_id,
channel.server(),
user_id,
event.event == "track_published" || event.event == "track_unmuted", // to avoid duplicating this entire case twice
event.event == "track_published", // to avoid duplicating this entire case twice
track.source,
)
.await?;
+9 -11
View File
@@ -35,7 +35,7 @@ nanoid = "0.4.0"
serde_json = "1.0.57"
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.16", features = ["derive"] }
iso8601-timestamp = { version = "0.4.0", features = [] }
iso8601-timestamp = { version = "0.2.11", features = [] }
# async
futures = "0.3.8"
@@ -55,13 +55,13 @@ lettre = "0.10.0-alpha.4"
rocket = { version = "0.5.1", default-features = false, features = ["json"] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" }
rocket_empty = { version = "0.1.1" }
rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_authifier = { version = "1.0.15" }
rocket_prometheus = "0.10.0-rc.3"
# spec generation
utoipa = { version = "5.4.0", features = ["rocket_extras"] }
utoipa-scalar = { version = "0.3.0", features = ["rocket"] }
schemars = "0.8.8"
revolt_rocket_okapi = { version = "0.10.0", features = ["swagger"] }
# rabbit
amqprs = { version = "1.7.0" }
@@ -72,22 +72,20 @@ revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database", features = [
"rocket-impl",
"redis-is-patched",
"utoipa",
"voice",
] }
revolt-models = { path = "../core/models", features = [
"utoipa",
"schemas",
"validator",
"rocket",
] }
revolt-presence = { path = "../core/presence" }
revolt-result = { path = "../core/result", features = ["rocket", "utoipa"] }
revolt-permissions = { path = "../core/permissions", features = ["utoipa"] }
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
revolt-ratelimits = { path = "../core/ratelimits", features = ["rocket"] }
# voice
livekit-api = "=0.4.4"
livekit-protocol = "=0.4.0"
livekit-api = "0.4.4"
livekit-protocol = "0.4.0"
[build-dependencies]
vergen = "7.5.0"
+28 -4
View File
@@ -1,7 +1,7 @@
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate utoipa;
extern crate revolt_rocket_okapi;
#[macro_use]
extern crate serde_json;
@@ -15,8 +15,6 @@ use revolt_ratelimits::rocket as ratelimiter;
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics;
use utoipa::OpenApi;
use utoipa_scalar::{Scalar, Servable};
use std::net::Ipv4Addr;
use std::str::FromStr;
@@ -77,6 +75,31 @@ pub async fn web() -> Rocket<Build> {
.to_cors()
.expect("Failed to create CORS.");
// Configure Swagger
let swagger = revolt_rocket_okapi::swagger_ui::make_swagger_ui(
&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "/openapi.json".to_owned(),
..Default::default()
},
)
.into();
let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui(
&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "/0.8/openapi.json".to_owned(),
..Default::default()
},
)
.into();
let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui(
&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "/0.8/openapi.json".to_owned(),
..Default::default()
},
)
.into();
// Voice handler
let voice_client = VoiceClient::new(config.api.livekit.nodes.clone());
// Configure Rabbit
@@ -120,7 +143,8 @@ pub async fn web() -> Rocket<Build> {
.mount("/metrics", prometheus)
.mount("/", rocket_cors::catch_all_options_routes())
.mount("/", ratelimiter::routes())
.mount("/", Scalar::with_url("/scalar", routes::ApiDoc::openapi()))
.mount("/swagger/", swagger)
.mount("/0.8/swagger/", swagger_0_8)
.manage(authifier)
.manage(db)
.manage(amqp)
+2 -8
View File
@@ -5,16 +5,10 @@ use rocket::serde::json::Json;
use rocket::State;
use validator::Validate;
/// Create Bot
/// # Create Bot
///
/// Create a new Revolt bot.
#[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = v0::BotWithUserResponse),
),
)]
#[openapi(tag = "Bots")]
#[post("/create", data = "<info>")]
pub async fn create_bot(
db: &State<Database>,
+12 -10
View File
@@ -1,18 +1,12 @@
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;
/// Delete Bot
/// # Delete Bot
///
/// Delete a bot by its id.
#[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 204),
),
)]
#[openapi(tag = "Bots")]
#[delete("/<target>")]
pub async fn delete_bot(
db: &State<Database>,
@@ -27,7 +21,15 @@ 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? {
if let Some(node) = get_channel_node(&channel_id).await? {
let _ = voice_client.remove_user(&node, &bot.id, &channel_id).await;
}
let channel = Reference::from_unchecked(&channel_id).as_channel(db).await?;
delete_voice_state(&channel_id, channel.server(), &bot.id).await?;
}
Ok(EmptyResponse)
}
+11 -10
View File
@@ -6,16 +6,10 @@ use rocket::State;
use rocket::serde::json::Json;
use validator::Validate;
/// Edit Bot
/// # Edit Bot
///
/// Edit bot details by its id.
#[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = v0::BotWithUserResponse),
),
)]
#[openapi(tag = "Bots")]
#[patch("/<target>", data = "<data>")]
pub async fn edit_bot(
db: &State<Database>,
@@ -66,8 +60,15 @@ pub async fn edit_bot(
..Default::default()
};
bot.update(db, partial, remove.into_iter().map(|v| v.into()).collect())
.await?;
bot.update(
db,
partial,
remove
.into_iter()
.map(|v| v.into())
.collect(),
)
.await?;
Ok(Json(v0::BotWithUserResponse {
bot: bot.into(),
+2 -8
View File
@@ -3,16 +3,10 @@ use revolt_models::v0::FetchBotResponse;
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// Fetch Bot
/// # Fetch Bot
///
/// Fetch details of a bot you own by its id.
#[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = FetchBotResponse),
),
)]
#[openapi(tag = "Bots")]
#[get("/<bot>")]
pub async fn fetch_bot(
db: &State<Database>,
+2 -8
View File
@@ -5,16 +5,10 @@ use revolt_result::Result;
use rocket::serde::json::Json;
use rocket::State;
/// Fetch Owned Bots
/// # Fetch Owned Bots
///
/// Fetch all of the bots that you have control over.
#[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = OwnedBotsResponse),
),
)]
#[openapi(tag = "Bots")]
#[get("/@me")]
pub async fn fetch_owned_bots(db: &State<Database>, user: User) -> Result<Json<OwnedBotsResponse>> {
let mut bots = db.fetch_bots_by_user(&user.id).await?;
+2 -8
View File
@@ -5,16 +5,10 @@ use revolt_result::{create_error, Result};
use rocket::serde::json::Json;
use rocket::State;
/// Fetch Public Bot
/// # Fetch Public Bot
///
/// Fetch details of a public (or owned) bot by its id.
#[utoipa::path(
tag = "Bots",
security(("Session-Token" = []), ()),
responses(
(status = 200, body = PublicBot),
),
)]
#[openapi(tag = "Bots")]
#[get("/<target>/invite")]
pub async fn fetch_public_bot(
db: &State<Database>,
+2 -8
View File
@@ -11,16 +11,10 @@ use rocket::State;
use rocket::serde::json::Json;
use rocket_empty::EmptyResponse;
/// Invite Bot
/// # Invite Bot
///
/// Invite a bot to a server or group by its id.`
#[utoipa::path(
tag = "Bots",
security(("Session-Token" = [])),
responses(
(status = 200, body = v0::BotWithUserResponse),
),
)]
#[openapi(tag = "Bots")]
#[post("/<target>/invite", data = "<dest>")]
pub async fn invite_bot(
db: &State<Database>,
+3 -16
View File
@@ -1,3 +1,4 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
mod create;
@@ -8,22 +9,8 @@ mod fetch_owned;
mod fetch_public;
mod invite;
#[derive(OpenApi)]
#[openapi(
paths(
create::create_bot,
invite::invite_bot,
fetch_public::fetch_public_bot,
fetch::fetch_bot,
fetch_owned::fetch_owned_bots,
edit::edit_bot,
delete::delete_bot,
)
)]
pub struct ApiDoc;
pub fn routes() -> Vec<Route> {
routes![
pub fn routes() -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
create::create_bot,
invite::invite_bot,
fetch_public::fetch_public_bot,
@@ -7,20 +7,10 @@ use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// Acknowledge Message
/// # Acknowledge Message
///
/// Lets the server and all other clients know that we've seen this message id in this channel.
#[utoipa::path(
tag = "Messaging",
security(("Session-Token" = [])),
params(
("target" = Reference, Path),
("message" = Reference, Path),
),
responses(
(status = 204)
)
)]
#[openapi(tag = "Messaging")]
#[put("/<target>/ack/<message>")]
pub async fn ack(
db: &State<Database>,
@@ -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};
@@ -9,20 +11,10 @@ use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// Close Channel
/// # Close Channel
///
/// Deletes a server channel, leaves a group or closes a group.
#[utoipa::path(
tag = "Channel Information",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
v0::OptionsChannelDelete,
),
responses(
(status = 204),
),
)]
#[openapi(tag = "Channel Information")]
#[delete("/<target>?<options..>")]
pub async fn delete(
db: &State<Database>,
@@ -65,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};
@@ -9,19 +7,10 @@ use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
use validator::Validate;
/// Edit Channel
/// # Edit Channel
///
/// Edit a channel object by its id.
#[utoipa::path(
tag = "Channel Information",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 200, body = v0::Channel),
),
)]
#[openapi(tag = "Channel Information")]
#[patch("/<target>", data = "<data>")]
pub async fn edit(
db: &State<Database>,
@@ -264,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()))
@@ -8,19 +8,10 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::Result;
use rocket::{serde::json::Json, State};
/// Fetch Channel
/// # Fetch Channel
///
/// Fetch channel by its id.
#[utoipa::path(
tag = "Channel Information",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 200, body = v0::Channel),
),
)]
#[openapi(tag = "Channel Information")]
#[get("/<target>")]
pub async fn fetch(
db: &State<Database>,
@@ -8,20 +8,10 @@ use revolt_result::{create_error, Result};
use rocket::State;
use rocket_empty::EmptyResponse;
/// Add Member to Group
/// # Add Member to Group
///
/// Adds another user to the group.
#[utoipa::path(
tag = "Groups",
security(("Session-Token" = [])),
params(
("group_id" = Reference, Path),
("member_id" = Reference, Path),
),
responses(
(status = 204),
),
)]
#[openapi(tag = "Groups")]
#[put("/<group_id>/recipients/<member_id>")]
pub async fn add_member(
db: &State<Database>,
@@ -6,16 +6,10 @@ use rocket::serde::json::Json;
use rocket::State;
use validator::Validate;
/// Create Group
/// # Create Group
///
/// Create a new group channel.
#[utoipa::path(
tag = "Groups",
security(("Session-Token" = [])),
responses(
(status = 200, body = v0::Channel),
),
)]
#[openapi(tag = "Groups")]
#[post("/create", data = "<data>")]
pub async fn create_group(
db: &State<Database>,
@@ -1,28 +1,14 @@
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};
use rocket::State;
use rocket_empty::EmptyResponse;
/// Remove Member from Group
/// # Remove Member from Group
///
/// Removes a user from the group.
#[utoipa::path(
tag = "Groups",
security(("Session-Token" = [])),
params(
("target" = Reference, Path),
("member" = Reference, Path),
),
responses(
(status = 204),
),
)]
#[openapi(tag = "Groups")]
#[delete("/<target>/recipients/<member>")]
pub async fn remove_member(
db: &State<Database>,
@@ -38,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()
@@ -61,11 +44,15 @@ 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? {
if let Some(node) = get_channel_node(channel.id()).await? {
let _ = voice_client.remove_user(&node, &user.id, channel.id()).await;
}
delete_voice_state(channel.id(), None, &user.id).await?;
};
Ok(EmptyResponse)
@@ -8,21 +8,12 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// Create Invite
/// # Create Invite
///
/// Creates an invite to this channel.
///
/// Channel must be a `TextChannel`.
#[utoipa::path(
tag = "Channel Invites",
security(("Session-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 200, body = v0::Invite),
),
)]
#[openapi(tag = "Channel Invites")]
#[post("/<target>/invites")]
pub async fn create_invite(
db: &State<Database>,
@@ -7,21 +7,12 @@ use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
/// Fetch Group Members
/// # Fetch Group Members
///
/// Retrieves all users who are part of this group.
///
/// This may not return full user information if users are not friends but have mutual connections.
#[utoipa::path(
tag = "Groups",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 200, body = Vec<v0::User>),
),
)]
#[openapi(tag = "Groups")]
#[get("/<target>/members")]
pub async fn fetch_members(
db: &State<Database>,
@@ -10,23 +10,14 @@ use rocket::{serde::json::Json, State};
use rocket_empty::EmptyResponse;
use validator::Validate;
/// Bulk Delete Messages
/// # Bulk Delete Messages
///
/// Delete multiple messages you've sent or one you have permission to delete.
///
/// This will always require `ManageMessages` permission regardless of whether you own the message or not.
///
/// Messages must have been sent within the past 1 week.
#[utoipa::path(
tag = "Messaging",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
),
responses(
(status = 204),
),
)]
#[openapi(tag = "Messaging")]
#[delete("/<target>/messages/bulk", data = "<options>", rank = 1)]
pub async fn bulk_delete_messages(
db: &State<Database>,
@@ -7,22 +7,12 @@ use revolt_result::Result;
use rocket::State;
use rocket_empty::EmptyResponse;
/// Remove All Reactions from Message
/// # Remove All Reactions from Message
///
/// Remove your own, someone else's or all of a given reaction.
///
/// Requires `ManageMessages` permission.
#[utoipa::path(
tag = "Interactions",
security(("Session-Token" = []), ("Bot-Token" = [])),
params(
("target" = Reference, Path),
("msg" = Reference, Path),
),
responses(
(status = 204),
),
)]
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions")]
pub async fn clear_reactions(
db: &State<Database>,
@@ -47,7 +37,7 @@ pub async fn clear_reactions(
reactions: Some(Default::default()),
..Default::default()
},
vec![],
vec![]
)
.await
.map(|_| EmptyResponse)

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