Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdc898c50b | ||
|
|
03656e99a3 | ||
|
|
b0313b8acb | ||
|
|
252a116766 | ||
|
|
5120b071d1 |
No files matched your search
@@ -1,2 +0,0 @@
|
|||||||
[profile.default]
|
|
||||||
slow-timeout = { period = "3s", terminate-after = 2 }
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
Dockerfile
|
|
||||||
target
|
target
|
||||||
.mongo
|
.mongo
|
||||||
.env
|
.env
|
||||||
@@ -59,9 +59,6 @@ REVOLT_UNSAFE_NO_EMAIL=1
|
|||||||
## Application Settings
|
## Application Settings
|
||||||
##
|
##
|
||||||
|
|
||||||
# Whether to enable staging only features
|
|
||||||
REVOLT_IS_STAGING=1
|
|
||||||
|
|
||||||
# Whether to only allow users to sign up if they have an invite code
|
# Whether to only allow users to sign up if they have an invite code
|
||||||
REVOLT_INVITE_ONLY=0
|
REVOLT_INVITE_ONLY=0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ko_fi: insertish
|
||||||
|
custom: https://insrt.uk/donate
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
name: "CLA Assistant"
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened,closed,synchronize]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
CLAssistant:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: "CLA Assistant"
|
|
||||||
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
|
|
||||||
# Beta Release
|
|
||||||
uses: cla-assistant/github-action@v2.1.3-beta
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# the below token should have repo scope and must be manually added by you in the repository's secret
|
|
||||||
PERSONAL_ACCESS_TOKEN : ${{ secrets.PAT }}
|
|
||||||
with:
|
|
||||||
path-to-signatures: 'signatures/version1/cla.json'
|
|
||||||
path-to-document: 'https://github.com/revoltchat/cla/blob/master/CLA.md' # e.g. a CLA or a DCO document
|
|
||||||
# branch should not be protected
|
|
||||||
branch: 'master'
|
|
||||||
allowlist: insertish,bot*
|
|
||||||
|
|
||||||
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
|
|
||||||
remote-organization-name: revoltchat
|
|
||||||
remote-repository-name: cla
|
|
||||||
create-file-commit-message: 'cla(create): creating file for storing CLA Signatures'
|
|
||||||
signed-commit-message: 'cla(sign): $contributorName has signed the CLA in #$pullRequestNo'
|
|
||||||
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
|
|
||||||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
|
||||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
|
||||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
|
||||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
name: Docker Test & Publish
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "master"
|
|
||||||
tags:
|
|
||||||
- "*"
|
|
||||||
paths-ignore:
|
|
||||||
- ".github/**"
|
|
||||||
- "!.github/workflows/docker.yml"
|
|
||||||
- ".vscode/**"
|
|
||||||
- ".gitignore"
|
|
||||||
- "LICENSE"
|
|
||||||
- "README"
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- "master"
|
|
||||||
paths:
|
|
||||||
- "Dockerfile"
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
base:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Build base image
|
|
||||||
steps:
|
|
||||||
# Configure build environment
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
# Authenticate with GHCR
|
|
||||||
- name: Login to Github Container Registry
|
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build base image
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: ghcr.io/${{ github.repository_owner }}/base:latest
|
|
||||||
cache-from: type=gha,scope=buildx-base-multi-arch
|
|
||||||
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
|
|
||||||
|
|
||||||
publish:
|
|
||||||
needs: [base]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
project: [delta, bonfire]
|
|
||||||
name: Build ${{ matrix.project }} image
|
|
||||||
steps:
|
|
||||||
# Configure build environment
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
# Authenticate with Docker Hub and GHCR
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: docker.io
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
- name: Login to Github Container Registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
# Resolve the correct project
|
|
||||||
- uses: kanga333/variable-mapper@master
|
|
||||||
id: export
|
|
||||||
with:
|
|
||||||
key: "${{ matrix.project }}"
|
|
||||||
map: |
|
|
||||||
{
|
|
||||||
"delta": {
|
|
||||||
"path": "crates/delta",
|
|
||||||
"tag": "${{ github.repository_owner }}/server"
|
|
||||||
},
|
|
||||||
"bonfire": {
|
|
||||||
"path": "crates/bonfire",
|
|
||||||
"tag": "${{ github.repository_owner }}/bonfire"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export_to: output
|
|
||||||
|
|
||||||
# Configure metadata
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
docker.io/${{ steps.export.outputs.tag }}
|
|
||||||
ghcr.io/${{ steps.export.outputs.tag }}
|
|
||||||
|
|
||||||
# Build crate image
|
|
||||||
- name: Publish
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
file: ${{ steps.export.outputs.path }}/Dockerfile
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
build-args: |
|
|
||||||
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
+10
-65
@@ -1,9 +1,10 @@
|
|||||||
name: Rust build, test, and generate specification
|
name: Rust build and test
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches: [ master ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [ master ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -14,75 +15,19 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install latest stable
|
- name: Install latest nightly
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: nightly
|
||||||
override: true
|
override: true
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
- name: Install cargo-nextest
|
|
||||||
uses: baptiste0928/cargo-install@v1
|
|
||||||
with:
|
|
||||||
crate: cargo-nextest
|
|
||||||
locked: true
|
|
||||||
|
|
||||||
- name: Run cargo build
|
- name: Run cargo build
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: build
|
command: build
|
||||||
|
|
||||||
- name: Run services in background
|
|
||||||
run: |
|
|
||||||
docker-compose -f docker-compose.db.yml up -d
|
|
||||||
|
|
||||||
- name: Copy .env.example
|
|
||||||
run: |
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
- name: Run cargo test
|
- name: Run cargo test
|
||||||
env:
|
uses: actions-rs/cargo@v1
|
||||||
TEST_DB: REFERENCE
|
|
||||||
run: |
|
|
||||||
cargo nextest run
|
|
||||||
|
|
||||||
- name: Run cargo test (with MongoDB)
|
|
||||||
env:
|
|
||||||
TEST_DB: MONGODB
|
|
||||||
MONGODB: mongodb://localhost
|
|
||||||
run: |
|
|
||||||
cargo nextest run
|
|
||||||
|
|
||||||
- name: Start API in background
|
|
||||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
|
||||||
env:
|
|
||||||
TEST_DB: REFERENCE
|
|
||||||
run: |
|
|
||||||
cargo run --bin revolt-delta &
|
|
||||||
|
|
||||||
- name: Wait for API to go up
|
|
||||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
|
||||||
uses: nev7n/wait_for_response@v1
|
|
||||||
with:
|
with:
|
||||||
url: "http://localhost:8000/"
|
command: test
|
||||||
|
|
||||||
- name: Checkout API repository
|
|
||||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: revoltchat/api
|
|
||||||
path: api
|
|
||||||
token: ${{ secrets.PAT }}
|
|
||||||
|
|
||||||
- name: Download OpenAPI specification
|
|
||||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
|
||||||
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
|
|
||||||
|
|
||||||
- name: Commit changes
|
|
||||||
if: github.event_name != 'pull_request' && github.ref_name == 'master'
|
|
||||||
uses: EndBug/add-and-commit@v4
|
|
||||||
with:
|
|
||||||
cwd: "api"
|
|
||||||
add: "*.json"
|
|
||||||
author_name: Revolt CI
|
|
||||||
author_email: revolt-ci@users.noreply.github.com
|
|
||||||
message: "chore: generate OpenAPI specification"
|
|
||||||
@@ -15,27 +15,22 @@ jobs:
|
|||||||
gh api graphql -f query='
|
gh api graphql -f query='
|
||||||
query {
|
query {
|
||||||
organization(login: "revoltchat"){
|
organization(login: "revoltchat"){
|
||||||
projectV2(number: 3) {
|
projectNext(number: 3) {
|
||||||
id
|
id
|
||||||
fields(first:20) {
|
fields(first:20) {
|
||||||
nodes {
|
nodes {
|
||||||
... on ProjectV2SingleSelectField {
|
id
|
||||||
id
|
name
|
||||||
name
|
settings
|
||||||
options {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' > project_data.json
|
}' > project_data.json
|
||||||
|
|
||||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||||
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Add issue to project
|
- name: Add issue to project
|
||||||
env:
|
env:
|
||||||
@@ -44,11 +39,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
item_id="$( gh api graphql -f query='
|
item_id="$( gh api graphql -f query='
|
||||||
mutation($project:ID!, $issue:ID!) {
|
mutation($project:ID!, $issue:ID!) {
|
||||||
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
|
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||||
item {
|
projectNextItem {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
|
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||||
|
|
||||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||||
@@ -15,27 +15,22 @@ jobs:
|
|||||||
gh api graphql -f query='
|
gh api graphql -f query='
|
||||||
query {
|
query {
|
||||||
organization(login: "revoltchat"){
|
organization(login: "revoltchat"){
|
||||||
projectV2(number: 3) {
|
projectNext(number: 3) {
|
||||||
id
|
id
|
||||||
fields(first:20) {
|
fields(first:20) {
|
||||||
nodes {
|
nodes {
|
||||||
... on ProjectV2SingleSelectField {
|
id
|
||||||
id
|
name
|
||||||
name
|
settings
|
||||||
options {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' > project_data.json
|
}' > project_data.json
|
||||||
|
|
||||||
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
|
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||||
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||||
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Add PR to project
|
- name: Add PR to project
|
||||||
env:
|
env:
|
||||||
@@ -44,13 +39,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
item_id="$( gh api graphql -f query='
|
item_id="$( gh api graphql -f query='
|
||||||
mutation($project:ID!, $pr:ID!) {
|
mutation($project:ID!, $pr:ID!) {
|
||||||
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
|
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||||
item {
|
projectNextItem {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
|
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||||
|
|
||||||
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set fields
|
- name: Set fields
|
||||||
@@ -64,16 +59,14 @@ jobs:
|
|||||||
$status_field: ID!
|
$status_field: ID!
|
||||||
$status_value: String!
|
$status_value: String!
|
||||||
) {
|
) {
|
||||||
set_status: updateProjectV2ItemFieldValue(input: {
|
set_status: updateProjectNextItemField(input: {
|
||||||
projectId: $project
|
projectId: $project
|
||||||
itemId: $item
|
itemId: $item
|
||||||
fieldId: $status_field
|
fieldId: $status_field
|
||||||
value: {
|
value: $status_value
|
||||||
singleSelectOptionId: $status_value
|
|
||||||
}
|
|
||||||
}) {
|
}) {
|
||||||
projectV2Item {
|
projectNextItem {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||||
@@ -1,9 +1,2 @@
|
|||||||
Rocket.toml
|
Rocket.toml
|
||||||
Revolt.toml
|
|
||||||
|
|
||||||
target
|
target
|
||||||
.data
|
|
||||||
.env
|
|
||||||
|
|
||||||
.vercel
|
|
||||||
.DS_Store
|
|
||||||
Vendored
+3
-5
@@ -1,6 +1,4 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"rust-analyzer.checkOnSave.command": "clippy",
|
"rust-analyzer.checkOnSave.command": "clippy"
|
||||||
"nixEnvSelector.suggestion": false,
|
}
|
||||||
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
|
|
||||||
}
|
|
||||||
Generated
+1088
-1654
File diff suppressed because it is too large.
Load diff
+1
-9
@@ -1,10 +1,2 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
members = ["crates/bonfire", "crates/delta", "crates/quark", "crates/revcord/*"]
|
||||||
members = ["crates/delta", "crates/bonfire", "crates/quark", "crates/core/*"]
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
|
||||||
redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
|
|
||||||
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
|
|
||||||
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
|
|
||||||
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
|
|
||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
# Build Stage
|
|
||||||
FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
|
|
||||||
USER 0:0
|
|
||||||
WORKDIR /home/rust/src
|
|
||||||
|
|
||||||
ARG TARGETARCH
|
|
||||||
|
|
||||||
# Install build requirements
|
|
||||||
RUN dpkg --add-architecture "${TARGETARCH}"
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
make \
|
|
||||||
pkg-config \
|
|
||||||
libssl-dev:"${TARGETARCH}"
|
|
||||||
COPY scripts/build-image-layer.sh /tmp/
|
|
||||||
RUN sh /tmp/build-image-layer.sh tools
|
|
||||||
|
|
||||||
# Build all dependencies
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
|
||||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
|
||||||
COPY crates/quark/Cargo.toml ./crates/quark/
|
|
||||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
|
||||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
|
||||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
|
||||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
|
||||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
|
||||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
|
||||||
RUN sh /tmp/build-image-layer.sh deps
|
|
||||||
|
|
||||||
# Build all apps
|
|
||||||
COPY crates ./crates
|
|
||||||
RUN sh /tmp/build-image-layer.sh apps
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Build Stage
|
|
||||||
FROM rust:1.70.0-slim
|
|
||||||
USER 0:0
|
|
||||||
WORKDIR /home/rust/src
|
|
||||||
|
|
||||||
# Install build requirements
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
make \
|
|
||||||
pkg-config \
|
|
||||||
libssl-dev
|
|
||||||
COPY scripts/build-image-layer.sh /tmp/
|
|
||||||
|
|
||||||
# Build all dependencies
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
|
||||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
|
||||||
COPY crates/quark/Cargo.toml ./crates/quark/
|
|
||||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
|
||||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
|
||||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
|
||||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
|
||||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
|
||||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
|
||||||
RUN sh /tmp/build-image-layer.sh deps
|
|
||||||
|
|
||||||
# Build all apps
|
|
||||||
COPY crates ./crates
|
|
||||||
RUN sh /tmp/build-image-layer.sh apps
|
|
||||||
@@ -2,14 +2,14 @@
|
|||||||
|
|
||||||
This is a monorepo for the Revolt backend.
|
This is a monorepo for the Revolt backend.
|
||||||
|
|
||||||
| Crate | Path | Description |
|
| Crate | Description |
|
||||||
| ---------------- | ---------------------------------------------- | ------------------------------------ |
|
| ---------------- | ------------------------------------ |
|
||||||
| `delta` | [crates/delta](crates/delta) | REST API server |
|
| `delta` | REST API server |
|
||||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
|
| `bonfire` | WebSocket events server |
|
||||||
| `quark` | [crates/quark](crates/quark) | Models and logic |
|
| `quark` | Models and logic |
|
||||||
<!--| `revcord/api` | [crates/revcord/api](crates/revcord/api) | Discord REST translation layer |
|
| `revcord/api` | Discord REST translation layer |
|
||||||
| `revcord/ws` | [crates/revcord/ws](crates/revcord/ws) | Discord gateway translation layer |
|
| `revcord/ws` | Discord gateway translation layer |
|
||||||
| `revcord/models` | [crates/revcord/models](crates/revcord/models) | Discord models and quark translation |-->
|
| `revcord/models` | Discord models and quark translation |
|
||||||
|
|
||||||
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
||||||
|
|
||||||
@@ -21,10 +21,6 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
|||||||
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
|
- [Revolt Testers Server](https://app.revolt.chat/invite/Testers)
|
||||||
- [Contribution Guide](https://developers.revolt.chat/contributing)
|
- [Contribution Guide](https://developers.revolt.chat/contributing)
|
||||||
|
|
||||||
## Minimum Supported Rust Version
|
|
||||||
|
|
||||||
Rust 1.70 or higher.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
|
The contribution guide is located at [developers.revolt.chat/contributing](https://developers.revolt.chat/contributing).
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
# Code Style Guide
|
|
||||||
|
|
||||||
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
|
|
||||||
|
|
||||||
## Writing Style
|
|
||||||
|
|
||||||
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
|
|
||||||
|
|
||||||
## `core/database` crate
|
|
||||||
|
|
||||||
w.r.t. `model.rs` files
|
|
||||||
|
|
||||||
- All struct definitions must be commented.
|
|
||||||
```rust
|
|
||||||
/// Server
|
|
||||||
pub struct Server {
|
|
||||||
/// Name of the server
|
|
||||||
pub name: String,
|
|
||||||
```
|
|
||||||
- Struct definitions should not include derives unless necessary (if additional traits such as Hash are required) and instead use `auto_derived!` and `auto_derived_partial!`.
|
|
||||||
```rust
|
|
||||||
auto_derived_partial!(
|
|
||||||
/// Server
|
|
||||||
pub struct Server { .. },
|
|
||||||
"PartialServer"
|
|
||||||
);
|
|
||||||
```
|
|
||||||
- `auto_derived!` macro accepts multiple entries and should be used as such:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
auto_derived!(
|
|
||||||
/// Optional fields on server object
|
|
||||||
pub enum FieldsServer { .. }
|
|
||||||
|
|
||||||
/// Optional fields on server object
|
|
||||||
pub enum FieldsRole { .. }
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
|
|
||||||
```rust
|
|
||||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
|
||||||
```
|
|
||||||
- `impl` blocks may be defined below the struct definitions and should be ordered in the same order of definition. Methods in the block must follow the same guidelines as traits where-in: methods are ordered in terms of CRUD, there are empty line breaks, and methods are commented.
|
|
||||||
|
|
||||||
w.r.t. `ops` module for models
|
|
||||||
|
|
||||||
- All traits must use a the name format `AbstractPlural` where Plural is the plural form of the collection. e.g. Servers
|
|
||||||
- Traits defined must follow these guidelines:
|
|
||||||
|
|
||||||
- Methods are ordered in terms of CRUD, create-read-update-delete ordering.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractServerMembers: Sync + Send {
|
|
||||||
/// Insert a new server member into the database
|
|
||||||
async fn insert_member(&self, member: &Member) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch a server member by their id
|
|
||||||
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
|
|
||||||
|
|
||||||
/// Update information for a server member
|
|
||||||
async fn update_member(&self, .. ) -> Result<()>;
|
|
||||||
|
|
||||||
/// Delete a server member by their id
|
|
||||||
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- There should be an empty line break between each method declaration.
|
|
||||||
- All methods must have an appropriate comment.
|
|
||||||
|
|
||||||
- When implementing the trait defined in `ops.rs` with each driver, the method declaration style should be the same for ease of searching: same ordering, same comments, same line breaks.
|
|
||||||
-28
@@ -1,28 +0,0 @@
|
|||||||
disallowed-methods = [
|
|
||||||
# Shouldn't need to access these directly
|
|
||||||
"revolt_database::models::bots::model::Bot::remove_field",
|
|
||||||
"revolt_database::models::messages::model::Message::attach_sendable_embed",
|
|
||||||
"revolt_database::models::users::model::User::set_relationship",
|
|
||||||
"revolt_database::models::users::model::User::apply_relationship",
|
|
||||||
|
|
||||||
# Prefer to use Object::create()
|
|
||||||
"revolt_database::models::bots::ops::AbstractBots::insert_bot",
|
|
||||||
"revolt_database::models::channel_invites::ops::AbstractChannelInvites::insert_invite",
|
|
||||||
"revolt_database::models::channel_unreads::ops::AbstractChannelUnreads::acknowledge_message",
|
|
||||||
"revolt_database::models::channel_webhooks::ops::AbstractWebhooks::insert_webhook",
|
|
||||||
"revolt_database::models::channels::ops::AbstractChannels::insert_channel",
|
|
||||||
"revolt_database::models::emojis::ops::AbstractEmojis::insert_emoji",
|
|
||||||
"revolt_database::models::files::ops::AbstractAttachments::insert_attachment",
|
|
||||||
"revolt_database::models::messages::ops::AbstractMessages::insert_message",
|
|
||||||
"revolt_database::models::ratelimit_events::ops::AbstractRatelimitEvents::insert_ratelimit_event",
|
|
||||||
"revolt_database::models::server_bans::ops::AbstractServerBans::insert_ban",
|
|
||||||
"revolt_database::models::server_members::ops::AbstractServerMembers::insert_member",
|
|
||||||
"revolt_database::models::servers::ops::AbstractServers::insert_server",
|
|
||||||
"revolt_database::models::users::ops::AbstractUsers::insert_user",
|
|
||||||
|
|
||||||
# Prefer to use Object::update(&self)
|
|
||||||
"revolt_database::models::bots::ops::AbstractBots::update_bot",
|
|
||||||
|
|
||||||
# Prefer to use Object::delete(&self)
|
|
||||||
"revolt_database::models::bots::ops::AbstractBots::delete_bot",
|
|
||||||
]
|
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ko_fi: insertish
|
||||||
|
custom: https://insrt.uk/donate
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "master"
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
paths-ignore:
|
||||||
|
- ".github/**"
|
||||||
|
- "!.github/workflows/docker.yml"
|
||||||
|
- ".vscode/**"
|
||||||
|
- ".gitignore"
|
||||||
|
- ".gitlab-ci.yml"
|
||||||
|
- "LICENSE"
|
||||||
|
- "README"
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "master"
|
||||||
|
paths:
|
||||||
|
- "Dockerfile"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
architecture: [linux/amd64]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: "recursive"
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||||
|
key: ${{ runner.os }}-buildx-${{ matrix.architecture }}-${{ github.sha }}
|
||||||
|
- name: Build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: ${{ matrix.architecture }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache/${{ matrix.architecture }}
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new/${{ matrix.architecture }},mode=max
|
||||||
|
- name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||||
|
mv /tmp/.buildx-cache-new/${{ matrix.architecture }} /tmp/.buildx-cache/${{ matrix.architecture }}
|
||||||
|
|
||||||
|
publish_amd64:
|
||||||
|
needs: [test]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: "recursive"
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Cache amd64 Docker layers
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache/linux/amd64
|
||||||
|
key: ${{ runner.os }}-buildx-linux/amd64-${{ github.sha }}
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: ghcr.io/revoltchat/bonfire
|
||||||
|
- name: Login to Github Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build and publish
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache/linux/amd64
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
- name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache
|
||||||
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
name: Rust build and test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
name: Rust project
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install latest nightly
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
override: true
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Run cargo build
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: build
|
||||||
|
|
||||||
|
- name: Run cargo test
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Add Issue to Board
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
track_issue:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get project data
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||||
|
run: |
|
||||||
|
gh api graphql -f query='
|
||||||
|
query {
|
||||||
|
organization(login: "revoltchat"){
|
||||||
|
projectNext(number: 3) {
|
||||||
|
id
|
||||||
|
fields(first:20) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' > project_data.json
|
||||||
|
|
||||||
|
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||||
|
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||||
|
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Add issue to project
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||||
|
ISSUE_ID: ${{ github.event.issue.node_id }}
|
||||||
|
run: |
|
||||||
|
item_id="$( gh api graphql -f query='
|
||||||
|
mutation($project:ID!, $issue:ID!) {
|
||||||
|
addProjectNextItem(input: {projectId: $project, contentId: $issue}) {
|
||||||
|
projectNextItem {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||||
|
|
||||||
|
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
name: Add PR to Board
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, ready_for_review, review_requested]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
track_pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Get project data
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||||
|
run: |
|
||||||
|
gh api graphql -f query='
|
||||||
|
query {
|
||||||
|
organization(login: "revoltchat"){
|
||||||
|
projectNext(number: 3) {
|
||||||
|
id
|
||||||
|
fields(first:20) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' > project_data.json
|
||||||
|
|
||||||
|
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
|
||||||
|
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
|
||||||
|
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") |.settings | fromjson.options[] | select(.name=="Incoming PRs") |.id' project_data.json) >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Add PR to project
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||||
|
PR_ID: ${{ github.event.pull_request.node_id }}
|
||||||
|
run: |
|
||||||
|
item_id="$( gh api graphql -f query='
|
||||||
|
mutation($project:ID!, $pr:ID!) {
|
||||||
|
addProjectNextItem(input: {projectId: $project, contentId: $pr}) {
|
||||||
|
projectNextItem {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
|
||||||
|
|
||||||
|
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set fields
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.PAT }}
|
||||||
|
run: |
|
||||||
|
gh api graphql -f query='
|
||||||
|
mutation (
|
||||||
|
$project: ID!
|
||||||
|
$item: ID!
|
||||||
|
$status_field: ID!
|
||||||
|
$status_value: String!
|
||||||
|
) {
|
||||||
|
set_status: updateProjectNextItemField(input: {
|
||||||
|
projectId: $project
|
||||||
|
itemId: $item
|
||||||
|
fieldId: $status_field
|
||||||
|
value: $status_value
|
||||||
|
}) {
|
||||||
|
projectNextItem {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent
|
||||||
Generated
+4680
File diff suppressed because it is too large.
Load diff
@@ -1,7 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-bonfire"
|
name = "revolt-bonfire"
|
||||||
version = "0.6.9"
|
version = "1.0.6-patch.2"
|
||||||
license = "AGPL-3.0-or-later"
|
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# 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
|
||||||
@@ -25,12 +24,4 @@ serde = "1.0.136"
|
|||||||
# async
|
# async
|
||||||
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 = [
|
async-std = { version = "1.8.0", features = ["tokio1", "tokio02", "attributes"] }
|
||||||
"tokio1",
|
|
||||||
"tokio02",
|
|
||||||
"attributes",
|
|
||||||
] }
|
|
||||||
|
|
||||||
# core
|
|
||||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
|
||||||
sentry = "0.31.5"
|
|
||||||
@@ -1,11 +1,19 @@
|
|||||||
# Build Stage
|
# Build Stage
|
||||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
FROM rustlang/rust:nightly-slim AS builder
|
||||||
|
USER 0:0
|
||||||
|
WORKDIR /home/rust/src
|
||||||
|
|
||||||
|
RUN USER=root cargo new --bin bonfire
|
||||||
|
WORKDIR /home/rust/src/bonfire
|
||||||
|
RUN apt-get update && apt-get install -y libssl-dev pkg-config
|
||||||
|
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY src ./src
|
||||||
|
RUN cargo install --locked --path .
|
||||||
|
|
||||||
# Bundle Stage
|
# Bundle Stage
|
||||||
FROM debian:bullseye-slim
|
FROM debian:buster-slim
|
||||||
RUN apt-get update && \
|
RUN apt-get update && apt-get install -y ca-certificates
|
||||||
apt-get install -y ca-certificates && \
|
COPY --from=builder /usr/local/cargo/bin/bonfire ./
|
||||||
apt-get clean
|
|
||||||
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
CMD ["./revolt-bonfire"]
|
CMD ["./bonfire"]
|
||||||
@@ -1 +0,0 @@
|
|||||||
../../LICENSE
|
|
||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
Revolt Bonfire
|
||||||
|
Copyright (C) 2022 Pawel Makles
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use async_std::net::TcpListener;
|
use async_std::net::TcpListener;
|
||||||
use revolt_presence::clear_region;
|
use revolt_quark::presence::presence_clear_region;
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
@@ -14,11 +14,11 @@ mod websocket;
|
|||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// Configure requirements for Bonfire.
|
// Configure requirements for Bonfire.
|
||||||
revolt_quark::configure!();
|
let _guard = revolt_quark::setup_logging();
|
||||||
database::connect().await;
|
database::connect().await;
|
||||||
|
|
||||||
// Clean up the current region information.
|
// Clean up the current region information.
|
||||||
clear_region(None).await;
|
presence_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.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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,
|
||||||
@@ -9,6 +8,7 @@ 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,7 +69,8 @@ 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) = create_session(&user_id, 0).await;
|
let (first_session, session_id) =
|
||||||
|
presence_create_session(&user_id, 0).await;
|
||||||
|
|
||||||
// Notify socket we have authenticated.
|
// Notify socket we have authenticated.
|
||||||
write
|
write
|
||||||
@@ -136,13 +137,13 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
|||||||
);*/
|
);*/
|
||||||
|
|
||||||
// Handle incoming events.
|
// Handle incoming events.
|
||||||
match conn.on_message().next().await.map(|res| {
|
match conn.on_message().next().await.map(|item| {
|
||||||
res.map(|item|(
|
(
|
||||||
item.get_channel_name().to_string(),
|
item.get_channel_name().to_string(),
|
||||||
redis_kiss::decode_payload::<EventV1>(&item),
|
redis_kiss::decode_payload::<EventV1>(&item),
|
||||||
))
|
)
|
||||||
}) {
|
}) {
|
||||||
Some(Ok((channel, item))) => {
|
Some((channel, item)) => {
|
||||||
if let Ok(mut event) = item {
|
if let Ok(mut event) = item {
|
||||||
if state
|
if state
|
||||||
.handle_incoming_event_v1(
|
.handle_incoming_event_v1(
|
||||||
@@ -160,11 +161,6 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
|||||||
warn!("Failed to deserialise an event for {channel}!");
|
warn!("Failed to deserialise an event for {channel}!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Err(e)) => {
|
|
||||||
info!("Error while consuming pub/sub messages: {e:?}");
|
|
||||||
sentry::capture_error(&e);
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// No more data, assume we disconnected or otherwise
|
// No more data, assume we disconnected or otherwise
|
||||||
// something bad occurred, so disconnect user.
|
// something bad occurred, so disconnect user.
|
||||||
None => break,
|
None => break,
|
||||||
@@ -229,7 +225,7 @@ pub fn spawn_client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean up presence session.
|
// Clean up presence session.
|
||||||
let last_session = delete_session(&user_id, session_id).await;
|
let last_session = presence_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 {
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "revolt-config"
|
|
||||||
version = "0.6.9"
|
|
||||||
edition = "2021"
|
|
||||||
license = "AGPL-3.0-or-later"
|
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
|
||||||
description = "Revolt Backend: Configuration"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[features]
|
|
||||||
test = ["async-std"]
|
|
||||||
default = ["test"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# Utility
|
|
||||||
config = "0.13.3"
|
|
||||||
cached = "0.44.0"
|
|
||||||
once_cell = "1.18.0"
|
|
||||||
|
|
||||||
# Serde
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
|
|
||||||
# Async
|
|
||||||
futures-locks = "0.7.1"
|
|
||||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[database]
|
|
||||||
mongodb = "mongodb://localhost"
|
|
||||||
redis = "redis://localhost/"
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
[database]
|
|
||||||
mongodb = "mongodb://database"
|
|
||||||
redis = "redis://redis/"
|
|
||||||
|
|
||||||
[hosts]
|
|
||||||
app = "http://local.revolt.chat"
|
|
||||||
api = "http://local.revolt.chat/api"
|
|
||||||
events = "ws://local.revolt.chat/ws"
|
|
||||||
autumn = "http://local.revolt.chat/autumn"
|
|
||||||
january = "http://local.revolt.chat/january"
|
|
||||||
voso_legacy = ""
|
|
||||||
voso_legacy_ws = ""
|
|
||||||
|
|
||||||
[api]
|
|
||||||
staging = false
|
|
||||||
|
|
||||||
[api.registration]
|
|
||||||
invite_only = false
|
|
||||||
|
|
||||||
[api.smtp]
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
password = ""
|
|
||||||
from_address = ""
|
|
||||||
|
|
||||||
[api.vapid]
|
|
||||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
|
||||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
|
||||||
|
|
||||||
[api.fcm]
|
|
||||||
api_key = ""
|
|
||||||
|
|
||||||
[api.security]
|
|
||||||
authifier_shield_key = ""
|
|
||||||
voso_legacy_token = ""
|
|
||||||
|
|
||||||
[api.security.captcha]
|
|
||||||
hcaptcha_key = ""
|
|
||||||
hcaptcha_sitekey = ""
|
|
||||||
|
|
||||||
[api.workers]
|
|
||||||
max_concurrent_connections = 50
|
|
||||||
|
|
||||||
[features]
|
|
||||||
|
|
||||||
[features.limits]
|
|
||||||
|
|
||||||
[features.limits.default]
|
|
||||||
group_size = 100
|
|
||||||
bots = 5
|
|
||||||
message_length = 2000
|
|
||||||
message_embeds = 5
|
|
||||||
message_replies = 5
|
|
||||||
message_attachments = 5
|
|
||||||
message_reactions = 20
|
|
||||||
servers = 100
|
|
||||||
server_emoji = 100
|
|
||||||
server_roles = 200
|
|
||||||
server_channels = 200
|
|
||||||
|
|
||||||
attachment_size = 20000000
|
|
||||||
avatar_size = 4000000
|
|
||||||
background_size = 6000000
|
|
||||||
icon_size = 2500000
|
|
||||||
banner_size = 6000000
|
|
||||||
emoji_size = 500000
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use cached::proc_macro::cached;
|
|
||||||
use config::{Config, File, FileFormat};
|
|
||||||
use futures_locks::RwLock;
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
|
||||||
RwLock::new({
|
|
||||||
let mut builder = Config::builder().add_source(File::from_str(
|
|
||||||
include_str!("../Revolt.toml"),
|
|
||||||
FileFormat::Toml,
|
|
||||||
));
|
|
||||||
|
|
||||||
if std::env::var("TEST_DB").is_ok() {
|
|
||||||
builder = builder.add_source(File::from_str(
|
|
||||||
include_str!("../Revolt.test.toml"),
|
|
||||||
FileFormat::Toml,
|
|
||||||
));
|
|
||||||
} else if std::path::Path::new("Revolt.toml").exists() {
|
|
||||||
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.build().unwrap()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://gifbox.me/view/gT5mqxYKCZv-twilight-meow
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct Database {
|
|
||||||
pub mongodb: String,
|
|
||||||
pub redis: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct Hosts {
|
|
||||||
pub app: String,
|
|
||||||
pub api: String,
|
|
||||||
pub events: String,
|
|
||||||
pub autumn: String,
|
|
||||||
pub january: String,
|
|
||||||
pub voso_legacy: String,
|
|
||||||
pub voso_legacy_ws: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct ApiRegistration {
|
|
||||||
pub invite_only: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct ApiSmtp {
|
|
||||||
pub host: String,
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub from_address: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct ApiVapid {
|
|
||||||
pub private_key: String,
|
|
||||||
pub public_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct ApiFcm {
|
|
||||||
pub api_key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct ApiSecurityCaptcha {
|
|
||||||
pub hcaptcha_key: String,
|
|
||||||
pub hcaptcha_sitekey: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct ApiSecurity {
|
|
||||||
pub authifier_shield_key: String,
|
|
||||||
pub voso_legacy_token: String,
|
|
||||||
pub captcha: ApiSecurityCaptcha,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct ApiWorkers {
|
|
||||||
pub max_concurrent_connections: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct Api {
|
|
||||||
pub staging: bool,
|
|
||||||
pub registration: ApiRegistration,
|
|
||||||
pub smtp: ApiSmtp,
|
|
||||||
pub vapid: ApiVapid,
|
|
||||||
pub fcm: ApiFcm,
|
|
||||||
pub security: ApiSecurity,
|
|
||||||
pub workers: ApiWorkers,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct FeaturesLimits {
|
|
||||||
pub group_size: usize,
|
|
||||||
pub bots: usize,
|
|
||||||
pub message_length: usize,
|
|
||||||
pub message_replies: usize,
|
|
||||||
pub message_attachments: usize,
|
|
||||||
pub message_embeds: usize,
|
|
||||||
pub message_reactions: usize,
|
|
||||||
pub servers: usize,
|
|
||||||
pub server_emoji: usize,
|
|
||||||
pub server_roles: usize,
|
|
||||||
pub server_channels: usize,
|
|
||||||
|
|
||||||
pub attachment_size: usize,
|
|
||||||
pub avatar_size: usize,
|
|
||||||
pub background_size: usize,
|
|
||||||
pub icon_size: usize,
|
|
||||||
pub banner_size: usize,
|
|
||||||
pub emoji_size: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct FeaturesLimitsCollection {
|
|
||||||
pub default: FeaturesLimits,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub roles: HashMap<String, FeaturesLimits>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct Features {
|
|
||||||
pub limits: FeaturesLimitsCollection,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
|
||||||
pub struct Settings {
|
|
||||||
pub database: Database,
|
|
||||||
pub hosts: Hosts,
|
|
||||||
pub api: Api,
|
|
||||||
pub features: Features,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init() {
|
|
||||||
println!(
|
|
||||||
":: Revolt Configuration ::\n\x1b[32m{:?}\x1b[0m",
|
|
||||||
config().await
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read() -> Config {
|
|
||||||
CONFIG_BUILDER.read().await.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cached(time = 30)]
|
|
||||||
pub async fn config() -> Settings {
|
|
||||||
read().await.try_deserialize::<Settings>().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "test")]
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::init;
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn it_works() {
|
|
||||||
init().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "revolt-database"
|
|
||||||
version = "0.6.9"
|
|
||||||
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
|
|
||||||
tasks = ["isahc", "linkify", "url-escape"]
|
|
||||||
async-std-runtime = ["async-std"]
|
|
||||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
|
||||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
|
||||||
|
|
||||||
# Default Features
|
|
||||||
default = ["mongodb", "async-std-runtime", "tasks"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
# Core
|
|
||||||
revolt-config = { version = "0.6.9", path = "../config" }
|
|
||||||
revolt-result = { version = "0.6.9", path = "../result" }
|
|
||||||
revolt-models = { version = "0.6.9", path = "../models" }
|
|
||||||
revolt-presence = { version = "0.6.9", path = "../presence" }
|
|
||||||
revolt-permissions = { version = "0.6.9", path = "../permissions", features = [
|
|
||||||
"serde",
|
|
||||||
"bson",
|
|
||||||
] }
|
|
||||||
|
|
||||||
# Utility
|
|
||||||
log = "0.4"
|
|
||||||
lru = "0.11.0"
|
|
||||||
rand = "0.8.5"
|
|
||||||
ulid = "1.0.0"
|
|
||||||
nanoid = "0.4.0"
|
|
||||||
base64 = "0.21.3"
|
|
||||||
once_cell = "1.17"
|
|
||||||
indexmap = "1.9.1"
|
|
||||||
decancer = "1.6.2"
|
|
||||||
deadqueue = "0.2.4"
|
|
||||||
linkify = { optional = true, version = "0.8.1" }
|
|
||||||
url-escape = { optional = true, version = "0.1.1" }
|
|
||||||
isahc = { optional = true, version = "1.7", features = ["json"] }
|
|
||||||
|
|
||||||
# Serialisation
|
|
||||||
serde_json = "1"
|
|
||||||
revolt_optional_struct = "0.2.0"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
|
|
||||||
|
|
||||||
# Events
|
|
||||||
redis-kiss = { version = "0.1.4" }
|
|
||||||
|
|
||||||
# Database
|
|
||||||
bson = { optional = true, version = "2.1.0" }
|
|
||||||
mongodb = { optional = true, version = "2.1.0", default-features = false }
|
|
||||||
|
|
||||||
# Database Migration
|
|
||||||
unicode-segmentation = "1.10.1"
|
|
||||||
regex = "1"
|
|
||||||
|
|
||||||
# Async Language Features
|
|
||||||
futures = "0.3.19"
|
|
||||||
async-lock = "2.8.0"
|
|
||||||
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 }
|
|
||||||
revolt_okapi = { version = "0.9.1", optional = true }
|
|
||||||
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
|
||||||
|
|
||||||
# Notifications
|
|
||||||
fcm = "0.9.2"
|
|
||||||
web-push = "0.10.0"
|
|
||||||
|
|
||||||
# Authifier
|
|
||||||
authifier = { version = "1.0.8" }
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"_object_type": "User",
|
|
||||||
"_id": "__ID:0__",
|
|
||||||
"username": "Owner",
|
|
||||||
"discriminator": "0001"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_object_type": "User",
|
|
||||||
"_id": "__ID:1__",
|
|
||||||
"username": "Moderator",
|
|
||||||
"discriminator": "0001"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_object_type": "User",
|
|
||||||
"_id": "__ID:2__",
|
|
||||||
"username": "User",
|
|
||||||
"discriminator": "0001"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_object_type": "Channel",
|
|
||||||
"_id": "__ID:3__",
|
|
||||||
"channel_type": "TextChannel",
|
|
||||||
"name": "General",
|
|
||||||
"server": "__ID:4__",
|
|
||||||
"default_permissions": {
|
|
||||||
"a": 0,
|
|
||||||
"d": 1048576
|
|
||||||
},
|
|
||||||
"role_permissions": {
|
|
||||||
"__ID:5__": {
|
|
||||||
"a": 1048576,
|
|
||||||
"d": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_object_type": "Server",
|
|
||||||
"_id": "__ID:4__",
|
|
||||||
"owner": "__ID:0__",
|
|
||||||
"name": "Server",
|
|
||||||
"channels": ["__ID:3__"],
|
|
||||||
"roles": {
|
|
||||||
"__ID:5__": {
|
|
||||||
"name": "Moderator",
|
|
||||||
"permissions": {
|
|
||||||
"a": 545270208,
|
|
||||||
"d": 0
|
|
||||||
},
|
|
||||||
"rank": 3
|
|
||||||
},
|
|
||||||
"__ID:6__": {
|
|
||||||
"name": "Owner",
|
|
||||||
"permissions": {
|
|
||||||
"a": 0,
|
|
||||||
"d": 0
|
|
||||||
},
|
|
||||||
"rank": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"default_permissions": 4000322560
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_object_type": "ServerMember",
|
|
||||||
"_id": {
|
|
||||||
"user": "__ID:0__",
|
|
||||||
"server": "__ID:4__"
|
|
||||||
},
|
|
||||||
"roles": ["__ID:6__"],
|
|
||||||
"joined_at": 1698318340195
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_object_type": "ServerMember",
|
|
||||||
"_id": {
|
|
||||||
"user": "__ID:1__",
|
|
||||||
"server": "__ID:4__"
|
|
||||||
},
|
|
||||||
"roles": ["__ID:5__"],
|
|
||||||
"joined_at": 1698318340195
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"_object_type": "ServerMember",
|
|
||||||
"_id": {
|
|
||||||
"user": "__ID:2__",
|
|
||||||
"server": "__ID:4__"
|
|
||||||
},
|
|
||||||
"joined_at": 1698318340195
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
use rand::Rng;
|
|
||||||
use revolt_config::config;
|
|
||||||
|
|
||||||
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> {
|
|
||||||
let config = config().await;
|
|
||||||
|
|
||||||
Ok(match self {
|
|
||||||
DatabaseInfo::Auto => {
|
|
||||||
if std::env::var("TEST_DB").is_ok() {
|
|
||||||
DatabaseInfo::Test(format!(
|
|
||||||
"revolt_test_{}",
|
|
||||||
rand::thread_rng().gen_range(1_000_000..10_000_000)
|
|
||||||
))
|
|
||||||
.connect()
|
|
||||||
.await?
|
|
||||||
} else if !config.database.mongodb.is_empty() {
|
|
||||||
DatabaseInfo::MongoDb {
|
|
||||||
uri: config.database.mongodb,
|
|
||||||
database_name: "revolt".to_string(),
|
|
||||||
}
|
|
||||||
.connect()
|
|
||||||
.await?
|
|
||||||
} else {
|
|
||||||
DatabaseInfo::Reference.connect().await?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DatabaseInfo::Test(database_name) => {
|
|
||||||
match std::env::var("TEST_DB")
|
|
||||||
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
|
|
||||||
.as_str()
|
|
||||||
{
|
|
||||||
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
|
|
||||||
"MONGODB" => {
|
|
||||||
DatabaseInfo::MongoDb {
|
|
||||||
uri: config.database.mongodb,
|
|
||||||
database_name,
|
|
||||||
}
|
|
||||||
.connect()
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
_ => unreachable!("must specify REFERENCE or MONGODB"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
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)| (format!("{}{}", prefix.to_owned(), k), v))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
|
|
||||||
use futures::lock::Mutex;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
|
|
||||||
MemberCompositeKey, Message, RatelimitEvent, Server, ServerBan, User, UserSettings, Webhook,
|
|
||||||
};
|
|
||||||
|
|
||||||
database_derived!(
|
|
||||||
/// Reference implementation
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ReferenceDb {
|
|
||||||
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
|
|
||||||
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
|
|
||||||
pub channel_invites: Arc<Mutex<HashMap<String, Invite>>>,
|
|
||||||
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
|
|
||||||
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
|
|
||||||
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
|
|
||||||
pub files: Arc<Mutex<HashMap<String, File>>>,
|
|
||||||
pub messages: Arc<Mutex<HashMap<String, Message>>>,
|
|
||||||
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
|
|
||||||
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
|
|
||||||
pub users: Arc<Mutex<HashMap<String, User>>>,
|
|
||||||
pub server_bans: Arc<Mutex<HashMap<MemberCompositeKey, ServerBan>>>,
|
|
||||||
pub server_members: Arc<Mutex<HashMap<MemberCompositeKey, Member>>>,
|
|
||||||
pub servers: Arc<Mutex<HashMap<String, Server>>>,
|
|
||||||
pub safety_reports: Arc<Mutex<HashMap<String, ()>>>,
|
|
||||||
pub safety_snapshots: Arc<Mutex<HashMap<String, ()>>>,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
use authifier::AuthifierEvent;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use revolt_models::v0::{
|
|
||||||
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
|
|
||||||
FieldsUser, FieldsWebhook, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
|
||||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Server, User,
|
|
||||||
UserSettings, Webhook,
|
|
||||||
};
|
|
||||||
use revolt_result::Error;
|
|
||||||
|
|
||||||
use crate::Database;
|
|
||||||
|
|
||||||
/// WebSocket Client Errors
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
#[serde(tag = "error")]
|
|
||||||
pub enum WebSocketError {
|
|
||||||
LabelMe,
|
|
||||||
InternalError { at: String },
|
|
||||||
InvalidSession,
|
|
||||||
OnboardingNotFinished,
|
|
||||||
AlreadyAuthenticated,
|
|
||||||
MalformedData { msg: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ping Packet
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum Ping {
|
|
||||||
Binary(Vec<u8>),
|
|
||||||
Number(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Untagged Error
|
|
||||||
#[derive(Serialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum ErrorEvent {
|
|
||||||
Error(WebSocketError),
|
|
||||||
APIError(Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Protocol Events
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum EventV1 {
|
|
||||||
/// Multiple events
|
|
||||||
Bulk { v: Vec<EventV1> },
|
|
||||||
|
|
||||||
/// Successfully authenticated
|
|
||||||
Authenticated,
|
|
||||||
/* /// Basic data to cache
|
|
||||||
Ready {
|
|
||||||
users: Vec<User>,
|
|
||||||
servers: Vec<Server>,
|
|
||||||
channels: Vec<Channel>,
|
|
||||||
members: Vec<Member>,
|
|
||||||
emojis: Option<Vec<Emoji>>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Ping response
|
|
||||||
Pong { data: Ping }, */
|
|
||||||
/// New message
|
|
||||||
Message(Message),
|
|
||||||
|
|
||||||
/// Update existing message
|
|
||||||
MessageUpdate {
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
data: PartialMessage,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Append information to existing message
|
|
||||||
MessageAppend {
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
append: AppendMessage,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Delete message
|
|
||||||
MessageDelete { id: String, channel: String },
|
|
||||||
|
|
||||||
/// New reaction to a message
|
|
||||||
MessageReact {
|
|
||||||
id: String,
|
|
||||||
channel_id: String,
|
|
||||||
user_id: String,
|
|
||||||
emoji_id: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Remove user's reaction from message
|
|
||||||
MessageUnreact {
|
|
||||||
id: String,
|
|
||||||
channel_id: String,
|
|
||||||
user_id: String,
|
|
||||||
emoji_id: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Remove a reaction from message
|
|
||||||
MessageRemoveReaction {
|
|
||||||
id: String,
|
|
||||||
channel_id: String,
|
|
||||||
emoji_id: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Bulk delete messages
|
|
||||||
BulkMessageDelete { channel: String, ids: Vec<String> },
|
|
||||||
|
|
||||||
/// New server
|
|
||||||
ServerCreate {
|
|
||||||
id: String,
|
|
||||||
server: Server,
|
|
||||||
channels: Vec<Channel>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Update existing server
|
|
||||||
ServerUpdate {
|
|
||||||
id: String,
|
|
||||||
data: PartialServer,
|
|
||||||
clear: Vec<FieldsServer>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Delete server
|
|
||||||
ServerDelete { id: String },
|
|
||||||
|
|
||||||
/// Update existing server member
|
|
||||||
ServerMemberUpdate {
|
|
||||||
id: MemberCompositeKey,
|
|
||||||
data: PartialMember,
|
|
||||||
clear: Vec<FieldsMember>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// User joins server
|
|
||||||
ServerMemberJoin { id: String, user: String },
|
|
||||||
|
|
||||||
/// User left server
|
|
||||||
ServerMemberLeave { id: String, user: String },
|
|
||||||
|
|
||||||
/// Server role created or updated
|
|
||||||
ServerRoleUpdate {
|
|
||||||
id: String,
|
|
||||||
role_id: String,
|
|
||||||
data: PartialRole,
|
|
||||||
clear: Vec<FieldsRole>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Server role deleted
|
|
||||||
ServerRoleDelete { id: String, role_id: String },
|
|
||||||
|
|
||||||
/// Update existing user
|
|
||||||
UserUpdate {
|
|
||||||
id: String,
|
|
||||||
data: PartialUser,
|
|
||||||
clear: Vec<FieldsUser>,
|
|
||||||
event_id: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Relationship with another user changed
|
|
||||||
UserRelationship { id: String, user: User },
|
|
||||||
/// Settings updated remotely
|
|
||||||
UserSettingsUpdate { id: String, update: UserSettings },
|
|
||||||
|
|
||||||
/// User has been platform banned or deleted their account
|
|
||||||
///
|
|
||||||
/// Clients should remove the following associated data:
|
|
||||||
/// - Messages
|
|
||||||
/// - DM Channels
|
|
||||||
/// - Relationships
|
|
||||||
/// - Server Memberships
|
|
||||||
///
|
|
||||||
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
|
|
||||||
UserPlatformWipe { user_id: String, flags: i32 },
|
|
||||||
/// New emoji
|
|
||||||
EmojiCreate(Emoji),
|
|
||||||
|
|
||||||
/// Delete emoji
|
|
||||||
EmojiDelete { id: String },
|
|
||||||
|
|
||||||
/*/// New report
|
|
||||||
ReportCreate(Report), */
|
|
||||||
/// New channel
|
|
||||||
ChannelCreate(Channel),
|
|
||||||
|
|
||||||
/// Update existing channel
|
|
||||||
ChannelUpdate {
|
|
||||||
id: String,
|
|
||||||
data: PartialChannel,
|
|
||||||
clear: Vec<FieldsChannel>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Delete channel
|
|
||||||
ChannelDelete { id: String },
|
|
||||||
|
|
||||||
/// User joins a group
|
|
||||||
ChannelGroupJoin { id: String, user: String },
|
|
||||||
|
|
||||||
/// User leaves a group
|
|
||||||
ChannelGroupLeave { id: String, user: String },
|
|
||||||
|
|
||||||
/// User started typing in a channel
|
|
||||||
ChannelStartTyping { id: String, user: String },
|
|
||||||
|
|
||||||
/// User stopped typing in a channel
|
|
||||||
ChannelStopTyping { id: String, user: String },
|
|
||||||
|
|
||||||
/// User acknowledged message in channel
|
|
||||||
ChannelAck {
|
|
||||||
id: String,
|
|
||||||
user: String,
|
|
||||||
message_id: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// New webhook
|
|
||||||
WebhookCreate(Webhook),
|
|
||||||
|
|
||||||
/// Update existing webhook
|
|
||||||
WebhookUpdate {
|
|
||||||
id: String,
|
|
||||||
data: PartialWebhook,
|
|
||||||
remove: Vec<FieldsWebhook>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Delete webhook
|
|
||||||
WebhookDelete { id: String },
|
|
||||||
|
|
||||||
/// Auth events
|
|
||||||
Auth(AuthifierEvent),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventV1 {
|
|
||||||
/// Publish helper wrapper
|
|
||||||
pub async fn p(self, channel: String) {
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
redis_kiss::p(channel, self).await;
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
info!("Publishing event to {channel}: {self:?}");
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
redis_kiss::publish(channel, self).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish user event
|
|
||||||
pub async fn p_user(self, id: String, db: &Database) {
|
|
||||||
self.clone().p(id.clone()).await;
|
|
||||||
|
|
||||||
// ! FIXME: this should be captured by member list in the future
|
|
||||||
// ! and not immediately fanned out to users
|
|
||||||
if let Ok(members) = db.fetch_all_memberships(&id).await {
|
|
||||||
for member in members {
|
|
||||||
self.clone().p(member.id.server).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish private event
|
|
||||||
pub async fn private(self, id: String) {
|
|
||||||
self.p(format!("{id}!")).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish internal global event
|
|
||||||
pub async fn global(self) {
|
|
||||||
self.p("global".to_string()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod client;
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#[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, 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::*;
|
|
||||||
|
|
||||||
pub mod events;
|
|
||||||
pub mod tasks;
|
|
||||||
|
|
||||||
/// Utility function to check if a boolean value is false
|
|
||||||
pub fn if_false(t: &bool) -> bool {
|
|
||||||
!t
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractMigrations: Sync + Send {
|
|
||||||
#[cfg(test)]
|
|
||||||
/// Drop the database
|
|
||||||
async fn drop_database(&self);
|
|
||||||
|
|
||||||
/// Migrate the database
|
|
||||||
async fn migrate_database(&self) -> Result<(), ()>;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
use revolt_config::config;
|
|
||||||
use revolt_result::Result;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::{BotInformation, Database, PartialUser, User};
|
|
||||||
|
|
||||||
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::derivable_impls)]
|
|
||||||
impl Default for Bot {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: Default::default(),
|
|
||||||
owner: Default::default(),
|
|
||||||
token: Default::default(),
|
|
||||||
public: Default::default(),
|
|
||||||
analytics: Default::default(),
|
|
||||||
discoverable: Default::default(),
|
|
||||||
interactions_url: Default::default(),
|
|
||||||
terms_of_service_url: Default::default(),
|
|
||||||
privacy_policy_url: Default::default(),
|
|
||||||
flags: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
impl Bot {
|
|
||||||
/// Create a new bot
|
|
||||||
pub async fn create<D>(db: &Database, username: String, owner: &User, data: D) -> Result<Bot>
|
|
||||||
where
|
|
||||||
D: Into<Option<PartialBot>>,
|
|
||||||
{
|
|
||||||
if owner.bot.is_some() {
|
|
||||||
return Err(create_error!(IsBot));
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = config().await;
|
|
||||||
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
|
|
||||||
return Err(create_error!(ReachedMaximumBots));
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = Ulid::new().to_string();
|
|
||||||
|
|
||||||
User::create(
|
|
||||||
db,
|
|
||||||
username,
|
|
||||||
Some(id.to_string()),
|
|
||||||
Some(PartialUser {
|
|
||||||
bot: Some(BotInformation {
|
|
||||||
owner: id.to_string(),
|
|
||||||
}),
|
|
||||||
..Default::default()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut bot = Bot {
|
|
||||||
id,
|
|
||||||
owner: owner.id.to_string(),
|
|
||||||
token: nanoid::nanoid!(64),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(data) = data.into() {
|
|
||||||
bot.apply_options(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.insert_bot(&bot).await?;
|
|
||||||
Ok(bot)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a field from this object
|
|
||||||
pub fn remove_field(&mut self, field: &FieldsBot) {
|
|
||||||
match field {
|
|
||||||
FieldsBot::Token => self.token = nanoid::nanoid!(64),
|
|
||||||
FieldsBot::InteractionsURL => {
|
|
||||||
self.interactions_url = String::new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update this bot
|
|
||||||
pub async fn update(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
mut partial: PartialBot,
|
|
||||||
remove: Vec<FieldsBot>,
|
|
||||||
) -> Result<()> {
|
|
||||||
if remove.contains(&FieldsBot::Token) {
|
|
||||||
partial.token = Some(nanoid::nanoid!(64));
|
|
||||||
}
|
|
||||||
|
|
||||||
for field in &remove {
|
|
||||||
self.remove_field(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.update_bot(&self.id, &partial, remove).await?;
|
|
||||||
|
|
||||||
self.apply_options(partial);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete this bot
|
|
||||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
|
||||||
db.fetch_user(&self.id).await?.mark_deleted(db).await?;
|
|
||||||
db.delete_bot(&self.id).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{Bot, FieldsBot, PartialBot, User};
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn crud() {
|
|
||||||
database_test!(|db| async move {
|
|
||||||
let owner = User::create(&db, "Owner".to_string(), None, None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let bot = Bot::create(
|
|
||||||
&db,
|
|
||||||
"Bot Name".to_string(),
|
|
||||||
&owner,
|
|
||||||
PartialBot {
|
|
||||||
token: Some("my token".to_string()),
|
|
||||||
interactions_url: Some("some url".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(!bot.interactions_url.is_empty());
|
|
||||||
|
|
||||||
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(&owner.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(&owner.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(&owner.id).await.unwrap());
|
|
||||||
assert_eq!(db.fetch_user(&bot.id).await.unwrap().flags, Some(2))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::{Bot, FieldsBot, PartialBot};
|
|
||||||
use crate::{IntoDocumentPath, MongoDb};
|
|
||||||
|
|
||||||
use super::AbstractBots;
|
|
||||||
|
|
||||||
static COL: &str = "bots";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractBots for MongoDb {
|
|
||||||
/// Insert new bot into the database
|
|
||||||
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &bot).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a bot by its id
|
|
||||||
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
|
|
||||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a bot by its token
|
|
||||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find_one,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"token": token
|
|
||||||
}
|
|
||||||
)?
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch bots owned by a user
|
|
||||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"owner": user_id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the number of bots owned by a user
|
|
||||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
count_documents,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"owner": user_id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.map(|v| v as usize)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update bot with new information
|
|
||||||
async fn update_bot(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
partial: &PartialBot,
|
|
||||||
remove: Vec<FieldsBot>,
|
|
||||||
) -> Result<()> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
update_one_by_id,
|
|
||||||
COL,
|
|
||||||
id,
|
|
||||||
partial,
|
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a bot from the database
|
|
||||||
async fn delete_bot(&self, id: &str) -> Result<()> {
|
|
||||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsBot {
|
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
|
||||||
match self {
|
|
||||||
FieldsBot::InteractionsURL => Some("interactions_url"),
|
|
||||||
FieldsBot::Token => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::ReferenceDb;
|
|
||||||
use crate::{Bot, FieldsBot, PartialBot};
|
|
||||||
|
|
||||||
use super::AbstractBots;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractBots for ReferenceDb {
|
|
||||||
/// Insert new bot into the database
|
|
||||||
async fn insert_bot(&self, bot: &Bot) -> Result<()> {
|
|
||||||
let mut bots = self.bots.lock().await;
|
|
||||||
if bots.contains_key(&bot.id) {
|
|
||||||
Err(create_database_error!("insert", "bot"))
|
|
||||||
} else {
|
|
||||||
bots.insert(bot.id.to_string(), bot.clone());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a bot by its id
|
|
||||||
async fn fetch_bot(&self, id: &str) -> Result<Bot> {
|
|
||||||
let bots = self.bots.lock().await;
|
|
||||||
bots.get(id).cloned().ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a bot by its token
|
|
||||||
async fn fetch_bot_by_token(&self, token: &str) -> Result<Bot> {
|
|
||||||
let bots = self.bots.lock().await;
|
|
||||||
bots.values()
|
|
||||||
.find(|bot| bot.token == token)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch bots owned by a user
|
|
||||||
async fn fetch_bots_by_user(&self, user_id: &str) -> Result<Vec<Bot>> {
|
|
||||||
let bots = self.bots.lock().await;
|
|
||||||
Ok(bots
|
|
||||||
.values()
|
|
||||||
.filter(|bot| bot.owner == user_id)
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the number of bots owned by a user
|
|
||||||
async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result<usize> {
|
|
||||||
let bots = self.bots.lock().await;
|
|
||||||
Ok(bots.values().filter(|bot| bot.owner == user_id).count())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update bot with new information
|
|
||||||
async fn update_bot(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
partial: &PartialBot,
|
|
||||||
remove: Vec<FieldsBot>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut bots = self.bots.lock().await;
|
|
||||||
if let Some(bot) = bots.get_mut(id) {
|
|
||||||
for field in remove {
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
bot.remove_field(&field);
|
|
||||||
}
|
|
||||||
|
|
||||||
bot.apply_options(partial.clone());
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a bot from the database
|
|
||||||
async fn delete_bot(&self, id: &str) -> Result<()> {
|
|
||||||
let mut bots = self.bots.lock().await;
|
|
||||||
if bots.remove(id).is_some() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
use revolt_result::{create_error, Result};
|
|
||||||
|
|
||||||
use crate::Database;
|
|
||||||
|
|
||||||
/* static ALPHABET: [char; 54] = [
|
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J',
|
|
||||||
'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
|
||||||
'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
|
|
||||||
]; */
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
/// Invite
|
|
||||||
pub enum Invite {
|
|
||||||
/// Invite to a specific server channel
|
|
||||||
Server {
|
|
||||||
/// Invite code
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
code: String,
|
|
||||||
/// Id of the server this invite points to
|
|
||||||
server: String,
|
|
||||||
/// Id of user who created this invite
|
|
||||||
creator: String,
|
|
||||||
/// Id of the server channel this invite points to
|
|
||||||
channel: String,
|
|
||||||
},
|
|
||||||
/// Invite to a group channel
|
|
||||||
Group {
|
|
||||||
/// Invite code
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
code: String,
|
|
||||||
/// Id of user who created this invite
|
|
||||||
creator: String,
|
|
||||||
/// Id of the group channel this invite points to
|
|
||||||
channel: String,
|
|
||||||
}, /* User {
|
|
||||||
code: String,
|
|
||||||
user: String
|
|
||||||
} */
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
impl Invite {
|
|
||||||
/// Get the invite code for this invite
|
|
||||||
pub fn code(&'_ self) -> &'_ str {
|
|
||||||
match self {
|
|
||||||
Invite::Server { code, .. } | Invite::Group { code, .. } => code,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the ID of the user who created this invite
|
|
||||||
pub fn creator(&'_ self) -> &'_ str {
|
|
||||||
match self {
|
|
||||||
Invite::Server { creator, .. } | Invite::Group { creator, .. } => creator,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new invite from given information
|
|
||||||
/*pub async fn create_channel_invite(db: &Database, creator_id: String, target: &Channel) -> Result<Invite> {
|
|
||||||
let code = nanoid::nanoid!(8, &ALPHABET);
|
|
||||||
let invite = match &target {
|
|
||||||
Channel::Group { id, .. } => Ok(Invite::Group {
|
|
||||||
code,
|
|
||||||
creator: creator.id.clone(),
|
|
||||||
channel: id.clone(),
|
|
||||||
}),
|
|
||||||
Channel::TextChannel { id, server, .. } | Channel::VoiceChannel { id, server, .. } => {
|
|
||||||
Ok(Invite::Server {
|
|
||||||
code,
|
|
||||||
creator: creator.id.clone(),
|
|
||||||
server: server.clone(),
|
|
||||||
channel: id.clone(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => Err(Error::InvalidOperation),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
db.insert_invite(&invite).await?;
|
|
||||||
Ok(invite)
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/// Resolve an invite by its ID or by a public server ID
|
|
||||||
pub async fn find(db: &Database, code: &str) -> Result<Invite> {
|
|
||||||
if let Ok(invite) = db.fetch_invite(code).await {
|
|
||||||
return Ok(invite);
|
|
||||||
} else if let Ok(server) = db.fetch_server(code).await {
|
|
||||||
if server.discoverable {
|
|
||||||
if let Some(channel) = server.channels.into_iter().next() {
|
|
||||||
return Ok(Invite::Server {
|
|
||||||
code: code.to_string(),
|
|
||||||
server: server.id,
|
|
||||||
creator: server.owner,
|
|
||||||
channel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::Invite;
|
|
||||||
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractChannelInvites: Sync + Send {
|
|
||||||
/// Insert a new invite into the database
|
|
||||||
async fn insert_invite(&self, invite: &Invite) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch an invite by its id
|
|
||||||
async fn fetch_invite(&self, code: &str) -> Result<Invite>;
|
|
||||||
|
|
||||||
/// Fetch all invites for a server
|
|
||||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>>;
|
|
||||||
|
|
||||||
/// Delete an invite by its id
|
|
||||||
async fn delete_invite(&self, code: &str) -> Result<()>;
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use futures::StreamExt;
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::Invite;
|
|
||||||
use crate::MongoDb;
|
|
||||||
|
|
||||||
use super::AbstractChannelInvites;
|
|
||||||
|
|
||||||
static COL: &str = "channel_invites";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannelInvites for MongoDb {
|
|
||||||
/// Insert a new invite into the database
|
|
||||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &invite).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch an invite by the code
|
|
||||||
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
|
|
||||||
query!(self, find_one_by_id, COL, code)?.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all invites for a server
|
|
||||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
|
|
||||||
Ok(self
|
|
||||||
.col::<Invite>(COL)
|
|
||||||
.find(
|
|
||||||
doc! {
|
|
||||||
"server": server_id,
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| create_database_error!("find", COL))?
|
|
||||||
.filter_map(|s| async {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
Some(s.unwrap())
|
|
||||||
} else {
|
|
||||||
s.ok()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
.await)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete an invite by its code
|
|
||||||
async fn delete_invite(&self, code: &str) -> Result<()> {
|
|
||||||
query!(self, delete_one_by_id, COL, code).map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::Invite;
|
|
||||||
use crate::ReferenceDb;
|
|
||||||
|
|
||||||
use super::AbstractChannelInvites;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannelInvites for ReferenceDb {
|
|
||||||
/// Insert a new invite into the database
|
|
||||||
async fn insert_invite(&self, invite: &Invite) -> Result<()> {
|
|
||||||
let mut invites = self.channel_invites.lock().await;
|
|
||||||
if invites.contains_key(invite.code()) {
|
|
||||||
Err(create_database_error!("insert", "invite"))
|
|
||||||
} else {
|
|
||||||
invites.insert(invite.code().to_string(), invite.clone());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch an invite by the code
|
|
||||||
async fn fetch_invite(&self, code: &str) -> Result<Invite> {
|
|
||||||
let invites = self.channel_invites.lock().await;
|
|
||||||
invites
|
|
||||||
.get(code)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all invites for a server
|
|
||||||
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
|
|
||||||
let invites = self.channel_invites.lock().await;
|
|
||||||
Ok(invites
|
|
||||||
.values()
|
|
||||||
.filter(|invite| match invite {
|
|
||||||
Invite::Server { server, .. } => server == server_id,
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete an invite by its code
|
|
||||||
async fn delete_invite(&self, code: &str) -> Result<()> {
|
|
||||||
let mut invites = self.channel_invites.lock().await;
|
|
||||||
if invites.remove(code).is_some() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
auto_derived!(
|
|
||||||
/// Channel Unread
|
|
||||||
pub struct ChannelUnread {
|
|
||||||
/// Composite key pointing to a user's view of a channel
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: ChannelCompositeKey,
|
|
||||||
|
|
||||||
/// Id of the last message read in this channel by a user
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub last_id: Option<String>,
|
|
||||||
/// Array of message ids that mention the user
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub mentions: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Composite primary key consisting of channel and user id
|
|
||||||
#[derive(Hash)]
|
|
||||||
pub struct ChannelCompositeKey {
|
|
||||||
/// Channel Id
|
|
||||||
pub channel: String,
|
|
||||||
/// User Id
|
|
||||||
pub user: String,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::ChannelUnread;
|
|
||||||
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractChannelUnreads: Sync + Send {
|
|
||||||
/// Acknowledge a message.
|
|
||||||
async fn acknowledge_message(
|
|
||||||
&self,
|
|
||||||
channel_id: &str,
|
|
||||||
user_id: &str,
|
|
||||||
message_id: &str,
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
/// Acknowledge many channels.
|
|
||||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
|
|
||||||
|
|
||||||
/// Add a mention.
|
|
||||||
async fn add_mention_to_unread<'a>(
|
|
||||||
&self,
|
|
||||||
channel_id: &str,
|
|
||||||
user_id: &str,
|
|
||||||
message_ids: &[String],
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch all channel unreads for a user.
|
|
||||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
use bson::Document;
|
|
||||||
use mongodb::options::UpdateOptions;
|
|
||||||
use revolt_result::Result;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::ChannelUnread;
|
|
||||||
use crate::MongoDb;
|
|
||||||
|
|
||||||
use super::AbstractChannelUnreads;
|
|
||||||
|
|
||||||
static COL: &str = "channel_unreads";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannelUnreads for MongoDb {
|
|
||||||
/// Acknowledge a message.
|
|
||||||
async fn acknowledge_message(
|
|
||||||
&self,
|
|
||||||
channel_id: &str,
|
|
||||||
user_id: &str,
|
|
||||||
message_id: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id.channel": channel_id,
|
|
||||||
"_id.user": user_id,
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$unset": {
|
|
||||||
"mentions": 1_i32
|
|
||||||
},
|
|
||||||
"$set": {
|
|
||||||
"last_id": message_id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UpdateOptions::builder().upsert(true).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Acknowledge many channels.
|
|
||||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
|
|
||||||
let current_time = Ulid::new().to_string();
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"_id.channel": {
|
|
||||||
"$in": channel_ids
|
|
||||||
},
|
|
||||||
"_id.user": user_id
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| create_database_error!("delete_many", COL))?;
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.insert_many(
|
|
||||||
channel_ids
|
|
||||||
.iter()
|
|
||||||
.map(|channel_id| {
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"channel": channel_id,
|
|
||||||
"user": user_id
|
|
||||||
},
|
|
||||||
"last_id": ¤t_time
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<Document>>(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_many", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a mention.
|
|
||||||
async fn add_mention_to_unread<'a>(
|
|
||||||
&self,
|
|
||||||
channel_id: &str,
|
|
||||||
user_id: &str,
|
|
||||||
message_ids: &[String],
|
|
||||||
) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id.channel": channel_id,
|
|
||||||
"_id.user": user_id,
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$push": {
|
|
||||||
"mentions": {
|
|
||||||
"$each": message_ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
UpdateOptions::builder().upsert(true).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all channel unreads for a user.
|
|
||||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.user": user_id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::{ChannelCompositeKey, ChannelUnread, ReferenceDb};
|
|
||||||
|
|
||||||
use super::AbstractChannelUnreads;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannelUnreads for ReferenceDb {
|
|
||||||
/// Acknowledge a message.
|
|
||||||
async fn acknowledge_message(
|
|
||||||
&self,
|
|
||||||
channel_id: &str,
|
|
||||||
user_id: &str,
|
|
||||||
message_id: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut unreads = self.channel_unreads.lock().await;
|
|
||||||
let key = ChannelCompositeKey {
|
|
||||||
channel: channel_id.to_string(),
|
|
||||||
user: user_id.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(unread) = unreads.get_mut(&key) {
|
|
||||||
unread.mentions = None;
|
|
||||||
unread.last_id.replace(message_id.to_string());
|
|
||||||
} else {
|
|
||||||
unreads.insert(
|
|
||||||
key.clone(),
|
|
||||||
ChannelUnread {
|
|
||||||
id: key,
|
|
||||||
last_id: Some(message_id.to_string()),
|
|
||||||
mentions: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Acknowledge many channels.
|
|
||||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()> {
|
|
||||||
let current_time = Ulid::new().to_string();
|
|
||||||
for channel_id in channel_ids {
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
self.acknowledge_message(channel_id, user_id, ¤t_time)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a mention.
|
|
||||||
async fn add_mention_to_unread<'a>(
|
|
||||||
&self,
|
|
||||||
channel_id: &str,
|
|
||||||
user_id: &str,
|
|
||||||
message_ids: &[String],
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut unreads = self.channel_unreads.lock().await;
|
|
||||||
let key = ChannelCompositeKey {
|
|
||||||
channel: channel_id.to_string(),
|
|
||||||
user: user_id.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(unread) = unreads.get_mut(&key) {
|
|
||||||
unread.mentions.replace(message_ids.to_vec());
|
|
||||||
} else {
|
|
||||||
unreads.insert(
|
|
||||||
key.clone(),
|
|
||||||
ChannelUnread {
|
|
||||||
id: key,
|
|
||||||
last_id: None,
|
|
||||||
mentions: Some(message_ids.to_vec()),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all channel unreads for a user.
|
|
||||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
|
|
||||||
let unreads = self.channel_unreads.lock().await;
|
|
||||||
Ok(unreads
|
|
||||||
.values()
|
|
||||||
.filter(|unread| unread.id.user == user_id)
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::events::client::EventV1;
|
|
||||||
use crate::{Database, File};
|
|
||||||
|
|
||||||
auto_derived_partial!(
|
|
||||||
/// Webhook
|
|
||||||
pub struct Webhook {
|
|
||||||
/// Webhook Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
|
|
||||||
/// The name of the webhook
|
|
||||||
pub name: String,
|
|
||||||
|
|
||||||
/// The avatar of the webhook
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub avatar: Option<File>,
|
|
||||||
|
|
||||||
/// The channel this webhook belongs to
|
|
||||||
pub channel_id: String,
|
|
||||||
|
|
||||||
/// The permissions of the webhook
|
|
||||||
pub permissions: u64,
|
|
||||||
|
|
||||||
/// The private token for the webhook
|
|
||||||
pub token: Option<String>,
|
|
||||||
},
|
|
||||||
"PartialWebhook"
|
|
||||||
);
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
/// Optional fields on webhook object
|
|
||||||
pub enum FieldsWebhook {
|
|
||||||
Avatar,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
#[allow(clippy::derivable_impls)]
|
|
||||||
impl Default for Webhook {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: Default::default(),
|
|
||||||
name: Default::default(),
|
|
||||||
avatar: None,
|
|
||||||
channel_id: Default::default(),
|
|
||||||
permissions: Default::default(),
|
|
||||||
token: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
impl Webhook {
|
|
||||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
|
||||||
db.insert_webhook(self).await?;
|
|
||||||
|
|
||||||
// Avoid leaking the token to people who receive the event
|
|
||||||
let mut webhook = self.clone();
|
|
||||||
webhook.token = None;
|
|
||||||
|
|
||||||
EventV1::WebhookCreate(webhook.into())
|
|
||||||
.p(self.channel_id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assert_token(&self, token: &str) -> Result<()> {
|
|
||||||
if self.token.as_deref() == Some(token) {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(InvalidCredentials))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
mut partial: PartialWebhook,
|
|
||||||
remove: Vec<FieldsWebhook>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for field in &remove {
|
|
||||||
self.remove_field(field)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
db.update_webhook(&self.id, &partial, &remove).await?;
|
|
||||||
|
|
||||||
partial.token = None; // Avoid leaking the token to people who receive the event
|
|
||||||
|
|
||||||
EventV1::WebhookUpdate {
|
|
||||||
id: self.id.clone(),
|
|
||||||
data: partial.into(),
|
|
||||||
remove: remove.into_iter().map(|v| v.into()).collect(),
|
|
||||||
}
|
|
||||||
.p(self.channel_id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_field(&mut self, field: &FieldsWebhook) {
|
|
||||||
match field {
|
|
||||||
FieldsWebhook::Avatar => self.avatar = None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
|
||||||
db.delete_webhook(&self.id).await?;
|
|
||||||
|
|
||||||
EventV1::WebhookDelete {
|
|
||||||
id: self.id.clone(),
|
|
||||||
}
|
|
||||||
.p(self.channel_id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn crud() {
|
|
||||||
database_test!(|db| async move {
|
|
||||||
let webhook_id = "webhook";
|
|
||||||
let channel_id = "channel";
|
|
||||||
|
|
||||||
let webhook = Webhook {
|
|
||||||
id: webhook_id.to_string(),
|
|
||||||
name: "Webhook Name".to_string(),
|
|
||||||
channel_id: channel_id.to_string(),
|
|
||||||
avatar: None,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
db.insert_webhook(&webhook).await.unwrap();
|
|
||||||
|
|
||||||
let mut updated_webhook = webhook.clone();
|
|
||||||
updated_webhook
|
|
||||||
.update(
|
|
||||||
&db,
|
|
||||||
PartialWebhook {
|
|
||||||
name: Some("New Name".to_string()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![FieldsWebhook::Avatar],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let fetched_webhook = db.fetch_webhook(webhook_id).await.unwrap();
|
|
||||||
let fetched_webhooks = db.fetch_webhooks_for_channel(channel_id).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(updated_webhook, fetched_webhook);
|
|
||||||
assert_ne!(webhook, fetched_webhook);
|
|
||||||
assert_eq!(1, fetched_webhooks.len());
|
|
||||||
assert_eq!(fetched_webhook, fetched_webhooks[0]);
|
|
||||||
|
|
||||||
webhook.delete(&db).await.unwrap();
|
|
||||||
assert!(db.fetch_webhook(webhook_id).await.is_err());
|
|
||||||
assert_eq!(
|
|
||||||
0,
|
|
||||||
db.fetch_webhooks_for_channel(channel_id)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.len()
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
|
||||||
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractWebhooks: Sync + Send {
|
|
||||||
/// Insert new webhook into the database
|
|
||||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch webhook by id
|
|
||||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook>;
|
|
||||||
|
|
||||||
/// Fetch webhooks for channel
|
|
||||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>>;
|
|
||||||
|
|
||||||
/// Update webhook with new information
|
|
||||||
async fn update_webhook(
|
|
||||||
&self,
|
|
||||||
webhook_id: &str,
|
|
||||||
partial: &PartialWebhook,
|
|
||||||
remove: &[FieldsWebhook],
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
/// Delete webhook by id
|
|
||||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()>;
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
use futures::StreamExt;
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
|
||||||
use crate::{IntoDocumentPath, MongoDb};
|
|
||||||
|
|
||||||
use super::AbstractWebhooks;
|
|
||||||
|
|
||||||
static COL: &str = "channel_webhooks";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractWebhooks for MongoDb {
|
|
||||||
/// Insert new webhook into the database
|
|
||||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &webhook).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch webhook by id
|
|
||||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
|
|
||||||
query!(self, find_one_by_id, COL, webhook_id)?.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch webhooks for channel
|
|
||||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
|
||||||
Ok(self
|
|
||||||
.col::<Webhook>(COL)
|
|
||||||
.find(
|
|
||||||
doc! {
|
|
||||||
"channel_id": channel_id,
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| create_database_error!("find", COL))?
|
|
||||||
.filter_map(|s| async {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
Some(s.unwrap())
|
|
||||||
} else {
|
|
||||||
s.ok()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
.await)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update webhook with new information
|
|
||||||
async fn update_webhook(
|
|
||||||
&self,
|
|
||||||
webhook_id: &str,
|
|
||||||
partial: &PartialWebhook,
|
|
||||||
remove: &[FieldsWebhook],
|
|
||||||
) -> Result<()> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
update_one_by_id,
|
|
||||||
COL,
|
|
||||||
webhook_id,
|
|
||||||
partial,
|
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete webhook by id
|
|
||||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
|
|
||||||
query!(self, delete_one_by_id, COL, webhook_id).map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsWebhook {
|
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
|
||||||
Some(match self {
|
|
||||||
FieldsWebhook::Avatar => "avatar",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::ReferenceDb;
|
|
||||||
use crate::{FieldsWebhook, PartialWebhook, Webhook};
|
|
||||||
|
|
||||||
use super::AbstractWebhooks;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractWebhooks for ReferenceDb {
|
|
||||||
/// Insert new webhook into the database
|
|
||||||
async fn insert_webhook(&self, webhook: &Webhook) -> Result<()> {
|
|
||||||
let mut webhooks = self.channel_webhooks.lock().await;
|
|
||||||
if webhooks.contains_key(&webhook.id) {
|
|
||||||
Err(create_database_error!("insert", "webhook"))
|
|
||||||
} else {
|
|
||||||
webhooks.insert(webhook.id.to_string(), webhook.clone());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch webhook by id
|
|
||||||
async fn fetch_webhook(&self, webhook_id: &str) -> Result<Webhook> {
|
|
||||||
let webhooks = self.channel_webhooks.lock().await;
|
|
||||||
webhooks
|
|
||||||
.get(webhook_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch webhooks for channel
|
|
||||||
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
|
|
||||||
let webhooks = self.channel_webhooks.lock().await;
|
|
||||||
Ok(webhooks
|
|
||||||
.values()
|
|
||||||
.filter(|webhook| webhook.channel_id == channel_id)
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update webhook with new information
|
|
||||||
async fn update_webhook(
|
|
||||||
&self,
|
|
||||||
webhook_id: &str,
|
|
||||||
partial: &PartialWebhook,
|
|
||||||
remove: &[FieldsWebhook],
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut webhooks = self.channel_webhooks.lock().await;
|
|
||||||
if let Some(webhook) = webhooks.get_mut(webhook_id) {
|
|
||||||
for field in remove {
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
webhook.remove_field(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
webhook.apply_options(partial.clone());
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete webhook by id
|
|
||||||
async fn delete_webhook(&self, webhook_id: &str) -> Result<()> {
|
|
||||||
let mut webhooks = self.channel_webhooks.lock().await;
|
|
||||||
if webhooks.remove(webhook_id).is_some() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,723 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use revolt_config::config;
|
|
||||||
use revolt_models::v0::{self, MessageAuthor};
|
|
||||||
use revolt_permissions::OverrideField;
|
|
||||||
use revolt_result::Result;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server,
|
|
||||||
SystemMessage, User,
|
|
||||||
};
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
#[serde(tag = "channel_type")]
|
|
||||||
pub enum Channel {
|
|
||||||
/// Personal "Saved Notes" channel which allows users to save messages
|
|
||||||
SavedMessages {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
/// Id of the user this channel belongs to
|
|
||||||
user: String,
|
|
||||||
},
|
|
||||||
/// Direct message channel between two users
|
|
||||||
DirectMessage {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
|
|
||||||
/// Whether this direct message channel is currently open on both sides
|
|
||||||
active: bool,
|
|
||||||
/// 2-tuple of user ids participating in direct message
|
|
||||||
recipients: Vec<String>,
|
|
||||||
/// Id of the last message sent in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
last_message_id: Option<String>,
|
|
||||||
},
|
|
||||||
/// Group channel between 1 or more participants
|
|
||||||
Group {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
|
|
||||||
/// Display name of the channel
|
|
||||||
name: String,
|
|
||||||
/// User id of the owner of the group
|
|
||||||
owner: String,
|
|
||||||
/// Channel description
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
description: Option<String>,
|
|
||||||
/// Array of user ids participating in channel
|
|
||||||
recipients: Vec<String>,
|
|
||||||
|
|
||||||
/// Custom icon attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
icon: Option<File>,
|
|
||||||
/// Id of the last message sent in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
last_message_id: Option<String>,
|
|
||||||
|
|
||||||
/// Permissions assigned to members of this group
|
|
||||||
/// (does not apply to the owner of the group)
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
permissions: Option<i64>,
|
|
||||||
|
|
||||||
/// Whether this group is marked as not safe for work
|
|
||||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
|
||||||
nsfw: bool,
|
|
||||||
},
|
|
||||||
/// Text channel belonging to a server
|
|
||||||
TextChannel {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
/// Id of the server this channel belongs to
|
|
||||||
server: String,
|
|
||||||
|
|
||||||
/// Display name of the channel
|
|
||||||
name: String,
|
|
||||||
/// Channel description
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
description: Option<String>,
|
|
||||||
|
|
||||||
/// Custom icon attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
icon: Option<File>,
|
|
||||||
/// Id of the last message sent in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
last_message_id: Option<String>,
|
|
||||||
|
|
||||||
/// Default permissions assigned to users in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
default_permissions: Option<OverrideField>,
|
|
||||||
/// Permissions assigned based on role to this channel
|
|
||||||
#[serde(
|
|
||||||
default = "HashMap::<String, OverrideField>::new",
|
|
||||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
|
||||||
)]
|
|
||||||
role_permissions: HashMap<String, OverrideField>,
|
|
||||||
|
|
||||||
/// Whether this channel is marked as not safe for work
|
|
||||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
|
||||||
nsfw: bool,
|
|
||||||
},
|
|
||||||
/// Voice channel belonging to a server
|
|
||||||
VoiceChannel {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
id: String,
|
|
||||||
/// Id of the server this channel belongs to
|
|
||||||
server: String,
|
|
||||||
|
|
||||||
/// Display name of the channel
|
|
||||||
name: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
/// Channel description
|
|
||||||
description: Option<String>,
|
|
||||||
/// Custom icon attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
icon: Option<File>,
|
|
||||||
|
|
||||||
/// Default permissions assigned to users in this channel
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
default_permissions: Option<OverrideField>,
|
|
||||||
/// Permissions assigned based on role to this channel
|
|
||||||
#[serde(
|
|
||||||
default = "HashMap::<String, OverrideField>::new",
|
|
||||||
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
|
|
||||||
)]
|
|
||||||
role_permissions: HashMap<String, OverrideField>,
|
|
||||||
|
|
||||||
/// Whether this channel is marked as not safe for work
|
|
||||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
|
||||||
nsfw: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct PartialChannel {
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub name: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub owner: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub icon: Option<File>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nsfw: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub active: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub permissions: Option<i64>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub role_permissions: Option<HashMap<String, OverrideField>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub default_permissions: Option<OverrideField>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub last_message_id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional fields on channel object
|
|
||||||
pub enum FieldsChannel {
|
|
||||||
Description,
|
|
||||||
Icon,
|
|
||||||
DefaultPermissions,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
impl Channel {
|
|
||||||
/* /// Create a channel
|
|
||||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
|
||||||
db.insert_channel(self).await?;
|
|
||||||
|
|
||||||
let event = EventV1::ChannelCreate(self.clone().into());
|
|
||||||
match self {
|
|
||||||
Self::SavedMessages { user, .. } => event.private(user.clone()).await,
|
|
||||||
Self::DirectMessage { recipients, .. } | Self::Group { recipients, .. } => {
|
|
||||||
for recipient in recipients {
|
|
||||||
event.clone().private(recipient.clone()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => {
|
|
||||||
event.p(server.clone()).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}*/
|
|
||||||
|
|
||||||
/// Create a new server channel
|
|
||||||
pub async fn create_server_channel(
|
|
||||||
db: &Database,
|
|
||||||
server: &mut Server,
|
|
||||||
data: v0::DataCreateServerChannel,
|
|
||||||
update_server: bool,
|
|
||||||
) -> Result<Channel> {
|
|
||||||
let config = config().await;
|
|
||||||
if server.channels.len() > config.features.limits.default.server_channels {
|
|
||||||
return Err(create_error!(TooManyChannels {
|
|
||||||
max: config.features.limits.default.server_channels,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
let id = ulid::Ulid::new().to_string();
|
|
||||||
let channel = match data.channel_type {
|
|
||||||
v0::LegacyServerChannelType::Text => Channel::TextChannel {
|
|
||||||
id: id.clone(),
|
|
||||||
server: server.id.to_owned(),
|
|
||||||
name: data.name,
|
|
||||||
description: data.description,
|
|
||||||
icon: None,
|
|
||||||
last_message_id: None,
|
|
||||||
default_permissions: None,
|
|
||||||
role_permissions: HashMap::new(),
|
|
||||||
nsfw: data.nsfw.unwrap_or(false),
|
|
||||||
},
|
|
||||||
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
|
|
||||||
id: id.clone(),
|
|
||||||
server: server.id.to_owned(),
|
|
||||||
name: data.name,
|
|
||||||
description: data.description,
|
|
||||||
icon: None,
|
|
||||||
default_permissions: None,
|
|
||||||
role_permissions: HashMap::new(),
|
|
||||||
nsfw: data.nsfw.unwrap_or(false),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
db.insert_channel(&channel).await?;
|
|
||||||
|
|
||||||
if update_server {
|
|
||||||
server
|
|
||||||
.update(
|
|
||||||
db,
|
|
||||||
PartialServer {
|
|
||||||
channels: Some([server.channels.clone(), [id].into()].concat()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
EventV1::ChannelCreate(channel.clone().into())
|
|
||||||
.p(server.id.clone())
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a group
|
|
||||||
pub async fn create_group(
|
|
||||||
db: &Database,
|
|
||||||
mut data: v0::DataCreateGroup,
|
|
||||||
owner_id: String,
|
|
||||||
) -> Result<Channel> {
|
|
||||||
data.users.insert(owner_id.to_string());
|
|
||||||
|
|
||||||
let config = config().await;
|
|
||||||
if data.users.len() > config.features.limits.default.group_size {
|
|
||||||
return Err(create_error!(GroupTooLarge {
|
|
||||||
max: config.features.limits.default.group_size,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let recipients = data.users.into_iter().collect::<Vec<String>>();
|
|
||||||
let channel = Channel::Group {
|
|
||||||
id: ulid::Ulid::new().to_string(),
|
|
||||||
|
|
||||||
name: data.name,
|
|
||||||
owner: owner_id,
|
|
||||||
description: data.description,
|
|
||||||
recipients: recipients.clone(),
|
|
||||||
|
|
||||||
icon: None,
|
|
||||||
last_message_id: None,
|
|
||||||
|
|
||||||
permissions: None,
|
|
||||||
|
|
||||||
nsfw: data.nsfw.unwrap_or(false),
|
|
||||||
};
|
|
||||||
|
|
||||||
db.insert_channel(&channel).await?;
|
|
||||||
|
|
||||||
let event = EventV1::ChannelCreate(channel.clone().into());
|
|
||||||
for recipient in recipients {
|
|
||||||
event.clone().private(recipient).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add user to a group
|
|
||||||
pub async fn add_user_to_group(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
user: &User,
|
|
||||||
by_id: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
if let Channel::Group { recipients, .. } = self {
|
|
||||||
if recipients.contains(&String::from(&user.id)) {
|
|
||||||
return Err(create_error!(AlreadyInGroup));
|
|
||||||
}
|
|
||||||
|
|
||||||
recipients.push(String::from(&user.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
match &self {
|
|
||||||
Channel::Group { id, .. } => {
|
|
||||||
db.add_user_to_group(id, &user.id).await?;
|
|
||||||
|
|
||||||
EventV1::ChannelGroupJoin {
|
|
||||||
id: id.to_string(),
|
|
||||||
user: user.id.to_string(),
|
|
||||||
}
|
|
||||||
.p(id.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
SystemMessage::UserAdded {
|
|
||||||
id: user.id.to_string(),
|
|
||||||
by: by_id.to_string(),
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.send(
|
|
||||||
db,
|
|
||||||
MessageAuthor::System {
|
|
||||||
username: &user.username,
|
|
||||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
|
||||||
},
|
|
||||||
self,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
EventV1::ChannelCreate(self.clone().into())
|
|
||||||
.private(user.id.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err(create_error!(InvalidOperation)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map out whether it is a direct DM
|
|
||||||
pub fn is_direct_dm(&self) -> bool {
|
|
||||||
matches!(self, Channel::DirectMessage { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether has a user as a recipient
|
|
||||||
pub fn contains_user(&self, user_id: &str) -> bool {
|
|
||||||
match self {
|
|
||||||
Channel::Group { recipients, .. } => recipients.contains(&String::from(user_id)),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get list of recipients
|
|
||||||
pub fn users(&self) -> Result<Vec<String>> {
|
|
||||||
match self {
|
|
||||||
Channel::Group { recipients, .. } => Ok(recipients.to_owned()),
|
|
||||||
_ => Err(create_error!(NotFound)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clone this channel's id
|
|
||||||
pub fn id(&self) -> String {
|
|
||||||
match self {
|
|
||||||
Channel::DirectMessage { id, .. }
|
|
||||||
| Channel::Group { id, .. }
|
|
||||||
| Channel::SavedMessages { id, .. }
|
|
||||||
| Channel::TextChannel { id, .. }
|
|
||||||
| Channel::VoiceChannel { id, .. } => id.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set role permission on a channel
|
|
||||||
pub async fn set_role_permission(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
role_id: &str,
|
|
||||||
permissions: OverrideField,
|
|
||||||
) -> Result<()> {
|
|
||||||
match self {
|
|
||||||
Channel::TextChannel {
|
|
||||||
id,
|
|
||||||
server,
|
|
||||||
role_permissions,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| Channel::VoiceChannel {
|
|
||||||
id,
|
|
||||||
server,
|
|
||||||
role_permissions,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
db.set_channel_role_permission(id, role_id, permissions)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
role_permissions.insert(role_id.to_string(), permissions);
|
|
||||||
|
|
||||||
EventV1::ChannelUpdate {
|
|
||||||
id: id.clone(),
|
|
||||||
data: PartialChannel {
|
|
||||||
role_permissions: Some(role_permissions.clone()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
clear: vec![],
|
|
||||||
}
|
|
||||||
.p(server.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
_ => Err(create_error!(InvalidOperation)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update channel data
|
|
||||||
pub async fn update(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
partial: PartialChannel,
|
|
||||||
remove: Vec<FieldsChannel>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for field in &remove {
|
|
||||||
self.remove_field(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
let id = self.id().to_string();
|
|
||||||
db.update_channel(&id, &partial, remove.clone()).await?;
|
|
||||||
|
|
||||||
EventV1::ChannelUpdate {
|
|
||||||
id: id.clone(),
|
|
||||||
data: partial.into(),
|
|
||||||
clear: remove.into_iter().map(|v| v.into()).collect(),
|
|
||||||
}
|
|
||||||
.p(match self {
|
|
||||||
Self::TextChannel { server, .. } | Self::VoiceChannel { server, .. } => server.clone(),
|
|
||||||
_ => id,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a field from Channel object
|
|
||||||
pub fn remove_field(&mut self, field: &FieldsChannel) {
|
|
||||||
match field {
|
|
||||||
FieldsChannel::Description => match self {
|
|
||||||
Self::Group { description, .. }
|
|
||||||
| Self::TextChannel { description, .. }
|
|
||||||
| Self::VoiceChannel { description, .. } => {
|
|
||||||
description.take();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
FieldsChannel::Icon => match self {
|
|
||||||
Self::Group { icon, .. }
|
|
||||||
| Self::TextChannel { icon, .. }
|
|
||||||
| Self::VoiceChannel { icon, .. } => {
|
|
||||||
icon.take();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
FieldsChannel::DefaultPermissions => match self {
|
|
||||||
Self::TextChannel {
|
|
||||||
default_permissions,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| Self::VoiceChannel {
|
|
||||||
default_permissions,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
default_permissions.take();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove multiple fields from Channel object
|
|
||||||
pub fn remove_fields(&mut self, partial: Vec<FieldsChannel>) {
|
|
||||||
for field in partial {
|
|
||||||
self.remove_field(&field)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply partial channel to channel
|
|
||||||
pub fn apply_options(&mut self, partial: PartialChannel) {
|
|
||||||
match self {
|
|
||||||
Self::SavedMessages { .. } => {}
|
|
||||||
Self::DirectMessage { active, .. } => {
|
|
||||||
if let Some(v) = partial.active {
|
|
||||||
*active = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Group {
|
|
||||||
name,
|
|
||||||
owner,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
nsfw,
|
|
||||||
permissions,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Some(v) = partial.name {
|
|
||||||
*name = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.owner {
|
|
||||||
*owner = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.description {
|
|
||||||
description.replace(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.icon {
|
|
||||||
icon.replace(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.nsfw {
|
|
||||||
*nsfw = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.permissions {
|
|
||||||
permissions.replace(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::TextChannel {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
nsfw,
|
|
||||||
default_permissions,
|
|
||||||
role_permissions,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| Self::VoiceChannel {
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
nsfw,
|
|
||||||
default_permissions,
|
|
||||||
role_permissions,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if let Some(v) = partial.name {
|
|
||||||
*name = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.description {
|
|
||||||
description.replace(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.icon {
|
|
||||||
icon.replace(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.nsfw {
|
|
||||||
*nsfw = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.role_permissions {
|
|
||||||
*role_permissions = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = partial.default_permissions {
|
|
||||||
default_permissions.replace(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove user from a group
|
|
||||||
pub async fn remove_user_from_group(
|
|
||||||
&self,
|
|
||||||
db: &Database,
|
|
||||||
user: &User,
|
|
||||||
by_id: Option<&str>,
|
|
||||||
silent: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
match &self {
|
|
||||||
Channel::Group {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
owner,
|
|
||||||
recipients,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if &user.id == owner {
|
|
||||||
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
|
|
||||||
db.update_channel(
|
|
||||||
id,
|
|
||||||
&PartialChannel {
|
|
||||||
owner: Some(new_owner.into()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
SystemMessage::ChannelOwnershipChanged {
|
|
||||||
from: owner.to_string(),
|
|
||||||
to: new_owner.to_string(),
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.send(
|
|
||||||
db,
|
|
||||||
MessageAuthor::System {
|
|
||||||
username: name,
|
|
||||||
avatar: None,
|
|
||||||
},
|
|
||||||
self,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
db.delete_channel(self).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventV1::ChannelGroupLeave {
|
|
||||||
id: id.to_string(),
|
|
||||||
user: user.id.to_string(),
|
|
||||||
}
|
|
||||||
.p(id.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if !silent {
|
|
||||||
if let Some(by) = by_id {
|
|
||||||
SystemMessage::UserRemove {
|
|
||||||
id: user.id.to_string(),
|
|
||||||
by: by.to_string(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SystemMessage::UserLeft {
|
|
||||||
id: user.id.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.send(
|
|
||||||
db,
|
|
||||||
MessageAuthor::System {
|
|
||||||
username: &user.username,
|
|
||||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
|
||||||
},
|
|
||||||
self,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => Err(create_error!(InvalidOperation)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a channel
|
|
||||||
pub async fn delete(&self, db: &Database) -> Result<()> {
|
|
||||||
let id = self.id().to_string();
|
|
||||||
EventV1::ChannelDelete { id: id.clone() }.p(id).await;
|
|
||||||
db.delete_channel(self).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsChannel {
|
|
||||||
fn as_path(&self) -> Option<&'static str> {
|
|
||||||
Some(match self {
|
|
||||||
FieldsChannel::Description => "description",
|
|
||||||
FieldsChannel::Icon => "icon",
|
|
||||||
FieldsChannel::DefaultPermissions => "default_permissions",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
|
||||||
|
|
||||||
use crate::{fixture, util::permissions::DatabasePermissionQuery};
|
|
||||||
|
|
||||||
#[async_std::test]
|
|
||||||
async fn permissions_text_channel() {
|
|
||||||
database_test!(|db| async move {
|
|
||||||
fixture!(db, "server_with_roles",
|
|
||||||
owner user 0
|
|
||||||
moderator user 1
|
|
||||||
user user 2
|
|
||||||
channel channel 3);
|
|
||||||
|
|
||||||
let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel);
|
|
||||||
assert!(calculate_channel_permissions(&mut query)
|
|
||||||
.await
|
|
||||||
.has_channel_permission(ChannelPermission::SendMessage));
|
|
||||||
|
|
||||||
let mut query = DatabasePermissionQuery::new(&db, &moderator).channel(&channel);
|
|
||||||
assert!(calculate_channel_permissions(&mut query)
|
|
||||||
.await
|
|
||||||
.has_channel_permission(ChannelPermission::SendMessage));
|
|
||||||
|
|
||||||
let mut query = DatabasePermissionQuery::new(&db, &user).channel(&channel);
|
|
||||||
assert!(!calculate_channel_permissions(&mut query)
|
|
||||||
.await
|
|
||||||
.has_channel_permission(ChannelPermission::SendMessage));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use crate::{revolt_result::Result, Channel, FieldsChannel, PartialChannel};
|
|
||||||
use revolt_permissions::OverrideField;
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractChannels: Sync + Send {
|
|
||||||
/// Insert a new channel in the database
|
|
||||||
async fn insert_channel(&self, channel: &Channel) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch a channel from the database
|
|
||||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel>;
|
|
||||||
|
|
||||||
/// Fetch all channels from the database
|
|
||||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>>;
|
|
||||||
|
|
||||||
/// Fetch all direct messages for a user
|
|
||||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>>;
|
|
||||||
|
|
||||||
// Fetch saved messages channel
|
|
||||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel>;
|
|
||||||
|
|
||||||
// Fetch direct message channel (DM or Saved Messages)
|
|
||||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel>;
|
|
||||||
|
|
||||||
/// Insert a user to a group
|
|
||||||
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
|
||||||
|
|
||||||
/// Insert channel role permissions
|
|
||||||
async fn set_channel_role_permission(
|
|
||||||
&self,
|
|
||||||
channel_id: &str,
|
|
||||||
role_id: &str,
|
|
||||||
permissions: OverrideField,
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
// Update channel
|
|
||||||
async fn update_channel(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
channel_id: &PartialChannel,
|
|
||||||
remove: Vec<FieldsChannel>,
|
|
||||||
) -> Result<()>;
|
|
||||||
|
|
||||||
// Remove a user from a group
|
|
||||||
async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>;
|
|
||||||
|
|
||||||
// Delete a channel
|
|
||||||
async fn delete_channel(&self, channel_id: &Channel) -> Result<()>;
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
use super::AbstractChannels;
|
|
||||||
use crate::{Channel, FieldsChannel, IntoDocumentPath, MongoDb, PartialChannel};
|
|
||||||
use bson::Document;
|
|
||||||
use futures::StreamExt;
|
|
||||||
use revolt_permissions::OverrideField;
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
static COL: &str = "channels";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannels for MongoDb {
|
|
||||||
/// Insert a new channel in the database
|
|
||||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &channel).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a channel from the database
|
|
||||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
|
|
||||||
query!(self, find_one_by_id, COL, channel_id)?.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all channels from the database
|
|
||||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
|
||||||
Ok(self
|
|
||||||
.col::<Channel>(COL)
|
|
||||||
.find(
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| create_database_error!("fetch", "channels"))?
|
|
||||||
.filter_map(|s| async {
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
Some(s.unwrap())
|
|
||||||
} else {
|
|
||||||
s.ok()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
.await)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all direct messages for a user
|
|
||||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"$or": [
|
|
||||||
{
|
|
||||||
"$or": [
|
|
||||||
{
|
|
||||||
"channel_type": "DirectMessage"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"channel_type": "Group"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"recipients": user_id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"channel_type": "SavedMessages",
|
|
||||||
"user": user_id
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch saved messages channel
|
|
||||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find_one,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"channel_type": "SavedMessages",
|
|
||||||
"user": user_id
|
|
||||||
}
|
|
||||||
)?
|
|
||||||
.ok_or_else(|| create_error!(InternalError))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch direct message channel (DM or Saved Messages)
|
|
||||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
|
||||||
let doc = match (user_a, user_b) {
|
|
||||||
self_user if self_user.0 == self_user.1 => {
|
|
||||||
doc! {
|
|
||||||
"channel_type": "SavedMessages",
|
|
||||||
"user": self_user.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
users => {
|
|
||||||
doc! {
|
|
||||||
"channel_type": "DirectMessage",
|
|
||||||
"recipients": {
|
|
||||||
"$all": [ users.0, users.1 ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
query!(self, find_one, COL, doc)?.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a user to a group
|
|
||||||
async fn add_user_to_group(&self, channel: &str, user: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": channel
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$push": {
|
|
||||||
"recipients": user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", "channel"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert channel role permissions
|
|
||||||
async fn set_channel_role_permission(
|
|
||||||
&self,
|
|
||||||
channel: &str,
|
|
||||||
role: &str,
|
|
||||||
permissions: OverrideField,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! { "_id": channel },
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"role_permissions.".to_owned() + role: permissions
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", "channel"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update channel
|
|
||||||
async fn update_channel(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
channel: &PartialChannel,
|
|
||||||
remove: Vec<FieldsChannel>,
|
|
||||||
) -> Result<()> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
update_one_by_id,
|
|
||||||
COL,
|
|
||||||
id,
|
|
||||||
channel,
|
|
||||||
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
|
||||||
None
|
|
||||||
)
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a user from a group
|
|
||||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": channel
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$pull": {
|
|
||||||
"recipients": user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", "channels"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a channel
|
|
||||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
|
||||||
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
use std::collections::hash_map::Entry;
|
|
||||||
|
|
||||||
use super::AbstractChannels;
|
|
||||||
use crate::ReferenceDb;
|
|
||||||
use crate::{Channel, FieldsChannel, PartialChannel};
|
|
||||||
use revolt_permissions::OverrideField;
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractChannels for ReferenceDb {
|
|
||||||
/// Insert a new channel in the database
|
|
||||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
|
||||||
let mut channels = self.channels.lock().await;
|
|
||||||
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
|
|
||||||
entry.insert(channel.clone());
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_database_error!("insert", "channel"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a channel from the database
|
|
||||||
async fn fetch_channel(&self, channel_id: &str) -> Result<Channel> {
|
|
||||||
let channels = self.channels.lock().await;
|
|
||||||
channels
|
|
||||||
.get(channel_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all channels from the database
|
|
||||||
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
|
|
||||||
let channels = self.channels.lock().await;
|
|
||||||
ids.iter()
|
|
||||||
.map(|id| {
|
|
||||||
channels
|
|
||||||
.get(id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all direct messages for a user
|
|
||||||
async fn find_direct_messages(&self, user_id: &str) -> Result<Vec<Channel>> {
|
|
||||||
let channels = self.channels.lock().await;
|
|
||||||
Ok(channels
|
|
||||||
.values()
|
|
||||||
.filter(|channel| channel.contains_user(user_id))
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch saved messages channel
|
|
||||||
async fn find_saved_messages_channel(&self, user_id: &str) -> Result<Channel> {
|
|
||||||
let channels = self.channels.lock().await;
|
|
||||||
channels
|
|
||||||
.get(user_id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_database_error!("fetch", "channel"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch direct message channel (DM or Saved Messages)
|
|
||||||
async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result<Channel> {
|
|
||||||
let channels = self.channels.lock().await;
|
|
||||||
for (_, data) in channels.iter() {
|
|
||||||
if data.contains_user(user_a) && data.contains_user(user_b) {
|
|
||||||
return Ok(data.to_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
/// Insert a user to a group
|
|
||||||
async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()> {
|
|
||||||
let mut channels = self.channels.lock().await;
|
|
||||||
|
|
||||||
if let Some(Channel::Group { recipients, .. }) = channels.get_mut(channel_id) {
|
|
||||||
recipients.push(String::from(user_id));
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(InvalidOperation))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Insert channel role permissions
|
|
||||||
async fn set_channel_role_permission(
|
|
||||||
&self,
|
|
||||||
channel_id: &str,
|
|
||||||
role_id: &str,
|
|
||||||
permissions: OverrideField,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut channels = self.channels.lock().await;
|
|
||||||
|
|
||||||
if let Some(mut channel) = channels.get_mut(channel_id) {
|
|
||||||
match &mut channel {
|
|
||||||
Channel::TextChannel {
|
|
||||||
role_permissions, ..
|
|
||||||
}
|
|
||||||
| Channel::VoiceChannel {
|
|
||||||
role_permissions, ..
|
|
||||||
} => {
|
|
||||||
if role_permissions.get(role_id).is_some() {
|
|
||||||
role_permissions.remove(role_id);
|
|
||||||
role_permissions.insert(String::from(role_id), permissions);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(create_error!(NotFound)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update channel
|
|
||||||
async fn update_channel(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
channel: &PartialChannel,
|
|
||||||
remove: Vec<FieldsChannel>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut channels = self.channels.lock().await;
|
|
||||||
if let Some(channel_data) = channels.get_mut(id) {
|
|
||||||
channel_data.apply_options(channel.to_owned());
|
|
||||||
channel_data.remove_fields(remove);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a user from a group
|
|
||||||
async fn remove_user_from_group(&self, channel: &str, user: &str) -> Result<()> {
|
|
||||||
let mut channels = self.channels.lock().await;
|
|
||||||
if let Some(channel_data) = channels.get_mut(channel) {
|
|
||||||
if channel_data.users()?.contains(&String::from(user)) {
|
|
||||||
channel_data.users()?.retain(|x| x != user);
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
|
||||||
return Err(create_error!(NotFound));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a channel
|
|
||||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
|
||||||
let mut channels = self.channels.lock().await;
|
|
||||||
if channels.remove(&channel.id()).is_some() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use revolt_result::Result;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::events::client::EventV1;
|
|
||||||
use crate::Database;
|
|
||||||
|
|
||||||
static PERMISSIBLE_EMOJIS: Lazy<HashSet<String>> = Lazy::new(|| {
|
|
||||||
include_str!("unicode_emoji.txt")
|
|
||||||
.split('\n')
|
|
||||||
.map(|x| x.into())
|
|
||||||
.collect()
|
|
||||||
});
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
/// Emoji
|
|
||||||
pub struct Emoji {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// What owns this emoji
|
|
||||||
pub parent: EmojiParent,
|
|
||||||
/// Uploader user id
|
|
||||||
pub creator_id: String,
|
|
||||||
/// Emoji name
|
|
||||||
pub name: String,
|
|
||||||
/// Whether the emoji is animated
|
|
||||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
|
||||||
pub animated: bool,
|
|
||||||
/// Whether the emoji is marked as nsfw
|
|
||||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
|
||||||
pub nsfw: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parent Id of the emoji
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum EmojiParent {
|
|
||||||
Server { id: String },
|
|
||||||
Detached,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
impl Emoji {
|
|
||||||
/// Get parent id
|
|
||||||
fn parent(&self) -> &str {
|
|
||||||
match &self.parent {
|
|
||||||
EmojiParent::Server { id } => id,
|
|
||||||
EmojiParent::Detached => "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create an emoji
|
|
||||||
pub async fn create(&self, db: &Database) -> Result<()> {
|
|
||||||
db.insert_emoji(self).await?;
|
|
||||||
|
|
||||||
EventV1::EmojiCreate(self.clone().into())
|
|
||||||
.p(self.parent().to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete an emoji
|
|
||||||
pub async fn delete(self, db: &Database) -> Result<()> {
|
|
||||||
EventV1::EmojiDelete {
|
|
||||||
id: self.id.to_string(),
|
|
||||||
}
|
|
||||||
.p(self.parent().to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
db.detach_emoji(&self).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether we can use a given emoji
|
|
||||||
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
|
|
||||||
if Ulid::from_str(emoji).is_ok() {
|
|
||||||
db.fetch_emoji(emoji).await?;
|
|
||||||
Ok(true)
|
|
||||||
} else {
|
|
||||||
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::Emoji;
|
|
||||||
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractEmojis: Sync + Send {
|
|
||||||
/// Insert emoji into database.
|
|
||||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch an emoji by its id
|
|
||||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji>;
|
|
||||||
|
|
||||||
/// Fetch emoji by their parent id
|
|
||||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>>;
|
|
||||||
|
|
||||||
/// Fetch emoji by their parent ids
|
|
||||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>>;
|
|
||||||
|
|
||||||
/// Detach an emoji by its id
|
|
||||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
use bson::Document;
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::Emoji;
|
|
||||||
use crate::MongoDb;
|
|
||||||
|
|
||||||
use super::AbstractEmojis;
|
|
||||||
|
|
||||||
static COL: &str = "emojis";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractEmojis for MongoDb {
|
|
||||||
/// Insert emoji into database.
|
|
||||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &emoji).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch an emoji by its id
|
|
||||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
|
||||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch emoji by their parent id
|
|
||||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find_one,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"parent.id": parent_id
|
|
||||||
}
|
|
||||||
)?
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch emoji by their parent ids
|
|
||||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"parent.id": {
|
|
||||||
"$in": parent_ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detach an emoji by its id
|
|
||||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": &emoji.id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"parent": {
|
|
||||||
"type": "Detached"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::Emoji;
|
|
||||||
use crate::EmojiParent;
|
|
||||||
use crate::ReferenceDb;
|
|
||||||
|
|
||||||
use super::AbstractEmojis;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractEmojis for ReferenceDb {
|
|
||||||
/// Insert emoji into database.
|
|
||||||
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()> {
|
|
||||||
let mut emojis = self.emojis.lock().await;
|
|
||||||
if emojis.contains_key(&emoji.id) {
|
|
||||||
Err(create_database_error!("insert", "emoji"))
|
|
||||||
} else {
|
|
||||||
emojis.insert(emoji.id.to_string(), emoji.clone());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch an emoji by its id
|
|
||||||
async fn fetch_emoji(&self, id: &str) -> Result<Emoji> {
|
|
||||||
let emojis = self.emojis.lock().await;
|
|
||||||
emojis
|
|
||||||
.get(id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch emoji by their parent id
|
|
||||||
async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result<Vec<Emoji>> {
|
|
||||||
let emojis = self.emojis.lock().await;
|
|
||||||
Ok(emojis
|
|
||||||
.values()
|
|
||||||
.filter(|emoji| match &emoji.parent {
|
|
||||||
EmojiParent::Server { id } => id == parent_id,
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch emoji by their parent ids
|
|
||||||
async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result<Vec<Emoji>> {
|
|
||||||
let emojis = self.emojis.lock().await;
|
|
||||||
Ok(emojis
|
|
||||||
.values()
|
|
||||||
.filter(|emoji| match &emoji.parent {
|
|
||||||
EmojiParent::Server { id } => parent_ids.contains(id),
|
|
||||||
_ => false,
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detach an emoji by its id
|
|
||||||
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
|
|
||||||
let mut emojis = self.emojis.lock().await;
|
|
||||||
if let Some(bot) = emojis.get_mut(&emoji.id) {
|
|
||||||
bot.parent = EmojiParent::Detached;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
use crate::Database;
|
|
||||||
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
auto_derived_partial!(
|
|
||||||
/// File
|
|
||||||
pub struct File {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// Tag / bucket this file was uploaded to
|
|
||||||
pub tag: String,
|
|
||||||
/// Original filename
|
|
||||||
pub filename: String,
|
|
||||||
/// Parsed metadata of this file
|
|
||||||
pub metadata: Metadata,
|
|
||||||
/// Raw content type of this file
|
|
||||||
pub content_type: String,
|
|
||||||
/// Size of this file (in bytes)
|
|
||||||
pub size: isize,
|
|
||||||
|
|
||||||
/// Whether this file was deleted
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub deleted: Option<bool>,
|
|
||||||
/// Whether this file was reported
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub reported: Option<bool>,
|
|
||||||
|
|
||||||
// TODO: migrate this mess to having:
|
|
||||||
// - author_id
|
|
||||||
// - parent: Parent { Message(id), User(id), etc }
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub message_id: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub user_id: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub server_id: Option<String>,
|
|
||||||
|
|
||||||
/// Id of the object this file is associated with
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub object_id: Option<String>,
|
|
||||||
},
|
|
||||||
"PartialFile"
|
|
||||||
);
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
/// Metadata associated with a file
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
#[derive(Default)]
|
|
||||||
pub enum Metadata {
|
|
||||||
/// File is just a generic uncategorised file
|
|
||||||
#[default]
|
|
||||||
File,
|
|
||||||
/// File contains textual data and should be displayed as such
|
|
||||||
Text,
|
|
||||||
/// File is an image with specific dimensions
|
|
||||||
Image { width: isize, height: isize },
|
|
||||||
/// File is a video with specific dimensions
|
|
||||||
Video { width: isize, height: isize },
|
|
||||||
/// File is audio
|
|
||||||
Audio,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
impl File {
|
|
||||||
/// Use a file for a message attachment
|
|
||||||
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "attachments", "message", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use a file for a user profile background
|
|
||||||
pub async fn use_background(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "backgrounds", "user", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use a file for a user avatar
|
|
||||||
pub async fn use_avatar(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "avatars", "user", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use a file for an icon
|
|
||||||
pub async fn use_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use a file for a server icon
|
|
||||||
pub async fn use_server_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use a file for a server banner
|
|
||||||
pub async fn use_banner(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "banners", "server", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use a file for an emoji
|
|
||||||
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
|
|
||||||
db.find_and_use_attachment(id, "emojis", "object", parent)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::File;
|
|
||||||
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractAttachments: Sync + Send {
|
|
||||||
/// Insert attachment into database.
|
|
||||||
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
|
|
||||||
|
|
||||||
/// Find an attachment by its details and mark it as used by a given parent.
|
|
||||||
async fn find_and_use_attachment(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
tag: &str,
|
|
||||||
parent_type: &str,
|
|
||||||
parent_id: &str,
|
|
||||||
) -> Result<File>;
|
|
||||||
|
|
||||||
/// Mark an attachment as having been reported.
|
|
||||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>;
|
|
||||||
|
|
||||||
/// Mark an attachment as having been deleted.
|
|
||||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>;
|
|
||||||
|
|
||||||
/// Mark multiple attachments as having been deleted.
|
|
||||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>;
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
use bson::Document;
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::File;
|
|
||||||
use crate::MongoDb;
|
|
||||||
|
|
||||||
use super::AbstractAttachments;
|
|
||||||
|
|
||||||
static COL: &str = "attachments";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractAttachments for MongoDb {
|
|
||||||
/// Insert attachment into database.
|
|
||||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &attachment).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find an attachment by its details and mark it as used by a given parent.
|
|
||||||
async fn find_and_use_attachment(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
tag: &str,
|
|
||||||
parent_type: &str,
|
|
||||||
parent_id: &str,
|
|
||||||
) -> Result<File> {
|
|
||||||
let key = format!("{parent_type}_id");
|
|
||||||
let file = query!(
|
|
||||||
self,
|
|
||||||
find_one,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id": id,
|
|
||||||
"tag": tag,
|
|
||||||
&key: {
|
|
||||||
"$exists": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)?
|
|
||||||
.ok_or_else(|| create_error!(NotFound))?;
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
key: parent_id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))?;
|
|
||||||
|
|
||||||
Ok(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark an attachment as having been reported.
|
|
||||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"reported": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark an attachment as having been deleted.
|
|
||||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"deleted": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark multiple attachments as having been deleted.
|
|
||||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$set": {
|
|
||||||
"deleted": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::File;
|
|
||||||
use crate::ReferenceDb;
|
|
||||||
|
|
||||||
use super::AbstractAttachments;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractAttachments for ReferenceDb {
|
|
||||||
/// Insert attachment into database.
|
|
||||||
async fn insert_attachment(&self, attachment: &File) -> Result<()> {
|
|
||||||
let mut attachments = self.files.lock().await;
|
|
||||||
if attachments.contains_key(&attachment.id) {
|
|
||||||
Err(create_database_error!("insert", "attachment"))
|
|
||||||
} else {
|
|
||||||
attachments.insert(attachment.id.to_string(), attachment.clone());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find an attachment by its details and mark it as used by a given parent.
|
|
||||||
async fn find_and_use_attachment(
|
|
||||||
&self,
|
|
||||||
id: &str,
|
|
||||||
tag: &str,
|
|
||||||
parent_type: &str,
|
|
||||||
parent_id: &str,
|
|
||||||
) -> Result<File> {
|
|
||||||
let mut files = self.files.lock().await;
|
|
||||||
if let Some(file) = files.get_mut(id) {
|
|
||||||
if file.tag == tag {
|
|
||||||
match parent_type {
|
|
||||||
"message" => file.message_id = Some(parent_id.to_owned()),
|
|
||||||
"user" => file.user_id = Some(parent_id.to_owned()),
|
|
||||||
"object" => file.object_id = Some(parent_id.to_owned()),
|
|
||||||
"server" => file.server_id = Some(parent_id.to_owned()),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(file.clone())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark an attachment as having been reported.
|
|
||||||
async fn mark_attachment_as_reported(&self, id: &str) -> Result<()> {
|
|
||||||
let mut files = self.files.lock().await;
|
|
||||||
if let Some(file) = files.get_mut(id) {
|
|
||||||
file.reported = Some(true);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark an attachment as having been deleted.
|
|
||||||
async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()> {
|
|
||||||
let mut files = self.files.lock().await;
|
|
||||||
if let Some(file) = files.get_mut(id) {
|
|
||||||
file.deleted = Some(true);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mark multiple attachments as having been deleted.
|
|
||||||
async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()> {
|
|
||||||
let mut files = self.files.lock().await;
|
|
||||||
|
|
||||||
for id in ids {
|
|
||||||
if !files.contains_key(id) {
|
|
||||||
return Err(create_error!(NotFound));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for id in ids {
|
|
||||||
if let Some(file) = files.get_mut(id) {
|
|
||||||
file.reported = Some(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,572 +0,0 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use indexmap::{IndexMap, IndexSet};
|
|
||||||
use iso8601_timestamp::Timestamp;
|
|
||||||
use revolt_config::config;
|
|
||||||
use revolt_models::v0::{
|
|
||||||
self, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook, PushNotification,
|
|
||||||
ReplyIntent, SendableEmbed, RE_MENTION,
|
|
||||||
};
|
|
||||||
use revolt_permissions::{ChannelPermission, PermissionValue};
|
|
||||||
use revolt_result::Result;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
events::client::EventV1,
|
|
||||||
tasks::{self, ack::AckEvent},
|
|
||||||
util::idempotency::IdempotencyKey,
|
|
||||||
Channel, Database, Emoji, File,
|
|
||||||
};
|
|
||||||
|
|
||||||
auto_derived_partial!(
|
|
||||||
/// Message
|
|
||||||
pub struct Message {
|
|
||||||
/// Unique Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// Unique value generated by client sending this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nonce: Option<String>,
|
|
||||||
/// Id of the channel this message was sent in
|
|
||||||
pub channel: String,
|
|
||||||
/// Id of the user or webhook that sent this message
|
|
||||||
pub author: String,
|
|
||||||
/// The webhook that sent this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub webhook: Option<MessageWebhook>,
|
|
||||||
/// Message content
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub content: Option<String>,
|
|
||||||
/// System message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub system: Option<SystemMessage>,
|
|
||||||
/// Array of attachments
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub attachments: Option<Vec<File>>,
|
|
||||||
/// Time at which this message was last edited
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub edited: Option<Timestamp>,
|
|
||||||
/// Attached embeds to this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub embeds: Option<Vec<Embed>>,
|
|
||||||
/// Array of user ids mentioned in this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub mentions: Option<Vec<String>>,
|
|
||||||
/// Array of message ids this message is replying to
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub replies: Option<Vec<String>>,
|
|
||||||
/// Hashmap of emoji IDs to array of user IDs
|
|
||||||
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
|
|
||||||
pub reactions: IndexMap<String, IndexSet<String>>,
|
|
||||||
/// Information about how this message should be interacted with
|
|
||||||
#[serde(skip_serializing_if = "Interactions::is_default", default)]
|
|
||||||
pub interactions: Interactions,
|
|
||||||
/// Name and / or avatar overrides for this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub masquerade: Option<Masquerade>,
|
|
||||||
},
|
|
||||||
"PartialMessage"
|
|
||||||
);
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
/// System Event
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum SystemMessage {
|
|
||||||
#[serde(rename = "text")]
|
|
||||||
Text { content: String },
|
|
||||||
#[serde(rename = "user_added")]
|
|
||||||
UserAdded { id: String, by: String },
|
|
||||||
#[serde(rename = "user_remove")]
|
|
||||||
UserRemove { id: String, by: String },
|
|
||||||
#[serde(rename = "user_joined")]
|
|
||||||
UserJoined { id: String },
|
|
||||||
#[serde(rename = "user_left")]
|
|
||||||
UserLeft { id: String },
|
|
||||||
#[serde(rename = "user_kicked")]
|
|
||||||
UserKicked { id: String },
|
|
||||||
#[serde(rename = "user_banned")]
|
|
||||||
UserBanned { id: String },
|
|
||||||
#[serde(rename = "channel_renamed")]
|
|
||||||
ChannelRenamed { name: String, by: String },
|
|
||||||
#[serde(rename = "channel_description_changed")]
|
|
||||||
ChannelDescriptionChanged { by: String },
|
|
||||||
#[serde(rename = "channel_icon_changed")]
|
|
||||||
ChannelIconChanged { by: String },
|
|
||||||
#[serde(rename = "channel_ownership_changed")]
|
|
||||||
ChannelOwnershipChanged { from: String, to: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Name and / or avatar override information
|
|
||||||
pub struct Masquerade {
|
|
||||||
/// Replace the display name shown on this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// Replace the avatar shown on this message (URL to image file)
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub avatar: Option<String>,
|
|
||||||
/// Replace the display role colour shown on this message
|
|
||||||
///
|
|
||||||
/// Must have `ManageRole` permission to use
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub colour: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Information to guide interactions on this message
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Interactions {
|
|
||||||
/// Reactions which should always appear and be distinct
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
|
||||||
pub reactions: Option<IndexSet<String>>,
|
|
||||||
/// Whether reactions should be restricted to the given list
|
|
||||||
///
|
|
||||||
/// Can only be set to true if reactions list is of at least length 1
|
|
||||||
#[serde(skip_serializing_if = "crate::if_false", default)]
|
|
||||||
pub restrict_reactions: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Appended Information
|
|
||||||
pub struct AppendMessage {
|
|
||||||
/// Additional embeds to include in this message
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub embeds: Option<Vec<Embed>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Message Time Period
|
|
||||||
///
|
|
||||||
/// Filter and sort messages by time
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum MessageTimePeriod {
|
|
||||||
Relative {
|
|
||||||
/// Message id to search around
|
|
||||||
///
|
|
||||||
/// Specifying 'nearby' ignores 'before', 'after' and 'sort'.
|
|
||||||
/// It will also take half of limit rounded as the limits to each side.
|
|
||||||
/// It also fetches the message ID specified.
|
|
||||||
nearby: String,
|
|
||||||
},
|
|
||||||
Absolute {
|
|
||||||
/// Message id before which messages should be fetched
|
|
||||||
before: Option<String>,
|
|
||||||
/// Message id after which messages should be fetched
|
|
||||||
after: Option<String>,
|
|
||||||
/// Message sort direction
|
|
||||||
sort: Option<MessageSort>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Message Filter
|
|
||||||
pub struct MessageFilter {
|
|
||||||
/// Parent channel ID
|
|
||||||
pub channel: Option<String>,
|
|
||||||
/// Message author ID
|
|
||||||
pub author: Option<String>,
|
|
||||||
/// Search query
|
|
||||||
pub query: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Message Query
|
|
||||||
pub struct MessageQuery {
|
|
||||||
/// Maximum number of messages to fetch
|
|
||||||
///
|
|
||||||
/// For fetching nearby messages, this is \`(limit + 1)\`.
|
|
||||||
pub limit: Option<i64>,
|
|
||||||
/// Filter to apply
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub filter: MessageFilter,
|
|
||||||
/// Time period to fetch
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub time_period: MessageTimePeriod,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
#[allow(clippy::derivable_impls)]
|
|
||||||
impl Default for Message {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: Default::default(),
|
|
||||||
nonce: None,
|
|
||||||
channel: Default::default(),
|
|
||||||
author: Default::default(),
|
|
||||||
webhook: None,
|
|
||||||
content: None,
|
|
||||||
system: None,
|
|
||||||
attachments: None,
|
|
||||||
edited: None,
|
|
||||||
embeds: None,
|
|
||||||
mentions: None,
|
|
||||||
replies: None,
|
|
||||||
reactions: Default::default(),
|
|
||||||
interactions: Default::default(),
|
|
||||||
masquerade: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
impl Message {
|
|
||||||
/// Create message from API data
|
|
||||||
pub async fn create_from_api(
|
|
||||||
db: &Database,
|
|
||||||
channel: Channel,
|
|
||||||
data: DataMessageSend,
|
|
||||||
author: MessageAuthor<'_>,
|
|
||||||
mut idempotency: IdempotencyKey,
|
|
||||||
generate_embeds: bool,
|
|
||||||
) -> Result<Message> {
|
|
||||||
let config = config().await;
|
|
||||||
|
|
||||||
Message::validate_sum(
|
|
||||||
&data.content,
|
|
||||||
data.embeds.as_deref().unwrap_or_default(),
|
|
||||||
config.features.limits.default.message_length,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
idempotency
|
|
||||||
.consume_nonce(data.nonce)
|
|
||||||
.await
|
|
||||||
.map_err(|_| create_error!(InvalidOperation))?;
|
|
||||||
|
|
||||||
// Check the message is not empty
|
|
||||||
if (data.content.as_ref().map_or(true, |v| v.is_empty()))
|
|
||||||
&& (data.attachments.as_ref().map_or(true, |v| v.is_empty()))
|
|
||||||
&& (data.embeds.as_ref().map_or(true, |v| v.is_empty()))
|
|
||||||
{
|
|
||||||
return Err(create_error!(EmptyMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure restrict_reactions is not specified without reactions list
|
|
||||||
if let Some(interactions) = &data.interactions {
|
|
||||||
if interactions.restrict_reactions {
|
|
||||||
let disallowed = if let Some(list) = &interactions.reactions {
|
|
||||||
list.is_empty()
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if disallowed {
|
|
||||||
return Err(create_error!(InvalidProperty));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let (author_id, webhook) = match &author {
|
|
||||||
MessageAuthor::User(user) => (user.id.clone(), None),
|
|
||||||
MessageAuthor::Webhook(webhook) => (webhook.id.clone(), Some((*webhook).clone())),
|
|
||||||
MessageAuthor::System { .. } => ("00000000000000000000000000".to_string(), None),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start constructing the message
|
|
||||||
let message_id = Ulid::new().to_string();
|
|
||||||
let mut message = Message {
|
|
||||||
id: message_id.clone(),
|
|
||||||
channel: channel.id(),
|
|
||||||
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
|
|
||||||
interactions: data
|
|
||||||
.interactions
|
|
||||||
.map(|interactions| interactions.into())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
author: author_id,
|
|
||||||
webhook: webhook.map(|w| w.into()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parse mentions in message.
|
|
||||||
let mut mentions = HashSet::new();
|
|
||||||
if let Some(content) = &data.content {
|
|
||||||
for capture in RE_MENTION.captures_iter(content) {
|
|
||||||
if let Some(mention) = capture.get(1) {
|
|
||||||
mentions.insert(mention.as_str().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify replies are valid.
|
|
||||||
let mut replies = HashSet::new();
|
|
||||||
if let Some(entries) = data.replies {
|
|
||||||
if entries.len() > config.features.limits.default.message_replies {
|
|
||||||
return Err(create_error!(TooManyReplies {
|
|
||||||
max: config.features.limits.default.message_replies,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
for ReplyIntent { id, mention } in entries {
|
|
||||||
let message = db.fetch_message(&id).await?;
|
|
||||||
|
|
||||||
if mention {
|
|
||||||
mentions.insert(message.author.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
replies.insert(message.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mentions.is_empty() {
|
|
||||||
message.mentions.replace(mentions.into_iter().collect());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !replies.is_empty() {
|
|
||||||
message
|
|
||||||
.replies
|
|
||||||
.replace(replies.into_iter().collect::<Vec<String>>());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add attachments to message.
|
|
||||||
let mut attachments = vec![];
|
|
||||||
if data
|
|
||||||
.attachments
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
|
|
||||||
{
|
|
||||||
return Err(create_error!(TooManyAttachments {
|
|
||||||
max: config.features.limits.default.message_attachments,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if data
|
|
||||||
.embeds
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
|
|
||||||
{
|
|
||||||
return Err(create_error!(TooManyEmbeds {
|
|
||||||
max: config.features.limits.default.message_embeds,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
|
|
||||||
attachments.push(
|
|
||||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !attachments.is_empty() {
|
|
||||||
message.attachments.replace(attachments);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process included embeds.
|
|
||||||
for sendable_embed in data.embeds.unwrap_or_default() {
|
|
||||||
message.attach_sendable_embed(db, sendable_embed).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set content
|
|
||||||
message.content = data.content;
|
|
||||||
|
|
||||||
// Pass-through nonce value for clients
|
|
||||||
message.nonce = Some(idempotency.into_key());
|
|
||||||
|
|
||||||
// Send the message
|
|
||||||
message.send(db, author, &channel, generate_embeds).await?;
|
|
||||||
|
|
||||||
Ok(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a message without any notifications
|
|
||||||
pub async fn send_without_notifications(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
is_dm: bool,
|
|
||||||
generate_embeds: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
db.insert_message(self).await?;
|
|
||||||
|
|
||||||
// Fan out events
|
|
||||||
EventV1::Message(self.clone().into())
|
|
||||||
.p(self.channel.to_string())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Update last_message_id
|
|
||||||
tasks::last_message_id::queue(self.channel.to_string(), self.id.to_string(), is_dm).await;
|
|
||||||
|
|
||||||
// Add mentions for affected users
|
|
||||||
if let Some(mentions) = &self.mentions {
|
|
||||||
for user in mentions {
|
|
||||||
tasks::ack::queue(
|
|
||||||
self.channel.to_string(),
|
|
||||||
user.to_string(),
|
|
||||||
AckEvent::AddMention {
|
|
||||||
ids: vec![self.id.to_string()],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate embeds
|
|
||||||
if generate_embeds {
|
|
||||||
if let Some(content) = &self.content {
|
|
||||||
tasks::process_embeds::queue(
|
|
||||||
self.channel.to_string(),
|
|
||||||
self.id.to_string(),
|
|
||||||
content.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a message
|
|
||||||
pub async fn send(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
author: MessageAuthor<'_>,
|
|
||||||
channel: &Channel,
|
|
||||||
generate_embeds: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.send_without_notifications(
|
|
||||||
db,
|
|
||||||
matches!(channel, Channel::DirectMessage { .. }),
|
|
||||||
generate_embeds,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Push out Web Push notifications
|
|
||||||
crate::tasks::web_push::queue(
|
|
||||||
{
|
|
||||||
match channel {
|
|
||||||
Channel::DirectMessage { recipients, .. }
|
|
||||||
| Channel::Group { recipients, .. } => recipients.clone(),
|
|
||||||
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
|
|
||||||
_ => vec![],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append content to message
|
|
||||||
pub async fn append(
|
|
||||||
db: &Database,
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
append: AppendMessage,
|
|
||||||
) -> Result<()> {
|
|
||||||
db.append_message(&id, &append).await?;
|
|
||||||
|
|
||||||
EventV1::MessageAppend {
|
|
||||||
id,
|
|
||||||
channel: channel.to_string(),
|
|
||||||
append: append.into(),
|
|
||||||
}
|
|
||||||
.p(channel)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert sendable embed to text embed and attach to message
|
|
||||||
pub async fn attach_sendable_embed(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
embed: v0::SendableEmbed,
|
|
||||||
) -> Result<()> {
|
|
||||||
let media: Option<v0::File> = if let Some(id) = embed.media {
|
|
||||||
Some(
|
|
||||||
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
|
|
||||||
.await?
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let embed = v0::Embed::Text(v0::Text {
|
|
||||||
icon_url: embed.icon_url,
|
|
||||||
url: embed.url,
|
|
||||||
title: embed.title,
|
|
||||||
description: embed.description,
|
|
||||||
media,
|
|
||||||
colour: embed.colour,
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(embeds) = &mut self.embeds {
|
|
||||||
embeds.push(embed);
|
|
||||||
} else {
|
|
||||||
self.embeds = Some(vec![embed]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate the sum of content of a message is under threshold
|
|
||||||
pub fn validate_sum(
|
|
||||||
content: &Option<String>,
|
|
||||||
embeds: &[SendableEmbed],
|
|
||||||
max_length: usize,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut running_total = 0;
|
|
||||||
if let Some(content) = content {
|
|
||||||
running_total += content.len();
|
|
||||||
}
|
|
||||||
|
|
||||||
for embed in embeds {
|
|
||||||
if let Some(desc) = &embed.description {
|
|
||||||
running_total += desc.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if running_total <= max_length {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(PayloadTooLarge))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SystemMessage {
|
|
||||||
pub fn into_message(self, channel: String) -> Message {
|
|
||||||
Message {
|
|
||||||
id: Ulid::new().to_string(),
|
|
||||||
channel,
|
|
||||||
author: "00000000000000000000000000".to_string(),
|
|
||||||
system: Some(self),
|
|
||||||
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Interactions {
|
|
||||||
/// Validate interactions info is correct
|
|
||||||
pub async fn validate(&self, db: &Database, permissions: &PermissionValue) -> Result<()> {
|
|
||||||
let config = config().await;
|
|
||||||
|
|
||||||
if let Some(reactions) = &self.reactions {
|
|
||||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
|
||||||
|
|
||||||
if reactions.len() > config.features.limits.default.message_reactions {
|
|
||||||
return Err(create_error!(InvalidOperation));
|
|
||||||
}
|
|
||||||
|
|
||||||
for reaction in reactions {
|
|
||||||
if !Emoji::can_use(db, reaction).await? {
|
|
||||||
return Err(create_error!(InvalidOperation));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if we can use a given emoji to react
|
|
||||||
pub fn can_use(&self, emoji: &str) -> bool {
|
|
||||||
if self.restrict_reactions {
|
|
||||||
if let Some(reactions) = &self.reactions {
|
|
||||||
reactions.contains(emoji)
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if default initialisation of fields
|
|
||||||
pub fn is_default(&self) -> bool {
|
|
||||||
!self.restrict_reactions && self.reactions.is_none()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
|
|
||||||
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractMessages: Sync + Send {
|
|
||||||
/// Insert a new message into the database
|
|
||||||
async fn insert_message(&self, message: &Message) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch a message by its id
|
|
||||||
async fn fetch_message(&self, id: &str) -> Result<Message>;
|
|
||||||
|
|
||||||
/// Fetch multiple messages by given query
|
|
||||||
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>>;
|
|
||||||
|
|
||||||
/// Update a given message with new information
|
|
||||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
|
|
||||||
|
|
||||||
/// Append information to a given message
|
|
||||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
|
|
||||||
|
|
||||||
/// Add a new reaction to a message
|
|
||||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
|
|
||||||
|
|
||||||
/// Remove a reaction from a message
|
|
||||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
|
|
||||||
|
|
||||||
/// Remove reaction from a message
|
|
||||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
|
|
||||||
|
|
||||||
/// Delete a message from the database by its id
|
|
||||||
async fn delete_message(&self, id: &str) -> Result<()>;
|
|
||||||
|
|
||||||
/// Delete messages from a channel by their ids and corresponding channel id
|
|
||||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>;
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
use bson::{to_bson, Document};
|
|
||||||
use futures::try_join;
|
|
||||||
use mongodb::options::FindOptions;
|
|
||||||
use revolt_models::v0::MessageSort;
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::{AppendMessage, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage};
|
|
||||||
|
|
||||||
use super::AbstractMessages;
|
|
||||||
|
|
||||||
static COL: &str = "messages";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractMessages for MongoDb {
|
|
||||||
/// Insert a new message into the database
|
|
||||||
async fn insert_message(&self, message: &Message) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &message).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a message by its id
|
|
||||||
async fn fetch_message(&self, id: &str) -> Result<Message> {
|
|
||||||
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch multiple messages by given query
|
|
||||||
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
|
|
||||||
let mut filter = doc! {};
|
|
||||||
|
|
||||||
// 1. Apply message filters
|
|
||||||
if let Some(channel) = query.filter.channel {
|
|
||||||
filter.insert("channel", channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(author) = query.filter.author {
|
|
||||||
filter.insert("author", author);
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_search_query = if let Some(query) = query.filter.query {
|
|
||||||
filter.insert(
|
|
||||||
"$text",
|
|
||||||
doc! {
|
|
||||||
"$search": query
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// 2. Find query limit
|
|
||||||
let limit = query.limit.unwrap_or(50);
|
|
||||||
|
|
||||||
// 3. Apply message time period
|
|
||||||
match query.time_period {
|
|
||||||
MessageTimePeriod::Relative { nearby } => {
|
|
||||||
// 3.1. Prepare filters
|
|
||||||
let mut older_message_filter = filter.clone();
|
|
||||||
let mut newer_message_filter = filter;
|
|
||||||
|
|
||||||
older_message_filter.insert(
|
|
||||||
"_id",
|
|
||||||
doc! {
|
|
||||||
"$lt": &nearby
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
newer_message_filter.insert(
|
|
||||||
"_id",
|
|
||||||
doc! {
|
|
||||||
"$gte": &nearby
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3.2. Execute in both directions
|
|
||||||
let (a, b) = try_join!(
|
|
||||||
self.find_with_options::<_, Message>(
|
|
||||||
COL,
|
|
||||||
newer_message_filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit / 2 + 1)
|
|
||||||
.sort(doc! {
|
|
||||||
"_id": 1_i32
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
),
|
|
||||||
self.find_with_options::<_, Message>(
|
|
||||||
COL,
|
|
||||||
older_message_filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit / 2)
|
|
||||||
.sort(doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map_err(|_| create_database_error!("find", COL))?;
|
|
||||||
|
|
||||||
Ok([a, b].concat())
|
|
||||||
}
|
|
||||||
MessageTimePeriod::Absolute {
|
|
||||||
before,
|
|
||||||
after,
|
|
||||||
sort,
|
|
||||||
} => {
|
|
||||||
// 3.1. Apply message ID filter
|
|
||||||
if let Some(doc) = match (before, after) {
|
|
||||||
(Some(before), Some(after)) => Some(doc! {
|
|
||||||
"$lt": before,
|
|
||||||
"$gt": after
|
|
||||||
}),
|
|
||||||
(Some(before), _) => Some(doc! {
|
|
||||||
"$lt": before
|
|
||||||
}),
|
|
||||||
(_, Some(after)) => Some(doc! {
|
|
||||||
"$gt": after
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
} {
|
|
||||||
filter.insert("_id", doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. Execute with given message sort
|
|
||||||
self.find_with_options(
|
|
||||||
COL,
|
|
||||||
filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit)
|
|
||||||
.sort(match sort.unwrap_or(MessageSort::Latest) {
|
|
||||||
// Sort by relevance, fallback to latest
|
|
||||||
MessageSort::Relevance => {
|
|
||||||
if is_search_query {
|
|
||||||
doc! {
|
|
||||||
"score": {
|
|
||||||
"$meta": "textScore"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort by latest first
|
|
||||||
MessageSort::Latest => doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
},
|
|
||||||
// Sort by oldest first
|
|
||||||
MessageSort::Oldest => doc! {
|
|
||||||
"_id": 1_i32
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| create_database_error!("find", COL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update a given message with new information
|
|
||||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
|
||||||
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append information to a given message
|
|
||||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
|
||||||
let mut query = doc! {};
|
|
||||||
|
|
||||||
if let Some(embeds) = &append.embeds {
|
|
||||||
if !embeds.is_empty() {
|
|
||||||
query.insert(
|
|
||||||
"$push",
|
|
||||||
doc! {
|
|
||||||
"embeds": {
|
|
||||||
"$each": to_bson(embeds)
|
|
||||||
.map_err(|_| create_database_error!("to_bson", "embeds"))?
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new reaction to a message
|
|
||||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$addToSet": {
|
|
||||||
format!("reactions.{emoji}"): user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a reaction from a message
|
|
||||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$pull": {
|
|
||||||
format!("reactions.{emoji}"): user
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove reaction from a message
|
|
||||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.update_one(
|
|
||||||
doc! {
|
|
||||||
"_id": id
|
|
||||||
},
|
|
||||||
doc! {
|
|
||||||
"$unset": {
|
|
||||||
format!("reactions.{emoji}"): 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a message from the database by its id
|
|
||||||
async fn delete_message(&self, id: &str) -> Result<()> {
|
|
||||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete messages from a channel by their ids and corresponding channel id
|
|
||||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
|
|
||||||
self.col::<Document>(COL)
|
|
||||||
.delete_many(
|
|
||||||
doc! {
|
|
||||||
"channel": channel,
|
|
||||||
"_id": {
|
|
||||||
"$in": ids
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("delete_many", COL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
use indexmap::IndexSet;
|
|
||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
|
||||||
|
|
||||||
use super::AbstractMessages;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractMessages for ReferenceDb {
|
|
||||||
/// Insert a new message into the database
|
|
||||||
async fn insert_message(&self, message: &Message) -> Result<()> {
|
|
||||||
let mut messages = self.messages.lock().await;
|
|
||||||
if messages.contains_key(&message.id) {
|
|
||||||
Err(create_database_error!("insert", "message"))
|
|
||||||
} else {
|
|
||||||
messages.insert(message.id.to_string(), message.clone());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a message by its id
|
|
||||||
async fn fetch_message(&self, id: &str) -> Result<Message> {
|
|
||||||
let messages = self.messages.lock().await;
|
|
||||||
messages
|
|
||||||
.get(id)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch multiple messages by given query
|
|
||||||
async fn fetch_messages(&self, query: MessageQuery) -> Result<Vec<Message>> {
|
|
||||||
let messages = self.messages.lock().await;
|
|
||||||
let matched_messages = messages
|
|
||||||
.values()
|
|
||||||
.filter(|message| {
|
|
||||||
if let Some(channel) = &query.filter.channel {
|
|
||||||
if &message.channel != channel {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(author) = &query.filter.author {
|
|
||||||
if &message.author != author {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(query) = &query.filter.query {
|
|
||||||
if let Some(content) = &message.content {
|
|
||||||
if !content.to_lowercase().contains(query) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// TODO: sorting, etc (will be required for tests)
|
|
||||||
|
|
||||||
Ok(matched_messages)
|
|
||||||
|
|
||||||
/*
|
|
||||||
// 2. Find query limit
|
|
||||||
let limit = query.limit.unwrap_or(50);
|
|
||||||
|
|
||||||
// 3. Apply message time period
|
|
||||||
match query.time_period {
|
|
||||||
MessageTimePeriod::Relative { nearby } => {
|
|
||||||
// 3.1. Prepare filters
|
|
||||||
let mut older_message_filter = filter.clone();
|
|
||||||
let mut newer_message_filter = filter;
|
|
||||||
|
|
||||||
older_message_filter.insert(
|
|
||||||
"_id",
|
|
||||||
doc! {
|
|
||||||
"$lt": &nearby
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
newer_message_filter.insert(
|
|
||||||
"_id",
|
|
||||||
doc! {
|
|
||||||
"$gte": &nearby
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3.2. Execute in both directions
|
|
||||||
let (a, b) = try_join!(
|
|
||||||
self.find_with_options::<_, Message>(
|
|
||||||
COL,
|
|
||||||
newer_message_filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit / 2 + 1)
|
|
||||||
.sort(doc! {
|
|
||||||
"_id": 1_i32
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
),
|
|
||||||
self.find_with_options::<_, Message>(
|
|
||||||
COL,
|
|
||||||
older_message_filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit / 2)
|
|
||||||
.sort(doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map_err(|_| create_database_error!("find", COL))?;
|
|
||||||
|
|
||||||
Ok([a, b].concat())
|
|
||||||
}
|
|
||||||
MessageTimePeriod::Absolute {
|
|
||||||
before,
|
|
||||||
after,
|
|
||||||
sort,
|
|
||||||
} => {
|
|
||||||
// 3.1. Apply message ID filter
|
|
||||||
if let Some(doc) = match (before, after) {
|
|
||||||
(Some(before), Some(after)) => Some(doc! {
|
|
||||||
"$lt": before,
|
|
||||||
"$gt": after
|
|
||||||
}),
|
|
||||||
(Some(before), _) => Some(doc! {
|
|
||||||
"$lt": before
|
|
||||||
}),
|
|
||||||
(_, Some(after)) => Some(doc! {
|
|
||||||
"$gt": after
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
} {
|
|
||||||
filter.insert("_id", doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. Execute with given message sort
|
|
||||||
self.find_with_options(
|
|
||||||
COL,
|
|
||||||
filter,
|
|
||||||
FindOptions::builder()
|
|
||||||
.limit(limit)
|
|
||||||
.sort(match sort.unwrap_or(MessageSort::Latest) {
|
|
||||||
// Sort by relevance, fallback to latest
|
|
||||||
MessageSort::Relevance => {
|
|
||||||
if is_search_query {
|
|
||||||
doc! {
|
|
||||||
"score": {
|
|
||||||
"$meta": "textScore"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Sort by latest first
|
|
||||||
MessageSort::Latest => doc! {
|
|
||||||
"_id": -1_i32
|
|
||||||
},
|
|
||||||
// Sort by oldest first
|
|
||||||
MessageSort::Oldest => doc! {
|
|
||||||
"_id": 1_i32
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| create_database_error!("find", COL))
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update a given message with new information
|
|
||||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
|
||||||
let mut messages = self.messages.lock().await;
|
|
||||||
if let Some(message_data) = messages.get_mut(id) {
|
|
||||||
message_data.apply_options(message.to_owned());
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append information to a given message
|
|
||||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()> {
|
|
||||||
let mut messages = self.messages.lock().await;
|
|
||||||
if let Some(message_data) = messages.get_mut(id) {
|
|
||||||
if let Some(embeds) = &append.embeds {
|
|
||||||
if !embeds.is_empty() {
|
|
||||||
if let Some(embeds_data) = &mut message_data.embeds {
|
|
||||||
embeds_data.extend(embeds.clone());
|
|
||||||
} else {
|
|
||||||
message_data.embeds = Some(embeds.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new reaction to a message
|
|
||||||
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
|
||||||
let mut messages = self.messages.lock().await;
|
|
||||||
if let Some(message) = messages.get_mut(id) {
|
|
||||||
if let Some(users) = message.reactions.get_mut(emoji) {
|
|
||||||
users.insert(user.to_string());
|
|
||||||
} else {
|
|
||||||
message
|
|
||||||
.reactions
|
|
||||||
.insert(emoji.to_string(), IndexSet::from([user.to_string()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove a reaction from a message
|
|
||||||
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
|
|
||||||
let mut messages = self.messages.lock().await;
|
|
||||||
if let Some(message) = messages.get_mut(id) {
|
|
||||||
if let Some(users) = message.reactions.get_mut(emoji) {
|
|
||||||
users.remove(&user.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove reaction from a message
|
|
||||||
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
|
|
||||||
let mut messages = self.messages.lock().await;
|
|
||||||
if let Some(message) = messages.get_mut(id) {
|
|
||||||
message.reactions.remove(emoji);
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a message from the database by its id
|
|
||||||
async fn delete_message(&self, id: &str) -> Result<()> {
|
|
||||||
let mut messages = self.messages.lock().await;
|
|
||||||
if messages.remove(id).is_some() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete messages from a channel by their ids and corresponding channel id
|
|
||||||
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
|
|
||||||
self.messages
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.retain(|id, message| message.channel != channel && !ids.contains(id));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
mod admin_migrations;
|
|
||||||
mod bots;
|
|
||||||
mod channel_invites;
|
|
||||||
mod channel_unreads;
|
|
||||||
mod channel_webhooks;
|
|
||||||
mod channels;
|
|
||||||
mod emojis;
|
|
||||||
mod files;
|
|
||||||
mod messages;
|
|
||||||
mod ratelimit_events;
|
|
||||||
mod server_bans;
|
|
||||||
mod server_members;
|
|
||||||
mod servers;
|
|
||||||
mod user_settings;
|
|
||||||
mod users;
|
|
||||||
|
|
||||||
pub use admin_migrations::*;
|
|
||||||
pub use bots::*;
|
|
||||||
pub use channel_invites::*;
|
|
||||||
pub use channel_unreads::*;
|
|
||||||
pub use channel_webhooks::*;
|
|
||||||
pub use channels::*;
|
|
||||||
pub use emojis::*;
|
|
||||||
pub use files::*;
|
|
||||||
pub use messages::*;
|
|
||||||
pub use ratelimit_events::*;
|
|
||||||
pub use server_bans::*;
|
|
||||||
pub use server_members::*;
|
|
||||||
pub use servers::*;
|
|
||||||
pub use user_settings::*;
|
|
||||||
pub use users::*;
|
|
||||||
|
|
||||||
use crate::{Database, MongoDb, ReferenceDb};
|
|
||||||
|
|
||||||
pub trait AbstractDatabase:
|
|
||||||
Sync
|
|
||||||
+ Send
|
|
||||||
+ admin_migrations::AbstractMigrations
|
|
||||||
+ bots::AbstractBots
|
|
||||||
+ channels::AbstractChannels
|
|
||||||
+ channel_invites::AbstractChannelInvites
|
|
||||||
+ channel_unreads::AbstractChannelUnreads
|
|
||||||
+ channel_webhooks::AbstractWebhooks
|
|
||||||
+ emojis::AbstractEmojis
|
|
||||||
+ files::AbstractAttachments
|
|
||||||
+ messages::AbstractMessages
|
|
||||||
+ ratelimit_events::AbstractRatelimitEvents
|
|
||||||
+ server_bans::AbstractServerBans
|
|
||||||
+ server_members::AbstractServerMembers
|
|
||||||
+ servers::AbstractServers
|
|
||||||
+ user_settings::AbstractUserSettings
|
|
||||||
+ users::AbstractUsers
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AbstractDatabase for ReferenceDb {}
|
|
||||||
impl AbstractDatabase for MongoDb {}
|
|
||||||
|
|
||||||
impl std::ops::Deref for Database {
|
|
||||||
type Target = dyn AbstractDatabase;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
match &self {
|
|
||||||
Database::Reference(dummy) => dummy,
|
|
||||||
Database::MongoDb(mongo) => mongo,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
use revolt_result::Result;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
use crate::Database;
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
/// Ratelimit Event
|
|
||||||
pub struct RatelimitEvent {
|
|
||||||
/// Id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: String,
|
|
||||||
/// Relevant Object Id
|
|
||||||
pub target_id: String,
|
|
||||||
/// Type of event
|
|
||||||
pub event_type: RatelimitEventType,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Event type
|
|
||||||
pub enum RatelimitEventType {
|
|
||||||
DiscriminatorChange,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
impl fmt::Display for RatelimitEventType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
fmt::Debug::fmt(self, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
impl RatelimitEvent {
|
|
||||||
/// Create ratelimit event
|
|
||||||
pub async fn create(
|
|
||||||
db: &Database,
|
|
||||||
target_id: String,
|
|
||||||
event_type: RatelimitEventType,
|
|
||||||
) -> Result<()> {
|
|
||||||
db.insert_ratelimit_event(&RatelimitEvent {
|
|
||||||
id: Ulid::new().to_string(),
|
|
||||||
target_id,
|
|
||||||
event_type,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crate::{revolt_result::Result, RatelimitEvent, RatelimitEventType};
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractRatelimitEvents: Sync + Send {
|
|
||||||
/// Insert a new ratelimit event
|
|
||||||
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()>;
|
|
||||||
|
|
||||||
/// Count number of events in given duration and check if we've hit the limit
|
|
||||||
async fn has_ratelimited(
|
|
||||||
&self,
|
|
||||||
target_id: &str,
|
|
||||||
event_type: RatelimitEventType,
|
|
||||||
period: Duration,
|
|
||||||
count: usize,
|
|
||||||
) -> Result<bool>;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
use std::time::{Duration, SystemTime};
|
|
||||||
|
|
||||||
use super::AbstractRatelimitEvents;
|
|
||||||
use crate::{MongoDb, RatelimitEvent, RatelimitEventType};
|
|
||||||
use revolt_result::Result;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
static COL: &str = "ratelimit_events";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractRatelimitEvents for MongoDb {
|
|
||||||
/// Insert a new ratelimit event
|
|
||||||
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &event).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count number of events in given duration and check if we've hit the limit
|
|
||||||
async fn has_ratelimited(
|
|
||||||
&self,
|
|
||||||
target_id: &str,
|
|
||||||
event_type: RatelimitEventType,
|
|
||||||
period: Duration,
|
|
||||||
count: usize,
|
|
||||||
) -> Result<bool> {
|
|
||||||
self.col::<RatelimitEvent>(COL)
|
|
||||||
.count_documents(
|
|
||||||
doc! {
|
|
||||||
"_id": {
|
|
||||||
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
|
|
||||||
},
|
|
||||||
"target_id": target_id,
|
|
||||||
"event_type": event_type.to_string()
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map(|c| c as usize >= count)
|
|
||||||
.map_err(|_| create_database_error!("count_documents", COL))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
use std::cmp::Ordering;
|
|
||||||
use std::time::Duration;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
use super::AbstractRatelimitEvents;
|
|
||||||
use crate::RatelimitEvent;
|
|
||||||
use crate::RatelimitEventType;
|
|
||||||
use crate::ReferenceDb;
|
|
||||||
use revolt_result::Result;
|
|
||||||
use ulid::Ulid;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractRatelimitEvents for ReferenceDb {
|
|
||||||
/// Insert a new ratelimit event
|
|
||||||
async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> {
|
|
||||||
let mut ratelimit_events = self.ratelimit_events.lock().await;
|
|
||||||
if ratelimit_events.contains_key(&event.id) {
|
|
||||||
Err(create_database_error!("insert", "message"))
|
|
||||||
} else {
|
|
||||||
ratelimit_events.insert(event.id.to_string(), event.clone());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Count number of events in given duration and check if we've hit the limit
|
|
||||||
async fn has_ratelimited(
|
|
||||||
&self,
|
|
||||||
target_id: &str,
|
|
||||||
event_type: RatelimitEventType,
|
|
||||||
period: Duration,
|
|
||||||
count: usize,
|
|
||||||
) -> Result<bool> {
|
|
||||||
let ratelimit_events = self.ratelimit_events.lock().await;
|
|
||||||
let gte_cmp_id = Ulid::from_datetime(SystemTime::now() - period).to_string();
|
|
||||||
|
|
||||||
Ok(ratelimit_events
|
|
||||||
.iter()
|
|
||||||
.filter(|(id, event)| {
|
|
||||||
id.cmp(&>e_cmp_id) == Ordering::Greater
|
|
||||||
&& event.target_id == target_id
|
|
||||||
&& event.event_type == event_type
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
>= count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
<mxfile host="65bd71144e">
|
|
||||||
<diagram id="9BKyaxnbqnTkbfLuLd1z" name="Page-1">
|
|
||||||
<mxGraphModel dx="2466" dy="650" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
|
||||||
<root>
|
|
||||||
<mxCell id="0"/>
|
|
||||||
<mxCell id="1" parent="0"/>
|
|
||||||
<mxCell id="56" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="6" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="57" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="5" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="260" y="370"/>
|
|
||||||
<mxPoint x="275" y="370"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="58" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="9" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="260" y="370"/>
|
|
||||||
<mxPoint x="342" y="370"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="59" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<mxPoint x="300" y="250" as="targetPoint"/>
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="260" y="370"/>
|
|
||||||
<mxPoint x="300" y="370"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="2" value="Attachment" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="220" y="490" width="80" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="51" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="3" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<mxPoint x="290" y="160" as="targetPoint"/>
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="95" y="340"/>
|
|
||||||
<mxPoint x="195" y="340"/>
|
|
||||||
<mxPoint x="195" y="260"/>
|
|
||||||
<mxPoint x="260" y="260"/>
|
|
||||||
<mxPoint x="260" y="220"/>
|
|
||||||
<mxPoint x="290" y="220"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="68" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" edge="1" parent="1" source="3" target="5">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="95" y="340"/>
|
|
||||||
<mxPoint x="245" y="340"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="3" value="Channel Invite" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="50" y="400" width="90" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="4" value="Channel Unread" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="-70" y="400" width="100" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="43" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;dashed=1;" parent="1" source="5" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="60" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;dashed=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="5" target="9" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="67" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.25;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=none;endFill=0;startArrow=diamondThin;startFill=1;" edge="1" parent="1" source="5" target="66">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="245" y="370"/>
|
|
||||||
<mxPoint x="195" y="370"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="5" value="Channel" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="230" y="280" width="60" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="45" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="6" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="46" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="6" target="5" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="310" y="355"/>
|
|
||||||
<mxPoint x="260" y="355"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="6" value="Message" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="280" y="400" width="60" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="54" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="7" target="9" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="505" y="350"/>
|
|
||||||
<mxPoint x="355" y="350"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="7" value="Server Ban" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="470" y="400" width="70" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="47" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="8" target="9" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="405" y="350"/>
|
|
||||||
<mxPoint x="355" y="350"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="48" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="8" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="405" y="350"/>
|
|
||||||
<mxPoint x="310" y="350"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="8" value="Server Member" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="360" y="400" width="90" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="29" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;startArrow=diamondThin;startFill=1;" parent="1" source="10" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="240" y="145"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="10" value="Bot" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="200" y="130" width="40" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="31" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="11" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="11" value="User Settings" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="150" y="210" width="90" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="15" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;" parent="1" source="12" target="13" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="12" value="User" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="280" y="130" width="40" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="13" value="Account" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="310" y="40" width="60" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="17" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;endArrow=diamondThin;endFill=1;" parent="1" source="14" target="13" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="14" value="Session" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="350" y="130" width="60" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="44" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;entryX=0.75;entryY=1;entryDx=0;entryDy=0;" parent="1" source="9" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<mxPoint x="310" y="160" as="targetPoint"/>
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="355" y="220"/>
|
|
||||||
<mxPoint x="310" y="220"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="9" value="Server" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="330" y="280" width="50" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="55" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;" parent="1" source="4" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<mxPoint x="290" y="160" as="targetPoint"/>
|
|
||||||
<mxPoint x="200" y="400" as="sourcePoint"/>
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="-20" y="340"/>
|
|
||||||
<mxPoint x="195" y="340"/>
|
|
||||||
<mxPoint x="195" y="260"/>
|
|
||||||
<mxPoint x="260" y="260"/>
|
|
||||||
<mxPoint x="260" y="220"/>
|
|
||||||
<mxPoint x="290" y="220"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="63" value="" style="edgeStyle=orthogonalEdgeStyle;html=1;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#FFFFFF;entryX=1;entryY=1;entryDx=0;entryDy=0;" parent="1" source="61" target="12" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<mxPoint x="584.25" y="335" as="targetPoint"/>
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="584" y="340"/>
|
|
||||||
<mxPoint x="390" y="340"/>
|
|
||||||
<mxPoint x="390" y="210"/>
|
|
||||||
<mxPoint x="320" y="210"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="64" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=1;strokeColor=#FFFFFF;" parent="1" source="61" target="9" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<Array as="points">
|
|
||||||
<mxPoint x="584" y="340"/>
|
|
||||||
<mxPoint x="368" y="340"/>
|
|
||||||
<mxPoint x="368" y="310"/>
|
|
||||||
</Array>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="61" value="Emoji" style="rounded=0;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
|
||||||
<mxGeometry x="558.5" y="400" width="51.5" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="65" style="edgeStyle=orthogonalEdgeStyle;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;startArrow=none;startFill=0;endArrow=diamondThin;endFill=0;strokeColor=#B3B3B3;dashed=1;" parent="1" source="2" target="61" edge="1">
|
|
||||||
<mxGeometry relative="1" as="geometry">
|
|
||||||
<mxPoint x="270" y="500" as="sourcePoint"/>
|
|
||||||
<mxPoint x="320" y="440" as="targetPoint"/>
|
|
||||||
</mxGeometry>
|
|
||||||
</mxCell>
|
|
||||||
<mxCell id="66" value="Webhook" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
|
||||||
<mxGeometry x="160" y="400" width="70" height="30" as="geometry"/>
|
|
||||||
</mxCell>
|
|
||||||
</root>
|
|
||||||
</mxGraphModel>
|
|
||||||
</diagram>
|
|
||||||
</mxfile>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
use crate::MemberCompositeKey;
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
/// Server Ban
|
|
||||||
pub struct ServerBan {
|
|
||||||
/// Unique member id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: MemberCompositeKey,
|
|
||||||
/// Reason for ban creation
|
|
||||||
pub reason: Option<String>,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::{MemberCompositeKey, ServerBan};
|
|
||||||
|
|
||||||
mod mongodb;
|
|
||||||
mod reference;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait AbstractServerBans: Sync + Send {
|
|
||||||
/// Insert new ban into database
|
|
||||||
async fn insert_ban(&self, ban: &ServerBan) -> Result<()>;
|
|
||||||
|
|
||||||
/// Fetch a server ban by server and user id
|
|
||||||
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan>;
|
|
||||||
|
|
||||||
/// Fetch all bans in a server
|
|
||||||
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>>;
|
|
||||||
|
|
||||||
/// Delete a ban from the database
|
|
||||||
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()>;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::MongoDb;
|
|
||||||
use crate::{MemberCompositeKey, ServerBan};
|
|
||||||
|
|
||||||
use super::AbstractServerBans;
|
|
||||||
|
|
||||||
static COL: &str = "server_bans";
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractServerBans for MongoDb {
|
|
||||||
/// Insert new ban into database
|
|
||||||
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
|
|
||||||
query!(self, insert_one, COL, &ban).map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a server ban by server and user id
|
|
||||||
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find_one,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": server_id,
|
|
||||||
"_id.user": user_id
|
|
||||||
}
|
|
||||||
)?
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all bans in a server
|
|
||||||
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
find,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": server_id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a ban from the database
|
|
||||||
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
|
|
||||||
query!(
|
|
||||||
self,
|
|
||||||
delete_one,
|
|
||||||
COL,
|
|
||||||
doc! {
|
|
||||||
"_id.server": &id.server,
|
|
||||||
"_id.user": &id.user
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.map(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
use revolt_result::Result;
|
|
||||||
|
|
||||||
use crate::ReferenceDb;
|
|
||||||
use crate::{MemberCompositeKey, ServerBan};
|
|
||||||
|
|
||||||
use super::AbstractServerBans;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AbstractServerBans for ReferenceDb {
|
|
||||||
/// Insert new ban into database
|
|
||||||
async fn insert_ban(&self, ban: &ServerBan) -> Result<()> {
|
|
||||||
let mut server_bans = self.server_bans.lock().await;
|
|
||||||
if server_bans.contains_key(&ban.id) {
|
|
||||||
Err(create_database_error!("insert", "ban"))
|
|
||||||
} else {
|
|
||||||
server_bans.insert(ban.id.clone(), ban.clone());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch a server ban by server and user id
|
|
||||||
async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result<ServerBan> {
|
|
||||||
let server_bans = self.server_bans.lock().await;
|
|
||||||
server_bans
|
|
||||||
.get(&MemberCompositeKey {
|
|
||||||
server: server_id.to_string(),
|
|
||||||
user: user_id.to_string(),
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch all bans in a server
|
|
||||||
async fn fetch_bans(&self, server_id: &str) -> Result<Vec<ServerBan>> {
|
|
||||||
let server_bans = self.server_bans.lock().await;
|
|
||||||
Ok(server_bans
|
|
||||||
.values()
|
|
||||||
.filter(|member| member.id.server == server_id)
|
|
||||||
.cloned()
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a ban from the database
|
|
||||||
async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> {
|
|
||||||
let mut server_bans = self.server_bans.lock().await;
|
|
||||||
if server_bans.remove(id).is_some() {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(create_error!(NotFound))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
mod model;
|
|
||||||
mod ops;
|
|
||||||
|
|
||||||
pub use model::*;
|
|
||||||
pub use ops::*;
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
use iso8601_timestamp::Timestamp;
|
|
||||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
|
||||||
use revolt_result::{create_error, Result};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, File,
|
|
||||||
Server, SystemMessage, User,
|
|
||||||
};
|
|
||||||
|
|
||||||
auto_derived_partial!(
|
|
||||||
/// Server Member
|
|
||||||
pub struct Member {
|
|
||||||
/// Unique member id
|
|
||||||
#[serde(rename = "_id")]
|
|
||||||
pub id: MemberCompositeKey,
|
|
||||||
|
|
||||||
/// Time at which this user joined the server
|
|
||||||
pub joined_at: Timestamp,
|
|
||||||
|
|
||||||
/// Member's nickname
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nickname: Option<String>,
|
|
||||||
/// Avatar attachment
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub avatar: Option<File>,
|
|
||||||
|
|
||||||
/// Member's roles
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
|
||||||
pub roles: Vec<String>,
|
|
||||||
/// Timestamp this member is timed out until
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub timeout: Option<Timestamp>,
|
|
||||||
},
|
|
||||||
"PartialMember"
|
|
||||||
);
|
|
||||||
|
|
||||||
auto_derived!(
|
|
||||||
/// Composite primary key consisting of server and user id
|
|
||||||
#[derive(Hash, Default)]
|
|
||||||
pub struct MemberCompositeKey {
|
|
||||||
/// Server Id
|
|
||||||
pub server: String,
|
|
||||||
/// User Id
|
|
||||||
pub user: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Optional fields on server member object
|
|
||||||
pub enum FieldsMember {
|
|
||||||
Nickname,
|
|
||||||
Avatar,
|
|
||||||
Roles,
|
|
||||||
Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Member removal intention
|
|
||||||
pub enum RemovalIntention {
|
|
||||||
Leave,
|
|
||||||
Kick,
|
|
||||||
Ban,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
impl Default for Member {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
id: Default::default(),
|
|
||||||
joined_at: Timestamp::now_utc(),
|
|
||||||
nickname: None,
|
|
||||||
avatar: None,
|
|
||||||
roles: vec![],
|
|
||||||
timeout: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::disallowed_methods)]
|
|
||||||
impl Member {
|
|
||||||
/// Create a new member in a server
|
|
||||||
pub async fn create(
|
|
||||||
db: &Database,
|
|
||||||
server: &Server,
|
|
||||||
user: &User,
|
|
||||||
channels: Option<Vec<Channel>>,
|
|
||||||
) -> Result<Vec<Channel>> {
|
|
||||||
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
|
|
||||||
return Err(create_error!(Banned));
|
|
||||||
}
|
|
||||||
|
|
||||||
if db.fetch_member(&server.id, &user.id).await.is_ok() {
|
|
||||||
return Err(create_error!(AlreadyInServer));
|
|
||||||
}
|
|
||||||
|
|
||||||
let member = Member {
|
|
||||||
id: MemberCompositeKey {
|
|
||||||
server: server.id.to_string(),
|
|
||||||
user: user.id.to_string(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
db.insert_member(&member).await?;
|
|
||||||
|
|
||||||
let should_fetch = channels.is_none();
|
|
||||||
let mut channels = channels.unwrap_or_default();
|
|
||||||
|
|
||||||
if should_fetch {
|
|
||||||
let query = DatabasePermissionQuery::new(db, user).server(server);
|
|
||||||
let existing_channels = db.fetch_channels(&server.channels).await?;
|
|
||||||
|
|
||||||
for channel in existing_channels {
|
|
||||||
let mut channel_query = query.clone().channel(&channel);
|
|
||||||
|
|
||||||
if calculate_channel_permissions(&mut channel_query)
|
|
||||||
.await
|
|
||||||
.has_channel_permission(ChannelPermission::ViewChannel)
|
|
||||||
{
|
|
||||||
channels.push(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventV1::ServerMemberJoin {
|
|
||||||
id: server.id.clone(),
|
|
||||||
user: user.id.clone(),
|
|
||||||
}
|
|
||||||
.p(server.id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
EventV1::ServerCreate {
|
|
||||||
id: server.id.clone(),
|
|
||||||
server: server.clone().into(),
|
|
||||||
channels: channels
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|channel| channel.into())
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
.private(user.id.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if let Some(id) = server
|
|
||||||
.system_messages
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|x| x.user_joined.as_ref())
|
|
||||||
{
|
|
||||||
SystemMessage::UserJoined {
|
|
||||||
id: user.id.clone(),
|
|
||||||
}
|
|
||||||
.into_message(id.to_string())
|
|
||||||
.send_without_notifications(db, false, false)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(channels)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update member data
|
|
||||||
pub async fn update<'a>(
|
|
||||||
&mut self,
|
|
||||||
db: &Database,
|
|
||||||
partial: PartialMember,
|
|
||||||
remove: Vec<FieldsMember>,
|
|
||||||
) -> Result<()> {
|
|
||||||
for field in &remove {
|
|
||||||
self.remove_field(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.apply_options(partial.clone());
|
|
||||||
|
|
||||||
db.update_member(&self.id, &partial, remove.clone()).await?;
|
|
||||||
|
|
||||||
EventV1::ServerMemberUpdate {
|
|
||||||
id: self.id.clone().into(),
|
|
||||||
data: partial.into(),
|
|
||||||
clear: remove.into_iter().map(|field| field.into()).collect(),
|
|
||||||
}
|
|
||||||
.p(self.id.server.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_field(&mut self, field: &FieldsMember) {
|
|
||||||
match field {
|
|
||||||
FieldsMember::Avatar => self.avatar = None,
|
|
||||||
FieldsMember::Nickname => self.nickname = None,
|
|
||||||
FieldsMember::Roles => self.roles.clear(),
|
|
||||||
FieldsMember::Timeout => self.timeout = None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get this user's current ranking
|
|
||||||
pub fn get_ranking(&self, server: &Server) -> i64 {
|
|
||||||
let mut value = i64::MAX;
|
|
||||||
for role in &self.roles {
|
|
||||||
if let Some(role) = server.roles.get(role) {
|
|
||||||
if role.rank < value {
|
|
||||||
value = role.rank;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check whether this member is in timeout
|
|
||||||
pub fn in_timeout(&self) -> bool {
|
|
||||||
if let Some(timeout) = self.timeout {
|
|
||||||
*timeout > *Timestamp::now_utc()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 405 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user