mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-06 11:16:00 +00:00
Compare commits
177 Commits
0.5.4
...
20230531-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b1170633 | ||
|
|
4abd4070f7 | ||
|
|
bca17b11a7 | ||
|
|
2a6d532852 | ||
|
|
a9a5af8cc8 | ||
|
|
edfa8e5256 | ||
|
|
2ebcfdc770 | ||
|
|
fc47df786b | ||
|
|
f6aa405607 | ||
|
|
101e8feaa2 | ||
|
|
df8f588e15 | ||
|
|
3ef549537e | ||
|
|
d81d08f1ce | ||
|
|
a77b7717b8 | ||
|
|
d5f903781d | ||
|
|
8b12546b56 | ||
|
|
5bb91a7113 | ||
|
|
a04c2512b4 | ||
|
|
c3f0df6567 | ||
|
|
8b8aec4dc1 | ||
|
|
1d228ff76e | ||
|
|
b5863c0104 | ||
|
|
08ccc43f8d | ||
|
|
e33ae17061 | ||
|
|
60ebdb64d7 | ||
|
|
c27d0ad5bb | ||
|
|
c1b9d941f3 | ||
|
|
1afb27a508 | ||
|
|
6a8481f7df | ||
|
|
7f201565c0 | ||
|
|
ae9474b95d | ||
|
|
050f3abc89 | ||
|
|
888c22cb54 | ||
|
|
f633fccbca | ||
|
|
39fec310f9 | ||
|
|
d5d922d830 | ||
|
|
f8c8407af3 | ||
|
|
e6d0d44c5a | ||
|
|
1933c9ea3d | ||
|
|
8a695b4bb5 | ||
|
|
bf71b45fbb | ||
|
|
9124c9f1e3 | ||
|
|
bcc2d05625 | ||
|
|
22bfd720b5 | ||
|
|
63f56aec0c | ||
|
|
dd3d7e9c49 | ||
|
|
dbb66edd9f | ||
|
|
8bfb48dff3 | ||
|
|
12d963d2bd | ||
|
|
c817c2dd40 | ||
|
|
8a230ba989 | ||
|
|
2a9cc3190c | ||
|
|
e84d55a697 | ||
|
|
403a94f70c | ||
|
|
633eb78630 | ||
|
|
bbe1f4936c | ||
|
|
11a87263be | ||
|
|
8b30dddc06 | ||
|
|
b93dd90caf | ||
|
|
0054019f82 | ||
|
|
736220a94e | ||
|
|
a9c82791b3 | ||
|
|
eacf4decab | ||
|
|
b8cda2ec74 | ||
|
|
69ab7e031b | ||
|
|
ace6431cb8 | ||
|
|
40790de909 | ||
|
|
750037b5d2 | ||
|
|
6b10385c0d | ||
|
|
56ead0f894 | ||
|
|
d633cba630 | ||
|
|
e43833c0ea | ||
|
|
7f86337cb2 | ||
|
|
f2bb388b3b | ||
|
|
43e45aef3f | ||
|
|
c5880596a4 | ||
|
|
59a644891d | ||
|
|
a326cdc736 | ||
|
|
03a28dbb3e | ||
|
|
32542a822e | ||
|
|
1df90ff53b | ||
|
|
487b979f0d | ||
|
|
b9d813d8f0 | ||
|
|
36dd128459 | ||
|
|
144f0d39c6 | ||
|
|
32a294a64a | ||
|
|
b7b70346b4 | ||
|
|
59832e6ba2 | ||
|
|
f309218573 | ||
|
|
0321eff62b | ||
|
|
4c8ea31d98 | ||
|
|
92ac86a6bd | ||
|
|
13ed69c82a | ||
|
|
b83f6da648 | ||
|
|
89f1167239 | ||
|
|
82d868751f | ||
|
|
31b9f18921 | ||
|
|
fd80823910 | ||
|
|
6bd8221eda | ||
|
|
ce77e926a5 | ||
|
|
2710edb76b | ||
|
|
8f1ff9e774 | ||
|
|
c09244039e | ||
|
|
bf9108408e | ||
|
|
304336d905 | ||
|
|
42b4906594 | ||
|
|
3038fb230f | ||
|
|
73af105feb | ||
|
|
2de37b23c1 | ||
|
|
0c75d8c8b3 | ||
|
|
be793b1570 | ||
|
|
5a3a97df34 | ||
|
|
0c072b01d7 | ||
|
|
056c0380b2 | ||
|
|
49598daf70 | ||
|
|
0445181d5b | ||
|
|
e780178cc0 | ||
|
|
c4b9884825 | ||
|
|
fdab997d17 | ||
|
|
8e0addc3e0 | ||
|
|
0da4baf777 | ||
|
|
c03fcda85b | ||
|
|
0afbcc065f | ||
|
|
a406e5504d | ||
|
|
e8c233f60b | ||
|
|
72595f30d4 | ||
|
|
39e345af74 | ||
|
|
e0b918771d | ||
|
|
7a6bd70dcd | ||
|
|
c0ef3d295a | ||
|
|
f5c95f9d12 | ||
|
|
fed1da72fd | ||
|
|
cf9c0bc5e1 | ||
|
|
cc2054838a | ||
|
|
d9d2735efa | ||
|
|
94b0472934 | ||
|
|
4a7917e9ea | ||
|
|
ce6b454874 | ||
|
|
fc32996b51 | ||
|
|
d74104fae4 | ||
|
|
32d3683013 | ||
|
|
0cebb69e02 | ||
|
|
117958d8cd | ||
|
|
c5b823ad13 | ||
|
|
27b4699299 | ||
|
|
ee1c4cc2d5 | ||
|
|
0e0daf86f7 | ||
|
|
572e08c733 | ||
|
|
9011939344 | ||
|
|
f4dcd091cc | ||
|
|
86ad72b426 | ||
|
|
044d82d566 | ||
|
|
deffe663cb | ||
|
|
e7f6433d77 | ||
|
|
345e4d5f13 | ||
|
|
81fc3e7760 | ||
|
|
ca8d2e6118 | ||
|
|
c1227e8e33 | ||
|
|
0e0c6f2c3a | ||
|
|
fa82be74b5 | ||
|
|
8491ced13d | ||
|
|
229d4e2e1d | ||
|
|
7bdd2d69d6 | ||
|
|
ab2af9b5e5 | ||
|
|
5aebda2d16 | ||
|
|
a7273d0556 | ||
|
|
72aed64e5b | ||
|
|
00ad4d54e7 | ||
|
|
cba8c4ef3b | ||
|
|
b6e395fe0d | ||
|
|
8d07457c58 | ||
|
|
f1171e5358 | ||
|
|
d96c9f62c4 | ||
|
|
4f73e43a03 | ||
|
|
741b8ee8fd | ||
|
|
692081b7f0 | ||
|
|
943b1f08f0 |
79
.env
79
.env
@@ -1,79 +0,0 @@
|
||||
# MongoDB URI
|
||||
MONGODB=mongodb://localhost
|
||||
|
||||
# URL to where the Revolt app is publicly accessible
|
||||
REVOLT_APP_URL=http://local.revolt.chat:5000
|
||||
|
||||
# URL to where the API is publicly accessible
|
||||
REVOLT_PUBLIC_URL=http://local.revolt.chat:8000
|
||||
VITE_API_URL=http://local.revolt.chat:8000
|
||||
|
||||
# URL to where the WebSocket server is publicly accessible
|
||||
REVOLT_EXTERNAL_WS_URL=ws://local.revolt.chat:9000
|
||||
|
||||
# URL to where Autumn is publicly available
|
||||
AUTUMN_PUBLIC_URL=http://local.revolt.chat:3000
|
||||
|
||||
# URL to where January is publicly available
|
||||
JANUARY_PUBLIC_URL=http://local.revolt.chat:7000
|
||||
|
||||
# URL to where Vortex is publicly available
|
||||
# VOSO_PUBLIC_URL=https://voso.revolt.chat
|
||||
|
||||
|
||||
##
|
||||
## hCaptcha Settings
|
||||
##
|
||||
|
||||
# If you are sure that you don't want to use hCaptcha, set to 1.
|
||||
REVOLT_UNSAFE_NO_CAPTCHA=1
|
||||
|
||||
# hCaptcha API key
|
||||
# REVOLT_HCAPTCHA_KEY=0x0000000000000000000000000000000000000000
|
||||
|
||||
# hCaptcha site key
|
||||
# REVOLT_HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
|
||||
|
||||
|
||||
##
|
||||
## Email Settings
|
||||
##
|
||||
|
||||
# If you are sure that you don't want to use email verification, set to 1.
|
||||
REVOLT_UNSAFE_NO_EMAIL=1
|
||||
|
||||
# SMTP host
|
||||
# REVOLT_SMTP_HOST=smtp.example.com
|
||||
|
||||
# SMTP username
|
||||
# REVOLT_SMTP_USERNAME=noreply@example.com
|
||||
|
||||
# SMTP password
|
||||
# REVOLT_SMTP_PASSWORD=CHANGEME
|
||||
|
||||
# SMTP From header
|
||||
# REVOLT_SMTP_FROM=Revolt <noreply@example.com>
|
||||
|
||||
|
||||
##
|
||||
## Application Settings
|
||||
##
|
||||
|
||||
# Whether to only allow users to sign up if they have an invite code
|
||||
REVOLT_INVITE_ONLY=0
|
||||
|
||||
# Maximum number of people that can be in a group chat
|
||||
REVOLT_MAX_GROUP_SIZE=150
|
||||
|
||||
# VAPID keys for push notifications
|
||||
# Generate using this guide: https://gitlab.insrt.uk/revolt/delta/-/wikis/vapid
|
||||
# --> Please replace these keys before going into production! <--
|
||||
REVOLT_VAPID_PRIVATE_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
||||
REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw=
|
||||
|
||||
|
||||
##
|
||||
## Vortex configuration
|
||||
##
|
||||
|
||||
# VOSO_MANAGE_TOKEN=CHANGEME
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
ko_fi: insertish
|
||||
custom: https://insrt.uk/donate
|
||||
52
.github/workflows/docker.yaml
vendored
52
.github/workflows/docker.yaml
vendored
@@ -20,47 +20,52 @@ on:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
base:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build base image (amd64)
|
||||
name: Build base image
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
# Authenticate with GHCR
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Build all projects and cache
|
||||
- name: Build Base Image
|
||||
uses: docker/build-push-action@v3
|
||||
- name: Build base image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/revoltchat/base:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
cache-from: type=gha,scope=buildx-base-multi-arch
|
||||
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
|
||||
|
||||
publish_amd64:
|
||||
publish:
|
||||
needs: [base]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
strategy:
|
||||
matrix:
|
||||
project: [delta, bonfire]
|
||||
name: Build ${{ matrix.project }} image (amd64)
|
||||
name: Build ${{ matrix.project }} image
|
||||
steps:
|
||||
# Configure build environment
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
@@ -68,10 +73,11 @@ jobs:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -86,11 +92,11 @@ jobs:
|
||||
{
|
||||
"delta": {
|
||||
"path": "crates/delta",
|
||||
"tag": "revoltchat/server"
|
||||
"tag": "${{ github.repository_owner }}/server"
|
||||
},
|
||||
"bonfire": {
|
||||
"path": "crates/bonfire",
|
||||
"tag": "revoltchat/bonfire"
|
||||
"tag": "${{ github.repository_owner }}/bonfire"
|
||||
}
|
||||
}
|
||||
export_to: output
|
||||
@@ -98,19 +104,21 @@ jobs:
|
||||
# Configure metadata
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ steps.export.outputs.tag }}, ghcr.io/${{ steps.export.outputs.tag }}
|
||||
images: |
|
||||
docker.io/${{ steps.export.outputs.tag }}
|
||||
ghcr.io/${{ steps.export.outputs.tag }}
|
||||
|
||||
# Build crate image
|
||||
- name: Publish
|
||||
uses: docker/build-push-action@v3
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ${{ steps.export.outputs.path }}/Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
64
.github/workflows/rust.yaml
vendored
64
.github/workflows/rust.yaml
vendored
@@ -1,10 +1,9 @@
|
||||
name: Rust build and test
|
||||
name: Rust build, test, and generate specification
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
@@ -15,19 +14,68 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install latest nightly
|
||||
- name: Install latest stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
||||
|
||||
- name: Run services in background
|
||||
run: |
|
||||
docker-compose -f docker-compose.db.yml up -d
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
|
||||
- name: Run cargo test (with MongoDB)
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
MONGODB: mongodb://localhost
|
||||
with:
|
||||
command: test
|
||||
|
||||
- name: Copy .env.example
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
run: |
|
||||
cp .env.example .env
|
||||
|
||||
- name: Start API in background
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
run: |
|
||||
cargo run --bin revolt-delta &
|
||||
|
||||
- name: Wait for API to go up
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
uses: nev7n/wait_for_response@v1
|
||||
with:
|
||||
url: "http://localhost:8000/"
|
||||
|
||||
- name: Checkout API repository
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: revoltchat/api
|
||||
path: api
|
||||
token: ${{ secrets.PAT }}
|
||||
|
||||
- name: Download OpenAPI specification
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
||||
|
||||
- name: Commit changes
|
||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
||||
uses: EndBug/add-and-commit@v4
|
||||
with:
|
||||
cwd: "api"
|
||||
add: "*.json"
|
||||
author_name: Revolt CI
|
||||
author_email: revolt-ci@users.noreply.github.com
|
||||
message: "chore: generate OpenAPI specification"
|
||||
|
||||
27
.github/workflows/triage_issue.yml
vendored
27
.github/workflows/triage_issue.yml
vendored
@@ -15,22 +15,27 @@ jobs:
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
projectV2(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||
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:
|
||||
@@ -39,11 +44,11 @@ jobs:
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $issue:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||
projectNextItem {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
37
.github/workflows/triage_pr.yml
vendored
37
.github/workflows/triage_pr.yml
vendored
@@ -15,22 +15,27 @@ jobs:
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
projectV2(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
... on ProjectV2SingleSelectField {
|
||||
id
|
||||
name
|
||||
options {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
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=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||
|
||||
- name: Add PR to project
|
||||
env:
|
||||
@@ -39,13 +44,13 @@ jobs:
|
||||
run: |
|
||||
item_id="$( gh api graphql -f query='
|
||||
mutation($project:ID!, $pr:ID!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||
projectNextItem {
|
||||
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
|
||||
- name: Set fields
|
||||
@@ -59,14 +64,16 @@ jobs:
|
||||
$status_field: ID!
|
||||
$status_value: String!
|
||||
) {
|
||||
set_status: updateProjectNextItemField(input: {
|
||||
set_status: updateProjectV2ItemFieldValue(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: $status_value
|
||||
value: {
|
||||
singleSelectOptionId: $status_value
|
||||
}
|
||||
}) {
|
||||
projectNextItem {
|
||||
projectV2Item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
Rocket.toml
|
||||
target
|
||||
.data
|
||||
.env
|
||||
|
||||
.vercel
|
||||
|
||||
809
Cargo.lock
generated
809
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,6 @@
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
|
||||
|
||||
[patch.crates-io]
|
||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||
redis = { git = "https://github.com/insertish/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -1,12 +1,32 @@
|
||||
# Build Stage
|
||||
FROM rustlang/rust:nightly-slim AS builder
|
||||
FROM --platform="${BUILDPLATFORM}" rust:slim
|
||||
USER 0:0
|
||||
WORKDIR /home/rust/src
|
||||
|
||||
# Install build requirements
|
||||
RUN apt-get update && apt-get install -y libssl-dev pkg-config
|
||||
ARG TARGETARCH
|
||||
|
||||
# Build all crates
|
||||
# Install build requirements
|
||||
RUN dpkg --add-architecture "${TARGETARCH}"
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
make \
|
||||
pkg-config \
|
||||
libssl-dev:"${TARGETARCH}"
|
||||
COPY scripts/build-image-layer.sh /tmp/
|
||||
RUN sh /tmp/build-image-layer.sh tools
|
||||
|
||||
# Build all dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||
COPY crates/quark/Cargo.toml ./crates/quark/
|
||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||
RUN sh /tmp/build-image-layer.sh deps
|
||||
|
||||
# Build all apps
|
||||
COPY crates ./crates
|
||||
RUN cargo build --locked --release
|
||||
RUN sh /tmp/build-image-layer.sh apps
|
||||
|
||||
67
STYLE_GUIDE.md
Normal file
67
STYLE_GUIDE.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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.
|
||||
|
||||
## `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
build.sh
7
build.sh
@@ -1,7 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Build base image
|
||||
docker build -t revolt.chat/base:latest -f Dockerfile .
|
||||
|
||||
# Build crates
|
||||
docker build -t revolt.chat/delta:latest -f crates/delta/Dockerfile .
|
||||
docker build -t revolt.chat/bonfire:latest -f crates/bonfire/Dockerfile .
|
||||
12
clippy.toml
Normal file
12
clippy.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
disallowed-methods = [
|
||||
# Shouldn't need to access these directly
|
||||
"revolt_database::models::bots::model::Bot::remove_field",
|
||||
|
||||
# Prefer to use Object::update()
|
||||
"revolt_database::models::bots::ops::AbstractBots::update_bot",
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::update_account_strike",
|
||||
|
||||
# Prefer to use Object::delete()
|
||||
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
||||
"revolt_database::models::safety_strikes::ops::AbstractAccountStrikes::delete_account_strike",
|
||||
]
|
||||
2
crates/bonfire/.github/FUNDING.yml
vendored
2
crates/bonfire/.github/FUNDING.yml
vendored
@@ -1,2 +0,0 @@
|
||||
ko_fi: insertish
|
||||
custom: https://insrt.uk/donate
|
||||
98
crates/bonfire/.github/workflows/docker.yml
vendored
98
crates/bonfire/.github/workflows/docker.yml
vendored
@@ -1,98 +0,0 @@
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
tags:
|
||||
- "*"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "!.github/workflows/docker.yml"
|
||||
- ".vscode/**"
|
||||
- ".gitignore"
|
||||
- ".gitlab-ci.yml"
|
||||
- "LICENSE"
|
||||
- "README"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "Dockerfile"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [linux/amd64]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ matrix.architecture }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||
|
||||
publish_amd64:
|
||||
needs: [test]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: "recursive"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Cache amd64 Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache/linux/amd64
|
||||
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ghcr.io/revoltchat/bonfire
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and publish
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
33
crates/bonfire/.github/workflows/rust.yaml
vendored
33
crates/bonfire/.github/workflows/rust.yaml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Rust build and test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Rust project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install latest nightly
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
@@ -1,49 +0,0 @@
|
||||
name: Add Issue to Board
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
track_issue:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.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!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||
|
||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||
72
crates/bonfire/.github/workflows/triage_pr.yml
vendored
72
crates/bonfire/.github/workflows/triage_pr.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: Add PR to Board
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, review_requested]
|
||||
|
||||
jobs:
|
||||
track_pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get project data
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||
run: |
|
||||
gh api graphql -f query='
|
||||
query {
|
||||
organization(login: "revoltchat"){
|
||||
projectNext(number: 3) {
|
||||
id
|
||||
fields(first:20) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
settings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' > project_data.json
|
||||
|
||||
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.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!) {
|
||||
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.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: updateProjectNextItemField(input: {
|
||||
projectId: $project
|
||||
itemId: $item
|
||||
fieldId: $status_field
|
||||
value: $status_value
|
||||
}) {
|
||||
projectNextItem {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-bonfire"
|
||||
version = "1.0.6-patch.2"
|
||||
version = "0.5.20"
|
||||
license = "AGPL-3.0-or-later"
|
||||
edition = "2021"
|
||||
|
||||
@@ -26,3 +26,6 @@ serde = "1.0.136"
|
||||
futures = "0.3.21"
|
||||
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
|
||||
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
|
||||
|
||||
# core
|
||||
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Build Stage
|
||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||
RUN cargo install --locked --path crates/bonfire
|
||||
|
||||
# Bundle Stage
|
||||
FROM debian:buster-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
COPY --from=builder /usr/local/cargo/bin/revolt-bonfire ./
|
||||
FROM debian:bullseye-slim
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ca-certificates && \
|
||||
apt-get clean
|
||||
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
||||
EXPOSE 9000
|
||||
CMD ["./revolt-bonfire"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::env;
|
||||
|
||||
use async_std::net::TcpListener;
|
||||
use revolt_quark::presence::presence_clear_region;
|
||||
use revolt_presence::clear_region;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
@@ -18,7 +18,7 @@ async fn main() {
|
||||
database::connect().await;
|
||||
|
||||
// Clean up the current region information.
|
||||
presence_clear_region(None).await;
|
||||
clear_region(None).await;
|
||||
|
||||
// Setup a TCP listener to accept WebSocket connections on.
|
||||
// By default, we bind to port 9000 on all interfaces.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
|
||||
use revolt_presence::{create_session, delete_session};
|
||||
use revolt_quark::{
|
||||
events::{
|
||||
client::EventV1,
|
||||
@@ -8,7 +9,6 @@ use revolt_quark::{
|
||||
state::{State, SubscriptionStateChange},
|
||||
},
|
||||
models::{user::UserHint, User},
|
||||
presence::{presence_create_session, presence_delete_session},
|
||||
redis_kiss, Database,
|
||||
};
|
||||
|
||||
@@ -69,8 +69,7 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
let user_id = state.cache.user_id.clone();
|
||||
|
||||
// Create presence session.
|
||||
let (first_session, session_id) =
|
||||
presence_create_session(&user_id, 0).await;
|
||||
let (first_session, session_id) = create_session(&user_id, 0).await;
|
||||
|
||||
// Notify socket we have authenticated.
|
||||
write
|
||||
@@ -225,7 +224,7 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
||||
}
|
||||
|
||||
// Clean up presence session.
|
||||
let last_session = presence_delete_session(&user_id, session_id).await;
|
||||
let last_session = delete_session(&user_id, session_id).await;
|
||||
|
||||
// If this was the last session, notify other users that we just went offline.
|
||||
if last_session {
|
||||
|
||||
56
crates/core/database/Cargo.toml
Normal file
56
crates/core/database/Cargo.toml
Normal file
@@ -0,0 +1,56 @@
|
||||
[package]
|
||||
name = "revolt-database"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
description = "Revolt Backend: Database Implementation"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
# Databases
|
||||
mongodb = [ "dep:mongodb", "bson" ]
|
||||
|
||||
# ... Other
|
||||
async-std-runtime = [ "async-std" ]
|
||||
rocket-impl = [ "rocket", "schemars" ]
|
||||
|
||||
# Default Features
|
||||
default = [ "mongodb", "async-std-runtime" ]
|
||||
|
||||
[dependencies]
|
||||
# Core
|
||||
revolt-result = { version = "0.0.2", path = "../result" }
|
||||
revolt-permissions = { version = "0.0.2", path = "../permissions", features = [ "serde" ] }
|
||||
|
||||
# Utility
|
||||
log = "0.4"
|
||||
ulid = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
once_cell = "1.17"
|
||||
|
||||
# Serialisation
|
||||
serde_json = "1"
|
||||
revolt_optional_struct = "0.2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
|
||||
|
||||
# Database
|
||||
bson = { optional = true, version = "2.1.0" }
|
||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
||||
|
||||
# Async Language Features
|
||||
futures = "0.3.19"
|
||||
async-trait = "0.1.51"
|
||||
async-recursion = "1.0.4"
|
||||
|
||||
# Async
|
||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||
|
||||
# Rocket Impl
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"], optional = true }
|
||||
|
||||
# Authifier
|
||||
authifier = { version = "1.0" }
|
||||
67
crates/core/database/src/drivers/mod.rs
Normal file
67
crates/core/database/src/drivers/mod.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
pub use self::mongodb::*;
|
||||
pub use self::reference::*;
|
||||
|
||||
/// Database information to use to create a client
|
||||
pub enum DatabaseInfo {
|
||||
/// Auto-detect the database in use
|
||||
Auto,
|
||||
/// Auto-detect the database in use and create an empty testing database
|
||||
Test(String),
|
||||
/// Use the mock database
|
||||
Reference,
|
||||
/// Connect to MongoDB
|
||||
MongoDb { uri: String, database_name: String },
|
||||
/// Use existing MongoDB connection
|
||||
MongoDbFromClient(::mongodb::Client, String),
|
||||
}
|
||||
|
||||
/// Database
|
||||
#[derive(Clone)]
|
||||
pub enum Database {
|
||||
/// Mock database
|
||||
Reference(ReferenceDb),
|
||||
/// MongoDB database
|
||||
MongoDb(MongoDb),
|
||||
}
|
||||
|
||||
impl DatabaseInfo {
|
||||
/// Create a database client from the given database information
|
||||
#[async_recursion]
|
||||
pub async fn connect(self) -> Result<Database, String> {
|
||||
Ok(match self {
|
||||
DatabaseInfo::Auto => {
|
||||
if let Ok(uri) = std::env::var("MONGODB") {
|
||||
return DatabaseInfo::MongoDb {
|
||||
uri,
|
||||
database_name: "revolt".to_string(),
|
||||
}
|
||||
.connect()
|
||||
.await;
|
||||
}
|
||||
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
DatabaseInfo::Test(database_name) => {
|
||||
if let Ok(uri) = std::env::var("MONGODB") {
|
||||
return DatabaseInfo::MongoDb { uri, database_name }.connect().await;
|
||||
}
|
||||
|
||||
DatabaseInfo::Reference.connect().await?
|
||||
}
|
||||
DatabaseInfo::Reference => Database::Reference(Default::default()),
|
||||
DatabaseInfo::MongoDb { uri, database_name } => {
|
||||
let client = ::mongodb::Client::with_uri_str(uri)
|
||||
.await
|
||||
.map_err(|_| "Failed to init db connection.".to_string())?;
|
||||
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
}
|
||||
DatabaseInfo::MongoDbFromClient(client, database_name) => {
|
||||
Database::MongoDb(MongoDb(client, database_name))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
243
crates/core/database/src/drivers/mongodb.rs
Normal file
243
crates/core/database/src/drivers/mongodb.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Deref;
|
||||
|
||||
use futures::StreamExt;
|
||||
use mongodb::bson::{doc, to_document, Document};
|
||||
use mongodb::error::Result;
|
||||
use mongodb::options::{FindOneOptions, FindOptions};
|
||||
use mongodb::results::{DeleteResult, InsertOneResult, UpdateResult};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
|
||||
database_derived!(
|
||||
#[cfg(feature = "mongodb")]
|
||||
/// MongoDB implementation
|
||||
pub struct MongoDb(pub ::mongodb::Client, pub String);
|
||||
);
|
||||
|
||||
impl Deref for MongoDb {
|
||||
type Target = mongodb::Client;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl MongoDb {
|
||||
/// Get the Revolt database
|
||||
pub fn db(&self) -> mongodb::Database {
|
||||
self.database(&self.1)
|
||||
}
|
||||
|
||||
/// Get a collection by its name
|
||||
pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
|
||||
self.db().collection(collection)
|
||||
}
|
||||
|
||||
/// Insert one document into a collection
|
||||
pub async fn insert_one<T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
document: T,
|
||||
) -> Result<InsertOneResult> {
|
||||
self.col::<T>(collection).insert_one(document, None).await
|
||||
}
|
||||
|
||||
/// Count documents by projection
|
||||
pub async fn count_documents(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<u64> {
|
||||
self.col::<Document>(collection)
|
||||
.count_documents(projection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find multiple documents in a collection with options
|
||||
pub async fn find_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
options: O,
|
||||
) -> Result<Vec<T>>
|
||||
where
|
||||
O: Into<Option<FindOptions>>,
|
||||
{
|
||||
Ok(self
|
||||
.col::<T>(collection)
|
||||
.find(projection, options)
|
||||
.await?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
// Hard fail on invalid documents
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<T>>()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Find multiple documents in a collection
|
||||
pub async fn find<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<Vec<T>> {
|
||||
self.find_with_options(collection, projection, None).await
|
||||
}
|
||||
|
||||
/// Find one document with options
|
||||
pub async fn find_one_with_options<O, T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
options: O,
|
||||
) -> Result<Option<T>>
|
||||
where
|
||||
O: Into<Option<FindOneOptions>>,
|
||||
{
|
||||
self.col::<T>(collection)
|
||||
.find_one(projection, options)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find one document
|
||||
pub async fn find_one<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<Option<T>> {
|
||||
self.find_one_with_options(collection, projection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Find one document by its ID
|
||||
pub async fn find_one_by_id<T: DeserializeOwned + Unpin + Send + Sync>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
) -> Result<Option<T>> {
|
||||
self.find_one(
|
||||
collection,
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Update one document given a projection, partial document, and list of paths to unset
|
||||
pub async fn update_one<P, T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
partial: T,
|
||||
remove: Vec<&dyn IntoDocumentPath>,
|
||||
prefix: P,
|
||||
) -> Result<UpdateResult>
|
||||
where
|
||||
P: Into<Option<String>>,
|
||||
{
|
||||
let prefix = prefix.into();
|
||||
|
||||
let mut unset = doc! {};
|
||||
for field in remove {
|
||||
if let Some(path) = field.as_path() {
|
||||
if let Some(prefix) = &prefix {
|
||||
unset.insert(prefix.to_owned() + path, 1_i32);
|
||||
} else {
|
||||
unset.insert(path, 1_i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let query = doc! {
|
||||
"$unset": unset,
|
||||
"$set": if let Some(prefix) = &prefix {
|
||||
to_document(&prefix_keys(&partial, prefix))
|
||||
} else {
|
||||
to_document(&partial)
|
||||
}?
|
||||
};
|
||||
|
||||
self.col::<Document>(collection)
|
||||
.update_one(projection, query, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Update one document given an ID, partial document, and list of paths to unset
|
||||
pub async fn update_one_by_id<P, T: Serialize>(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
partial: T,
|
||||
remove: Vec<&dyn IntoDocumentPath>,
|
||||
prefix: P,
|
||||
) -> Result<UpdateResult>
|
||||
where
|
||||
P: Into<Option<String>>,
|
||||
{
|
||||
self.update_one(
|
||||
collection,
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
partial,
|
||||
remove,
|
||||
prefix,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Delete one document by the given projection
|
||||
pub async fn delete_one(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
projection: Document,
|
||||
) -> Result<DeleteResult> {
|
||||
self.col::<Document>(collection)
|
||||
.delete_one(projection, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Delete one document by the given ID
|
||||
pub async fn delete_one_by_id(
|
||||
&self,
|
||||
collection: &'static str,
|
||||
id: &str,
|
||||
) -> Result<DeleteResult> {
|
||||
self.delete_one(
|
||||
collection,
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// Just a string ID struct
|
||||
#[derive(Deserialize)]
|
||||
pub struct DocumentId {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
pub trait IntoDocumentPath: Send + Sync {
|
||||
/// Create JSON key path
|
||||
fn as_path(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
/// Prefix keys on an arbitrary object
|
||||
pub fn prefix_keys<T: Serialize>(t: &T, prefix: &str) -> HashMap<String, serde_json::Value> {
|
||||
let v: String = serde_json::to_string(t).unwrap();
|
||||
let v: HashMap<String, serde_json::Value> = serde_json::from_str(&v).unwrap();
|
||||
v.into_iter()
|
||||
.filter(|(_k, v)| !v.is_null())
|
||||
.map(|(k, v)| (prefix.to_owned() + &k, v))
|
||||
.collect()
|
||||
}
|
||||
28
crates/core/database/src/drivers/reference.rs
Normal file
28
crates/core/database/src/drivers/reference.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use futures::lock::Mutex;
|
||||
|
||||
use crate::{AccountStrike, Bot, File, Member, MemberCompositeKey, Server, User, UserSettings};
|
||||
|
||||
database_derived!(
|
||||
/// Reference implementation
|
||||
#[derive(Default)]
|
||||
pub struct ReferenceDb {
|
||||
pub account_strikes: Arc<Mutex<HashMap<String, AccountStrike>>>,
|
||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
||||
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
||||
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
|
||||
pub servers: Arc<Mutex<HashMap<String, Server>>>,
|
||||
pub files: Arc<Mutex<HashMap<String, File>>>,
|
||||
|
||||
pub server_bans: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub emoji: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub messages: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub channels: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub channel_invites: Arc<Mutex<HashMap<String, ()>>>,
|
||||
pub channel_unreads: Arc<Mutex<HashMap<String, ()>>>,
|
||||
}
|
||||
);
|
||||
86
crates/core/database/src/lib.rs
Normal file
86
crates/core/database/src/lib.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_recursion;
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
#[macro_use]
|
||||
extern crate revolt_optional_struct;
|
||||
|
||||
#[macro_use]
|
||||
extern crate revolt_result;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
pub use mongodb;
|
||||
|
||||
#[cfg(feature = "mongodb")]
|
||||
#[macro_use]
|
||||
extern crate bson;
|
||||
|
||||
macro_rules! database_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
#[derive(Clone)]
|
||||
$item
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! auto_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)]
|
||||
$item
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! auto_derived_partial {
|
||||
( $item:item, $name:expr ) => {
|
||||
#[derive(OptionalStruct, Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[optional_derive(Serialize, Deserialize, Debug, Clone, Default, Eq, PartialEq)]
|
||||
#[optional_name = $name]
|
||||
#[opt_skip_serializing_none]
|
||||
#[opt_some_priority]
|
||||
$item
|
||||
};
|
||||
}
|
||||
|
||||
mod drivers;
|
||||
pub use drivers::*;
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! database_test {
|
||||
( | $db: ident | $test:expr ) => {
|
||||
let db = $crate::DatabaseInfo::Test(format!(
|
||||
"{}:{}",
|
||||
file!().replace('/', "_").replace(".rs", ""),
|
||||
line!()
|
||||
))
|
||||
.connect()
|
||||
.await
|
||||
.expect("Database connection failed.");
|
||||
|
||||
db.drop_database().await;
|
||||
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
(|$db: $crate::Database| $test)(db.clone()).await;
|
||||
|
||||
db.drop_database().await
|
||||
};
|
||||
}
|
||||
|
||||
mod models;
|
||||
pub mod util;
|
||||
pub use models::*;
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
}
|
||||
5
crates/core/database/src/models/admin_migrations/mod.rs
Normal file
5
crates/core/database/src/models/admin_migrations/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
24
crates/core/database/src/models/admin_migrations/model.rs
Normal file
24
crates/core/database/src/models/admin_migrations/model.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
auto_derived!(
|
||||
/// Document representing migration information
|
||||
pub struct MigrationInfo {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: i32,
|
||||
/// Current database revision
|
||||
pub revision: i32,
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[async_std::test]
|
||||
async fn migrate() {
|
||||
database_test!(|db| async move {
|
||||
// Initialise the database
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Migrate the existing database
|
||||
db.migrate_database().await.unwrap()
|
||||
});
|
||||
}
|
||||
}
|
||||
12
crates/core/database/src/models/admin_migrations/ops.rs
Normal file
12
crates/core/database/src/models/admin_migrations/ops.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractMigrations: Sync + Send {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self);
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()>;
|
||||
}
|
||||
@@ -1,13 +1,20 @@
|
||||
use crate::{AbstractMigrations, Result};
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::super::MongoDb;
|
||||
use super::AbstractMigrations;
|
||||
|
||||
mod init;
|
||||
mod scripts;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractMigrations for MongoDb {
|
||||
async fn migrate_database(&self) -> Result<()> {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self) {
|
||||
self.db().drop(None).await.ok();
|
||||
}
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()> {
|
||||
info!("Migrating the database.");
|
||||
|
||||
let list = self
|
||||
@@ -15,7 +22,7 @@ impl AbstractMigrations for MongoDb {
|
||||
.await
|
||||
.expect("Failed to fetch database names.");
|
||||
|
||||
if list.iter().any(|x| x == "revolt") {
|
||||
if list.iter().any(|x| x == &self.1) {
|
||||
scripts::migrate_database(self).await;
|
||||
} else {
|
||||
init::create_database(self).await;
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::r#impl::mongo::MongoDb;
|
||||
|
||||
use super::scripts::LATEST_REVISION;
|
||||
|
||||
use mongodb::bson::doc;
|
||||
use mongodb::options::CreateCollectionOptions;
|
||||
use crate::mongodb::bson::doc;
|
||||
use crate::mongodb::options::CreateCollectionOptions;
|
||||
use crate::MongoDb;
|
||||
|
||||
pub async fn create_database(db: &MongoDb) {
|
||||
info!("Creating database.");
|
||||
@@ -57,6 +56,18 @@ pub async fn create_database(db: &MongoDb) {
|
||||
.await
|
||||
.expect("Failed to create user_settings collection.");
|
||||
|
||||
db.create_collection("safety_reports", None)
|
||||
.await
|
||||
.expect("Failed to create safety_reports collection.");
|
||||
|
||||
db.create_collection("safety_snapshots", None)
|
||||
.await
|
||||
.expect("Failed to create safety_snapshots collection.");
|
||||
|
||||
db.create_collection("safety_strikes", None)
|
||||
.await
|
||||
.expect("Failed to create safety_strikes collection.");
|
||||
|
||||
db.create_collection("bots", None)
|
||||
.await
|
||||
.expect("Failed to create bots collection.");
|
||||
@@ -103,18 +114,18 @@ pub async fn create_database(db: &MongoDb) {
|
||||
},
|
||||
"name": "content"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32
|
||||
},
|
||||
"name": "channel"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"channel": 1_i32,
|
||||
"_id": 1_i32
|
||||
},
|
||||
"name": "channel_id_compound"
|
||||
},
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32
|
||||
},
|
||||
"name": "author"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1,22 +1,22 @@
|
||||
use std::time::Duration;
|
||||
use std::{ops::BitXor, time::Duration};
|
||||
|
||||
use bson::Bson;
|
||||
use futures::StreamExt;
|
||||
use mongodb::{
|
||||
bson::{doc, from_bson, from_document, to_document, Document},
|
||||
options::FindOptions,
|
||||
use crate::{
|
||||
mongodb::{
|
||||
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
||||
options::FindOptions,
|
||||
},
|
||||
MongoDb,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{r#impl::mongo::MongoDb, Permission, DEFAULT_PERMISSION_SERVER};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MigrationInfo {
|
||||
_id: i32,
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 17;
|
||||
pub const LATEST_REVISION: i32 = 22;
|
||||
|
||||
pub async fn migrate_database(db: &MongoDb) {
|
||||
let migrations = db.col::<Document>("migrations");
|
||||
@@ -215,7 +215,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
}
|
||||
|
||||
if revision <= 8 {
|
||||
info!("Running migration [revision 8 / 2021-09-10]: Update to rAuth version 1.");
|
||||
info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1.");
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
@@ -503,13 +503,8 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
|
||||
update.insert(
|
||||
"default_permissions",
|
||||
(*DEFAULT_PERMISSION_SERVER
|
||||
// Remove Send Message permission if it wasn't originally granted
|
||||
^ (if has_send {
|
||||
0
|
||||
} else {
|
||||
Permission::SendMessage as u64
|
||||
})) as i64,
|
||||
// Remove Send Message permission if it wasn't originally granted
|
||||
(4000323584).bitxor(if has_send { 0 } else { (1 << 22) as u64 }) as i64,
|
||||
);
|
||||
|
||||
if let Some(Bson::Document(mut roles)) = document.remove("roles") {
|
||||
@@ -568,7 +563,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
doc! {
|
||||
"default_permissions": {
|
||||
"a": 0_i64,
|
||||
"d": Permission::SendMessage as i64
|
||||
"d": (1 << 22) as i64
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -603,20 +598,20 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
}
|
||||
|
||||
if revision <= 15 {
|
||||
info!("Running migration [revision 15 / 04-06-2022]: Migrate rAuth to latest version.");
|
||||
info!("Running migration [revision 15 / 04-06-2022]: Migrate Authifier to latest version.");
|
||||
|
||||
let db = rauth::Database::MongoDb(rauth::database::MongoDb(db.db()));
|
||||
db.run_migration(rauth::Migration::M2022_06_03EnsureUpToSpec)
|
||||
let db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
|
||||
db.run_migration(authifier::Migration::M2022_06_03EnsureUpToSpec)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if revision <= 16 {
|
||||
info!("Running migration [revision 16 / 07-07-2022]: Add `emojis` collection and rAuth migration.");
|
||||
info!("Running migration [revision 16 / 07-07-2022]: Add `emojis` collection and Authifier migration.");
|
||||
|
||||
let rauth_db = rauth::Database::MongoDb(rauth::database::MongoDb(db.db()));
|
||||
rauth_db
|
||||
.run_migration(rauth::Migration::M2022_06_09AddIndexForDeletion)
|
||||
let authifier_db = authifier::Database::MongoDb(authifier::database::MongoDb(db.db()));
|
||||
authifier_db
|
||||
.run_migration(authifier::Migration::M2022_06_09AddIndexForDeletion)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -644,6 +639,118 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
||||
.expect("Failed to create emoji parent index.");
|
||||
}
|
||||
|
||||
if revision <= 17 {
|
||||
info!("Running migration [revision 17 / 15-07-2022]: Initialise `joined_at` property on server members.");
|
||||
|
||||
db.col::<Document>("server_members")
|
||||
.update_many(
|
||||
doc! {},
|
||||
doc! {
|
||||
"$set": {
|
||||
"joined_at": DateTime::now().to_rfc3339_string()
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update server members.");
|
||||
}
|
||||
|
||||
if revision <= 18 {
|
||||
info!("Running migration [revision 18 / 27-02-2022]: Create author index on messages. Drop plain channel index if exists.");
|
||||
|
||||
if db
|
||||
.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"dropIndexes": "messages",
|
||||
"index": ["channel"]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
info!("Failed to drop `messages.channel` index but this is ok since that means it's probably gone.");
|
||||
}
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "messages",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"author": 1_i32,
|
||||
},
|
||||
"name": "author"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create messages author index.");
|
||||
}
|
||||
|
||||
if revision <= 19 {
|
||||
info!("Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections, migrate to new model if applicable.");
|
||||
|
||||
db.db()
|
||||
.create_collection("safety_reports", None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.db()
|
||||
.create_collection("safety_snapshots", None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.col::<Document>("safety_reports")
|
||||
.update_many(
|
||||
doc! {},
|
||||
doc! {
|
||||
"$set": {
|
||||
"status": "Created"
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if revision <= 20 {
|
||||
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
|
||||
|
||||
db.db()
|
||||
.run_command(
|
||||
doc! {
|
||||
"createIndexes": "safety_snapshots",
|
||||
"indexes": [
|
||||
{
|
||||
"key": {
|
||||
"report_id": 1_i32
|
||||
},
|
||||
"name": "report_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create safety snapshot index.");
|
||||
}
|
||||
|
||||
if revision <= 21 {
|
||||
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
|
||||
|
||||
db.db()
|
||||
.create_collection("safety_strikes", None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractMigrations;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractMigrations for ReferenceDb {
|
||||
#[cfg(test)]
|
||||
/// Drop the database
|
||||
async fn drop_database(&self) {}
|
||||
|
||||
/// Migrate the database
|
||||
async fn migrate_database(&self) -> Result<(), ()> {
|
||||
// Here you would do your typical migrations if this was a real database.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/bots/mod.rs
Normal file
5
crates/core/database/src/models/bots/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
154
crates/core/database/src/models/bots/model.rs
Normal file
154
crates/core/database/src/models/bots/model.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Bot
|
||||
pub struct Bot {
|
||||
/// Bot Id
|
||||
///
|
||||
/// This equals the associated bot user's id.
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// User Id of the bot owner
|
||||
pub owner: String,
|
||||
/// Token used to authenticate requests for this bot
|
||||
pub token: String,
|
||||
/// Whether the bot is public
|
||||
/// (may be invited by anyone)
|
||||
pub public: bool,
|
||||
|
||||
/// Whether to enable analytics
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub analytics: bool,
|
||||
/// Whether this bot should be publicly discoverable
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub discoverable: bool,
|
||||
/// Reserved; URL for handling interactions
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub interactions_url: String,
|
||||
/// URL for terms of service
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub terms_of_service_url: String,
|
||||
/// URL for privacy policy
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub privacy_policy_url: String,
|
||||
|
||||
/// Enum of bot flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
},
|
||||
"PartialBot"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Optional fields on bot object
|
||||
pub enum FieldsBot {
|
||||
Token,
|
||||
InteractionsURL,
|
||||
}
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl Bot {
|
||||
/// Remove a field from this object
|
||||
pub fn remove_field(&mut self, field: &FieldsBot) {
|
||||
match field {
|
||||
FieldsBot::Token => self.token = nanoid::nanoid!(64),
|
||||
FieldsBot::InteractionsURL => {
|
||||
self.interactions_url = String::new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update this bot
|
||||
pub async fn update(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
mut partial: PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
if remove.contains(&FieldsBot::Token) {
|
||||
partial.token = Some(nanoid::nanoid!(64));
|
||||
}
|
||||
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
db.update_bot(&self.id, &partial, remove).await?;
|
||||
|
||||
self.apply_options(partial);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete this bot
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
db.fetch_user(&self.id).await?.mark_deleted(db).await?;
|
||||
db.delete_bot(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Bot, FieldsBot, PartialBot, User};
|
||||
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let bot_id = "bot";
|
||||
let user_id = "user";
|
||||
let token = "my_token";
|
||||
|
||||
let user = User {
|
||||
id: bot_id.to_string(),
|
||||
username: "Bot Name".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_user(&user).await.unwrap();
|
||||
|
||||
let bot = Bot {
|
||||
id: bot_id.to_string(),
|
||||
owner: user_id.to_string(),
|
||||
token: token.to_string(),
|
||||
interactions_url: "some url".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
db.insert_bot(&bot).await.unwrap();
|
||||
|
||||
let mut updated_bot = bot.clone();
|
||||
updated_bot
|
||||
.update(
|
||||
&db,
|
||||
PartialBot {
|
||||
public: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
vec![FieldsBot::Token, FieldsBot::InteractionsURL],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let fetched_bot1 = db.fetch_bot(bot_id).await.unwrap();
|
||||
let fetched_bot2 = db.fetch_bot_by_token(&fetched_bot1.token).await.unwrap();
|
||||
let fetched_bots = db.fetch_bots_by_user(user_id).await.unwrap();
|
||||
|
||||
assert!(!bot.public);
|
||||
assert!(fetched_bot1.public);
|
||||
assert!(!bot.interactions_url.is_empty());
|
||||
assert!(fetched_bot1.interactions_url.is_empty());
|
||||
assert_ne!(bot.token, fetched_bot1.token);
|
||||
assert_eq!(updated_bot, fetched_bot1);
|
||||
assert_eq!(fetched_bot1, fetched_bot2);
|
||||
assert_eq!(fetched_bot1, fetched_bots[0]);
|
||||
assert_eq!(1, db.get_number_of_bots_by_user(user_id).await.unwrap());
|
||||
|
||||
bot.delete(&db).await.unwrap();
|
||||
assert!(db.fetch_bot(bot_id).await.is_err());
|
||||
assert_eq!(0, db.get_number_of_bots_by_user(user_id).await.unwrap());
|
||||
assert_eq!(db.fetch_user(bot_id).await.unwrap().flags, Some(2))
|
||||
});
|
||||
}
|
||||
}
|
||||
35
crates/core/database/src/models/bots/ops.rs
Normal file
35
crates/core/database/src/models/bots/ops.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractBots: Sync + Send {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()>;
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot>;
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot>;
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>>;
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize>;
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
92
crates/core/database/src/models/bots/ops/mongodb.rs
Normal file
92
crates/core/database/src/models/bots/ops/mongodb.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractBots;
|
||||
|
||||
static COL: &str = "bots";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractBots for MongoDb {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
|
||||
query!(self, insert_one, COL, &bot).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"token": token
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
|
||||
query!(
|
||||
self,
|
||||
find,
|
||||
COL,
|
||||
doc! {
|
||||
"owner": user_id
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
|
||||
query!(
|
||||
self,
|
||||
count_documents,
|
||||
COL,
|
||||
doc! {
|
||||
"owner": user_id
|
||||
}
|
||||
)
|
||||
.map(|v| v as usize)
|
||||
}
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsBot {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
FieldsBot::InteractionsURL => Some("interactions_url"),
|
||||
FieldsBot::Token => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
82
crates/core/database/src/models/bots/ops/reference.rs
Normal file
82
crates/core/database/src/models/bots/ops/reference.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{Bot, FieldsBot, PartialBot};
|
||||
|
||||
use super::AbstractBots;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractBots for ReferenceDb {
|
||||
/// Insert new bot into the database
|
||||
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if bots.contains_key(&bot.id) {
|
||||
Err(create_database_error!("insert", "bot"))
|
||||
} else {
|
||||
bots.insert(bot.id.to_string(), bot.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a bot by its id
|
||||
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
|
||||
let bots = self.bots.lock().await;
|
||||
bots.get(id).cloned().ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a bot by its token
|
||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
|
||||
let bots = self.bots.lock().await;
|
||||
bots.values()
|
||||
.find(|bot| bot.token == token)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch bots owned by a user
|
||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
|
||||
let bots = self.bots.lock().await;
|
||||
Ok(bots
|
||||
.values()
|
||||
.filter(|bot| bot.owner == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Get the number of bots owned by a user
|
||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
|
||||
let bots = self.bots.lock().await;
|
||||
Ok(bots.values().filter(|bot| bot.owner == user_id).count())
|
||||
}
|
||||
|
||||
/// Update bot with new information
|
||||
async fn update_bot(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialBot,
|
||||
remove: Vec<FieldsBot>,
|
||||
) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if let Some(bot) = bots.get_mut(id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
bot.remove_field(&field);
|
||||
}
|
||||
|
||||
bot.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a bot from the database
|
||||
async fn delete_bot(&self, id: &str) -> Result<()> {
|
||||
let mut bots = self.bots.lock().await;
|
||||
if bots.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/files/mod.rs
Normal file
5
crates/core/database/src/models/files/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
107
crates/core/database/src/models/files/model.rs
Normal file
107
crates/core/database/src/models/files/model.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::Database;
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// File
|
||||
pub struct File {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Tag / bucket this file was uploaded to
|
||||
pub tag: String,
|
||||
/// Original filename
|
||||
pub filename: String,
|
||||
/// Parsed metadata of this file
|
||||
pub metadata: Metadata,
|
||||
/// Raw content type of this file
|
||||
pub content_type: String,
|
||||
/// Size of this file (in bytes)
|
||||
pub size: isize,
|
||||
|
||||
/// Whether this file was deleted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deleted: Option<bool>,
|
||||
/// Whether this file was reported
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reported: Option<bool>,
|
||||
|
||||
// TODO: migrate this mess to having:
|
||||
// - author_id
|
||||
// - parent: Parent { Message(id), User(id), etc }
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub server_id: Option<String>,
|
||||
|
||||
/// Id of the object this file is associated with
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_id: Option<String>,
|
||||
},
|
||||
"PartialFile"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Metadata associated with a file
|
||||
#[serde(tag = "type")]
|
||||
#[derive(Default)]
|
||||
pub enum Metadata {
|
||||
/// File is just a generic uncategorised file
|
||||
#[default]
|
||||
File,
|
||||
/// File contains textual data and should be displayed as such
|
||||
Text,
|
||||
/// File is an image with specific dimensions
|
||||
Image { width: isize, height: isize },
|
||||
/// File is a video with specific dimensions
|
||||
Video { width: isize, height: isize },
|
||||
/// File is audio
|
||||
Audio,
|
||||
}
|
||||
);
|
||||
|
||||
impl File {
|
||||
/// Use a file for a message attachment
|
||||
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "attachments", "message", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a user profile background
|
||||
pub async fn use_background(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "backgrounds", "user", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a user avatar
|
||||
pub async fn use_avatar(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "avatars", "user", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for an icon
|
||||
pub async fn use_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a server icon
|
||||
pub async fn use_server_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for a server banner
|
||||
pub async fn use_banner(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "banners", "server", parent)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use a file for an emoji
|
||||
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
|
||||
db.find_and_use_attachment(id, "emojis", "object", parent)
|
||||
.await
|
||||
}
|
||||
}
|
||||
30
crates/core/database/src/models/files/ops.rs
Normal file
30
crates/core/database/src/models/files/ops.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::File;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAttachments: Sync + Send {
|
||||
/// Insert attachment into database.
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
id: &str,
|
||||
tag: &str,
|
||||
parent_type: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<File>;
|
||||
|
||||
/// Mark an attachment as having been reported.
|
||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Mark an attachment as having been deleted.
|
||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Mark multiple attachments as having been deleted.
|
||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>;
|
||||
}
|
||||
117
crates/core/database/src/models/files/ops/mongodb.rs
Normal file
117
crates/core/database/src/models/files/ops/mongodb.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use bson::Document;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::File;
|
||||
use crate::MongoDb;
|
||||
|
||||
use super::AbstractAttachments;
|
||||
|
||||
static COL: &str = "bots";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAttachments for MongoDb {
|
||||
/// Insert attachment into database.
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
||||
query!(self, insert_one, COL, &attachment).map(|_| ())
|
||||
}
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
id: &str,
|
||||
tag: &str,
|
||||
parent_type: &str,
|
||||
parent_id: &str,
|
||||
) -> Result<File> {
|
||||
let key = format!("{parent_type}_id");
|
||||
let file = query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id": id,
|
||||
"tag": tag,
|
||||
&key: {
|
||||
"$exists": false
|
||||
}
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))?;
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
key: parent_id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", COL))?;
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Mark an attachment as having been reported.
|
||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"reported": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Mark an attachment as having been deleted.
|
||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Mark multiple attachments as having been deleted.
|
||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"deleted": true
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
}
|
||||
79
crates/core/database/src/models/files/ops/reference.rs
Normal file
79
crates/core/database/src/models/files/ops/reference.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::File;
|
||||
use crate::ReferenceDb;
|
||||
|
||||
use super::AbstractAttachments;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAttachments for ReferenceDb {
|
||||
/// Insert attachment into database.
|
||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
||||
let mut attachments = self.files.lock().await;
|
||||
if attachments.contains_key(&attachment.id) {
|
||||
Err(create_database_error!("insert", "attachment"))
|
||||
} else {
|
||||
attachments.insert(attachment.id.to_string(), attachment.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an attachment by its details and mark it as used by a given parent.
|
||||
async fn find_and_use_attachment(
|
||||
&self,
|
||||
id: &str,
|
||||
_tag: &str,
|
||||
_parent_type: &str,
|
||||
_parent_id: &str,
|
||||
) -> Result<File> {
|
||||
let mut files = self.files.lock().await;
|
||||
if let Some(file) = files.get_mut(id) {
|
||||
// TODO: check tag
|
||||
// TODO: set parent ID
|
||||
Ok(file.clone())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark an attachment as having been reported.
|
||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
|
||||
let mut files = self.files.lock().await;
|
||||
if let Some(file) = files.get_mut(id) {
|
||||
file.reported = Some(true);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark an attachment as having been deleted.
|
||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
|
||||
let mut files = self.files.lock().await;
|
||||
if let Some(file) = files.get_mut(id) {
|
||||
file.deleted = Some(true);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark multiple attachments as having been deleted.
|
||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
|
||||
let mut files = self.files.lock().await;
|
||||
|
||||
for id in ids {
|
||||
if !files.contains_key(id) {
|
||||
return Err(create_error!(NotFound));
|
||||
}
|
||||
}
|
||||
|
||||
for id in ids {
|
||||
if let Some(file) = files.get_mut(id) {
|
||||
file.reported = Some(true);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
47
crates/core/database/src/models/mod.rs
Normal file
47
crates/core/database/src/models/mod.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
mod admin_migrations;
|
||||
mod bots;
|
||||
mod files;
|
||||
mod safety_strikes;
|
||||
mod server_members;
|
||||
mod servers;
|
||||
mod user_settings;
|
||||
mod users;
|
||||
|
||||
pub use admin_migrations::*;
|
||||
pub use bots::*;
|
||||
pub use files::*;
|
||||
pub use safety_strikes::*;
|
||||
pub use server_members::*;
|
||||
pub use servers::*;
|
||||
pub use user_settings::*;
|
||||
pub use users::*;
|
||||
|
||||
use crate::{Database, MongoDb, ReferenceDb};
|
||||
|
||||
pub trait AbstractDatabase:
|
||||
Sync
|
||||
+ Send
|
||||
+ admin_migrations::AbstractMigrations
|
||||
+ bots::AbstractBots
|
||||
+ files::AbstractAttachments
|
||||
+ safety_strikes::AbstractAccountStrikes
|
||||
+ server_members::AbstractServerMembers
|
||||
+ servers::AbstractServers
|
||||
+ user_settings::AbstractUserSettings
|
||||
+ users::AbstractUsers
|
||||
{
|
||||
}
|
||||
|
||||
impl AbstractDatabase for ReferenceDb {}
|
||||
impl AbstractDatabase for MongoDb {}
|
||||
|
||||
impl std::ops::Deref for Database {
|
||||
type Target = dyn AbstractDatabase;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match &self {
|
||||
Database::Reference(dummy) => dummy,
|
||||
Database::MongoDb(mongo) => mongo,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="9BKyaxnbqnTkbfLuLd1z" name="Page-1">
|
||||
<mxGraphModel dx="1033" dy="710" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<mxGraphModel dx="2466" dy="650" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
@@ -35,18 +35,12 @@
|
||||
<mxCell id="2" value="Attachment" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="220" y="490" width="80" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="49" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="3" target="5" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="195" y="340"/>
|
||||
<mxPoint x="245" y="340"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="51" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="3" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="290" y="160" as="targetPoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="95" y="340"/>
|
||||
<mxPoint x="195" y="340"/>
|
||||
<mxPoint x="195" y="260"/>
|
||||
<mxPoint x="260" y="260"/>
|
||||
<mxPoint x="260" y="220"/>
|
||||
@@ -54,11 +48,19 @@
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="68" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" edge="1" parent="1" source="3" target="5">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="95" y="340"/>
|
||||
<mxPoint x="245" y="340"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="3" value="Channel Invite" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="150" y="400" width="90" height="30" as="geometry"/>
|
||||
<mxGeometry x="50" y="400" width="90" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="Channel Unread" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="40" y="400" width="100" height="30" as="geometry"/>
|
||||
<mxGeometry x="-70" y="400" width="100" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="43" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;dashed=1;" parent="1" source="5" target="12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
@@ -66,6 +68,14 @@
|
||||
<mxCell id="60" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="5" target="9" edge="1">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="67" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;startArrow=diamondThin;startFill=1;" edge="1" parent="1" source="5" target="66">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="245" y="370"/>
|
||||
<mxPoint x="195" y="370"/>
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Channel" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="230" y="280" width="60" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
@@ -161,8 +171,8 @@
|
||||
<mxPoint x="290" y="160" as="targetPoint"/>
|
||||
<mxPoint x="200" y="400" as="sourcePoint"/>
|
||||
<Array as="points">
|
||||
<mxPoint x="90" y="370"/>
|
||||
<mxPoint x="195" y="370"/>
|
||||
<mxPoint x="-20" y="340"/>
|
||||
<mxPoint x="195" y="340"/>
|
||||
<mxPoint x="195" y="260"/>
|
||||
<mxPoint x="260" y="260"/>
|
||||
<mxPoint x="260" y="220"/>
|
||||
@@ -199,6 +209,9 @@
|
||||
<mxPoint x="320" y="440" as="targetPoint"/>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="66" value="Webhook" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="160" y="400" width="70" height="30" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
5
crates/core/database/src/models/safety_strikes/mod.rs
Normal file
5
crates/core/database/src/models/safety_strikes/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
105
crates/core/database/src/models/safety_strikes/model.rs
Normal file
105
crates/core/database/src/models/safety_strikes/model.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Account Strike
|
||||
pub struct AccountStrike {
|
||||
/// Strike Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// User Id of reported user
|
||||
pub user_id: String,
|
||||
|
||||
/// Attached reason
|
||||
pub reason: String,
|
||||
},
|
||||
"PartialAccountStrike"
|
||||
);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
impl AccountStrike {
|
||||
/// Update this strike
|
||||
pub async fn update(&mut self, db: &Database, partial: PartialAccountStrike) -> Result<()> {
|
||||
db.update_account_strike(&self.id, &partial).await?;
|
||||
self.apply_options(partial);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete this strike
|
||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
||||
db.delete_account_strike(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{AccountStrike, PartialAccountStrike};
|
||||
|
||||
#[async_std::test]
|
||||
async fn crud() {
|
||||
database_test!(|db| async move {
|
||||
let user_id = "user";
|
||||
let strike_a = "a";
|
||||
let strike_b = "b";
|
||||
|
||||
let strike = AccountStrike {
|
||||
id: strike_a.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
reason: "reason 1".to_string(),
|
||||
};
|
||||
|
||||
db.insert_account_strike(&strike).await.unwrap();
|
||||
|
||||
let mut updated_strike = strike.clone();
|
||||
updated_strike
|
||||
.update(
|
||||
&db,
|
||||
PartialAccountStrike {
|
||||
reason: Some("new reason".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.insert_account_strike(&AccountStrike {
|
||||
id: strike_b.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
reason: "reason 2".to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let strikes = db.fetch_account_strikes_by_user(user_id).await.unwrap();
|
||||
|
||||
let ids = strikes
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|strike| strike.id)
|
||||
.collect::<HashSet<String>>();
|
||||
|
||||
assert!(ids.contains(strike_a));
|
||||
assert!(ids.contains(strike_b));
|
||||
|
||||
let fetched_strike = strikes
|
||||
.into_iter()
|
||||
.find(|strike| strike.id == strike_a)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(fetched_strike, updated_strike);
|
||||
assert_ne!(fetched_strike, strike);
|
||||
|
||||
strike.delete(&db).await.unwrap();
|
||||
assert_eq!(
|
||||
1,
|
||||
db.fetch_account_strikes_by_user(user_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.len()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
24
crates/core/database/src/models/safety_strikes/ops.rs
Normal file
24
crates/core/database/src/models/safety_strikes/ops.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{AccountStrike, PartialAccountStrike};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractAccountStrikes: Sync + Send {
|
||||
/// Insert new strike into the database
|
||||
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()>;
|
||||
|
||||
/// Fetch strike by id
|
||||
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike>;
|
||||
|
||||
/// Fetch strikes by user id
|
||||
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>>;
|
||||
|
||||
/// Update strike with new information
|
||||
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()>;
|
||||
|
||||
/// Delete a strike from the database
|
||||
async fn delete_account_strike(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::{AccountStrike, PartialAccountStrike};
|
||||
|
||||
use super::AbstractAccountStrikes;
|
||||
|
||||
static COL: &str = "bots";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAccountStrikes for MongoDb {
|
||||
/// Insert new strike into the database
|
||||
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
|
||||
query!(self, insert_one, COL, &strike).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch strike by id
|
||||
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch strikes by user id
|
||||
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
|
||||
Ok(self
|
||||
.col::<AccountStrike>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"user_id": user_id,
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Update strike with new information
|
||||
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
|
||||
query!(self, update_one_by_id, COL, id, partial, vec![], None).map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete a strike from the database
|
||||
async fn delete_account_strike(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{AccountStrike, PartialAccountStrike};
|
||||
|
||||
use super::AbstractAccountStrikes;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractAccountStrikes for ReferenceDb {
|
||||
/// Insert new strike into the database
|
||||
async fn insert_account_strike(&self, strike: &AccountStrike) -> Result<()> {
|
||||
let mut strikes = self.account_strikes.lock().await;
|
||||
if strikes.contains_key(&strike.id) {
|
||||
Err(create_database_error!("insert", "strike"))
|
||||
} else {
|
||||
strikes.insert(strike.id.to_string(), strike.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch strike by id
|
||||
async fn fetch_account_strike(&self, id: &str) -> Result<AccountStrike> {
|
||||
let strikes = self.account_strikes.lock().await;
|
||||
strikes
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch strikes by user id
|
||||
async fn fetch_account_strikes_by_user(&self, user_id: &str) -> Result<Vec<AccountStrike>> {
|
||||
let strikes = self.account_strikes.lock().await;
|
||||
Ok(strikes
|
||||
.values()
|
||||
.filter(|strike| strike.user_id == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Update strike with new information
|
||||
async fn update_account_strike(&self, id: &str, partial: &PartialAccountStrike) -> Result<()> {
|
||||
let mut strikes = self.account_strikes.lock().await;
|
||||
if let Some(strike) = strikes.get_mut(id) {
|
||||
strike.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a strike from the database
|
||||
async fn delete_account_strike(&self, id: &str) -> Result<()> {
|
||||
let mut strikes = self.account_strikes.lock().await;
|
||||
if strikes.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/server_members/mod.rs
Normal file
5
crates/core/database/src/models/server_members/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
118
crates/core/database/src/models/server_members/model.rs
Normal file
118
crates/core/database/src/models/server_members/model.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use iso8601_timestamp::Timestamp;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{Database, File, Server};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server Member
|
||||
pub struct Member {
|
||||
/// Unique member id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: MemberCompositeKey,
|
||||
|
||||
/// Time at which this user joined the server
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub joined_at: Option<Timestamp>,
|
||||
|
||||
/// Member's nickname
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nickname: Option<String>,
|
||||
/// Avatar attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<File>,
|
||||
|
||||
/// Member's roles
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub roles: Vec<String>,
|
||||
/// Timestamp this member is timed out until
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timeout: Option<Timestamp>,
|
||||
},
|
||||
"PartialMember"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Composite primary key consisting of server and user id
|
||||
#[derive(Hash, Default)]
|
||||
pub struct MemberCompositeKey {
|
||||
/// Server Id
|
||||
pub server: String,
|
||||
/// User Id
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
/// Optional fields on server member object
|
||||
pub enum FieldsMember {
|
||||
Nickname,
|
||||
Avatar,
|
||||
Roles,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Member removal intention
|
||||
pub enum RemovalIntention {
|
||||
Leave,
|
||||
Kick,
|
||||
Ban,
|
||||
}
|
||||
);
|
||||
|
||||
impl Member {
|
||||
/// Update member data
|
||||
pub async fn update<'a>(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialMember,
|
||||
remove: Vec<FieldsMember>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_member(&self.id, &partial, remove.clone()).await?;
|
||||
|
||||
/* // TODO: EventV1::ServerMemberUpdate {
|
||||
id: self.id.clone(),
|
||||
data: partial,
|
||||
clear: remove,
|
||||
}
|
||||
.p(self.id.server.clone())
|
||||
.await; */
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_field(&mut self, field: &FieldsMember) {
|
||||
match field {
|
||||
FieldsMember::Avatar => self.avatar = None,
|
||||
FieldsMember::Nickname => self.nickname = None,
|
||||
FieldsMember::Roles => self.roles.clear(),
|
||||
FieldsMember::Timeout => self.timeout = None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get this user's current ranking
|
||||
pub fn get_ranking(&self, server: &Server) -> i64 {
|
||||
let mut value = i64::MAX;
|
||||
for role in &self.roles {
|
||||
if let Some(role) = server.roles.get(role) {
|
||||
if role.rank < value {
|
||||
value = role.rank;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
/// Check whether this member is in timeout
|
||||
pub fn in_timeout(&self) -> bool {
|
||||
if let Some(timeout) = self.timeout {
|
||||
*timeout > *Timestamp::now_utc()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
41
crates/core/database/src/models/server_members/ops.rs
Normal file
41
crates/core/database/src/models/server_members/ops.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[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>;
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>>;
|
||||
|
||||
/// Fetch member count of a server
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize>;
|
||||
|
||||
/// Fetch server count of a user
|
||||
async fn fetch_server_count(&self, user_id: &str) -> Result<usize>;
|
||||
|
||||
/// Update information for a server member
|
||||
async fn update_member(
|
||||
&self,
|
||||
id: &MemberCompositeKey,
|
||||
partial: &PartialMember,
|
||||
remove: Vec<FieldsMember>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
||||
}
|
||||
178
crates/core/database/src/models/server_members/ops/mongodb.rs
Normal file
178
crates/core/database/src/models/server_members/ops/mongodb.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractServerMembers;
|
||||
|
||||
static COL: &str = "server_members";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractServerMembers for MongoDb {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
||||
query!(self, insert_one, COL, &member).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a server member by their id
|
||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member> {
|
||||
query!(
|
||||
self,
|
||||
find_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.server": server_id,
|
||||
"_id.user": user_id
|
||||
}
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id.server": server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id.user": user_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
||||
Ok(self
|
||||
.col::<Member>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id.server": server_id,
|
||||
"_id.user": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch member count of a server
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
|
||||
self.col::<Member>(COL)
|
||||
.count_documents(
|
||||
doc! {
|
||||
"_id.server": server_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|c| c as usize)
|
||||
.map_err(|_| create_database_error!("count_documents", COL))
|
||||
}
|
||||
|
||||
/// Fetch server count of a user
|
||||
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
|
||||
self.col::<Member>(COL)
|
||||
.count_documents(
|
||||
doc! {
|
||||
"_id.user": user_id
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|c| c as usize)
|
||||
.map_err(|_| create_database_error!("count_documents", COL))
|
||||
}
|
||||
|
||||
/// Update information for a server member
|
||||
async fn update_member(
|
||||
&self,
|
||||
id: &MemberCompositeKey,
|
||||
partial: &PartialMember,
|
||||
remove: Vec<FieldsMember>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.server": &id.server,
|
||||
"_id.user": &id.user
|
||||
},
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
delete_one,
|
||||
COL,
|
||||
doc! {
|
||||
"_id.server": &id.server,
|
||||
"_id.user": &id.user
|
||||
}
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsMember {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsMember::Avatar => "avatar",
|
||||
FieldsMember::Nickname => "nickname",
|
||||
FieldsMember::Roles => "roles",
|
||||
FieldsMember::Timeout => "timeout",
|
||||
})
|
||||
}
|
||||
}
|
||||
117
crates/core/database/src/models/server_members/ops/reference.rs
Normal file
117
crates/core/database/src/models/server_members/ops/reference.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
|
||||
|
||||
use super::AbstractServerMembers;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractServerMembers for ReferenceDb {
|
||||
/// Insert a new server member into the database
|
||||
async fn insert_member(&self, member: &Member) -> Result<()> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
if server_members.contains_key(&member.id) {
|
||||
Err(create_database_error!("insert", "member"))
|
||||
} else {
|
||||
server_members.insert(member.id.clone(), member.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a server member by their id
|
||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
server_members
|
||||
.get(&MemberCompositeKey {
|
||||
server: server_id.to_string(),
|
||||
user: user_id.to_string(),
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch all members in a server
|
||||
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
.filter(|member| member.id.server == server_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch all memberships for a user
|
||||
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
.filter(|member| member.id.user == user_id)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Fetch multiple members by their ids
|
||||
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
ids.iter()
|
||||
.map(|id| {
|
||||
server_members
|
||||
.get(&MemberCompositeKey {
|
||||
server: server_id.to_string(),
|
||||
user: id.to_string(),
|
||||
})
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fetch member count of a server
|
||||
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
.filter(|member| member.id.server == server_id)
|
||||
.count())
|
||||
}
|
||||
|
||||
/// Fetch server count of a user
|
||||
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
|
||||
let server_members = self.server_members.lock().await;
|
||||
Ok(server_members
|
||||
.values()
|
||||
.filter(|member| member.id.user == user_id)
|
||||
.count())
|
||||
}
|
||||
|
||||
/// Update information for a server member
|
||||
async fn update_member(
|
||||
&self,
|
||||
id: &MemberCompositeKey,
|
||||
partial: &PartialMember,
|
||||
remove: Vec<FieldsMember>,
|
||||
) -> Result<()> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
if let Some(member) = server_members.get_mut(id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
member.remove_field(&field);
|
||||
}
|
||||
|
||||
member.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a server member by their id
|
||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()> {
|
||||
let mut server_members = self.server_members.lock().await;
|
||||
if server_members.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/servers/mod.rs
Normal file
5
crates/core/database/src/models/servers/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
451
crates/core/database/src/models/servers/model.rs
Normal file
451
crates/core/database/src/models/servers/model.rs
Normal file
@@ -0,0 +1,451 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use revolt_permissions::OverrideField;
|
||||
use revolt_result::Result;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{Database, File};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Server
|
||||
pub struct Server {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// User id of the owner
|
||||
pub owner: String,
|
||||
|
||||
/// Name of the server
|
||||
pub name: String,
|
||||
/// Description for the server
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Channels within this server
|
||||
// ! FIXME: this may be redundant
|
||||
pub channels: Vec<String>,
|
||||
/// Categories for this server
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub categories: Option<Vec<Category>>,
|
||||
/// Configuration for sending system event messages
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub system_messages: Option<SystemMessageChannels>,
|
||||
|
||||
/// Roles for this server
|
||||
#[serde(
|
||||
default = "HashMap::<String, Role>::new",
|
||||
skip_serializing_if = "HashMap::<String, Role>::is_empty"
|
||||
)]
|
||||
pub roles: HashMap<String, Role>,
|
||||
/// Default set of server and channel permissions
|
||||
pub default_permissions: i64,
|
||||
|
||||
/// Icon attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub icon: Option<File>,
|
||||
/// Banner attachment
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub banner: Option<File>,
|
||||
|
||||
/// Bitfield of server flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
|
||||
/// Whether this server is flagged as not safe for work
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub nsfw: bool,
|
||||
/// Whether to enable analytics
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub analytics: bool,
|
||||
/// Whether this server should be publicly discoverable
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub discoverable: bool,
|
||||
},
|
||||
"PartialServer"
|
||||
);
|
||||
|
||||
auto_derived_partial!(
|
||||
/// Role
|
||||
pub struct Role {
|
||||
/// Role name
|
||||
pub name: String,
|
||||
/// Permissions available to this role
|
||||
pub permissions: OverrideField,
|
||||
/// Colour used for this role
|
||||
///
|
||||
/// This can be any valid CSS colour
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub colour: Option<String>,
|
||||
/// Whether this role should be shown separately on the member sidebar
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub hoist: bool,
|
||||
/// Ranking of this role
|
||||
#[serde(default)]
|
||||
pub rank: i64,
|
||||
},
|
||||
"PartialRole"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// Channel category
|
||||
pub struct Category {
|
||||
/// Unique ID for this category
|
||||
pub id: String,
|
||||
/// Title for this category
|
||||
pub title: String,
|
||||
/// Channels in this category
|
||||
pub channels: Vec<String>,
|
||||
}
|
||||
|
||||
/// System message channel assignments
|
||||
pub struct SystemMessageChannels {
|
||||
/// ID of channel to send user join messages in
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_joined: Option<String>,
|
||||
/// ID of channel to send user left messages in
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_left: Option<String>,
|
||||
/// ID of channel to send user kicked messages in
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_kicked: Option<String>,
|
||||
/// ID of channel to send user banned messages in
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_banned: Option<String>,
|
||||
}
|
||||
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsServer {
|
||||
Description,
|
||||
Categories,
|
||||
SystemMessages,
|
||||
Icon,
|
||||
Banner,
|
||||
}
|
||||
|
||||
/// Optional fields on server object
|
||||
pub enum FieldsRole {
|
||||
Colour,
|
||||
}
|
||||
);
|
||||
|
||||
impl Server {
|
||||
/// Create a server
|
||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
||||
db.insert_server(self).await
|
||||
}
|
||||
|
||||
/// Update server data
|
||||
pub async fn update<'a>(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialServer,
|
||||
remove: Vec<FieldsServer>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_server(&self.id, &partial, remove.clone()).await?;
|
||||
|
||||
/* // TODO: EventV1::ServerUpdate {
|
||||
id: self.id.clone(),
|
||||
data: partial,
|
||||
clear: remove,
|
||||
}
|
||||
.p(self.id.clone())
|
||||
.await; */
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a server
|
||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||
/* // TODO: EventV1::ServerDelete {
|
||||
id: self.id.clone(),
|
||||
}
|
||||
.p(self.id.clone())
|
||||
.await; */
|
||||
|
||||
db.delete_server(&self.id).await
|
||||
}
|
||||
|
||||
/// Remove a field from Server
|
||||
pub fn remove_field(&mut self, field: &FieldsServer) {
|
||||
match field {
|
||||
FieldsServer::Description => self.description = None,
|
||||
FieldsServer::Categories => self.categories = None,
|
||||
FieldsServer::SystemMessages => self.system_messages = None,
|
||||
FieldsServer::Icon => self.icon = None,
|
||||
FieldsServer::Banner => self.banner = None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set role permission on a server
|
||||
pub async fn set_role_permission(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
role_id: &str,
|
||||
permissions: OverrideField,
|
||||
) -> Result<()> {
|
||||
if let Some(role) = self.roles.get_mut(role_id) {
|
||||
role.update(
|
||||
db,
|
||||
&self.id,
|
||||
role_id,
|
||||
PartialRole {
|
||||
permissions: Some(permissions),
|
||||
..Default::default()
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/* /// Create a new member in a server
|
||||
pub async fn create_member(
|
||||
&self,
|
||||
db: &Database,
|
||||
user: User,
|
||||
channels: Option<Vec<Channel>>,
|
||||
) -> Result<Vec<Channel>> {
|
||||
if db.fetch_ban(&self.id, &user.id).await.is_ok() {
|
||||
return Err(Error::Banned);
|
||||
}
|
||||
|
||||
let member = Member {
|
||||
id: MemberCompositeKey {
|
||||
server: self.id.clone(),
|
||||
user: user.id.clone(),
|
||||
},
|
||||
joined_at: Timestamp::now_utc(),
|
||||
nickname: None,
|
||||
avatar: None,
|
||||
roles: vec![],
|
||||
timeout: None,
|
||||
};
|
||||
|
||||
db.insert_member(&member).await?;
|
||||
|
||||
let should_fetch = channels.is_none();
|
||||
let mut channels = channels.unwrap_or_default();
|
||||
|
||||
if should_fetch {
|
||||
let perm = perms(&user).server(self).member(&member);
|
||||
let existing_channels = db.fetch_channels(&self.channels).await?;
|
||||
for channel in existing_channels {
|
||||
if perm
|
||||
.clone()
|
||||
.channel(&channel)
|
||||
.has_permission(db, Permission::ViewChannel)
|
||||
.await?
|
||||
{
|
||||
channels.push(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* // TODO: EventV1::ServerMemberJoin {
|
||||
id: self.id.clone(),
|
||||
user: user.id.clone(),
|
||||
}
|
||||
.p(self.id.clone())
|
||||
.await;
|
||||
|
||||
EventV1::ServerCreate {
|
||||
id: self.id.clone(),
|
||||
server: self.clone(),
|
||||
channels: channels.clone(),
|
||||
}
|
||||
.private(user.id.clone())
|
||||
.await; */
|
||||
|
||||
if let Some(id) = self
|
||||
.system_messages
|
||||
.as_ref()
|
||||
.and_then(|x| x.user_joined.as_ref())
|
||||
{
|
||||
SystemMessage::UserJoined {
|
||||
id: user.id.clone(),
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create_no_web_push(db, id, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
/// Remove a member from a server
|
||||
pub async fn remove_member(
|
||||
&self,
|
||||
db: &Database,
|
||||
member: Member,
|
||||
intention: RemovalIntention,
|
||||
silent: bool,
|
||||
) -> Result<()> {
|
||||
db.delete_member(&member.id).await?;
|
||||
|
||||
/* // TODO: EventV1::ServerMemberLeave {
|
||||
id: self.id.to_string(),
|
||||
user: member.id.user.clone(),
|
||||
}
|
||||
.p(member.id.server)
|
||||
.await; */
|
||||
|
||||
if !silent {
|
||||
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
|
||||
RemovalIntention::Leave => x.user_left.as_ref(),
|
||||
RemovalIntention::Kick => x.user_kicked.as_ref(),
|
||||
RemovalIntention::Ban => x.user_banned.as_ref(),
|
||||
}) {
|
||||
match intention {
|
||||
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
|
||||
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
|
||||
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
|
||||
}
|
||||
.into_message(id.to_string())
|
||||
.create_no_web_push(db, id, false)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create ban
|
||||
pub async fn ban_user(
|
||||
self,
|
||||
db: &Database,
|
||||
id: MemberCompositeKey,
|
||||
reason: Option<String>,
|
||||
) -> Result<ServerBan> {
|
||||
let ban = ServerBan { id, reason };
|
||||
db.insert_ban(&ban).await?;
|
||||
Ok(ban)
|
||||
}
|
||||
|
||||
/// Ban a member from a server
|
||||
pub async fn ban_member(
|
||||
self,
|
||||
db: &Database,
|
||||
member: Member,
|
||||
reason: Option<String>,
|
||||
) -> Result<ServerBan> {
|
||||
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
|
||||
.await?;
|
||||
|
||||
self.ban_user(db, member.id, reason).await
|
||||
} */
|
||||
}
|
||||
|
||||
impl Role {
|
||||
/// Into optional struct
|
||||
pub fn into_optional(self) -> PartialRole {
|
||||
PartialRole {
|
||||
name: Some(self.name),
|
||||
permissions: Some(self.permissions),
|
||||
colour: self.colour,
|
||||
hoist: Some(self.hoist),
|
||||
rank: Some(self.rank),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a role
|
||||
pub async fn create(&self, db: &Database, server_id: &str) -> Result<String> {
|
||||
let role_id = Ulid::new().to_string();
|
||||
db.insert_role(server_id, &role_id, self).await?;
|
||||
|
||||
/* // TODO: EventV1::ServerRoleUpdate {
|
||||
id: server_id.to_string(),
|
||||
role_id: role_id.to_string(),
|
||||
data: self.clone().into_optional(),
|
||||
clear: vec![],
|
||||
}
|
||||
.p(server_id.to_string())
|
||||
.await; */
|
||||
|
||||
Ok(role_id)
|
||||
}
|
||||
|
||||
/// Update server data
|
||||
pub async fn update<'a>(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
server_id: &str,
|
||||
role_id: &str,
|
||||
partial: PartialRole,
|
||||
remove: Vec<FieldsRole>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
|
||||
db.update_role(server_id, role_id, &partial, remove.clone())
|
||||
.await?;
|
||||
|
||||
/* // TODO: EventV1::ServerRoleUpdate {
|
||||
id: server_id.to_string(),
|
||||
role_id: role_id.to_string(),
|
||||
data: partial,
|
||||
clear: vec![],
|
||||
}
|
||||
.p(server_id.to_string())
|
||||
.await; */
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove field from Role
|
||||
pub fn remove_field(&mut self, field: &FieldsRole) {
|
||||
match field {
|
||||
FieldsRole::Colour => self.colour = None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a role
|
||||
pub async fn delete(self, db: &Database, server_id: &str, role_id: &str) -> Result<()> {
|
||||
/* // TODO: EventV1::ServerRoleDelete {
|
||||
id: server_id.to_string(),
|
||||
role_id: role_id.to_string(),
|
||||
}
|
||||
.p(server_id.to_string())
|
||||
.await; */
|
||||
|
||||
db.delete_role(server_id, role_id).await
|
||||
}
|
||||
}
|
||||
|
||||
impl SystemMessageChannels {
|
||||
pub fn into_channel_ids(self) -> HashSet<String> {
|
||||
let mut ids = HashSet::new();
|
||||
|
||||
if let Some(id) = self.user_joined {
|
||||
ids.insert(id);
|
||||
}
|
||||
|
||||
if let Some(id) = self.user_left {
|
||||
ids.insert(id);
|
||||
}
|
||||
|
||||
if let Some(id) = self.user_kicked {
|
||||
ids.insert(id);
|
||||
}
|
||||
|
||||
if let Some(id) = self.user_banned {
|
||||
ids.insert(id);
|
||||
}
|
||||
|
||||
ids
|
||||
}
|
||||
}
|
||||
46
crates/core/database/src/models/servers/ops.rs
Normal file
46
crates/core/database/src/models/servers/ops.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractServers: Sync + Send {
|
||||
/// Insert a new server into database
|
||||
async fn insert_server(&self, server: &Server) -> Result<()>;
|
||||
|
||||
/// Fetch a server by its id
|
||||
async fn fetch_server(&self, id: &str) -> Result<Server>;
|
||||
|
||||
/// Fetch a servers by their ids
|
||||
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>>;
|
||||
|
||||
/// Update a server with new information
|
||||
async fn update_server(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialServer,
|
||||
remove: Vec<FieldsServer>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a server by its id
|
||||
async fn delete_server(&self, id: &str) -> Result<()>;
|
||||
|
||||
/// Insert a new role into server object
|
||||
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()>;
|
||||
|
||||
/// Update an existing role on a server
|
||||
async fn update_role(
|
||||
&self,
|
||||
server_id: &str,
|
||||
role_id: &str,
|
||||
partial: &PartialRole,
|
||||
remove: Vec<FieldsRole>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Delete a role from a server
|
||||
///
|
||||
/// Also updates channels and members.
|
||||
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()>;
|
||||
}
|
||||
183
crates/core/database/src/models/servers/ops/mongodb.rs
Normal file
183
crates/core/database/src/models/servers/ops/mongodb.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use bson::{to_document, Document};
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
||||
use crate::{IntoDocumentPath, MongoDb};
|
||||
|
||||
use super::AbstractServers;
|
||||
|
||||
static COL: &str = "servers";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractServers for MongoDb {
|
||||
/// Insert a new server into database
|
||||
async fn insert_server(&self, server: &Server) -> Result<()> {
|
||||
query!(self, insert_one, COL, &server).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a server by its id
|
||||
async fn fetch_server(&self, id: &str) -> Result<Server> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a servers by their ids
|
||||
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
|
||||
Ok(self
|
||||
.col::<Server>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", "servers"))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Update a server with new information
|
||||
async fn update_server(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialServer,
|
||||
remove: Vec<FieldsServer>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete a server by its id
|
||||
async fn delete_server(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
|
||||
/// Insert a new role into server object
|
||||
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": server_id
|
||||
},
|
||||
doc! {
|
||||
"$set": {
|
||||
"roles.".to_owned() + role_id: to_document(role)
|
||||
.map_err(|_| create_database_error!("to_document", "role"))?
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "server"))
|
||||
}
|
||||
|
||||
/// Update an existing role on a server
|
||||
async fn update_role(
|
||||
&self,
|
||||
server_id: &str,
|
||||
role_id: &str,
|
||||
partial: &PartialRole,
|
||||
remove: Vec<FieldsRole>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
server_id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
"roles.".to_owned() + role_id + "."
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Delete a role from a server
|
||||
///
|
||||
/// Also updates channels and members.
|
||||
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
|
||||
self.col::<Document>("server_members")
|
||||
.update_many(
|
||||
doc! {
|
||||
"_id.server": server_id
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"roles": &role_id
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_many", "server_members"))?;
|
||||
|
||||
self.col::<Document>("channels")
|
||||
.update_one(
|
||||
doc! {
|
||||
"server": server_id
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
"role_permissions.".to_owned() + role_id: 1_i32
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("update_one", "channels"))?;
|
||||
|
||||
self.col::<Document>("servers")
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": server_id
|
||||
},
|
||||
doc! {
|
||||
"$unset": {
|
||||
"roles.".to_owned() + role_id: 1_i32
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "servers"))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsServer {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsServer::Banner => "banner",
|
||||
FieldsServer::Categories => "categories",
|
||||
FieldsServer::Description => "description",
|
||||
FieldsServer::Icon => "icon",
|
||||
FieldsServer::SystemMessages => "system_messages",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsRole {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsRole::Colour => "colour",
|
||||
})
|
||||
}
|
||||
}
|
||||
126
crates/core/database/src/models/servers/ops/reference.rs
Normal file
126
crates/core/database/src/models/servers/ops/reference.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server};
|
||||
|
||||
use super::AbstractServers;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractServers for ReferenceDb {
|
||||
/// Insert a new server into database
|
||||
async fn insert_server(&self, server: &Server) -> Result<()> {
|
||||
let mut servers = self.servers.lock().await;
|
||||
if servers.contains_key(&server.id) {
|
||||
Err(create_database_error!("insert", "server"))
|
||||
} else {
|
||||
servers.insert(server.id.to_string(), server.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a server by its id
|
||||
async fn fetch_server(&self, id: &str) -> Result<Server> {
|
||||
let servers = self.servers.lock().await;
|
||||
servers
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a servers by their ids
|
||||
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
|
||||
let servers = self.servers.lock().await;
|
||||
ids.iter()
|
||||
.map(|id| {
|
||||
servers
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update a server with new information
|
||||
async fn update_server(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialServer,
|
||||
remove: Vec<FieldsServer>,
|
||||
) -> Result<()> {
|
||||
let mut servers = self.servers.lock().await;
|
||||
if let Some(server) = servers.get_mut(id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
server.remove_field(&field);
|
||||
}
|
||||
|
||||
server.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a server by its id
|
||||
async fn delete_server(&self, id: &str) -> Result<()> {
|
||||
let mut servers = self.servers.lock().await;
|
||||
if servers.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a new role into server object
|
||||
async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()> {
|
||||
let mut servers = self.servers.lock().await;
|
||||
if let Some(server) = servers.get_mut(server_id) {
|
||||
server.roles.insert(role_id.to_string(), role.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing role on a server
|
||||
async fn update_role(
|
||||
&self,
|
||||
server_id: &str,
|
||||
role_id: &str,
|
||||
partial: &PartialRole,
|
||||
remove: Vec<FieldsRole>,
|
||||
) -> Result<()> {
|
||||
let mut servers = self.servers.lock().await;
|
||||
if let Some(server) = servers.get_mut(server_id) {
|
||||
if let Some(role) = server.roles.get_mut(role_id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
role.remove_field(&field);
|
||||
}
|
||||
|
||||
role.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a role from a server
|
||||
///
|
||||
/// Also updates channels and members.
|
||||
async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()> {
|
||||
let mut servers = self.servers.lock().await;
|
||||
if let Some(server) = servers.get_mut(server_id) {
|
||||
if server.roles.remove(role_id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
5
crates/core/database/src/models/user_settings/mod.rs
Normal file
5
crates/core/database/src/models/user_settings/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
28
crates/core/database/src/models/user_settings/model.rs
Normal file
28
crates/core/database/src/models/user_settings/model.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::Database;
|
||||
|
||||
use revolt_result::Result;
|
||||
|
||||
pub type UserSettings = HashMap<String, (i64, String)>;
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserSettingsImpl {
|
||||
async fn set(self, db: &Database, user: &str) -> Result<()>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserSettingsImpl for UserSettings {
|
||||
async fn set(self, db: &Database, user: &str) -> Result<()> {
|
||||
db.set_user_settings(user, &self).await?;
|
||||
|
||||
/* // TODO: EventV1::UserSettingsUpdate {
|
||||
id: user.to_string(),
|
||||
update: self,
|
||||
}
|
||||
.private(user.to_string())
|
||||
.await; */
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
18
crates/core/database/src/models/user_settings/ops.rs
Normal file
18
crates/core/database/src/models/user_settings/ops.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::UserSettings;
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractUserSettings: Sync + Send {
|
||||
/// Fetch a subset of user settings
|
||||
async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result<UserSettings>;
|
||||
|
||||
/// Update a subset of user settings
|
||||
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()>;
|
||||
|
||||
/// Delete all user settings
|
||||
async fn delete_user_settings(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
67
crates/core/database/src/models/user_settings/ops/mongodb.rs
Normal file
67
crates/core/database/src/models/user_settings/ops/mongodb.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use ::mongodb::options::FindOneOptions;
|
||||
use bson::to_bson;
|
||||
use bson::Document;
|
||||
use mongodb::options::UpdateOptions;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::MongoDb;
|
||||
use crate::UserSettings;
|
||||
|
||||
use super::AbstractUserSettings;
|
||||
|
||||
static COL: &str = "user_settings";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractUserSettings for MongoDb {
|
||||
/// Fetch a subset of user settings
|
||||
async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result<UserSettings> {
|
||||
let mut projection = doc! {
|
||||
"_id": 0,
|
||||
};
|
||||
|
||||
for key in filter {
|
||||
projection.insert(key, 1);
|
||||
}
|
||||
|
||||
Ok(query!(
|
||||
self,
|
||||
find_one_with_options,
|
||||
COL,
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
FindOneOptions::builder().projection(projection).build()
|
||||
)?
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Update a subset of user settings
|
||||
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()> {
|
||||
let mut set = doc! {};
|
||||
for (key, data) in settings {
|
||||
set.insert(
|
||||
key,
|
||||
vec![to_bson(&data.0).unwrap(), to_bson(&data.1).unwrap()],
|
||||
);
|
||||
}
|
||||
|
||||
self.col::<Document>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": id
|
||||
},
|
||||
doc! {
|
||||
"$set": set
|
||||
},
|
||||
UpdateOptions::builder().upsert(true).build(),
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "user_settings"))
|
||||
}
|
||||
|
||||
/// Delete all user settings
|
||||
async fn delete_user_settings(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::UserSettings;
|
||||
|
||||
use super::AbstractUserSettings;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractUserSettings for ReferenceDb {
|
||||
/// Fetch a subset of user settings
|
||||
async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result<UserSettings> {
|
||||
let user_settings = self.user_settings.lock().await;
|
||||
user_settings
|
||||
.get(id)
|
||||
.cloned()
|
||||
.map(|settings| {
|
||||
settings
|
||||
.into_iter()
|
||||
.filter(|(key, _)| filter.contains(key))
|
||||
.collect()
|
||||
})
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Update a subset of user settings
|
||||
async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()> {
|
||||
let mut user_settings = self.user_settings.lock().await;
|
||||
if let Some(settings) = user_settings.get_mut(id) {
|
||||
settings.extend(settings.clone());
|
||||
} else {
|
||||
user_settings.insert(id.to_string(), settings.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all user settings
|
||||
async fn delete_user_settings(&self, id: &str) -> Result<()> {
|
||||
let mut user_settings = self.user_settings.lock().await;
|
||||
if user_settings.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
9
crates/core/database/src/models/users/mod.rs
Normal file
9
crates/core/database/src/models/users/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod model;
|
||||
mod ops;
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
mod rocket;
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
pub use self::rocket::*;
|
||||
pub use model::*;
|
||||
pub use ops::*;
|
||||
197
crates/core/database/src/models/users/model.rs
Normal file
197
crates/core/database/src/models/users/model.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use crate::{Database, File};
|
||||
|
||||
use revolt_result::{Error, ErrorType, Result};
|
||||
|
||||
auto_derived_partial!(
|
||||
/// # User
|
||||
pub struct User {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Username
|
||||
pub username: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Avatar attachment
|
||||
pub avatar: Option<File>,
|
||||
/// Relationships with other users
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub relations: Option<Vec<Relationship>>,
|
||||
|
||||
/// Bitfield of user badges
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub badges: Option<i32>,
|
||||
/// User's current status
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<UserStatus>,
|
||||
/// User's profile page
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<UserProfile>,
|
||||
|
||||
/// Enum of user flags
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub flags: Option<i32>,
|
||||
/// Whether this user is privileged
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub privileged: bool,
|
||||
/// Bot information
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bot: Option<BotInformation>,
|
||||
},
|
||||
"PartialUser"
|
||||
);
|
||||
|
||||
auto_derived!(
|
||||
/// User's relationship with another user (or themselves)
|
||||
pub enum RelationshipStatus {
|
||||
None,
|
||||
User,
|
||||
Friend,
|
||||
Outgoing,
|
||||
Incoming,
|
||||
Blocked,
|
||||
BlockedOther,
|
||||
}
|
||||
|
||||
/// Relationship entry indicating current status with other user
|
||||
pub struct Relationship {
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
pub status: RelationshipStatus,
|
||||
}
|
||||
|
||||
/// Presence status
|
||||
pub enum Presence {
|
||||
/// User is online
|
||||
Online,
|
||||
/// User is not currently available
|
||||
Idle,
|
||||
/// User is focusing / will only receive mentions
|
||||
Focus,
|
||||
/// User is busy / will not receive any notifications
|
||||
Busy,
|
||||
/// User appears to be offline
|
||||
Invisible,
|
||||
}
|
||||
|
||||
/// User's active status
|
||||
pub struct UserStatus {
|
||||
/// Custom status text
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub text: String,
|
||||
/// Current presence option
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub presence: Option<Presence>,
|
||||
}
|
||||
|
||||
/// User's profile
|
||||
pub struct UserProfile {
|
||||
/// Text content on user's profile
|
||||
#[serde(skip_serializing_if = "String::is_empty", default)]
|
||||
pub content: String,
|
||||
/// Background visible on user's profile
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<File>,
|
||||
}
|
||||
|
||||
/// Bot information for if the user is a bot
|
||||
pub struct BotInformation {
|
||||
/// Id of the owner of this bot
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
/// Optional fields on user object
|
||||
pub enum FieldsUser {
|
||||
Avatar,
|
||||
StatusText,
|
||||
StatusPresence,
|
||||
ProfileContent,
|
||||
ProfileBackground,
|
||||
}
|
||||
);
|
||||
|
||||
impl User {
|
||||
/// Check whether a username is already in use by another user
|
||||
#[allow(dead_code)]
|
||||
async fn is_username_taken(db: &Database, username: &str) -> Result<bool> {
|
||||
match db.fetch_user_by_username(username).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(Error {
|
||||
error_type: ErrorType::NotFound,
|
||||
..
|
||||
}) => Ok(false),
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update user data
|
||||
pub async fn update<'a>(
|
||||
&mut self,
|
||||
db: &Database,
|
||||
partial: PartialUser,
|
||||
remove: Vec<FieldsUser>,
|
||||
) -> Result<()> {
|
||||
for field in &remove {
|
||||
self.remove_field(field);
|
||||
}
|
||||
|
||||
self.apply_options(partial.clone());
|
||||
db.update_user(&self.id, &partial, remove.clone()).await?;
|
||||
|
||||
/* // TODO: EventV1::UserUpdate {
|
||||
id: self.id.clone(),
|
||||
data: partial,
|
||||
clear: remove,
|
||||
}
|
||||
.p_user(self.id.clone(), db)
|
||||
.await; */
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a field from User object
|
||||
pub fn remove_field(&mut self, field: &FieldsUser) {
|
||||
match field {
|
||||
FieldsUser::Avatar => self.avatar = None,
|
||||
FieldsUser::StatusText => {
|
||||
if let Some(x) = self.status.as_mut() {
|
||||
x.text = String::new();
|
||||
}
|
||||
}
|
||||
FieldsUser::StatusPresence => {
|
||||
if let Some(x) = self.status.as_mut() {
|
||||
x.presence = None;
|
||||
}
|
||||
}
|
||||
FieldsUser::ProfileContent => {
|
||||
if let Some(x) = self.profile.as_mut() {
|
||||
x.content = String::new();
|
||||
}
|
||||
}
|
||||
FieldsUser::ProfileBackground => {
|
||||
if let Some(x) = self.profile.as_mut() {
|
||||
x.background = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark as deleted
|
||||
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
|
||||
self.update(
|
||||
db,
|
||||
PartialUser {
|
||||
username: Some(format!("Deleted User {}", self.id)),
|
||||
flags: Some(2),
|
||||
..Default::default()
|
||||
},
|
||||
vec![
|
||||
FieldsUser::Avatar,
|
||||
FieldsUser::StatusText,
|
||||
FieldsUser::StatusPresence,
|
||||
FieldsUser::ProfileContent,
|
||||
FieldsUser::ProfileBackground,
|
||||
],
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
57
crates/core/database/src/models/users/ops.rs
Normal file
57
crates/core/database/src/models/users/ops.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
|
||||
mod mongodb;
|
||||
mod reference;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AbstractUsers: Sync + Send {
|
||||
/// Insert a new user into the database
|
||||
async fn insert_user(&self, user: &User) -> Result<()>;
|
||||
|
||||
/// Fetch a user from the database
|
||||
async fn fetch_user(&self, id: &str) -> Result<User>;
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User>;
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User>;
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>>;
|
||||
|
||||
/// Fetch ids of users that both users are friends with
|
||||
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
|
||||
|
||||
/// Fetch ids of channels that both users are in
|
||||
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
|
||||
|
||||
/// Fetch ids of servers that both users share
|
||||
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>>;
|
||||
|
||||
/// Update a user by their id given some data
|
||||
async fn update_user(
|
||||
&self,
|
||||
id: &str,
|
||||
user: &PartialUser,
|
||||
remove: Vec<FieldsUser>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Set relationship with another user
|
||||
///
|
||||
/// This should use pull_relationship if relationship is None.
|
||||
async fn set_relationship(
|
||||
&self,
|
||||
user_id: &str,
|
||||
target_id: &str,
|
||||
relationship: &RelationshipStatus,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Remove relationship with another user
|
||||
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()>;
|
||||
|
||||
/// Delete a user by their id
|
||||
async fn delete_user(&self, id: &str) -> Result<()>;
|
||||
}
|
||||
300
crates/core/database/src/models/users/ops/mongodb.rs
Normal file
300
crates/core/database/src/models/users/ops/mongodb.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use ::mongodb::options::{Collation, CollationStrength, FindOneOptions, FindOptions};
|
||||
use authifier::models::Session;
|
||||
use futures::StreamExt;
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::DocumentId;
|
||||
use crate::IntoDocumentPath;
|
||||
use crate::MongoDb;
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
|
||||
use super::AbstractUsers;
|
||||
|
||||
static COL: &str = "users";
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractUsers for MongoDb {
|
||||
/// Insert a new user into the database
|
||||
async fn insert_user(&self, user: &User) -> Result<()> {
|
||||
query!(self, insert_one, COL, &user).map(|_| ())
|
||||
}
|
||||
|
||||
/// Fetch a user from the database
|
||||
async fn fetch_user(&self, id: &str) -> Result<User> {
|
||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
|
||||
query!(
|
||||
self,
|
||||
find_one_with_options,
|
||||
COL,
|
||||
doc! {
|
||||
"username": username
|
||||
},
|
||||
FindOneOptions::builder()
|
||||
.collation(
|
||||
Collation::builder()
|
||||
.locale("en")
|
||||
.strength(CollationStrength::Secondary)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
)?
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, token: &str) -> Result<User> {
|
||||
let session = self
|
||||
.col::<Session>("sessions")
|
||||
.find_one(
|
||||
doc! {
|
||||
"token": token
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find_one", "sessions"))?
|
||||
.ok_or_else(|| create_error!(InvalidSession))?;
|
||||
|
||||
self.fetch_user(&session.id).await
|
||||
}
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
|
||||
Ok(self
|
||||
.col::<User>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"_id": {
|
||||
"$in": ids
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async {
|
||||
if cfg!(debug_assertions) {
|
||||
Some(s.unwrap())
|
||||
} else {
|
||||
s.ok()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch ids of users that both users are friends with
|
||||
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
|
||||
Ok(self
|
||||
.col::<DocumentId>(COL)
|
||||
.find(
|
||||
doc! {
|
||||
"$and": [
|
||||
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
|
||||
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
|
||||
]
|
||||
},
|
||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", COL))?
|
||||
.filter_map(|s| async { s.ok() })
|
||||
.map(|user| user.id)
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch ids of channels that both users are in
|
||||
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
|
||||
Ok(self
|
||||
.col::<DocumentId>("channels")
|
||||
.find(
|
||||
doc! {
|
||||
"channel_type": {
|
||||
"$in": ["Group", "DirectMessage"]
|
||||
},
|
||||
"recipients": {
|
||||
"$all": [ user_a, user_b ]
|
||||
}
|
||||
},
|
||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("find", "channels"))?
|
||||
.filter_map(|s| async { s.ok() })
|
||||
.map(|user| user.id)
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Fetch ids of servers that both users share
|
||||
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
|
||||
Ok(self
|
||||
.col::<DocumentId>("server_members")
|
||||
.aggregate(
|
||||
vec![
|
||||
doc! {
|
||||
"$match": {
|
||||
"_id.user": user_a
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$lookup": {
|
||||
"from": "server_members",
|
||||
"as": "members",
|
||||
"let": {
|
||||
"server": "$_id.server"
|
||||
},
|
||||
"pipeline": [
|
||||
{
|
||||
"$match": {
|
||||
"$expr": {
|
||||
"$and": [
|
||||
{ "$eq": [ "$_id.user", user_b ] },
|
||||
{ "$eq": [ "$_id.server", "$$server" ] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$match": {
|
||||
"members": {
|
||||
"$size": 1_i32
|
||||
}
|
||||
}
|
||||
},
|
||||
doc! {
|
||||
"$project": {
|
||||
"_id": "$_id.server"
|
||||
}
|
||||
},
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map_err(|_| create_database_error!("aggregate", "server_members"))?
|
||||
.filter_map(|s| async { s.ok() })
|
||||
.filter_map(|doc| async move { doc.get_str("_id").map(|id| id.to_string()).ok() })
|
||||
.collect()
|
||||
.await)
|
||||
}
|
||||
|
||||
/// Update a user by their id given some data
|
||||
async fn update_user(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialUser,
|
||||
remove: Vec<FieldsUser>,
|
||||
) -> Result<()> {
|
||||
query!(
|
||||
self,
|
||||
update_one_by_id,
|
||||
COL,
|
||||
id,
|
||||
partial,
|
||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||
None
|
||||
)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
/// Set relationship with another user
|
||||
///
|
||||
/// This should use pull_relationship if relationship is None.
|
||||
async fn set_relationship(
|
||||
&self,
|
||||
user_id: &str,
|
||||
target_id: &str,
|
||||
relationship: &RelationshipStatus,
|
||||
) -> Result<()> {
|
||||
if let RelationshipStatus::None = relationship {
|
||||
return self.pull_relationship(user_id, target_id).await;
|
||||
}
|
||||
|
||||
self.col::<User>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": user_id
|
||||
},
|
||||
vec![doc! {
|
||||
"$set": {
|
||||
"relations": {
|
||||
"$concatArrays": [
|
||||
{
|
||||
"$ifNull": [
|
||||
{
|
||||
"$filter": {
|
||||
"input": "$relations",
|
||||
"cond": {
|
||||
"$ne": [
|
||||
"$$this._id",
|
||||
target_id
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
[]
|
||||
]
|
||||
},
|
||||
[
|
||||
{
|
||||
"_id": target_id,
|
||||
"status": format!("{relationship:?}")
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", "user"))
|
||||
}
|
||||
|
||||
/// Remove relationship with another user
|
||||
async fn pull_relationship(&self, user_id: &str, target_id: &str) -> Result<()> {
|
||||
self.col::<User>(COL)
|
||||
.update_one(
|
||||
doc! {
|
||||
"_id": user_id
|
||||
},
|
||||
doc! {
|
||||
"$pull": {
|
||||
"relations": {
|
||||
"_id": target_id
|
||||
}
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|_| create_database_error!("update_one", COL))
|
||||
}
|
||||
|
||||
/// Delete a user by their id
|
||||
async fn delete_user(&self, id: &str) -> Result<()> {
|
||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDocumentPath for FieldsUser {
|
||||
fn as_path(&self) -> Option<&'static str> {
|
||||
Some(match self {
|
||||
FieldsUser::Avatar => "avatar",
|
||||
FieldsUser::ProfileBackground => "profile.background",
|
||||
FieldsUser::ProfileContent => "profile.content",
|
||||
FieldsUser::StatusPresence => "status.presence",
|
||||
FieldsUser::StatusText => "status.text",
|
||||
})
|
||||
}
|
||||
}
|
||||
121
crates/core/database/src/models/users/ops/reference.rs
Normal file
121
crates/core/database/src/models/users/ops/reference.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use revolt_result::Result;
|
||||
|
||||
use crate::ReferenceDb;
|
||||
use crate::{FieldsUser, PartialUser, RelationshipStatus, User};
|
||||
|
||||
use super::AbstractUsers;
|
||||
|
||||
#[async_trait]
|
||||
impl AbstractUsers for ReferenceDb {
|
||||
/// Insert a new user into the database
|
||||
async fn insert_user(&self, user: &User) -> Result<()> {
|
||||
let mut users = self.users.lock().await;
|
||||
if users.contains_key(&user.id) {
|
||||
Err(create_database_error!("insert", "user"))
|
||||
} else {
|
||||
users.insert(user.id.to_string(), user.clone());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a user from the database
|
||||
async fn fetch_user(&self, id: &str) -> Result<User> {
|
||||
let users = self.users.lock().await;
|
||||
users
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their username
|
||||
async fn fetch_user_by_username(&self, username: &str) -> Result<User> {
|
||||
let users = self.users.lock().await;
|
||||
let lowercase = username.to_lowercase();
|
||||
users
|
||||
.values()
|
||||
.find(|user| user.username.to_lowercase() == lowercase)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
}
|
||||
|
||||
/// Fetch a user from the database by their session token
|
||||
async fn fetch_user_by_token(&self, _token: &str) -> Result<User> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Fetch multiple users by their ids
|
||||
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
|
||||
let users = self.users.lock().await;
|
||||
ids.iter()
|
||||
.map(|id| {
|
||||
users
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| create_error!(NotFound))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Fetch ids of users that both users are friends with
|
||||
async fn fetch_mutual_user_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Fetch ids of channels that both users are in
|
||||
async fn fetch_mutual_channel_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Fetch ids of servers that both users share
|
||||
async fn fetch_mutual_server_ids(&self, _user_a: &str, _user_b: &str) -> Result<Vec<String>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Update a user by their id given some data
|
||||
async fn update_user(
|
||||
&self,
|
||||
id: &str,
|
||||
partial: &PartialUser,
|
||||
remove: Vec<FieldsUser>,
|
||||
) -> Result<()> {
|
||||
let mut users = self.users.lock().await;
|
||||
if let Some(user) = users.get_mut(id) {
|
||||
for field in remove {
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
user.remove_field(&field);
|
||||
}
|
||||
|
||||
user.apply_options(partial.clone());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
/// Set relationship with another user
|
||||
///
|
||||
/// This should use pull_relationship if relationship is None.
|
||||
async fn set_relationship(
|
||||
&self,
|
||||
_user_id: &str,
|
||||
_target_id: &str,
|
||||
_relationship: &RelationshipStatus,
|
||||
) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Remove relationship with another user
|
||||
async fn pull_relationship(&self, _user_id: &str, _target_id: &str) -> Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Delete a user by their id
|
||||
async fn delete_user(&self, id: &str) -> Result<()> {
|
||||
let mut users = self.users.lock().await;
|
||||
if users.remove(id).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(create_error!(NotFound))
|
||||
}
|
||||
}
|
||||
}
|
||||
44
crates/core/database/src/models/users/rocket.rs
Normal file
44
crates/core/database/src/models/users/rocket.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use authifier::models::Session;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{self, FromRequest, Outcome, Request};
|
||||
|
||||
use crate::{Database, User};
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for User {
|
||||
type Error = authifier::Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let user: &Option<User> = request
|
||||
.local_cache_async(async {
|
||||
let db = request.rocket().state::<Database>().expect("`Database`");
|
||||
|
||||
let _header_bot_token = request
|
||||
.headers()
|
||||
.get("x-bot-token")
|
||||
.next()
|
||||
.map(|x| x.to_string());
|
||||
|
||||
/* if let Some(bot_token) = header_bot_token {
|
||||
if let Ok(user) = User::from_token(db, &bot_token, UserHint::Bot).await {
|
||||
return Some(user);
|
||||
}
|
||||
} else */
|
||||
if let Outcome::Success(session) = request.guard::<Session>().await {
|
||||
// This uses a guard so can't really easily be refactored into from_token at this stage.
|
||||
if let Ok(user) = db.fetch_user(&session.user_id).await {
|
||||
return Some(user);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(user) = user {
|
||||
Outcome::Success(user.clone())
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, authifier::Error::InvalidSession))
|
||||
}
|
||||
}
|
||||
}
|
||||
2
crates/core/database/src/util/mod.rs
Normal file
2
crates/core/database/src/util/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod permissions;
|
||||
pub mod reference;
|
||||
206
crates/core/database/src/util/permissions.rs
Normal file
206
crates/core/database/src/util/permissions.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use revolt_permissions::{
|
||||
calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus,
|
||||
};
|
||||
|
||||
use crate::{Database, User};
|
||||
|
||||
/// Permissions calculator
|
||||
pub struct PermissionCalculator<'a> {
|
||||
#[allow(dead_code)]
|
||||
database: &'a Database,
|
||||
|
||||
perspective: &'a User,
|
||||
user: Option<Cow<'a, User>>,
|
||||
// pub channel: Cow<'a, Channel>,
|
||||
// pub server: Cow<'a, Server>,
|
||||
// pub member: Cow<'a, Member>,
|
||||
|
||||
// flag_known_relationship: Option<&'a RelationshipStatus>,
|
||||
cached_user_permission: Option<u32>,
|
||||
cached_permission: Option<u64>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PermissionQuery for PermissionCalculator<'_> {
|
||||
// * For calculating user permission
|
||||
|
||||
/// Is our perspective user privileged?
|
||||
async fn are_we_privileged(&mut self) -> bool {
|
||||
self.perspective.privileged
|
||||
}
|
||||
|
||||
/// Is our perspective user a bot?
|
||||
async fn are_we_a_bot(&mut self) -> bool {
|
||||
self.perspective.bot.is_some()
|
||||
}
|
||||
|
||||
/// Is our perspective user and the currently selected user the same?
|
||||
async fn are_the_users_same(&mut self) -> bool {
|
||||
if let Some(other_user) = &self.user {
|
||||
self.perspective.id == other_user.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the relationship with have with the currently selected user
|
||||
async fn user_relationship(&mut self) -> RelationshipStatus {
|
||||
if let Some(other_user) = &self.user {
|
||||
if let Some(relations) = &self.perspective.relations {
|
||||
for entry in relations {
|
||||
if entry.id == other_user.id {
|
||||
return match entry.status {
|
||||
crate::RelationshipStatus::None => RelationshipStatus::None,
|
||||
crate::RelationshipStatus::User => RelationshipStatus::User,
|
||||
crate::RelationshipStatus::Friend => RelationshipStatus::Friend,
|
||||
crate::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
|
||||
crate::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
|
||||
crate::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
|
||||
crate::RelationshipStatus::BlockedOther => {
|
||||
RelationshipStatus::BlockedOther
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RelationshipStatus::None
|
||||
}
|
||||
|
||||
/// Whether the currently selected user is a bot
|
||||
async fn user_is_bot(&mut self) -> bool {
|
||||
if let Some(other_user) = &self.user {
|
||||
other_user.bot.is_some()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Do we have a mutual connection with the currently selected user?
|
||||
async fn have_mutual_connection(&mut self) -> bool {
|
||||
// TODO: User::has_mutual_connection
|
||||
false
|
||||
}
|
||||
|
||||
// * For calculating server permission
|
||||
|
||||
/// Is our perspective user the server's owner?
|
||||
async fn are_we_server_owner(&mut self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Is our perspective user a member of the server?
|
||||
async fn are_we_a_member(&mut self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get default server permission
|
||||
async fn get_default_server_permissions(&mut self) -> u64 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get the ordered role overrides (from lowest to highest) for this member in this server
|
||||
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Is our perspective user timed out on this server?
|
||||
async fn are_we_timed_out(&mut self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// * For calculating channel permission
|
||||
|
||||
/// Get the type of the channel
|
||||
async fn get_channel_type(&mut self) -> ChannelType {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get the default channel permissions
|
||||
/// Group channel defaults should be mapped to an allow-only override
|
||||
async fn get_default_channel_permissions(&mut self) -> Override {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Get the ordered role overrides (from lowest to highest) for this member in this channel
|
||||
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Do we own this group or saved messages channel if it is one of those?
|
||||
async fn do_we_own_the_channel(&mut self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Are we a recipient of this channel?
|
||||
async fn are_we_part_of_the_channel(&mut self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Set the current user as the recipient of this channel
|
||||
/// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths)
|
||||
async fn set_recipient_as_user(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Set the current server as the server owning this channel
|
||||
/// (this will only ever be called for server channels, use unimplemented!() for other code paths)
|
||||
async fn set_server_from_channel(&mut self) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PermissionCalculator<'a> {
|
||||
/// Create a new permission calculator
|
||||
pub fn new(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> {
|
||||
PermissionCalculator {
|
||||
database,
|
||||
perspective,
|
||||
user: None,
|
||||
|
||||
cached_user_permission: None,
|
||||
cached_permission: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the user permission value
|
||||
pub async fn calc_user(mut self) -> PermissionCalculator<'a> {
|
||||
if self.cached_user_permission.is_some() {
|
||||
return self;
|
||||
}
|
||||
|
||||
if self.user.is_none() {
|
||||
panic!("Expected `PermissionCalculator.user to exist.");
|
||||
}
|
||||
|
||||
PermissionCalculator {
|
||||
cached_user_permission: Some(calculate_user_permissions(&mut self).await),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the permission value
|
||||
pub async fn calc(self) -> PermissionCalculator<'a> {
|
||||
if self.cached_permission.is_some() {
|
||||
return self;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Use user
|
||||
pub fn user(self, user: Cow<'a, User>) -> PermissionCalculator {
|
||||
PermissionCalculator {
|
||||
user: Some(user),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Short-hand for creating a permission calculator
|
||||
pub fn perms<'a>(database: &'a Database, perspective: &'a User) -> PermissionCalculator<'a> {
|
||||
PermissionCalculator::new(database, perspective)
|
||||
}
|
||||
52
crates/core/database/src/util/reference.rs
Normal file
52
crates/core/database/src/util/reference.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
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, Database};
|
||||
|
||||
/// Reference to some object in the database
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Reference {
|
||||
/// Id of object
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl Reference {
|
||||
/// Create a Ref from an unchecked string
|
||||
pub fn from_unchecked(id: String) -> Reference {
|
||||
Reference { id }
|
||||
}
|
||||
|
||||
/// Fetch bot from Ref
|
||||
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
|
||||
db.fetch_bot(&self.id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
impl<'r> FromParam<'r> for Reference {
|
||||
type Error = &'r str;
|
||||
|
||||
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
|
||||
Ok(Reference::from_unchecked(param.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rocket-impl")]
|
||||
impl JsonSchema for Reference {
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
29
crates/core/models/Cargo.toml
Normal file
29
crates/core/models/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "revolt-models"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
description = "Revolt Backend: API Models"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
serde = [ "dep:serde" ]
|
||||
schemas = [ "dep:schemars" ]
|
||||
from_database = [ "revolt-database", "revolt-presence" ]
|
||||
|
||||
redis-is-patched = [ "revolt-presence/redis-is-patched" ]
|
||||
|
||||
default = [ "serde", "from_database" ]
|
||||
|
||||
[dependencies]
|
||||
# Repo
|
||||
revolt-database = { version = "0.0.2", path = "../database", optional = true }
|
||||
revolt-presence = { version = "0.0.2", path = "../presence", optional = true }
|
||||
|
||||
# Serialisation
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
# Spec Generation
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
30
crates/core/models/src/lib.rs
Normal file
30
crates/core/models/src/lib.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
#[cfg(feature = "serde")]
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
#[cfg(feature = "schemas")]
|
||||
#[macro_use]
|
||||
extern crate schemars;
|
||||
|
||||
macro_rules! auto_derived {
|
||||
( $( $item:item )+ ) => {
|
||||
$(
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
$item
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
pub mod v0;
|
||||
|
||||
/// Utility function to check if a boolean value is false
|
||||
pub fn if_false(t: &bool) -> bool {
|
||||
!t
|
||||
}
|
||||
|
||||
/// Utility function to check if an u32 is zero
|
||||
pub fn if_zero_u32(t: &u32) -> bool {
|
||||
t == &0
|
||||
}
|
||||
30
crates/core/models/src/v0/account_strikes.rs
Normal file
30
crates/core/models/src/v0/account_strikes.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
auto_derived!(
|
||||
/// Account Strike
|
||||
pub struct AccountStrike {
|
||||
/// Strike Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// User Id of reported user
|
||||
pub user_id: String,
|
||||
|
||||
/// Attached reason
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
/// # Strike Data
|
||||
pub struct DataEditAccountStrike {
|
||||
/// New attached reason
|
||||
pub reason: String,
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::AccountStrike> for AccountStrike {
|
||||
fn from(value: revolt_database::AccountStrike) -> Self {
|
||||
AccountStrike {
|
||||
id: value.id,
|
||||
user_id: value.user_id,
|
||||
reason: value.reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
124
crates/core/models/src/v0/bots.rs
Normal file
124
crates/core/models/src/v0/bots.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use super::User;
|
||||
|
||||
auto_derived!(
|
||||
/// Bot
|
||||
pub struct Bot {
|
||||
/// Bot Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
|
||||
/// User Id of the bot owner
|
||||
#[serde(rename = "owner")]
|
||||
pub owner_id: String,
|
||||
/// Token used to authenticate requests for this bot
|
||||
pub token: String,
|
||||
/// Whether the bot is public
|
||||
/// (may be invited by anyone)
|
||||
pub public: bool,
|
||||
|
||||
/// Whether to enable analytics
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "crate::if_false", default)
|
||||
)]
|
||||
pub analytics: bool,
|
||||
/// Whether this bot should be publicly discoverable
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "crate::if_false", default)
|
||||
)]
|
||||
pub discoverable: bool,
|
||||
/// Reserved; URL for handling interactions
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "String::is_empty", default)
|
||||
)]
|
||||
pub interactions_url: String,
|
||||
/// URL for terms of service
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "String::is_empty", default)
|
||||
)]
|
||||
pub terms_of_service_url: String,
|
||||
/// URL for privacy policy
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "String::is_empty", default)
|
||||
)]
|
||||
pub privacy_policy_url: String,
|
||||
|
||||
/// Enum of bot flags
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(skip_serializing_if = "crate::if_zero_u32", default)
|
||||
)]
|
||||
pub flags: u32,
|
||||
}
|
||||
|
||||
/// Flags that may be attributed to a bot
|
||||
#[repr(u32)]
|
||||
pub enum BotFlags {
|
||||
Verified = 1,
|
||||
Official = 2,
|
||||
}
|
||||
|
||||
/// Public Bot
|
||||
pub struct PublicBot {
|
||||
/// Bot Id
|
||||
#[serde(rename = "_id")]
|
||||
id: String,
|
||||
|
||||
/// Bot Username
|
||||
username: String,
|
||||
/// Profile Avatar
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
avatar: String,
|
||||
/// Profile Description
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
description: String,
|
||||
}
|
||||
|
||||
/// Bot Response
|
||||
pub struct FetchBotResponse {
|
||||
/// Bot object
|
||||
pub bot: Bot,
|
||||
/// User object
|
||||
pub user: User,
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl PublicBot {
|
||||
pub fn from(bot: revolt_database::Bot, user: revolt_database::User) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
assert_eq!(bot.id, user.id);
|
||||
|
||||
PublicBot {
|
||||
id: bot.id,
|
||||
username: user.username,
|
||||
avatar: user.avatar.map(|x| x.id).unwrap_or_default(),
|
||||
description: user
|
||||
.profile
|
||||
.map(|profile| profile.content)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::Bot> for Bot {
|
||||
fn from(value: revolt_database::Bot) -> Self {
|
||||
Bot {
|
||||
id: value.id,
|
||||
owner_id: value.owner,
|
||||
token: value.token,
|
||||
public: value.public,
|
||||
analytics: value.analytics,
|
||||
discoverable: value.discoverable,
|
||||
interactions_url: value.interactions_url,
|
||||
terms_of_service_url: value.terms_of_service_url,
|
||||
privacy_policy_url: value.privacy_policy_url,
|
||||
flags: value.flags.unwrap_or_default() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
95
crates/core/models/src/v0/files.rs
Normal file
95
crates/core/models/src/v0/files.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
auto_derived!(
|
||||
/// File
|
||||
pub struct File {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Tag / bucket this file was uploaded to
|
||||
pub tag: String,
|
||||
/// Original filename
|
||||
pub filename: String,
|
||||
/// Parsed metadata of this file
|
||||
pub metadata: Metadata,
|
||||
/// Raw content type of this file
|
||||
pub content_type: String,
|
||||
/// Size of this file (in bytes)
|
||||
pub size: isize,
|
||||
|
||||
/// Whether this file was deleted
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deleted: Option<bool>,
|
||||
/// Whether this file was reported
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reported: Option<bool>,
|
||||
|
||||
// TODO: migrate this mess to having:
|
||||
// - author_id
|
||||
// - parent: Parent { Message(id), User(id), etc }
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub server_id: Option<String>,
|
||||
|
||||
/// Id of the object this file is associated with
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub object_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Metadata associated with a file
|
||||
#[serde(tag = "type")]
|
||||
#[derive(Default)]
|
||||
pub enum Metadata {
|
||||
/// File is just a generic uncategorised file
|
||||
#[default]
|
||||
File,
|
||||
/// File contains textual data and should be displayed as such
|
||||
Text,
|
||||
/// File is an image with specific dimensions
|
||||
Image { width: usize, height: usize },
|
||||
/// File is a video with specific dimensions
|
||||
Video { width: usize, height: usize },
|
||||
/// File is audio
|
||||
Audio,
|
||||
}
|
||||
);
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::File> for File {
|
||||
fn from(value: revolt_database::File) -> Self {
|
||||
File {
|
||||
id: value.id,
|
||||
tag: value.tag,
|
||||
filename: value.filename,
|
||||
metadata: value.metadata.into(),
|
||||
content_type: value.content_type,
|
||||
size: value.size,
|
||||
deleted: value.deleted,
|
||||
reported: value.reported,
|
||||
message_id: value.message_id,
|
||||
user_id: value.user_id,
|
||||
server_id: value.server_id,
|
||||
object_id: value.object_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::Metadata> for Metadata {
|
||||
fn from(value: revolt_database::Metadata) -> Self {
|
||||
match value {
|
||||
revolt_database::Metadata::File => Metadata::File,
|
||||
revolt_database::Metadata::Text => Metadata::Text,
|
||||
revolt_database::Metadata::Image { width, height } => Metadata::Image {
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
},
|
||||
revolt_database::Metadata::Video { width, height } => Metadata::Video {
|
||||
width: width as usize,
|
||||
height: height as usize,
|
||||
},
|
||||
revolt_database::Metadata::Audio => Metadata::Audio,
|
||||
}
|
||||
}
|
||||
}
|
||||
9
crates/core/models/src/v0/mod.rs
Normal file
9
crates/core/models/src/v0/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod account_strikes;
|
||||
mod bots;
|
||||
mod files;
|
||||
mod users;
|
||||
|
||||
pub use account_strikes::*;
|
||||
pub use bots::*;
|
||||
pub use files::*;
|
||||
pub use users::*;
|
||||
266
crates/core/models/src/v0/users.rs
Normal file
266
crates/core/models/src/v0/users.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
use super::File;
|
||||
|
||||
auto_derived!(
|
||||
/// User
|
||||
pub struct User {
|
||||
/// Unique Id
|
||||
#[serde(rename = "_id")]
|
||||
pub id: String,
|
||||
/// Username
|
||||
pub username: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
/// Avatar attachment
|
||||
pub avatar: Option<File>,
|
||||
/// Relationships with other users
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub relations: Vec<Relationship>,
|
||||
|
||||
/// Bitfield of user badges
|
||||
#[serde(skip_serializing_if = "crate::if_zero_u32", default)]
|
||||
pub badges: u32,
|
||||
/// User's current status
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<UserStatus>,
|
||||
/// User's profile page
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile: Option<UserProfile>,
|
||||
|
||||
/// Enum of user flags
|
||||
#[serde(skip_serializing_if = "crate::if_zero_u32", default)]
|
||||
pub flags: u32,
|
||||
/// Whether this user is privileged
|
||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
||||
pub privileged: bool,
|
||||
/// Bot information
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bot: Option<BotInformation>,
|
||||
|
||||
/// Current session user's relationship with this user
|
||||
pub relationship: RelationshipStatus,
|
||||
/// Whether this user is currently online
|
||||
pub online: bool,
|
||||
}
|
||||
|
||||
/// User's relationship with another user (or themselves)
|
||||
#[derive(Default)]
|
||||
pub enum RelationshipStatus {
|
||||
#[default]
|
||||
None,
|
||||
User,
|
||||
Friend,
|
||||
Outgoing,
|
||||
Incoming,
|
||||
Blocked,
|
||||
BlockedOther,
|
||||
}
|
||||
|
||||
/// Relationship entry indicating current status with other user
|
||||
pub struct Relationship {
|
||||
#[serde(rename = "_id")]
|
||||
pub user_id: String,
|
||||
pub status: RelationshipStatus,
|
||||
}
|
||||
|
||||
/// Presence status
|
||||
pub enum Presence {
|
||||
/// User is online
|
||||
Online,
|
||||
/// User is not currently available
|
||||
Idle,
|
||||
/// User is focusing / will only receive mentions
|
||||
Focus,
|
||||
/// User is busy / will not receive any notifications
|
||||
Busy,
|
||||
/// User appears to be offline
|
||||
Invisible,
|
||||
}
|
||||
|
||||
/// User's active status
|
||||
pub struct UserStatus {
|
||||
/// Custom status text
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub text: String,
|
||||
/// Current presence option
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub presence: Option<Presence>,
|
||||
}
|
||||
|
||||
/// User's profile
|
||||
pub struct UserProfile {
|
||||
/// Text content on user's profile
|
||||
#[serde(skip_serializing_if = "String::is_empty")]
|
||||
pub content: String,
|
||||
/// Background visible on user's profile
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub background: Option<File>,
|
||||
}
|
||||
|
||||
/// User badge bitfield
|
||||
#[repr(u32)]
|
||||
pub enum UserBadges {
|
||||
/// Revolt Developer
|
||||
Developer = 1,
|
||||
/// Helped translate Revolt
|
||||
Translator = 2,
|
||||
/// Monetarily supported Revolt
|
||||
Supporter = 4,
|
||||
/// Responsibly disclosed a security issue
|
||||
ResponsibleDisclosure = 8,
|
||||
/// Revolt Founder
|
||||
Founder = 16,
|
||||
/// Platform moderator
|
||||
PlatformModeration = 32,
|
||||
/// Active monetary supporter
|
||||
ActiveSupporter = 64,
|
||||
/// 🦊🦝
|
||||
Paw = 128,
|
||||
/// Joined as one of the first 1000 users in 2021
|
||||
EarlyAdopter = 256,
|
||||
/// Amogus
|
||||
ReservedRelevantJokeBadge1 = 512,
|
||||
/// Low resolution troll face
|
||||
ReservedRelevantJokeBadge2 = 1024,
|
||||
}
|
||||
|
||||
/// User flag enum
|
||||
#[repr(u32)]
|
||||
pub enum UserFlags {
|
||||
/// User has been suspended from the platform
|
||||
Suspended = 1,
|
||||
/// User has deleted their account
|
||||
Deleted = 2,
|
||||
/// User was banned off the platform
|
||||
Banned = 4,
|
||||
/// User was marked as spam and removed from platform
|
||||
Spam = 8,
|
||||
}
|
||||
|
||||
/// Bot information for if the user is a bot
|
||||
pub struct BotInformation {
|
||||
/// Id of the owner of this bot
|
||||
#[serde(rename = "owner")]
|
||||
pub owner_id: String,
|
||||
}
|
||||
);
|
||||
|
||||
pub trait CheckRelationship {
|
||||
fn with(&self, user: &str) -> RelationshipStatus;
|
||||
}
|
||||
|
||||
impl CheckRelationship for Vec<Relationship> {
|
||||
fn with(&self, user: &str) -> RelationshipStatus {
|
||||
for entry in self {
|
||||
if entry.user_id == user {
|
||||
return entry.status.clone();
|
||||
}
|
||||
}
|
||||
|
||||
RelationshipStatus::None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl User {
|
||||
pub async fn from<P>(user: revolt_database::User, perspective: P) -> Self
|
||||
where
|
||||
P: Into<Option<revolt_database::User>>,
|
||||
{
|
||||
let relationship = if let Some(perspective) = perspective.into() {
|
||||
perspective
|
||||
.relations
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.find(|relationship| relationship.id == user.id)
|
||||
.map(|relationship| relationship.status.into())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
RelationshipStatus::None
|
||||
};
|
||||
|
||||
// do permission stuff here
|
||||
// TODO: implement permissions =)
|
||||
let can_see_profile = false;
|
||||
|
||||
Self {
|
||||
username: user.username,
|
||||
avatar: user.avatar.map(|file| file.into()),
|
||||
relations: vec![],
|
||||
badges: user.badges.unwrap_or_default() as u32,
|
||||
status: None,
|
||||
profile: None,
|
||||
flags: user.flags.unwrap_or_default() as u32,
|
||||
privileged: user.privileged,
|
||||
bot: user.bot.map(|bot| bot.into()),
|
||||
relationship,
|
||||
online: can_see_profile && revolt_presence::is_online(&user.id).await,
|
||||
id: user.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::RelationshipStatus> for RelationshipStatus {
|
||||
fn from(value: revolt_database::RelationshipStatus) -> Self {
|
||||
match value {
|
||||
revolt_database::RelationshipStatus::None => RelationshipStatus::None,
|
||||
revolt_database::RelationshipStatus::User => RelationshipStatus::User,
|
||||
revolt_database::RelationshipStatus::Friend => RelationshipStatus::Friend,
|
||||
revolt_database::RelationshipStatus::Outgoing => RelationshipStatus::Outgoing,
|
||||
revolt_database::RelationshipStatus::Incoming => RelationshipStatus::Incoming,
|
||||
revolt_database::RelationshipStatus::Blocked => RelationshipStatus::Blocked,
|
||||
revolt_database::RelationshipStatus::BlockedOther => RelationshipStatus::BlockedOther,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::Relationship> for Relationship {
|
||||
fn from(value: revolt_database::Relationship) -> Self {
|
||||
Self {
|
||||
user_id: value.id,
|
||||
status: value.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::Presence> for Presence {
|
||||
fn from(value: revolt_database::Presence) -> Self {
|
||||
match value {
|
||||
revolt_database::Presence::Online => Presence::Online,
|
||||
revolt_database::Presence::Idle => Presence::Idle,
|
||||
revolt_database::Presence::Focus => Presence::Focus,
|
||||
revolt_database::Presence::Busy => Presence::Busy,
|
||||
revolt_database::Presence::Invisible => Presence::Invisible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::UserStatus> for UserStatus {
|
||||
fn from(value: revolt_database::UserStatus) -> Self {
|
||||
UserStatus {
|
||||
text: value.text,
|
||||
presence: value.presence.map(|presence| presence.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::UserProfile> for UserProfile {
|
||||
fn from(value: revolt_database::UserProfile) -> Self {
|
||||
UserProfile {
|
||||
content: value.content,
|
||||
background: value.background.map(|file| file.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "from_database")]
|
||||
impl From<revolt_database::BotInformation> for BotInformation {
|
||||
fn from(value: revolt_database::BotInformation) -> Self {
|
||||
BotInformation {
|
||||
owner_id: value.owner,
|
||||
}
|
||||
}
|
||||
}
|
||||
33
crates/core/permissions/Cargo.toml
Normal file
33
crates/core/permissions/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "revolt-permissions"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
description = "Revolt Backend: Permission Logic"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
serde = [ "dep:serde" ]
|
||||
schemas = [ "dep:schemars" ]
|
||||
try-from-primitive = [ "dep:num_enum" ]
|
||||
|
||||
[dev-dependencies]
|
||||
# Async
|
||||
async-std = { version = "1.8.0", features = ["attributes"] }
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
auto_ops = "0.3.0"
|
||||
once_cell = "1.17"
|
||||
num_enum = { version = "0.6.1", optional = true }
|
||||
|
||||
# Async
|
||||
async-trait = "0.1.51"
|
||||
|
||||
# Serialisation
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
# Spec Generation
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
128
crates/core/permissions/src/impl.rs
Normal file
128
crates/core/permissions/src/impl.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use crate::{
|
||||
ChannelPermission, ChannelType, PermissionQuery, PermissionValue, RelationshipStatus,
|
||||
UserPermission, ALLOW_IN_TIMEOUT, DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
||||
DEFAULT_PERMISSION_SAVED_MESSAGES, DEFAULT_PERMISSION_VIEW_ONLY,
|
||||
};
|
||||
|
||||
/// Calculate permissions against a user
|
||||
pub async fn calculate_user_permissions<P: PermissionQuery>(query: &mut P) -> u32 {
|
||||
if query.are_we_privileged().await {
|
||||
return u32::MAX;
|
||||
}
|
||||
|
||||
if query.are_the_users_same().await {
|
||||
return u32::MAX;
|
||||
}
|
||||
|
||||
let mut permissions = 0_u32;
|
||||
match query.user_relationship().await {
|
||||
RelationshipStatus::Friend => return u32::MAX,
|
||||
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
|
||||
return UserPermission::Access as u32
|
||||
}
|
||||
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
|
||||
permissions = UserPermission::Access as u32;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if query.have_mutual_connection().await {
|
||||
permissions = UserPermission::Access + UserPermission::ViewProfile;
|
||||
|
||||
if query.user_is_bot().await || query.are_we_a_bot().await {
|
||||
permissions += UserPermission::SendMessage as u32;
|
||||
}
|
||||
|
||||
permissions
|
||||
} else {
|
||||
permissions
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate permissions against a server
|
||||
pub async fn calculate_server_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
|
||||
if query.are_we_privileged().await || query.are_we_server_owner().await {
|
||||
return ChannelPermission::GrantAllSafe.into();
|
||||
}
|
||||
|
||||
if !query.are_we_a_member().await {
|
||||
return 0_u64.into();
|
||||
}
|
||||
|
||||
let mut permissions: PermissionValue = query.get_default_server_permissions().await.into();
|
||||
|
||||
for role_override in query.get_our_server_role_overrides().await {
|
||||
permissions.apply(role_override);
|
||||
}
|
||||
|
||||
if query.are_we_timed_out().await {
|
||||
permissions.restrict(*ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
permissions
|
||||
}
|
||||
|
||||
/// Calculate permissions against a channel
|
||||
pub async fn calculate_channel_permissions<P: PermissionQuery>(query: &mut P) -> PermissionValue {
|
||||
if query.are_we_privileged().await {
|
||||
return ChannelPermission::GrantAllSafe.into();
|
||||
}
|
||||
|
||||
match query.get_channel_type().await {
|
||||
ChannelType::SavedMessages => {
|
||||
if query.do_we_own_the_channel().await {
|
||||
DEFAULT_PERMISSION_SAVED_MESSAGES.into()
|
||||
} else {
|
||||
0_u64.into()
|
||||
}
|
||||
}
|
||||
ChannelType::DirectMessage => {
|
||||
if query.are_we_part_of_the_channel().await {
|
||||
query.set_recipient_as_user().await;
|
||||
|
||||
let permissions = calculate_user_permissions(query).await;
|
||||
if (permissions & UserPermission::SendMessage as u32)
|
||||
== UserPermission::SendMessage as u32
|
||||
{
|
||||
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
|
||||
} else {
|
||||
(*DEFAULT_PERMISSION_VIEW_ONLY).into()
|
||||
}
|
||||
} else {
|
||||
0_u64.into()
|
||||
}
|
||||
}
|
||||
ChannelType::Group => {
|
||||
if query.do_we_own_the_channel().await {
|
||||
ChannelPermission::GrantAllSafe.into()
|
||||
} else if query.are_we_part_of_the_channel().await {
|
||||
(*DEFAULT_PERMISSION_VIEW_ONLY
|
||||
| query.get_default_channel_permissions().await.allow)
|
||||
.into()
|
||||
} else {
|
||||
0_u64.into()
|
||||
}
|
||||
}
|
||||
ChannelType::ServerChannel => {
|
||||
query.set_server_from_channel().await;
|
||||
|
||||
if query.are_we_a_member().await {
|
||||
let mut permissions = calculate_server_permissions(query).await;
|
||||
permissions.apply(query.get_default_channel_permissions().await);
|
||||
|
||||
for role_override in query.get_our_channel_role_overrides().await {
|
||||
permissions.apply(role_override);
|
||||
}
|
||||
|
||||
if query.are_we_timed_out().await {
|
||||
permissions.restrict(*ALLOW_IN_TIMEOUT);
|
||||
}
|
||||
|
||||
permissions
|
||||
} else {
|
||||
0_u64.into()
|
||||
}
|
||||
}
|
||||
ChannelType::Unknown => 0_u64.into(),
|
||||
}
|
||||
}
|
||||
16
crates/core/permissions/src/lib.rs
Normal file
16
crates/core/permissions/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
#[macro_use]
|
||||
extern crate auto_ops;
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
|
||||
mod r#impl;
|
||||
mod models;
|
||||
mod r#trait;
|
||||
|
||||
pub use models::*;
|
||||
pub use r#impl::*;
|
||||
pub use r#trait::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
137
crates/core/permissions/src/models/channel.rs
Normal file
137
crates/core/permissions/src/models/channel.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::ops::Add;
|
||||
|
||||
/// Abstract channel type
|
||||
pub enum ChannelType {
|
||||
SavedMessages,
|
||||
DirectMessage,
|
||||
Group,
|
||||
ServerChannel,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Permission value on Revolt
|
||||
///
|
||||
/// This should be restricted to the lower 52 bits to prevent any
|
||||
/// potential issues with Javascript. Also leave empty spaces for
|
||||
/// future permission flags to be added.
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "try-from-primitive", derive(num_enum::TryFromPrimitive))]
|
||||
#[repr(u64)]
|
||||
pub enum ChannelPermission {
|
||||
// * Generic permissions
|
||||
/// Manage the channel or channels on the server
|
||||
ManageChannel = 1 << 0,
|
||||
/// Manage the server
|
||||
ManageServer = 1 << 1,
|
||||
/// Manage permissions on servers or channels
|
||||
ManagePermissions = 1 << 2,
|
||||
/// Manage roles on server
|
||||
ManageRole = 1 << 3,
|
||||
/// Manage server customisation (includes emoji)
|
||||
ManageCustomisation = 1 << 4,
|
||||
|
||||
// % 1 bit reserved
|
||||
|
||||
// * Member permissions
|
||||
/// Kick other members below their ranking
|
||||
KickMembers = 1 << 6,
|
||||
/// Ban other members below their ranking
|
||||
BanMembers = 1 << 7,
|
||||
/// Timeout other members below their ranking
|
||||
TimeoutMembers = 1 << 8,
|
||||
/// Assign roles to members below their ranking
|
||||
AssignRoles = 1 << 9,
|
||||
/// Change own nickname
|
||||
ChangeNickname = 1 << 10,
|
||||
/// Change or remove other's nicknames below their ranking
|
||||
ManageNicknames = 1 << 11,
|
||||
/// Change own avatar
|
||||
ChangeAvatar = 1 << 12,
|
||||
/// Remove other's avatars below their ranking
|
||||
RemoveAvatars = 1 << 13,
|
||||
|
||||
// % 7 bits reserved
|
||||
|
||||
// * Channel permissions
|
||||
/// View a channel
|
||||
ViewChannel = 1 << 20,
|
||||
/// Read a channel's past message history
|
||||
ReadMessageHistory = 1 << 21,
|
||||
/// Send a message in a channel
|
||||
SendMessage = 1 << 22,
|
||||
/// Delete messages in a channel
|
||||
ManageMessages = 1 << 23,
|
||||
/// Manage webhook entries on a channel
|
||||
ManageWebhooks = 1 << 24,
|
||||
/// Create invites to this channel
|
||||
InviteOthers = 1 << 25,
|
||||
/// Send embedded content in this channel
|
||||
SendEmbeds = 1 << 26,
|
||||
/// Send attachments and media in this channel
|
||||
UploadFiles = 1 << 27,
|
||||
/// Masquerade messages using custom nickname and avatar
|
||||
Masquerade = 1 << 28,
|
||||
/// React to messages with emojis
|
||||
React = 1 << 29,
|
||||
|
||||
// * Voice permissions
|
||||
/// Connect to a voice channel
|
||||
Connect = 1 << 30,
|
||||
/// Speak in a voice call
|
||||
Speak = 1 << 31,
|
||||
/// Share video in a voice call
|
||||
Video = 1 << 32,
|
||||
/// Mute other members with lower ranking in a voice call
|
||||
MuteMembers = 1 << 33,
|
||||
/// Deafen other members with lower ranking in a voice call
|
||||
DeafenMembers = 1 << 34,
|
||||
/// Move members between voice channels
|
||||
MoveMembers = 1 << 35,
|
||||
|
||||
// * Misc. permissions
|
||||
// % Bits 36 to 52: free area
|
||||
// % Bits 53 to 64: do not use
|
||||
|
||||
// * Grant all permissions
|
||||
/// Safely grant all permissions
|
||||
GrantAllSafe = 0x000F_FFFF_FFFF_FFFF,
|
||||
|
||||
/// Grant all permissions
|
||||
GrantAll = u64::MAX,
|
||||
}
|
||||
|
||||
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u64 { *a as u64 | *b as u64 });
|
||||
impl_op_ex_commutative!(+ |a: &u64, b: &ChannelPermission| -> u64 { *a | *b as u64 });
|
||||
|
||||
pub static ALLOW_IN_TIMEOUT: Lazy<u64> =
|
||||
Lazy::new(|| ChannelPermission::ViewChannel + ChannelPermission::ReadMessageHistory);
|
||||
|
||||
pub static DEFAULT_PERMISSION_VIEW_ONLY: Lazy<u64> =
|
||||
Lazy::new(|| ChannelPermission::ViewChannel + ChannelPermission::ReadMessageHistory);
|
||||
|
||||
pub static DEFAULT_PERMISSION: Lazy<u64> = Lazy::new(|| {
|
||||
DEFAULT_PERMISSION_VIEW_ONLY.add(
|
||||
ChannelPermission::SendMessage
|
||||
+ ChannelPermission::InviteOthers
|
||||
+ ChannelPermission::SendEmbeds
|
||||
+ ChannelPermission::UploadFiles
|
||||
+ ChannelPermission::Connect
|
||||
+ ChannelPermission::Speak,
|
||||
)
|
||||
});
|
||||
|
||||
pub static DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = ChannelPermission::GrantAllSafe as u64;
|
||||
|
||||
pub static DEFAULT_PERMISSION_DIRECT_MESSAGE: Lazy<u64> = Lazy::new(|| {
|
||||
DEFAULT_PERMISSION.add(ChannelPermission::ManageChannel + ChannelPermission::React)
|
||||
});
|
||||
|
||||
pub static DEFAULT_PERMISSION_SERVER: Lazy<u64> = Lazy::new(|| {
|
||||
DEFAULT_PERMISSION.add(
|
||||
ChannelPermission::React
|
||||
+ ChannelPermission::ChangeNickname
|
||||
+ ChannelPermission::ChangeAvatar,
|
||||
)
|
||||
});
|
||||
58
crates/core/permissions/src/models/mod.rs
Normal file
58
crates/core/permissions/src/models/mod.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
mod channel;
|
||||
mod server;
|
||||
mod user;
|
||||
|
||||
pub use channel::*;
|
||||
pub use server::*;
|
||||
pub use user::*;
|
||||
|
||||
/// Holds a permission value to manipulate.
|
||||
#[derive(Debug)]
|
||||
pub struct PermissionValue(u64);
|
||||
|
||||
impl PermissionValue {
|
||||
/// Apply a given override to this value
|
||||
pub fn apply(&mut self, v: Override) {
|
||||
self.allow(v.allow);
|
||||
self.revoke(v.deny);
|
||||
}
|
||||
|
||||
/// Allow given permissions
|
||||
pub fn allow(&mut self, v: u64) {
|
||||
self.0 |= v;
|
||||
}
|
||||
|
||||
/// Revoke given permissions
|
||||
pub fn revoke(&mut self, v: u64) {
|
||||
self.0 &= !v;
|
||||
}
|
||||
|
||||
/// Restrict to given permissions
|
||||
pub fn restrict(&mut self, v: u64) {
|
||||
self.0 &= v;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for PermissionValue {
|
||||
fn from(v: i64) -> Self {
|
||||
Self(v as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for PermissionValue {
|
||||
fn from(v: u64) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PermissionValue> for u64 {
|
||||
fn from(v: PermissionValue) -> Self {
|
||||
v.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChannelPermission> for PermissionValue {
|
||||
fn from(v: ChannelPermission) -> Self {
|
||||
(v as u64).into()
|
||||
}
|
||||
}
|
||||
56
crates/core/permissions/src/models/server.rs
Normal file
56
crates/core/permissions/src/models/server.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
/// Representation of a single permission override
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Override {
|
||||
/// Allow bit flags
|
||||
pub allow: u64,
|
||||
/// Disallow bit flags
|
||||
pub deny: u64,
|
||||
}
|
||||
|
||||
/// Representation of a single permission override
|
||||
/// as it appears on models and in the database
|
||||
#[derive(/*JsonSchema, */ Debug, Clone, Copy, Default, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct OverrideField {
|
||||
/// Allow bit flags
|
||||
a: i64,
|
||||
/// Disallow bit flags
|
||||
d: i64,
|
||||
}
|
||||
|
||||
impl Override {
|
||||
/// Into allows
|
||||
pub fn allows(&self) -> u64 {
|
||||
self.allow
|
||||
}
|
||||
|
||||
/// Into denies
|
||||
pub fn denies(&self) -> u64 {
|
||||
self.deny
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Override> for OverrideField {
|
||||
fn from(v: Override) -> Self {
|
||||
Self {
|
||||
a: v.allow as i64,
|
||||
d: v.deny as i64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OverrideField> for Override {
|
||||
fn from(v: OverrideField) -> Self {
|
||||
Self {
|
||||
allow: v.a as u64,
|
||||
deny: v.d as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*impl From<OverrideField> for Bson {
|
||||
fn from(v: OverrideField) -> Self {
|
||||
Self::Document(bson::to_document(&v).unwrap())
|
||||
}
|
||||
}*/
|
||||
25
crates/core/permissions/src/models/user.rs
Normal file
25
crates/core/permissions/src/models/user.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
/// User's relationship with another user (or themselves)
|
||||
pub enum RelationshipStatus {
|
||||
None,
|
||||
User,
|
||||
Friend,
|
||||
Outgoing,
|
||||
Incoming,
|
||||
Blocked,
|
||||
BlockedOther,
|
||||
}
|
||||
|
||||
/// User permission definitions
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "try-from-primitive", derive(num_enum::TryFromPrimitive))]
|
||||
#[repr(u32)]
|
||||
pub enum UserPermission {
|
||||
Access = 1 << 0,
|
||||
ViewProfile = 1 << 1,
|
||||
SendMessage = 1 << 2,
|
||||
Invite = 1 << 3,
|
||||
}
|
||||
|
||||
impl_op_ex!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 });
|
||||
impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 });
|
||||
377
crates/core/permissions/src/test.rs
Normal file
377
crates/core/permissions/src/test.rs
Normal file
@@ -0,0 +1,377 @@
|
||||
use crate::{
|
||||
calculate_channel_permissions, calculate_user_permissions, ChannelPermission, ChannelType,
|
||||
Override, PermissionQuery, RelationshipStatus, DEFAULT_PERMISSION_DIRECT_MESSAGE,
|
||||
DEFAULT_PERMISSION_SERVER, DEFAULT_PERMISSION_VIEW_ONLY,
|
||||
};
|
||||
|
||||
#[async_std::test]
|
||||
async fn validate_user_permissions() {
|
||||
/// Scenario in which we are friends with a user
|
||||
/// and we have a DM channel open with them
|
||||
struct Scenario {}
|
||||
let mut query = Scenario {};
|
||||
|
||||
let perms = calculate_user_permissions(&mut query).await;
|
||||
assert_eq!(perms, u32::MAX);
|
||||
|
||||
let perms = calculate_channel_permissions(&mut query).await;
|
||||
let value: u64 = perms.into();
|
||||
assert_eq!(value, *DEFAULT_PERMISSION_DIRECT_MESSAGE);
|
||||
|
||||
#[async_trait]
|
||||
impl PermissionQuery for Scenario {
|
||||
async fn are_we_privileged(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_we_a_bot(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_the_users_same(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn user_relationship(&mut self) -> RelationshipStatus {
|
||||
RelationshipStatus::Friend
|
||||
}
|
||||
|
||||
async fn user_is_bot(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn have_mutual_connection(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_we_server_owner(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_a_member(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn get_default_server_permissions(&mut self) -> u64 {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_timed_out(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn get_channel_type(&mut self) -> ChannelType {
|
||||
ChannelType::DirectMessage
|
||||
}
|
||||
|
||||
async fn get_default_channel_permissions(&mut self) -> Override {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn do_we_own_the_channel(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_part_of_the_channel(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn set_recipient_as_user(&mut self) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
async fn set_server_from_channel(&mut self) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn validate_group_permissions() {
|
||||
/// Scenario in which we are in a group channel with only talking permission
|
||||
struct Scenario {}
|
||||
let mut query = Scenario {};
|
||||
|
||||
let perms = calculate_channel_permissions(&mut query).await;
|
||||
let value: u64 = perms.into();
|
||||
assert_eq!(
|
||||
value,
|
||||
*DEFAULT_PERMISSION_VIEW_ONLY | ChannelPermission::SendMessage as u64
|
||||
);
|
||||
|
||||
#[async_trait]
|
||||
impl PermissionQuery for Scenario {
|
||||
async fn are_we_privileged(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_we_a_bot(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_the_users_same(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn user_relationship(&mut self) -> RelationshipStatus {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn user_is_bot(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn have_mutual_connection(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_server_owner(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_a_member(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn get_default_server_permissions(&mut self) -> u64 {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_timed_out(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn get_channel_type(&mut self) -> ChannelType {
|
||||
ChannelType::Group
|
||||
}
|
||||
|
||||
async fn get_default_channel_permissions(&mut self) -> Override {
|
||||
Override {
|
||||
allow: ChannelPermission::SendMessage as u64,
|
||||
deny: 0,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn do_we_own_the_channel(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_we_part_of_the_channel(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn set_recipient_as_user(&mut self) {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn set_server_from_channel(&mut self) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn validate_server_permissions() {
|
||||
/// Scenario in which we are in a server channel where:
|
||||
/// - the server grants reading history and sending messages by default
|
||||
/// - we have a role that allows us to upload files and react but denies reading history
|
||||
/// - however the channel disallows sending messages
|
||||
/// - and removes our role specific react permission
|
||||
struct Scenario {}
|
||||
let mut query = Scenario {};
|
||||
|
||||
let perms = calculate_channel_permissions(&mut query).await;
|
||||
let value: u64 = perms.into();
|
||||
assert_eq!(
|
||||
value,
|
||||
ChannelPermission::ViewChannel as u64 | ChannelPermission::UploadFiles as u64
|
||||
);
|
||||
|
||||
#[async_trait]
|
||||
impl PermissionQuery for Scenario {
|
||||
async fn are_we_privileged(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_we_a_bot(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_the_users_same(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn user_relationship(&mut self) -> RelationshipStatus {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn user_is_bot(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn have_mutual_connection(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_server_owner(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_we_a_member(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn get_default_server_permissions(&mut self) -> u64 {
|
||||
ChannelPermission::ViewChannel as u64
|
||||
| ChannelPermission::SendMessage as u64
|
||||
| ChannelPermission::ReadMessageHistory as u64
|
||||
}
|
||||
|
||||
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
|
||||
vec![Override {
|
||||
allow: ChannelPermission::UploadFiles as u64 | ChannelPermission::React as u64,
|
||||
deny: ChannelPermission::ReadMessageHistory as u64,
|
||||
}]
|
||||
}
|
||||
|
||||
async fn are_we_timed_out(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn get_channel_type(&mut self) -> ChannelType {
|
||||
ChannelType::ServerChannel
|
||||
}
|
||||
|
||||
async fn get_default_channel_permissions(&mut self) -> Override {
|
||||
Override {
|
||||
allow: 0,
|
||||
deny: ChannelPermission::SendMessage as u64,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
|
||||
vec![Override {
|
||||
allow: 0,
|
||||
deny: ChannelPermission::React as u64,
|
||||
}]
|
||||
}
|
||||
|
||||
async fn do_we_own_the_channel(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_part_of_the_channel(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn set_recipient_as_user(&mut self) {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn set_server_from_channel(&mut self) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn validate_timed_out_member() {
|
||||
/// Scenario in which we are in a server that we have been timed out from
|
||||
struct Scenario {}
|
||||
let mut query = Scenario {};
|
||||
|
||||
let perms = calculate_channel_permissions(&mut query).await;
|
||||
let value: u64 = perms.into();
|
||||
assert_eq!(value, *DEFAULT_PERMISSION_VIEW_ONLY);
|
||||
|
||||
#[async_trait]
|
||||
impl PermissionQuery for Scenario {
|
||||
async fn are_we_privileged(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_we_a_bot(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_the_users_same(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn user_relationship(&mut self) -> RelationshipStatus {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn user_is_bot(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn have_mutual_connection(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_server_owner(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn are_we_a_member(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn get_default_server_permissions(&mut self) -> u64 {
|
||||
*DEFAULT_PERMISSION_SERVER
|
||||
}
|
||||
|
||||
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
async fn are_we_timed_out(&mut self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn get_channel_type(&mut self) -> ChannelType {
|
||||
ChannelType::ServerChannel
|
||||
}
|
||||
|
||||
async fn get_default_channel_permissions(&mut self) -> Override {
|
||||
Override { allow: 0, deny: 0 }
|
||||
}
|
||||
|
||||
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
async fn do_we_own_the_channel(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn are_we_part_of_the_channel(&mut self) -> bool {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn set_recipient_as_user(&mut self) {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
async fn set_server_from_channel(&mut self) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
}
|
||||
67
crates/core/permissions/src/trait.rs
Normal file
67
crates/core/permissions/src/trait.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use crate::{ChannelType, Override, RelationshipStatus};
|
||||
|
||||
#[async_trait]
|
||||
pub trait PermissionQuery {
|
||||
// * For calculating user permission
|
||||
|
||||
/// Is our perspective user privileged?
|
||||
async fn are_we_privileged(&mut self) -> bool;
|
||||
|
||||
/// Is our perspective user a bot?
|
||||
async fn are_we_a_bot(&mut self) -> bool;
|
||||
|
||||
/// Is our perspective user and the currently selected user the same?
|
||||
async fn are_the_users_same(&mut self) -> bool;
|
||||
|
||||
/// Get the relationship with have with the currently selected user
|
||||
async fn user_relationship(&mut self) -> RelationshipStatus;
|
||||
|
||||
/// Whether the currently selected user is a bot
|
||||
async fn user_is_bot(&mut self) -> bool;
|
||||
|
||||
/// Do we have a mutual connection with the currently selected user?
|
||||
async fn have_mutual_connection(&mut self) -> bool;
|
||||
|
||||
// * For calculating server permission
|
||||
|
||||
/// Is our perspective user the server's owner?
|
||||
async fn are_we_server_owner(&mut self) -> bool;
|
||||
|
||||
/// Is our perspective user a member of the server?
|
||||
async fn are_we_a_member(&mut self) -> bool;
|
||||
|
||||
/// Get default server permission
|
||||
async fn get_default_server_permissions(&mut self) -> u64;
|
||||
|
||||
/// Get the ordered role overrides (from lowest to highest) for this member in this server
|
||||
async fn get_our_server_role_overrides(&mut self) -> Vec<Override>;
|
||||
|
||||
/// Is our perspective user timed out on this server?
|
||||
async fn are_we_timed_out(&mut self) -> bool;
|
||||
|
||||
// * For calculating channel permission
|
||||
|
||||
/// Get the type of the channel
|
||||
async fn get_channel_type(&mut self) -> ChannelType;
|
||||
|
||||
/// Get the default channel permissions
|
||||
/// Group channel defaults should be mapped to an allow-only override
|
||||
async fn get_default_channel_permissions(&mut self) -> Override;
|
||||
|
||||
/// Get the ordered role overrides (from lowest to highest) for this member in this channel
|
||||
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override>;
|
||||
|
||||
/// Do we own this group or saved messages channel if it is one of those?
|
||||
async fn do_we_own_the_channel(&mut self) -> bool;
|
||||
|
||||
/// Are we a recipient of this channel?
|
||||
async fn are_we_part_of_the_channel(&mut self) -> bool;
|
||||
|
||||
/// Set the current user as the recipient of this channel
|
||||
/// (this will only ever be called for DirectMessage channels, use unimplemented!() for other code paths)
|
||||
async fn set_recipient_as_user(&mut self);
|
||||
|
||||
/// Set the current server as the server owning this channel
|
||||
/// (this will only ever be called for server channels, use unimplemented!() for other code paths)
|
||||
async fn set_server_from_channel(&mut self);
|
||||
}
|
||||
25
crates/core/presence/Cargo.toml
Normal file
25
crates/core/presence/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "revolt-presence"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
description = "Revolt Backend: User Presence"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
redis-is-patched = []
|
||||
|
||||
[dev-dependencies]
|
||||
# Async
|
||||
async-std = { version = "1.8.0", features = ["attributes"] }
|
||||
|
||||
[dependencies]
|
||||
# Utility
|
||||
log = "0.4.17"
|
||||
rand = "0.8.5"
|
||||
once_cell = "1.17.1"
|
||||
|
||||
# Redis
|
||||
redis-kiss = "0.1.4"
|
||||
247
crates/core/presence/src/lib.rs
Normal file
247
crates/core/presence/src/lib.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use rand::Rng;
|
||||
use redis_kiss::{get_connection, AsyncCommands};
|
||||
use std::collections::HashSet;
|
||||
|
||||
mod operations;
|
||||
use operations::{
|
||||
__add_to_set_string, __add_to_set_u32, __delete_key, __get_set_members_as_string,
|
||||
__get_set_size, __remove_from_set_string, __remove_from_set_u32,
|
||||
};
|
||||
|
||||
pub static REGION_ID: Lazy<u16> = Lazy::new(|| {
|
||||
std::env::var("REGION_ID")
|
||||
.unwrap_or_else(|_| "0".to_string())
|
||||
.parse()
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub static REGION_KEY: Lazy<String> = Lazy::new(|| format!("region{}", &*REGION_ID));
|
||||
pub static ONLINE_SET: &str = "online";
|
||||
|
||||
pub static FLAG_BITS: u32 = 0b1;
|
||||
|
||||
/// Create a new presence session, returns the ID of this session
|
||||
pub async fn create_session(user_id: &str, flags: u8) -> (bool, u32) {
|
||||
info!("Creating a presence session for {user_id} with flags {flags}");
|
||||
|
||||
if let Ok(mut conn) = get_connection().await {
|
||||
// Check whether this is the first session
|
||||
let was_empty = __get_set_size(&mut conn, user_id).await == 0;
|
||||
|
||||
// A session ID is comprised of random data and any flags ORed to the end
|
||||
let session_id = {
|
||||
let mut rng = rand::thread_rng();
|
||||
(rng.gen::<u32>() & !FLAG_BITS) | (flags as u32 & FLAG_BITS)
|
||||
};
|
||||
|
||||
// Add session to user's sessions and to the region
|
||||
__add_to_set_u32(&mut conn, user_id, session_id).await;
|
||||
__add_to_set_string(&mut conn, ONLINE_SET, user_id).await;
|
||||
__add_to_set_string(&mut conn, ®ION_KEY, &format!("{user_id}:{session_id}")).await;
|
||||
info!("Created session for {user_id}, assigned them a session ID of {session_id}.");
|
||||
|
||||
(was_empty, session_id)
|
||||
} else {
|
||||
// Fail through
|
||||
(false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete existing presence session
|
||||
pub async fn delete_session(user_id: &str, session_id: u32) -> bool {
|
||||
delete_session_internal(user_id, session_id, false).await
|
||||
}
|
||||
|
||||
/// Delete existing presence session (but also choose whether to skip region)
|
||||
async fn delete_session_internal(user_id: &str, session_id: u32, skip_region: bool) -> bool {
|
||||
info!("Deleting presence session for {user_id} with id {session_id}");
|
||||
|
||||
if let Ok(mut conn) = get_connection().await {
|
||||
// Remove the session
|
||||
__remove_from_set_u32(&mut conn, user_id, session_id).await;
|
||||
|
||||
// Remove from the region
|
||||
if !skip_region {
|
||||
__remove_from_set_string(&mut conn, ®ION_KEY, &format!("{user_id}:{session_id}"))
|
||||
.await;
|
||||
}
|
||||
|
||||
// Return whether this was the last session
|
||||
let is_empty = __get_set_size(&mut conn, user_id).await == 0;
|
||||
if is_empty {
|
||||
__remove_from_set_string(&mut conn, ONLINE_SET, user_id).await;
|
||||
info!("User ID {} just went offline.", &user_id);
|
||||
}
|
||||
|
||||
is_empty
|
||||
} else {
|
||||
// Fail through
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a given user ID is online
|
||||
pub async fn is_online(user_id: &str) -> bool {
|
||||
if let Ok(mut conn) = get_connection().await {
|
||||
conn.exists(user_id).await.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a set of users is online, returns a set of the online user IDs
|
||||
#[cfg(feature = "redis-is-patched")]
|
||||
pub async fn filter_online(user_ids: &'_ [String]) -> HashSet<String> {
|
||||
// Ignore empty list immediately, to save time.
|
||||
let mut set = HashSet::new();
|
||||
if user_ids.is_empty() {
|
||||
return set;
|
||||
}
|
||||
|
||||
// NOTE: at the point that we need mobile indicators
|
||||
// you can interpret the data here and return a new data
|
||||
// structure like HashMap<String /* id */, u8 /* flags */>
|
||||
|
||||
// We need to handle a special case where only one is present
|
||||
// as for some reason or another, Redis does not like us sending
|
||||
// a list of just one ID to the server.
|
||||
if user_ids.len() == 1 {
|
||||
if is_online(&user_ids[0]).await {
|
||||
set.insert(user_ids[0].to_string());
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
// Otherwise, go ahead as normal.
|
||||
if let Ok(mut conn) = get_connection().await {
|
||||
// Ok so, if this breaks, that means we've lost the Redis patch which adds SMISMEMBER
|
||||
// Currently it's patched in through a forked repository, investigate what happen to it
|
||||
let data: Vec<bool> = conn
|
||||
.smismember(ONLINE_SET, user_ids)
|
||||
.await
|
||||
.expect("this shouldn't happen, please read this code! presence/mod.rs");
|
||||
|
||||
if data.is_empty() {
|
||||
return set;
|
||||
}
|
||||
|
||||
// We filter known values to figure out who is online.
|
||||
for i in 0..user_ids.len() {
|
||||
if data[i] {
|
||||
set.insert(user_ids[i].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
}
|
||||
|
||||
/// Check whether a set of users is online, returns a set of the online user IDs
|
||||
#[cfg(not(feature = "redis-is-patched"))]
|
||||
pub async fn filter_online(user_ids: &'_ [String]) -> HashSet<String> {
|
||||
if user_ids.is_empty() {
|
||||
HashSet::new()
|
||||
} else if let Ok(mut conn) = get_connection().await {
|
||||
let members: Vec<String> = conn.smembers(ONLINE_SET).await.unwrap_or_default();
|
||||
let members: HashSet<&String> = members.iter().collect();
|
||||
let user_ids: HashSet<&String> = user_ids.iter().collect();
|
||||
|
||||
members
|
||||
.intersection(&user_ids)
|
||||
.map(|x| x.to_string())
|
||||
.collect()
|
||||
} else {
|
||||
HashSet::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset any stale presence data
|
||||
pub async fn clear_region(region_id: Option<&str>) {
|
||||
let region_id = region_id.unwrap_or(&*REGION_KEY);
|
||||
let mut conn = get_connection().await.expect("Redis connection");
|
||||
|
||||
let sessions = __get_set_members_as_string(&mut conn, region_id).await;
|
||||
if !sessions.is_empty() {
|
||||
info!(
|
||||
"Cleaning up {} sessions, this may take a while...",
|
||||
sessions.len()
|
||||
);
|
||||
|
||||
// Iterate and delete each session, this will
|
||||
// also send out any relevant events.
|
||||
for session in sessions {
|
||||
let parts = session.split(':').collect::<Vec<&str>>();
|
||||
if let (Some(user_id), Some(session_id)) = (parts.first(), parts.get(1)) {
|
||||
if let Ok(session_id) = session_id.parse() {
|
||||
delete_session_internal(user_id, session_id, true).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then clear the set in Redis.
|
||||
__delete_key(&mut conn, region_id).await;
|
||||
|
||||
info!("Clean up complete.");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{clear_region, create_session, delete_session, filter_online, is_online};
|
||||
use rand::Rng;
|
||||
|
||||
#[async_std::test]
|
||||
async fn it_works() {
|
||||
// Clear the region before we start the tests:
|
||||
clear_region(None).await;
|
||||
|
||||
// Generate some data we'll use:
|
||||
let user_id = rand::thread_rng().gen::<u32>().to_string();
|
||||
let other_id = rand::thread_rng().gen::<u32>().to_string();
|
||||
let flags = 1;
|
||||
|
||||
// Create a session
|
||||
let (first_session, session_id) = create_session(&user_id, flags).await;
|
||||
assert!(first_session);
|
||||
assert_ne!(session_id, 0);
|
||||
assert_eq!(session_id as u8 & flags, flags);
|
||||
|
||||
// Check if the user is online
|
||||
assert!(is_online(&user_id).await);
|
||||
|
||||
let user_ids = filter_online(&[user_id.to_string()]).await;
|
||||
assert_eq!(user_ids.len(), 1);
|
||||
assert!(user_ids.contains(&user_id));
|
||||
|
||||
// Create a few more sessions
|
||||
let (first_session, second_session_id) = create_session(&user_id, 0).await;
|
||||
assert!(!first_session);
|
||||
dbg!(second_session_id);
|
||||
assert_eq!(second_session_id as u8 & 1, 0);
|
||||
|
||||
let (first_session, other_session_id) = create_session(&other_id, 0).await;
|
||||
assert!(first_session);
|
||||
|
||||
let user_ids = filter_online(&[user_id.to_string(), other_id.to_string()]).await;
|
||||
assert_eq!(user_ids.len(), 2);
|
||||
assert!(user_ids.contains(&user_id));
|
||||
assert!(user_ids.contains(&other_id));
|
||||
|
||||
// Remove sessions
|
||||
delete_session(&user_id, session_id).await;
|
||||
delete_session(&other_id, other_session_id).await;
|
||||
assert!(!is_online(&other_id).await);
|
||||
|
||||
// Check if we can wipe everything too
|
||||
clear_region(None).await;
|
||||
assert!(!is_online(&user_id).await);
|
||||
|
||||
let user_ids = filter_online(&[user_id.to_string(), other_id.to_string()]).await;
|
||||
assert!(user_ids.is_empty())
|
||||
}
|
||||
}
|
||||
42
crates/core/presence/src/operations.rs
Normal file
42
crates/core/presence/src/operations.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use redis_kiss::{AsyncCommands, Conn};
|
||||
|
||||
/// Add to set (string)
|
||||
pub async fn __add_to_set_string(conn: &mut Conn, key: &str, value: &str) {
|
||||
let _: Option<()> = conn.sadd(key, value).await.ok();
|
||||
}
|
||||
|
||||
/// Add to set (u32)
|
||||
pub async fn __add_to_set_u32(conn: &mut Conn, key: &str, value: u32) {
|
||||
let _: Option<()> = conn.sadd(key, value).await.ok();
|
||||
}
|
||||
|
||||
/// Remove from set (string)
|
||||
pub async fn __remove_from_set_string(conn: &mut Conn, key: &str, value: &str) {
|
||||
let _: Option<()> = conn.srem(key, value).await.ok();
|
||||
}
|
||||
|
||||
/// Remove from set (u32)
|
||||
pub async fn __remove_from_set_u32(conn: &mut Conn, key: &str, value: u32) {
|
||||
let _: Option<()> = conn.srem(key, value).await.ok();
|
||||
}
|
||||
|
||||
/// Get set members as string
|
||||
pub async fn __get_set_members_as_string(conn: &mut Conn, key: &str) -> Vec<String> {
|
||||
conn.smembers::<_, Vec<String>>(key)
|
||||
.await
|
||||
.expect("could not get set members as string")
|
||||
}
|
||||
|
||||
/// Get set size
|
||||
pub async fn __get_set_size(conn: &mut Conn, id: &str) -> u32 {
|
||||
conn.scard::<_, u32>(id)
|
||||
.await
|
||||
.expect("could not get set size")
|
||||
}
|
||||
|
||||
/// Delete key by id
|
||||
pub async fn __delete_key(conn: &mut Conn, id: &str) {
|
||||
conn.del::<_, ()>(id)
|
||||
.await
|
||||
.expect("could not delete key by id");
|
||||
}
|
||||
22
crates/core/result/Cargo.toml
Normal file
22
crates/core/result/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "revolt-result"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = [ "Paul Makles <me@insrt.uk>" ]
|
||||
description = "Revolt Backend: Result and Error types"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
serde = [ "dep:serde" ]
|
||||
schemas = [ "dep:schemars" ]
|
||||
|
||||
default = [ "serde" ]
|
||||
|
||||
[dependencies]
|
||||
# Serialisation
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
# Spec Generation
|
||||
schemars = { version = "0.8.8", optional = true }
|
||||
178
crates/core/result/src/lib.rs
Normal file
178
crates/core/result/src/lib.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
#[cfg(feature = "serde")]
|
||||
#[macro_use]
|
||||
extern crate serde;
|
||||
|
||||
#[cfg(feature = "schemas")]
|
||||
#[macro_use]
|
||||
extern crate schemars;
|
||||
|
||||
/// 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 = "schemas", derive(JsonSchema))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Error {
|
||||
/// Type of error and additional information
|
||||
#[cfg_attr(feature = "serde", serde(flatten))]
|
||||
pub error_type: ErrorType,
|
||||
|
||||
/// Where this error occurred
|
||||
pub location: String,
|
||||
}
|
||||
|
||||
/// Possible error types
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(tag = "type"))]
|
||||
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ErrorType {
|
||||
/// This error was not labeled :(
|
||||
LabelMe,
|
||||
|
||||
// ? Onboarding related errors
|
||||
AlreadyOnboarded,
|
||||
|
||||
// ? User related errors
|
||||
UsernameTaken,
|
||||
InvalidUsername,
|
||||
UnknownUser,
|
||||
AlreadyFriends,
|
||||
AlreadySentRequest,
|
||||
Blocked,
|
||||
BlockedByOther,
|
||||
NotFriends,
|
||||
|
||||
// ? Channel related errors
|
||||
UnknownChannel,
|
||||
UnknownAttachment,
|
||||
UnknownMessage,
|
||||
CannotEditMessage,
|
||||
CannotJoinCall,
|
||||
TooManyAttachments {
|
||||
max: usize,
|
||||
},
|
||||
TooManyReplies {
|
||||
max: usize,
|
||||
},
|
||||
TooManyChannels {
|
||||
max: usize,
|
||||
},
|
||||
EmptyMessage,
|
||||
PayloadTooLarge,
|
||||
CannotRemoveYourself,
|
||||
GroupTooLarge {
|
||||
max: usize,
|
||||
},
|
||||
AlreadyInGroup,
|
||||
NotInGroup,
|
||||
|
||||
// ? Server related errors
|
||||
UnknownServer,
|
||||
InvalidRole,
|
||||
Banned,
|
||||
TooManyServers {
|
||||
max: usize,
|
||||
},
|
||||
TooManyEmoji {
|
||||
max: usize,
|
||||
},
|
||||
TooManyRoles {
|
||||
max: usize,
|
||||
},
|
||||
|
||||
// ? Bot related errors
|
||||
ReachedMaximumBots,
|
||||
IsBot,
|
||||
BotIsPrivate,
|
||||
|
||||
// ? User safety related errors
|
||||
CannotReportYourself,
|
||||
|
||||
// ? Permission errors
|
||||
MissingPermission {
|
||||
permission: String,
|
||||
},
|
||||
MissingUserPermission {
|
||||
permission: String,
|
||||
},
|
||||
NotElevated,
|
||||
NotPrivileged,
|
||||
CannotGiveMissingPermissions,
|
||||
NotOwner,
|
||||
|
||||
// ? General errors
|
||||
DatabaseError {
|
||||
operation: String,
|
||||
collection: String,
|
||||
},
|
||||
InternalError,
|
||||
InvalidOperation,
|
||||
InvalidCredentials,
|
||||
InvalidProperty,
|
||||
InvalidSession,
|
||||
DuplicateNonce,
|
||||
NotFound,
|
||||
NoEffect,
|
||||
FailedValidation {
|
||||
error: String,
|
||||
},
|
||||
|
||||
// ? Legacy errors
|
||||
VosoUnavailable,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! create_error {
|
||||
( $error: ident $( $tt:tt )? ) => {
|
||||
$crate::Error {
|
||||
error_type: $crate::ErrorType::$error $( $tt )?,
|
||||
location: format!("{}:{}:{}", file!(), line!(), column!()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! create_database_error {
|
||||
( $operation: expr, $collection: expr ) => {
|
||||
create_error!(DatabaseError {
|
||||
operation: $operation.to_string(),
|
||||
collection: $collection.to_string()
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! query {
|
||||
( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => {
|
||||
Ok($self.$type($collection, $($rest),+).await.unwrap())
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[cfg(not(debug_assertions))]
|
||||
macro_rules! query {
|
||||
( $self: ident, $type: ident, $collection: expr, $($rest:expr),+ ) => {
|
||||
$self.$type($collection, $($rest),+).await
|
||||
.map_err(|_| create_database_error!(stringify!($type), $collection))
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ErrorType;
|
||||
|
||||
#[test]
|
||||
fn use_macro_to_construct_error() {
|
||||
let error = create_error!(LabelMe);
|
||||
assert!(matches!(error.error_type, ErrorType::LabelMe));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_macro_to_construct_complex_error() {
|
||||
let error = create_error!(LabelMe);
|
||||
assert!(matches!(error.error_type, ErrorType::LabelMe));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "revolt-delta"
|
||||
version = "0.5.4"
|
||||
version = "0.5.20"
|
||||
license = "AGPL-3.0-or-later"
|
||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||
edition = "2018"
|
||||
@@ -15,9 +15,8 @@ log = "0.4.11"
|
||||
dotenv = "0.15.0"
|
||||
dashmap = "5.2.0"
|
||||
linkify = "0.6.0"
|
||||
once_cell = "1.4.1"
|
||||
once_cell = "1.17.1"
|
||||
env_logger = "0.7.1"
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
# Lang. Utilities
|
||||
regex = "1"
|
||||
@@ -44,20 +43,21 @@ async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"]
|
||||
# internal util
|
||||
lettre = "0.10.0-alpha.4"
|
||||
|
||||
# redis
|
||||
redis = { version = "0.21.2", features = ["async-std-comp"] }
|
||||
mobc = { version = "0.7.3" }
|
||||
mobc-redis = { version = "0.7.0", default-features = false, features = ["async-std-comp"] }
|
||||
|
||||
# web
|
||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
|
||||
rocket_empty = { git = "https://github.com/insertish/rocket_empty", branch = "master" }
|
||||
rocket_rauth = { git = "https://github.com/insertish/rauth", rev = "c2acaf6b31213e3969f8b48f9deed3daa125d394" }
|
||||
rocket_empty = { version = "0.1.1", features = ["schema"] }
|
||||
rocket_authifier = { version = "1.0.7" }
|
||||
|
||||
# spec generation
|
||||
schemars = "0.8.8"
|
||||
# rocket_okapi = "0.8.0-rc.1"
|
||||
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "a1048d0c8cd771e424ec97d33d825c32e06aa120", features = [ "swagger" ] }
|
||||
revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
|
||||
|
||||
# quark
|
||||
revolt-quark = { path = "../quark" }
|
||||
|
||||
# core
|
||||
revolt-database = { path = "../core/database", features = [ "rocket-impl" ] }
|
||||
revolt-models = { path = "../core/models", features = [ "schemas", "redis-is-patched" ] }
|
||||
|
||||
[build-dependencies]
|
||||
vergen = "7.5.0"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
# Build Stage
|
||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||
RUN cargo install --locked --path crates/delta
|
||||
|
||||
# Bundle Stage
|
||||
FROM debian:buster-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates
|
||||
COPY --from=builder /usr/local/cargo/bin/revolt-delta ./
|
||||
FROM debian:bullseye-slim
|
||||
RUN apt-get update && \
|
||||
apt-get install -y ca-certificates && \
|
||||
apt-get clean
|
||||
COPY --from=builder /home/rust/src/target/release/revolt-delta ./
|
||||
|
||||
EXPOSE 8000
|
||||
ENV ROCKET_ADDRESS 0.0.0.0
|
||||
|
||||
16
crates/delta/build.rs
Normal file
16
crates/delta/build.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
if let Ok(output) = Command::new("git")
|
||||
.args(["config", "--get", "remote.origin.url"])
|
||||
.output()
|
||||
{
|
||||
if let Ok(git_origin) = String::from_utf8(output.stdout) {
|
||||
println!("cargo:rustc-env=GIT_ORIGIN_URL={git_origin}");
|
||||
}
|
||||
}
|
||||
|
||||
vergen(Config::default()).ok();
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate rocket_okapi;
|
||||
extern crate revolt_rocket_okapi;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
pub mod routes;
|
||||
pub mod util;
|
||||
|
||||
use revolt_quark::rauth::RAuth;
|
||||
use async_std::channel::unbounded;
|
||||
use revolt_quark::authifier::{Authifier, AuthifierEvent};
|
||||
use revolt_quark::events::client::EventV1;
|
||||
use revolt_quark::DatabaseInfo;
|
||||
|
||||
#[launch]
|
||||
@@ -22,17 +22,40 @@ async fn rocket() -> _ {
|
||||
revolt_quark::variables::delta::preflight_checks();
|
||||
|
||||
// Setup database
|
||||
let db = DatabaseInfo::Auto.connect().await.unwrap();
|
||||
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
|
||||
db.migrate_database().await.unwrap();
|
||||
|
||||
// Setup rAuth
|
||||
let rauth = RAuth {
|
||||
database: db.clone().into(),
|
||||
config: revolt_quark::util::rauth::config(),
|
||||
// Legacy database setup from quark
|
||||
let legacy_db = DatabaseInfo::Auto.connect().await.unwrap();
|
||||
|
||||
// Setup Authifier event channel
|
||||
let (sender, receiver) = unbounded();
|
||||
|
||||
// Setup Authifier
|
||||
let authifier = Authifier {
|
||||
database: legacy_db.clone().into(),
|
||||
config: revolt_quark::util::authifier::config(),
|
||||
event_channel: Some(sender),
|
||||
};
|
||||
|
||||
// Launch background task workers.
|
||||
async_std::task::spawn(revolt_quark::tasks::start_workers(db.clone()));
|
||||
// Launch a listener for Authifier events
|
||||
async_std::task::spawn(async move {
|
||||
while let Ok(event) = receiver.recv().await {
|
||||
match &event {
|
||||
AuthifierEvent::CreateSession { .. } | AuthifierEvent::CreateAccount { .. } => {
|
||||
EventV1::Auth(event).global().await
|
||||
}
|
||||
AuthifierEvent::DeleteSession { user_id, .. }
|
||||
| AuthifierEvent::DeleteAllSessions { user_id, .. } => {
|
||||
let id = user_id.to_string();
|
||||
EventV1::Auth(event).private(id).await
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Launch background task workers
|
||||
async_std::task::spawn(revolt_quark::tasks::start_workers(legacy_db.clone()));
|
||||
|
||||
// Configure CORS
|
||||
let cors = revolt_quark::web::cors::new();
|
||||
@@ -43,8 +66,9 @@ async fn rocket() -> _ {
|
||||
.mount("/", revolt_quark::web::cors::catch_all_options_routes())
|
||||
.mount("/", revolt_quark::web::ratelimiter::routes())
|
||||
.mount("/swagger/", revolt_quark::web::swagger::routes())
|
||||
.manage(rauth)
|
||||
.manage(authifier)
|
||||
.manage(db)
|
||||
.manage(legacy_db)
|
||||
.manage(cors.clone())
|
||||
.attach(revolt_quark::web::ratelimiter::RatelimitFairing)
|
||||
.attach(cors)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user