Merge remote-tracking branch 'revoltchat/master' into webhooks

This commit is contained in:
Zomatree
2023-04-25 20:38:09 +01:00
90 changed files with 3907 additions and 634 deletions

View File

@@ -20,47 +20,52 @@ on:
- "Dockerfile" - "Dockerfile"
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
packages: write
jobs: jobs:
base: base:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Build base image (amd64) name: Build base image
steps: steps:
# Configure build environment # Configure build environment
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
# Authenticate with GHCR # Authenticate with GHCR
- name: Login to Github Container Registry - name: Login to Github Container Registry
uses: docker/login-action@v1 if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
# Build all projects and cache - name: Build base image
- name: Build Base Image uses: docker/build-push-action@v4
uses: docker/build-push-action@v3
with: with:
context: . context: .
push: true push: ${{ github.event_name != 'pull_request' }}
tags: ghcr.io/revoltchat/base:latest platforms: linux/amd64,linux/arm64
cache-from: type=gha tags: ghcr.io/${{ github.repository_owner }}/base:latest
cache-to: type=gha,mode=max 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] needs: [base]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
strategy: strategy:
matrix: matrix:
project: [delta, bonfire] project: [delta, bonfire]
name: Build ${{ matrix.project }} image (amd64) name: Build ${{ matrix.project }} image
steps: steps:
# Configure build environment # Configure build environment
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
@@ -68,10 +73,11 @@ jobs:
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry - name: Login to Github Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -86,11 +92,11 @@ jobs:
{ {
"delta": { "delta": {
"path": "crates/delta", "path": "crates/delta",
"tag": "revoltchat/server" "tag": "${{ github.repository_owner }}/server"
}, },
"bonfire": { "bonfire": {
"path": "crates/bonfire", "path": "crates/bonfire",
"tag": "revoltchat/bonfire" "tag": "${{ github.repository_owner }}/bonfire"
} }
} }
export_to: output export_to: output
@@ -98,19 +104,21 @@ jobs:
# Configure metadata # Configure metadata
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: 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 # Build crate image
- name: Publish - name: Publish
uses: docker/build-push-action@v3 uses: docker/build-push-action@v4
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
file: ${{ steps.export.outputs.path }}/Dockerfile file: ${{ steps.export.outputs.path }}/Dockerfile
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -2,7 +2,6 @@ name: Rust build, test, and generate specification
on: on:
push: push:
branches: [master]
pull_request: pull_request:
branches: [master] branches: [master]
@@ -27,34 +26,40 @@ jobs:
with: with:
command: build command: build
- name: Run services in background
run: |
docker-compose -f docker-compose.db.yml up -d
- name: Run cargo test - name: Run cargo test
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
- name: Run cargo test (with MongoDB)
uses: actions-rs/cargo@v1
env:
MONGODB: mongodb://localhost
with:
command: test
- name: Copy .env.example - name: Copy .env.example
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: | run: |
cp .env.example .env cp .env.example .env
- name: Run services in background
if: github.event_name != 'pull_request'
run: |
docker-compose -f docker-compose.db.yml up -d
- name: Start API in background - name: Start API in background
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: | run: |
cargo run --bin revolt-delta & cargo run --bin revolt-delta &
- name: Wait for API to go up - name: Wait for API to go up
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: nev7n/wait_for_response@v1 uses: nev7n/wait_for_response@v1
with: with:
url: "http://localhost:8000/" url: "http://localhost:8000/"
- name: Checkout API repository - name: Checkout API repository
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
repository: revoltchat/api repository: revoltchat/api
@@ -62,11 +67,11 @@ jobs:
token: ${{ secrets.PAT }} token: ${{ secrets.PAT }}
- name: Download OpenAPI specification - name: Download OpenAPI specification
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request' && github.ref_name == 'master'
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
- name: Commit changes - name: Commit changes
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request' && github.ref_name == 'master'
uses: EndBug/add-and-commit@v4 uses: EndBug/add-and-commit@v4
with: with:
cwd: "api" cwd: "api"

333
Cargo.lock generated
View File

@@ -105,7 +105,7 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
dependencies = [ dependencies = [
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -199,20 +199,20 @@ dependencies = [
[[package]] [[package]]
name = "async-recursion" name = "async-recursion"
version = "1.0.0" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 2.0.15",
] ]
[[package]] [[package]]
name = "async-std" name = "async-std"
version = "1.11.0" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52580991739c5cdb36cde8b2a516371c0a3b70dda36d916cc08b82372916808c" checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
dependencies = [ dependencies = [
"async-attributes", "async-attributes",
"async-channel", "async-channel",
@@ -229,7 +229,6 @@ dependencies = [
"kv-log-macro", "kv-log-macro",
"log", "log",
"memchr", "memchr",
"num_cpus",
"once_cell", "once_cell",
"pin-project-lite 0.2.9", "pin-project-lite 0.2.9",
"pin-utils", "pin-utils",
@@ -269,7 +268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -286,7 +285,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -364,6 +363,12 @@ dependencies = [
"validator 0.15.0", "validator 0.15.0",
] ]
[[package]]
name = "auto_ops"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7460f7dd8e100147b82a63afca1a20eb6c231ee36b90ba7272e14951cb58af59"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "0.1.8" version = "0.1.8"
@@ -715,7 +720,7 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c"
dependencies = [ dependencies = [
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -747,7 +752,7 @@ dependencies = [
"fnv", "fnv",
"ident_case", "ident_case",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"strsim", "strsim",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -759,7 +764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -808,7 +813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -829,7 +834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "123c73e7a6e51b05c75fe1a1b2f4e241399ea5740ed810b0e3e6cacd9db5e7b2" checksum = "123c73e7a6e51b05c75fe1a1b2f4e241399ea5740ed810b0e3e6cacd9db5e7b2"
dependencies = [ dependencies = [
"devise_core", "devise_core",
"quote 1.0.18", "quote 1.0.26",
] ]
[[package]] [[package]]
@@ -841,7 +846,7 @@ dependencies = [
"bitflags", "bitflags",
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -871,12 +876,6 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "dtoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.5" version = "1.0.5"
@@ -915,7 +914,7 @@ checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -935,7 +934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -1119,7 +1118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -1210,7 +1209,7 @@ checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9"
dependencies = [ dependencies = [
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -1831,6 +1830,27 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "metrics"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e52eb6380b6d2a10eb3434aec0885374490f5b82c8aaf5cd487a183c98be834"
dependencies = [
"ahash",
"metrics-macros",
]
[[package]]
name = "metrics-macros"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49e30813093f757be5cf21e50389a24dc7dbb22c49f23b7e8f51d69b508a5ffa"
dependencies = [
"proc-macro2",
"quote 1.0.26",
"syn 1.0.107",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.16" version = "0.3.16"
@@ -1897,9 +1917,9 @@ dependencies = [
[[package]] [[package]]
name = "mobc" name = "mobc"
version = "0.7.3" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f76d2f2e2dcbb00a8d3b2b09f026a74a82693ea52cd071647aa6cfa7f1ff37e" checksum = "fc79c4a77e312fee9c7bd4b957c12ad1196db73c4a81e5c0b13f02083c4f7f2f"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -1908,17 +1928,21 @@ dependencies = [
"futures-timer", "futures-timer",
"futures-util", "futures-util",
"log", "log",
"metrics",
"thiserror",
"tokio 1.18.2", "tokio 1.18.2",
"tracing",
"tracing-subscriber",
] ]
[[package]] [[package]]
name = "mobc-redis" name = "mobc-redis"
version = "0.7.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7b5db77b37c9224d5b9949b214041ea3e1c15b6b1e5dd24a5acb8e73975d6d6" checksum = "7bd8e2fd6bf7e35263b86662e663a9496a0352ceddd413b6c33313c36d5068fd"
dependencies = [ dependencies = [
"mobc", "mobc",
"redis 0.19.0", "redis 0.22.3",
] ]
[[package]] [[package]]
@@ -2080,7 +2104,16 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9"
dependencies = [ dependencies = [
"num_enum_derive", "num_enum_derive 0.5.7",
]
[[package]]
name = "num_enum"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1"
dependencies = [
"num_enum_derive 0.6.1",
] ]
[[package]] [[package]]
@@ -2091,10 +2124,22 @@ checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
[[package]]
name = "num_enum_derive"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote 1.0.26",
"syn 2.0.15",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.28.4" version = "0.28.4"
@@ -2144,7 +2189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -2167,15 +2212,6 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "optional_struct"
version = "0.2.0"
source = "git+https://github.com/insertish/OptionalStruct?rev=ee56427cee1f007839825d93d07fffd5a5e038c7#ee56427cee1f007839825d93d07fffd5a5e038c7"
dependencies = [
"quote 0.3.15",
"syn 0.11.11",
]
[[package]] [[package]]
name = "os_info" name = "os_info"
version = "3.4.0" version = "3.4.0"
@@ -2249,7 +2285,7 @@ checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -2287,7 +2323,7 @@ dependencies = [
"pest", "pest",
"pest_meta", "pest_meta",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -2318,7 +2354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -2405,7 +2441,7 @@ checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [ dependencies = [
"proc-macro-error-attr", "proc-macro-error-attr",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
"version_check", "version_check",
] ]
@@ -2417,15 +2453,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"version_check", "version_check",
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.50" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -2437,7 +2473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada" checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
"version_check", "version_check",
"yansi", "yansi",
@@ -2463,9 +2499,9 @@ checksum = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.18" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -2624,57 +2660,55 @@ dependencies = [
[[package]] [[package]]
name = "redis" name = "redis"
version = "0.19.0" version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/insertish/redis-rs?rev=1a41faf356fd21aebba71cea7eb7eb2653e5f0ef#1a41faf356fd21aebba71cea7eb7eb2653e5f0ef"
checksum = "1a6ddfecac9391fed21cce10e83c65fa4abafd77df05c98b1c647c65374ce9b3"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
"bytes 1.1.0", "bytes 1.1.0",
"combine", "combine",
"dtoa",
"futures-util", "futures-util",
"itoa 0.4.8", "itoa 1.0.2",
"percent-encoding", "percent-encoding",
"pin-project-lite 0.2.9", "pin-project-lite 0.2.9",
"sha1", "ryu",
"sha1_smol",
"tokio 1.18.2", "tokio 1.18.2",
"tokio-util 0.6.10", "tokio-util 0.7.2",
"url", "url",
] ]
[[package]] [[package]]
name = "redis" name = "redis"
version = "0.21.5" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a80b5f38d7f5a020856a0e16e40a9cfabf88ae8f0e4c2dcd8a3114c1e470852" checksum = "3ea8c51b5dc1d8e5fd3350ec8167f464ec0995e79f2e90a075b63371500d557f"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
"bytes 1.1.0", "bytes 1.1.0",
"combine", "combine",
"dtoa",
"futures-util", "futures-util",
"itoa 0.4.8", "itoa 1.0.2",
"percent-encoding", "percent-encoding",
"pin-project-lite 0.2.9", "pin-project-lite 0.2.9",
"sha1", "ryu",
"tokio 1.18.2", "tokio 1.18.2",
"tokio-util 0.6.10", "tokio-util 0.7.2",
"url", "url",
] ]
[[package]] [[package]]
name = "redis-kiss" name = "redis-kiss"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4f2f48fb776a308331c4a1ecb3a7b7b99f4d46c6beb2fc03db2da0f63fcad6" checksum = "58605cfd83a2146161de13bb253f24db25eef6919b35403fc90f508ef89bec92"
dependencies = [ dependencies = [
"bincode", "bincode",
"lazy_static", "lazy_static",
"mobc", "mobc",
"mobc-redis", "mobc-redis",
"redis 0.21.5", "redis 0.23.0",
"rmp-serde", "rmp-serde",
"serde", "serde",
"serde_json", "serde_json",
@@ -2705,7 +2739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a043824e29c94169374ac5183ac0ed43f5724dc4556b19568007486bd840fa1f" checksum = "a043824e29c94169374ac5183ac0ed43f5724dc4556b19568007486bd840fa1f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -2792,7 +2826,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.5.17" version = "0.5.19"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
@@ -2800,15 +2834,38 @@ dependencies = [
"log", "log",
"once_cell", "once_cell",
"querystring", "querystring",
"revolt-presence",
"revolt-quark", "revolt-quark",
"rmp-serde", "rmp-serde",
"serde", "serde",
"serde_json", "serde_json",
] ]
[[package]]
name = "revolt-database"
version = "0.0.2"
dependencies = [
"async-recursion",
"async-std",
"async-trait",
"authifier",
"bson",
"futures",
"log",
"mongodb",
"nanoid",
"revolt-permissions",
"revolt-result",
"revolt_optional_struct",
"rocket",
"schemars",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "revolt-delta" name = "revolt-delta"
version = "0.5.17" version = "0.5.19"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"async-std", "async-std",
@@ -2823,14 +2880,13 @@ dependencies = [
"linkify 0.6.0", "linkify 0.6.0",
"log", "log",
"lru", "lru",
"mobc",
"mobc-redis",
"nanoid", "nanoid",
"num_enum", "num_enum 0.5.7",
"once_cell", "once_cell",
"redis 0.21.5",
"regex", "regex",
"reqwest", "reqwest",
"revolt-database",
"revolt-models",
"revolt-quark", "revolt-quark",
"revolt_rocket_okapi", "revolt_rocket_okapi",
"rocket", "rocket",
@@ -2845,9 +2901,43 @@ dependencies = [
"vergen", "vergen",
] ]
[[package]]
name = "revolt-models"
version = "0.0.2"
dependencies = [
"revolt-database",
"revolt-presence",
"schemars",
"serde",
]
[[package]]
name = "revolt-permissions"
version = "0.0.2"
dependencies = [
"async-std",
"async-trait",
"auto_ops",
"num_enum 0.6.1",
"once_cell",
"schemars",
"serde",
]
[[package]]
name = "revolt-presence"
version = "0.0.2"
dependencies = [
"async-std",
"log",
"once_cell",
"rand 0.8.5",
"redis-kiss",
]
[[package]] [[package]]
name = "revolt-quark" name = "revolt-quark"
version = "0.5.17" version = "0.5.19"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"async-recursion", "async-recursion",
@@ -2870,14 +2960,17 @@ dependencies = [
"lru", "lru",
"mongodb", "mongodb",
"nanoid", "nanoid",
"num_enum", "num_enum 0.5.7",
"once_cell", "once_cell",
"optional_struct",
"pretty_env_logger", "pretty_env_logger",
"rand 0.8.5",
"redis-kiss", "redis-kiss",
"regex", "regex",
"reqwest", "reqwest",
"revolt-presence",
"revolt-result",
"revolt_okapi", "revolt_okapi",
"revolt_optional_struct",
"revolt_rocket_okapi", "revolt_rocket_okapi",
"rocket", "rocket",
"rocket_cors", "rocket_cors",
@@ -2892,6 +2985,14 @@ dependencies = [
"web-push", "web-push",
] ]
[[package]]
name = "revolt-result"
version = "0.0.2"
dependencies = [
"schemars",
"serde",
]
[[package]] [[package]]
name = "revolt_okapi" name = "revolt_okapi"
version = "0.9.1" version = "0.9.1"
@@ -2904,6 +3005,16 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "revolt_optional_struct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d342739658623fe9d72b42f7a36ba19094d1d9bf9b423c3e337ab203c15d53e"
dependencies = [
"quote 0.3.15",
"syn 0.11.11",
]
[[package]] [[package]]
name = "revolt_rocket_okapi" name = "revolt_rocket_okapi"
version = "0.9.1" version = "0.9.1"
@@ -2928,7 +3039,7 @@ checksum = "cc6620569d8ac8f0a1690fcca13f488503807a60e96ebf729749b59aca1dbef9"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"rocket_http", "rocket_http",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -3035,7 +3146,7 @@ dependencies = [
"glob", "glob",
"indexmap", "indexmap",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"rocket_http", "rocket_http",
"syn 1.0.107", "syn 1.0.107",
"unicode-xid 0.2.3", "unicode-xid 0.2.3",
@@ -3210,7 +3321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4d7e1b012cb3d9129567661a63755ea4b8a7386d339dc945ae187e403c6743" checksum = "af4d7e1b012cb3d9129567661a63755ea4b8a7386d339dc945ae187e403c6743"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"serde_derive_internals", "serde_derive_internals",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -3363,9 +3474,9 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.152" version = "1.0.160"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
@@ -3381,13 +3492,13 @@ dependencies = [
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.152" version = "1.0.160"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 2.0.15",
] ]
[[package]] [[package]]
@@ -3397,15 +3508,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.91" version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa 1.0.2", "itoa 1.0.2",
@@ -3443,7 +3554,7 @@ checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -3470,15 +3581,6 @@ dependencies = [
"digest 0.10.3", "digest 0.10.3",
] ]
[[package]]
name = "sha1"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
dependencies = [
"sha1_smol",
]
[[package]] [[package]]
name = "sha1_smol" name = "sha1_smol"
version = "1.0.0" version = "1.0.0"
@@ -3627,7 +3729,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
dependencies = [
"proc-macro2",
"quote 1.0.26",
"unicode-ident", "unicode-ident",
] ]
@@ -3699,7 +3812,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -3808,7 +3921,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -3944,7 +4057,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -4074,7 +4187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
] ]
@@ -4284,7 +4397,7 @@ dependencies = [
"lazy_static", "lazy_static",
"proc-macro-error", "proc-macro-error",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"regex", "regex",
"syn 1.0.107", "syn 1.0.107",
"validator_types", "validator_types",
@@ -4394,7 +4507,7 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -4417,7 +4530,7 @@ version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5"
dependencies = [ dependencies = [
"quote 1.0.18", "quote 1.0.26",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
] ]
@@ -4428,7 +4541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote 1.0.18", "quote 1.0.26",
"syn 1.0.107", "syn 1.0.107",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",

View File

@@ -1,2 +1,6 @@
[workspace] [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" }

View File

@@ -1,12 +1,32 @@
# Build Stage # Build Stage
FROM rustlang/rust:nightly-slim AS builder FROM --platform="${BUILDPLATFORM}" rustlang/rust:nightly-slim
USER 0:0 USER 0:0
WORKDIR /home/rust/src WORKDIR /home/rust/src
# Install build requirements ARG TARGETARCH
RUN apt-get update && apt-get install -y libssl-dev pkg-config
# 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 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 COPY crates ./crates
RUN cargo build --locked --release RUN sh /tmp/build-image-layer.sh apps

7
clippy.toml Normal file
View File

@@ -0,0 +1,7 @@
disallowed-methods = [
# Shouldn't need to access these directly
"revolt_database::models::bots::model::Bot::remove_field",
# Prefer to use Object::delete()
"revolt_database::models::bots::ops::AbstractBots::update_bot",
]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.5.17" version = "0.5.19"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -26,3 +26,6 @@ serde = "1.0.136"
futures = "0.3.21" futures = "0.3.21"
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] } async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] } async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
# core
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }

View File

@@ -1,10 +1,11 @@
# Build Stage # Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder FROM ghcr.io/revoltchat/base:latest AS builder
RUN cargo install --locked --path crates/bonfire
# Bundle Stage # Bundle Stage
FROM debian:buster-slim FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y ca-certificates RUN apt-get update && \
COPY --from=builder /usr/local/cargo/bin/revolt-bonfire ./ apt-get install -y ca-certificates && \
apt-get clean
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
EXPOSE 9000 EXPOSE 9000
CMD ["./revolt-bonfire"] CMD ["./revolt-bonfire"]

View File

@@ -1,7 +1,7 @@
use std::env; use std::env;
use async_std::net::TcpListener; use async_std::net::TcpListener;
use revolt_quark::presence::presence_clear_region; use revolt_presence::clear_region;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
@@ -18,7 +18,7 @@ async fn main() {
database::connect().await; database::connect().await;
// Clean up the current region information. // Clean up the current region information.
presence_clear_region(None).await; clear_region(None).await;
// Setup a TCP listener to accept WebSocket connections on. // Setup a TCP listener to accept WebSocket connections on.
// By default, we bind to port 9000 on all interfaces. // By default, we bind to port 9000 on all interfaces.

View File

@@ -1,6 +1,7 @@
use std::net::SocketAddr; use std::net::SocketAddr;
use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt}; use futures::{channel::oneshot, pin_mut, select, FutureExt, SinkExt, StreamExt, TryStreamExt};
use revolt_presence::{create_session, delete_session};
use revolt_quark::{ use revolt_quark::{
events::{ events::{
client::EventV1, client::EventV1,
@@ -8,7 +9,6 @@ use revolt_quark::{
state::{State, SubscriptionStateChange}, state::{State, SubscriptionStateChange},
}, },
models::{user::UserHint, User}, models::{user::UserHint, User},
presence::{presence_create_session, presence_delete_session},
redis_kiss, Database, 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(); let user_id = state.cache.user_id.clone();
// Create presence session. // Create presence session.
let (first_session, session_id) = let (first_session, session_id) = create_session(&user_id, 0).await;
presence_create_session(&user_id, 0).await;
// Notify socket we have authenticated. // Notify socket we have authenticated.
write write
@@ -225,7 +224,7 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
} }
// Clean up presence session. // 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 this was the last session, notify other users that we just went offline.
if last_session { if last_session {

View File

@@ -0,0 +1,53 @@
[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" }
# Utility
log = "0.4"
nanoid = "0.4.0"
# Serialisation
serde_json = "1"
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] }
# 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" }

View 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))
}
})
}
}

View 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()
}

View File

@@ -0,0 +1,14 @@
use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{Bot, User};
database_derived!(
/// Reference implementation
#[derive(Default)]
pub struct ReferenceDb {
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,
}
);

View 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
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View 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()
});
}
}

View 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<(), ()>;
}

View File

@@ -1,13 +1,20 @@
use crate::{AbstractMigrations, Result}; use crate::MongoDb;
use super::super::MongoDb; use super::AbstractMigrations;
mod init; mod init;
mod scripts; mod scripts;
#[async_trait] #[async_trait]
impl AbstractMigrations for MongoDb { 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."); info!("Migrating the database.");
let list = self let list = self
@@ -15,7 +22,7 @@ impl AbstractMigrations for MongoDb {
.await .await
.expect("Failed to fetch database names."); .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; scripts::migrate_database(self).await;
} else { } else {
init::create_database(self).await; init::create_database(self).await;

View File

@@ -1,9 +1,8 @@
use crate::r#impl::mongo::MongoDb;
use super::scripts::LATEST_REVISION; use super::scripts::LATEST_REVISION;
use mongodb::bson::doc; use crate::mongodb::bson::doc;
use mongodb::options::CreateCollectionOptions; use crate::mongodb::options::CreateCollectionOptions;
use crate::MongoDb;
pub async fn create_database(db: &MongoDb) { pub async fn create_database(db: &MongoDb) {
info!("Creating database."); info!("Creating database.");

View File

@@ -1,15 +1,15 @@
use std::{time::Duration, ops::BitXor}; use std::{ops::BitXor, time::Duration};
use bson::{Bson, DateTime}; use crate::{
use futures::StreamExt; mongodb::{
use mongodb::{ bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
bson::{doc, from_bson, from_document, to_document, Document}, options::FindOptions,
options::FindOptions, },
MongoDb,
}; };
use futures::StreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{r#impl::mongo::MongoDb, Permission, DEFAULT_PERMISSION_SERVER};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct MigrationInfo { struct MigrationInfo {
_id: i32, _id: i32,
@@ -504,7 +504,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
update.insert( update.insert(
"default_permissions", "default_permissions",
// Remove Send Message permission if it wasn't originally granted // Remove Send Message permission if it wasn't originally granted
DEFAULT_PERMISSION_SERVER.bitxor(if has_send { 0 } else { Permission::SendMessage as u64}) as i64, (4000323584).bitxor(if has_send { 0 } else { (1 << 22) as u64 }) as i64,
); );
if let Some(Bson::Document(mut roles)) = document.remove("roles") { if let Some(Bson::Document(mut roles)) = document.remove("roles") {
@@ -563,7 +563,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! { doc! {
"default_permissions": { "default_permissions": {
"a": 0_i64, "a": 0_i64,
"d": Permission::SendMessage as i64 "d": (1 << 22) as i64
} }
}, },
); );

View File

@@ -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(())
}
}

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,145 @@
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};
#[async_std::test]
async fn crud() {
database_test!(|db| async move {
let bot_id = "bot";
let user_id = "user";
let token = "my_token";
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())
});
}
}

View 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 {
/// 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>;
/// Insert new bot into the database
async fn insert_bot(&self, bot: &Bot) -> Result<()>;
/// 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<()>;
/// 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>;
}

View 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 {
/// 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))
}
/// Insert new bot into the database
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
query!(self, insert_one, COL, &bot).map(|_| ())
}
/// 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(|_| ())
}
/// 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)
}
}
impl IntoDocumentPath for FieldsBot {
fn as_path(&self) -> Option<&'static str> {
match self {
FieldsBot::InteractionsURL => Some("interactions_url"),
FieldsBot::Token => None,
}
}
}

View 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 {
/// 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))
}
/// 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(())
}
}
/// 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))
}
}
/// 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())
}
}

View File

@@ -0,0 +1,3 @@
mod model;
pub use model::*;

View File

@@ -0,0 +1,59 @@
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,
}
);

View File

@@ -0,0 +1,30 @@
mod admin_migrations;
mod bots;
mod files;
mod users;
pub use admin_migrations::*;
pub use bots::*;
pub use files::*;
pub use users::*;
use crate::{Database, MongoDb, ReferenceDb};
pub trait AbstractDatabase:
Sync + Send + admin_migrations::AbstractMigrations + bots::AbstractBots + 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,
}
}
}

View 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::*;

View File

@@ -0,0 +1,99 @@
use crate::File;
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")]
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>,
}
/// Bot information for if the user is a bot
pub struct BotInformation {
/// Id of the owner of this bot
pub owner: String,
}
);

View File

@@ -0,0 +1,12 @@
use revolt_result::Result;
use crate::User;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractUsers: Sync + Send {
/// Fetch a user from the database
async fn fetch_user(&self, id: &str) -> Result<User>;
}

View File

@@ -0,0 +1,16 @@
use revolt_result::Result;
use crate::MongoDb;
use crate::User;
use super::AbstractUsers;
static COL: &str = "bots";
#[async_trait]
impl AbstractUsers for MongoDb {
/// 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))
}
}

View File

@@ -0,0 +1,18 @@
use revolt_result::Result;
use crate::ReferenceDb;
use crate::User;
use super::AbstractUsers;
#[async_trait]
impl AbstractUsers for ReferenceDb {
/// 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))
}
}

View 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))
}
}
}

View File

@@ -0,0 +1,2 @@
pub mod permissions;
pub mod reference;

View File

@@ -0,0 +1,205 @@
use std::borrow::Cow;
use revolt_permissions::{
calculate_user_permissions, ChannelType, Override, PermissionQuery, RelationshipStatus,
};
use crate::{Database, User};
/// Permissions calculator
pub struct PermissionCalculator<'a> {
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)
}

View 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()
})
}
}

View 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 }

View 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
}

View 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,
}
}
}

View 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,
}
}
}

View File

@@ -0,0 +1,7 @@
mod bots;
mod files;
mod users;
pub use bots::*;
pub use files::*;
pub use users::*;

View 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,
}
}
}

View 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 }

View 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(),
}
}

View 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;

View 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,
)
});

View 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()
}
}

View File

@@ -0,0 +1,56 @@
/// Representation of a single permission override
#[derive(Debug, Clone, Copy)]
#[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)]
#[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())
}
}*/

View 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 });

View 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
}
}
}

View 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);
}

View 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"

View 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, &REGION_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, &REGION_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())
}
}

View 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");
}

View 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 }

View 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));
}
}

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.5.17" version = "0.5.19"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -43,11 +43,6 @@ async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"]
# internal util # internal util
lettre = "0.10.0-alpha.4" 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 # web
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] } rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
rocket_empty = { version = "0.1.1", features = ["schema"] } rocket_empty = { version = "0.1.1", features = ["schema"] }
@@ -60,5 +55,9 @@ revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
# quark # quark
revolt-quark = { path = "../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] [build-dependencies]
vergen = "7.5.0" vergen = "7.5.0"

View File

@@ -1,11 +1,12 @@
# Build Stage # Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder FROM ghcr.io/revoltchat/base:latest AS builder
RUN cargo install --locked --path crates/delta
# Bundle Stage # Bundle Stage
FROM debian:buster-slim FROM debian:bullseye-slim
RUN apt-get update && apt-get install -y ca-certificates RUN apt-get update && \
COPY --from=builder /usr/local/cargo/bin/revolt-delta ./ apt-get install -y ca-certificates && \
apt-get clean
COPY --from=builder /home/rust/src/target/release/revolt-delta ./
EXPOSE 8000 EXPOSE 8000
ENV ROCKET_ADDRESS 0.0.0.0 ENV ROCKET_ADDRESS 0.0.0.0

View File

@@ -23,15 +23,18 @@ async fn rocket() -> _ {
revolt_quark::variables::delta::preflight_checks(); revolt_quark::variables::delta::preflight_checks();
// Setup database // Setup database
let db = DatabaseInfo::Auto.connect().await.unwrap(); let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
db.migrate_database().await.unwrap(); db.migrate_database().await.unwrap();
// Legacy database setup from quark
let legacy_db = DatabaseInfo::Auto.connect().await.unwrap();
// Setup Authifier event channel // Setup Authifier event channel
let (sender, receiver) = unbounded(); let (sender, receiver) = unbounded();
// Setup Authifier // Setup Authifier
let authifier = Authifier { let authifier = Authifier {
database: db.clone().into(), database: legacy_db.clone().into(),
config: revolt_quark::util::authifier::config(), config: revolt_quark::util::authifier::config(),
event_channel: Some(sender), event_channel: Some(sender),
}; };
@@ -53,7 +56,7 @@ async fn rocket() -> _ {
}); });
// Launch background task workers // Launch background task workers
async_std::task::spawn(revolt_quark::tasks::start_workers(db.clone())); async_std::task::spawn(revolt_quark::tasks::start_workers(legacy_db.clone()));
// Configure CORS // Configure CORS
let cors = revolt_quark::web::cors::new(); let cors = revolt_quark::web::cors::new();
@@ -66,6 +69,7 @@ async fn rocket() -> _ {
.mount("/swagger/", revolt_quark::web::swagger::routes()) .mount("/swagger/", revolt_quark::web::swagger::routes())
.manage(authifier) .manage(authifier)
.manage(db) .manage(db)
.manage(legacy_db)
.manage(cors.clone()) .manage(cors.clone())
.attach(revolt_quark::web::ratelimiter::RatelimitFairing) .attach(revolt_quark::web::ratelimiter::RatelimitFairing)
.attach(cors) .attach(cors)

View File

@@ -1,36 +1,33 @@
use revolt_quark::{ use revolt_database::{util::reference::Reference, Database};
models::{Bot, User}, use revolt_models::v0::FetchBotResponse;
Db, Error, Ref, Result, use revolt_quark::{models::User, Error, Result};
}; use rocket::{serde::json::Json, State};
use rocket::serde::json::Json;
use serde::Serialize;
/// # Bot Response
#[derive(Serialize, JsonSchema)]
pub struct BotResponse {
/// Bot object
bot: Bot,
/// User object
user: User,
}
/// # Fetch Bot /// # Fetch Bot
/// ///
/// Fetch details of a bot you own by its id. /// Fetch details of a bot you own by its id.
#[openapi(tag = "Bots")] #[openapi(tag = "Bots")]
#[get("/<target>")] #[get("/<bot>")]
pub async fn fetch_bot(db: &Db, user: User, target: Ref) -> Result<Json<BotResponse>> { pub async fn fetch_bot(
db: &State<Database>,
user: User,
bot: Reference,
) -> Result<Json<FetchBotResponse>> {
if user.bot.is_some() { if user.bot.is_some() {
return Err(Error::IsBot); return Err(Error::IsBot);
} }
let bot = target.as_bot(db).await?; let bot = bot.as_bot(db).await.map_err(Error::from_core)?;
if bot.owner != user.id { if bot.owner != user.id {
return Err(Error::NotFound); return Err(Error::NotFound);
} }
Ok(Json(BotResponse { Ok(Json(FetchBotResponse {
user: db.fetch_user(&bot.id).await?.foreign(), user: revolt_models::v0::User::from(
bot, db.fetch_user(&bot.id).await.map_err(Error::from_core)?,
None,
)
.await,
bot: bot.into(),
})) }))
} }

View File

@@ -1,44 +1,25 @@
use revolt_quark::{ use revolt_database::Database;
models::{File, User}, use revolt_models::v0::PublicBot;
Db, Error, Ref, Result, use revolt_quark::{models::User, Error, Ref, Result};
};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Deserialize, Serialize}; use rocket::State;
/// # Public Bot
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct PublicBot {
/// Bot Id
#[serde(rename = "_id")]
id: String,
/// Bot Username
username: String,
/// Profile Avatar
#[serde(skip_serializing_if = "Option::is_none")]
avatar: Option<File>,
/// Profile Description
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
}
/// # Fetch Public Bot /// # Fetch Public Bot
/// ///
/// Fetch details of a public (or owned) bot by its id. /// Fetch details of a public (or owned) bot by its id.
#[openapi(tag = "Bots")] #[openapi(tag = "Bots")]
#[get("/<target>/invite")] #[get("/<target>/invite")]
pub async fn fetch_public_bot(db: &Db, user: Option<User>, target: Ref) -> Result<Json<PublicBot>> { pub async fn fetch_public_bot(
let bot = target.as_bot(db).await?; db: &State<Database>,
user: Option<User>,
target: Ref,
) -> Result<Json<PublicBot>> {
let bot = db.fetch_bot(&target.id).await.map_err(Error::from_core)?;
if !bot.public && user.map_or(true, |x| x.id != bot.owner) { if !bot.public && user.map_or(true, |x| x.id != bot.owner) {
return Err(Error::NotFound); return Err(Error::NotFound);
} }
let user = db.fetch_user(&bot.id).await?; let user = db.fetch_user(&bot.id).await.map_err(Error::from_core)?;
Ok(Json(PublicBot::from(bot, user)))
Ok(Json(PublicBot {
id: bot.id,
username: user.username,
avatar: user.avatar,
description: user.profile.and_then(|p| p.content),
}))
} }

View File

@@ -1,6 +1,6 @@
use revolt_quark::{ use revolt_quark::{
models::{Invite, User}, models::{Invite, User},
perms, Db, Error, Permission, Ref, Result, perms, Db, Permission, Ref, Result,
}; };
use rocket::serde::json::Json; use rocket::serde::json::Json;
@@ -11,10 +11,6 @@ use rocket::serde::json::Json;
#[openapi(tag = "Server Members")] #[openapi(tag = "Server Members")]
#[get("/<target>/invites")] #[get("/<target>/invites")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<Invite>>> { pub async fn req(db: &Db, user: User, target: Ref) -> Result<Json<Vec<Invite>>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let server = target.as_server(db).await?; let server = target.as_server(db).await?;
perms(&user) perms(&user)
.server(&server) .server(&server)

View File

@@ -44,6 +44,10 @@ pub async fn req(
user: User, user: User,
info: Json<DataCreateServer>, info: Json<DataCreateServer>,
) -> Result<Json<CreateServerResponse>> { ) -> Result<Json<CreateServerResponse>> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let info = info.into_inner(); let info = info.into_inner();
info.validate() info.validate()
.map_err(|error| Error::FailedValidation { error })?; .map_err(|error| Error::FailedValidation { error })?;

View File

@@ -1,8 +1,8 @@
[package] [package]
name = "revolt-quark" name = "revolt-quark"
version = "0.5.17" version = "0.5.19"
license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -26,10 +26,10 @@ default = [ "test" ]
[dependencies] [dependencies]
# Serialisation # Serialisation
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
validator = { version = "0.14", features = ["derive"] } validator = { version = "0.14", features = ["derive"] }
iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] } iso8601-timestamp = { version = "0.1.8", features = ["schema", "bson"] }
optional_struct = { git = "https://github.com/insertish/OptionalStruct", rev = "ee56427cee1f007839825d93d07fffd5a5e038c7" }
# Formats # Formats
bincode = "1.3.3" bincode = "1.3.3"
@@ -42,7 +42,7 @@ revolt_okapi = "0.9.1"
revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] } revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
# Databases # Databases
redis-kiss = { version = "0.1.3" } redis-kiss = { version = "0.1.4" }
mongodb = { optional = true, version = "2.1.0", default-features = false } mongodb = { optional = true, version = "2.1.0", default-features = false }
# Async # Async
@@ -57,6 +57,7 @@ log = "0.4.14"
pretty_env_logger = "0.4.0" pretty_env_logger = "0.4.0"
# Util # Util
rand = "0.8.5"
ulid = "0.5.0" ulid = "0.5.0"
regex = "1.5.5" regex = "1.5.5"
nanoid = "0.4.0" nanoid = "0.4.0"
@@ -88,3 +89,7 @@ authifier = { version = "1.0.7", features = [ "async-std-runtime" ] }
# Sentry # Sentry
sentry = "0.25.0" sentry = "0.25.0"
# Core
revolt-result = { path = "../core/result", features = [ "serde", "schemas" ] }
revolt-presence = { path = "../core/presence", features = [ "redis-is-patched" ] }

View File

@@ -1,34 +0,0 @@
use revolt_quark::models::user::PartialUser;
use revolt_quark::presence::{
presence_create_session, presence_delete_session, presence_filter_online, presence_is_online,
};
use revolt_quark::*;
#[async_std::main]
async fn main() {
let db = DatabaseInfo::Dummy.connect().await.unwrap();
let sus = PartialUser {
username: Some("neat".into()),
..Default::default()
};
db.update_user("user id", &sus, vec![]).await.unwrap();
dbg!(presence_create_session("entry", 0).await);
dbg!(presence_is_online("entry").await);
dbg!(presence_filter_online(&["a".into(), "b".into(), "entry".into()]).await);
dbg!(presence_delete_session("entry", 0).await);
dbg!(presence_is_online("entry").await);
dbg!(presence_create_session("entry", 0).await);
dbg!(presence_create_session("entry", 0).await);
dbg!(presence_delete_session("entry", 0).await);
dbg!(presence_is_online("entry").await);
dbg!(presence_delete_session("entry", 1).await);
dbg!(presence_is_online("entry").await);
// __set_key("dietz", vec![0xFF]).await;
// dbg!(presence_filter_online(&["dietz".into(), "nuts".into()]).await);
}

View File

@@ -7,11 +7,11 @@ use crate::{
user::{PartialUser, Presence, RelationshipStatus}, user::{PartialUser, Presence, RelationshipStatus},
Channel, Member, User, Channel, Member, User,
}, },
perms, perms, Database, Permission, Result,
presence::presence_filter_online,
Database, Permission, Result,
}; };
use revolt_presence::filter_online;
use super::{ use super::{
client::EventV1, client::EventV1,
state::{Cache, State}, state::{Cache, State},
@@ -99,7 +99,7 @@ impl State {
let mut user = self.clone_user(); let mut user = self.clone_user();
// Find all relationships to the user. // Find all relationships to the user.
let mut user_ids: Vec<String> = user let mut user_ids: HashSet<String> = user
.relations .relations
.as_ref() .as_ref()
.map(|arr| arr.iter().map(|x| x.id.to_string()).collect()) .map(|arr| arr.iter().map(|x| x.id.to_string()).collect())
@@ -128,14 +128,14 @@ impl State {
for channel in &channels { for channel in &channels {
match channel { match channel {
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => { Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
user_ids.append(&mut recipients.clone()); user_ids.extend(&mut recipients.clone().into_iter());
} }
_ => {} _ => {}
} }
} }
// Fetch presence data for known users. // Fetch presence data for known users.
let online_ids = presence_filter_online(&user_ids).await; let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
user.online = Some(true); user.online = Some(true);
// Fetch user data. // Fetch user data.

View File

@@ -1,11 +0,0 @@
use crate::{AbstractMigrations, Result};
use super::super::DummyDb;
#[async_trait]
impl AbstractMigrations for DummyDb {
async fn migrate_database(&self) -> Result<()> {
info!("Migrating the database.");
Ok(())
}
}

View File

@@ -1,7 +1,6 @@
use crate::AbstractDatabase; use crate::AbstractDatabase;
pub mod admin { pub mod admin {
pub mod migrations;
pub mod stats; pub mod stats;
} }

View File

@@ -1,5 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use revolt_presence::filter_online;
use serde_json::json; use serde_json::json;
use ulid::Ulid; use ulid::Ulid;
use validator::Validate; use validator::Validate;
@@ -14,7 +15,6 @@ use crate::{
Channel, Emoji, Message, User, Channel, Emoji, Message, User,
}, },
permissions::PermissionCalculator, permissions::PermissionCalculator,
presence::presence_filter_online,
tasks::ack::AckEvent, tasks::ack::AckEvent,
types::{ types::{
january::{Embed, Text}, january::{Embed, Text},
@@ -79,7 +79,7 @@ impl Message {
Channel::DirectMessage { recipients, .. } Channel::DirectMessage { recipients, .. }
| Channel::Group { recipients, .. } => { | Channel::Group { recipients, .. } => {
target_ids = (&recipients.iter().cloned().collect::<HashSet<String>>() target_ids = (&recipients.iter().cloned().collect::<HashSet<String>>()
- &presence_filter_online(recipients).await) - &filter_online(recipients).await)
.into_iter() .into_iter()
.collect::<Vec<String>>(); .collect::<Vec<String>>();
} }
@@ -313,12 +313,12 @@ impl IntoUsers for Message {
impl IntoUsers for Vec<Message> { impl IntoUsers for Vec<Message> {
fn get_user_ids(&self) -> Vec<String> { fn get_user_ids(&self) -> Vec<String> {
let mut ids = vec![]; let mut ids = HashSet::new();
for message in self { for message in self {
ids.append(&mut message.get_user_ids()); ids.extend(&mut message.get_user_ids().into_iter());
} }
ids ids.into_iter().collect()
} }
} }

View File

@@ -1,9 +1,5 @@
//! Database agnostic implementations. //! Database agnostic implementations.
pub mod admin {
pub mod migrations;
}
pub mod media { pub mod media {
pub mod attachment; pub mod attachment;
pub mod emoji; pub mod emoji;

View File

@@ -4,11 +4,11 @@ use crate::models::user::{
}; };
use crate::permissions::defn::UserPerms; use crate::permissions::defn::UserPerms;
use crate::permissions::r#impl::user::get_relationship; use crate::permissions::r#impl::user::get_relationship;
use crate::presence::presence_filter_online;
use crate::{perms, Database, Error, Result}; use crate::{perms, Database, Error, Result};
use futures::try_join; use futures::try_join;
use impl_ops::impl_op_ex_commutative; use impl_ops::impl_op_ex_commutative;
use revolt_presence::filter_online;
use std::ops; use std::ops;
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 }); impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
@@ -98,7 +98,7 @@ impl User {
/// Fetch foreign users by a list of IDs /// Fetch foreign users by a list of IDs
pub async fn fetch_foreign_users(db: &Database, user_ids: &[String]) -> Result<Vec<User>> { pub async fn fetch_foreign_users(db: &Database, user_ids: &[String]) -> Result<Vec<User>> {
let online_ids = presence_filter_online(user_ids).await; let online_ids = filter_online(user_ids).await;
Ok(db Ok(db
.fetch_users(user_ids) .fetch_users(user_ids)

View File

@@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize};
use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result}; use crate::{util::manipulation::prefix_keys, AbstractDatabase, Error, Result};
pub mod admin { pub mod admin {
pub mod migrations;
pub mod stats; pub mod stats;
} }

View File

@@ -9,7 +9,7 @@ extern crate log;
#[macro_use] #[macro_use]
extern crate impl_ops; extern crate impl_ops;
#[macro_use] #[macro_use]
extern crate optional_struct; extern crate revolt_optional_struct;
#[macro_use] #[macro_use]
extern crate bitfield; extern crate bitfield;
#[macro_use] #[macro_use]
@@ -22,7 +22,6 @@ pub use redis_kiss;
pub mod events; pub mod events;
pub mod r#impl; pub mod r#impl;
pub mod models; pub mod models;
pub mod presence;
pub mod tasks; pub mod tasks;
pub mod types; pub mod types;
pub mod util; pub mod util;

View File

@@ -1,11 +0,0 @@
use serde::{Deserialize, Serialize};
/// Document representing migration information
#[derive(Serialize, Deserialize)]
pub struct MigrationInfo {
/// Unique Id
#[serde(rename = "_id")]
id: i32,
/// Current database revision
revision: i32,
}

View File

@@ -1,5 +1,4 @@
mod admin { mod admin {
pub mod migrations;
pub mod simple; pub mod simple;
pub mod stats; pub mod stats;
} }
@@ -48,7 +47,6 @@ pub use channel_invite::Invite;
pub use channel_unread::ChannelUnread; pub use channel_unread::ChannelUnread;
pub use emoji::Emoji; pub use emoji::Emoji;
pub use message::Message; pub use message::Message;
pub use migrations::MigrationInfo;
pub use report::Report; pub use report::Report;
pub use server::Server; pub use server::Server;
pub use server_ban::ServerBan; pub use server_ban::ServerBan;

View File

@@ -1,64 +0,0 @@
use std::env;
use serde::{Deserialize, Serialize};
use once_cell::sync::Lazy;
pub static REGION_ID: Lazy<u16> = Lazy::new(|| env::var("REGION_ID")
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap());
pub static REGION_KEY: Lazy<String> = Lazy::new(|| format!("region{}", &*REGION_ID));
/// Compact presence information for a user
#[derive(Serialize, Deserialize, Debug)]
pub struct PresenceEntry {
/// Region this session exists in
///
/// We can have up to 65535 regions
pub region_id: u16,
/// Unique session ID
pub session_id: u8,
/// Known flags about session
pub flags: u8,
}
impl PresenceEntry {
/// Create a new presence entry from a given session ID and known flags
pub fn from(session_id: u8, flags: u8) -> Self {
Self {
region_id: *REGION_ID,
session_id,
flags,
}
}
}
pub trait PresenceOp {
/// Find next available session ID
fn find_next_id(&self) -> u8;
}
impl PresenceOp for Vec<PresenceEntry> {
fn find_next_id(&self) -> u8 {
// O(n^2) scan algorithm
// should be relatively fast at low numbers anyways
for i in 0..255 {
let mut found = false;
for entry in self {
if entry.session_id == i {
found = true;
break;
}
}
if !found {
return i;
}
}
255
}
}

View File

@@ -1,162 +0,0 @@
use std::collections::HashSet;
use redis_kiss::{get_connection, AsyncCommands};
mod entry;
mod operations;
use entry::{PresenceEntry, PresenceOp};
use operations::{
__add_to_set_sessions, __delete_key_presence_entry, __get_key_presence_entry,
__get_set_sessions, __remove_from_set_sessions, __set_key_presence_entry,
};
use crate::presence::operations::__delete_set_sessions;
use self::entry::REGION_KEY;
/// Create a new presence session, returns the ID of this session
pub async fn presence_create_session(user_id: &str, flags: u8) -> (bool, u8) {
info!("Creating a presence session for {user_id} with flags {flags}");
// Try to find the presence entry for this user.
let mut conn = get_connection().await.unwrap();
let mut entry: Vec<PresenceEntry> = __get_key_presence_entry(&mut conn, user_id)
.await
.unwrap_or_default();
// Return whether this was the first session.
let was_empty = entry.is_empty();
info!("User ID {} just came online.", &user_id);
// Generate session ID and push new entry.
let session_id = entry.find_next_id();
entry.push(PresenceEntry::from(session_id, flags));
__set_key_presence_entry(&mut conn, user_id, entry).await;
// Add to region set in case of failure.
__add_to_set_sessions(&mut conn, &REGION_KEY, user_id, session_id).await;
(was_empty, session_id)
}
/// Delete existing presence session
pub async fn presence_delete_session(user_id: &str, session_id: u8) -> bool {
presence_delete_session_internal(user_id, session_id, false).await
}
/// Delete existing presence session (but also choose whether to skip region)
async fn presence_delete_session_internal(
user_id: &str,
session_id: u8,
skip_region: bool,
) -> bool {
info!("Deleting presence session for {user_id} with id {session_id}");
// Return whether this was the last session.
let mut is_empty = false;
// Only continue if we can actually find one.
let mut conn = get_connection().await.unwrap();
let entry: Option<Vec<PresenceEntry>> = __get_key_presence_entry(&mut conn, user_id).await;
if let Some(entry) = entry {
let entries = entry
.into_iter()
.filter(|x| x.session_id != session_id)
.collect::<Vec<PresenceEntry>>();
// If entry is empty, then just delete it.
if entries.is_empty() {
__delete_key_presence_entry(&mut conn, user_id).await;
is_empty = true;
} else {
__set_key_presence_entry(&mut conn, user_id, entries).await;
}
// Remove from region set.
if !skip_region {
__remove_from_set_sessions(&mut conn, &REGION_KEY, user_id, session_id).await;
}
}
if is_empty {
info!("User ID {} just went offline.", &user_id);
}
is_empty
}
/// Check whether a given user ID is online
pub async fn presence_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
pub async fn presence_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;
}
// 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 presence_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 {
let data: Vec<Option<Vec<u8>>> = conn.get(user_ids).await.unwrap_or_default();
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].is_some() {
set.insert(user_ids[i].to_string());
}
}
}
set
}
/// Reset any stale presence data
pub async fn presence_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_sessions(&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() {
presence_delete_session_internal(user_id, session_id, true).await;
}
}
}
// Then clear the set in Redis.
__delete_set_sessions(&mut conn, region_id).await;
info!("Clean up complete.");
}
}

View File

@@ -1,57 +0,0 @@
use redis_kiss::{AsyncCommands, Conn};
use super::entry::PresenceEntry;
/// Set presence entry by given ID
pub async fn __set_key_presence_entry(conn: &mut Conn, id: &str, data: Vec<PresenceEntry>) {
let _: Option<()> = conn.set(id, bincode::serialize(&data).unwrap()).await.ok();
}
/// Delete presence entry by given ID
pub async fn __delete_key_presence_entry(conn: &mut Conn, id: &str) {
let _: Option<()> = conn.del(id).await.ok();
}
/// Get presence entry by given ID
pub async fn __get_key_presence_entry(conn: &mut Conn, id: &str) -> Option<Vec<PresenceEntry>> {
conn.get::<_, Option<Vec<u8>>>(id)
.await
.unwrap()
.map(|entry| bincode::deserialize(&entry[..]).unwrap())
}
/// Add to region session set
pub async fn __add_to_set_sessions(
conn: &mut Conn,
region_id: &str,
user_id: &str,
session_id: u8,
) {
let _: Option<()> = conn
.sadd(region_id, format!("{user_id}:{session_id}"))
.await
.ok();
}
/// Remove from region session set
pub async fn __remove_from_set_sessions(
conn: &mut Conn,
region_id: &str,
user_id: &str,
session_id: u8,
) {
let _: Option<()> = conn
.srem(region_id, format!("{user_id}:{session_id}"))
.await
.ok();
}
/// Get region session set as list
pub async fn __get_set_sessions(conn: &mut Conn, region_id: &str) -> Vec<String> {
conn.smembers::<_, Vec<String>>(region_id).await.unwrap()
}
/// Delete region session set
pub async fn __delete_set_sessions(conn: &mut Conn, region_id: &str) {
conn.del::<_, ()>(region_id).await.unwrap();
}

View File

@@ -1,6 +0,0 @@
use crate::Result;
#[async_trait]
pub trait AbstractMigrations: Sync + Send {
async fn migrate_database(&self) -> Result<()>;
}

View File

@@ -1,5 +1,4 @@
mod admin { mod admin {
pub mod migrations;
pub mod stats; pub mod stats;
} }
@@ -33,7 +32,6 @@ mod safety {
pub mod snapshot; pub mod snapshot;
} }
pub use admin::migrations::AbstractMigrations;
pub use admin::stats::AbstractStats; pub use admin::stats::AbstractStats;
pub use media::attachment::AbstractAttachment; pub use media::attachment::AbstractAttachment;
@@ -59,7 +57,6 @@ pub use safety::snapshot::AbstractSnapshot;
pub trait AbstractDatabase: pub trait AbstractDatabase:
Sync Sync
+ Send + Send
+ AbstractMigrations
+ AbstractStats + AbstractStats
+ AbstractAttachment + AbstractAttachment
+ AbstractEmoji + AbstractEmoji

View File

@@ -14,7 +14,7 @@ pub fn setup_logging(release: &'static str) -> sentry::ClientInitGuard {
info!("Starting {release}"); info!("Starting {release}");
sentry::init(( sentry::init((
"https://62fd0e02c5354905b4e286757f4beb16@sentry.insert.moe/4", "https://d1d2a6f15c6245a987c532bbbcb30a04@glitchtip.insert.moe/2",
sentry::ClientOptions { sentry::ClientOptions {
release: Some(release.into()), release: Some(release.into()),
..Default::default() ..Default::default()

View File

@@ -1,11 +1,13 @@
use futures::future::join; use futures::future::join;
use revolt_presence::is_online;
use rocket::request::FromParam; use rocket::request::FromParam;
use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook, Report}; use crate::models::{
use crate::presence::presence_is_online; Bot, Channel, Emoji, Invite, Member, Message, Report, Server, ServerBan, User, Webhook
};
use crate::{Database, Error, Result}; use crate::{Database, Error, Result};
/// Reference to some object in the database /// Reference to some object in the database
@@ -23,7 +25,7 @@ impl Ref {
/// Fetch user from Ref /// Fetch user from Ref
pub async fn as_user(&self, db: &Database) -> Result<User> { pub async fn as_user(&self, db: &Database) -> Result<User> {
let (user, online) = join(db.fetch_user(&self.id), presence_is_online(&self.id)).await; let (user, online) = join(db.fetch_user(&self.id), is_online(&self.id)).await;
let mut user = user?; let mut user = user?;
user.online = Some(online); user.online = Some(online);
Ok(user) Ok(user)

View File

@@ -19,6 +19,12 @@ pub enum Error {
/// This error was not labeled :( /// This error was not labeled :(
LabelMe, LabelMe,
/// Core crate error
Core {
#[serde(flatten)]
error: revolt_result::Error,
},
// ? Onboarding related errors // ? Onboarding related errors
AlreadyOnboarded, AlreadyOnboarded,
@@ -39,13 +45,13 @@ pub enum Error {
CannotEditMessage, CannotEditMessage,
CannotJoinCall, CannotJoinCall,
TooManyAttachments { TooManyAttachments {
max: usize max: usize,
}, },
TooManyReplies { TooManyReplies {
max: usize max: usize,
}, },
TooManyChannels { TooManyChannels {
max: usize max: usize,
}, },
EmptyMessage, EmptyMessage,
PayloadTooLarge, PayloadTooLarge,
@@ -64,10 +70,10 @@ pub enum Error {
max: usize, max: usize,
}, },
TooManyEmoji { TooManyEmoji {
max: usize max: usize,
}, },
TooManyRoles { TooManyRoles {
max: usize max: usize,
}, },
// ? Bot related errors // ? Bot related errors
@@ -135,6 +141,11 @@ impl Error {
error: validation_error, error: validation_error,
}) })
} }
/// Create a error from core error
pub fn from_core(error: revolt_result::Error) -> Error {
Error::Core { error }
}
} }
/// Result type with custom Error /// Result type with custom Error
@@ -145,6 +156,7 @@ impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
let status = match self { let status = match self {
Error::LabelMe => Status::InternalServerError, Error::LabelMe => Status::InternalServerError,
Error::Core { .. } => Status::InternalServerError,
Error::AlreadyOnboarded => Status::Forbidden, Error::AlreadyOnboarded => Status::Forbidden,

View File

@@ -1,5 +1,11 @@
version: "3.3" version: "3.3"
services: services:
# Redis
redis:
image: eqalpha/keydb
ports:
- "6379:6379"
# MongoDB # MongoDB
database: database:
image: mongo image: mongo

6
justfile Normal file
View File

@@ -0,0 +1,6 @@
publish:
cargo publish --package revolt-result
cargo publish --package revolt-permissions
cargo publish --package revolt-database
cargo publish --package revolt-presence
cargo publish --package revolt-models

View File

@@ -0,0 +1,62 @@
#!/bin/sh
set -eu
case "${TARGETARCH}" in
"amd64")
LINKER_NAME="x86_64-linux-gnu-gcc"
LINKER_PACKAGE="gcc-x86-64-linux-gnu"
BUILD_TARGET="x86_64-unknown-linux-gnu" ;;
"arm64")
LINKER_NAME="aarch64-linux-gnu-gcc"
LINKER_PACKAGE="gcc-aarch64-linux-gnu"
BUILD_TARGET="aarch64-unknown-linux-gnu" ;;
esac
tools() {
apt-get install -y "${LINKER_PACKAGE}"
rustup target add "${BUILD_TARGET}"
}
deps() {
mkdir -p \
crates/bonfire/src \
crates/delta/src \
crates/quark/src \
crates/core/database/src \
crates/core/models/src \
crates/core/permissions/src \
crates/core/presence/src \
crates/core/result/src
echo 'fn main() { panic!("stub"); }' |
tee crates/bonfire/src/main.rs |
tee crates/delta/src/main.rs
echo '' |
tee crates/quark/src/lib.rs |
tee crates/core/database/src/lib.rs |
tee crates/core/models/src/lib.rs |
tee crates/core/permissions/src/lib.rs |
tee crates/core/presence/src/lib.rs |
tee crates/core/result/src/lib.rs
cargo build --locked --release --target "${BUILD_TARGET}"
}
apps() {
touch -am \
crates/bonfire/src/main.rs \
crates/delta/src/main.rs \
crates/quark/src/lib.rs \
crates/core/database/src/lib.rs \
crates/core/models/src/lib.rs \
crates/core/permissions/src/lib.rs \
crates/core/presence/src/lib.rs \
crates/core/result/src/lib.rs
cargo build --locked --release --target "${BUILD_TARGET}"
mv target _target && mv _target/"${BUILD_TARGET}" target
}
export RUSTFLAGS="-C linker=${LINKER_NAME}"
export PKG_CONFIG_ALLOW_CROSS="1"
export PKG_CONFIG_PATH="/usr/lib/pkgconfig:/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig"
"$@"