├── .config └── nextest.toml ├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── book.yml │ ├── docker.yaml │ ├── rust.yaml │ ├── triage_issue.yml │ └── triage_pr.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── Dockerfile.useCurrentArch ├── LICENSE ├── README.md ├── Revolt.toml ├── STYLE_GUIDE.md ├── clippy.toml ├── compose.yml ├── crates ├── bindings │ ├── node │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── src │ │ │ └── lib.rs │ │ └── test.js │ └── package-lock.json ├── bonfire │ ├── .gitignore │ ├── Cargo.toml │ ├── Dockerfile │ ├── LICENSE │ └── src │ │ ├── config.rs │ │ ├── database.rs │ │ ├── events │ │ ├── impl.rs │ │ ├── mod.rs │ │ └── state.rs │ │ ├── main.rs │ │ └── websocket.rs ├── core │ ├── config │ │ ├── Cargo.toml │ │ ├── LICENSE │ │ ├── Revolt.test.toml │ │ ├── Revolt.toml │ │ └── src │ │ │ └── lib.rs │ ├── database │ │ ├── Cargo.toml │ │ ├── fixtures │ │ │ ├── group_with_members.json │ │ │ └── server_with_roles.json │ │ ├── src │ │ │ ├── amqp │ │ │ │ ├── amqp.rs │ │ │ │ └── mod.rs │ │ │ ├── drivers │ │ │ │ ├── mod.rs │ │ │ │ ├── mongodb.rs │ │ │ │ └── reference.rs │ │ │ ├── events │ │ │ │ ├── client.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── rabbit.rs │ │ │ │ └── server.rs │ │ │ ├── lib.rs │ │ │ ├── models │ │ │ │ ├── admin_migrations │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ ├── mongodb │ │ │ │ │ │ ├── init.rs │ │ │ │ │ │ └── scripts.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── bots │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── channel_invites │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── channel_unreads │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── channel_webhooks │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── channels │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── emojis │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ ├── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ │ └── unicode_emoji.txt │ │ │ │ ├── file_hashes │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── files │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── messages │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── ratelimit_events │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── relations.drawio │ │ │ │ ├── safety_reports │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── safety_snapshots │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── server_bans │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── server_members │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── servers │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ ├── user_settings │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ └── ops │ │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ │ └── reference.rs │ │ │ │ └── users │ │ │ │ │ ├── axum.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── model.rs │ │ │ │ │ ├── ops.rs │ │ │ │ │ ├── ops │ │ │ │ │ ├── mongodb.rs │ │ │ │ │ └── reference.rs │ │ │ │ │ ├── rocket.rs │ │ │ │ │ └── schema.rs │ │ │ ├── tasks │ │ │ │ ├── ack.rs │ │ │ │ ├── authifier_relay.rs │ │ │ │ ├── last_message_id.rs │ │ │ │ ├── mod.rs │ │ │ │ └── process_embeds.rs │ │ │ └── util │ │ │ │ ├── bridge │ │ │ │ ├── mod.rs │ │ │ │ └── v0.rs │ │ │ │ ├── bulk_permissions.rs │ │ │ │ ├── idempotency.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── permissions.rs │ │ │ │ ├── reference.rs │ │ │ │ └── test_fixtures.rs │ │ └── templates │ │ │ ├── deletion.html │ │ │ ├── deletion.original.html │ │ │ ├── deletion.txt │ │ │ ├── reset.html │ │ │ ├── reset.original.html │ │ │ ├── reset.txt │ │ │ ├── suspension.html │ │ │ ├── suspension.original.html │ │ │ ├── suspension.txt │ │ │ ├── verify.html │ │ │ ├── verify.original.html │ │ │ └── verify.txt │ ├── files │ │ ├── Cargo.toml │ │ └── src │ │ │ └── lib.rs │ ├── models │ │ ├── Cargo.toml │ │ ├── LICENSE │ │ └── src │ │ │ ├── lib.rs │ │ │ └── v0 │ │ │ ├── bots.rs │ │ │ ├── channel_invites.rs │ │ │ ├── channel_unreads.rs │ │ │ ├── channel_webhooks.rs │ │ │ ├── channels.rs │ │ │ ├── embeds.rs │ │ │ ├── emojis.rs │ │ │ ├── files.rs │ │ │ ├── messages.rs │ │ │ ├── mod.rs │ │ │ ├── safety_reports.rs │ │ │ ├── server_bans.rs │ │ │ ├── server_members.rs │ │ │ ├── servers.rs │ │ │ ├── user_settings.rs │ │ │ └── users.rs │ ├── permissions │ │ ├── Cargo.toml │ │ ├── LICENSE │ │ └── src │ │ │ ├── impl.rs │ │ │ ├── lib.rs │ │ │ ├── models │ │ │ ├── channel.rs │ │ │ ├── mod.rs │ │ │ ├── server.rs │ │ │ └── user.rs │ │ │ ├── test.rs │ │ │ └── trait.rs │ ├── presence │ │ ├── Cargo.toml │ │ └── src │ │ │ ├── lib.rs │ │ │ └── operations.rs │ └── result │ │ ├── Cargo.toml │ │ ├── LICENSE │ │ └── src │ │ ├── axum.rs │ │ ├── lib.rs │ │ ├── okapi.rs │ │ └── rocket.rs ├── daemons │ └── pushd │ │ ├── Cargo.toml │ │ ├── Dockerfile │ │ ├── Pushd Flowchart.graffle │ │ └── src │ │ ├── consumers │ │ ├── inbound │ │ │ ├── ack.rs │ │ │ ├── fr_accepted.rs │ │ │ ├── fr_received.rs │ │ │ ├── generic.rs │ │ │ ├── internal.rs │ │ │ ├── message.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ └── outbound │ │ │ ├── apn.rs │ │ │ ├── fcm.rs │ │ │ ├── mod.rs │ │ │ └── vapid.rs │ │ └── main.rs ├── delta │ ├── Cargo.toml │ ├── Dockerfile │ ├── LICENSE │ ├── build.rs │ └── src │ │ ├── main.rs │ │ ├── routes │ │ ├── bots │ │ │ ├── create.rs │ │ │ ├── delete.rs │ │ │ ├── edit.rs │ │ │ ├── fetch.rs │ │ │ ├── fetch_owned.rs │ │ │ ├── fetch_public.rs │ │ │ ├── invite.rs │ │ │ └── mod.rs │ │ ├── channels │ │ │ ├── channel_ack.rs │ │ │ ├── channel_delete.rs │ │ │ ├── channel_edit.rs │ │ │ ├── channel_fetch.rs │ │ │ ├── group_add_member.rs │ │ │ ├── group_create.rs │ │ │ ├── group_remove_member.rs │ │ │ ├── invite_create.rs │ │ │ ├── members_fetch.rs │ │ │ ├── message_bulk_delete.rs │ │ │ ├── message_clear_reactions.rs │ │ │ ├── message_delete.rs │ │ │ ├── message_edit.rs │ │ │ ├── message_fetch.rs │ │ │ ├── message_pin.rs │ │ │ ├── message_query.rs │ │ │ ├── message_react.rs │ │ │ ├── message_search.rs │ │ │ ├── message_send.rs │ │ │ ├── message_unpin.rs │ │ │ ├── message_unreact.rs │ │ │ ├── mod.rs │ │ │ ├── permissions_set.rs │ │ │ ├── permissions_set_default.rs │ │ │ ├── voice_join.rs │ │ │ ├── webhook_create.rs │ │ │ └── webhook_fetch_all.rs │ │ ├── customisation │ │ │ ├── emoji_create.rs │ │ │ ├── emoji_delete.rs │ │ │ ├── emoji_fetch.rs │ │ │ └── mod.rs │ │ ├── invites │ │ │ ├── invite_delete.rs │ │ │ ├── invite_fetch.rs │ │ │ ├── invite_join.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── onboard │ │ │ ├── complete.rs │ │ │ ├── hello.rs │ │ │ └── mod.rs │ │ ├── push │ │ │ ├── mod.rs │ │ │ ├── subscribe.rs │ │ │ └── unsubscribe.rs │ │ ├── root.rs │ │ ├── safety │ │ │ ├── mod.rs │ │ │ └── report_content.rs │ │ ├── servers │ │ │ ├── ban_create.rs │ │ │ ├── ban_list.rs │ │ │ ├── ban_remove.rs │ │ │ ├── channel_create.rs │ │ │ ├── emoji_list.rs │ │ │ ├── invites_fetch.rs │ │ │ ├── member_edit.rs │ │ │ ├── member_experimental_query.rs │ │ │ ├── member_fetch.rs │ │ │ ├── member_fetch_all.rs │ │ │ ├── member_remove.rs │ │ │ ├── mod.rs │ │ │ ├── permissions_set.rs │ │ │ ├── permissions_set_default.rs │ │ │ ├── roles_create.rs │ │ │ ├── roles_delete.rs │ │ │ ├── roles_edit.rs │ │ │ ├── roles_fetch.rs │ │ │ ├── server_ack.rs │ │ │ ├── server_create.rs │ │ │ ├── server_delete.rs │ │ │ ├── server_edit.rs │ │ │ └── server_fetch.rs │ │ ├── sync │ │ │ ├── get_settings.rs │ │ │ ├── get_unreads.rs │ │ │ ├── mod.rs │ │ │ └── set_settings.rs │ │ ├── users │ │ │ ├── add_friend.rs │ │ │ ├── avatars │ │ │ │ ├── 1.png │ │ │ │ ├── 2.png │ │ │ │ ├── 3.png │ │ │ │ ├── 4.png │ │ │ │ ├── 5.png │ │ │ │ ├── 6.png │ │ │ │ └── 7.png │ │ │ ├── block_user.rs │ │ │ ├── change_username.rs │ │ │ ├── edit_user.rs │ │ │ ├── fetch_dms.rs │ │ │ ├── fetch_profile.rs │ │ │ ├── fetch_self.rs │ │ │ ├── fetch_user.rs │ │ │ ├── fetch_user_flags.rs │ │ │ ├── find_mutual.rs │ │ │ ├── get_default_avatar.rs │ │ │ ├── mod.rs │ │ │ ├── open_dm.rs │ │ │ ├── remove_friend.rs │ │ │ ├── send_friend_request.rs │ │ │ └── unblock_user.rs │ │ └── webhooks │ │ │ ├── mod.rs │ │ │ ├── webhook_delete.rs │ │ │ ├── webhook_delete_token.rs │ │ │ ├── webhook_edit.rs │ │ │ ├── webhook_edit_token.rs │ │ │ ├── webhook_execute.rs │ │ │ ├── webhook_execute_github.rs │ │ │ ├── webhook_fetch.rs │ │ │ └── webhook_fetch_token.rs │ │ └── util │ │ ├── mod.rs │ │ ├── ratelimiter.rs │ │ └── test.rs └── services │ ├── dove │ ├── Cargo.toml │ ├── Dockerfile │ └── src │ │ ├── api.rs │ │ ├── main.rs │ │ ├── requests.rs │ │ └── website_embed.rs │ └── pigeon │ ├── Cargo.toml │ ├── Dockerfile │ └── src │ ├── api.rs │ ├── clamav.rs │ ├── exif.rs │ ├── main.rs │ ├── metadata.rs │ └── mime_type.rs ├── default.nix ├── deny.toml ├── doc ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── hello.md │ └── new_features.md ├── justfile └── scripts ├── build-image-layer.sh ├── publish-debug-image.sh ├── start.sh └── try-tag-and-release.sh /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | slow-timeout = { period = "3s", terminate-after = 2 } 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | target 3 | .mongo 4 | .env -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | -------------------------------------------------------------------------------- /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: Build documentation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write # To push a branch 12 | pages: write # To push to a GitHub Pages site 13 | id-token: write # To update the deployment status 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Install latest mdbook 19 | run: | 20 | tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name') 21 | url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz" 22 | mkdir mdbook 23 | curl -sSL $url | tar -xz --directory=./mdbook 24 | echo `pwd`/mdbook >> $GITHUB_PATH 25 | - name: Build Book 26 | run: | 27 | cd doc 28 | mdbook build 29 | - name: Setup Pages 30 | uses: actions/configure-pages@v4 31 | - name: Upload artifact 32 | uses: actions/upload-pages-artifact@v3 33 | with: 34 | path: "doc/book" 35 | - name: Deploy to GitHub Pages 36 | id: deployment 37 | uses: actions/deploy-pages@v4 38 | -------------------------------------------------------------------------------- /.github/workflows/triage_issue.yml: -------------------------------------------------------------------------------- 1 | name: Add Issue to Board 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | track_issue: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Get project data 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.PAT }} 14 | run: | 15 | gh api graphql -f query=' 16 | query { 17 | organization(login: "revoltchat"){ 18 | projectV2(number: 3) { 19 | id 20 | fields(first:20) { 21 | nodes { 22 | ... on ProjectV2SingleSelectField { 23 | id 24 | name 25 | options { 26 | id 27 | name 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | }' > project_data.json 35 | 36 | echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV 37 | echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV 38 | echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV 39 | 40 | - name: Add issue to project 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.PAT }} 43 | ISSUE_ID: ${{ github.event.issue.node_id }} 44 | run: | 45 | item_id="$( gh api graphql -f query=' 46 | mutation($project:ID!, $issue:ID!) { 47 | addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) { 48 | item { 49 | id 50 | } 51 | } 52 | }' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')" 53 | 54 | echo 'ITEM_ID='$item_id >> $GITHUB_ENV 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Rocket.toml 2 | Revolt.*.toml 3 | 4 | target 5 | .data 6 | .env 7 | 8 | .vercel 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "rust-analyzer.checkOnSave.command": "clippy", 4 | "nixEnvSelector.suggestion": false, 5 | "nixEnvSelector.nixFile": "${workspaceFolder}/default.nix" 6 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "crates/delta", 6 | "crates/bonfire", 7 | "crates/core/*", 8 | "crates/services/*", 9 | "crates/bindings/*", 10 | "crates/daemons/pushd", 11 | ] 12 | 13 | [patch.crates-io] 14 | redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" } 15 | redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" } 16 | 17 | [profile.release] 18 | lto = true -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM --platform="${BUILDPLATFORM}" rust:1.77.2-slim-bookworm 3 | USER 0:0 4 | WORKDIR /home/rust/src 5 | 6 | ARG TARGETARCH 7 | 8 | # Install build requirements 9 | RUN dpkg --add-architecture "${TARGETARCH}" 10 | RUN apt-get update && \ 11 | apt-get install -y \ 12 | make \ 13 | pkg-config \ 14 | libssl-dev:"${TARGETARCH}" 15 | COPY scripts/build-image-layer.sh /tmp/ 16 | RUN sh /tmp/build-image-layer.sh tools 17 | 18 | # Build all dependencies 19 | COPY Cargo.toml Cargo.lock ./ 20 | COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/ 21 | COPY crates/bonfire/Cargo.toml ./crates/bonfire/ 22 | COPY crates/delta/Cargo.toml ./crates/delta/ 23 | COPY crates/core/config/Cargo.toml ./crates/core/config/ 24 | COPY crates/core/database/Cargo.toml ./crates/core/database/ 25 | COPY crates/core/files/Cargo.toml ./crates/core/files/ 26 | COPY crates/core/models/Cargo.toml ./crates/core/models/ 27 | COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ 28 | COPY crates/core/presence/Cargo.toml ./crates/core/presence/ 29 | COPY crates/core/result/Cargo.toml ./crates/core/result/ 30 | COPY crates/services/pigeon/Cargo.toml ./crates/services/pigeon/ 31 | COPY crates/services/dove/Cargo.toml ./crates/services/dove/ 32 | COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/ 33 | RUN sh /tmp/build-image-layer.sh deps 34 | 35 | # Build all apps 36 | COPY crates ./crates 37 | RUN sh /tmp/build-image-layer.sh apps 38 | -------------------------------------------------------------------------------- /Dockerfile.useCurrentArch: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM rust:1.77.2-slim-bookworm 3 | USER 0:0 4 | WORKDIR /home/rust/src 5 | 6 | # Install build requirements 7 | RUN apt-get update && \ 8 | apt-get install -y \ 9 | make \ 10 | pkg-config \ 11 | libssl-dev 12 | COPY scripts/build-image-layer.sh /tmp/ 13 | 14 | # Build all dependencies 15 | COPY Cargo.toml Cargo.lock ./ 16 | COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/ 17 | COPY crates/bonfire/Cargo.toml ./crates/bonfire/ 18 | COPY crates/delta/Cargo.toml ./crates/delta/ 19 | COPY crates/core/config/Cargo.toml ./crates/core/config/ 20 | COPY crates/core/database/Cargo.toml ./crates/core/database/ 21 | COPY crates/core/files/Cargo.toml ./crates/core/files/ 22 | COPY crates/core/models/Cargo.toml ./crates/core/models/ 23 | COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/ 24 | COPY crates/core/presence/Cargo.toml ./crates/core/presence/ 25 | COPY crates/core/result/Cargo.toml ./crates/core/result/ 26 | COPY crates/services/pigeon/Cargo.toml ./crates/services/pigeon/ 27 | COPY crates/services/dove/Cargo.toml ./crates/services/dove/ 28 | RUN sh /tmp/build-image-layer.sh deps 29 | 30 | # Build all apps 31 | COPY crates ./crates 32 | RUN sh /tmp/build-image-layer.sh apps 33 | -------------------------------------------------------------------------------- /Revolt.toml: -------------------------------------------------------------------------------- 1 | # ⚠️ This configuration is intended for development environment. 2 | # If you'd like to override anything, create a Revolt.override.toml 3 | 4 | [database] 5 | # MongoDB connection URL 6 | # Defaults to the container name specified in self-hosted 7 | mongodb = "mongodb://127.0.0.1:14017" 8 | # Redis connection URL 9 | # Defaults to the container name specified in self-hosted 10 | redis = "redis://127.0.0.1:14079/" 11 | 12 | [hosts] 13 | # Web locations of various services 14 | # Defaults assume all services are reverse-proxied 15 | # See https://github.com/revoltchat/self-hosted/blob/main/Caddyfile 16 | # 17 | # Remember to change these to https/wss where appropriate in production! 18 | app = "http://local.revolt.chat:14701" 19 | api = "http://local.revolt.chat:14702" 20 | events = "ws://local.revolt.chat:14703" 21 | pigeon = "http://local.revolt.chat:14704" 22 | dove = "http://local.revolt.chat:14705" 23 | voso_legacy = "" 24 | voso_legacy_ws = "" 25 | 26 | [api] 27 | 28 | [api.smtp] 29 | # Email server configuration for verification 30 | # Defaults to no email verification (host field is empty) 31 | host = "localhost" 32 | username = "smtp" 33 | password = "smtp" 34 | from_address = "development@revolt.chat" 35 | reply_to = "support@revolt.chat" 36 | port = 14025 37 | use_tls = false 38 | 39 | [files.s3] 40 | # S3 protocol endpoint 41 | endpoint = "http://127.0.0.1:14009" 42 | # S3 region name 43 | region = "minio" 44 | # S3 protocol key ID 45 | access_key_id = "miniopigeon" 46 | # S3 protocol access key 47 | secret_access_key = "miniopigeon" 48 | # Bucket to upload to by default 49 | default_bucket = "upryzing-uploads" 50 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | disallowed-methods = [ 2 | # Shouldn't need to access these directly 3 | "upryzing_database::models::bots::model::Bot::remove_field", 4 | "upryzing_database::models::messages::model::Message::attach_sendable_embed", 5 | "upryzing_database::models::users::model::User::set_relationship", 6 | "upryzing_database::models::users::model::User::apply_relationship", 7 | 8 | # Prefer to use Object::create() 9 | "upryzing_database::models::bots::ops::AbstractBots::insert_bot", 10 | "upryzing_database::models::channel_invites::ops::AbstractChannelInvites::insert_invite", 11 | "upryzing_database::models::channel_unreads::ops::AbstractChannelUnreads::acknowledge_message", 12 | "upryzing_database::models::channel_webhooks::ops::AbstractWebhooks::insert_webhook", 13 | "upryzing_database::models::channels::ops::AbstractChannels::insert_channel", 14 | "upryzing_database::models::emojis::ops::AbstractEmojis::insert_emoji", 15 | "upryzing_database::models::files::ops::AbstractAttachments::insert_attachment", 16 | "upryzing_database::models::messages::ops::AbstractMessages::insert_message", 17 | "upryzing_database::models::ratelimit_events::ops::AbstractRatelimitEvents::insert_ratelimit_event", 18 | "upryzing_database::models::server_bans::ops::AbstractServerBans::insert_ban", 19 | "upryzing_database::models::server_members::ops::AbstractServerMembers::insert_member", 20 | "upryzing_database::models::servers::ops::AbstractServers::insert_server", 21 | "upryzing_database::models::users::ops::AbstractUsers::insert_user", 22 | 23 | # Prefer to use Object::update(&self) 24 | "upryzing_database::models::bots::ops::AbstractBots::update_bot", 25 | 26 | # Prefer to use Object::delete(&self) 27 | "upryzing_database::models::bots::ops::AbstractBots::delete_bot", 28 | ] 29 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # Redis 3 | redis: 4 | image: eqalpha/keydb 5 | ports: 6 | - "6379:6379" 7 | 8 | # MongoDB 9 | database: 10 | image: mongo 11 | ports: 12 | - "27017:27017" 13 | volumes: 14 | - ./.data/db:/data/db 15 | 16 | # MinIO 17 | minio: 18 | image: minio/minio 19 | command: server /data 20 | environment: 21 | MINIO_ROOT_USER: miniopigeon 22 | MINIO_ROOT_PASSWORD: miniopigeon 23 | volumes: 24 | - ./.data/minio:/data 25 | ports: 26 | - "14009:9000" 27 | - "14010:9001" 28 | restart: always 29 | 30 | # Create buckets for minio. 31 | createbuckets: 32 | image: minio/mc 33 | depends_on: 34 | - minio 35 | entrypoint: > 36 | /bin/sh -c "while ! /usr/bin/mc ready minio; do 37 | /usr/bin/mc config host add minio http://minio:9000 miniopigeon miniopigeon; 38 | echo 'Waiting minio...' && sleep 1; 39 | done; /usr/bin/mc mb minio/upryzing-uploads; exit 0;" 40 | 41 | # Rabbit 42 | rabbit: 43 | image: rabbitmq:3-management 44 | environment: 45 | RABBITMQ_DEFAULT_USER: rabbituser 46 | RABBITMQ_DEFAULT_PASS: rabbitpass 47 | volumes: 48 | - ./.data/rabbit:/var/lib/rabbitmq 49 | #- ./rabbit_plugins:/opt/rabbitmq/plugins/ 50 | #- ./rabbit_enabled_plugins:/etc/rabbitmq/enabled_plugins 51 | # uncomment this if you need to enable other plugins 52 | ports: 53 | - "5672:5672" 54 | - "15672:15672" # management UI, for development 55 | 56 | # Mock SMTP server 57 | maildev: 58 | image: soulteary/maildev 59 | ports: 60 | - "14025:25" 61 | - "14080:8080" 62 | environment: 63 | MAILDEV_SMTP_PORT: 25 64 | MAILDEV_WEB_PORT: 8080 65 | MAILDEV_INCOMING_USER: smtp 66 | MAILDEV_INCOMING_PASS: smtp 67 | -------------------------------------------------------------------------------- /crates/bindings/node/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | index.node 3 | **/node_modules 4 | **/.DS_Store 5 | npm-debug.log* 6 | cargo.log 7 | cross.log 8 | -------------------------------------------------------------------------------- /crates/bindings/node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-nodejs-bindings" 3 | version = "0.1.0" 4 | description = "Node.js bindings for the Revolt software" 5 | authors = ["Paul Makles "] 6 | license = "MIT" 7 | edition = "2021" 8 | exclude = ["index.node"] 9 | 10 | [lib] 11 | crate-type = ["cdylib"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | neon = "1.0.0" 17 | neon-serde4 = "1.0.0" 18 | 19 | serde = { version = "1", features = ["derive"] } 20 | 21 | async-std = "1.12.0" 22 | 23 | upryzing-config = { version = "0.1.0", path = "../../core/config" } 24 | upryzing-result = { version = "0.1.0", path = "../../core/result" } 25 | upryzing-database = { version = "0.1.0", path = "../../core/database" } 26 | -------------------------------------------------------------------------------- /crates/bindings/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upryzing-nodejs-bindings", 3 | "version": "0.7.15-rev0.0.3", 4 | "description": "Node.js bindings for the Revolt software", 5 | "main": "index.node", 6 | "scripts": { 7 | "test": "cargo test", 8 | "cargo-build": "cargo build --message-format=json > cargo.log", 9 | "cross-build": "cross build --message-format=json > cross.log", 10 | "postcargo-build": "neon dist < cargo.log", 11 | "postcross-build": "neon dist -m /target < cross.log", 12 | "debug": "npm run cargo-build --", 13 | "build": "npm run cargo-build -- --release", 14 | "cross": "npm run cross-build -- --release" 15 | }, 16 | "author": "Paul Makles", 17 | "license": "AGPL-3.0", 18 | "devDependencies": { 19 | "@neon-rs/cli": "0.1.73" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/upryzing/parrot" 24 | }, 25 | "keywords": [ 26 | "revolt", 27 | "chat" 28 | ], 29 | "bugs": { 30 | "url": "https://github.com/upryzing/parrot/issues" 31 | }, 32 | "homepage": "https://github.com/upryzing/parrot#readme", 33 | "dependencies": { 34 | "revolt-api": "^0.7.15" 35 | } 36 | } -------------------------------------------------------------------------------- /crates/bindings/node/test.js: -------------------------------------------------------------------------------- 1 | const Internal = require("."); 2 | 3 | // playing around with class wrapper, not practical 4 | class Model { 5 | constructor(model) { 6 | this.model = model; 7 | } 8 | 9 | data() { 10 | return Internal.model_data.bind(this.model)(); 11 | } 12 | 13 | error() { 14 | return Internal.model_error.bind(this.model)(); 15 | } 16 | } 17 | 18 | class User extends Model { 19 | constructor(db, user) { 20 | super(user); 21 | this.db = db; 22 | } 23 | } 24 | 25 | class Database { 26 | constructor() { 27 | this.db = Internal.database(); 28 | } 29 | 30 | async fetchUser(userId) { 31 | return new User( 32 | this, 33 | await Internal.database_fetch_user.bind(this.db)(userId) 34 | ); 35 | } 36 | 37 | async fetchUserByUsername(username, discriminator) { 38 | return new User( 39 | this, 40 | await Internal.database_fetch_user_by_username.bind(this.db)( 41 | username, 42 | discriminator 43 | ) 44 | ); 45 | } 46 | } 47 | 48 | const db = new Database(); 49 | db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data())); 50 | db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data())); 51 | db.fetchUserByUsername("dos", "1111").then((user) => 52 | console.info(user.error()) 53 | ); 54 | -------------------------------------------------------------------------------- /crates/bindings/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bindings", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /crates/bonfire/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /crates/bonfire/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-bonfire" 3 | version = "0.1.0" 4 | license = "AGPL-3.0-or-later" 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | # util 11 | log = "*" 12 | sentry = "0.31.5" 13 | lru = "0.7.6" 14 | ulid = "0.5.0" 15 | once_cell = "1.9.0" 16 | redis-kiss = "0.1.4" 17 | lru_time_cache = "0.11.11" 18 | async-channel = "2.3.1" 19 | 20 | # parsing 21 | querystring = "1.1.0" 22 | 23 | # serde 24 | bincode = "1.3.3" 25 | serde_json = "1.0.79" 26 | rmp-serde = "1.0.0" 27 | serde = "1.0.136" 28 | 29 | # async 30 | futures = "0.3.21" 31 | async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] } 32 | async-std = { version = "1.8.0", features = [ 33 | "tokio1", 34 | "tokio02", 35 | "attributes", 36 | ] } 37 | 38 | # core 39 | authifier = { version = "1.0.9" } 40 | upryzing-result = { path = "../core/result" } 41 | upryzing-models = { path = "../core/models" } 42 | upryzing-config = { path = "../core/config" } 43 | upryzing-database = { path = "../core/database" } 44 | upryzing-permissions = { version = "0.1.0", path = "../core/permissions" } 45 | upryzing-presence = { path = "../core/presence", features = ["redis-is-patched"] } 46 | 47 | # redis 48 | fred = { version = "8.0.1", features = ["subscriber-client"] } 49 | -------------------------------------------------------------------------------- /crates/bonfire/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM ghcr.io/upryzing/base:latest AS builder 3 | FROM debian:12 AS debian 4 | 5 | # Bundle Stage 6 | FROM gcr.io/distroless/cc-debian12:nonroot 7 | COPY --from=builder /home/rust/src/target/release/upryzing-bonfire ./ 8 | COPY --from=debian /usr/bin/uname /usr/bin/uname 9 | 10 | EXPOSE 14703 11 | USER nonroot 12 | CMD ["./upryzing-bonfire"] 13 | -------------------------------------------------------------------------------- /crates/bonfire/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /crates/bonfire/src/database.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::OnceCell; 2 | use upryzing_database::{Database, DatabaseInfo}; 3 | 4 | static DBCONN: OnceCell = OnceCell::new(); 5 | 6 | /// Connect Bonfire to the database. 7 | pub async fn connect() { 8 | let database = DatabaseInfo::Auto 9 | .connect() 10 | .await 11 | .expect("Failed to connect to the database."); 12 | 13 | if DBCONN.set(database).is_err() { 14 | panic!("couldn't set database") 15 | } 16 | } 17 | 18 | /// Get a reference to the current database. 19 | pub fn get_db() -> &'static Database { 20 | DBCONN.get().expect("Valid `Database`") 21 | } 22 | -------------------------------------------------------------------------------- /crates/bonfire/src/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod r#impl; 2 | pub mod state; 3 | -------------------------------------------------------------------------------- /crates/bonfire/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use async_std::net::TcpListener; 4 | use upryzing_presence::clear_region; 5 | 6 | #[macro_use] 7 | extern crate log; 8 | 9 | pub mod config; 10 | pub mod events; 11 | 12 | mod database; 13 | mod websocket; 14 | 15 | #[async_std::main] 16 | async fn main() { 17 | // Configure requirements for Bonfire. 18 | upryzing_config::configure!(events); 19 | database::connect().await; 20 | 21 | // Clean up the current region information. 22 | clear_region(None).await; 23 | 24 | // Setup a TCP listener to accept WebSocket connections on. 25 | // By default, we bind to port 14703 on all interfaces. 26 | let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into()); 27 | info!("Listening on host {bind}"); 28 | let try_socket = TcpListener::bind(bind).await; 29 | let listener = try_socket.expect("Failed to bind"); 30 | 31 | // Start accepting new connections and spawn a client for each connection. 32 | while let Ok((stream, addr)) = listener.accept().await { 33 | async_std::task::spawn(async move { 34 | info!("User connected from {addr:?}"); 35 | websocket::client(database::get_db(), stream, addr).await; 36 | info!("User disconnected from {addr:?}"); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/core/config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-config" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["Paul Makles "] 7 | description = "Revolt Backend: Configuration" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [features] 12 | report-macros = ["upryzing-result"] 13 | test = ["async-std"] 14 | default = ["test"] 15 | 16 | [dependencies] 17 | # Utility 18 | config = { version = "0.15.11", default-features = false, features = ["toml"] } 19 | cached = "0.44.0" 20 | once_cell = "1.18.0" 21 | 22 | # Serde 23 | serde = { version = "1", features = ["derive"] } 24 | 25 | # Async 26 | futures-locks = "0.7.1" 27 | async-std = { version = "1.8.0", features = ["attributes"], optional = true } 28 | 29 | # Logging 30 | log = "0.4.14" 31 | pretty_env_logger = "0.4.0" 32 | 33 | # Sentry 34 | sentry = "0.31.5" 35 | 36 | # Core 37 | upryzing-result = { version = "0.1.0", path = "../result", optional = true } 38 | -------------------------------------------------------------------------------- /crates/core/config/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pawel Makles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /crates/core/config/Revolt.test.toml: -------------------------------------------------------------------------------- 1 | [database] 2 | mongodb = "mongodb://localhost" 3 | redis = "redis://localhost/" 4 | 5 | [rabbit] 6 | host = "127.0.0.1" 7 | port = 5672 8 | username = "rabbituser" 9 | password = "rabbitpass" 10 | -------------------------------------------------------------------------------- /crates/core/database/fixtures/group_with_members.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "_object_type": "User", 4 | "_id": "__ID:0__", 5 | "username": "Owner", 6 | "discriminator": "0001" 7 | }, 8 | { 9 | "_object_type": "User", 10 | "_id": "__ID:1__", 11 | "username": "Member", 12 | "discriminator": "0001" 13 | }, 14 | { 15 | "_object_type": "User", 16 | "_id": "__ID:2__", 17 | "username": "Member", 18 | "discriminator": "0002" 19 | }, 20 | { 21 | "_object_type": "Channel", 22 | "_id": "__ID:3__", 23 | "channel_type": "Group", 24 | "name": "My Group", 25 | "owner": "__ID:0__", 26 | "recipients": ["__ID:0__", "__ID:1__"] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /crates/core/database/src/amqp/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::module_inception)] 2 | pub mod amqp; 3 | -------------------------------------------------------------------------------- /crates/core/database/src/drivers/reference.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc}; 2 | 3 | use futures::lock::Mutex; 4 | 5 | use crate::{ 6 | Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member, 7 | MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User, 8 | UserSettings, Webhook, 9 | }; 10 | 11 | database_derived!( 12 | /// Reference implementation 13 | #[derive(Default)] 14 | pub struct ReferenceDb { 15 | pub bots: Arc>>, 16 | pub channels: Arc>>, 17 | pub channel_invites: Arc>>, 18 | pub channel_unreads: Arc>>, 19 | pub channel_webhooks: Arc>>, 20 | pub emojis: Arc>>, 21 | pub file_hashes: Arc>>, 22 | pub files: Arc>>, 23 | pub messages: Arc>>, 24 | pub ratelimit_events: Arc>>, 25 | pub user_settings: Arc>>, 26 | pub users: Arc>>, 27 | pub server_bans: Arc>>, 28 | pub server_members: Arc>>, 29 | pub servers: Arc>>, 30 | pub safety_reports: Arc>>, 31 | pub safety_snapshots: Arc>>, 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /crates/core/database/src/events/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod rabbit; 3 | pub mod server; 4 | -------------------------------------------------------------------------------- /crates/core/database/src/events/rabbit.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use upryzing_models::v0::PushNotification; 5 | 6 | use crate::User; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct MessageSentPayload { 10 | pub notification: PushNotification, 11 | pub users: Vec, 12 | } 13 | 14 | #[derive(Serialize, Deserialize, Clone)] 15 | pub struct FRAcceptedPayload { 16 | pub accepted_user: User, 17 | pub user: String, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Clone)] 21 | pub struct FRReceivedPayload { 22 | pub from_user: User, 23 | pub user: String, 24 | } 25 | 26 | #[derive(Serialize, Deserialize, Clone)] 27 | pub struct GenericPayload { 28 | pub title: String, 29 | pub body: String, 30 | pub icon: Option, 31 | pub user: User, 32 | } 33 | 34 | #[derive(Serialize, Deserialize)] 35 | #[serde(tag = "type", content = "data")] 36 | #[allow(clippy::large_enum_variant)] 37 | pub enum PayloadKind { 38 | MessageNotification(PushNotification), 39 | FRAccepted(FRAcceptedPayload), 40 | FRReceived(FRReceivedPayload), 41 | BadgeUpdate(usize), 42 | Generic(GenericPayload), 43 | } 44 | 45 | #[derive(Serialize, Deserialize)] 46 | pub struct PayloadToService { 47 | pub notification: PayloadKind, 48 | pub user_id: String, 49 | pub session_id: String, 50 | pub token: String, 51 | pub extras: HashMap, 52 | } 53 | 54 | #[derive(Serialize, Deserialize)] 55 | pub struct AckPayload { 56 | pub user_id: String, 57 | pub channel_id: String, 58 | pub message_id: String, 59 | } 60 | -------------------------------------------------------------------------------- /crates/core/database/src/events/server.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | use super::client::Ping; 4 | 5 | #[derive(Deserialize, Debug)] 6 | #[serde(tag = "type")] 7 | pub enum ClientMessage { 8 | Authenticate { token: String }, 9 | BeginTyping { channel: String }, 10 | EndTyping { channel: String }, 11 | Subscribe { server_id: String }, 12 | Ping { data: Ping, responded: Option<()> }, 13 | } 14 | -------------------------------------------------------------------------------- /crates/core/database/src/models/admin_migrations/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/admin_migrations/model.rs: -------------------------------------------------------------------------------- 1 | auto_derived!( 2 | /// Document representing migration information 3 | pub struct MigrationInfo { 4 | /// Unique Id 5 | #[serde(rename = "_id")] 6 | pub id: i32, 7 | /// Current database revision 8 | pub revision: i32, 9 | } 10 | ); 11 | 12 | #[cfg(test)] 13 | mod tests { 14 | #[async_std::test] 15 | async fn migrate() { 16 | database_test!(|db| async move { 17 | // Initialise the database 18 | db.migrate_database().await.unwrap(); 19 | 20 | // Migrate the existing database 21 | db.migrate_database().await.unwrap(); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/core/database/src/models/admin_migrations/ops.rs: -------------------------------------------------------------------------------- 1 | mod mongodb; 2 | mod reference; 3 | 4 | #[async_trait] 5 | pub trait AbstractMigrations: Sync + Send { 6 | #[cfg(test)] 7 | /// Drop the database 8 | async fn drop_database(&self); 9 | 10 | /// Migrate the database 11 | async fn migrate_database(&self) -> Result<(), ()>; 12 | } 13 | -------------------------------------------------------------------------------- /crates/core/database/src/models/admin_migrations/ops/mongodb.rs: -------------------------------------------------------------------------------- 1 | use crate::MongoDb; 2 | 3 | use super::AbstractMigrations; 4 | 5 | mod init; 6 | mod scripts; 7 | 8 | #[async_trait] 9 | impl AbstractMigrations for MongoDb { 10 | #[cfg(test)] 11 | /// Drop the database 12 | async fn drop_database(&self) { 13 | self.db().drop(None).await.ok(); 14 | } 15 | 16 | /// Migrate the database 17 | async fn migrate_database(&self) -> Result<(), ()> { 18 | info!("Migrating the database."); 19 | 20 | let list = self 21 | .list_database_names(None, None) 22 | .await 23 | .expect("Failed to fetch database names."); 24 | 25 | if list.iter().any(|x| x == &self.1) { 26 | scripts::migrate_database(self).await; 27 | } else { 28 | init::create_database(self).await; 29 | } 30 | 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /crates/core/database/src/models/admin_migrations/ops/reference.rs: -------------------------------------------------------------------------------- 1 | use crate::ReferenceDb; 2 | 3 | use super::AbstractMigrations; 4 | 5 | #[async_trait] 6 | impl AbstractMigrations for ReferenceDb { 7 | #[cfg(test)] 8 | /// Drop the database 9 | async fn drop_database(&self) {} 10 | 11 | /// Migrate the database 12 | async fn migrate_database(&self) -> Result<(), ()> { 13 | // Here you would do your typical migrations if this was a real database. 14 | Ok(()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/core/database/src/models/bots/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/bots/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::{Bot, FieldsBot, PartialBot}; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractBots: Sync + Send { 10 | /// Insert new bot into the database 11 | async fn insert_bot(&self, bot: &Bot) -> Result<()>; 12 | 13 | /// Fetch a bot by its id 14 | async fn fetch_bot(&self, id: &str) -> Result; 15 | 16 | /// Fetch a bot by its token 17 | async fn fetch_bot_by_token(&self, token: &str) -> Result; 18 | 19 | /// Fetch bots owned by a user 20 | async fn fetch_bots_by_user(&self, user_id: &str) -> Result>; 21 | 22 | /// Get the number of bots owned by a user 23 | async fn get_number_of_bots_by_user(&self, user_id: &str) -> Result; 24 | 25 | /// Update bot with new information 26 | async fn update_bot( 27 | &self, 28 | id: &str, 29 | partial: &PartialBot, 30 | remove: Vec, 31 | ) -> Result<()>; 32 | 33 | /// Delete a bot from the database 34 | async fn delete_bot(&self, id: &str) -> Result<()>; 35 | } 36 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_invites/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_invites/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::Invite; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractChannelInvites: Sync + Send { 10 | /// Insert a new invite into the database 11 | async fn insert_invite(&self, invite: &Invite) -> Result<()>; 12 | 13 | /// Fetch an invite by its id 14 | async fn fetch_invite(&self, code: &str) -> Result; 15 | 16 | /// Fetch all invites for a server 17 | async fn fetch_invites_for_server(&self, server_id: &str) -> Result>; 18 | 19 | /// Delete an invite by its id 20 | async fn delete_invite(&self, code: &str) -> Result<()>; 21 | } 22 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_invites/ops/mongodb.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt; 2 | use upryzing_result::Result; 3 | 4 | use crate::Invite; 5 | use crate::MongoDb; 6 | 7 | use super::AbstractChannelInvites; 8 | 9 | static COL: &str = "channel_invites"; 10 | 11 | #[async_trait] 12 | impl AbstractChannelInvites for MongoDb { 13 | /// Insert a new invite into the database 14 | async fn insert_invite(&self, invite: &Invite) -> Result<()> { 15 | query!(self, insert_one, COL, &invite).map(|_| ()) 16 | } 17 | 18 | /// Fetch an invite by the code 19 | async fn fetch_invite(&self, code: &str) -> Result { 20 | query!(self, find_one_by_id, COL, code)?.ok_or_else(|| create_error!(NotFound)) 21 | } 22 | 23 | /// Fetch all invites for a server 24 | async fn fetch_invites_for_server(&self, server_id: &str) -> Result> { 25 | Ok(self 26 | .col::(COL) 27 | .find( 28 | doc! { 29 | "server": server_id, 30 | }, 31 | None, 32 | ) 33 | .await 34 | .map_err(|_| create_database_error!("find", COL))? 35 | .filter_map(|s| async { 36 | if cfg!(debug_assertions) { 37 | Some(s.unwrap()) 38 | } else { 39 | s.ok() 40 | } 41 | }) 42 | .collect() 43 | .await) 44 | } 45 | 46 | /// Delete an invite by its code 47 | async fn delete_invite(&self, code: &str) -> Result<()> { 48 | query!(self, delete_one_by_id, COL, code).map(|_| ()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_invites/ops/reference.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::Invite; 4 | use crate::ReferenceDb; 5 | 6 | use super::AbstractChannelInvites; 7 | 8 | #[async_trait] 9 | impl AbstractChannelInvites for ReferenceDb { 10 | /// Insert a new invite into the database 11 | async fn insert_invite(&self, invite: &Invite) -> Result<()> { 12 | let mut invites = self.channel_invites.lock().await; 13 | if invites.contains_key(invite.code()) { 14 | Err(create_database_error!("insert", "invite")) 15 | } else { 16 | invites.insert(invite.code().to_string(), invite.clone()); 17 | Ok(()) 18 | } 19 | } 20 | 21 | /// Fetch an invite by the code 22 | async fn fetch_invite(&self, code: &str) -> Result { 23 | let invites = self.channel_invites.lock().await; 24 | invites 25 | .get(code) 26 | .cloned() 27 | .ok_or_else(|| create_error!(NotFound)) 28 | } 29 | 30 | /// Fetch all invites for a server 31 | async fn fetch_invites_for_server(&self, server_id: &str) -> Result> { 32 | let invites = self.channel_invites.lock().await; 33 | Ok(invites 34 | .values() 35 | .filter(|invite| match invite { 36 | Invite::Server { server, .. } => server == server_id, 37 | _ => false, 38 | }) 39 | .cloned() 40 | .collect()) 41 | } 42 | 43 | /// Delete an invite by its code 44 | async fn delete_invite(&self, code: &str) -> Result<()> { 45 | let mut invites = self.channel_invites.lock().await; 46 | if invites.remove(code).is_some() { 47 | Ok(()) 48 | } else { 49 | Err(create_error!(NotFound)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_unreads/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_unreads/model.rs: -------------------------------------------------------------------------------- 1 | auto_derived!( 2 | /// Channel Unread 3 | pub struct ChannelUnread { 4 | /// Composite key pointing to a user's view of a channel 5 | #[serde(rename = "_id")] 6 | pub id: ChannelCompositeKey, 7 | 8 | /// Id of the last message read in this channel by a user 9 | #[serde(skip_serializing_if = "Option::is_none")] 10 | pub last_id: Option, 11 | /// Array of message ids that mention the user 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub mentions: Option>, 14 | } 15 | 16 | /// Composite primary key consisting of channel and user id 17 | #[derive(Hash)] 18 | pub struct ChannelCompositeKey { 19 | /// Channel Id 20 | pub channel: String, 21 | /// User Id 22 | pub user: String, 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_unreads/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::ChannelUnread; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractChannelUnreads: Sync + Send { 10 | /// Acknowledge a message, and returns updated channel unread. 11 | async fn acknowledge_message( 12 | &self, 13 | channel_id: &str, 14 | user_id: &str, 15 | message_id: &str, 16 | ) -> Result>; 17 | 18 | /// Acknowledge many channels. 19 | async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>; 20 | 21 | /// Add a mention. 22 | async fn add_mention_to_unread<'a>( 23 | &self, 24 | channel_id: &str, 25 | user_id: &str, 26 | message_ids: &[String], 27 | ) -> Result<()>; 28 | 29 | /// Fetch all unreads with mentions for a user. 30 | async fn fetch_unread_mentions(&self, user_id: &str) -> Result>; 31 | 32 | /// Fetch all channel unreads for a user. 33 | async fn fetch_unreads(&self, user_id: &str) -> Result>; 34 | 35 | /// Fetch unread for a specific user in a channel. 36 | async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result>; 37 | } 38 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_webhooks/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channel_webhooks/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::{FieldsWebhook, PartialWebhook, Webhook}; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractWebhooks: Sync + Send { 10 | /// Insert new webhook into the database 11 | async fn insert_webhook(&self, webhook: &Webhook) -> Result<()>; 12 | 13 | /// Fetch webhook by id 14 | async fn fetch_webhook(&self, webhook_id: &str) -> Result; 15 | 16 | /// Fetch webhooks for channel 17 | async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result>; 18 | 19 | /// Update webhook with new information 20 | async fn update_webhook( 21 | &self, 22 | webhook_id: &str, 23 | partial: &PartialWebhook, 24 | remove: &[FieldsWebhook], 25 | ) -> Result<()>; 26 | 27 | /// Delete webhook by id 28 | async fn delete_webhook(&self, webhook_id: &str) -> Result<()>; 29 | } 30 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channels/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/channels/ops.rs: -------------------------------------------------------------------------------- 1 | use crate::{upryzing_result::Result, Channel, FieldsChannel, PartialChannel}; 2 | use upryzing_permissions::OverrideField; 3 | mod mongodb; 4 | mod reference; 5 | 6 | #[async_trait] 7 | pub trait AbstractChannels: Sync + Send { 8 | /// Insert a new channel in the database 9 | async fn insert_channel(&self, channel: &Channel) -> Result<()>; 10 | 11 | /// Fetch a channel from the database 12 | async fn fetch_channel(&self, channel_id: &str) -> Result; 13 | 14 | /// Fetch all channels from the database 15 | async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result>; 16 | 17 | /// Fetch all direct messages for a user 18 | async fn find_direct_messages(&self, user_id: &str) -> Result>; 19 | 20 | // Fetch saved messages channel 21 | async fn find_saved_messages_channel(&self, user_id: &str) -> Result; 22 | 23 | // Fetch direct message channel (DM or Saved Messages) 24 | async fn find_direct_message_channel(&self, user_a: &str, user_b: &str) -> Result; 25 | 26 | /// Insert a user to a group 27 | async fn add_user_to_group(&self, channel_id: &str, user_id: &str) -> Result<()>; 28 | 29 | /// Insert channel role permissions 30 | async fn set_channel_role_permission( 31 | &self, 32 | channel_id: &str, 33 | role_id: &str, 34 | permissions: OverrideField, 35 | ) -> Result<()>; 36 | 37 | // Update channel 38 | async fn update_channel( 39 | &self, 40 | id: &str, 41 | channel_id: &PartialChannel, 42 | remove: Vec, 43 | ) -> Result<()>; 44 | 45 | // Remove a user from a group 46 | async fn remove_user_from_group(&self, channel_id: &str, user_id: &str) -> Result<()>; 47 | 48 | // Delete a channel 49 | async fn delete_channel(&self, channel_id: &Channel) -> Result<()>; 50 | } 51 | -------------------------------------------------------------------------------- /crates/core/database/src/models/emojis/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/emojis/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::Emoji; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractEmojis: Sync + Send { 10 | /// Insert emoji into database. 11 | async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>; 12 | 13 | /// Fetch an emoji by its id 14 | async fn fetch_emoji(&self, id: &str) -> Result; 15 | 16 | /// Fetch emoji by their parent id 17 | async fn fetch_emoji_by_parent_id(&self, parent_id: &str) -> Result>; 18 | 19 | /// Fetch emoji by their parent ids 20 | async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result>; 21 | 22 | /// Detach an emoji by its id 23 | async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>; 24 | } 25 | -------------------------------------------------------------------------------- /crates/core/database/src/models/file_hashes/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/file_hashes/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::FileHash; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractAttachmentHashes: Sync + Send { 10 | /// Insert a new attachment hash into the database. 11 | async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()>; 12 | 13 | /// Fetch an attachment hash entry by sha256 hash. 14 | async fn fetch_attachment_hash(&self, hash: &str) -> Result; 15 | 16 | /// Update an attachment hash nonce value. 17 | async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()>; 18 | } 19 | -------------------------------------------------------------------------------- /crates/core/database/src/models/file_hashes/ops/mongodb.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::FileHash; 4 | use crate::MongoDb; 5 | 6 | use super::AbstractAttachmentHashes; 7 | 8 | static COL: &str = "attachment_hashes"; 9 | 10 | #[async_trait] 11 | impl AbstractAttachmentHashes for MongoDb { 12 | /// Insert a new attachment hash into the database. 13 | async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()> { 14 | query!(self, insert_one, COL, &hash).map(|_| ()) 15 | } 16 | 17 | /// Fetch an attachment hash entry by sha256 hash. 18 | async fn fetch_attachment_hash(&self, hash: &str) -> Result { 19 | query!( 20 | self, 21 | find_one, 22 | COL, 23 | doc! { 24 | "$or": [ 25 | {"_id": hash}, 26 | {"processed_hash": hash} 27 | ] 28 | } 29 | )? 30 | .ok_or_else(|| create_error!(NotFound)) 31 | } 32 | 33 | /// Update an attachment hash nonce value. 34 | async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()> { 35 | self.col::(COL) 36 | .update_one( 37 | doc! { 38 | "_id": hash 39 | }, 40 | doc! { 41 | "$set": { 42 | "iv": nonce 43 | } 44 | }, 45 | None, 46 | ) 47 | .await 48 | .map(|_| ()) 49 | .map_err(|_| create_database_error!("update_one", COL)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/core/database/src/models/file_hashes/ops/reference.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::FileHash; 4 | use crate::ReferenceDb; 5 | 6 | use super::AbstractAttachmentHashes; 7 | 8 | #[async_trait] 9 | impl AbstractAttachmentHashes for ReferenceDb { 10 | /// Insert a new attachment hash into the database. 11 | async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()> { 12 | let mut hashes = self.file_hashes.lock().await; 13 | if hashes.contains_key(&hash.id) { 14 | Err(create_database_error!("insert", "attachment")) 15 | } else { 16 | hashes.insert(hash.id.to_string(), hash.clone()); 17 | Ok(()) 18 | } 19 | } 20 | 21 | /// Fetch an attachment hash entry by sha256 hash. 22 | async fn fetch_attachment_hash(&self, hash_value: &str) -> Result { 23 | let hashes = self.file_hashes.lock().await; 24 | hashes 25 | .values() 26 | .cloned() 27 | .find(|hash| hash.id == hash_value || hash.processed_hash == hash_value) 28 | .ok_or(create_error!(NotFound)) 29 | } 30 | 31 | /// Update an attachment hash nonce value. 32 | async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()> { 33 | let mut hashes = self.file_hashes.lock().await; 34 | if let Some(file) = hashes.get_mut(hash) { 35 | file.iv = nonce.to_owned(); 36 | Ok(()) 37 | } else { 38 | Err(create_error!(NotFound)) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/core/database/src/models/files/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/files/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::File; 4 | 5 | use super::FileUsedFor; 6 | 7 | mod mongodb; 8 | mod reference; 9 | 10 | #[async_trait] 11 | pub trait AbstractAttachments: Sync + Send { 12 | /// Insert attachment into database. 13 | async fn insert_attachment(&self, attachment: &File) -> Result<()>; 14 | 15 | /// Fetch an attachment by its id. 16 | async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result; 17 | 18 | /// Find an attachment by its details and mark it as used by a given parent. 19 | async fn find_and_use_attachment( 20 | &self, 21 | id: &str, 22 | tag: &str, 23 | used_for: FileUsedFor, 24 | uploader_id: String, 25 | ) -> Result; 26 | 27 | /// Mark an attachment as having been reported. 28 | async fn mark_attachment_as_reported(&self, id: &str) -> Result<()>; 29 | 30 | /// Mark an attachment as having been deleted. 31 | async fn mark_attachment_as_deleted(&self, id: &str) -> Result<()>; 32 | 33 | /// Mark multiple attachments as having been deleted. 34 | async fn mark_attachments_as_deleted(&self, ids: &[String]) -> Result<()>; 35 | } 36 | -------------------------------------------------------------------------------- /crates/core/database/src/models/messages/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/messages/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage}; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractMessages: Sync + Send { 10 | /// Insert a new message into the database 11 | async fn insert_message(&self, message: &Message) -> Result<()>; 12 | 13 | /// Fetch a message by its id 14 | async fn fetch_message(&self, id: &str) -> Result; 15 | 16 | /// Fetch multiple messages by given query 17 | async fn fetch_messages(&self, query: MessageQuery) -> Result>; 18 | 19 | /// Fetch multiple messages by given IDs 20 | async fn fetch_messages_by_id(&self, ids: &[String]) -> Result>; 21 | 22 | /// Update a given message with new information 23 | async fn update_message( 24 | &self, 25 | id: &str, 26 | message: &PartialMessage, 27 | remove: Vec, 28 | ) -> Result<()>; 29 | 30 | /// Append information to a given message 31 | async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>; 32 | 33 | /// Add a new reaction to a message 34 | async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>; 35 | 36 | /// Remove a reaction from a message 37 | async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>; 38 | 39 | /// Remove reaction from a message 40 | async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>; 41 | 42 | /// Delete a message from the database by its id 43 | async fn delete_message(&self, id: &str) -> Result<()>; 44 | 45 | /// Delete messages from a channel by their ids and corresponding channel id 46 | async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()>; 47 | } 48 | -------------------------------------------------------------------------------- /crates/core/database/src/models/ratelimit_events/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/ratelimit_events/model.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use ulid::Ulid; 4 | use upryzing_result::Result; 5 | 6 | use crate::Database; 7 | 8 | auto_derived!( 9 | /// Ratelimit Event 10 | pub struct RatelimitEvent { 11 | /// Id 12 | #[serde(rename = "_id")] 13 | pub id: String, 14 | /// Relevant Object Id 15 | pub target_id: String, 16 | /// Type of event 17 | pub event_type: RatelimitEventType, 18 | } 19 | 20 | /// Event type 21 | pub enum RatelimitEventType { 22 | DiscriminatorChange, 23 | } 24 | ); 25 | 26 | impl fmt::Display for RatelimitEventType { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 28 | fmt::Debug::fmt(self, f) 29 | } 30 | } 31 | 32 | #[allow(clippy::disallowed_methods)] 33 | impl RatelimitEvent { 34 | /// Create ratelimit event 35 | pub async fn create( 36 | db: &Database, 37 | target_id: String, 38 | event_type: RatelimitEventType, 39 | ) -> Result<()> { 40 | db.insert_ratelimit_event(&RatelimitEvent { 41 | id: Ulid::new().to_string(), 42 | target_id, 43 | event_type, 44 | }) 45 | .await 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/core/database/src/models/ratelimit_events/ops.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crate::{upryzing_result::Result, RatelimitEvent, RatelimitEventType}; 4 | mod mongodb; 5 | mod reference; 6 | 7 | #[async_trait] 8 | pub trait AbstractRatelimitEvents: Sync + Send { 9 | /// Insert a new ratelimit event 10 | async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()>; 11 | 12 | /// Count number of events in given duration and check if we've hit the limit 13 | async fn has_ratelimited( 14 | &self, 15 | target_id: &str, 16 | event_type: RatelimitEventType, 17 | period: Duration, 18 | count: usize, 19 | ) -> Result; 20 | } 21 | -------------------------------------------------------------------------------- /crates/core/database/src/models/ratelimit_events/ops/mongodb.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, SystemTime}; 2 | 3 | use super::AbstractRatelimitEvents; 4 | use crate::{MongoDb, RatelimitEvent, RatelimitEventType}; 5 | use ulid::Ulid; 6 | use upryzing_result::Result; 7 | 8 | static COL: &str = "ratelimit_events"; 9 | 10 | #[async_trait] 11 | impl AbstractRatelimitEvents for MongoDb { 12 | /// Insert a new ratelimit event 13 | async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> { 14 | query!(self, insert_one, COL, &event).map(|_| ()) 15 | } 16 | 17 | /// Count number of events in given duration and check if we've hit the limit 18 | async fn has_ratelimited( 19 | &self, 20 | target_id: &str, 21 | event_type: RatelimitEventType, 22 | period: Duration, 23 | count: usize, 24 | ) -> Result { 25 | self.col::(COL) 26 | .count_documents( 27 | doc! { 28 | "_id": { 29 | "$gte": Ulid::from_datetime(SystemTime::now() - period).to_string() 30 | }, 31 | "target_id": target_id, 32 | "event_type": event_type.to_string() 33 | }, 34 | None, 35 | ) 36 | .await 37 | .map(|c| c as usize >= count) 38 | .map_err(|_| create_database_error!("count_documents", COL)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/core/database/src/models/ratelimit_events/ops/reference.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | use std::time::Duration; 3 | use std::time::SystemTime; 4 | 5 | use super::AbstractRatelimitEvents; 6 | use crate::RatelimitEvent; 7 | use crate::RatelimitEventType; 8 | use crate::ReferenceDb; 9 | use ulid::Ulid; 10 | use upryzing_result::Result; 11 | 12 | #[async_trait] 13 | impl AbstractRatelimitEvents for ReferenceDb { 14 | /// Insert a new ratelimit event 15 | async fn insert_ratelimit_event(&self, event: &RatelimitEvent) -> Result<()> { 16 | let mut ratelimit_events = self.ratelimit_events.lock().await; 17 | if ratelimit_events.contains_key(&event.id) { 18 | Err(create_database_error!("insert", "message")) 19 | } else { 20 | ratelimit_events.insert(event.id.to_string(), event.clone()); 21 | Ok(()) 22 | } 23 | } 24 | 25 | /// Count number of events in given duration and check if we've hit the limit 26 | async fn has_ratelimited( 27 | &self, 28 | target_id: &str, 29 | event_type: RatelimitEventType, 30 | period: Duration, 31 | count: usize, 32 | ) -> Result { 33 | let ratelimit_events = self.ratelimit_events.lock().await; 34 | let gte_cmp_id = Ulid::from_datetime(SystemTime::now() - period).to_string(); 35 | 36 | Ok(ratelimit_events 37 | .iter() 38 | .filter(|(id, event)| { 39 | id.cmp(&>e_cmp_id) == Ordering::Greater 40 | && event.target_id == target_id 41 | && event.event_type == event_type 42 | }) 43 | .count() 44 | >= count) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_reports/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_reports/model.rs: -------------------------------------------------------------------------------- 1 | use upryzing_models::v0::{ReportStatus, ReportedContent}; 2 | 3 | auto_derived!( 4 | /// User-generated platform moderation report 5 | pub struct Report { 6 | /// Unique Id 7 | #[serde(rename = "_id")] 8 | pub id: String, 9 | /// Id of the user creating this report 10 | pub author_id: String, 11 | /// Reported content 12 | pub content: ReportedContent, 13 | /// Additional report context 14 | pub additional_context: String, 15 | /// Status of the report 16 | #[serde(flatten)] 17 | pub status: ReportStatus, 18 | /// Additional notes included on the report 19 | #[serde(default)] 20 | pub notes: String, 21 | } 22 | ); 23 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_reports/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::Report; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractReport: Sync + Send { 10 | /// Insert a new report into the database 11 | async fn insert_report(&self, report: &Report) -> Result<()>; 12 | } 13 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_reports/ops/mongodb.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::MongoDb; 4 | use crate::Report; 5 | 6 | use super::AbstractReport; 7 | 8 | static COL: &str = "safety_reports"; 9 | 10 | #[async_trait] 11 | impl AbstractReport for MongoDb { 12 | /// Insert a new report into the database 13 | async fn insert_report(&self, report: &Report) -> Result<()> { 14 | query!(self, insert_one, COL, &report).map(|_| ()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_reports/ops/reference.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::ReferenceDb; 4 | use crate::Report; 5 | 6 | use super::AbstractReport; 7 | 8 | #[async_trait] 9 | impl AbstractReport for ReferenceDb { 10 | /// Insert a new report into the database 11 | async fn insert_report(&self, report: &Report) -> Result<()> { 12 | let mut reports = self.safety_reports.lock().await; 13 | if reports.contains_key(&report.id) { 14 | Err(create_database_error!("insert", "report")) 15 | } else { 16 | reports.insert(report.id.to_string(), report.clone()); 17 | Ok(()) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_snapshots/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_snapshots/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::Snapshot; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractSnapshot: Sync + Send { 10 | /// Insert a new snapshot into the database 11 | async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()>; 12 | } 13 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_snapshots/ops/mongodb.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::MongoDb; 4 | use crate::Snapshot; 5 | 6 | use super::AbstractSnapshot; 7 | 8 | static COL: &str = "safety_snapshots"; 9 | 10 | #[async_trait] 11 | impl AbstractSnapshot for MongoDb { 12 | /// Insert a new snapshot into the database 13 | async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> { 14 | query!(self, insert_one, COL, &snapshot).map(|_| ()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /crates/core/database/src/models/safety_snapshots/ops/reference.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::ReferenceDb; 4 | use crate::Snapshot; 5 | 6 | use super::AbstractSnapshot; 7 | 8 | #[async_trait] 9 | impl AbstractSnapshot for ReferenceDb { 10 | /// Insert a new report into the database 11 | async fn insert_snapshot(&self, snapshot: &Snapshot) -> Result<()> { 12 | let mut snapshots = self.safety_snapshots.lock().await; 13 | if snapshots.contains_key(&snapshot.id) { 14 | Err(create_database_error!("insert", "snapshot")) 15 | } else { 16 | snapshots.insert(snapshot.id.to_string(), snapshot.clone()); 17 | Ok(()) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/core/database/src/models/server_bans/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/server_bans/model.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::{Database, MemberCompositeKey, Server}; 4 | 5 | auto_derived!( 6 | /// Server Ban 7 | pub struct ServerBan { 8 | /// Unique member id 9 | #[serde(rename = "_id")] 10 | pub id: MemberCompositeKey, 11 | /// Reason for ban creation 12 | pub reason: Option, 13 | } 14 | ); 15 | 16 | impl ServerBan { 17 | /// Create ban 18 | pub async fn create( 19 | db: &Database, 20 | server: &Server, 21 | user_id: &str, 22 | reason: Option, 23 | ) -> Result { 24 | let ban = ServerBan { 25 | id: MemberCompositeKey { 26 | server: server.id.to_string(), 27 | user: user_id.to_string(), 28 | }, 29 | reason, 30 | }; 31 | 32 | db.insert_ban(&ban).await?; 33 | Ok(ban) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/core/database/src/models/server_bans/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::{MemberCompositeKey, ServerBan}; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractServerBans: Sync + Send { 10 | /// Insert new ban into database 11 | async fn insert_ban(&self, ban: &ServerBan) -> Result<()>; 12 | 13 | /// Fetch a server ban by server and user id 14 | async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result; 15 | 16 | /// Fetch all bans in a server 17 | async fn fetch_bans(&self, server_id: &str) -> Result>; 18 | 19 | /// Delete a ban from the database 20 | async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()>; 21 | } 22 | -------------------------------------------------------------------------------- /crates/core/database/src/models/server_bans/ops/mongodb.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::MongoDb; 4 | use crate::{MemberCompositeKey, ServerBan}; 5 | 6 | use super::AbstractServerBans; 7 | 8 | static COL: &str = "server_bans"; 9 | 10 | #[async_trait] 11 | impl AbstractServerBans for MongoDb { 12 | /// Insert new ban into database 13 | async fn insert_ban(&self, ban: &ServerBan) -> Result<()> { 14 | query!(self, insert_one, COL, &ban).map(|_| ()) 15 | } 16 | 17 | /// Fetch a server ban by server and user id 18 | async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result { 19 | query!( 20 | self, 21 | find_one, 22 | COL, 23 | doc! { 24 | "_id.server": server_id, 25 | "_id.user": user_id 26 | } 27 | )? 28 | .ok_or_else(|| create_error!(NotFound)) 29 | } 30 | 31 | /// Fetch all bans in a server 32 | async fn fetch_bans(&self, server_id: &str) -> Result> { 33 | query!( 34 | self, 35 | find, 36 | COL, 37 | doc! { 38 | "_id.server": server_id 39 | } 40 | ) 41 | } 42 | 43 | /// Delete a ban from the database 44 | async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> { 45 | query!( 46 | self, 47 | delete_one, 48 | COL, 49 | doc! { 50 | "_id.server": &id.server, 51 | "_id.user": &id.user 52 | } 53 | ) 54 | .map(|_| ()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/core/database/src/models/server_bans/ops/reference.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::ReferenceDb; 4 | use crate::{MemberCompositeKey, ServerBan}; 5 | 6 | use super::AbstractServerBans; 7 | 8 | #[async_trait] 9 | impl AbstractServerBans for ReferenceDb { 10 | /// Insert new ban into database 11 | async fn insert_ban(&self, ban: &ServerBan) -> Result<()> { 12 | let mut server_bans = self.server_bans.lock().await; 13 | if server_bans.contains_key(&ban.id) { 14 | Err(create_database_error!("insert", "ban")) 15 | } else { 16 | server_bans.insert(ban.id.clone(), ban.clone()); 17 | Ok(()) 18 | } 19 | } 20 | 21 | /// Fetch a server ban by server and user id 22 | async fn fetch_ban(&self, server_id: &str, user_id: &str) -> Result { 23 | let server_bans = self.server_bans.lock().await; 24 | server_bans 25 | .get(&MemberCompositeKey { 26 | server: server_id.to_string(), 27 | user: user_id.to_string(), 28 | }) 29 | .cloned() 30 | .ok_or_else(|| create_error!(NotFound)) 31 | } 32 | 33 | /// Fetch all bans in a server 34 | async fn fetch_bans(&self, server_id: &str) -> Result> { 35 | let server_bans = self.server_bans.lock().await; 36 | Ok(server_bans 37 | .values() 38 | .filter(|member| member.id.server == server_id) 39 | .cloned() 40 | .collect()) 41 | } 42 | 43 | /// Delete a ban from the database 44 | async fn delete_ban(&self, id: &MemberCompositeKey) -> Result<()> { 45 | let mut server_bans = self.server_bans.lock().await; 46 | if server_bans.remove(id).is_some() { 47 | Ok(()) 48 | } else { 49 | Err(create_error!(NotFound)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /crates/core/database/src/models/server_members/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/server_members/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractServerMembers: Sync + Send { 10 | /// Insert a new server member into the database 11 | async fn insert_member(&self, member: &Member) -> Result<()>; 12 | 13 | /// Fetch a server member by their id 14 | async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result; 15 | 16 | /// Fetch all members in a server 17 | async fn fetch_all_members<'a>(&self, server_id: &str) -> Result>; 18 | 19 | /// Fetch all memberships for a user 20 | async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result>; 21 | 22 | /// Fetch multiple members by their ids 23 | async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result>; 24 | 25 | /// Fetch member count of a server 26 | async fn fetch_member_count(&self, server_id: &str) -> Result; 27 | 28 | /// Fetch server count of a user 29 | async fn fetch_server_count(&self, user_id: &str) -> Result; 30 | 31 | /// Update information for a server member 32 | async fn update_member( 33 | &self, 34 | id: &MemberCompositeKey, 35 | partial: &PartialMember, 36 | remove: Vec, 37 | ) -> Result<()>; 38 | 39 | /// Delete a server member by their id 40 | async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>; 41 | } 42 | -------------------------------------------------------------------------------- /crates/core/database/src/models/servers/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/servers/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::{FieldsRole, FieldsServer, PartialRole, PartialServer, Role, Server}; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractServers: Sync + Send { 10 | /// Insert a new server into database 11 | async fn insert_server(&self, server: &Server) -> Result<()>; 12 | 13 | /// Fetch a server by its id 14 | async fn fetch_server(&self, id: &str) -> Result; 15 | 16 | /// Fetch a servers by their ids 17 | async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result>; 18 | 19 | /// Update a server with new information 20 | async fn update_server( 21 | &self, 22 | id: &str, 23 | partial: &PartialServer, 24 | remove: Vec, 25 | ) -> Result<()>; 26 | 27 | /// Delete a server by its id 28 | async fn delete_server(&self, id: &str) -> Result<()>; 29 | 30 | /// Insert a new role into server object 31 | async fn insert_role(&self, server_id: &str, role_id: &str, role: &Role) -> Result<()>; 32 | 33 | /// Update an existing role on a server 34 | async fn update_role( 35 | &self, 36 | server_id: &str, 37 | role_id: &str, 38 | partial: &PartialRole, 39 | remove: Vec, 40 | ) -> Result<()>; 41 | 42 | /// Delete a role from a server 43 | /// 44 | /// Also updates channels and members. 45 | async fn delete_role(&self, server_id: &str, role_id: &str) -> Result<()>; 46 | } 47 | -------------------------------------------------------------------------------- /crates/core/database/src/models/user_settings/mod.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | mod ops; 3 | 4 | pub use model::*; 5 | pub use ops::*; 6 | -------------------------------------------------------------------------------- /crates/core/database/src/models/user_settings/model.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{events::client::EventV1, Database}; 4 | 5 | use upryzing_result::Result; 6 | 7 | pub type UserSettings = HashMap; 8 | 9 | #[async_trait] 10 | pub trait UserSettingsImpl { 11 | async fn set(self, db: &Database, user: &str) -> Result<()>; 12 | } 13 | 14 | #[async_trait] 15 | impl UserSettingsImpl for UserSettings { 16 | async fn set(self, db: &Database, user: &str) -> Result<()> { 17 | db.set_user_settings(user, &self).await?; 18 | 19 | EventV1::UserSettingsUpdate { 20 | id: user.to_string(), 21 | update: self, 22 | } 23 | .private(user.to_string()) 24 | .await; 25 | 26 | Ok(()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /crates/core/database/src/models/user_settings/ops.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::UserSettings; 4 | 5 | mod mongodb; 6 | mod reference; 7 | 8 | #[async_trait] 9 | pub trait AbstractUserSettings: Sync + Send { 10 | /// Fetch a subset of user settings 11 | async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result; 12 | 13 | /// Update a subset of user settings 14 | async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()>; 15 | 16 | /// Delete all user settings 17 | async fn delete_user_settings(&self, id: &str) -> Result<()>; 18 | } 19 | -------------------------------------------------------------------------------- /crates/core/database/src/models/user_settings/ops/reference.rs: -------------------------------------------------------------------------------- 1 | use upryzing_result::Result; 2 | 3 | use crate::ReferenceDb; 4 | use crate::UserSettings; 5 | 6 | use super::AbstractUserSettings; 7 | 8 | #[async_trait] 9 | impl AbstractUserSettings for ReferenceDb { 10 | /// Fetch a subset of user settings 11 | async fn fetch_user_settings(&'_ self, id: &str, filter: &'_ [String]) -> Result { 12 | let user_settings = self.user_settings.lock().await; 13 | user_settings 14 | .get(id) 15 | .cloned() 16 | .map(|settings| { 17 | settings 18 | .into_iter() 19 | .filter(|(key, _)| filter.contains(key)) 20 | .collect() 21 | }) 22 | .ok_or_else(|| create_error!(NotFound)) 23 | } 24 | 25 | /// Update a subset of user settings 26 | async fn set_user_settings(&self, id: &str, settings: &UserSettings) -> Result<()> { 27 | let mut user_settings = self.user_settings.lock().await; 28 | if let Some(settings) = user_settings.get_mut(id) { 29 | settings.extend(settings.clone()); 30 | } else { 31 | user_settings.insert(id.to_string(), settings.clone()); 32 | } 33 | 34 | Ok(()) 35 | } 36 | 37 | /// Delete all user settings 38 | async fn delete_user_settings(&self, id: &str) -> Result<()> { 39 | let mut user_settings = self.user_settings.lock().await; 40 | if user_settings.remove(id).is_some() { 41 | Ok(()) 42 | } else { 43 | Err(create_error!(NotFound)) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /crates/core/database/src/models/users/axum.rs: -------------------------------------------------------------------------------- 1 | use axum::{extract::FromRequestParts, http::request::Parts}; 2 | 3 | use upryzing_result::{create_error, Error, Result}; 4 | 5 | use crate::{Database, User}; 6 | 7 | #[async_trait::async_trait] 8 | impl FromRequestParts for User { 9 | type Rejection = Error; 10 | 11 | async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result { 12 | if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) { 13 | let bot = db.fetch_bot_by_token(bot_token).await?; 14 | db.fetch_user(&bot.id).await 15 | } else if let Some(Ok(session_token)) = 16 | parts.headers.get("x-session-token").map(|v| v.to_str()) 17 | { 18 | let session = db.fetch_session_by_token(session_token).await?; 19 | db.fetch_user(&session.user_id).await 20 | } else { 21 | Err(create_error!(NotAuthenticated)) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/core/database/src/models/users/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "axum-impl")] 2 | mod axum; 3 | mod model; 4 | mod ops; 5 | #[cfg(feature = "rocket-impl")] 6 | mod rocket; 7 | #[cfg(feature = "rocket-impl")] 8 | mod schema; 9 | 10 | pub use model::*; 11 | pub use ops::*; 12 | -------------------------------------------------------------------------------- /crates/core/database/src/models/users/rocket.rs: -------------------------------------------------------------------------------- 1 | use authifier::models::Session; 2 | use rocket::http::Status; 3 | use rocket::request::{self, FromRequest, Outcome, Request}; 4 | 5 | use crate::{Database, User}; 6 | 7 | #[rocket::async_trait] 8 | impl<'r> FromRequest<'r> for User { 9 | type Error = authifier::Error; 10 | 11 | async fn from_request(request: &'r Request<'_>) -> request::Outcome { 12 | let user: &Option = request 13 | .local_cache_async(async { 14 | let db = request.rocket().state::().expect("`Database`"); 15 | 16 | let header_bot_token = request 17 | .headers() 18 | .get("x-bot-token") 19 | .next() 20 | .map(|x| x.to_string()); 21 | 22 | if let Some(bot_token) = header_bot_token { 23 | if let Ok(bot) = db.fetch_bot_by_token(&bot_token).await { 24 | if let Ok(user) = db.fetch_user(&bot.id).await { 25 | return Some(user); 26 | } 27 | } 28 | } else if let Outcome::Success(session) = request.guard::().await { 29 | if let Ok(user) = db.fetch_user(&session.user_id).await { 30 | return Some(user); 31 | } 32 | } 33 | 34 | None 35 | }) 36 | .await; 37 | 38 | if let Some(user) = user { 39 | Outcome::Success(user.clone()) 40 | } else { 41 | Outcome::Error((Status::Unauthorized, authifier::Error::InvalidSession)) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /crates/core/database/src/models/users/schema.rs: -------------------------------------------------------------------------------- 1 | use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData}; 2 | use revolt_rocket_okapi::{ 3 | gen::OpenApiGenerator, 4 | request::{OpenApiFromRequest, RequestHeaderInput}, 5 | }; 6 | 7 | use crate::User; 8 | 9 | impl<'r> OpenApiFromRequest<'r> for User { 10 | fn from_request_input( 11 | _gen: &mut OpenApiGenerator, 12 | _name: String, 13 | _required: bool, 14 | ) -> revolt_rocket_okapi::Result { 15 | let mut requirements = schemars::Map::new(); 16 | requirements.insert("Session Token".to_owned(), vec![]); 17 | 18 | Ok(RequestHeaderInput::Security( 19 | "Session Token".to_owned(), 20 | SecurityScheme { 21 | data: SecuritySchemeData::ApiKey { 22 | name: "x-session-token".to_owned(), 23 | location: "header".to_owned(), 24 | }, 25 | description: Some("Used to authenticate as a user.".to_owned()), 26 | extensions: schemars::Map::new(), 27 | }, 28 | requirements, 29 | )) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/core/database/src/tasks/authifier_relay.rs: -------------------------------------------------------------------------------- 1 | use async_std::channel::{unbounded, Receiver, Sender}; 2 | use authifier::AuthifierEvent; 3 | use once_cell::sync::Lazy; 4 | 5 | use crate::events::client::EventV1; 6 | 7 | static Q: Lazy<(Sender, Receiver)> = Lazy::new(|| unbounded()); 8 | 9 | /// Get sender 10 | pub fn sender() -> Sender { 11 | Q.0.clone() 12 | } 13 | 14 | /// Start a new worker 15 | pub async fn worker() { 16 | loop { 17 | let event = Q.1.recv().await.unwrap(); 18 | match &event { 19 | AuthifierEvent::CreateSession { .. } | AuthifierEvent::CreateAccount { .. } => { 20 | EventV1::Auth(event).global().await 21 | } 22 | AuthifierEvent::DeleteSession { user_id, .. } 23 | | AuthifierEvent::DeleteAllSessions { user_id, .. } => { 24 | let id = user_id.to_string(); 25 | EventV1::Auth(event).private(id).await 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/core/database/src/tasks/mod.rs: -------------------------------------------------------------------------------- 1 | //! Semi-important background task management 2 | 3 | use crate::{Database, AMQP}; 4 | 5 | use async_std::task; 6 | use std::time::Instant; 7 | 8 | const WORKER_COUNT: usize = 5; 9 | 10 | pub mod ack; 11 | pub mod authifier_relay; 12 | pub mod last_message_id; 13 | pub mod process_embeds; 14 | 15 | /// Spawn background workers 16 | pub fn start_workers(db: Database, amqp: AMQP) { 17 | task::spawn(authifier_relay::worker()); 18 | 19 | for _ in 0..WORKER_COUNT { 20 | task::spawn(ack::worker(db.clone(), amqp.clone())); 21 | task::spawn(last_message_id::worker(db.clone())); 22 | task::spawn(process_embeds::worker(db.clone())); 23 | } 24 | } 25 | 26 | /// Task with additional information on when it should run 27 | pub struct DelayedTask { 28 | pub data: T, 29 | last_updated: Instant, 30 | first_seen: Instant, 31 | } 32 | 33 | /// Commit to database every 30 seconds if the task is particularly active. 34 | static EXPIRE_CONSTANT: u64 = 30; 35 | 36 | /// Otherwise, commit to database after 5 seconds. 37 | static SAVE_CONSTANT: u64 = 5; 38 | 39 | impl DelayedTask { 40 | /// Create a new delayed task 41 | pub fn new(data: T) -> Self { 42 | DelayedTask { 43 | data, 44 | last_updated: Instant::now(), 45 | first_seen: Instant::now(), 46 | } 47 | } 48 | 49 | /// Push a task further back in time 50 | pub fn delay(&mut self) { 51 | self.last_updated = Instant::now() 52 | } 53 | 54 | /// Check if a task should run yet 55 | pub fn should_run(&self) -> bool { 56 | self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT 57 | || self.last_updated.elapsed().as_secs() > SAVE_CONSTANT 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/core/database/src/util/bridge/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod v0; 2 | -------------------------------------------------------------------------------- /crates/core/database/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bridge; 2 | pub mod bulk_permissions; 3 | pub mod idempotency; 4 | pub mod permissions; 5 | pub mod reference; 6 | pub mod test_fixtures; 7 | -------------------------------------------------------------------------------- /crates/core/database/templates/deletion.original.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 |
10 | Revolt Logo 15 |
16 |

Account Deletion

17 |

18 | You requested to have your account deleted, if you did not perform 19 | this action please take measures to secure your account immediately. 20 |

21 | Confirm 22 |
23 |
24 | This email is intended for {{email}}
25 | Sent from Revolt
26 | Made in Europe 27 |
28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /crates/core/database/templates/deletion.txt: -------------------------------------------------------------------------------- 1 | You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately. 2 | 3 | Please navigate to: {{url}} 4 | 5 | This email is intended for {{email}} 6 | Sent by Revolt 7 | Made in Europe 8 | -------------------------------------------------------------------------------- /crates/core/database/templates/reset.original.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 |
10 | 14 |
15 |

Password Reset

16 |

You requested a password reset, click below to continue.

17 | Reset 18 |
19 |
20 | This email is intended for {{email}}
21 | Sent from Revolt
22 | Made in Europe 23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /crates/core/database/templates/reset.txt: -------------------------------------------------------------------------------- 1 | You requested a password reset, if you did not perform this action you can safely ignore this email. 2 | 3 | Please navigate to: {{url}} 4 | 5 | This email is intended for {{email}} 6 | Sent by Revolt 7 | Made in Europe 8 | -------------------------------------------------------------------------------- /crates/core/database/templates/suspension.original.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 |
10 | Revolt Logo 15 |
16 |

Account Suspended

17 |

Your account has been suspended, for one or more reasons:

18 |
    19 | {{list}} 20 |
21 |

22 | You will be able to use your account again in {{duration}} days. 23 |

24 |

25 | Further violations may result in a permanent ban depending on 26 | severity, please abide by the 27 | Acceptable Usage Policy. 28 |

29 |

Ban evasion is prohibited and will be dealt with accordingly.

30 |
31 |
32 | This email is intended for {{email}}
33 | Sent from Revolt
34 | Made in Europe 35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /crates/core/database/templates/suspension.txt: -------------------------------------------------------------------------------- 1 | Your account has been suspended, for one or more reasons: 2 | {{list}} 3 | 4 | You will be able to use your account again in {{duration}} days. 5 | 6 | Further violations may result in a permanent ban depending on severity, please abide by the Acceptable Usage Policy (https://revolt.chat/aup). 7 | 8 | Ban evasion is prohibited and will be dealt with accordingly. 9 | 10 | This email is intended for {{email}} 11 | Sent by Revolt 12 | Made in Europe 13 | -------------------------------------------------------------------------------- /crates/core/database/templates/verify.original.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 |
10 | Revolt Logo 15 |
16 |

Almost there!

17 |

To complete your sign up, we just need to verify your email.

18 | Confirm 19 |
20 |
21 | This email is intended for {{email}}
22 | Sent from Revolt
23 | Made in Europe 24 |
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /crates/core/database/templates/verify.txt: -------------------------------------------------------------------------------- 1 | Almost there! 2 | To complete your sign up, we just need to verify your email. 3 | 4 | Please navigate to: {{url}} 5 | 6 | This email is intended for {{email}} 7 | Sent by Revolt 8 | Made in Europe 9 | -------------------------------------------------------------------------------- /crates/core/files/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-files" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "AGPL-3.0-or-later" 6 | authors = ["Paul Makles "] 7 | description = "Revolt Backend: S3 and encryption subroutines" 8 | 9 | [dependencies] 10 | tracing = "0.1" 11 | 12 | ffprobe = "0.4.0" 13 | imagesize = "0.13.0" 14 | tempfile = "3.12.0" 15 | 16 | base64 = "0.22.1" 17 | aes-gcm = "0.10.3" 18 | typenum = "1.17.0" 19 | 20 | aws-config = "1.5.5" 21 | aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] } 22 | 23 | upryzing-config = { version = "0.1.0", path = "../config", features = [ 24 | "report-macros", 25 | ] } 26 | upryzing-result = { version = "0.1.0", path = "../result" } 27 | 28 | # image processing 29 | jxl-oxide = "0.8.1" 30 | image = { version = "0.25.2" } 31 | 32 | # svg rendering 33 | usvg = "0.44.0" 34 | resvg = "0.44.0" 35 | tiny-skia = "0.11.4" 36 | 37 | # encoding 38 | webp = "0.3.0" 39 | -------------------------------------------------------------------------------- /crates/core/models/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-models" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["Paul Makles "] 7 | description = "Revolt Backend: API Models" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [features] 12 | serde = ["dep:serde", "upryzing-permissions/serde", "indexmap/serde"] 13 | schemas = ["dep:schemars", "upryzing-permissions/schemas"] 14 | utoipa = ["dep:utoipa"] 15 | validator = ["dep:validator"] 16 | rocket = ["dep:rocket"] 17 | partials = ["dep:revolt_optional_struct", "serde", "schemas", "utoipa"] 18 | 19 | default = ["serde", "partials", "rocket"] 20 | 21 | [dependencies] 22 | # Core 23 | upryzing-config = { version = "0.1.0", path = "../config" } 24 | upryzing-permissions = { version = "0.1.0", path = "../permissions" } 25 | 26 | # Utility 27 | regex = "1" 28 | indexmap = "1.9.3" 29 | once_cell = "1.17.1" 30 | 31 | # Rocket 32 | rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } 33 | 34 | # Serialisation 35 | revolt_optional_struct = { version = "0.2.0", optional = true } 36 | serde = { version = "1", features = ["derive"], optional = true } 37 | iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] } 38 | 39 | # Spec Generation 40 | schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] } 41 | utoipa = { version = "4.2.3", optional = true } 42 | 43 | # Validation 44 | validator = { version = "0.16.0", optional = true, features = ["derive"] } 45 | -------------------------------------------------------------------------------- /crates/core/models/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pawel Makles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /crates/core/models/src/v0/channel_unreads.rs: -------------------------------------------------------------------------------- 1 | auto_derived!( 2 | /// Channel Unread 3 | pub struct ChannelUnread { 4 | /// Composite key pointing to a user's view of a channel 5 | #[serde(rename = "_id")] 6 | pub id: ChannelCompositeKey, 7 | 8 | /// Id of the last message read in this channel by a user 9 | #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] 10 | pub last_id: Option, 11 | /// Array of message ids that mention the user 12 | #[cfg_attr( 13 | feature = "serde", 14 | serde(skip_serializing_if = "Vec::is_empty", default) 15 | )] 16 | pub mentions: Vec, 17 | } 18 | 19 | /// Composite primary key consisting of channel and user id 20 | #[derive(Hash)] 21 | pub struct ChannelCompositeKey { 22 | /// Channel Id 23 | pub channel: String, 24 | /// User Id 25 | pub user: String, 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /crates/core/models/src/v0/emojis.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use regex::Regex; 3 | 4 | #[cfg(feature = "validator")] 5 | use validator::Validate; 6 | 7 | /// Regex for valid emoji names 8 | /// 9 | /// Alphanumeric and underscores 10 | pub static RE_EMOJI: Lazy = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap()); 11 | 12 | auto_derived!( 13 | /// Emoji 14 | pub struct Emoji { 15 | /// Unique Id 16 | #[cfg_attr(feature = "serde", serde(rename = "_id"))] 17 | pub id: String, 18 | /// What owns this emoji 19 | pub parent: EmojiParent, 20 | /// Uploader user id 21 | pub creator_id: String, 22 | /// Emoji name 23 | pub name: String, 24 | /// Whether the emoji is animated 25 | #[cfg_attr( 26 | feature = "serde", 27 | serde(skip_serializing_if = "crate::if_false", default) 28 | )] 29 | pub animated: bool, 30 | /// Whether the emoji is marked as nsfw 31 | #[cfg_attr( 32 | feature = "serde", 33 | serde(skip_serializing_if = "crate::if_false", default) 34 | )] 35 | pub nsfw: bool, 36 | } 37 | 38 | /// Parent Id of the emoji 39 | #[serde(tag = "type")] 40 | pub enum EmojiParent { 41 | Server { id: String }, 42 | Detached, 43 | } 44 | 45 | /// Create a new emoji 46 | #[cfg_attr(feature = "validator", derive(Validate))] 47 | pub struct DataCreateEmoji { 48 | /// Server name 49 | #[validate(length(min = 1, max = 32), regex = "RE_EMOJI")] 50 | pub name: String, 51 | /// Parent information 52 | pub parent: EmojiParent, 53 | /// Whether the emoji is mature 54 | #[serde(default)] 55 | pub nsfw: bool, 56 | } 57 | ); 58 | -------------------------------------------------------------------------------- /crates/core/models/src/v0/mod.rs: -------------------------------------------------------------------------------- 1 | mod bots; 2 | mod channel_invites; 3 | mod channel_unreads; 4 | mod channel_webhooks; 5 | mod channels; 6 | mod embeds; 7 | mod emojis; 8 | mod files; 9 | mod messages; 10 | mod safety_reports; 11 | mod server_bans; 12 | mod server_members; 13 | mod servers; 14 | mod user_settings; 15 | mod users; 16 | 17 | pub use bots::*; 18 | pub use channel_invites::*; 19 | pub use channel_unreads::*; 20 | pub use channel_webhooks::*; 21 | pub use channels::*; 22 | pub use embeds::*; 23 | pub use emojis::*; 24 | pub use files::*; 25 | pub use messages::*; 26 | pub use safety_reports::*; 27 | pub use server_bans::*; 28 | pub use server_members::*; 29 | pub use servers::*; 30 | pub use user_settings::*; 31 | pub use users::*; 32 | -------------------------------------------------------------------------------- /crates/core/models/src/v0/server_bans.rs: -------------------------------------------------------------------------------- 1 | use super::{File, MemberCompositeKey, User}; 2 | 3 | #[cfg(feature = "validator")] 4 | use validator::Validate; 5 | 6 | auto_derived!( 7 | /// Server Ban 8 | pub struct ServerBan { 9 | /// Unique member id 10 | #[cfg_attr(feature = "serde", serde(rename = "_id"))] 11 | pub id: MemberCompositeKey, 12 | /// Reason for ban creation 13 | pub reason: Option, 14 | } 15 | 16 | /// Information for new server ban 17 | #[cfg_attr(feature = "validator", derive(Validate))] 18 | pub struct DataBanCreate { 19 | /// Ban reason 20 | #[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))] 21 | pub reason: Option, 22 | } 23 | 24 | /// Just enough information to list a ban 25 | pub struct BannedUser { 26 | /// Id of the banned user 27 | #[cfg_attr(feature = "serde", serde(rename = "_id"))] 28 | pub id: String, 29 | /// Username of the banned user 30 | pub username: String, 31 | /// Discriminator of the banned user 32 | pub discriminator: String, 33 | /// Avatar of the banned user 34 | pub avatar: Option, 35 | } 36 | 37 | /// Ban list result 38 | pub struct BanListResult { 39 | /// Users objects 40 | pub users: Vec, 41 | /// Ban objects 42 | pub bans: Vec, 43 | } 44 | ); 45 | 46 | impl From for BannedUser { 47 | fn from(user: User) -> Self { 48 | BannedUser { 49 | id: user.id, 50 | username: user.username, 51 | discriminator: user.discriminator, 52 | avatar: user.avatar, 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /crates/core/models/src/v0/user_settings.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "rocket")] 2 | use rocket::FromForm; 3 | 4 | use std::collections::HashMap; 5 | 6 | /// HashMap of user settings 7 | /// Each key is mapped to a tuple consisting of the 8 | /// revision timestamp and serialised data (in JSON format) 9 | pub type UserSettings = HashMap; 10 | 11 | auto_derived!( 12 | /// Options for fetching settings 13 | pub struct OptionsFetchSettings { 14 | /// Keys to fetch 15 | pub keys: Vec, 16 | } 17 | 18 | /// Additional options for inserting settings 19 | #[cfg_attr(feature = "rocket", derive(FromForm))] 20 | pub struct OptionsSetSettings { 21 | /// Timestamp of settings change. 22 | /// 23 | /// Used to avoid feedback loops. 24 | pub timestamp: Option, 25 | } 26 | ); 27 | -------------------------------------------------------------------------------- /crates/core/permissions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-permissions" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["Paul Makles "] 7 | description = "Revolt Backend: Permission Logic" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [features] 12 | bson = ["dep:bson"] 13 | serde = ["dep:serde"] 14 | schemas = ["dep:schemars"] 15 | try-from-primitive = ["dep:num_enum"] 16 | 17 | 18 | [dev-dependencies] 19 | # Async 20 | async-std = { version = "1.8.0", features = ["attributes"] } 21 | 22 | [dependencies] 23 | # Core 24 | upryzing-result = { version = "0.1.0", path = "../result" } 25 | 26 | # Utility 27 | auto_ops = "0.3.0" 28 | once_cell = "1.17" 29 | num_enum = { version = "0.6.1", optional = true } 30 | 31 | # Async 32 | async-trait = "0.1.51" 33 | 34 | # Serialisation 35 | serde = { version = "1", features = ["derive"], optional = true } 36 | bson = { version = "2.1.0", optional = true } 37 | 38 | # Spec Generation 39 | schemars = { version = "0.8.8", optional = true } 40 | -------------------------------------------------------------------------------- /crates/core/permissions/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pawel Makles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /crates/core/permissions/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate auto_ops; 3 | 4 | #[macro_use] 5 | extern crate async_trait; 6 | 7 | mod r#impl; 8 | mod models; 9 | mod r#trait; 10 | 11 | pub use models::*; 12 | pub use r#impl::*; 13 | pub use r#trait::*; 14 | 15 | #[cfg(test)] 16 | mod test; 17 | -------------------------------------------------------------------------------- /crates/core/permissions/src/models/user.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// User's relationship with another user (or themselves) 4 | pub enum RelationshipStatus { 5 | None, 6 | User, 7 | Friend, 8 | Outgoing, 9 | Incoming, 10 | Blocked, 11 | BlockedOther, 12 | } 13 | 14 | /// User permission definitions 15 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 16 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 17 | #[cfg_attr(feature = "try-from-primitive", derive(num_enum::TryFromPrimitive))] 18 | #[repr(u32)] 19 | pub enum UserPermission { 20 | Access = 1 << 0, 21 | ViewProfile = 1 << 1, 22 | SendMessage = 1 << 2, 23 | Invite = 1 << 3, 24 | } 25 | 26 | impl fmt::Display for UserPermission { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 28 | fmt::Debug::fmt(self, f) 29 | } 30 | } 31 | 32 | impl_op_ex!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 }); 33 | impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 }); 34 | -------------------------------------------------------------------------------- /crates/core/presence/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-presence" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "AGPL-3.0-or-later" 6 | authors = ["Paul Makles "] 7 | description = "Revolt Backend: User Presence" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [features] 12 | redis-is-patched = [] 13 | 14 | [dev-dependencies] 15 | # Async 16 | async-std = { version = "1.8.0", features = ["attributes"] } 17 | 18 | [dependencies] 19 | # Utility 20 | log = "0.4.17" 21 | rand = "0.8.5" 22 | once_cell = "1.17.1" 23 | 24 | # Redis 25 | redis-kiss = "0.1.4" 26 | -------------------------------------------------------------------------------- /crates/core/presence/src/operations.rs: -------------------------------------------------------------------------------- 1 | use redis_kiss::{AsyncCommands, Conn}; 2 | 3 | /// Add to set (string) 4 | pub async fn __add_to_set_string(conn: &mut Conn, key: &str, value: &str) { 5 | let _: Option<()> = conn.sadd(key, value).await.ok(); 6 | } 7 | 8 | /// Add to set (u32) 9 | pub async fn __add_to_set_u32(conn: &mut Conn, key: &str, value: u32) { 10 | let _: Option<()> = conn.sadd(key, value).await.ok(); 11 | } 12 | 13 | /// Remove from set (string) 14 | pub async fn __remove_from_set_string(conn: &mut Conn, key: &str, value: &str) { 15 | let _: Option<()> = conn.srem(key, value).await.ok(); 16 | } 17 | 18 | /// Remove from set (u32) 19 | pub async fn __remove_from_set_u32(conn: &mut Conn, key: &str, value: u32) { 20 | let _: Option<()> = conn.srem(key, value).await.ok(); 21 | } 22 | 23 | /// Get set members as string 24 | pub async fn __get_set_members_as_string(conn: &mut Conn, key: &str) -> Vec { 25 | conn.smembers::<_, Vec>(key) 26 | .await 27 | .expect("could not get set members as string") 28 | } 29 | 30 | /// Get set size 31 | pub async fn __get_set_size(conn: &mut Conn, id: &str) -> u32 { 32 | conn.scard::<_, u32>(id) 33 | .await 34 | .expect("could not get set size") 35 | } 36 | 37 | /// Delete key by id 38 | pub async fn __delete_key(conn: &mut Conn, id: &str) { 39 | conn.del::<_, ()>(id) 40 | .await 41 | .expect("could not delete key by id"); 42 | } 43 | -------------------------------------------------------------------------------- /crates/core/result/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-result" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "MIT" 6 | authors = ["Paul Makles "] 7 | description = "Revolt Backend: Result and Error types" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [features] 12 | serde = ["dep:serde"] 13 | schemas = ["dep:schemars"] 14 | utoipa = ["dep:utoipa"] 15 | rocket = ["dep:rocket", "dep:serde_json"] 16 | axum = ["dep:axum", "dep:serde_json"] 17 | okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"] 18 | 19 | default = ["serde"] 20 | 21 | [dependencies] 22 | # Serialisation 23 | serde_json = { version = "1", optional = true } 24 | serde = { version = "1", features = ["derive"], optional = true } 25 | 26 | # Spec Generation 27 | schemars = { version = "0.8.8", optional = true } 28 | utoipa = { version = "4.2.3", optional = true } 29 | 30 | # Rocket 31 | rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } 32 | revolt_rocket_okapi = { version = "0.10.0", optional = true } 33 | revolt_okapi = { version = "0.9.1", optional = true } 34 | 35 | # Axum 36 | axum = { version = "0.7.5", optional = true } 37 | -------------------------------------------------------------------------------- /crates/core/result/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pawel Makles 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /crates/core/result/src/okapi.rs: -------------------------------------------------------------------------------- 1 | use revolt_okapi::openapi3::SchemaObject; 2 | use revolt_rocket_okapi::revolt_okapi::openapi3; 3 | use schemars::schema::Schema; 4 | 5 | use crate::Error; 6 | 7 | impl revolt_rocket_okapi::response::OpenApiResponderInner for Error { 8 | fn responses( 9 | gen: &mut revolt_rocket_okapi::gen::OpenApiGenerator, 10 | ) -> std::result::Result { 11 | let mut content = revolt_okapi::Map::new(); 12 | 13 | let settings = schemars::gen::SchemaSettings::default().with(|s| { 14 | s.option_nullable = true; 15 | s.option_add_null_type = false; 16 | s.definitions_path = "#/components/schemas/".to_string(); 17 | }); 18 | 19 | let mut schema_generator = settings.into_generator(); 20 | let schema = schema_generator.root_schema_for::(); 21 | 22 | let definitions = gen.schema_generator().definitions_mut(); 23 | for (key, value) in schema.definitions { 24 | definitions.insert(key, value); 25 | } 26 | 27 | definitions.insert("Error".to_string(), Schema::Object(schema.schema)); 28 | 29 | content.insert( 30 | "application/json".to_string(), 31 | openapi3::MediaType { 32 | schema: Some(SchemaObject { 33 | reference: Some("#/components/schemas/Error".to_string()), 34 | ..Default::default() 35 | }), 36 | ..Default::default() 37 | }, 38 | ); 39 | 40 | Ok(openapi3::Responses { 41 | default: Some(openapi3::RefOr::Object(openapi3::Response { 42 | content, 43 | description: "An error occurred.".to_string(), 44 | ..Default::default() 45 | })), 46 | ..Default::default() 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/daemons/pushd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-pushd" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | upryzing-config = { version = "0.1.0", path = "../../core/config" } 8 | upryzing-database = { version = "0.1.0", path = "../../core/database" } 9 | upryzing-models = { version = "0.1.0", path = "../../core/models", features = [ 10 | "validator", 11 | ] } 12 | 13 | amqprs = { version = "1.7.0" } 14 | fcm_v1 = "0.3.0" 15 | web-push = "0.10.0" 16 | isahc = { optional = true, version = "1.7", features = ["json"] } 17 | revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] } 18 | tokio = "1.39.2" 19 | async-trait = "0.1.81" 20 | ulid = "1.0.0" 21 | 22 | authifier = "1.0.8" 23 | 24 | log = "0.4.11" 25 | 26 | #serialization 27 | serde_json = "1" 28 | revolt_optional_struct = "0.2.0" 29 | serde = { version = "1", features = ["derive"] } 30 | iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] } 31 | base64 = "0.22.1" 32 | -------------------------------------------------------------------------------- /crates/daemons/pushd/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM ghcr.io/upryzing/base:latest AS builder 3 | FROM debian:12 AS debian 4 | 5 | # Bundle Stage 6 | FROM gcr.io/distroless/cc-debian12:nonroot 7 | COPY --from=builder /home/rust/src/target/release/upryzing-pushd ./ 8 | COPY --from=debian /usr/bin/uname /usr/bin/uname 9 | 10 | USER nonroot 11 | CMD ["./upryzing-pushd"] -------------------------------------------------------------------------------- /crates/daemons/pushd/Pushd Flowchart.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upryzing/parrot/4b5a7661666152dcf70219ceb03030ee6e1757ea/crates/daemons/pushd/Pushd Flowchart.graffle -------------------------------------------------------------------------------- /crates/daemons/pushd/src/consumers/inbound/internal.rs: -------------------------------------------------------------------------------- 1 | use amqprs::{ 2 | channel::{BasicPublishArguments, Channel}, 3 | connection::{Connection, OpenConnectionArguments}, 4 | BasicProperties, 5 | }; 6 | use log::{debug, warn}; 7 | 8 | pub(crate) trait Channeled { 9 | #[allow(unused)] 10 | fn get_connection(&self) -> Option<&Connection>; 11 | fn get_channel(&self) -> Option<&Channel>; 12 | fn set_connection(&mut self, conn: Connection); 13 | fn set_channel(&mut self, channel: Channel); 14 | } 15 | 16 | pub(crate) async fn make_channel(consumer: &mut T) { 17 | let config = upryzing_config::config().await; 18 | 19 | let args = OpenConnectionArguments::new( 20 | &config.rabbit.host, 21 | config.rabbit.port, 22 | &config.rabbit.username, 23 | &config.rabbit.password, 24 | ); 25 | let conn = amqprs::connection::Connection::open(&args).await.unwrap(); 26 | 27 | let channel = conn.open_channel(None).await.unwrap(); 28 | 29 | consumer.set_connection(conn); 30 | consumer.set_channel(channel); 31 | } 32 | 33 | pub(crate) async fn publish_message( 34 | consumer: &mut T, 35 | payload: Vec, 36 | args: BasicPublishArguments, 37 | ) { 38 | let routing_key = &args.routing_key.clone(); 39 | let mut channel = consumer.get_channel(); 40 | if channel.is_none() { 41 | make_channel(consumer).await; 42 | channel = consumer.get_channel(); 43 | } 44 | 45 | if let Some(chnl) = channel { 46 | chnl.basic_publish(BasicProperties::default(), payload.clone(), args.clone()) 47 | .await 48 | .unwrap(); 49 | debug!("Sent message to queue for target {}", routing_key); 50 | } else { 51 | warn!("Failed to unwrap channel (including attempt to make a channel)!") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /crates/daemons/pushd/src/consumers/inbound/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ack; 2 | pub mod fr_accepted; 3 | pub mod fr_received; 4 | pub mod generic; 5 | mod internal; 6 | pub mod message; 7 | -------------------------------------------------------------------------------- /crates/daemons/pushd/src/consumers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod inbound; 2 | pub mod outbound; 3 | -------------------------------------------------------------------------------- /crates/daemons/pushd/src/consumers/outbound/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod apn; 2 | pub mod fcm; 3 | pub mod vapid; 4 | -------------------------------------------------------------------------------- /crates/delta/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM ghcr.io/upryzing/base:latest AS builder 3 | FROM debian:12 AS debian 4 | 5 | # Bundle Stage 6 | FROM gcr.io/distroless/cc-debian12:nonroot 7 | COPY --from=builder /home/rust/src/target/release/upryzing-delta ./ 8 | COPY --from=debian /usr/bin/uname /usr/bin/uname 9 | 10 | EXPOSE 14702 11 | ENV ROCKET_ADDRESS 0.0.0.0 12 | USER nonroot 13 | CMD ["./upryzing-delta"] 14 | -------------------------------------------------------------------------------- /crates/delta/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /crates/delta/build.rs: -------------------------------------------------------------------------------- 1 | use vergen::{vergen, Config}; 2 | 3 | use std::process::Command; 4 | 5 | fn main() { 6 | if let Ok(output) = Command::new("git") 7 | .args(["config", "--get", "remote.origin.url"]) 8 | .output() 9 | { 10 | if let Ok(git_origin) = String::from_utf8(output.stdout) { 11 | println!("cargo:rustc-env=GIT_ORIGIN_URL={git_origin}"); 12 | } 13 | } 14 | 15 | vergen(Config::default()).ok(); 16 | } 17 | -------------------------------------------------------------------------------- /crates/delta/src/routes/bots/create.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::json::Json; 2 | use rocket::State; 3 | use upryzing_database::{Bot, Database, User}; 4 | use upryzing_models::v0; 5 | use upryzing_result::{create_error, Result}; 6 | use validator::Validate; 7 | 8 | /// # Create Bot 9 | /// 10 | /// Create a new Upryzing bot. 11 | #[openapi(tag = "Bots")] 12 | #[post("/create", data = "")] 13 | pub async fn create_bot( 14 | db: &State, 15 | user: User, 16 | info: Json, 17 | ) -> Result> { 18 | let info = info.into_inner(); 19 | info.validate().map_err(|error| { 20 | create_error!(FailedValidation { 21 | error: error.to_string() 22 | }) 23 | })?; 24 | 25 | let (bot, user) = Bot::create(db, info.name, &user, None).await?; 26 | Ok(Json(v0::BotWithUserResponse { 27 | bot: bot.into(), 28 | user: user.into_self(false).await, 29 | })) 30 | } 31 | 32 | #[cfg(test)] 33 | mod test { 34 | use crate::{rocket, util::test::TestHarness}; 35 | use rocket::http::{ContentType, Header, Status}; 36 | use upryzing_models::v0; 37 | 38 | #[rocket::async_test] 39 | async fn create_bot() { 40 | let harness = TestHarness::new().await; 41 | let (_, session, _) = harness.new_user().await; 42 | 43 | let response = harness 44 | .client 45 | .post("/bots/create") 46 | .header(Header::new("x-session-token", session.token.to_string())) 47 | .header(ContentType::JSON) 48 | .body( 49 | json!(v0::DataCreateBot { 50 | name: TestHarness::rand_string(), 51 | }) 52 | .to_string(), 53 | ) 54 | .dispatch() 55 | .await; 56 | 57 | assert_eq!(response.status(), Status::Ok); 58 | 59 | let bot: v0::Bot = response.into_json().await.expect("`Bot`"); 60 | assert!(harness.db.fetch_bot(&bot.id).await.is_ok()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /crates/delta/src/routes/bots/fetch.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{util::reference::Reference, Database, User}; 3 | use upryzing_models::v0::FetchBotResponse; 4 | use upryzing_result::{create_error, Result}; 5 | 6 | /// # Fetch Bot 7 | /// 8 | /// Fetch details of a bot you own by its id. 9 | #[openapi(tag = "Bots")] 10 | #[get("/")] 11 | pub async fn fetch_bot( 12 | db: &State, 13 | user: User, 14 | bot: Reference, 15 | ) -> Result> { 16 | if user.bot.is_some() { 17 | return Err(create_error!(IsBot)); 18 | } 19 | 20 | let bot = bot.as_bot(db).await?; 21 | if bot.owner != user.id { 22 | return Err(create_error!(NotFound)); 23 | } 24 | 25 | Ok(Json(FetchBotResponse { 26 | user: db.fetch_user(&bot.id).await?.into(db, None).await, 27 | bot: bot.into(), 28 | })) 29 | } 30 | 31 | #[cfg(test)] 32 | mod test { 33 | use crate::{rocket, util::test::TestHarness}; 34 | use rocket::http::{Header, Status}; 35 | use upryzing_database::Bot; 36 | use upryzing_models::v0; 37 | 38 | #[rocket::async_test] 39 | async fn fetch_bot() { 40 | let harness = TestHarness::new().await; 41 | let (_, session, user) = harness.new_user().await; 42 | 43 | let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) 44 | .await 45 | .expect("`Bot`"); 46 | 47 | let response = harness 48 | .client 49 | .get(format!("/bots/{}", bot.id)) 50 | .header(Header::new("x-session-token", session.token.to_string())) 51 | .dispatch() 52 | .await; 53 | 54 | assert_eq!(response.status(), Status::Ok); 55 | 56 | let response: v0::FetchBotResponse = response.into_json().await.expect("`Bot`"); 57 | assert_eq!(response.bot, bot.into()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/delta/src/routes/bots/fetch_public.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::{util::reference::Reference, Database, User}; 2 | use upryzing_models::v0::PublicBot; 3 | use upryzing_result::{create_error, Result}; 4 | 5 | use rocket::serde::json::Json; 6 | use rocket::State; 7 | 8 | /// # Fetch Public Bot 9 | /// 10 | /// Fetch details of a public (or owned) bot by its id. 11 | #[openapi(tag = "Bots")] 12 | #[get("//invite")] 13 | pub async fn fetch_public_bot( 14 | db: &State, 15 | user: Option, 16 | target: Reference, 17 | ) -> Result> { 18 | let bot = db.fetch_bot(&target.id).await?; 19 | if !bot.public && user.map_or(true, |x| x.id != bot.owner) { 20 | return Err(create_error!(NotFound)); 21 | } 22 | 23 | let user = db.fetch_user(&bot.id).await?; 24 | Ok(Json(bot.into_public_bot(user))) 25 | } 26 | 27 | #[cfg(test)] 28 | mod test { 29 | use crate::{rocket, util::test::TestHarness}; 30 | use upryzing_database::{Bot, PartialBot}; 31 | use upryzing_models::v0; 32 | 33 | #[rocket::async_test] 34 | async fn fetch_public() { 35 | let harness = TestHarness::new().await; 36 | let (_, _, user) = harness.new_user().await; 37 | 38 | let (mut bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None) 39 | .await 40 | .expect("`Bot`"); 41 | 42 | bot.update( 43 | &harness.db, 44 | PartialBot { 45 | public: Some(true), 46 | ..Default::default() 47 | }, 48 | vec![], 49 | ) 50 | .await 51 | .unwrap(); 52 | 53 | let bot_user = harness.db.fetch_user(&bot.id).await.expect("`User`"); 54 | let response = harness 55 | .client 56 | .get(format!("/bots/{}/invite", bot.id)) 57 | .dispatch() 58 | .await; 59 | 60 | let public_bot: v0::PublicBot = response.into_json().await.expect("`PublicBot`"); 61 | assert_eq!(public_bot, bot.into_public_bot(bot_user)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/delta/src/routes/bots/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod create; 5 | mod delete; 6 | mod edit; 7 | mod fetch; 8 | mod fetch_owned; 9 | mod fetch_public; 10 | mod invite; 11 | 12 | pub fn routes() -> (Vec, OpenApi) { 13 | openapi_get_routes_spec![ 14 | create::create_bot, 15 | invite::invite_bot, 16 | fetch_public::fetch_public_bot, 17 | fetch::fetch_bot, 18 | fetch_owned::fetch_owned_bots, 19 | edit::edit_bot, 20 | delete::delete_bot, 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/invite_create.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::{ 2 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 3 | Database, Invite, User, 4 | }; 5 | use upryzing_models::v0; 6 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 7 | 8 | use rocket::{serde::json::Json, State}; 9 | use upryzing_result::{create_error, Result}; 10 | 11 | /// # Create Invite 12 | /// 13 | /// Creates an invite to this channel. 14 | /// 15 | /// Channel must be a `TextChannel`. 16 | #[openapi(tag = "Channel Invites")] 17 | #[post("//invites")] 18 | pub async fn create_invite( 19 | db: &State, 20 | user: User, 21 | target: Reference, 22 | ) -> Result> { 23 | if user.bot.is_some() { 24 | return Err(create_error!(IsBot)); 25 | } 26 | 27 | let channel = target.as_channel(db).await?; 28 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 29 | calculate_channel_permissions(&mut query) 30 | .await 31 | .throw_if_lacking_channel_permission(ChannelPermission::InviteOthers)?; 32 | 33 | Invite::create_channel_invite(db, &user, &channel) 34 | .await 35 | .map(|invite| invite.into()) 36 | .map(Json) 37 | } 38 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/members_fetch.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Channel, Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Fetch Group Members 11 | /// 12 | /// Retrieves all users who are part of this group. 13 | /// 14 | /// This may not return full user information if users are not friends but have mutual connections. 15 | #[openapi(tag = "Groups")] 16 | #[get("//members")] 17 | pub async fn fetch_members( 18 | db: &State, 19 | user: User, 20 | target: Reference, 21 | ) -> Result>> { 22 | let channel = target.as_channel(db).await?; 23 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 24 | calculate_channel_permissions(&mut query) 25 | .await 26 | .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; 27 | 28 | if let Channel::Group { recipients, .. } = channel { 29 | Ok(Json( 30 | User::fetch_many_ids_as_mutuals(db, &user, &recipients).await?, 31 | )) 32 | } else { 33 | Err(create_error!(InvalidOperation)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/message_clear_reactions.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, PartialMessage, User, 6 | }; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Remove All Reactions from Message 11 | /// 12 | /// Remove your own, someone else's or all of a given reaction. 13 | /// 14 | /// Requires `ManageMessages` permission. 15 | #[openapi(tag = "Interactions")] 16 | #[delete("//messages//reactions")] 17 | pub async fn clear_reactions( 18 | db: &State, 19 | user: User, 20 | target: Reference, 21 | msg: Reference, 22 | ) -> Result { 23 | let channel = target.as_channel(db).await?; 24 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 25 | calculate_channel_permissions(&mut query) 26 | .await 27 | .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; 28 | 29 | // Fetch relevant message 30 | let mut message = msg.as_message_in_channel(db, channel.id()).await?; 31 | 32 | // Clear reactions 33 | message 34 | .update( 35 | db, 36 | PartialMessage { 37 | reactions: Some(Default::default()), 38 | ..Default::default() 39 | }, 40 | vec![], 41 | ) 42 | .await 43 | .map(|_| EmptyResponse) 44 | } 45 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/message_delete.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, User, 6 | }; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Delete Message 11 | /// 12 | /// Delete a message you've sent or one you have permission to delete. 13 | #[openapi(tag = "Messaging")] 14 | #[delete("//messages/", rank = 2)] 15 | pub async fn delete( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | msg: Reference, 20 | ) -> Result { 21 | let message = msg.as_message_in_channel(db, &target.id).await?; 22 | 23 | if message.author != user.id { 24 | let channel = target.as_channel(db).await?; 25 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 26 | calculate_channel_permissions(&mut query) 27 | .await 28 | .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; 29 | } 30 | 31 | message.delete(db).await.map(|_| EmptyResponse) 32 | } 33 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/message_fetch.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Fetch Message 11 | /// 12 | /// Retrieves a message by its id. 13 | #[openapi(tag = "Messaging")] 14 | #[get("//messages/")] 15 | pub async fn fetch( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | msg: Reference, 20 | ) -> Result> { 21 | let channel = target.as_channel(db).await?; 22 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 23 | calculate_channel_permissions(&mut query) 24 | .await 25 | .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; 26 | 27 | let message = msg.as_message(db).await?; 28 | if message.channel != channel.id() { 29 | return Err(create_error!(NotFound)); 30 | } 31 | 32 | Ok(Json(message.into_model(None, None))) 33 | } 34 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/message_react.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, User, 6 | }; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Add Reaction to Message 11 | /// 12 | /// React to a given message. 13 | #[openapi(tag = "Interactions")] 14 | #[put("//messages//reactions/")] 15 | pub async fn react_message( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | msg: Reference, 20 | emoji: Reference, 21 | ) -> Result { 22 | let channel = target.as_channel(db).await?; 23 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 24 | calculate_channel_permissions(&mut query) 25 | .await 26 | .throw_if_lacking_channel_permission(ChannelPermission::React)?; 27 | 28 | // Fetch relevant message 29 | let message = msg.as_message_in_channel(db, channel.id()).await?; 30 | 31 | // Add the reaction 32 | message 33 | .add_reaction(db, &user, &emoji.id) 34 | .await 35 | .map(|_| EmptyResponse) 36 | } 37 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/message_unreact.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, User, 6 | }; 7 | use upryzing_models::v0; 8 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 9 | use upryzing_result::Result; 10 | 11 | /// # Remove Reaction(s) to Message 12 | /// 13 | /// Remove your own, someone else's or all of a given reaction. 14 | /// 15 | /// Requires `ManageMessages` if changing others' reactions. 16 | #[openapi(tag = "Interactions")] 17 | #[delete("//messages//reactions/?")] 18 | pub async fn unreact_message( 19 | db: &State, 20 | user: User, 21 | target: Reference, 22 | msg: Reference, 23 | emoji: Reference, 24 | options: v0::OptionsUnreact, 25 | ) -> Result { 26 | let channel = target.as_channel(db).await?; 27 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 28 | let permissions = calculate_channel_permissions(&mut query).await; 29 | 30 | permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?; 31 | 32 | // Check if we need to escalate permissions 33 | let remove_all = options.remove_all.unwrap_or_default(); 34 | if options.user_id.is_some() || remove_all { 35 | permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?; 36 | } 37 | 38 | // Fetch relevant message 39 | let message = msg.as_message_in_channel(db, channel.id()).await?; 40 | 41 | // Check if we should wipe all of this reaction 42 | if remove_all { 43 | return message 44 | .clear_reaction(db, &emoji.id) 45 | .await 46 | .map(|_| EmptyResponse); 47 | } 48 | 49 | // Remove the reaction 50 | message 51 | .remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), &emoji.id) 52 | .await 53 | .map(|_| EmptyResponse) 54 | } 55 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod channel_ack; 5 | mod channel_delete; 6 | mod channel_edit; 7 | mod channel_fetch; 8 | mod group_add_member; 9 | mod group_create; 10 | mod group_remove_member; 11 | mod invite_create; 12 | mod members_fetch; 13 | mod message_bulk_delete; 14 | mod message_clear_reactions; 15 | mod message_delete; 16 | mod message_edit; 17 | mod message_fetch; 18 | mod message_pin; 19 | mod message_query; 20 | mod message_react; 21 | mod message_search; 22 | mod message_send; 23 | mod message_unpin; 24 | mod message_unreact; 25 | mod permissions_set; 26 | mod permissions_set_default; 27 | mod voice_join; 28 | mod webhook_create; 29 | mod webhook_fetch_all; 30 | 31 | pub fn routes() -> (Vec, OpenApi) { 32 | openapi_get_routes_spec![ 33 | channel_ack::ack, 34 | channel_fetch::fetch, 35 | members_fetch::fetch_members, 36 | channel_delete::delete, 37 | channel_edit::edit, 38 | invite_create::create_invite, 39 | message_send::message_send, 40 | message_query::query, 41 | message_search::search, 42 | message_pin::message_pin, 43 | message_fetch::fetch, 44 | message_edit::edit, 45 | message_bulk_delete::bulk_delete_messages, 46 | message_delete::delete, 47 | message_unpin::message_unpin, 48 | group_create::create_group, 49 | group_add_member::add_member, 50 | group_remove_member::remove_member, 51 | voice_join::call, 52 | permissions_set::set_role_permissions, 53 | permissions_set_default::set_default_permissions, 54 | message_react::react_message, 55 | message_unreact::unreact_message, 56 | message_clear_reactions::clear_reactions, 57 | webhook_create::create_webhook, 58 | webhook_fetch_all::fetch_webhooks, 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/permissions_set.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, User, 4 | }; 5 | use upryzing_models::v0; 6 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission, Override}; 7 | use upryzing_result::{create_error, Result}; 8 | 9 | /// # Set Role Permission 10 | /// 11 | /// Sets permissions for the specified role in this channel. 12 | /// 13 | /// Channel must be a `TextChannel` or `VoiceChannel`. 14 | #[openapi(tag = "Channel Permissions")] 15 | #[put("//permissions/", data = "", rank = 2)] 16 | pub async fn set_role_permissions( 17 | db: &State, 18 | user: User, 19 | target: Reference, 20 | role_id: String, 21 | data: Json, 22 | ) -> Result> { 23 | let mut channel = target.as_channel(db).await?; 24 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 25 | let permissions = calculate_channel_permissions(&mut query).await; 26 | 27 | permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; 28 | 29 | if let Some(server) = query.server_ref() { 30 | if let Some(role) = server.roles.get(&role_id) { 31 | if role.rank <= query.get_member_rank().unwrap_or(i64::MIN) { 32 | return Err(create_error!(NotElevated)); 33 | } 34 | 35 | let current_value: Override = role.permissions.into(); 36 | permissions 37 | .throw_permission_override(current_value, &data.permissions) 38 | .await?; 39 | 40 | channel 41 | .set_role_permission(db, &role_id, data.permissions.clone().into()) 42 | .await?; 43 | 44 | Ok(Json(channel.into())) 45 | } else { 46 | Err(create_error!(NotFound)) 47 | } 48 | } else { 49 | Err(create_error!(InvalidOperation)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/delta/src/routes/channels/webhook_fetch_all.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0::Webhook; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Gets all webhooks 11 | /// 12 | /// Gets all webhooks inside the channel 13 | #[openapi(tag = "Webhooks")] 14 | #[get("//webhooks")] 15 | pub async fn fetch_webhooks( 16 | db: &State, 17 | user: User, 18 | channel_id: Reference, 19 | ) -> Result>> { 20 | let channel = channel_id.as_channel(db).await?; 21 | 22 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 23 | calculate_channel_permissions(&mut query) 24 | .await 25 | .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; 26 | 27 | Ok(Json( 28 | db.fetch_webhooks_for_channel(channel.id()) 29 | .await? 30 | .into_iter() 31 | .map(|v| v.into()) 32 | .collect::>(), 33 | )) 34 | } 35 | -------------------------------------------------------------------------------- /crates/delta/src/routes/customisation/emoji_delete.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::{ 2 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 3 | Database, EmojiParent, User, 4 | }; 5 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission}; 6 | use upryzing_result::{create_error, Result}; 7 | 8 | use rocket::State; 9 | use rocket_empty::EmptyResponse; 10 | 11 | /// # Delete Emoji 12 | /// 13 | /// Delete an emoji by its id. 14 | #[openapi(tag = "Emojis")] 15 | #[delete("/emoji/")] 16 | pub async fn delete_emoji( 17 | db: &State, 18 | user: User, 19 | emoji_id: Reference, 20 | ) -> Result { 21 | // Bots cannot manage emoji 22 | if user.bot.is_some() { 23 | return Err(create_error!(IsBot)); 24 | } 25 | 26 | // Fetch the emoji 27 | let emoji = emoji_id.as_emoji(db).await?; 28 | 29 | // If we uploaded the emoji, then we have permission to delete it 30 | if emoji.creator_id != user.id { 31 | // Otherwise, validate we have permission to delete from parent 32 | match &emoji.parent { 33 | EmojiParent::Server { id } => { 34 | let server = db.fetch_server(id).await?; 35 | 36 | // Check for permission 37 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 38 | calculate_server_permissions(&mut query) 39 | .await 40 | .throw_if_lacking_channel_permission(ChannelPermission::ManageCustomisation)?; 41 | } 42 | EmojiParent::Detached => return Ok(EmptyResponse), 43 | }; 44 | } 45 | 46 | // Delete the emoji 47 | emoji.delete(db).await.map(|_| EmptyResponse) 48 | } 49 | -------------------------------------------------------------------------------- /crates/delta/src/routes/customisation/emoji_fetch.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::{util::reference::Reference, Database}; 2 | use upryzing_models::v0; 3 | use upryzing_result::Result; 4 | 5 | use rocket::{serde::json::Json, State}; 6 | 7 | /// # Fetch Emoji 8 | /// 9 | /// Fetch an emoji by its id. 10 | #[openapi(tag = "Emojis")] 11 | #[get("/emoji/")] 12 | pub async fn fetch_emoji(db: &State, emoji_id: Reference) -> Result> { 13 | emoji_id 14 | .as_emoji(db) 15 | .await 16 | .map(|emoji| emoji.into()) 17 | .map(Json) 18 | } 19 | -------------------------------------------------------------------------------- /crates/delta/src/routes/customisation/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod emoji_create; 5 | mod emoji_delete; 6 | mod emoji_fetch; 7 | 8 | pub fn routes() -> (Vec, OpenApi) { 9 | openapi_get_routes_spec![ 10 | emoji_create::create_emoji, 11 | emoji_delete::delete_emoji, 12 | emoji_fetch::fetch_emoji 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /crates/delta/src/routes/invites/invite_delete.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, Invite, User, 6 | }; 7 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Delete Invite 11 | /// 12 | /// Delete an invite by its id. 13 | #[openapi(tag = "Invites")] 14 | #[delete("/")] 15 | pub async fn delete(db: &State, user: User, target: Reference) -> Result { 16 | let invite = target.as_invite(db).await?; 17 | 18 | if user.id == invite.creator() { 19 | db.delete_invite(invite.code()).await 20 | } else { 21 | match invite { 22 | Invite::Server { code, server, .. } => { 23 | let server = db.fetch_server(&server).await?; 24 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 25 | calculate_server_permissions(&mut query) 26 | .await 27 | .throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?; 28 | 29 | db.delete_invite(&code).await 30 | } 31 | _ => unreachable!(), 32 | } 33 | } 34 | .map(|_| EmptyResponse) 35 | } 36 | -------------------------------------------------------------------------------- /crates/delta/src/routes/invites/invite_join.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::reference::Reference, Channel, Database, Invite, Member, User, AMQP, 4 | }; 5 | use upryzing_models::v0::{self, InviteJoinResponse}; 6 | use upryzing_result::{create_error, Result}; 7 | 8 | /// # Join Invite 9 | /// 10 | /// Join an invite by its ID 11 | #[openapi(tag = "Invites")] 12 | #[post("/")] 13 | pub async fn join( 14 | db: &State, 15 | amqp: &State, 16 | user: User, 17 | target: Reference, 18 | ) -> Result> { 19 | if user.bot.is_some() { 20 | return Err(create_error!(IsBot)); 21 | } 22 | 23 | user.can_acquire_server(db).await?; 24 | 25 | let invite = target.as_invite(db).await?; 26 | match &invite { 27 | Invite::Server { server, .. } => { 28 | let server = db.fetch_server(server).await?; 29 | let (_, channels) = Member::create(db, &server, &user, None).await?; 30 | 31 | Ok(Json(InviteJoinResponse::Server { 32 | channels: channels.into_iter().map(|c| c.into()).collect(), 33 | server: server.into(), 34 | })) 35 | } 36 | Invite::Group { 37 | channel, creator, .. 38 | } => { 39 | let mut channel = db.fetch_channel(channel).await?; 40 | channel.add_user_to_group(db, amqp, &user, creator).await?; 41 | if let Channel::Group { recipients, .. } = &channel { 42 | Ok(Json(InviteJoinResponse::Group { 43 | users: User::fetch_many_ids_as_mutuals(db, &user, recipients).await?, 44 | channel: channel.into(), 45 | })) 46 | } else { 47 | unreachable!() 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/delta/src/routes/invites/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod invite_delete; 5 | mod invite_fetch; 6 | mod invite_join; 7 | 8 | pub fn routes() -> (Vec, OpenApi) { 9 | openapi_get_routes_spec![ 10 | invite_fetch::fetch, 11 | invite_join::join, 12 | invite_delete::delete 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /crates/delta/src/routes/onboard/complete.rs: -------------------------------------------------------------------------------- 1 | use authifier::models::Session; 2 | use once_cell::sync::Lazy; 3 | use regex::Regex; 4 | use upryzing_database::{Database, User}; 5 | use upryzing_models::v0; 6 | use upryzing_result::{create_error, Result}; 7 | 8 | use rocket::{serde::json::Json, State}; 9 | use serde::{Deserialize, Serialize}; 10 | use validator::Validate; 11 | 12 | /// Regex for valid usernames 13 | /// 14 | /// Block zero width space 15 | /// Block lookalike characters 16 | pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); 17 | 18 | /// # New User Data 19 | #[derive(Validate, Serialize, Deserialize, JsonSchema)] 20 | pub struct DataOnboard { 21 | /// New username which will be used to identify the user on the platform 22 | #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] 23 | username: String, 24 | } 25 | 26 | /// # Complete Onboarding 27 | /// 28 | /// This sets a new username, completes onboarding and allows a user to start using Revolt. 29 | #[openapi(tag = "Onboarding")] 30 | #[post("/complete", data = "")] 31 | pub async fn complete( 32 | db: &State, 33 | session: Session, 34 | user: Option, 35 | data: Json, 36 | ) -> Result> { 37 | if user.is_some() { 38 | return Err(create_error!(AlreadyOnboarded)); 39 | } 40 | 41 | let data = data.into_inner(); 42 | data.validate().map_err(|error| { 43 | create_error!(FailedValidation { 44 | error: error.to_string() 45 | }) 46 | })?; 47 | 48 | Ok(Json( 49 | User::create(db, data.username, session.user_id, None) 50 | .await? 51 | .into_self(false) 52 | .await, 53 | )) 54 | } 55 | -------------------------------------------------------------------------------- /crates/delta/src/routes/onboard/hello.rs: -------------------------------------------------------------------------------- 1 | use authifier::models::Session; 2 | use upryzing_database::User; 3 | 4 | use rocket::serde::json::Json; 5 | use serde::Serialize; 6 | 7 | /// # Onboarding Status 8 | #[derive(Serialize, JsonSchema)] 9 | pub struct DataHello { 10 | /// Whether onboarding is required 11 | onboarding: bool, 12 | } 13 | 14 | /// # Check Onboarding Status 15 | /// 16 | /// This will tell you whether the current account requires onboarding or whether you can continue to send requests as usual. You may skip calling this if you're restoring an existing session. 17 | #[openapi(tag = "Onboarding")] 18 | #[get("/hello")] 19 | pub async fn hello(_session: Session, user: Option) -> Json { 20 | Json(DataHello { 21 | onboarding: user.is_none(), 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /crates/delta/src/routes/onboard/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod complete; 5 | mod hello; 6 | 7 | pub fn routes() -> (Vec, OpenApi) { 8 | openapi_get_routes_spec![hello::hello, complete::complete] 9 | } 10 | -------------------------------------------------------------------------------- /crates/delta/src/routes/push/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod subscribe; 5 | mod unsubscribe; 6 | 7 | pub fn routes() -> (Vec, OpenApi) { 8 | openapi_get_routes_spec![subscribe::subscribe, unsubscribe::unsubscribe] 9 | } 10 | -------------------------------------------------------------------------------- /crates/delta/src/routes/push/subscribe.rs: -------------------------------------------------------------------------------- 1 | use authifier::{ 2 | models::{Session, WebPushSubscription}, 3 | Authifier, 4 | }; 5 | use rocket::{serde::json::Json, State}; 6 | use rocket_empty::EmptyResponse; 7 | use upryzing_result::{create_database_error, Result}; 8 | 9 | /// # Push Subscribe 10 | /// 11 | /// Create a new Web Push subscription. 12 | /// 13 | /// If an existing subscription exists on this session, it will be removed. 14 | #[openapi(tag = "Web Push")] 15 | #[post("/subscribe", data = "")] 16 | pub async fn subscribe( 17 | authifier: &State, 18 | mut session: Session, 19 | data: Json, 20 | ) -> Result { 21 | session.subscription = Some(data.into_inner()); 22 | session 23 | .save(authifier) 24 | .await 25 | .map(|_| EmptyResponse) 26 | .map_err(|_| create_database_error!("save", "session")) 27 | } 28 | -------------------------------------------------------------------------------- /crates/delta/src/routes/push/unsubscribe.rs: -------------------------------------------------------------------------------- 1 | use authifier::{models::Session, Authifier}; 2 | 3 | use rocket_empty::EmptyResponse; 4 | use upryzing_result::{create_database_error, Result}; 5 | 6 | use rocket::State; 7 | 8 | /// # Unsubscribe 9 | /// 10 | /// Remove the Web Push subscription associated with the current session. 11 | #[openapi(tag = "Web Push")] 12 | #[post("/unsubscribe")] 13 | pub async fn unsubscribe( 14 | authifier: &State, 15 | mut session: Session, 16 | ) -> Result { 17 | session.subscription = None; 18 | session 19 | .save(authifier) 20 | .await 21 | .map(|_| EmptyResponse) 22 | .map_err(|_| create_database_error!("save", "session")) 23 | } 24 | -------------------------------------------------------------------------------- /crates/delta/src/routes/safety/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod report_content; 5 | 6 | pub fn routes() -> (Vec, OpenApi) { 7 | openapi_get_routes_spec![ 8 | // Reports 9 | report_content::report_content, 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/ban_list.rs: -------------------------------------------------------------------------------- 1 | use futures::future::join_all; 2 | use upryzing_database::util::permissions::DatabasePermissionQuery; 3 | use upryzing_database::util::reference::Reference; 4 | use upryzing_database::{Database, User}; 5 | use upryzing_models::v0; 6 | 7 | use rocket::serde::json::Json; 8 | use rocket::State; 9 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission}; 10 | use upryzing_result::Result; 11 | 12 | /// # Fetch Bans 13 | /// 14 | /// Fetch all bans on a server. 15 | #[openapi(tag = "Server Members")] 16 | #[get("//bans")] 17 | pub async fn list( 18 | db: &State, 19 | user: User, 20 | target: Reference, 21 | ) -> Result> { 22 | let server = target.as_server(db).await?; 23 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 24 | calculate_server_permissions(&mut query) 25 | .await 26 | .throw_if_lacking_channel_permission(ChannelPermission::BanMembers)?; 27 | 28 | let bans = db.fetch_bans(&server.id).await?; 29 | let users = join_all( 30 | db.fetch_users( 31 | &bans 32 | .iter() 33 | .map(|x| &x.id.user) 34 | .cloned() 35 | .collect::>(), 36 | ) 37 | .await? 38 | .into_iter() 39 | .map(|u| u.into_self(false)), 40 | ) 41 | .await; 42 | 43 | Ok(Json(v0::BanListResult { 44 | users: users.into_iter().map(Into::into).collect(), 45 | bans: bans.into_iter().map(Into::into).collect(), 46 | })) 47 | } 48 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/ban_remove.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, User, 6 | }; 7 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Unban user 11 | /// 12 | /// Remove a user's ban. 13 | #[openapi(tag = "Server Members")] 14 | #[delete("//bans/")] 15 | pub async fn unban( 16 | db: &State, 17 | user: User, 18 | server: Reference, 19 | target: Reference, 20 | ) -> Result { 21 | let server = server.as_server(db).await?; 22 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 23 | calculate_server_permissions(&mut query) 24 | .await 25 | .throw_if_lacking_channel_permission(ChannelPermission::BanMembers)?; 26 | 27 | let ban = target.as_ban(db, &server.id).await?; 28 | db.delete_ban(&ban.id).await.map(|_| EmptyResponse) 29 | } 30 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/channel_create.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::util::permissions::DatabasePermissionQuery; 2 | use upryzing_database::{util::reference::Reference, Channel, Database, User}; 3 | use upryzing_models::v0; 4 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission}; 5 | use upryzing_result::{create_error, Result}; 6 | 7 | use rocket::serde::json::Json; 8 | use rocket::State; 9 | use validator::Validate; 10 | 11 | /// # Create Channel 12 | /// 13 | /// Create a new Text or Voice channel. 14 | #[openapi(tag = "Server Information")] 15 | #[post("//channels", data = "")] 16 | pub async fn create_server_channel( 17 | db: &State, 18 | user: User, 19 | server: Reference, 20 | data: Json, 21 | ) -> Result> { 22 | let data = data.into_inner(); 23 | data.validate().map_err(|error| { 24 | create_error!(FailedValidation { 25 | error: error.to_string() 26 | }) 27 | })?; 28 | 29 | let mut server = server.as_server(db).await?; 30 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 31 | calculate_server_permissions(&mut query) 32 | .await 33 | .throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; 34 | 35 | Channel::create_server_channel(db, &mut server, data, true) 36 | .await 37 | .map(|channel| channel.into()) 38 | .map(Json) 39 | } 40 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/emoji_list.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::PermissionQuery; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Fetch Server Emoji 11 | /// 12 | /// Fetch all emoji on a server. 13 | #[openapi(tag = "Server Customisation")] 14 | #[get("//emojis")] 15 | pub async fn list_emoji( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | ) -> Result>> { 20 | let server = target.as_server(db).await?; 21 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 22 | if !query.are_we_a_member().await { 23 | return Err(create_error!(NotFound)); 24 | } 25 | 26 | // Fetch all emoji from server if we can view it 27 | db.fetch_emoji_by_parent_id(&server.id) 28 | .await 29 | .map(|v| v.into_iter().map(Into::into).collect()) 30 | .map(Json) 31 | } 32 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/invites_fetch.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Fetch Invites 11 | /// 12 | /// Fetch all server invites. 13 | #[openapi(tag = "Server Members")] 14 | #[get("//invites")] 15 | pub async fn invites( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | ) -> Result>> { 20 | let server = target.as_server(db).await?; 21 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 22 | calculate_server_permissions(&mut query) 23 | .await 24 | .throw_if_lacking_channel_permission(ChannelPermission::ManageServer)?; 25 | 26 | db.fetch_invites_for_server(&server.id) 27 | .await 28 | .map(|v| v.into_iter().map(Into::into).collect()) 29 | .map(Json) 30 | } 31 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/member_fetch.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::PermissionQuery; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Fetch Member 11 | /// 12 | /// Retrieve a member. 13 | #[openapi(tag = "Server Members")] 14 | #[get("//members/?")] 15 | pub async fn fetch( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | member: Reference, 20 | roles: Option, 21 | ) -> Result> { 22 | let server = target.as_server(db).await?; 23 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 24 | if !query.are_we_a_member().await { 25 | return Err(create_error!(NotFound)); 26 | } 27 | 28 | let member = member.as_member(db, &server.id).await?; 29 | if let Some(true) = roles { 30 | Ok(Json(v0::MemberResponse::MemberWithRoles { 31 | roles: server 32 | .roles 33 | .into_iter() 34 | .filter(|(k, _)| member.roles.contains(k)) 35 | .map(|(k, v)| (k, v.into())) 36 | .collect(), 37 | member: member.into(), 38 | })) 39 | } else { 40 | Ok(Json(v0::MemberResponse::Member(member.into()))) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/member_fetch_all.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::PermissionQuery; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Fetch Members 11 | /// 12 | /// Fetch all server members. 13 | #[openapi(tag = "Server Members")] 14 | #[get("//members?")] 15 | pub async fn fetch_all( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | options: v0::OptionsFetchAllMembers, 20 | ) -> Result> { 21 | let server = target.as_server(db).await?; 22 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 23 | if !query.are_we_a_member().await { 24 | return Err(create_error!(NotFound)); 25 | } 26 | 27 | let mut members = db.fetch_all_members(&server.id).await?; 28 | 29 | let user_ids: Vec = members 30 | .iter() 31 | .map(|member| member.id.user.clone()) 32 | .collect(); 33 | 34 | let mut users = User::fetch_many_ids_as_mutuals(db, &user, &user_ids).await?; 35 | 36 | // Ensure the lists match up exactly. 37 | members.sort_by(|a, b| a.id.user.cmp(&b.id.user)); 38 | users.sort_by(|a, b| a.id.cmp(&b.id)); 39 | 40 | // Optionally, remove all offline user entries. 41 | if let Some(true) = options.exclude_offline { 42 | let mut iter = users.iter(); 43 | members.retain(|_| iter.next().unwrap().online); 44 | users.retain(|user| user.online); 45 | } 46 | 47 | Ok(Json(v0::AllMemberResponse { 48 | members: members.into_iter().map(Into::into).collect(), 49 | users, 50 | })) 51 | } 52 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/member_remove.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, RemovalIntention, User, 6 | }; 7 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission}; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Kick Member 11 | /// 12 | /// Removes a member from the server. 13 | #[openapi(tag = "Server Members")] 14 | #[delete("//members/")] 15 | pub async fn kick( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | member: Reference, 20 | ) -> Result { 21 | let server = target.as_server(db).await?; 22 | 23 | if member.id == user.id { 24 | return Err(create_error!(CannotRemoveYourself)); 25 | } 26 | 27 | if member.id == server.owner { 28 | return Err(create_error!(InvalidOperation)); 29 | } 30 | 31 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 32 | calculate_server_permissions(&mut query) 33 | .await 34 | .throw_if_lacking_channel_permission(ChannelPermission::KickMembers)?; 35 | 36 | let member = member.as_member(db, &server.id).await?; 37 | if member.get_ranking(query.server_ref().as_ref().unwrap()) 38 | <= query.get_member_rank().unwrap_or(i64::MIN) 39 | { 40 | return Err(create_error!(NotElevated)); 41 | } 42 | 43 | member 44 | .remove(db, &server, RemovalIntention::Kick, false) 45 | .await 46 | .map(|_| EmptyResponse) 47 | } 48 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod ban_create; 5 | mod ban_list; 6 | mod ban_remove; 7 | mod channel_create; 8 | mod emoji_list; 9 | mod invites_fetch; 10 | mod member_edit; 11 | mod member_experimental_query; 12 | mod member_fetch; 13 | mod member_fetch_all; 14 | mod member_remove; 15 | mod permissions_set; 16 | mod permissions_set_default; 17 | mod roles_create; 18 | mod roles_delete; 19 | mod roles_edit; 20 | mod roles_fetch; 21 | mod server_ack; 22 | mod server_create; 23 | mod server_delete; 24 | mod server_edit; 25 | mod server_fetch; 26 | 27 | pub fn routes() -> (Vec, OpenApi) { 28 | openapi_get_routes_spec![ 29 | server_create::create_server, 30 | server_delete::delete, 31 | server_fetch::fetch, 32 | server_edit::edit, 33 | server_ack::ack, 34 | channel_create::create_server_channel, 35 | member_fetch_all::fetch_all, 36 | member_remove::kick, 37 | member_fetch::fetch, 38 | member_edit::edit, 39 | member_experimental_query::member_experimental_query, 40 | ban_create::ban, 41 | ban_remove::unban, 42 | ban_list::list, 43 | invites_fetch::invites, 44 | roles_create::create, 45 | roles_edit::edit, 46 | roles_fetch::fetch, 47 | roles_delete::delete, 48 | permissions_set::set_role_permission, 49 | permissions_set_default::set_default_permissions, 50 | emoji_list::list_emoji 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/permissions_set.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission, Override}; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Set Role Permission 11 | /// 12 | /// Sets permissions for the specified role in the server. 13 | #[openapi(tag = "Server Permissions")] 14 | #[put("//permissions/", data = "", rank = 2)] 15 | pub async fn set_role_permission( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | role_id: String, 20 | data: Json, 21 | ) -> Result> { 22 | let data = data.into_inner(); 23 | 24 | let mut server = target.as_server(db).await?; 25 | if let Some((current_value, rank)) = server.roles.get(&role_id).map(|x| (x.permissions, x.rank)) 26 | { 27 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 28 | let permissions = calculate_server_permissions(&mut query).await; 29 | 30 | permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; 31 | 32 | // Prevent us from editing roles above us 33 | if rank <= query.get_member_rank().unwrap_or(i64::MIN) { 34 | return Err(create_error!(NotElevated)); 35 | } 36 | 37 | // Ensure we have access to grant these permissions forwards 38 | let current_value: Override = current_value.into(); 39 | permissions 40 | .throw_permission_override(current_value, &data.permissions) 41 | .await?; 42 | 43 | server 44 | .set_role_permission(db, &role_id, data.permissions.into()) 45 | .await?; 46 | 47 | Ok(Json(server.into())) 48 | } else { 49 | Err(create_error!(NotFound)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/permissions_set_default.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, PartialServer, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::{ 8 | calculate_server_permissions, ChannelPermission, DataPermissionsValue, Override, 9 | }; 10 | use upryzing_result::Result; 11 | 12 | /// # Set Default Permission 13 | /// 14 | /// Sets permissions for the default role in this server. 15 | #[openapi(tag = "Server Permissions")] 16 | #[put("//permissions/default", data = "", rank = 1)] 17 | pub async fn set_default_permissions( 18 | db: &State, 19 | user: User, 20 | target: Reference, 21 | data: Json, 22 | ) -> Result> { 23 | let data = data.into_inner(); 24 | 25 | let mut server = target.as_server(db).await?; 26 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 27 | let permissions = calculate_server_permissions(&mut query).await; 28 | 29 | permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; 30 | 31 | // Ensure we have permissions to grant these permissions forwards 32 | permissions 33 | .throw_permission_override( 34 | None, 35 | &Override { 36 | allow: data.permissions, 37 | deny: 0, 38 | }, 39 | ) 40 | .await?; 41 | 42 | server 43 | .update( 44 | db, 45 | PartialServer { 46 | default_permissions: Some(data.permissions as i64), 47 | ..Default::default() 48 | }, 49 | vec![], 50 | ) 51 | .await?; 52 | 53 | Ok(Json(server.into())) 54 | } 55 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/roles_delete.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, User, 6 | }; 7 | use upryzing_permissions::{calculate_server_permissions, ChannelPermission}; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Delete Role 11 | /// 12 | /// Delete a server role by its id. 13 | #[openapi(tag = "Server Permissions")] 14 | #[delete("//roles/")] 15 | pub async fn delete( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | role_id: String, 20 | ) -> Result { 21 | let mut server = target.as_server(db).await?; 22 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 23 | calculate_server_permissions(&mut query) 24 | .await 25 | .throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?; 26 | 27 | let member_rank = query.get_member_rank().unwrap_or(i64::MIN); 28 | 29 | if let Some(role) = server.roles.remove(&role_id) { 30 | if role.rank <= member_rank { 31 | return Err(create_error!(NotElevated)); 32 | } 33 | 34 | role.delete(db, &server.id, &role_id) 35 | .await 36 | .map(|_| EmptyResponse) 37 | } else { 38 | Err(create_error!(NotFound)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/roles_fetch.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::PermissionQuery; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Fetch Role 11 | /// 12 | /// Fetch a role by its id. 13 | #[openapi(tag = "Server Permissions")] 14 | #[get("//roles/")] 15 | pub async fn fetch( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | role_id: String, 20 | ) -> Result> { 21 | let mut server = target.as_server(db).await?; 22 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 23 | if !query.are_we_a_member().await { 24 | return Err(create_error!(NotFound)); 25 | } 26 | 27 | let role = server.roles.remove(&role_id); 28 | 29 | if let Some(role) = role { 30 | Ok(Json(role.into())) 31 | } else { 32 | Err(create_error!(NotFound)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/server_ack.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, User, 6 | }; 7 | use upryzing_permissions::PermissionQuery; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Mark Server As Read 11 | /// 12 | /// Mark all channels in a server as read. 13 | #[openapi(tag = "Server Information")] 14 | #[put("//ack")] 15 | pub async fn ack(db: &State, user: User, target: Reference) -> Result { 16 | if user.bot.is_some() { 17 | return Err(create_error!(IsBot)); 18 | } 19 | 20 | let server = target.as_server(db).await?; 21 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 22 | if !query.are_we_a_member().await { 23 | return Err(create_error!(NotFound)); 24 | } 25 | 26 | db.acknowledge_channels(&user.id, &server.channels) 27 | .await 28 | .map(|_| EmptyResponse) 29 | } 30 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/server_create.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::{Database, Member, Server, User}; 2 | use upryzing_models::v0; 3 | use upryzing_result::{create_error, Result}; 4 | 5 | use rocket::serde::json::Json; 6 | use rocket::State; 7 | use validator::Validate; 8 | 9 | /// # Create Space 10 | /// 11 | /// Create a new space. 12 | #[openapi(tag = "Server Information")] 13 | #[post("/create", data = "")] 14 | pub async fn create_server( 15 | db: &State, 16 | user: User, 17 | data: Json, 18 | ) -> Result> { 19 | if user.bot.is_some() { 20 | return Err(create_error!(IsBot)); 21 | } 22 | 23 | let data = data.into_inner(); 24 | data.validate().map_err(|error| { 25 | create_error!(FailedValidation { 26 | error: error.to_string() 27 | }) 28 | })?; 29 | 30 | user.can_acquire_server(db).await?; 31 | 32 | let (server, channels) = Server::create(db, data, &user, true).await?; 33 | let (_, channels) = Member::create(db, &server, &user, Some(channels)).await?; 34 | 35 | Ok(Json(v0::CreateServerLegacyResponse { 36 | server: server.into(), 37 | channels: channels.into_iter().map(|channel| channel.into()).collect(), 38 | })) 39 | } 40 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/server_delete.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use upryzing_database::{util::reference::Reference, Database, RemovalIntention, User}; 3 | use upryzing_models::v0; 4 | use upryzing_result::Result; 5 | 6 | use rocket_empty::EmptyResponse; 7 | 8 | /// # Delete / Leave Server 9 | /// 10 | /// Deletes a server if owner otherwise leaves. 11 | #[openapi(tag = "Server Information")] 12 | #[delete("/?")] 13 | pub async fn delete( 14 | db: &State, 15 | user: User, 16 | target: Reference, 17 | options: v0::OptionsServerDelete, 18 | ) -> Result { 19 | let server = target.as_server(db).await?; 20 | let member = db.fetch_member(&target.id, &user.id).await?; 21 | 22 | if server.owner == user.id { 23 | server.delete(db).await 24 | } else { 25 | member 26 | .remove( 27 | db, 28 | &server, 29 | RemovalIntention::Leave, 30 | options.leave_silently.unwrap_or_default(), 31 | ) 32 | .await 33 | } 34 | .map(|_| EmptyResponse) 35 | } 36 | -------------------------------------------------------------------------------- /crates/delta/src/routes/servers/server_fetch.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission, PermissionQuery}; 8 | use upryzing_result::{create_error, Result}; 9 | 10 | /// # Fetch Server 11 | /// 12 | /// Fetch a server by its id. 13 | #[openapi(tag = "Server Information")] 14 | #[get("/?")] 15 | pub async fn fetch( 16 | db: &State, 17 | user: User, 18 | target: Reference, 19 | options: v0::OptionsFetchServer, 20 | ) -> Result> { 21 | let server = target.as_server(db).await?; 22 | let mut query = DatabasePermissionQuery::new(db, &user).server(&server); 23 | if !query.are_we_a_member().await { 24 | return Err(create_error!(NotFound)); 25 | } 26 | 27 | if let Some(true) = options.include_channels { 28 | let all_channels = db.fetch_channels(&server.channels).await?; 29 | let mut visible_channels: Vec = vec![]; 30 | 31 | for channel in all_channels { 32 | let mut channel_query = query.clone().channel(&channel); 33 | if calculate_channel_permissions(&mut channel_query) 34 | .await 35 | .has_channel_permission(ChannelPermission::ViewChannel) 36 | { 37 | visible_channels.push(channel.into()); 38 | } 39 | } 40 | 41 | Ok(Json(v0::FetchServerResponse::ServerWithChannels { 42 | server: server.into(), 43 | channels: visible_channels, 44 | })) 45 | } else { 46 | Ok(Json(v0::FetchServerResponse::JustServer(server.into()))) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /crates/delta/src/routes/sync/get_settings.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::json::Json; 2 | use rocket::State; 3 | use upryzing_database::{Database, User}; 4 | use upryzing_models::v0; 5 | use upryzing_result::Result; 6 | 7 | /// # Fetch Settings 8 | /// 9 | /// Fetch settings from server filtered by keys. 10 | /// 11 | /// This will return an object with the requested keys, each value is a tuple of `(timestamp, value)`, the value is the previously uploaded data. 12 | #[openapi(tag = "Sync")] 13 | #[post("/settings/fetch", data = "")] 14 | pub async fn fetch( 15 | db: &State, 16 | user: User, 17 | options: Json, 18 | ) -> Result> { 19 | db.fetch_user_settings(&user.id, &options.into_inner().keys) 20 | .await 21 | .map(Json) 22 | } 23 | -------------------------------------------------------------------------------- /crates/delta/src/routes/sync/get_unreads.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::json::Json; 2 | use rocket::State; 3 | use upryzing_database::{Database, User}; 4 | use upryzing_models::v0; 5 | use upryzing_result::Result; 6 | 7 | /// # Fetch Unreads 8 | /// 9 | /// Fetch information about unread state on channels. 10 | #[openapi(tag = "Sync")] 11 | #[get("/unreads")] 12 | pub async fn unreads(db: &State, user: User) -> Result>> { 13 | db.fetch_unreads(&user.id) 14 | .await 15 | .map(|v| v.into_iter().map(|u| u.into()).collect()) 16 | .map(Json) 17 | } 18 | -------------------------------------------------------------------------------- /crates/delta/src/routes/sync/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod get_settings; 5 | mod get_unreads; 6 | mod set_settings; 7 | 8 | pub fn routes() -> (Vec, OpenApi) { 9 | openapi_get_routes_spec![get_settings::fetch, set_settings::set, get_unreads::unreads] 10 | } 11 | -------------------------------------------------------------------------------- /crates/delta/src/routes/sync/set_settings.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::{Database, User, UserSettingsImpl}; 2 | use upryzing_models::v0; 3 | 4 | use chrono::prelude::*; 5 | use rocket::{serde::json::Json, State}; 6 | use rocket_empty::EmptyResponse; 7 | use std::collections::HashMap; 8 | use upryzing_result::Result; 9 | 10 | type Data = HashMap; 11 | 12 | /// # Set Settings 13 | /// 14 | /// Upload data to save to settings. 15 | #[openapi(tag = "Sync")] 16 | #[post("/settings/set?", data = "")] 17 | pub async fn set( 18 | db: &State, 19 | user: User, 20 | data: Json, 21 | options: v0::OptionsSetSettings, 22 | ) -> Result { 23 | let data = data.into_inner(); 24 | let current_time = Utc::now().timestamp_millis(); 25 | let timestamp = if let Some(timestamp) = options.timestamp { 26 | if timestamp > current_time { 27 | current_time 28 | } else { 29 | timestamp 30 | } 31 | } else { 32 | current_time 33 | }; 34 | 35 | let mut settings = HashMap::new(); 36 | for (key, data) in data { 37 | settings.insert(key, (timestamp, data)); 38 | } 39 | 40 | settings.set(db, &user.id).await.map(|_| EmptyResponse) 41 | } 42 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/add_friend.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::json::Json; 2 | use rocket::State; 3 | use upryzing_database::util::reference::Reference; 4 | use upryzing_database::{Database, User, AMQP}; 5 | use upryzing_models::v0; 6 | use upryzing_result::{create_error, Result}; 7 | 8 | /// # Accept Friend Request 9 | /// 10 | /// Accept another user's friend request. 11 | #[openapi(tag = "Relationships")] 12 | #[put("//friend")] 13 | pub async fn add( 14 | db: &State, 15 | amqp: &State, 16 | mut user: User, 17 | target: Reference, 18 | ) -> Result> { 19 | let mut target = target.as_user(db).await?; 20 | 21 | if user.bot.is_some() || target.bot.is_some() { 22 | return Err(create_error!(IsBot)); 23 | } 24 | 25 | user.add_friend(db, amqp, &mut target).await?; 26 | Ok(Json(target.into(db, &user).await)) 27 | } 28 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/avatars/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upryzing/parrot/4b5a7661666152dcf70219ceb03030ee6e1757ea/crates/delta/src/routes/users/avatars/1.png -------------------------------------------------------------------------------- /crates/delta/src/routes/users/avatars/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upryzing/parrot/4b5a7661666152dcf70219ceb03030ee6e1757ea/crates/delta/src/routes/users/avatars/2.png -------------------------------------------------------------------------------- /crates/delta/src/routes/users/avatars/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upryzing/parrot/4b5a7661666152dcf70219ceb03030ee6e1757ea/crates/delta/src/routes/users/avatars/3.png -------------------------------------------------------------------------------- /crates/delta/src/routes/users/avatars/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upryzing/parrot/4b5a7661666152dcf70219ceb03030ee6e1757ea/crates/delta/src/routes/users/avatars/4.png -------------------------------------------------------------------------------- /crates/delta/src/routes/users/avatars/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upryzing/parrot/4b5a7661666152dcf70219ceb03030ee6e1757ea/crates/delta/src/routes/users/avatars/5.png -------------------------------------------------------------------------------- /crates/delta/src/routes/users/avatars/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upryzing/parrot/4b5a7661666152dcf70219ceb03030ee6e1757ea/crates/delta/src/routes/users/avatars/6.png -------------------------------------------------------------------------------- /crates/delta/src/routes/users/avatars/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upryzing/parrot/4b5a7661666152dcf70219ceb03030ee6e1757ea/crates/delta/src/routes/users/avatars/7.png -------------------------------------------------------------------------------- /crates/delta/src/routes/users/block_user.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::json::Json; 2 | use rocket::State; 3 | use upryzing_database::util::reference::Reference; 4 | use upryzing_database::{Database, User}; 5 | use upryzing_models::v0; 6 | use upryzing_result::Result; 7 | 8 | /// # Block User 9 | /// 10 | /// Block another user by their id. 11 | #[openapi(tag = "Relationships")] 12 | #[put("//block")] 13 | pub async fn block( 14 | db: &State, 15 | mut user: User, 16 | target: Reference, 17 | ) -> Result> { 18 | let mut target = target.as_user(db).await?; 19 | 20 | user.block_user(db, &mut target).await?; 21 | Ok(Json(target.into(db, &user).await)) 22 | } 23 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/change_username.rs: -------------------------------------------------------------------------------- 1 | use authifier::models::Account; 2 | use once_cell::sync::Lazy; 3 | use regex::Regex; 4 | use rocket::{serde::json::Json, State}; 5 | use serde::{Deserialize, Serialize}; 6 | use upryzing_database::{Database, User}; 7 | use upryzing_models::v0; 8 | use upryzing_result::{create_error, Result}; 9 | use validator::Validate; 10 | 11 | /// Regex for valid usernames 12 | /// 13 | /// Block zero width space 14 | /// Block lookalike characters 15 | pub static RE_USERNAME: Lazy = Lazy::new(|| Regex::new(r"^(\p{L}|[\d_.-])+$").unwrap()); 16 | 17 | /// # Username Information 18 | #[derive(Validate, Serialize, Deserialize, JsonSchema)] 19 | pub struct DataChangeUsername { 20 | /// New username 21 | #[validate(length(min = 2, max = 32), regex = "RE_USERNAME")] 22 | username: String, 23 | /// Current account password 24 | #[validate(length(min = 8, max = 1024))] 25 | password: String, 26 | } 27 | 28 | /// # Change Username 29 | /// 30 | /// Change your username. 31 | #[openapi(tag = "User Information")] 32 | #[patch("/@me/username", data = "")] 33 | pub async fn change_username( 34 | db: &State, 35 | account: Account, 36 | mut user: User, 37 | data: Json, 38 | ) -> Result> { 39 | let data = data.into_inner(); 40 | data.validate().map_err(|error| { 41 | create_error!(FailedValidation { 42 | error: error.to_string() 43 | }) 44 | })?; 45 | 46 | account 47 | .verify_password(&data.password) 48 | .map_err(|_| create_error!(InvalidCredentials))?; 49 | 50 | user.update_username(db, data.username).await?; 51 | Ok(Json(user.into(db, None).await)) 52 | } 53 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/fetch_dms.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{Database, User}; 3 | use upryzing_models::v0; 4 | use upryzing_result::Result; 5 | 6 | /// # Fetch Direct Message Channels 7 | /// 8 | /// This fetches your direct messages, including any DM and group DM conversations. 9 | #[openapi(tag = "Direct Messaging")] 10 | #[get("/dms")] 11 | pub async fn direct_messages(db: &State, user: User) -> Result>> { 12 | db.find_direct_messages(&user.id) 13 | .await 14 | .map(|v| v.into_iter().map(Into::into).collect()) 15 | .map(Json) 16 | } 17 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/fetch_profile.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::{calculate_user_permissions, UserPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Fetch User Profile 11 | /// 12 | /// Retrieve a user's profile data. 13 | /// 14 | /// Will fail if you do not have permission to access the other user's profile. 15 | #[openapi(tag = "User Information")] 16 | #[get("//profile")] 17 | pub async fn profile( 18 | db: &State, 19 | user: User, 20 | target: Reference, 21 | ) -> Result> { 22 | if user.id == target.id { 23 | return Ok(Json(user.profile.map(Into::into).unwrap_or_default())); 24 | } 25 | 26 | let target = target.as_user(db).await?; 27 | 28 | let mut query = DatabasePermissionQuery::new(db, &user).user(&target); 29 | calculate_user_permissions(&mut query) 30 | .await 31 | .throw_if_lacking_user_permission(UserPermission::ViewProfile)?; 32 | 33 | Ok(Json(target.profile.map(Into::into).unwrap_or_default())) 34 | } 35 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/fetch_self.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::json::Json; 2 | use upryzing_database::User; 3 | use upryzing_models::v0; 4 | use upryzing_result::Result; 5 | 6 | /// # Fetch Self 7 | /// 8 | /// Retrieve your user information. 9 | #[openapi(tag = "User Information")] 10 | #[get("/@me")] 11 | pub async fn fetch(user: User) -> Result> { 12 | Ok(Json(user.into_self(false).await)) 13 | } 14 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/fetch_user.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::{ 2 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 3 | Database, User, 4 | }; 5 | use upryzing_models::v0; 6 | 7 | use rocket::{serde::json::Json, State}; 8 | use upryzing_permissions::{calculate_user_permissions, UserPermission}; 9 | use upryzing_result::Result; 10 | 11 | /// # Fetch User 12 | /// 13 | /// Retrieve a user's information. 14 | #[openapi(tag = "User Information")] 15 | #[get("/")] 16 | pub async fn fetch(db: &State, user: User, target: Reference) -> Result> { 17 | if user.id == target.id { 18 | return Ok(Json(user.into_self(false).await)); 19 | } 20 | 21 | let target = target.as_user(db).await?; 22 | 23 | let mut query = DatabasePermissionQuery::new(db, &user).user(&target); 24 | calculate_user_permissions(&mut query) 25 | .await 26 | .throw_if_lacking_user_permission(UserPermission::Access)?; 27 | 28 | Ok(Json(target.into(db, &user).await)) 29 | } 30 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/fetch_user_flags.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{util::reference::Reference, Database}; 3 | use upryzing_models::v0; 4 | use upryzing_result::Result; 5 | 6 | /// # Fetch User Flags 7 | /// 8 | /// Retrieve a user's flags. 9 | #[openapi(tag = "User Information")] 10 | #[get("//flags")] 11 | pub async fn fetch_user_flags( 12 | db: &State, 13 | target: Reference, 14 | ) -> Result> { 15 | let flags = if let Ok(target) = target.as_user(db).await { 16 | target.flags.unwrap_or_default() 17 | } else { 18 | 0 19 | }; 20 | 21 | Ok(Json(v0::FlagResponse { flags })) 22 | } 23 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/find_mutual.rs: -------------------------------------------------------------------------------- 1 | use upryzing_database::util::permissions::DatabasePermissionQuery; 2 | use upryzing_database::util::reference::Reference; 3 | use upryzing_database::{Database, User}; 4 | use upryzing_models::v0; 5 | 6 | use rocket::serde::json::Json; 7 | use rocket::State; 8 | use upryzing_permissions::{calculate_user_permissions, UserPermission}; 9 | use upryzing_result::{create_error, Result}; 10 | 11 | /// # Fetch Mutual Friends And Servers 12 | /// 13 | /// Retrieve a list of mutual friends and servers with another user. 14 | #[openapi(tag = "Relationships")] 15 | #[get("//mutual")] 16 | pub async fn mutual( 17 | db: &State, 18 | user: User, 19 | target: Reference, 20 | ) -> Result> { 21 | if target.id == user.id { 22 | return Err(create_error!(InvalidOperation)); 23 | } 24 | 25 | let target = target.as_user(db).await?; 26 | 27 | let mut query = DatabasePermissionQuery::new(db, &user).user(&target); 28 | calculate_user_permissions(&mut query) 29 | .await 30 | .throw_if_lacking_user_permission(UserPermission::ViewProfile)?; 31 | 32 | Ok(Json(v0::MutualResponse { 33 | users: db.fetch_mutual_user_ids(&user.id, &target.id).await?, 34 | servers: db.fetch_mutual_server_ids(&user.id, &target.id).await?, 35 | })) 36 | } 37 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod add_friend; 5 | mod block_user; 6 | mod change_username; 7 | mod edit_user; 8 | mod fetch_dms; 9 | mod fetch_profile; 10 | mod fetch_self; 11 | mod fetch_user; 12 | mod fetch_user_flags; 13 | mod find_mutual; 14 | mod get_default_avatar; 15 | mod open_dm; 16 | mod remove_friend; 17 | mod send_friend_request; 18 | mod unblock_user; 19 | 20 | pub fn routes() -> (Vec, OpenApi) { 21 | openapi_get_routes_spec![ 22 | // User Information 23 | fetch_self::fetch, 24 | fetch_user::fetch, 25 | fetch_user_flags::fetch_user_flags, 26 | edit_user::edit, 27 | change_username::change_username, 28 | get_default_avatar::default_avatar, 29 | fetch_profile::profile, 30 | // Direct Messaging 31 | fetch_dms::direct_messages, 32 | open_dm::open_dm, 33 | // Relationships 34 | find_mutual::mutual, 35 | add_friend::add, 36 | remove_friend::remove, 37 | block_user::block, 38 | unblock_user::unblock, 39 | send_friend_request::send_friend_request, 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/open_dm.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Channel, Database, User, 5 | }; 6 | use upryzing_models::v0; 7 | use upryzing_permissions::{calculate_user_permissions, UserPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Open Direct Message 11 | /// 12 | /// Open a DM with another user. 13 | /// 14 | /// If the target is oneself, a saved messages channel is returned. 15 | #[openapi(tag = "Direct Messaging")] 16 | #[get("//dm")] 17 | pub async fn open_dm( 18 | db: &State, 19 | user: User, 20 | target: Reference, 21 | ) -> Result> { 22 | let target = target.as_user(db).await?; 23 | 24 | let mut query = DatabasePermissionQuery::new(db, &user).user(&target); 25 | calculate_user_permissions(&mut query) 26 | .await 27 | .throw_if_lacking_user_permission(UserPermission::SendMessage)?; 28 | 29 | Channel::create_dm(db, &user, &target) 30 | .await 31 | .map(Into::into) 32 | .map(Json) 33 | } 34 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/remove_friend.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::json::Json; 2 | use rocket::State; 3 | use upryzing_database::util::reference::Reference; 4 | use upryzing_database::{Database, User}; 5 | use upryzing_models::v0; 6 | use upryzing_result::{create_error, Result}; 7 | 8 | /// # Deny Friend Request / Remove Friend 9 | /// 10 | /// Denies another user's friend request or removes an existing friend. 11 | #[openapi(tag = "Relationships")] 12 | #[delete("//friend")] 13 | pub async fn remove( 14 | db: &State, 15 | mut user: User, 16 | target: Reference, 17 | ) -> Result> { 18 | let mut target = target.as_user(db).await?; 19 | 20 | if user.bot.is_some() || target.bot.is_some() { 21 | return Err(create_error!(IsBot)); 22 | } 23 | 24 | user.remove_friend(db, &mut target).await?; 25 | Ok(Json(target.into(db, &user).await)) 26 | } 27 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/send_friend_request.rs: -------------------------------------------------------------------------------- 1 | // use upryzing_database::util::reference::Reference; 2 | use rocket::serde::json::Json; 3 | use rocket::State; 4 | use upryzing_database::{Database, User, AMQP}; 5 | use upryzing_models::v0; 6 | use upryzing_result::{create_error, Result}; 7 | 8 | /// # Send Friend Request 9 | /// 10 | /// Send a friend request to another user. 11 | #[openapi(tag = "Relationships")] 12 | #[post("/friend", data = "")] 13 | pub async fn send_friend_request( 14 | db: &State, 15 | amqp: &State, 16 | mut user: User, 17 | data: Json, 18 | ) -> Result> { 19 | if let Some((username, discriminator)) = data.username.split_once('#') { 20 | let mut target = db.fetch_user_by_username(username, discriminator).await?; 21 | 22 | if user.bot.is_some() || target.bot.is_some() { 23 | return Err(create_error!(IsBot)); 24 | } 25 | 26 | user.add_friend(db, amqp, &mut target).await?; 27 | Ok(Json(target.into(db, &user).await)) 28 | } else { 29 | Err(create_error!(InvalidProperty)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /crates/delta/src/routes/users/unblock_user.rs: -------------------------------------------------------------------------------- 1 | use rocket::serde::json::Json; 2 | use rocket::State; 3 | use upryzing_database::util::reference::Reference; 4 | use upryzing_database::{Database, User}; 5 | use upryzing_models::v0; 6 | use upryzing_result::Result; 7 | 8 | /// # Unblock User 9 | /// 10 | /// Unblock another user by their id. 11 | #[openapi(tag = "Relationships")] 12 | #[delete("//block")] 13 | pub async fn unblock( 14 | db: &State, 15 | mut user: User, 16 | target: Reference, 17 | ) -> Result> { 18 | let mut target = target.as_user(db).await?; 19 | 20 | user.unblock_user(db, &mut target).await?; 21 | Ok(Json(target.into(db, &user).await)) 22 | } 23 | -------------------------------------------------------------------------------- /crates/delta/src/routes/webhooks/mod.rs: -------------------------------------------------------------------------------- 1 | use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; 2 | use rocket::Route; 3 | 4 | mod webhook_delete; 5 | mod webhook_delete_token; 6 | mod webhook_edit; 7 | mod webhook_edit_token; 8 | mod webhook_execute; 9 | mod webhook_execute_github; 10 | mod webhook_fetch; 11 | mod webhook_fetch_token; 12 | 13 | pub fn routes() -> (Vec, OpenApi) { 14 | openapi_get_routes_spec![ 15 | webhook_delete_token::webhook_delete_token, 16 | webhook_delete::webhook_delete, 17 | webhook_edit_token::webhook_edit_token, 18 | webhook_edit::webhook_edit, 19 | webhook_execute_github::webhook_execute_github, 20 | webhook_execute::webhook_execute, 21 | webhook_fetch_token::webhook_fetch_token, 22 | webhook_fetch::webhook_fetch, 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /crates/delta/src/routes/webhooks/webhook_delete.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{ 4 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 5 | Database, User, 6 | }; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Deletes a webhook 11 | /// 12 | /// Deletes a webhook 13 | #[openapi(tag = "Webhooks")] 14 | #[delete("/")] 15 | pub async fn webhook_delete( 16 | db: &State, 17 | user: User, 18 | webhook_id: Reference, 19 | ) -> Result { 20 | let webhook = webhook_id.as_webhook(db).await?; 21 | let channel = db.fetch_channel(&webhook.channel_id).await?; 22 | 23 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 24 | calculate_channel_permissions(&mut query) 25 | .await 26 | .throw_if_lacking_channel_permission(ChannelPermission::ManageWebhooks)?; 27 | 28 | webhook.delete(db).await.map(|_| EmptyResponse) 29 | } 30 | -------------------------------------------------------------------------------- /crates/delta/src/routes/webhooks/webhook_delete_token.rs: -------------------------------------------------------------------------------- 1 | use rocket::State; 2 | use rocket_empty::EmptyResponse; 3 | use upryzing_database::{util::reference::Reference, Database}; 4 | use upryzing_result::Result; 5 | 6 | /// # Deletes a webhook 7 | /// 8 | /// Deletes a webhook with a token 9 | #[openapi(tag = "Webhooks")] 10 | #[delete("//")] 11 | pub async fn webhook_delete_token( 12 | db: &State, 13 | webhook_id: Reference, 14 | token: String, 15 | ) -> Result { 16 | let webhook = webhook_id.as_webhook(db).await?; 17 | webhook.assert_token(&token)?; 18 | webhook.delete(db).await.map(|_| EmptyResponse) 19 | } 20 | -------------------------------------------------------------------------------- /crates/delta/src/routes/webhooks/webhook_edit_token.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::util::reference::Reference; 3 | use upryzing_database::{Database, File, PartialWebhook}; 4 | use upryzing_models::v0::{DataEditWebhook, Webhook}; 5 | use upryzing_models::validator::Validate; 6 | use upryzing_result::{create_error, Result}; 7 | 8 | /// # Edits a webhook 9 | /// 10 | /// Edits a webhook with a token 11 | #[openapi(tag = "Webhooks")] 12 | #[patch("//", data = "")] 13 | pub async fn webhook_edit_token( 14 | db: &State, 15 | webhook_id: Reference, 16 | token: String, 17 | data: Json, 18 | ) -> Result> { 19 | let data = data.into_inner(); 20 | data.validate().map_err(|error| { 21 | create_error!(FailedValidation { 22 | error: error.to_string() 23 | }) 24 | })?; 25 | 26 | let mut webhook = webhook_id.as_webhook(db).await?; 27 | webhook.assert_token(&token)?; 28 | 29 | if data.name.is_none() && data.avatar.is_none() && data.remove.is_empty() { 30 | return Ok(Json(webhook.into())); 31 | }; 32 | 33 | let DataEditWebhook { 34 | name, 35 | avatar, 36 | permissions, 37 | remove, 38 | } = data; 39 | 40 | let mut partial = PartialWebhook { 41 | name, 42 | permissions, 43 | ..Default::default() 44 | }; 45 | 46 | if let Some(avatar) = avatar { 47 | let file = File::use_webhook_avatar(db, &avatar, &webhook.id, &webhook.creator_id).await?; 48 | partial.avatar = Some(file) 49 | } 50 | 51 | webhook 52 | .update(db, partial, remove.into_iter().map(|v| v.into()).collect()) 53 | .await?; 54 | 55 | Ok(Json(webhook.into())) 56 | } 57 | -------------------------------------------------------------------------------- /crates/delta/src/routes/webhooks/webhook_fetch.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{ 3 | util::{permissions::DatabasePermissionQuery, reference::Reference}, 4 | Database, User, 5 | }; 6 | use upryzing_models::v0::{ResponseWebhook, Webhook}; 7 | use upryzing_permissions::{calculate_channel_permissions, ChannelPermission}; 8 | use upryzing_result::Result; 9 | 10 | /// # Gets a webhook 11 | /// 12 | /// Gets a webhook 13 | #[openapi(tag = "Webhooks")] 14 | #[get("/")] 15 | pub async fn webhook_fetch( 16 | db: &State, 17 | webhook_id: Reference, 18 | user: User, 19 | ) -> Result> { 20 | let webhook = webhook_id.as_webhook(db).await?; 21 | let channel = db.fetch_channel(&webhook.channel_id).await?; 22 | 23 | let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); 24 | calculate_channel_permissions(&mut query) 25 | .await 26 | .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; 27 | 28 | Ok(Json(std::convert::Into::::into(webhook).into())) 29 | } 30 | -------------------------------------------------------------------------------- /crates/delta/src/routes/webhooks/webhook_fetch_token.rs: -------------------------------------------------------------------------------- 1 | use rocket::{serde::json::Json, State}; 2 | use upryzing_database::{util::reference::Reference, Database}; 3 | use upryzing_models::v0::Webhook; 4 | use upryzing_result::Result; 5 | 6 | /// # Gets a webhook 7 | /// 8 | /// Gets a webhook with a token 9 | #[openapi(tag = "Webhooks")] 10 | #[get("//")] 11 | pub async fn webhook_fetch_token( 12 | db: &State, 13 | webhook_id: Reference, 14 | token: String, 15 | ) -> Result> { 16 | let webhook = webhook_id.as_webhook(db).await?; 17 | webhook.assert_token(&token)?; 18 | Ok(Json(webhook.into())) 19 | } 20 | -------------------------------------------------------------------------------- /crates/delta/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ratelimiter; 2 | pub mod test; 3 | -------------------------------------------------------------------------------- /crates/services/dove/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-dove" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # Utility 8 | mime = "0.3.17" 9 | regex = "1.11.0" 10 | tempfile = "3.13.0" 11 | lazy_static = "1.5.0" 12 | moka = { version = "0.12.8", features = ["future"] } 13 | 14 | # Web scraping 15 | scraper = "0.20.0" 16 | encoding_rs = "0.8.34" 17 | 18 | # Serialisation 19 | serde = { version = "1.0", features = ["derive"] } 20 | serde_json = "1.0.68" 21 | 22 | # Async runtime 23 | async-recursion = "1.1.1" 24 | tokio = { version = "1.0", features = ["full"] } 25 | 26 | # Web requests 27 | reqwest = { version = "0.12", features = ["json"] } 28 | 29 | # Logging 30 | tracing = "0.1" 31 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 32 | 33 | # Core crates 34 | upryzing-config = { version = "0.1.0", path = "../../core/config" } 35 | upryzing-models = { version = "0.1.0", path = "../../core/models" } 36 | upryzing-result = { version = "0.1.0", path = "../../core/result", features = [ 37 | "utoipa", 38 | "axum", 39 | ] } 40 | upryzing-files = { version = "0.1.0", path = "../../core/files" } 41 | 42 | # Axum / web server 43 | axum = { version = "0.7.5" } 44 | axum-extra = { version = "0.9", features = ["typed-header"] } 45 | 46 | # OpenAPI & documentation generation 47 | utoipa-scalar = { version = "0.1.0", features = ["axum"] } 48 | utoipa = { version = "4.2.3", features = ["axum_extras", "ulid"] } 49 | -------------------------------------------------------------------------------- /crates/services/dove/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM ghcr.io/upryzing/base:latest AS builder 3 | 4 | # Bundle Stage 5 | FROM gcr.io/distroless/cc-debian12:nonroot 6 | COPY --from=builder /home/rust/src/target/release/upryzing-dove ./ 7 | COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/local/bin/ 8 | COPY --from=mwader/static-ffmpeg:7.0.2 /ffprobe /usr/local/bin/ 9 | 10 | EXPOSE 14705 11 | USER nonroot 12 | CMD ["./upryzing-dove"] 13 | -------------------------------------------------------------------------------- /crates/services/pigeon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "upryzing-pigeon" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # ID generation 8 | ulid = "1.1.3" 9 | nanoid = "0.4.0" 10 | 11 | # Media processing 12 | webp = "0.3.0" 13 | sha2 = "0.10.8" 14 | jxl-oxide = "0.8.1" 15 | kamadak-exif = "0.5.4" 16 | # revolt_little_exif = "0.5.1" 17 | image = { version = "0.25.2" } # avif encode requires dav1d system library: features = ["avif-native"] 18 | 19 | # File processing 20 | revolt_clamav-client = { version = "0.1.5" } 21 | simdutf8 = { version = "0.1.4", features = ["aarch64_neon"] } 22 | 23 | # Content type processing 24 | infer = "0.16.0" 25 | ffprobe = "0.4.0" 26 | imagesize = "0.13.0" 27 | 28 | # Utility 29 | lazy_static = "1.5.0" 30 | moka = { version = "0.12.8", features = ["future"] } 31 | 32 | # Serialisation 33 | strum_macros = "0.26.4" 34 | serde_json = "1.0.68" 35 | serde = { version = "1.0", features = ["derive"] } 36 | 37 | # Async runtime 38 | tokio = { version = "1.0", features = ["full"] } 39 | 40 | # Logging 41 | tracing = "0.1" 42 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 43 | 44 | # Core crates 45 | upryzing-files = { version = "0.1.0", path = "../../core/files" } 46 | upryzing-config = { version = "0.1.0", path = "../../core/config" } 47 | upryzing-database = { version = "0.1.0", path = "../../core/database", features = [ 48 | "axum-impl", 49 | ] } 50 | upryzing-result = { version = "0.1.0", path = "../../core/result", features = [ 51 | "utoipa", 52 | "axum", 53 | ] } 54 | 55 | # Axum / web server 56 | tempfile = "3.12.0" 57 | axum-macros = "0.4.1" 58 | axum_typed_multipart = "0.12.1" 59 | axum = { version = "0.7.5", features = ["multipart"] } 60 | tower-http = { version = "0.5.2", features = ["cors"] } 61 | 62 | # OpenAPI & documentation generation 63 | utoipa-scalar = { version = "0.1.0", features = ["axum"] } 64 | utoipa = { version = "4.2.3", features = ["axum_extras", "ulid"] } 65 | -------------------------------------------------------------------------------- /crates/services/pigeon/Dockerfile: -------------------------------------------------------------------------------- 1 | # Build Stage 2 | FROM ghcr.io/upryzing/base:latest AS builder 3 | 4 | # Bundle Stage 5 | FROM gcr.io/distroless/cc-debian12:nonroot 6 | COPY --from=builder /home/rust/src/target/release/upryzing-pigeon ./ 7 | COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/local/bin/ 8 | COPY --from=mwader/static-ffmpeg:7.0.2 /ffprobe /usr/local/bin/ 9 | 10 | EXPOSE 14704 11 | USER nonroot 12 | CMD ["./upryzing-pigeon"] 13 | -------------------------------------------------------------------------------- /crates/services/pigeon/src/clamav.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use upryzing_config::{config, report_internal_error}; 4 | use upryzing_result::Result; 5 | 6 | /// Initialise ClamAV 7 | pub async fn init() { 8 | let config = config().await; 9 | 10 | if !config.files.clamd_host.is_empty() { 11 | tracing::info!("Waiting for clamd to be ready..."); 12 | 13 | loop { 14 | let clamd_available = 15 | match revolt_clamav_client::ping_tcp(config.files.clamd_host.clone()) { 16 | Ok(ping_response) => ping_response == b"PONG\0", 17 | Err(_) => false, 18 | }; 19 | 20 | if clamd_available { 21 | tracing::info!("clamd is ready, virus protection enabled!"); 22 | break; 23 | } else { 24 | tracing::error!( 25 | "Could not ping clamd host at {}, retrying in 10 seconds...", 26 | config.files.clamd_host 27 | ); 28 | 29 | std::thread::sleep(Duration::from_secs(10)); 30 | } 31 | } 32 | } 33 | } 34 | 35 | /// Scan for malware 36 | pub async fn is_malware(buf: &[u8]) -> Result { 37 | let config = config().await; 38 | if config.files.clamd_host.is_empty() { 39 | Ok(false) 40 | } else { 41 | let scan_response = report_internal_error!(revolt_clamav_client::scan_buffer_tcp( 42 | buf, 43 | config.files.clamd_host, 44 | None 45 | ))?; 46 | 47 | report_internal_error!(revolt_clamav_client::clean(&scan_response)).map(|v| !v) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/services/pigeon/src/mime_type.rs: -------------------------------------------------------------------------------- 1 | use tempfile::NamedTempFile; 2 | 3 | /// Determine the mime type of the given temporary file and filename 4 | pub fn determine_mime_type(f: &mut NamedTempFile, buf: &[u8], file_name: &str) -> &'static str { 5 | // Force certain extensions into particular mime types 6 | if file_name.to_lowercase().ends_with(".apk") { 7 | return "application/vnd.android.package-archive"; 8 | } else if file_name.to_lowercase().ends_with(".exe") { 9 | return "application/vnd.microsoft.portable-executable"; 10 | } 11 | 12 | // Use magic signatures to determine mime type 13 | let kind = infer::get_from_path(f.path()).expect("file read successfully"); 14 | let mime_type = if let Some(kind) = kind { 15 | kind.mime_type() 16 | } else { 17 | "application/octet-stream" 18 | }; 19 | 20 | // See if the file is actually just plain Unicode/ASCII text 21 | if mime_type == "application/octet-stream" && simdutf8::basic::from_utf8(buf).is_ok() { 22 | if file_name.to_lowercase().ends_with(".svg") { 23 | return "image/svg+xml"; 24 | } else { 25 | return "plain/text"; 26 | } 27 | } 28 | 29 | mime_type 30 | } 31 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | # Pinned nixpkgs, deterministic. Last updated: 28-07-2024. 3 | pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/9b34ca580417e1ebc56c4df57d8b387dad686665.tar.gz")) {}; 4 | 5 | # Rolling updates, not deterministic. 6 | # pkgs = import (fetchTarball("channel:nixpkgs-unstable")) {}; 7 | in pkgs.mkShell { 8 | name = "upryzingEnv"; 9 | 10 | # LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ 11 | # pkgs.gcc-unwrapped 12 | # pkgs.zlib 13 | # pkgs.glib 14 | # pkgs.libGL 15 | # ]; 16 | 17 | buildInputs = [ 18 | # Tools 19 | pkgs.git 20 | 21 | # Database 22 | # pkgs.mongodb 23 | 24 | # Cargo 25 | pkgs.cargo 26 | pkgs.cargo-nextest 27 | 28 | # Rust 29 | pkgs.rustc 30 | pkgs.clippy 31 | pkgs.rustfmt 32 | pkgs.pkg-config 33 | pkgs.openssl.dev 34 | 35 | # mdbook 36 | pkgs.mdbook 37 | ]; 38 | 39 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; 40 | } 41 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /doc/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = [] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Revolt Backend" 7 | -------------------------------------------------------------------------------- /doc/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./hello.md) 4 | - [Project Structure]() 5 | - [Creating new API features](./new_features.md) 6 | - [Testing]() 7 | - [Writing a new database test]() 8 | - [Writing a new API test]() 9 | -------------------------------------------------------------------------------- /doc/src/hello.md: -------------------------------------------------------------------------------- 1 | # Revolt Backend 2 | 3 | Welcome to the developer documentation for the Revolt backend. 4 | 5 | This is very much incomplete and needs more work! 6 | -------------------------------------------------------------------------------- /doc/src/new_features.md: -------------------------------------------------------------------------------- 1 | # New API features 2 | 3 | New API features must be documented where appropriate, this document aims to cover everywhere you need to update for new features. 4 | 5 | Before writing new API features, generally a good idea to: 6 | 7 | - Consult with other developers in the [Revolt Developers space](https://rvlt.gg/API) 8 | - If it's a relatively big feature, also [write an RFC](https://github.com/revoltchat/rfcs) 9 | 10 | When your feature is ready to release, ensure to: 11 | 12 | - Update backend documentation (what you're reading now!) if applicable 13 | - Update the [developers documentation](https://github.com/revoltchat/wiki) if applicable 14 | - Update the Feature Matrix (or ask someone that can to do so) 15 | - Ensure it is properly listed in the backend release changelog 16 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | publish: 2 | cargo publish --package upryzing-config 3 | cargo publish --package upryzing-result 4 | cargo publish --package upryzing-files 5 | cargo publish --package upryzing-permissions 6 | cargo publish --package upryzing-models 7 | cargo publish --package upryzing-presence 8 | cargo publish --package upryzing-database 9 | 10 | patch: 11 | cargo release version patch --execute 12 | 13 | minor: 14 | cargo release version minor --execute 15 | 16 | major: 17 | cargo release version major --execute 18 | 19 | release: 20 | scripts/try-tag-and-release.sh 21 | -------------------------------------------------------------------------------- /scripts/publish-debug-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # fail asap 4 | set -e 5 | 6 | # Check if an argument was provided 7 | if [ $# -eq 0 ]; then 8 | echo "No arguments provided" 9 | echo "Usage: scripts/publish-debug-image.sh 20230826-1 true" 10 | echo "" 11 | echo "Last argument specifies whether we should have a debug build as opposed to release build." 12 | exit 1 13 | fi 14 | 15 | DEBUG=$2 16 | if [ "$DEBUG" = "true" ]; then 17 | echo "[profile.release]" >> Cargo.toml 18 | echo "debug = true" >> Cargo.toml 19 | fi 20 | 21 | TAG=$1-debug 22 | echo "Building images, will tag for ghcr.io with $TAG!" 23 | docker build -t ghcr.io/upryzing/base:latest -f Dockerfile.useCurrentArch . 24 | docker build -t ghcr.io/upryzing/server:$TAG - < crates/delta/Dockerfile 25 | docker build -t ghcr.io/upryzing/bonfire:$TAG - < crates/bonfire/Dockerfile 26 | docker build -t ghcr.io/upryzing/pigeon:$TAG - < crates/services/pigeon/Dockerfile 27 | docker build -t ghcr.io/upryzing/dove:$TAG - < crates/services/dove/Dockerfile 28 | 29 | if [ "$DEBUG" = "true" ]; then 30 | git restore Cargo.toml 31 | fi 32 | 33 | docker push ghcr.io/upryzing/server:$TAG 34 | docker push ghcr.io/upryzing/bonfire:$TAG 35 | docker push ghcr.io/upryzing/pigeon:$TAG 36 | docker push ghcr.io/upryzing/dove:$TAG 37 | -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | cargo build \ 4 | --bin upryzing-delta \ 5 | --bin upryzing-bonfire \ 6 | --bin upryzing-pigeon \ 7 | --bin upryzing-dove 8 | 9 | trap 'pkill -f upryzing-' SIGINT 10 | cargo run --bin upryzing-delta & 11 | cargo run --bin upryzing-bonfire & 12 | cargo run --bin upryzing-pigeon & 13 | cargo run --bin upryzing-dove 14 | -------------------------------------------------------------------------------- /scripts/try-tag-and-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | date=$(date +'%Y%m%d') 3 | incr=1 4 | 5 | while [ $(git tag -l "$date-$incr") ]; do 6 | incr=$((incr+1)) 7 | done 8 | 9 | tag=$date-$incr 10 | echo About to tag and push $tag in 3 seconds... 11 | sleep 3s 12 | 13 | git tag $tag 14 | git push --atomic origin $(git rev-parse --abbrev-ref HEAD) $tag 15 | --------------------------------------------------------------------------------