forked from jmug/stoatchat
Compare commits
63
Commits
20240623-1
...
20240830-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
730039eda6 | ||
|
|
79e1388000 | ||
|
|
5c8ece0727 | ||
|
|
ab6b92aa2a | ||
|
|
239710951a | ||
|
|
28960979ec | ||
|
|
1938ebc3fa | ||
|
|
1fb4032d9e | ||
|
|
ae2194cce8 | ||
|
|
949a0cf649 | ||
|
|
f6c57b23b4 | ||
|
|
12ae781621 | ||
|
|
a52d610e6c | ||
|
|
f513a9cb5e | ||
|
|
535f01604b | ||
|
|
f8ec6ba5ff | ||
|
|
b8fdc07508 | ||
|
|
5ce7530206 | ||
|
|
b36bac3225 | ||
|
|
beef06ebe3 | ||
|
|
4fc46f765b | ||
|
|
b45ae2cd4d | ||
|
|
0a8089787b | ||
|
|
475fa6f2e6 | ||
|
|
96bf73cb57 | ||
|
|
87a9fb79f2 | ||
|
|
c50435d499 | ||
|
|
d2f4823ceb | ||
|
|
39230c559a | ||
|
|
32d1d5df2e | ||
|
|
7547fbe245 | ||
|
|
9f13cb6f47 | ||
|
|
d94571fbf9 | ||
|
|
389ecc0e5c | ||
|
|
412f4a99d7 | ||
|
|
caa0795d07 | ||
|
|
5c82343bb3 | ||
|
|
e17af1c064 | ||
|
|
a9e309395e | ||
|
|
8468ce19cb | ||
|
|
27f15f7b02 | ||
|
|
78b9fa168b | ||
|
|
e290d168ac | ||
|
|
78cd89ec32 | ||
|
|
f9d9059e73 | ||
|
|
0954d71f51 | ||
|
|
9ea2bd9f2f | ||
|
|
d6bcb844db | ||
|
|
ce20e689cc | ||
|
|
d9cfc9d0e1 | ||
|
|
64a07d09f4 | ||
|
|
00e881799f | ||
|
|
fa598dd6f8 | ||
|
|
ac4e2cb10b | ||
|
|
ec578ab0ef | ||
|
|
ea6ba59841 | ||
|
|
f903f716e3 | ||
|
|
c5f4b94aa5 | ||
|
|
1d5dae4751 | ||
|
|
ac20b6bc99 | ||
|
|
6ec8007e4e | ||
|
|
e5eea267cf | ||
|
|
de5add09d0 |
@@ -2,8 +2,8 @@ name: Docker Test & Publish
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
# branches:
|
||||||
- "main"
|
# - "main"
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run services in background
|
- name: Run services in background
|
||||||
run: |
|
run: |
|
||||||
docker-compose -f docker-compose.db.yml up -d
|
docker compose -f docker-compose.db.yml up -d
|
||||||
|
|
||||||
- name: Copy .env.example
|
- name: Copy .env.example
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
Generated
+1785
-297
File diff suppressed because it is too large
Load Diff
+7
-1
@@ -1,6 +1,12 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
|
members = [
|
||||||
|
"crates/delta",
|
||||||
|
"crates/bonfire",
|
||||||
|
"crates/core/*",
|
||||||
|
"crates/services/*",
|
||||||
|
"crates/bindings/*",
|
||||||
|
]
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
|
||||||
|
|||||||
+4
-1
@@ -1,5 +1,5 @@
|
|||||||
# Build Stage
|
# Build Stage
|
||||||
FROM --platform="${BUILDPLATFORM}" rust:1.70.0-slim
|
FROM --platform="${BUILDPLATFORM}" rust:1.80.1-slim-bookworm
|
||||||
USER 0:0
|
USER 0:0
|
||||||
WORKDIR /home/rust/src
|
WORKDIR /home/rust/src
|
||||||
|
|
||||||
@@ -17,14 +17,17 @@ RUN sh /tmp/build-image-layer.sh tools
|
|||||||
|
|
||||||
# Build all dependencies
|
# Build all dependencies
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
|
||||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||||
|
COPY crates/core/files/Cargo.toml ./crates/core/files/
|
||||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||||
|
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||||
RUN sh /tmp/build-image-layer.sh deps
|
RUN sh /tmp/build-image-layer.sh deps
|
||||||
|
|
||||||
# Build all apps
|
# Build all apps
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Build Stage
|
# Build Stage
|
||||||
FROM rust:1.70.0-slim
|
FROM rust:1.80.1-slim-bookworm
|
||||||
USER 0:0
|
USER 0:0
|
||||||
WORKDIR /home/rust/src
|
WORKDIR /home/rust/src
|
||||||
|
|
||||||
@@ -13,14 +13,17 @@ COPY scripts/build-image-layer.sh /tmp/
|
|||||||
|
|
||||||
# Build all dependencies
|
# Build all dependencies
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
|
||||||
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
|
||||||
COPY crates/delta/Cargo.toml ./crates/delta/
|
COPY crates/delta/Cargo.toml ./crates/delta/
|
||||||
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
COPY crates/core/config/Cargo.toml ./crates/core/config/
|
||||||
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
COPY crates/core/database/Cargo.toml ./crates/core/database/
|
||||||
|
COPY crates/core/files/Cargo.toml ./crates/core/files/
|
||||||
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
COPY crates/core/models/Cargo.toml ./crates/core/models/
|
||||||
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
|
||||||
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
|
||||||
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
COPY crates/core/result/Cargo.toml ./crates/core/result/
|
||||||
|
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
|
||||||
RUN sh /tmp/build-image-layer.sh deps
|
RUN sh /tmp/build-image-layer.sh deps
|
||||||
|
|
||||||
# Build all apps
|
# Build all apps
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
|||||||
|
|
||||||
## Minimum Supported Rust Version
|
## Minimum Supported Rust Version
|
||||||
|
|
||||||
Rust 1.70 or higher.
|
Rust 1.76 or higher.
|
||||||
|
|
||||||
## Development Guide
|
## Development Guide
|
||||||
|
|
||||||
|
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
|
||||||
|
|
||||||
Before getting started, you'll want to install:
|
Before getting started, you'll want to install:
|
||||||
|
|
||||||
- Rust toolchain (rustup recommended)
|
- Rust toolchain (rustup recommended)
|
||||||
@@ -141,7 +143,7 @@ Now run tests for whichever database:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
TEST_DB=REFERENCE cargo nextest run
|
TEST_DB=REFERENCE cargo nextest run
|
||||||
TEST_DB=MONGOBD cargo nextest run
|
TEST_DB=MONGODB cargo nextest run
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
target
|
||||||
|
index.node
|
||||||
|
**/node_modules
|
||||||
|
**/.DS_Store
|
||||||
|
npm-debug.log*
|
||||||
|
cargo.log
|
||||||
|
cross.log
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "revolt-nodejs-bindings"
|
||||||
|
version = "0.7.16"
|
||||||
|
description = "Node.js bindings for the Revolt software"
|
||||||
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
|
license = "MIT"
|
||||||
|
edition = "2021"
|
||||||
|
exclude = ["index.node"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
neon = "1.0.0"
|
||||||
|
neon-serde4 = "1.0.0"
|
||||||
|
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
|
async-std = "1.12.0"
|
||||||
|
|
||||||
|
revolt-result = { version = "0.7.16", path = "../../core/result" }
|
||||||
|
revolt-database = { version = "0.7.16", path = "../../core/database" }
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# revolt.node
|
||||||
|
|
||||||
|
**revolt.node:** Node.js bindings for the Revolt software
|
||||||
|
|
||||||
|
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
|
||||||
|
|
||||||
|
## Building revolt.node
|
||||||
|
|
||||||
|
Building revolt.node requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
|
||||||
|
|
||||||
|
To run the build, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`.
|
||||||
|
|
||||||
|
## Exploring revolt.node
|
||||||
|
|
||||||
|
After building revolt.node, you can explore its exports at the Node console:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ npm i
|
||||||
|
$ npm run build
|
||||||
|
$ node
|
||||||
|
> require('.').hello()
|
||||||
|
'hello node'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
In the project directory, you can run:
|
||||||
|
|
||||||
|
#### `npm install`
|
||||||
|
|
||||||
|
Installs the project, including running `npm run build`.
|
||||||
|
|
||||||
|
#### `npm run build`
|
||||||
|
|
||||||
|
Builds the Node addon (`index.node`) from source, generating a release build with `cargo --release`.
|
||||||
|
|
||||||
|
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm run build` and similar commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build -- --feature=beetle
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `npm run debug`
|
||||||
|
|
||||||
|
Similar to `npm run build` but generates a debug build with `cargo`.
|
||||||
|
|
||||||
|
#### `npm run cross`
|
||||||
|
|
||||||
|
Similar to `npm run build` but uses [cross-rs](https://github.com/cross-rs/cross) to cross-compile for another platform. Use the [`CARGO_BUILD_TARGET`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget) environment variable to select the build target.
|
||||||
|
|
||||||
|
#### `npm test`
|
||||||
|
|
||||||
|
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
The directory structure of this project is:
|
||||||
|
|
||||||
|
```
|
||||||
|
revolt.node/
|
||||||
|
├── Cargo.toml
|
||||||
|
├── README.md
|
||||||
|
├── src/
|
||||||
|
| └── lib.rs
|
||||||
|
├── index.node
|
||||||
|
├── package.json
|
||||||
|
└── target/
|
||||||
|
```
|
||||||
|
|
||||||
|
| Entry | Purpose |
|
||||||
|
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
|
||||||
|
| `README.md` | This file. |
|
||||||
|
| `src/` | The directory tree containing the Rust source code for the project. |
|
||||||
|
| `lib.rs` | Entry point for the Rust source code. |
|
||||||
|
| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. |
|
||||||
|
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
|
||||||
|
| `target/` | Binary artifacts generated by the Rust build. |
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
Learn more about:
|
||||||
|
|
||||||
|
- [Neon](https://neon-bindings.com).
|
||||||
|
- [Rust](https://www.rust-lang.org).
|
||||||
|
- [Node](https://nodejs.org).
|
||||||
Vendored
+66
@@ -0,0 +1,66 @@
|
|||||||
|
import { Channel, User } from "revolt-api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opaque type for Revolt database
|
||||||
|
*/
|
||||||
|
export declare interface Database {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opaque type for Revolt database
|
||||||
|
*/
|
||||||
|
export declare interface OpaqueUser {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error type from Revolt backend
|
||||||
|
*/
|
||||||
|
export declare interface Err {
|
||||||
|
type: string;
|
||||||
|
location: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a new handle to the Revolt database
|
||||||
|
* @returns {Database} Handle
|
||||||
|
*/
|
||||||
|
export declare function database(): Database;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch user from database
|
||||||
|
* @param {string} userId User's ID
|
||||||
|
* @this {Database}
|
||||||
|
*/
|
||||||
|
export declare function database_fetch_user(userId: string): OpaqueUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch user from database
|
||||||
|
* @param {string} username Username
|
||||||
|
* @param {string} discriminator Discriminator
|
||||||
|
* @this {Database}
|
||||||
|
*/
|
||||||
|
export declare function database_fetch_user_by_username(
|
||||||
|
username: string,
|
||||||
|
discriminator: string
|
||||||
|
): OpaqueUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets model data as JSON
|
||||||
|
* @this {OpaqueUser}
|
||||||
|
*/
|
||||||
|
export declare function model_data(): User;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets error if the model failed to fetch
|
||||||
|
* @this {OpaqueUser}
|
||||||
|
*/
|
||||||
|
export declare function model_error(): Err;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a direct message channel between two users
|
||||||
|
* @param {string} userA User A ID
|
||||||
|
* @param {string} userB User B ID
|
||||||
|
* @returns Existing or newly created channel
|
||||||
|
*/
|
||||||
|
export declare function proc_channels_create_dm(
|
||||||
|
userA: string,
|
||||||
|
userB: string
|
||||||
|
): Promise<Channel & { error: Err }>;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "revolt-nodejs-bindings",
|
||||||
|
"version": "0.7.15",
|
||||||
|
"description": "Node.js bindings for the Revolt software",
|
||||||
|
"main": "index.node",
|
||||||
|
"scripts": {
|
||||||
|
"test": "cargo test",
|
||||||
|
"cargo-build": "cargo build --message-format=json > cargo.log",
|
||||||
|
"cross-build": "cross build --message-format=json > cross.log",
|
||||||
|
"postcargo-build": "neon dist < cargo.log",
|
||||||
|
"postcross-build": "neon dist -m /target < cross.log",
|
||||||
|
"debug": "npm run cargo-build --",
|
||||||
|
"build": "npm run cargo-build -- --release",
|
||||||
|
"cross": "npm run cross-build -- --release"
|
||||||
|
},
|
||||||
|
"author": "Paul Makles",
|
||||||
|
"license": "AGPL-3.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@neon-rs/cli": "0.1.73"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/revoltchat/backend"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"revolt",
|
||||||
|
"chat"
|
||||||
|
],
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/revoltchat/backend/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/revoltchat/backend#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"revolt-api": "^0.7.15"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+117
@@ -0,0 +1,117 @@
|
|||||||
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
revolt-api:
|
||||||
|
specifier: ^0.7.15
|
||||||
|
version: 0.7.15
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
'@neon-rs/cli':
|
||||||
|
specifier: 0.1.73
|
||||||
|
version: 0.1.73
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
/@cargo-messages/android-arm-eabi@0.1.72:
|
||||||
|
resolution: {integrity: sha512-gGZxIM1mj+Y5x+ULND6ZCNr7f70OJi9wDlycSK8hGONy9wrChN6JAIHryddC5cqcwlYAoQ6IDcDFElnhAYbybA==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cargo-messages/darwin-arm64@0.1.72:
|
||||||
|
resolution: {integrity: sha512-EAzN5MLaXPljZKZDO5qR+aBs44eSq2ZbEnS7AI/FziE3MzeXbrGOS3fLba5+7yWPFXJyZolXzePm8N1EBv8ovg==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cargo-messages/darwin-x64@0.1.72:
|
||||||
|
resolution: {integrity: sha512-RLo6j8s3nYbjdd1LDct4wamfChyRit7zokUuxtIYCu9XOlltkN5vnj1vwnrPvoqCMZ/7CbbuHFwSTn9A71de/w==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cargo-messages/linux-arm-gnueabihf@0.1.72:
|
||||||
|
resolution: {integrity: sha512-tHsRshuzfjrX6SDW3jg6al8vMNLTMgczGnVYl5RuBZf/yrAUuwe30KxA9ge6w6mW6Ox797DyBchzAc9OLgTgmQ==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cargo-messages/linux-x64-gnu@0.1.72:
|
||||||
|
resolution: {integrity: sha512-VGtL6CCnUbhsP4aYuBNT5kfrAL7o0qjrxw97a+ax13t+nJd26tVEEIKHMu5drvvS/Nm/hn7sLT8zMnnCv0pvHg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cargo-messages/win32-arm64-msvc@0.1.72:
|
||||||
|
resolution: {integrity: sha512-V93Cgz39K+yqa3MveNbhh29pYCp8izK5uEavjPoxlNxAbsMCWH+s0verGDdUcfGxjR1H2V7oZ4FszPqR2SqMRQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@cargo-messages/win32-x64-msvc@0.1.72:
|
||||||
|
resolution: {integrity: sha512-knz3uSrO0OSbq3U5VWfCY8FB4NsM43BOWLZ7x4sfaMOC1XWv+IyvDdkLe6DhJx8KUw46KIAimYs9YROrp6l46Q==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
requiresBuild: true
|
||||||
|
dev: true
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
/@neon-rs/cli@0.1.73:
|
||||||
|
resolution: {integrity: sha512-1kv8S/feB6UQWQQwsnGfkSkEBOtlFDNExnioL81E2BwvUWgjQPaseHgpi2EpWVgsPUgur5eBm4QowmlpWkD4/w==}
|
||||||
|
hasBin: true
|
||||||
|
optionalDependencies:
|
||||||
|
'@cargo-messages/android-arm-eabi': 0.1.72
|
||||||
|
'@cargo-messages/darwin-arm64': 0.1.72
|
||||||
|
'@cargo-messages/darwin-x64': 0.1.72
|
||||||
|
'@cargo-messages/linux-arm-gnueabihf': 0.1.72
|
||||||
|
'@cargo-messages/linux-x64-gnu': 0.1.72
|
||||||
|
'@cargo-messages/win32-arm64-msvc': 0.1.72
|
||||||
|
'@cargo-messages/win32-x64-msvc': 0.1.72
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/axios@0.26.1:
|
||||||
|
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.6
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/follow-redirects@1.15.6:
|
||||||
|
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.defaultsdeep@4.6.1:
|
||||||
|
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/revolt-api@0.7.15:
|
||||||
|
resolution: {integrity: sha512-rWRnjBFvoXt/RHRxHg9KuP+x1Jk61KdsOpKOczVJg5tIqGDmqp/6QZHw5rJTtG+wCH3Ah3YKwY+MM5NHfg1W3Q==}
|
||||||
|
dependencies:
|
||||||
|
axios: 0.26.1
|
||||||
|
lodash.defaultsdeep: 4.6.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate serde;
|
||||||
|
|
||||||
|
use neon::prelude::*;
|
||||||
|
use revolt_database::{Database, DatabaseInfo};
|
||||||
|
|
||||||
|
struct DatabaseBinding(Database, Channel);
|
||||||
|
impl Finalize for DatabaseBinding {}
|
||||||
|
impl DatabaseBinding {
|
||||||
|
fn take(&self) -> (Database, Channel) {
|
||||||
|
(self.0.clone(), self.1.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn js_database(mut cx: FunctionContext) -> JsResult<JsBox<DatabaseBinding>> {
|
||||||
|
let db = async_std::task::block_on(DatabaseInfo::Auto.connect())
|
||||||
|
.or_else(|err| cx.throw_error(err))?;
|
||||||
|
|
||||||
|
let channel = cx.channel();
|
||||||
|
Ok(cx.boxed(DatabaseBinding(db, channel)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementations for models
|
||||||
|
#[derive(Clone)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
enum Model {
|
||||||
|
User(revolt_database::User),
|
||||||
|
Error(revolt_result::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
fn give(&self) -> Model {
|
||||||
|
self.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Finalize for Model {}
|
||||||
|
|
||||||
|
macro_rules! shim_boxed {
|
||||||
|
($cx: ident, $name: ident, $model: ident, $( $variable: ident $type: ident $id: expr )+, $cmd: ident, $( $arg: expr, )+) => {
|
||||||
|
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||||
|
$(
|
||||||
|
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||||
|
)+
|
||||||
|
|
||||||
|
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||||
|
let (deferred, promise) = cx.promise();
|
||||||
|
|
||||||
|
async_std::task::spawn(async move {
|
||||||
|
let result = db.$cmd($($arg,)+).await;
|
||||||
|
deferred.settle_with(&channel, move |mut cx| {
|
||||||
|
Ok(cx.boxed(match result {
|
||||||
|
Ok(value) => Model::$model(value),
|
||||||
|
Err(error) => Model::Error(error)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
$cx.export_function(stringify!($name), $name)?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn js_data(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||||
|
match cx.this::<JsBox<Model>>()?.give() {
|
||||||
|
Model::Error(_) => neon_serde4::to_value(&mut cx, &None::<()>),
|
||||||
|
Model::User(user) => neon_serde4::to_value(&mut cx, &user),
|
||||||
|
}
|
||||||
|
.or_else(|e| cx.throw_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn js_error(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||||
|
let value = match cx.this::<JsBox<Model>>()?.give() {
|
||||||
|
Model::Error(err) => Some(err),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
neon_serde4::to_value(&mut cx, &value).or_else(|e| cx.throw_error(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic data implementation
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
struct ResultBinding<T> {
|
||||||
|
#[serde(flatten)]
|
||||||
|
value: Option<T>,
|
||||||
|
error: Option<revolt_result::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! shim {
|
||||||
|
($cx: ident, $name: ident, $( $variable: ident $type: ident $id: expr )*, $( $model: ident $modelType: ident $modelId: expr )*, | $db: ident | $closure: expr, $( $arg: expr, )+) => {
|
||||||
|
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
|
||||||
|
$(
|
||||||
|
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
|
||||||
|
)*
|
||||||
|
|
||||||
|
$(
|
||||||
|
let mut $model = if let Model::$modelType(value) = cx.argument::<JsBox<Model>>($modelId)?.give() {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
)*
|
||||||
|
|
||||||
|
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
|
||||||
|
let (deferred, promise) = cx.promise();
|
||||||
|
|
||||||
|
async_std::task::spawn(async move {
|
||||||
|
#[allow(clippy::redundant_closure_call)]
|
||||||
|
let result = (|$db: $crate::Database| $closure)(db.clone()).await;
|
||||||
|
deferred.settle_with(&channel, move |mut cx| {
|
||||||
|
neon_serde4::to_value(
|
||||||
|
&mut cx,
|
||||||
|
&match result {
|
||||||
|
Ok(value) => ResultBinding {
|
||||||
|
value: Some(value),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
Err(error) => ResultBinding {
|
||||||
|
value: None,
|
||||||
|
error: Some(error),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.or_else(|e| cx.throw_error(e.to_string()))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(promise)
|
||||||
|
}
|
||||||
|
|
||||||
|
$cx.export_function(stringify!($name), $name)?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[neon::main]
|
||||||
|
fn main(mut cx: ModuleContext) -> NeonResult<()> {
|
||||||
|
// database & model stuff
|
||||||
|
cx.export_function("database", js_database)?;
|
||||||
|
cx.export_function("model_data", js_data)?;
|
||||||
|
cx.export_function("model_error", js_error)?;
|
||||||
|
|
||||||
|
shim_boxed!(
|
||||||
|
cx,
|
||||||
|
database_fetch_user,
|
||||||
|
User,
|
||||||
|
user_id JsString 0,
|
||||||
|
fetch_user,
|
||||||
|
&user_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
shim_boxed!(
|
||||||
|
cx,
|
||||||
|
database_fetch_user_by_username,
|
||||||
|
User,
|
||||||
|
username JsString 0
|
||||||
|
discriminator JsString 1,
|
||||||
|
fetch_user_by_username,
|
||||||
|
&username, &discriminator,
|
||||||
|
);
|
||||||
|
|
||||||
|
// procedure calls
|
||||||
|
shim!(
|
||||||
|
cx,
|
||||||
|
proc_channels_create_dm,
|
||||||
|
user_a JsString 0
|
||||||
|
user_b JsString 1,
|
||||||
|
,
|
||||||
|
|db| async move {
|
||||||
|
let user_a = db.fetch_user(&user_a).await?;
|
||||||
|
let user_b = db.fetch_user(&user_b).await?;
|
||||||
|
revolt_database::Channel::create_dm(&db, &user_a, &user_b).await
|
||||||
|
},
|
||||||
|
&userA, &userB,
|
||||||
|
);
|
||||||
|
|
||||||
|
shim!(
|
||||||
|
cx,
|
||||||
|
proc_users_suspend,
|
||||||
|
,
|
||||||
|
user User 0,
|
||||||
|
|db| async move {
|
||||||
|
user.suspend(&db).await
|
||||||
|
},
|
||||||
|
&user,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
const Internal = require(".");
|
||||||
|
|
||||||
|
// playing around with class wrapper, not practical
|
||||||
|
class Model {
|
||||||
|
constructor(model) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return Internal.model_data.bind(this.model)();
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
return Internal.model_error.bind(this.model)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class User extends Model {
|
||||||
|
constructor(db, user) {
|
||||||
|
super(user);
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
constructor() {
|
||||||
|
this.db = Internal.database();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUser(userId) {
|
||||||
|
return new User(
|
||||||
|
this,
|
||||||
|
await Internal.database_fetch_user.bind(this.db)(userId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUserByUsername(username, discriminator) {
|
||||||
|
return new User(
|
||||||
|
this,
|
||||||
|
await Internal.database_fetch_user_by_username.bind(this.db)(
|
||||||
|
username,
|
||||||
|
discriminator
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = new Database();
|
||||||
|
db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data()));
|
||||||
|
db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data()));
|
||||||
|
db.fetchUserByUsername("dos", "1111").then((user) =>
|
||||||
|
console.info(user.error())
|
||||||
|
);
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "bindings",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-bonfire"
|
name = "revolt-bonfire"
|
||||||
version = "0.7.9"
|
version = "0.7.16"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ revolt-result = { path = "../core/result" }
|
|||||||
revolt-models = { path = "../core/models" }
|
revolt-models = { path = "../core/models" }
|
||||||
revolt-config = { path = "../core/config" }
|
revolt-config = { path = "../core/config" }
|
||||||
revolt-database = { path = "../core/database" }
|
revolt-database = { path = "../core/database" }
|
||||||
revolt-permissions = { version = "0.7.9", path = "../core/permissions" }
|
revolt-permissions = { version = "0.7.16", path = "../core/permissions" }
|
||||||
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
|
||||||
|
|
||||||
# redis
|
# redis
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||||
|
|
||||||
# Bundle Stage
|
# Bundle Stage
|
||||||
FROM debian:bullseye-slim
|
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y ca-certificates && \
|
|
||||||
apt-get clean
|
|
||||||
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
USER nonroot
|
||||||
CMD ["./revolt-bonfire"]
|
CMD ["./revolt-bonfire"]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use async_tungstenite::tungstenite::{handshake, Message};
|
use async_tungstenite::tungstenite::{handshake, Message};
|
||||||
use futures::channel::oneshot::Sender;
|
use futures::channel::oneshot::Sender;
|
||||||
|
use revolt_database::events::client::ReadyPayloadFields;
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -83,6 +84,17 @@ impl ProtocolConfiguration {
|
|||||||
pub fn get_protocol_format(&self) -> &ProtocolFormat {
|
pub fn get_protocol_format(&self) -> &ProtocolFormat {
|
||||||
&self.format
|
&self.format
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get ready payload fields
|
||||||
|
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
|
||||||
|
vec![
|
||||||
|
ReadyPayloadFields::Users,
|
||||||
|
ReadyPayloadFields::Servers,
|
||||||
|
ReadyPayloadFields::Channels,
|
||||||
|
ReadyPayloadFields::Members,
|
||||||
|
ReadyPayloadFields::Emoji,
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Object holding one side of a channel for receiving the parsed information
|
/// Object holding one side of a channel for receiving the parsed information
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
|
events::client::{EventV1, ReadyPayloadFields},
|
||||||
MemberCompositeKey, Presence, RelationshipStatus,
|
util::permissions::DatabasePermissionQuery,
|
||||||
|
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
|
||||||
};
|
};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||||
@@ -90,7 +91,11 @@ impl Cache {
|
|||||||
/// State Manager
|
/// State Manager
|
||||||
impl State {
|
impl State {
|
||||||
/// Generate a Ready packet for the current user
|
/// Generate a Ready packet for the current user
|
||||||
pub async fn generate_ready_payload(&mut self, db: &Database) -> Result<EventV1> {
|
pub async fn generate_ready_payload(
|
||||||
|
&mut self,
|
||||||
|
db: &Database,
|
||||||
|
fields: Vec<ReadyPayloadFields>,
|
||||||
|
) -> Result<EventV1> {
|
||||||
let user = self.clone_user();
|
let user = self.clone_user();
|
||||||
self.cache.is_bot = user.bot.is_some();
|
self.cache.is_bot = user.bot.is_some();
|
||||||
|
|
||||||
@@ -150,14 +155,36 @@ impl State {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Fetch customisations.
|
// Fetch customisations.
|
||||||
let emojis = db
|
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
|
||||||
.fetch_emoji_by_parent_ids(
|
Some(
|
||||||
&servers
|
db.fetch_emoji_by_parent_ids(
|
||||||
.iter()
|
&servers
|
||||||
.map(|x| x.id.to_string())
|
.iter()
|
||||||
.collect::<Vec<String>>(),
|
.map(|x| x.id.to_string())
|
||||||
|
.collect::<Vec<String>>(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
)
|
)
|
||||||
.await?;
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch user settings
|
||||||
|
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
|
||||||
|
.iter()
|
||||||
|
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
|
||||||
|
{
|
||||||
|
Some(db.fetch_user_settings(&user.id, &keys).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch channel unreads
|
||||||
|
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
|
||||||
|
Some(db.fetch_unreads(&user.id).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Copy data into local state cache.
|
// Copy data into local state cache.
|
||||||
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
|
||||||
@@ -180,7 +207,7 @@ impl State {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Make sure we see our own user correctly.
|
// Make sure we see our own user correctly.
|
||||||
users.push(user.into_self().await);
|
users.push(user.into_self(true).await);
|
||||||
|
|
||||||
// Set subscription state internally.
|
// Set subscription state internally.
|
||||||
self.reset_state().await;
|
self.reset_state().await;
|
||||||
@@ -201,13 +228,31 @@ impl State {
|
|||||||
for channel in &channels {
|
for channel in &channels {
|
||||||
self.insert_subscription(channel.id().to_string()).await;
|
self.insert_subscription(channel.id().to_string()).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(EventV1::Ready {
|
Ok(EventV1::Ready {
|
||||||
users,
|
users: if fields.contains(&ReadyPayloadFields::Users) {
|
||||||
servers: servers.into_iter().map(Into::into).collect(),
|
Some(users)
|
||||||
channels: channels.into_iter().map(Into::into).collect(),
|
} else {
|
||||||
members: members.into_iter().map(Into::into).collect(),
|
None
|
||||||
emojis: emojis.into_iter().map(Into::into).collect(),
|
},
|
||||||
|
servers: if fields.contains(&ReadyPayloadFields::Servers) {
|
||||||
|
Some(servers.into_iter().map(Into::into).collect())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
channels: if fields.contains(&ReadyPayloadFields::Channels) {
|
||||||
|
Some(channels.into_iter().map(Into::into).collect())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
members: if fields.contains(&ReadyPayloadFields::Members) {
|
||||||
|
Some(members.into_iter().map(Into::into).collect())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||||
|
|
||||||
|
user_settings,
|
||||||
|
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +488,7 @@ impl State {
|
|||||||
EventV1::ServerMemberJoin { .. } => {
|
EventV1::ServerMemberJoin { .. } => {
|
||||||
// We will always receive ServerCreate when joining a new server.
|
// We will always receive ServerCreate when joining a new server.
|
||||||
}
|
}
|
||||||
EventV1::ServerMemberLeave { id, user } => {
|
EventV1::ServerMemberLeave { id, user, .. } => {
|
||||||
if user == &self.cache.user_id {
|
if user == &self.cache.user_id {
|
||||||
self.remove_subscription(id).await;
|
self.remove_subscription(id).await;
|
||||||
|
|
||||||
@@ -540,6 +585,20 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EventV1::Message(message) => {
|
||||||
|
// Since Message events are fanned out to many clients,
|
||||||
|
// we must reconstruct the relationship value at this end.
|
||||||
|
if let Some(user) = &mut message.user {
|
||||||
|
user.relationship = self
|
||||||
|
.cache
|
||||||
|
.users
|
||||||
|
.get(&self.cache.user_id)
|
||||||
|
.expect("missing self?")
|
||||||
|
.relationship_with(&message.author)
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,10 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Download required data to local cache and send Ready payload.
|
// Download required data to local cache and send Ready payload.
|
||||||
let ready_payload = match state.generate_ready_payload(db).await {
|
let ready_payload = match state
|
||||||
|
.generate_ready_payload(db, config.get_ready_payload_fields())
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(ready_payload) => ready_payload,
|
Ok(ready_payload) => ready_payload,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
sentry::capture_error(&err);
|
sentry::capture_error(&err);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-config"
|
name = "revolt-config"
|
||||||
version = "0.7.9"
|
version = "0.7.16"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
|
|||||||
+128
-15
@@ -1,8 +1,17 @@
|
|||||||
[database]
|
[database]
|
||||||
|
# MongoDB connection URL
|
||||||
|
# Defaults to the container name specified in self-hosted
|
||||||
mongodb = "mongodb://database"
|
mongodb = "mongodb://database"
|
||||||
|
# Redis connection URL
|
||||||
|
# Defaults to the container name specified in self-hosted
|
||||||
redis = "redis://redis/"
|
redis = "redis://redis/"
|
||||||
|
|
||||||
[hosts]
|
[hosts]
|
||||||
|
# Web locations of various services
|
||||||
|
# Defaults assume all services are reverse-proxied
|
||||||
|
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
|
||||||
|
#
|
||||||
|
# Remember to change these to https/wss where appropriate in production!
|
||||||
app = "http://local.revolt.chat"
|
app = "http://local.revolt.chat"
|
||||||
api = "http://local.revolt.chat/api"
|
api = "http://local.revolt.chat/api"
|
||||||
events = "ws://local.revolt.chat/ws"
|
events = "ws://local.revolt.chat/ws"
|
||||||
@@ -14,60 +23,164 @@ voso_legacy_ws = ""
|
|||||||
[api]
|
[api]
|
||||||
|
|
||||||
[api.registration]
|
[api.registration]
|
||||||
|
# Whether an invite should be required for registration
|
||||||
|
# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
|
||||||
invite_only = false
|
invite_only = false
|
||||||
|
|
||||||
[api.smtp]
|
[api.smtp]
|
||||||
|
# Email server configuration for verification
|
||||||
|
# Defaults to no email verification (host field is empty)
|
||||||
host = ""
|
host = ""
|
||||||
username = ""
|
username = ""
|
||||||
password = ""
|
password = ""
|
||||||
from_address = ""
|
from_address = "noreply@example.com"
|
||||||
|
reply_to = "noreply@example.com"
|
||||||
|
port = 587
|
||||||
|
use_tls = true
|
||||||
|
|
||||||
[api.vapid]
|
[api.vapid]
|
||||||
|
# Generate your own keys:
|
||||||
|
# 1. Run `openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem`
|
||||||
|
# 2. Find `private_key` using `base64 vapid_private.pem`
|
||||||
|
# 3. Find `public_key` using `openssl ec -in vapid_private.pem -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n'`
|
||||||
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
|
||||||
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
|
||||||
|
|
||||||
[api.fcm]
|
[api.fcm]
|
||||||
api_key = ""
|
# Google Firebase Cloud Messaging Service Account Key
|
||||||
|
# Obtained from the cloud messaging console
|
||||||
|
key_type = ""
|
||||||
|
project_id = ""
|
||||||
|
private_key_id = ""
|
||||||
|
private_key = ""
|
||||||
|
client_email = ""
|
||||||
|
client_id = ""
|
||||||
|
auth_uri = ""
|
||||||
|
token_uri = ""
|
||||||
|
auth_provider_x509_cert_url = ""
|
||||||
|
client_x509_cert_url = ""
|
||||||
|
|
||||||
|
[api.apn]
|
||||||
|
# Apple Push Notifications keys for sending notifications
|
||||||
|
sandbox = false
|
||||||
|
pkcs8 = ""
|
||||||
|
key_id = ""
|
||||||
|
team_id = ""
|
||||||
|
|
||||||
[api.security]
|
[api.security]
|
||||||
|
# Authifier Shield API key
|
||||||
authifier_shield_key = ""
|
authifier_shield_key = ""
|
||||||
|
# Legacy voice server management token
|
||||||
voso_legacy_token = ""
|
voso_legacy_token = ""
|
||||||
|
# Whether services are behind the Cloudflare network
|
||||||
trust_cloudflare = false
|
trust_cloudflare = false
|
||||||
|
|
||||||
[api.security.captcha]
|
[api.security.captcha]
|
||||||
|
# hCaptcha configuration
|
||||||
hcaptcha_key = ""
|
hcaptcha_key = ""
|
||||||
hcaptcha_sitekey = ""
|
hcaptcha_sitekey = ""
|
||||||
|
|
||||||
[api.workers]
|
[api.workers]
|
||||||
|
# Maximum concurrent connections (to proxy server)
|
||||||
max_concurrent_connections = 50
|
max_concurrent_connections = 50
|
||||||
|
|
||||||
|
[files]
|
||||||
|
# Encryption key for stored files
|
||||||
|
# Generate your own key using `openssl rand -base64 32`
|
||||||
|
encryption_key = "qcuMA+ssxhMyKaNAKBGFfryfFtUH8NDlamQyDwGW6fU="
|
||||||
|
|
||||||
|
[files.limit]
|
||||||
|
# Minimum image resolution
|
||||||
|
min_resolution = [1, 1]
|
||||||
|
# Maximum MP of images
|
||||||
|
max_mega_pixels = 40
|
||||||
|
# Maximum pixel side of an image
|
||||||
|
max_pixel_side = 10_000
|
||||||
|
|
||||||
|
[files.preview]
|
||||||
|
# Maximum image resolution
|
||||||
|
attachments = [1280, 1280]
|
||||||
|
avatars = [128, 128]
|
||||||
|
backgrounds = [1280, 720]
|
||||||
|
icons = [128, 128]
|
||||||
|
banners = [480, 480]
|
||||||
|
emojis = [128, 128]
|
||||||
|
|
||||||
|
[files.s3]
|
||||||
|
# Configuration for S3
|
||||||
|
# Defaults included for MinIO + self-hosted setup
|
||||||
|
#
|
||||||
|
# Backblaze B2:
|
||||||
|
# - endpoint is listed on the "Buckets" page
|
||||||
|
# - region is `eu-central-003` string from endpoint URL
|
||||||
|
# - access_key_id is keyID generated on the "Application Keys" page
|
||||||
|
# - secret_access_key is token generated on the "Application Keys" page
|
||||||
|
# - default_bucket matches the name of the bucket you've created
|
||||||
|
|
||||||
|
# S3 protocol endpoint
|
||||||
|
endpoint = ""
|
||||||
|
# S3 region name
|
||||||
|
region = ""
|
||||||
|
# S3 protocol key ID
|
||||||
|
access_key_id = ""
|
||||||
|
# S3 protocol access key
|
||||||
|
secret_access_key = ""
|
||||||
|
# Bucket to upload to by default
|
||||||
|
default_bucket = "revolt-uploads"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
# Feature gate options
|
||||||
webhooks_enabled = false
|
webhooks_enabled = false
|
||||||
|
|
||||||
[features.limits]
|
[features.limits]
|
||||||
|
|
||||||
[features.limits.default]
|
[features.limits.global]
|
||||||
outgoing_friend_requests = 10
|
|
||||||
|
|
||||||
group_size = 100
|
group_size = 100
|
||||||
bots = 5
|
|
||||||
message_length = 2000
|
|
||||||
message_embeds = 5
|
message_embeds = 5
|
||||||
message_replies = 5
|
message_replies = 5
|
||||||
message_attachments = 5
|
|
||||||
message_reactions = 20
|
message_reactions = 20
|
||||||
servers = 100
|
|
||||||
server_emoji = 100
|
server_emoji = 100
|
||||||
server_roles = 200
|
server_roles = 200
|
||||||
server_channels = 200
|
server_channels = 200
|
||||||
|
|
||||||
attachment_size = 20000000
|
# How many days since creation a user is considered new
|
||||||
avatar_size = 4000000
|
new_user_days = 3
|
||||||
background_size = 6000000
|
|
||||||
icon_size = 2500000
|
# Maximum permissible body size in bytes for uploads
|
||||||
banner_size = 6000000
|
# (should be greater than any one file upload limit)
|
||||||
emoji_size = 500000
|
body_limit_size = 20_000_000
|
||||||
|
|
||||||
|
[features.limits.new_user]
|
||||||
|
outgoing_friend_requests = 5
|
||||||
|
|
||||||
|
bots = 2
|
||||||
|
message_length = 2000
|
||||||
|
message_attachments = 5
|
||||||
|
servers = 100
|
||||||
|
|
||||||
|
attachment_size = 20_000_000
|
||||||
|
avatar_size = 4_000_000
|
||||||
|
background_size = 6_000_000
|
||||||
|
icon_size = 2_500_000
|
||||||
|
banner_size = 6_000_000
|
||||||
|
emoji_size = 500_000
|
||||||
|
|
||||||
|
[features.limits.default]
|
||||||
|
outgoing_friend_requests = 10
|
||||||
|
|
||||||
|
bots = 5
|
||||||
|
message_length = 2000
|
||||||
|
message_attachments = 5
|
||||||
|
servers = 100
|
||||||
|
|
||||||
|
attachment_size = 20_000_000
|
||||||
|
avatar_size = 4_000_000
|
||||||
|
background_size = 6_000_000
|
||||||
|
icon_size = 2_500_000
|
||||||
|
banner_size = 6_000_000
|
||||||
|
emoji_size = 500_000
|
||||||
|
|
||||||
[sentry]
|
[sentry]
|
||||||
|
# Configuration for Sentry error reporting
|
||||||
api = ""
|
api = ""
|
||||||
events = ""
|
events = ""
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use futures_locks::RwLock;
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
pub use sentry::capture_error;
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
@@ -23,6 +25,8 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
|||||||
));
|
));
|
||||||
} else if std::path::Path::new("Revolt.toml").exists() {
|
} else if std::path::Path::new("Revolt.toml").exists() {
|
||||||
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
|
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
|
||||||
|
} else if std::path::Path::new("/Revolt.toml").exists() {
|
||||||
|
builder = builder.add_source(File::new("/Revolt.toml", FileFormat::Toml));
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.build().unwrap()
|
builder.build().unwrap()
|
||||||
@@ -72,7 +76,24 @@ pub struct ApiVapid {
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct ApiFcm {
|
pub struct ApiFcm {
|
||||||
pub api_key: String,
|
pub key_type: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub private_key_id: String,
|
||||||
|
pub private_key: String,
|
||||||
|
pub client_email: String,
|
||||||
|
pub client_id: String,
|
||||||
|
pub auth_uri: String,
|
||||||
|
pub token_uri: String,
|
||||||
|
pub auth_provider_x509_cert_url: String,
|
||||||
|
pub client_x509_cert_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct ApiApn {
|
||||||
|
pub sandbox: bool,
|
||||||
|
pub pkcs8: String,
|
||||||
|
pub key_id: String,
|
||||||
|
pub team_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
@@ -100,25 +121,34 @@ pub struct Api {
|
|||||||
pub smtp: ApiSmtp,
|
pub smtp: ApiSmtp,
|
||||||
pub vapid: ApiVapid,
|
pub vapid: ApiVapid,
|
||||||
pub fcm: ApiFcm,
|
pub fcm: ApiFcm,
|
||||||
|
pub apn: ApiApn,
|
||||||
pub security: ApiSecurity,
|
pub security: ApiSecurity,
|
||||||
pub workers: ApiWorkers,
|
pub workers: ApiWorkers,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
|
pub struct GlobalLimits {
|
||||||
|
pub group_size: usize,
|
||||||
|
pub message_embeds: usize,
|
||||||
|
pub message_replies: usize,
|
||||||
|
pub message_reactions: usize,
|
||||||
|
pub server_emoji: usize,
|
||||||
|
pub server_roles: usize,
|
||||||
|
pub server_channels: usize,
|
||||||
|
|
||||||
|
pub new_user_days: usize,
|
||||||
|
|
||||||
|
pub body_limit_size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct FeaturesLimits {
|
pub struct FeaturesLimits {
|
||||||
pub outgoing_friend_requests: usize,
|
pub outgoing_friend_requests: usize,
|
||||||
|
|
||||||
pub group_size: usize,
|
|
||||||
pub bots: usize,
|
pub bots: usize,
|
||||||
pub message_length: usize,
|
pub message_length: usize,
|
||||||
pub message_replies: usize,
|
|
||||||
pub message_attachments: usize,
|
pub message_attachments: usize,
|
||||||
pub message_embeds: usize,
|
|
||||||
pub message_reactions: usize,
|
|
||||||
pub servers: usize,
|
pub servers: usize,
|
||||||
pub server_emoji: usize,
|
|
||||||
pub server_roles: usize,
|
|
||||||
pub server_channels: usize,
|
|
||||||
|
|
||||||
pub attachment_size: usize,
|
pub attachment_size: usize,
|
||||||
pub avatar_size: usize,
|
pub avatar_size: usize,
|
||||||
@@ -130,6 +160,9 @@ pub struct FeaturesLimits {
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct FeaturesLimitsCollection {
|
pub struct FeaturesLimitsCollection {
|
||||||
|
pub global: GlobalLimits,
|
||||||
|
|
||||||
|
pub new_user: FeaturesLimits,
|
||||||
pub default: FeaturesLimits,
|
pub default: FeaturesLimits,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-database"
|
name = "revolt-database"
|
||||||
version = "0.7.9"
|
version = "0.7.16"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-config = { version = "0.7.9", path = "../config" }
|
revolt-config = { version = "0.7.16", path = "../config" }
|
||||||
revolt-result = { version = "0.7.9", path = "../result" }
|
revolt-result = { version = "0.7.16", path = "../result" }
|
||||||
revolt-models = { version = "0.7.9", path = "../models", features = [
|
revolt-models = { version = "0.7.16", path = "../models", features = [
|
||||||
"validator",
|
"validator",
|
||||||
] }
|
] }
|
||||||
revolt-presence = { version = "0.7.9", path = "../presence" }
|
revolt-presence = { version = "0.7.16", path = "../presence" }
|
||||||
revolt-permissions = { version = "0.7.9", path = "../permissions", features = [
|
revolt-permissions = { version = "0.7.16", path = "../permissions", features = [
|
||||||
"serde",
|
"serde",
|
||||||
"bson",
|
"bson",
|
||||||
] }
|
] }
|
||||||
@@ -85,8 +85,9 @@ revolt_okapi = { version = "0.9.1", optional = true }
|
|||||||
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
revolt_rocket_okapi = { version = "0.9.1", optional = true }
|
||||||
|
|
||||||
# Notifications
|
# Notifications
|
||||||
fcm = "0.9.2"
|
fcm_v1 = "0.3.0"
|
||||||
web-push = "0.10.0"
|
web-push = "0.10.0"
|
||||||
|
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
|
||||||
|
|
||||||
# Authifier
|
# Authifier
|
||||||
authifier = { version = "1.0.8" }
|
authifier = { version = "1.0.8" }
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ use authifier::AuthifierEvent;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use revolt_models::v0::{
|
use revolt_models::v0::{
|
||||||
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
|
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, Webhook
|
||||||
FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
|
|
||||||
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
|
|
||||||
UserSettings, Webhook,
|
|
||||||
};
|
};
|
||||||
use revolt_result::Error;
|
use revolt_result::Error;
|
||||||
|
|
||||||
@@ -39,6 +36,19 @@ pub enum ErrorEvent {
|
|||||||
APIError(Error),
|
APIError(Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fields provided in Ready payload
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum ReadyPayloadFields {
|
||||||
|
Users,
|
||||||
|
Servers,
|
||||||
|
Channels,
|
||||||
|
Members,
|
||||||
|
Emoji,
|
||||||
|
|
||||||
|
UserSettings(Vec<String>),
|
||||||
|
ChannelUnreads,
|
||||||
|
}
|
||||||
|
|
||||||
/// Protocol Events
|
/// Protocol Events
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
@@ -52,11 +62,21 @@ pub enum EventV1 {
|
|||||||
Logout,
|
Logout,
|
||||||
/// Basic data to cache
|
/// Basic data to cache
|
||||||
Ready {
|
Ready {
|
||||||
users: Vec<User>,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
servers: Vec<Server>,
|
users: Option<Vec<User>>,
|
||||||
channels: Vec<Channel>,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
members: Vec<Member>,
|
servers: Option<Vec<Server>>,
|
||||||
emojis: Vec<Emoji>,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
channels: Option<Vec<Channel>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
members: Option<Vec<Member>>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
emojis: Option<Vec<Emoji>>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_settings: Option<UserSettings>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
channel_unreads: Option<Vec<ChannelUnread>>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Ping response
|
/// Ping response
|
||||||
@@ -69,6 +89,7 @@ pub enum EventV1 {
|
|||||||
id: String,
|
id: String,
|
||||||
channel: String,
|
channel: String,
|
||||||
data: PartialMessage,
|
data: PartialMessage,
|
||||||
|
clear: Vec<FieldsMessage>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Append information to existing message
|
/// Append information to existing message
|
||||||
@@ -136,7 +157,11 @@ pub enum EventV1 {
|
|||||||
ServerMemberJoin { id: String, user: String },
|
ServerMemberJoin { id: String, user: String },
|
||||||
|
|
||||||
/// User left server
|
/// User left server
|
||||||
ServerMemberLeave { id: String, user: String },
|
ServerMemberLeave {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
reason: RemovalIntention,
|
||||||
|
},
|
||||||
|
|
||||||
/// Server role created or updated
|
/// Server role created or updated
|
||||||
ServerRoleUpdate {
|
ServerRoleUpdate {
|
||||||
|
|||||||
@@ -87,3 +87,8 @@ pub mod tasks;
|
|||||||
pub fn if_false(t: &bool) -> bool {
|
pub fn if_false(t: &bool) -> bool {
|
||||||
!t
|
!t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Utility function to check if an option doesnt contain true
|
||||||
|
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||||
|
t != &Some(true)
|
||||||
|
}
|
||||||
@@ -146,7 +146,14 @@ pub async fn create_database(db: &MongoDb) {
|
|||||||
"author": 1_i32
|
"author": 1_i32
|
||||||
},
|
},
|
||||||
"name": "author"
|
"name": "author"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"channel": 1_i32,
|
||||||
|
"pinned": 1_i32
|
||||||
|
},
|
||||||
|
"name": "channel_pinned_compound"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ struct MigrationInfo {
|
|||||||
revision: i32,
|
revision: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const LATEST_REVISION: i32 = 27;
|
pub const LATEST_REVISION: i32 = 28;
|
||||||
|
|
||||||
pub async fn migrate_database(db: &MongoDb) {
|
pub async fn migrate_database(db: &MongoDb) {
|
||||||
let migrations = db.col::<Document>("migrations");
|
let migrations = db.col::<Document>("migrations");
|
||||||
@@ -1071,6 +1071,29 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if revision <= 27 {
|
||||||
|
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
|
||||||
|
|
||||||
|
db.db()
|
||||||
|
.run_command(
|
||||||
|
doc! {
|
||||||
|
"createIndexes": "messages",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"channel": 1_i32,
|
||||||
|
"pinned": 1_i32
|
||||||
|
},
|
||||||
|
"name": "channel_pinned_compound"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create message index.");
|
||||||
|
}
|
||||||
|
|
||||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||||
|
|
||||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||||
|
|||||||
@@ -72,7 +72,12 @@ impl Default for Bot {
|
|||||||
#[allow(clippy::disallowed_methods)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
impl Bot {
|
impl Bot {
|
||||||
/// Create a new bot
|
/// Create a new bot
|
||||||
pub async fn create<D>(db: &Database, username: String, owner: &User, data: D) -> Result<Bot>
|
pub async fn create<D>(
|
||||||
|
db: &Database,
|
||||||
|
username: String,
|
||||||
|
owner: &User,
|
||||||
|
data: D,
|
||||||
|
) -> Result<(Bot, User)>
|
||||||
where
|
where
|
||||||
D: Into<Option<PartialBot>>,
|
D: Into<Option<PartialBot>>,
|
||||||
{
|
{
|
||||||
@@ -80,14 +85,13 @@ impl Bot {
|
|||||||
return Err(create_error!(IsBot));
|
return Err(create_error!(IsBot));
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = config().await;
|
if db.get_number_of_bots_by_user(&owner.id).await? >= owner.limits().await.bots {
|
||||||
if db.get_number_of_bots_by_user(&owner.id).await? >= config.features.limits.default.bots {
|
|
||||||
return Err(create_error!(ReachedMaximumBots));
|
return Err(create_error!(ReachedMaximumBots));
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = Ulid::new().to_string();
|
let id = Ulid::new().to_string();
|
||||||
|
|
||||||
User::create(
|
let user = User::create(
|
||||||
db,
|
db,
|
||||||
username,
|
username,
|
||||||
Some(id.to_string()),
|
Some(id.to_string()),
|
||||||
@@ -112,7 +116,7 @@ impl Bot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
db.insert_bot(&bot).await?;
|
db.insert_bot(&bot).await?;
|
||||||
Ok(bot)
|
Ok((bot, user))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a field from this object
|
/// Remove a field from this object
|
||||||
@@ -168,7 +172,7 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let bot = Bot::create(
|
let (bot, _) = Bot::create(
|
||||||
&db,
|
&db,
|
||||||
"Bot Name".to_string(),
|
"Bot Name".to_string(),
|
||||||
&owner,
|
&owner,
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ mod reference;
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AbstractChannelUnreads: Sync + Send {
|
pub trait AbstractChannelUnreads: Sync + Send {
|
||||||
/// Acknowledge a message.
|
/// Acknowledge a message, and returns updated channel unread.
|
||||||
async fn acknowledge_message(
|
async fn acknowledge_message(
|
||||||
&self,
|
&self,
|
||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
message_id: &str,
|
message_id: &str,
|
||||||
) -> Result<()>;
|
) -> Result<Option<ChannelUnread>>;
|
||||||
|
|
||||||
/// Acknowledge many channels.
|
/// Acknowledge many channels.
|
||||||
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
|
async fn acknowledge_channels(&self, user_id: &str, channel_ids: &[String]) -> Result<()>;
|
||||||
@@ -28,4 +28,7 @@ pub trait AbstractChannelUnreads: Sync + Send {
|
|||||||
|
|
||||||
/// Fetch all channel unreads for a user.
|
/// Fetch all channel unreads for a user.
|
||||||
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
|
||||||
|
|
||||||
|
/// Fetch unread for a specific user in a channel.
|
||||||
|
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use bson::Document;
|
use bson::Document;
|
||||||
|
use mongodb::options::FindOneAndUpdateOptions;
|
||||||
|
use mongodb::options::ReturnDocument;
|
||||||
use mongodb::options::UpdateOptions;
|
use mongodb::options::UpdateOptions;
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
use ulid::Ulid;
|
use ulid::Ulid;
|
||||||
@@ -12,31 +14,35 @@ static COL: &str = "channel_unreads";
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AbstractChannelUnreads for MongoDb {
|
impl AbstractChannelUnreads for MongoDb {
|
||||||
/// Acknowledge a message.
|
/// Acknowledge a message, and returns updated channel unread.
|
||||||
async fn acknowledge_message(
|
async fn acknowledge_message(
|
||||||
&self,
|
&self,
|
||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
message_id: &str,
|
message_id: &str,
|
||||||
) -> Result<()> {
|
) -> Result<Option<ChannelUnread>> {
|
||||||
self.col::<Document>(COL)
|
self.col::<ChannelUnread>(COL)
|
||||||
.update_one(
|
.find_one_and_update(
|
||||||
doc! {
|
doc! {
|
||||||
"_id.channel": channel_id,
|
"_id.channel": channel_id,
|
||||||
"_id.user": user_id,
|
"_id.user": user_id,
|
||||||
},
|
},
|
||||||
doc! {
|
doc! {
|
||||||
"$unset": {
|
"$pull": {
|
||||||
"mentions": 1_i32
|
"mentions": {
|
||||||
|
"$lt": message_id
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"$set": {
|
"$set": {
|
||||||
"last_id": message_id
|
"last_id": message_id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
UpdateOptions::builder().upsert(true).build(),
|
FindOneAndUpdateOptions::builder()
|
||||||
|
.upsert(true)
|
||||||
|
.return_document(ReturnDocument::After)
|
||||||
|
.build(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|_| create_database_error!("update_one", COL))
|
.map_err(|_| create_database_error!("update_one", COL))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,4 +122,18 @@ impl AbstractChannelUnreads for MongoDb {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch unread for a specific user in a channel.
|
||||||
|
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||||
|
query!(
|
||||||
|
self,
|
||||||
|
find_one,
|
||||||
|
COL,
|
||||||
|
doc! {
|
||||||
|
"_id.user": user_id,
|
||||||
|
"_id.channel": channel_id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ impl AbstractChannelUnreads for ReferenceDb {
|
|||||||
channel_id: &str,
|
channel_id: &str,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
message_id: &str,
|
message_id: &str,
|
||||||
) -> Result<()> {
|
) -> Result<Option<ChannelUnread>> {
|
||||||
let mut unreads = self.channel_unreads.lock().await;
|
let mut unreads = self.channel_unreads.lock().await;
|
||||||
let key = ChannelCompositeKey {
|
let key = ChannelCompositeKey {
|
||||||
channel: channel_id.to_string(),
|
channel: channel_id.to_string(),
|
||||||
@@ -27,14 +27,14 @@ impl AbstractChannelUnreads for ReferenceDb {
|
|||||||
unreads.insert(
|
unreads.insert(
|
||||||
key.clone(),
|
key.clone(),
|
||||||
ChannelUnread {
|
ChannelUnread {
|
||||||
id: key,
|
id: key.clone(),
|
||||||
last_id: Some(message_id.to_string()),
|
last_id: Some(message_id.to_string()),
|
||||||
mentions: None,
|
mentions: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(unreads.get(&key).cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acknowledge many channels.
|
/// Acknowledge many channels.
|
||||||
@@ -87,4 +87,14 @@ impl AbstractChannelUnreads for ReferenceDb {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch unread for a specific user in a channel.
|
||||||
|
async fn fetch_unread(&self, user_id: &str, channel_id: &str) -> Result<Option<ChannelUnread>> {
|
||||||
|
let unreads = self.channel_unreads.lock().await;
|
||||||
|
|
||||||
|
Ok(unreads.get(&ChannelCompositeKey {
|
||||||
|
channel: channel_id.to_string(),
|
||||||
|
user: user_id.to_string()
|
||||||
|
}).cloned())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,9 +201,9 @@ impl Channel {
|
|||||||
update_server: bool,
|
update_server: bool,
|
||||||
) -> Result<Channel> {
|
) -> Result<Channel> {
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
if server.channels.len() > config.features.limits.default.server_channels {
|
if server.channels.len() > config.features.limits.global.server_channels {
|
||||||
return Err(create_error!(TooManyChannels {
|
return Err(create_error!(TooManyChannels {
|
||||||
max: config.features.limits.default.server_channels,
|
max: config.features.limits.global.server_channels,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -263,9 +263,9 @@ impl Channel {
|
|||||||
data.users.insert(owner_id.to_string());
|
data.users.insert(owner_id.to_string());
|
||||||
|
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
if data.users.len() > config.features.limits.default.group_size {
|
if data.users.len() > config.features.limits.global.group_size {
|
||||||
return Err(create_error!(GroupTooLarge {
|
return Err(create_error!(GroupTooLarge {
|
||||||
max: config.features.limits.default.group_size,
|
max: config.features.limits.global.group_size,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,9 +346,9 @@ impl Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
if recipients.len() >= config.features.limits.default.group_size {
|
if recipients.len() >= config.features.limits.global.group_size {
|
||||||
return Err(create_error!(GroupTooLarge {
|
return Err(create_error!(GroupTooLarge {
|
||||||
max: config.features.limits.default.group_size
|
max: config.features.limits.global.group_size
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +377,8 @@ impl Channel {
|
|||||||
username: &user.username,
|
username: &user.username,
|
||||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||||
},
|
},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
self,
|
self,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -415,13 +417,13 @@ impl Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Clone this channel's id
|
/// Clone this channel's id
|
||||||
pub fn id(&self) -> String {
|
pub fn id(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Channel::DirectMessage { id, .. }
|
Channel::DirectMessage { id, .. }
|
||||||
| Channel::Group { id, .. }
|
| Channel::Group { id, .. }
|
||||||
| Channel::SavedMessages { id, .. }
|
| Channel::SavedMessages { id, .. }
|
||||||
| Channel::TextChannel { id, .. }
|
| Channel::TextChannel { id, .. }
|
||||||
| Channel::VoiceChannel { id, .. } => id.clone(),
|
| Channel::VoiceChannel { id, .. } => id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,6 +690,8 @@ impl Channel {
|
|||||||
username: name,
|
username: name,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
},
|
},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
self,
|
self,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
@@ -698,6 +702,8 @@ impl Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.remove_user_from_group(id, &user.id).await?;
|
||||||
|
|
||||||
EventV1::ChannelGroupLeave {
|
EventV1::ChannelGroupLeave {
|
||||||
id: id.to_string(),
|
id: id.to_string(),
|
||||||
user: user.id.to_string(),
|
user: user.id.to_string(),
|
||||||
@@ -723,6 +729,8 @@ impl Channel {
|
|||||||
username: &user.username,
|
username: &user.username,
|
||||||
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
avatar: user.avatar.as_ref().map(|file| file.id.as_ref()),
|
||||||
},
|
},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
self,
|
self,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ impl AbstractChannels for MongoDb {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Delete the channel itself
|
// Delete the channel itself
|
||||||
query!(self, delete_one_by_id, COL, &channel.id()).map(|_| ())
|
query!(self, delete_one_by_id, COL, channel.id()).map(|_| ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ impl AbstractChannels for ReferenceDb {
|
|||||||
/// Insert a new channel in the database
|
/// Insert a new channel in the database
|
||||||
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
|
||||||
let mut channels = self.channels.lock().await;
|
let mut channels = self.channels.lock().await;
|
||||||
if let Entry::Vacant(entry) = channels.entry(channel.id()) {
|
if let Entry::Vacant(entry) = channels.entry(channel.id().to_string()) {
|
||||||
entry.insert(channel.clone());
|
entry.insert(channel.clone());
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
@@ -148,7 +148,7 @@ impl AbstractChannels for ReferenceDb {
|
|||||||
// Delete a channel
|
// Delete a channel
|
||||||
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
|
||||||
let mut channels = self.channels.lock().await;
|
let mut channels = self.channels.lock().await;
|
||||||
if channels.remove(&channel.id()).is_some() {
|
if channels.remove(channel.id()).is_some() {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(create_error!(NotFound))
|
Err(create_error!(NotFound))
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use indexmap::{IndexMap, IndexSet};
|
use indexmap::{IndexMap, IndexSet};
|
||||||
use iso8601_timestamp::Timestamp;
|
use iso8601_timestamp::Timestamp;
|
||||||
use revolt_config::config;
|
use revolt_config::{config, FeaturesLimits};
|
||||||
use revolt_models::v0::{
|
use revolt_models::v0::{
|
||||||
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageSort, MessageWebhook,
|
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
|
||||||
PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
|
||||||
};
|
};
|
||||||
use revolt_permissions::{ChannelPermission, PermissionValue};
|
use revolt_permissions::{ChannelPermission, PermissionValue};
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
@@ -65,6 +65,13 @@ auto_derived_partial!(
|
|||||||
/// Name and / or avatar overrides for this message
|
/// Name and / or avatar overrides for this message
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub masquerade: Option<Masquerade>,
|
pub masquerade: Option<Masquerade>,
|
||||||
|
/// Whether or not the message in pinned
|
||||||
|
#[serde(skip_serializing_if = "crate::if_option_false")]
|
||||||
|
pub pinned: Option<bool>,
|
||||||
|
|
||||||
|
/// Bitfield of message flags
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub flags: Option<i32>,
|
||||||
},
|
},
|
||||||
"PartialMessage"
|
"PartialMessage"
|
||||||
);
|
);
|
||||||
@@ -95,6 +102,10 @@ auto_derived!(
|
|||||||
ChannelIconChanged { by: String },
|
ChannelIconChanged { by: String },
|
||||||
#[serde(rename = "channel_ownership_changed")]
|
#[serde(rename = "channel_ownership_changed")]
|
||||||
ChannelOwnershipChanged { from: String, to: String },
|
ChannelOwnershipChanged { from: String, to: String },
|
||||||
|
#[serde(rename = "message_pinned")]
|
||||||
|
MessagePinned { id: String, by: String },
|
||||||
|
#[serde(rename = "message_unpinned")]
|
||||||
|
MessageUnpinned { id: String, by: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name and / or avatar override information
|
/// Name and / or avatar override information
|
||||||
@@ -164,6 +175,8 @@ auto_derived!(
|
|||||||
pub author: Option<String>,
|
pub author: Option<String>,
|
||||||
/// Search query
|
/// Search query
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
/// Search for pinned
|
||||||
|
pub pinned: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Message Query
|
/// Message Query
|
||||||
@@ -179,6 +192,11 @@ auto_derived!(
|
|||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub time_period: MessageTimePeriod,
|
pub time_period: MessageTimePeriod,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Optional fields on message
|
||||||
|
pub enum FieldsMessage {
|
||||||
|
Pinned,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
#[allow(clippy::derivable_impls)]
|
#[allow(clippy::derivable_impls)]
|
||||||
@@ -200,6 +218,8 @@ impl Default for Message {
|
|||||||
reactions: Default::default(),
|
reactions: Default::default(),
|
||||||
interactions: Default::default(),
|
interactions: Default::default(),
|
||||||
masquerade: None,
|
masquerade: None,
|
||||||
|
flags: None,
|
||||||
|
pinned: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,11 +227,15 @@ impl Default for Message {
|
|||||||
#[allow(clippy::disallowed_methods)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
impl Message {
|
impl Message {
|
||||||
/// Create message from API data
|
/// Create message from API data
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn create_from_api(
|
pub async fn create_from_api(
|
||||||
db: &Database,
|
db: &Database,
|
||||||
channel: Channel,
|
channel: Channel,
|
||||||
data: DataMessageSend,
|
data: DataMessageSend,
|
||||||
author: MessageAuthor<'_>,
|
author: MessageAuthor<'_>,
|
||||||
|
user: Option<v0::User>,
|
||||||
|
member: Option<v0::Member>,
|
||||||
|
limits: FeaturesLimits,
|
||||||
mut idempotency: IdempotencyKey,
|
mut idempotency: IdempotencyKey,
|
||||||
generate_embeds: bool,
|
generate_embeds: bool,
|
||||||
allow_mentions: bool,
|
allow_mentions: bool,
|
||||||
@@ -221,7 +245,7 @@ impl Message {
|
|||||||
Message::validate_sum(
|
Message::validate_sum(
|
||||||
&data.content,
|
&data.content,
|
||||||
data.embeds.as_deref().unwrap_or_default(),
|
data.embeds.as_deref().unwrap_or_default(),
|
||||||
config.features.limits.default.message_length,
|
limits.message_length,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
idempotency
|
idempotency
|
||||||
@@ -237,6 +261,13 @@ impl Message {
|
|||||||
return Err(create_error!(EmptyMessage));
|
return Err(create_error!(EmptyMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure flags are either not set or have permissible values
|
||||||
|
if let Some(flags) = &data.flags {
|
||||||
|
if flags != &0 && flags != &1 {
|
||||||
|
return Err(create_error!(InvalidProperty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure restrict_reactions is not specified without reactions list
|
// Ensure restrict_reactions is not specified without reactions list
|
||||||
if let Some(interactions) = &data.interactions {
|
if let Some(interactions) = &data.interactions {
|
||||||
if interactions.restrict_reactions {
|
if interactions.restrict_reactions {
|
||||||
@@ -262,7 +293,7 @@ impl Message {
|
|||||||
let message_id = Ulid::new().to_string();
|
let message_id = Ulid::new().to_string();
|
||||||
let mut message = Message {
|
let mut message = Message {
|
||||||
id: message_id.clone(),
|
id: message_id.clone(),
|
||||||
channel: channel.id(),
|
channel: channel.id().to_string(),
|
||||||
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
|
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
|
||||||
interactions: data
|
interactions: data
|
||||||
.interactions
|
.interactions
|
||||||
@@ -270,6 +301,7 @@ impl Message {
|
|||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
author: author_id,
|
author: author_id,
|
||||||
webhook: webhook.map(|w| w.into()),
|
webhook: webhook.map(|w| w.into()),
|
||||||
|
flags: data.flags.map(|v| v as i32),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,9 +320,9 @@ impl Message {
|
|||||||
// Verify replies are valid.
|
// Verify replies are valid.
|
||||||
let mut replies = HashSet::new();
|
let mut replies = HashSet::new();
|
||||||
if let Some(entries) = data.replies {
|
if let Some(entries) = data.replies {
|
||||||
if entries.len() > config.features.limits.default.message_replies {
|
if entries.len() > config.features.limits.global.message_replies {
|
||||||
return Err(create_error!(TooManyReplies {
|
return Err(create_error!(TooManyReplies {
|
||||||
max: config.features.limits.default.message_replies,
|
max: config.features.limits.global.message_replies,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,20 +352,20 @@ impl Message {
|
|||||||
if data
|
if data
|
||||||
.attachments
|
.attachments
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|v| v.len() > config.features.limits.default.message_attachments)
|
.is_some_and(|v| v.len() > limits.message_attachments)
|
||||||
{
|
{
|
||||||
return Err(create_error!(TooManyAttachments {
|
return Err(create_error!(TooManyAttachments {
|
||||||
max: config.features.limits.default.message_attachments,
|
max: limits.message_attachments,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if data
|
if data
|
||||||
.embeds
|
.embeds
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|v| v.len() > config.features.limits.default.message_embeds)
|
.is_some_and(|v| v.len() > config.features.limits.global.message_embeds)
|
||||||
{
|
{
|
||||||
return Err(create_error!(TooManyEmbeds {
|
return Err(create_error!(TooManyEmbeds {
|
||||||
max: config.features.limits.default.message_embeds,
|
max: config.features.limits.global.message_embeds,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +392,9 @@ impl Message {
|
|||||||
message.nonce = Some(idempotency.into_key());
|
message.nonce = Some(idempotency.into_key());
|
||||||
|
|
||||||
// Send the message
|
// Send the message
|
||||||
message.send(db, author, &channel, generate_embeds).await?;
|
message
|
||||||
|
.send(db, author, user, member, &channel, generate_embeds)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
@@ -369,13 +403,15 @@ impl Message {
|
|||||||
pub async fn send_without_notifications(
|
pub async fn send_without_notifications(
|
||||||
&mut self,
|
&mut self,
|
||||||
db: &Database,
|
db: &Database,
|
||||||
|
user: Option<v0::User>,
|
||||||
|
member: Option<v0::Member>,
|
||||||
is_dm: bool,
|
is_dm: bool,
|
||||||
generate_embeds: bool,
|
generate_embeds: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
db.insert_message(self).await?;
|
db.insert_message(self).await?;
|
||||||
|
|
||||||
// Fan out events
|
// Fan out events
|
||||||
EventV1::Message(self.clone().into())
|
EventV1::Message(self.clone().into_model(user, member))
|
||||||
.p(self.channel.to_string())
|
.p(self.channel.to_string())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -416,29 +452,40 @@ impl Message {
|
|||||||
&mut self,
|
&mut self,
|
||||||
db: &Database,
|
db: &Database,
|
||||||
author: MessageAuthor<'_>,
|
author: MessageAuthor<'_>,
|
||||||
|
user: Option<v0::User>,
|
||||||
|
member: Option<v0::Member>,
|
||||||
channel: &Channel,
|
channel: &Channel,
|
||||||
generate_embeds: bool,
|
generate_embeds: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.send_without_notifications(
|
self.send_without_notifications(
|
||||||
db,
|
db,
|
||||||
|
user.clone(),
|
||||||
|
member.clone(),
|
||||||
matches!(channel, Channel::DirectMessage { .. }),
|
matches!(channel, Channel::DirectMessage { .. }),
|
||||||
generate_embeds,
|
generate_embeds,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Push out Web Push notifications
|
if !self.has_suppressed_notifications() {
|
||||||
crate::tasks::web_push::queue(
|
// Push out Web Push notifications
|
||||||
{
|
crate::tasks::web_push::queue(
|
||||||
match channel {
|
{
|
||||||
Channel::DirectMessage { recipients, .. }
|
match channel {
|
||||||
| Channel::Group { recipients, .. } => recipients.clone(),
|
Channel::DirectMessage { recipients, .. }
|
||||||
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
|
| Channel::Group { recipients, .. } => recipients.clone(),
|
||||||
_ => vec![],
|
Channel::TextChannel { .. } => self.mentions.clone().unwrap_or_default(),
|
||||||
}
|
_ => vec![],
|
||||||
},
|
}
|
||||||
PushNotification::from(self.clone().into(), Some(author), &channel.id()).await,
|
},
|
||||||
)
|
PushNotification::from(
|
||||||
.await;
|
self.clone().into_model(user, member),
|
||||||
|
Some(author),
|
||||||
|
channel.id(),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -470,15 +517,37 @@ impl Message {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this message has suppressed notifications
|
||||||
|
pub fn has_suppressed_notifications(&self) -> bool {
|
||||||
|
if let Some(flags) = self.flags {
|
||||||
|
flags & MessageFlags::SuppressNotifications as i32
|
||||||
|
== MessageFlags::SuppressNotifications as i32
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Update message data
|
/// Update message data
|
||||||
pub async fn update(&mut self, db: &Database, partial: PartialMessage) -> Result<()> {
|
pub async fn update(
|
||||||
|
&mut self,
|
||||||
|
db: &Database,
|
||||||
|
partial: PartialMessage,
|
||||||
|
remove: Vec<FieldsMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
self.apply_options(partial.clone());
|
self.apply_options(partial.clone());
|
||||||
db.update_message(&self.id, &partial).await?;
|
|
||||||
|
for field in &remove {
|
||||||
|
self.remove_field(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update_message(&self.id, &partial, remove.clone())
|
||||||
|
.await?;
|
||||||
|
|
||||||
EventV1::MessageUpdate {
|
EventV1::MessageUpdate {
|
||||||
id: self.id.clone(),
|
id: self.id.clone(),
|
||||||
channel: self.channel.clone(),
|
channel: self.channel.clone(),
|
||||||
data: partial.into(),
|
data: partial.into(),
|
||||||
|
clear: remove.into_iter().map(|field| field.into()).collect(),
|
||||||
}
|
}
|
||||||
.p(self.channel.clone())
|
.p(self.channel.clone())
|
||||||
.await;
|
.await;
|
||||||
@@ -498,7 +567,7 @@ impl Message {
|
|||||||
.fetch_messages(query)
|
.fetch_messages(query)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Into::into)
|
.map(|msg| msg.into_model(None, None))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if let Some(true) = include_users {
|
if let Some(true) = include_users {
|
||||||
@@ -529,6 +598,12 @@ impl Message {
|
|||||||
users.push(id.clone());
|
users.push(id.clone());
|
||||||
}
|
}
|
||||||
v0::SystemMessage::Text { .. } => {}
|
v0::SystemMessage::Text { .. } => {}
|
||||||
|
v0::SystemMessage::MessagePinned { by, .. } => {
|
||||||
|
users.push(by.clone());
|
||||||
|
}
|
||||||
|
v0::SystemMessage::MessageUnpinned { by, .. } => {
|
||||||
|
users.push(by.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
users
|
users
|
||||||
@@ -616,7 +691,7 @@ impl Message {
|
|||||||
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
|
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
|
||||||
// Check how many reactions are already on the message
|
// Check how many reactions are already on the message
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
if self.reactions.len() >= config.features.limits.default.message_reactions
|
if self.reactions.len() >= config.features.limits.global.message_reactions
|
||||||
&& !self.reactions.contains_key(emoji)
|
&& !self.reactions.contains_key(emoji)
|
||||||
{
|
{
|
||||||
return Err(create_error!(InvalidOperation));
|
return Err(create_error!(InvalidOperation));
|
||||||
@@ -758,6 +833,12 @@ impl Message {
|
|||||||
// Write to database
|
// Write to database
|
||||||
db.clear_reaction(&self.id, emoji).await
|
db.clear_reaction(&self.id, emoji).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_field(&mut self, field: &FieldsMessage) {
|
||||||
|
match field {
|
||||||
|
FieldsMessage::Pinned => self.pinned = None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemMessage {
|
impl SystemMessage {
|
||||||
@@ -781,7 +862,7 @@ impl Interactions {
|
|||||||
if let Some(reactions) = &self.reactions {
|
if let Some(reactions) = &self.reactions {
|
||||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
permissions.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
||||||
|
|
||||||
if reactions.len() > config.features.limits.default.message_reactions {
|
if reactions.len() > config.features.limits.global.message_reactions {
|
||||||
return Err(create_error!(InvalidOperation));
|
return Err(create_error!(InvalidOperation));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
|
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
|
||||||
|
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
@@ -20,7 +20,7 @@ pub trait AbstractMessages: Sync + Send {
|
|||||||
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
|
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
|
||||||
|
|
||||||
/// Update a given message with new information
|
/// Update a given message with new information
|
||||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()>;
|
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()>;
|
||||||
|
|
||||||
/// Append information to a given message
|
/// Append information to a given message
|
||||||
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
|
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use revolt_models::v0::MessageSort;
|
|||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
|
AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath, Message, MessageQuery,
|
||||||
|
MessageTimePeriod, MongoDb, PartialMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::AbstractMessages;
|
use super::AbstractMessages;
|
||||||
@@ -50,6 +51,10 @@ impl AbstractMessages for MongoDb {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(pinned) = query.filter.pinned {
|
||||||
|
filter.insert("pinned", pinned);
|
||||||
|
};
|
||||||
|
|
||||||
// 2. Find query limit
|
// 2. Find query limit
|
||||||
let limit = query.limit.unwrap_or(50);
|
let limit = query.limit.unwrap_or(50);
|
||||||
|
|
||||||
@@ -177,8 +182,22 @@ impl AbstractMessages for MongoDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update a given message with new information
|
/// Update a given message with new information
|
||||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
async fn update_message(
|
||||||
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
|
&self,
|
||||||
|
id: &str,
|
||||||
|
message: &PartialMessage,
|
||||||
|
remove: Vec<FieldsMessage>,
|
||||||
|
) -> Result<()> {
|
||||||
|
query!(
|
||||||
|
self,
|
||||||
|
update_one_by_id,
|
||||||
|
COL,
|
||||||
|
id,
|
||||||
|
message,
|
||||||
|
remove.iter().map(|x| x as &dyn IntoDocumentPath).collect(),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append information to a given message
|
/// Append information to a given message
|
||||||
@@ -296,6 +315,14 @@ impl AbstractMessages for MongoDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoDocumentPath for FieldsMessage {
|
||||||
|
fn as_path(&self) -> Option<&'static str> {
|
||||||
|
Some(match self {
|
||||||
|
FieldsMessage::Pinned => "pinned",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MongoDb {
|
impl MongoDb {
|
||||||
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
|
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
|
||||||
let mut for_attachments = projection.clone();
|
let mut for_attachments = projection.clone();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use futures::future::try_join_all;
|
|||||||
use indexmap::IndexSet;
|
use indexmap::IndexSet;
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
|
||||||
|
|
||||||
use super::AbstractMessages;
|
use super::AbstractMessages;
|
||||||
|
|
||||||
@@ -56,6 +56,12 @@ impl AbstractMessages for ReferenceDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(pinned) = query.filter.pinned {
|
||||||
|
if message.pinned.unwrap_or_default() == pinned {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -183,10 +189,15 @@ impl AbstractMessages for ReferenceDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update a given message with new information
|
/// Update a given message with new information
|
||||||
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
|
async fn update_message(&self, id: &str, message: &PartialMessage, remove: Vec<FieldsMessage>) -> Result<()> {
|
||||||
let mut messages = self.messages.lock().await;
|
let mut messages = self.messages.lock().await;
|
||||||
if let Some(message_data) = messages.get_mut(id) {
|
if let Some(message_data) = messages.get_mut(id) {
|
||||||
message_data.apply_options(message.to_owned());
|
message_data.apply_options(message.to_owned());
|
||||||
|
|
||||||
|
for field in remove {
|
||||||
|
#[allow(clippy::disallowed_methods)]
|
||||||
|
message_data.remove_field(&field);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(create_error!(NotFound))
|
Err(create_error!(NotFound))
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ impl Member {
|
|||||||
server: &Server,
|
server: &Server,
|
||||||
user: &User,
|
user: &User,
|
||||||
channels: Option<Vec<Channel>>,
|
channels: Option<Vec<Channel>>,
|
||||||
) -> Result<Vec<Channel>> {
|
) -> Result<(Member, Vec<Channel>)> {
|
||||||
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
|
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
|
||||||
return Err(create_error!(Banned));
|
return Err(create_error!(Banned));
|
||||||
}
|
}
|
||||||
@@ -150,12 +150,12 @@ impl Member {
|
|||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
}
|
}
|
||||||
.into_message(id.to_string())
|
.into_message(id.to_string())
|
||||||
.send_without_notifications(db, false, false)
|
.send_without_notifications(db, None, None, false, false)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(channels)
|
Ok((member, channels))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update member data
|
/// Update member data
|
||||||
@@ -229,6 +229,7 @@ impl Member {
|
|||||||
EventV1::ServerMemberLeave {
|
EventV1::ServerMemberLeave {
|
||||||
id: self.id.server.to_string(),
|
id: self.id.server.to_string(),
|
||||||
user: self.id.user.to_string(),
|
user: self.id.user.to_string(),
|
||||||
|
reason: intention.clone().into(),
|
||||||
}
|
}
|
||||||
.p(self.id.server.to_string())
|
.p(self.id.server.to_string())
|
||||||
.await;
|
.await;
|
||||||
@@ -250,7 +251,7 @@ impl Member {
|
|||||||
}
|
}
|
||||||
.into_message(id.to_string())
|
.into_message(id.to_string())
|
||||||
// TODO: support notifications here in the future?
|
// TODO: support notifications here in the future?
|
||||||
.send_without_notifications(db, false, false)
|
.send_without_notifications(db, None, None, false, false)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ impl Server {
|
|||||||
vec![]
|
vec![]
|
||||||
};
|
};
|
||||||
|
|
||||||
server.channels = channels.iter().map(|c| c.id()).collect();
|
server.channels = channels.iter().map(|c| c.id().to_string()).collect();
|
||||||
db.insert_server(&server).await?;
|
db.insert_server(&server).await?;
|
||||||
Ok((server, channels))
|
Ok((server, channels))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,14 +214,21 @@ impl MongoDb {
|
|||||||
|
|
||||||
// Delete all emoji.
|
// Delete all emoji.
|
||||||
self.col::<Document>("emojis")
|
self.col::<Document>("emojis")
|
||||||
.delete_many(
|
.update_many(
|
||||||
doc! {
|
doc! {
|
||||||
"parent.id": &server_id
|
"parent.id": &server_id
|
||||||
},
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"parent": {
|
||||||
|
"type": "Detached"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| create_database_error!("delete_many", "emojis"))?;
|
.map_err(|_| create_database_error!("update_many", "emojis"))?;
|
||||||
|
|
||||||
// Delete all channels.
|
// Delete all channels.
|
||||||
self.col::<Document>("channels")
|
self.col::<Document>("channels")
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::{collections::HashSet, time::Duration};
|
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||||
|
|
||||||
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
|
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
|
||||||
|
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use revolt_config::config;
|
use revolt_config::{config, FeaturesLimits};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
use revolt_presence::filter_online;
|
use revolt_presence::filter_online;
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
@@ -61,6 +61,7 @@ auto_derived!(
|
|||||||
StatusPresence,
|
StatusPresence,
|
||||||
ProfileContent,
|
ProfileContent,
|
||||||
ProfileBackground,
|
ProfileBackground,
|
||||||
|
DisplayName,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User's relationship with another user (or themselves)
|
/// User's relationship with another user (or themselves)
|
||||||
@@ -197,6 +198,22 @@ impl User {
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get limits for this user
|
||||||
|
pub async fn limits(&self) -> FeaturesLimits {
|
||||||
|
let config = config().await;
|
||||||
|
if ulid::Ulid::from_str(&self.id)
|
||||||
|
.expect("`ulid`")
|
||||||
|
.datetime()
|
||||||
|
.elapsed()
|
||||||
|
.expect("time went backwards")
|
||||||
|
<= Duration::from_secs(86400u64 * config.features.limits.global.new_user_days as u64)
|
||||||
|
{
|
||||||
|
config.features.limits.new_user
|
||||||
|
} else {
|
||||||
|
config.features.limits.default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the relationship with another user
|
/// Get the relationship with another user
|
||||||
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
|
pub fn relationship_with(&self, user_b: &str) -> RelationshipStatus {
|
||||||
if self.id == user_b {
|
if self.id == user_b {
|
||||||
@@ -235,12 +252,11 @@ impl User {
|
|||||||
|
|
||||||
/// Check if this user can acquire another server
|
/// Check if this user can acquire another server
|
||||||
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
|
pub async fn can_acquire_server(&self, db: &Database) -> Result<()> {
|
||||||
let config = config().await;
|
if db.fetch_server_count(&self.id).await? <= self.limits().await.servers {
|
||||||
if db.fetch_server_count(&self.id).await? <= config.features.limits.default.servers {
|
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(create_error!(TooManyServers {
|
Err(create_error!(TooManyServers {
|
||||||
max: config.features.limits.default.servers
|
max: self.limits().await.servers
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,24 +486,6 @@ impl User {
|
|||||||
|
|
||||||
/// Add another user as a friend
|
/// Add another user as a friend
|
||||||
pub async fn add_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
|
pub async fn add_friend(&mut self, db: &Database, target: &mut User) -> Result<()> {
|
||||||
let count = self
|
|
||||||
.relations
|
|
||||||
.as_ref()
|
|
||||||
.map(|relations| {
|
|
||||||
relations
|
|
||||||
.iter()
|
|
||||||
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
|
|
||||||
.count()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let config = config().await;
|
|
||||||
if count >= config.features.limits.default.outgoing_friend_requests {
|
|
||||||
return Err(create_error!(TooManyPendingFriendRequests {
|
|
||||||
max: config.features.limits.default.outgoing_friend_requests
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.relationship_with(&target.id) {
|
match self.relationship_with(&target.id) {
|
||||||
RelationshipStatus::User => Err(create_error!(NoEffect)),
|
RelationshipStatus::User => Err(create_error!(NoEffect)),
|
||||||
RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
|
RelationshipStatus::Friend => Err(create_error!(AlreadyFriends)),
|
||||||
@@ -495,6 +493,7 @@ impl User {
|
|||||||
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
|
RelationshipStatus::Blocked => Err(create_error!(Blocked)),
|
||||||
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
|
RelationshipStatus::BlockedOther => Err(create_error!(BlockedByOther)),
|
||||||
RelationshipStatus::Incoming => {
|
RelationshipStatus::Incoming => {
|
||||||
|
// Accept incoming friend request
|
||||||
self.apply_relationship(
|
self.apply_relationship(
|
||||||
db,
|
db,
|
||||||
target,
|
target,
|
||||||
@@ -504,6 +503,26 @@ impl User {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
RelationshipStatus::None => {
|
RelationshipStatus::None => {
|
||||||
|
// Get this user's current count of outgoing friend requests
|
||||||
|
let count = self
|
||||||
|
.relations
|
||||||
|
.as_ref()
|
||||||
|
.map(|relations| {
|
||||||
|
relations
|
||||||
|
.iter()
|
||||||
|
.filter(|r| matches!(r.status, RelationshipStatus::Outgoing))
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// If we're over the limit, don't allow creating more requests
|
||||||
|
if count >= self.limits().await.outgoing_friend_requests {
|
||||||
|
return Err(create_error!(TooManyPendingFriendRequests {
|
||||||
|
max: self.limits().await.outgoing_friend_requests
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the friend request
|
||||||
self.apply_relationship(
|
self.apply_relationship(
|
||||||
db,
|
db,
|
||||||
target,
|
target,
|
||||||
@@ -639,9 +658,18 @@ impl User {
|
|||||||
x.background = None;
|
x.background = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FieldsUser::DisplayName => self.display_name = None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Suspend the user
|
||||||
|
pub async fn suspend(&mut self, db: &Database) -> Result<()> {
|
||||||
|
// Remove sessions (logout all)
|
||||||
|
// Mark user as suspended
|
||||||
|
// Disable account
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark as deleted
|
/// Mark as deleted
|
||||||
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
|
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
|
||||||
self.update(
|
self.update(
|
||||||
|
|||||||
@@ -58,4 +58,7 @@ pub trait AbstractUsers: Sync + Send {
|
|||||||
|
|
||||||
/// Delete a user by their id
|
/// Delete a user by their id
|
||||||
async fn delete_user(&self, id: &str) -> Result<()>;
|
async fn delete_user(&self, id: &str) -> Result<()>;
|
||||||
|
|
||||||
|
/// Remove push subscription for a session by session id (TODO: remove)
|
||||||
|
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,6 +316,25 @@ impl AbstractUsers for MongoDb {
|
|||||||
async fn delete_user(&self, id: &str) -> Result<()> {
|
async fn delete_user(&self, id: &str) -> Result<()> {
|
||||||
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
query!(self, delete_one_by_id, COL, id).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove push subscription for a session by session id (TODO: remove)
|
||||||
|
async fn remove_push_subscription_by_session_id(&self, session_id: &str) -> Result<()> {
|
||||||
|
self.col::<User>("sessions")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": session_id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$unset": {
|
||||||
|
"subscription": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| create_database_error!("update_one", COL))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDocumentPath for FieldsUser {
|
impl IntoDocumentPath for FieldsUser {
|
||||||
@@ -326,6 +345,7 @@ impl IntoDocumentPath for FieldsUser {
|
|||||||
FieldsUser::ProfileContent => "profile.content",
|
FieldsUser::ProfileContent => "profile.content",
|
||||||
FieldsUser::StatusPresence => "status.presence",
|
FieldsUser::StatusPresence => "status.presence",
|
||||||
FieldsUser::StatusText => "status.text",
|
FieldsUser::StatusText => "status.text",
|
||||||
|
FieldsUser::DisplayName => "display_name",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,4 +163,9 @@ impl AbstractUsers for ReferenceDb {
|
|||||||
Err(create_error!(NotFound))
|
Err(create_error!(NotFound))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove push subscription for a session by session id (TODO: remove)
|
||||||
|
async fn remove_push_subscription_by_session_id(&self, _session_id: &str) -> Result<()> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use deadqueue::limited::Queue;
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::{collections::HashMap, time::Duration};
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
use super::DelayedTask;
|
use revolt_result::Result;
|
||||||
|
|
||||||
|
use super::{apple_notifications::{self, ApnJob}, DelayedTask};
|
||||||
|
|
||||||
/// Enumeration of possible events
|
/// Enumeration of possible events
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
@@ -52,8 +54,43 @@ pub async fn queue(channel: String, user: String, event: AckEvent) {
|
|||||||
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_ack_event(event: &AckEvent, db: &Database, authifier_db: &authifier::Database, user: &str, channel: &str) -> Result<()> {
|
||||||
|
match &event {
|
||||||
|
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
|
||||||
|
AckEvent::AckMessage { id } => {
|
||||||
|
let unread = db.fetch_unread(user, channel).await?;
|
||||||
|
let updated = db.acknowledge_message(channel, user, id).await?;
|
||||||
|
|
||||||
|
if let (Some(before), Some(after)) = (unread, updated) {
|
||||||
|
let before_mentions = before.mentions.unwrap_or_default().len();
|
||||||
|
let after_mentions = after.mentions.unwrap_or_default().len();
|
||||||
|
|
||||||
|
let mentions_acked = before_mentions - after_mentions;
|
||||||
|
|
||||||
|
if mentions_acked > 0 {
|
||||||
|
if let Ok(sessions) = authifier_db.find_sessions(user).await {
|
||||||
|
for session in sessions {
|
||||||
|
if let Some(sub) = session.subscription {
|
||||||
|
if sub.endpoint == "apn" {
|
||||||
|
apple_notifications::queue(ApnJob::from_ack(session.id, user.to_string(), sub.auth)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AckEvent::AddMention { ids } => {
|
||||||
|
db.add_mention_to_unread(channel, user, ids).await?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Start a new worker
|
/// Start a new worker
|
||||||
pub async fn worker(db: Database) {
|
pub async fn worker(db: Database, authifier_db: authifier::Database) {
|
||||||
let mut tasks = HashMap::<(String, String), DelayedTask<Task>>::new();
|
let mut tasks = HashMap::<(String, String), DelayedTask<Task>>::new();
|
||||||
let mut keys = vec![];
|
let mut keys = vec![];
|
||||||
|
|
||||||
@@ -71,13 +108,7 @@ pub async fn worker(db: Database) {
|
|||||||
let Task { event } = task.data;
|
let Task { event } = task.data;
|
||||||
let (user, channel) = key;
|
let (user, channel) = key;
|
||||||
|
|
||||||
if let Err(err) = match &event {
|
if let Err(err) = handle_ack_event(&event, &db, &authifier_db, user, channel).await {
|
||||||
#[allow(clippy::disallowed_methods)] // event is sent by higher level function
|
|
||||||
AckEvent::AckMessage { id } => db.acknowledge_message(channel, user, id).await,
|
|
||||||
AckEvent::AddMention { ids } => {
|
|
||||||
db.add_mention_to_unread(channel, user, ids).await
|
|
||||||
}
|
|
||||||
} {
|
|
||||||
error!("{err:?} for {event:?}. ({user}, {channel})");
|
error!("{err:?} for {event:?}. ({user}, {channel})");
|
||||||
} else {
|
} else {
|
||||||
info!("User {user} ack in {channel} with {event:?}");
|
info!("User {user} ack in {channel} with {event:?}");
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use base64::{
|
||||||
|
engine::{self},
|
||||||
|
Engine as _,
|
||||||
|
};
|
||||||
|
use deadqueue::limited::Queue;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use revolt_a2::{
|
||||||
|
request::{
|
||||||
|
notification::{DefaultAlert, NotificationOptions},
|
||||||
|
payload::{APSAlert, APSSound, PayloadLike, APS},
|
||||||
|
},
|
||||||
|
Client, ClientConfig, Endpoint, Error, ErrorBody, ErrorReason, Priority, PushType, Response,
|
||||||
|
};
|
||||||
|
use revolt_config::config;
|
||||||
|
use revolt_models::v0::{Message, PushNotification};
|
||||||
|
|
||||||
|
use crate::Database;
|
||||||
|
|
||||||
|
/// Payload information, before assembly
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApnPayload {
|
||||||
|
message: Message,
|
||||||
|
url: String,
|
||||||
|
authorAvatar: String,
|
||||||
|
authorDisplayName: String,
|
||||||
|
channelName: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct Payload<'a> {
|
||||||
|
aps: APS<'a>,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
options: NotificationOptions<'a>,
|
||||||
|
#[serde(skip_serializing)]
|
||||||
|
device_token: &'a str,
|
||||||
|
|
||||||
|
message: &'a Message,
|
||||||
|
url: &'a str,
|
||||||
|
authorAvatar: &'a str,
|
||||||
|
authorDisplayName: &'a str,
|
||||||
|
channelName: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PayloadLike for Payload<'a> {
|
||||||
|
fn get_device_token(&self) -> &'a str {
|
||||||
|
self.device_token
|
||||||
|
}
|
||||||
|
fn get_options(&self) -> &NotificationOptions {
|
||||||
|
&self.options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Task information
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AlertJob {
|
||||||
|
/// Session Id
|
||||||
|
session_id: String,
|
||||||
|
|
||||||
|
/// Device token
|
||||||
|
device_token: String,
|
||||||
|
|
||||||
|
/// User Id
|
||||||
|
user_id: String,
|
||||||
|
|
||||||
|
/// Title
|
||||||
|
title: String,
|
||||||
|
|
||||||
|
/// Body
|
||||||
|
body: String,
|
||||||
|
|
||||||
|
/// Thread Id
|
||||||
|
thread_id: String,
|
||||||
|
|
||||||
|
/// Category (informs the client what kind of notification is being sent.)
|
||||||
|
category: String,
|
||||||
|
|
||||||
|
/// Payload used by the iOS client to modify the notification
|
||||||
|
custom_payload: ApnPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlertJob {
|
||||||
|
fn format_title(notification: &PushNotification) -> String {
|
||||||
|
// ideally this changes depending on context
|
||||||
|
// in a server, it would look like "Sendername, #channelname in servername"
|
||||||
|
// in a group, it would look like "Sendername in groupname"
|
||||||
|
// in a dm it should just be "Sendername".
|
||||||
|
// not sure how feasible all those are given the PushNotification object as it currently stands.
|
||||||
|
format!(
|
||||||
|
"{} in {}",
|
||||||
|
notification.author, notification.message.channel
|
||||||
|
) // TODO: this absolutely needs a channel name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BadgeJob {
|
||||||
|
/// Session Id
|
||||||
|
session_id: String,
|
||||||
|
|
||||||
|
/// Device token
|
||||||
|
device_token: String,
|
||||||
|
|
||||||
|
/// User Id
|
||||||
|
user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum JobType {
|
||||||
|
Alert(AlertJob),
|
||||||
|
Badge(BadgeJob),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ApnJob {
|
||||||
|
job_type: JobType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApnJob {
|
||||||
|
pub fn from_notification(
|
||||||
|
session_id: String,
|
||||||
|
user_id: String,
|
||||||
|
device_token: String,
|
||||||
|
notification: &PushNotification,
|
||||||
|
) -> ApnJob {
|
||||||
|
ApnJob {
|
||||||
|
job_type: JobType::Alert(AlertJob {
|
||||||
|
session_id,
|
||||||
|
device_token,
|
||||||
|
user_id,
|
||||||
|
title: AlertJob::format_title(notification),
|
||||||
|
body: notification.body.to_string(),
|
||||||
|
thread_id: notification.tag.to_string(),
|
||||||
|
category: "ALERT_MESSAGE".to_string(),
|
||||||
|
custom_payload: ApnPayload {
|
||||||
|
message: notification.message.clone(),
|
||||||
|
url: notification.url.clone(),
|
||||||
|
authorAvatar: notification.icon.clone(),
|
||||||
|
authorDisplayName: notification.author.clone(),
|
||||||
|
channelName: "#fetchchannelnamehere".to_string(), // TODO: get actual channel name
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_ack(session_id: String, user_id: String, device_token: String) -> ApnJob {
|
||||||
|
ApnJob {
|
||||||
|
job_type: JobType::Badge(BadgeJob {
|
||||||
|
session_id,
|
||||||
|
device_token,
|
||||||
|
user_id,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AssembledPayload<'a> {
|
||||||
|
Alert(Payload<'a>),
|
||||||
|
Default(revolt_a2::request::payload::Payload<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
static Q: Lazy<Queue<ApnJob>> = Lazy::new(|| Queue::new(10_000));
|
||||||
|
|
||||||
|
/// Queue a new task for a worker
|
||||||
|
pub async fn queue(task: ApnJob) {
|
||||||
|
Q.try_push(task).ok();
|
||||||
|
info!("Queue is using {} slots from {}.", Q.len(), Q.capacity());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_badge_count(db: &Database, user: &str) -> Option<u32> {
|
||||||
|
if let Ok(unreads) = db.fetch_unreads(user).await {
|
||||||
|
let mut mention_count = 0;
|
||||||
|
for channel in unreads {
|
||||||
|
if let Some(mentions) = channel.mentions {
|
||||||
|
mention_count += mentions.len() as u32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Some(mention_count);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start a new worker
|
||||||
|
pub async fn worker(db: Database) {
|
||||||
|
let config = config().await;
|
||||||
|
if config.api.apn.pkcs8.is_empty()
|
||||||
|
|| config.api.apn.key_id.is_empty()
|
||||||
|
|| config.api.apn.team_id.is_empty()
|
||||||
|
{
|
||||||
|
eprintln!("Missing APN keys.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let endpoint = if config.api.apn.sandbox {
|
||||||
|
Endpoint::Sandbox
|
||||||
|
} else {
|
||||||
|
Endpoint::Production
|
||||||
|
};
|
||||||
|
|
||||||
|
let pkcs8 = engine::general_purpose::STANDARD
|
||||||
|
.decode(config.api.apn.pkcs8)
|
||||||
|
.expect("valid `pcks8`");
|
||||||
|
|
||||||
|
let client_config = ClientConfig::new(endpoint);
|
||||||
|
|
||||||
|
let client = Client::token(
|
||||||
|
&mut Cursor::new(pkcs8),
|
||||||
|
config.api.apn.key_id,
|
||||||
|
config.api.apn.team_id,
|
||||||
|
client_config,
|
||||||
|
)
|
||||||
|
.expect("could not create APN client");
|
||||||
|
|
||||||
|
let payload_options = NotificationOptions {
|
||||||
|
apns_id: None,
|
||||||
|
apns_push_type: Some(PushType::Alert),
|
||||||
|
apns_expiration: None,
|
||||||
|
apns_priority: Some(Priority::High),
|
||||||
|
apns_topic: Some("chat.revolt.app"),
|
||||||
|
apns_collapse_id: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let task = Q.pop().await;
|
||||||
|
let payload: AssembledPayload;
|
||||||
|
|
||||||
|
match task.job_type {
|
||||||
|
JobType::Alert(ref alert) => {
|
||||||
|
payload = AssembledPayload::Alert(Payload {
|
||||||
|
aps: APS {
|
||||||
|
alert: Some(APSAlert::Default(DefaultAlert {
|
||||||
|
title: Some(&alert.title),
|
||||||
|
subtitle: None,
|
||||||
|
body: Some(&alert.body),
|
||||||
|
title_loc_key: None,
|
||||||
|
title_loc_args: None,
|
||||||
|
action_loc_key: None,
|
||||||
|
loc_key: None,
|
||||||
|
loc_args: None,
|
||||||
|
launch_image: None,
|
||||||
|
})),
|
||||||
|
badge: get_badge_count(&db, &alert.user_id).await,
|
||||||
|
sound: Some(APSSound::Sound("default")),
|
||||||
|
thread_id: Some(&alert.thread_id),
|
||||||
|
content_available: None,
|
||||||
|
category: Some(&alert.category),
|
||||||
|
mutable_content: Some(1),
|
||||||
|
url_args: None,
|
||||||
|
},
|
||||||
|
device_token: &alert.device_token,
|
||||||
|
options: payload_options.clone(),
|
||||||
|
message: &alert.custom_payload.message,
|
||||||
|
url: &alert.custom_payload.url,
|
||||||
|
authorAvatar: &alert.custom_payload.authorAvatar,
|
||||||
|
authorDisplayName: &alert.custom_payload.authorDisplayName,
|
||||||
|
channelName: &alert.custom_payload.channelName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
JobType::Badge(ref alert) => {
|
||||||
|
payload = AssembledPayload::Default(revolt_a2::request::payload::Payload {
|
||||||
|
aps: APS {
|
||||||
|
alert: None,
|
||||||
|
badge: get_badge_count(&db, &alert.user_id).await,
|
||||||
|
sound: None,
|
||||||
|
thread_id: None,
|
||||||
|
content_available: None,
|
||||||
|
category: None,
|
||||||
|
mutable_content: None,
|
||||||
|
url_args: None,
|
||||||
|
},
|
||||||
|
device_token: &alert.device_token,
|
||||||
|
options: payload_options.clone(),
|
||||||
|
data: std::collections::BTreeMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = match payload {
|
||||||
|
AssembledPayload::Alert(p) => client.send(p).await,
|
||||||
|
AssembledPayload::Default(p) => client.send(p).await,
|
||||||
|
};
|
||||||
|
//println!("response from APNS: {:?}", resp);
|
||||||
|
|
||||||
|
if let Err(err) = resp {
|
||||||
|
match err {
|
||||||
|
Error::ResponseError(Response {
|
||||||
|
error:
|
||||||
|
Some(ErrorBody {
|
||||||
|
reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered,
|
||||||
|
..
|
||||||
|
}),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if let Err(err) = db
|
||||||
|
.remove_push_subscription_by_session_id(match task.job_type {
|
||||||
|
JobType::Alert(ref a) => &a.session_id.as_str(),
|
||||||
|
JobType::Badge(ref a) => &a.session_id.as_str(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
revolt_config::capture_error(&err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err => {
|
||||||
|
revolt_config::capture_error(&err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,14 +8,17 @@ use std::time::Instant;
|
|||||||
const WORKER_COUNT: usize = 5;
|
const WORKER_COUNT: usize = 5;
|
||||||
|
|
||||||
pub mod ack;
|
pub mod ack;
|
||||||
|
pub mod apple_notifications;
|
||||||
pub mod last_message_id;
|
pub mod last_message_id;
|
||||||
pub mod process_embeds;
|
pub mod process_embeds;
|
||||||
pub mod web_push;
|
pub mod web_push;
|
||||||
|
|
||||||
/// Spawn background workers
|
/// Spawn background workers
|
||||||
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
|
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
|
||||||
|
task::spawn(apple_notifications::worker(db.clone()));
|
||||||
|
|
||||||
for _ in 0..WORKER_COUNT {
|
for _ in 0..WORKER_COUNT {
|
||||||
task::spawn(ack::worker(db.clone()));
|
task::spawn(ack::worker(db.clone(), authifier_db.clone()));
|
||||||
task::spawn(last_message_id::worker(db.clone()));
|
task::spawn(last_message_id::worker(db.clone()));
|
||||||
task::spawn(process_embeds::worker(db.clone()));
|
task::spawn(process_embeds::worker(db.clone()));
|
||||||
task::spawn(web_push::worker(authifier_db.clone()));
|
task::spawn(web_push::worker(authifier_db.clone()));
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ pub async fn worker(db: Database) {
|
|||||||
let embeds = generate(
|
let embeds = generate(
|
||||||
task.content,
|
task.content,
|
||||||
&config.hosts.january,
|
&config.hosts.january,
|
||||||
config.features.limits.default.message_embeds,
|
config.features.limits.global.message_embeds,
|
||||||
semaphore,
|
semaphore,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use std::collections::HashSet;
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
use authifier::Database;
|
use authifier::Database;
|
||||||
use base64::{
|
use base64::{
|
||||||
@@ -6,6 +9,7 @@ use base64::{
|
|||||||
Engine as _,
|
Engine as _,
|
||||||
};
|
};
|
||||||
use deadqueue::limited::Queue;
|
use deadqueue::limited::Queue;
|
||||||
|
use fcm_v1::auth::{Authenticator, ServiceAccountKey};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use revolt_config::config;
|
use revolt_config::config;
|
||||||
use revolt_models::v0::PushNotification;
|
use revolt_models::v0::PushNotification;
|
||||||
@@ -16,6 +20,8 @@ use web_push::{
|
|||||||
WebPushClient, WebPushMessageBuilder,
|
WebPushClient, WebPushMessageBuilder,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::apple_notifications;
|
||||||
|
|
||||||
/// Task information
|
/// Task information
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct PushTask {
|
struct PushTask {
|
||||||
@@ -52,10 +58,28 @@ pub async fn worker(db: Database) {
|
|||||||
let config = config().await;
|
let config = config().await;
|
||||||
|
|
||||||
let web_push_client = IsahcWebPushClient::new().unwrap();
|
let web_push_client = IsahcWebPushClient::new().unwrap();
|
||||||
let fcm_client = if config.api.fcm.api_key.is_empty() {
|
let fcm_client = if config.api.fcm.key_type.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(fcm::Client::new())
|
Some(fcm_v1::Client::new(
|
||||||
|
Authenticator::service_account::<&str>(ServiceAccountKey {
|
||||||
|
key_type: Some(config.api.fcm.key_type),
|
||||||
|
project_id: Some(config.api.fcm.project_id.clone()),
|
||||||
|
private_key_id: Some(config.api.fcm.private_key_id),
|
||||||
|
private_key: config.api.fcm.private_key,
|
||||||
|
client_email: config.api.fcm.client_email,
|
||||||
|
client_id: Some(config.api.fcm.client_id),
|
||||||
|
auth_uri: Some(config.api.fcm.auth_uri),
|
||||||
|
token_uri: config.api.fcm.token_uri,
|
||||||
|
auth_provider_x509_cert_url: Some(config.api.fcm.auth_provider_x509_cert_url),
|
||||||
|
client_x509_cert_url: Some(config.api.fcm.client_x509_cert_url),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
config.api.fcm.project_id,
|
||||||
|
false,
|
||||||
|
Duration::from_secs(5),
|
||||||
|
))
|
||||||
};
|
};
|
||||||
|
|
||||||
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
|
let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD
|
||||||
@@ -74,26 +98,38 @@ pub async fn worker(db: Database) {
|
|||||||
let PushNotification {
|
let PushNotification {
|
||||||
author,
|
author,
|
||||||
icon,
|
icon,
|
||||||
image: _,
|
image,
|
||||||
body,
|
body,
|
||||||
tag,
|
tag: _,
|
||||||
timestamp: _,
|
timestamp,
|
||||||
url: _,
|
url: _,
|
||||||
|
message,
|
||||||
} = &task.payload;
|
} = &task.payload;
|
||||||
|
|
||||||
let mut notification = fcm::NotificationBuilder::new();
|
let message = fcm_v1::message::Message {
|
||||||
notification.title(author);
|
token: Some(sub.auth),
|
||||||
notification.icon(icon);
|
data: Some(HashMap::from([
|
||||||
notification.body(body);
|
(
|
||||||
notification.tag(tag);
|
"author".to_owned(),
|
||||||
// TODO: expand support for fields
|
serde_json::Value::String(author.clone()),
|
||||||
let notification = notification.finalize();
|
),
|
||||||
|
("icon".to_owned(), serde_json::Value::String(icon.clone())),
|
||||||
|
(
|
||||||
|
"image".to_owned(),
|
||||||
|
if let Some(image) = image {
|
||||||
|
serde_json::Value::String(image.clone())
|
||||||
|
} else {
|
||||||
|
serde_json::Value::Null
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("body".to_owned(), serde_json::Value::String(body.clone())),
|
||||||
|
("timestamp".to_owned(), json!(timestamp)),
|
||||||
|
("message".to_owned(), json!(&message)),
|
||||||
|
])),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let mut message_builder =
|
if let Err(err) = client.send(&message).await {
|
||||||
fcm::MessageBuilder::new(&config.api.fcm.api_key, &sub.auth);
|
|
||||||
message_builder.notification(notification);
|
|
||||||
|
|
||||||
if let Err(err) = client.send(message_builder.finalize()).await {
|
|
||||||
error!("Failed to send FCM notification! {:?}", err);
|
error!("Failed to send FCM notification! {:?}", err);
|
||||||
} else {
|
} else {
|
||||||
info!("Sent FCM notification to {:?}.", session.id);
|
info!("Sent FCM notification to {:?}.", session.id);
|
||||||
@@ -101,6 +137,14 @@ pub async fn worker(db: Database) {
|
|||||||
} else {
|
} else {
|
||||||
info!("No FCM token was specified!");
|
info!("No FCM token was specified!");
|
||||||
}
|
}
|
||||||
|
} else if sub.endpoint == "apn" {
|
||||||
|
apple_notifications::queue(apple_notifications::ApnJob::from_notification(
|
||||||
|
session.id,
|
||||||
|
session.user_id,
|
||||||
|
sub.auth,
|
||||||
|
&task.payload,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
} else {
|
} else {
|
||||||
// Use Web Push Standard
|
// Use Web Push Standard
|
||||||
let subscription = SubscriptionInfo {
|
let subscription = SubscriptionInfo {
|
||||||
|
|||||||
@@ -459,26 +459,30 @@ impl From<Metadata> for crate::Metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crate::Message> for Message {
|
impl crate::Message {
|
||||||
fn from(value: crate::Message) -> Self {
|
pub fn into_model(self, user: Option<User>, member: Option<Member>) -> Message {
|
||||||
Message {
|
Message {
|
||||||
id: value.id,
|
id: self.id,
|
||||||
nonce: value.nonce,
|
nonce: self.nonce,
|
||||||
channel: value.channel,
|
channel: self.channel,
|
||||||
author: value.author,
|
author: self.author,
|
||||||
webhook: value.webhook,
|
user,
|
||||||
content: value.content,
|
member,
|
||||||
system: value.system.map(|system| system.into()),
|
webhook: self.webhook,
|
||||||
attachments: value
|
content: self.content,
|
||||||
|
system: self.system.map(Into::into),
|
||||||
|
attachments: self
|
||||||
.attachments
|
.attachments
|
||||||
.map(|v| v.into_iter().map(|f| f.into()).collect()),
|
.map(|v| v.into_iter().map(|f| f.into()).collect()),
|
||||||
edited: value.edited,
|
edited: self.edited,
|
||||||
embeds: value.embeds,
|
embeds: self.embeds,
|
||||||
mentions: value.mentions,
|
mentions: self.mentions,
|
||||||
replies: value.replies,
|
replies: self.replies,
|
||||||
reactions: value.reactions,
|
reactions: self.reactions,
|
||||||
interactions: value.interactions.into(),
|
interactions: self.interactions.into(),
|
||||||
masquerade: value.masquerade.map(|masq| masq.into()),
|
masquerade: self.masquerade.map(Into::into),
|
||||||
|
flags: self.flags.map(|flags| flags as u32).unwrap_or_default(),
|
||||||
|
pinned: self.pinned,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -490,9 +494,11 @@ impl From<crate::PartialMessage> for PartialMessage {
|
|||||||
nonce: value.nonce,
|
nonce: value.nonce,
|
||||||
channel: value.channel,
|
channel: value.channel,
|
||||||
author: value.author,
|
author: value.author,
|
||||||
|
user: None,
|
||||||
|
member: None,
|
||||||
webhook: value.webhook,
|
webhook: value.webhook,
|
||||||
content: value.content,
|
content: value.content,
|
||||||
system: value.system.map(|system| system.into()),
|
system: value.system.map(Into::into),
|
||||||
attachments: value
|
attachments: value
|
||||||
.attachments
|
.attachments
|
||||||
.map(|v| v.into_iter().map(|f| f.into()).collect()),
|
.map(|v| v.into_iter().map(|f| f.into()).collect()),
|
||||||
@@ -501,8 +507,10 @@ impl From<crate::PartialMessage> for PartialMessage {
|
|||||||
mentions: value.mentions,
|
mentions: value.mentions,
|
||||||
replies: value.replies,
|
replies: value.replies,
|
||||||
reactions: value.reactions,
|
reactions: value.reactions,
|
||||||
interactions: value.interactions.map(|interactions| interactions.into()),
|
interactions: value.interactions.map(Into::into),
|
||||||
masquerade: value.masquerade.map(|masq| masq.into()),
|
masquerade: value.masquerade.map(Into::into),
|
||||||
|
flags: value.flags.map(|flags| flags as u32),
|
||||||
|
pinned: value.pinned,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,6 +533,8 @@ impl From<crate::SystemMessage> for SystemMessage {
|
|||||||
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
|
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
|
||||||
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
|
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
|
||||||
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
|
crate::SystemMessage::UserRemove { id, by } => Self::UserRemove { id, by },
|
||||||
|
crate::SystemMessage::MessagePinned { id, by } => Self::MessagePinned { id, by },
|
||||||
|
crate::SystemMessage::MessageUnpinned { id, by } => Self::MessageUnpinned { id, by },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1018,6 +1028,8 @@ impl crate::User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Convert user object into user model assuming mutual connection
|
/// Convert user object into user model assuming mutual connection
|
||||||
|
///
|
||||||
|
/// Relations will never be included, i.e. when we process ourselves
|
||||||
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
|
pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
|
||||||
where
|
where
|
||||||
P: Into<Option<&'a crate::User>>,
|
P: Into<Option<&'a crate::User>>,
|
||||||
@@ -1053,19 +1065,7 @@ impl crate::User {
|
|||||||
discriminator: self.discriminator,
|
discriminator: self.discriminator,
|
||||||
display_name: self.display_name,
|
display_name: self.display_name,
|
||||||
avatar: self.avatar.map(|file| file.into()),
|
avatar: self.avatar.map(|file| file.into()),
|
||||||
relations: if let Some(crate::User { id, .. }) = perspective {
|
relations: vec![],
|
||||||
if id == &self.id {
|
|
||||||
self.relations
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.map(|relation| relation.into())
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
},
|
|
||||||
badges: self.badges.unwrap_or_default() as u32,
|
badges: self.badges.unwrap_or_default() as u32,
|
||||||
online: can_see_profile
|
online: can_see_profile
|
||||||
&& is_online
|
&& is_online
|
||||||
@@ -1089,7 +1089,33 @@ impl crate::User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn into_self(self) -> User {
|
/// Convert user object into user model without presence information
|
||||||
|
pub fn into_known_static<'a>(self, is_online: bool) -> User {
|
||||||
|
User {
|
||||||
|
username: self.username,
|
||||||
|
discriminator: self.discriminator,
|
||||||
|
display_name: self.display_name,
|
||||||
|
avatar: self.avatar.map(|file| file.into()),
|
||||||
|
relations: vec![],
|
||||||
|
badges: self.badges.unwrap_or_default() as u32,
|
||||||
|
online: is_online
|
||||||
|
&& !matches!(
|
||||||
|
self.status,
|
||||||
|
Some(crate::UserStatus {
|
||||||
|
presence: Some(crate::Presence::Invisible),
|
||||||
|
..
|
||||||
|
})
|
||||||
|
),
|
||||||
|
status: self.status.map(|status| status.into()),
|
||||||
|
flags: self.flags.unwrap_or_default() as u32,
|
||||||
|
privileged: self.privileged,
|
||||||
|
bot: self.bot.map(|bot| bot.into()),
|
||||||
|
relationship: RelationshipStatus::None, // events client will populate this from cache
|
||||||
|
id: self.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn into_self(self, force_online: bool) -> User {
|
||||||
User {
|
User {
|
||||||
username: self.username,
|
username: self.username,
|
||||||
discriminator: self.discriminator,
|
discriminator: self.discriminator,
|
||||||
@@ -1105,7 +1131,7 @@ impl crate::User {
|
|||||||
})
|
})
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
badges: self.badges.unwrap_or_default() as u32,
|
badges: self.badges.unwrap_or_default() as u32,
|
||||||
online: revolt_presence::is_online(&self.id).await
|
online: (force_online || revolt_presence::is_online(&self.id).await)
|
||||||
&& !matches!(
|
&& !matches!(
|
||||||
self.status,
|
self.status,
|
||||||
Some(crate::UserStatus {
|
Some(crate::UserStatus {
|
||||||
@@ -1182,6 +1208,7 @@ impl From<FieldsUser> for crate::FieldsUser {
|
|||||||
FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent,
|
FieldsUser::ProfileContent => crate::FieldsUser::ProfileContent,
|
||||||
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
|
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
|
||||||
FieldsUser::StatusText => crate::FieldsUser::StatusText,
|
FieldsUser::StatusText => crate::FieldsUser::StatusText,
|
||||||
|
FieldsUser::DisplayName => crate::FieldsUser::DisplayName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1194,6 +1221,7 @@ impl From<crate::FieldsUser> for FieldsUser {
|
|||||||
crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent,
|
crate::FieldsUser::ProfileContent => FieldsUser::ProfileContent,
|
||||||
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
|
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
|
||||||
crate::FieldsUser::StatusText => FieldsUser::StatusText,
|
crate::FieldsUser::StatusText => FieldsUser::StatusText,
|
||||||
|
crate::FieldsUser::DisplayName => FieldsUser::DisplayName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1296,3 +1324,18 @@ impl From<BotInformation> for crate::BotInformation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<crate::FieldsMessage> for FieldsMessage {
|
||||||
|
fn from(value: crate::FieldsMessage) -> Self {
|
||||||
|
match value {
|
||||||
|
crate::FieldsMessage::Pinned => FieldsMessage::Pinned,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<FieldsMessage> for crate::FieldsMessage {
|
||||||
|
fn from(value: FieldsMessage) -> Self {
|
||||||
|
match value {
|
||||||
|
FieldsMessage::Pinned => crate::FieldsMessage::Pinned,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ static TOKEN_CACHE: Lazy<Mutex<lru::LruCache<String, ()>>> =
|
|||||||
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
|
Lazy::new(|| Mutex::new(lru::LruCache::new(NonZeroUsize::new(1000).unwrap())));
|
||||||
|
|
||||||
impl IdempotencyKey {
|
impl IdempotencyKey {
|
||||||
|
pub fn unchecked_from_string(key: String) -> Self {
|
||||||
|
Self { key }
|
||||||
|
}
|
||||||
|
|
||||||
// Backwards compatibility.
|
// Backwards compatibility.
|
||||||
// Issue #109
|
// Issue #109
|
||||||
pub async fn consume_nonce(&mut self, v: Option<String>) -> Result<()> {
|
pub async fn consume_nonce(&mut self, v: Option<String>) -> Result<()> {
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ impl PermissionQuery for DatabasePermissionQuery<'_> {
|
|||||||
if let Some(other_user) = &self.user {
|
if let Some(other_user) = &self.user {
|
||||||
if self.perspective.id == other_user.id {
|
if self.perspective.id == other_user.id {
|
||||||
return RelationshipStatus::User;
|
return RelationshipStatus::User;
|
||||||
|
} else if let Some(bot) = &other_user.bot {
|
||||||
|
// For the purposes of permissions checks,
|
||||||
|
// assume owner is the same as bot
|
||||||
|
if self.perspective.id == bot.owner {
|
||||||
|
return RelationshipStatus::User;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(relations) = &self.perspective.relations {
|
if let Some(relations) = &self.perspective.relations {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "revolt-files"
|
||||||
|
version = "0.7.16"
|
||||||
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
|
description = "Revolt Backend: S3 and encryption subroutines"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aes-gcm = "0.10.3"
|
||||||
|
|
||||||
|
aws-config = "1.5.5"
|
||||||
|
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// pub fn
|
||||||
|
|
||||||
|
pub fn decrypt(data: &mut [u8]) {}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-models"
|
name = "revolt-models"
|
||||||
version = "0.7.9"
|
version = "0.7.16"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-config = { version = "0.7.9", path = "../config" }
|
revolt-config = { version = "0.7.16", path = "../config" }
|
||||||
revolt-permissions = { version = "0.7.9", path = "../permissions" }
|
revolt-permissions = { version = "0.7.16", path = "../permissions" }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
|||||||
@@ -66,3 +66,8 @@ pub fn if_false(t: &bool) -> bool {
|
|||||||
pub fn if_zero_u32(t: &u32) -> bool {
|
pub fn if_zero_u32(t: &u32) -> bool {
|
||||||
t == &0
|
t == &0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Utility function to check if an option doesnt contain true
|
||||||
|
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||||
|
t != &Some(true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -162,4 +162,11 @@ auto_derived!(
|
|||||||
/// User objects
|
/// User objects
|
||||||
pub users: Vec<User>,
|
pub users: Vec<User>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bot with user response
|
||||||
|
pub struct BotWithUserResponse {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub bot: Bot,
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,6 +84,12 @@ auto_derived!(
|
|||||||
content_type: BandcampType,
|
content_type: BandcampType,
|
||||||
id: String,
|
id: String,
|
||||||
},
|
},
|
||||||
|
AppleMusic {
|
||||||
|
album_id: String,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
track_id: Option<String>,
|
||||||
|
},
|
||||||
/// Streamable Video
|
/// Streamable Video
|
||||||
Streamable { id: String },
|
Streamable { id: String },
|
||||||
}
|
}
|
||||||
@@ -92,38 +98,38 @@ auto_derived!(
|
|||||||
pub struct WebsiteMetadata {
|
pub struct WebsiteMetadata {
|
||||||
/// Direct URL to web page
|
/// Direct URL to web page
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
url: Option<String>,
|
pub url: Option<String>,
|
||||||
/// Original direct URL
|
/// Original direct URL
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
original_url: Option<String>,
|
pub original_url: Option<String>,
|
||||||
/// Remote content
|
/// Remote content
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
special: Option<Special>,
|
pub special: Option<Special>,
|
||||||
|
|
||||||
/// Title of website
|
/// Title of website
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
title: Option<String>,
|
pub title: Option<String>,
|
||||||
/// Description of website
|
/// Description of website
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
description: Option<String>,
|
pub description: Option<String>,
|
||||||
/// Embedded image
|
/// Embedded image
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
image: Option<Image>,
|
pub image: Option<Image>,
|
||||||
/// Embedded video
|
/// Embedded video
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
video: Option<Video>,
|
pub video: Option<Video>,
|
||||||
|
|
||||||
// #[serde(skip_serializing_if = "Option::is_none")]
|
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||||
// opengraph_type: Option<String>,
|
// opengraph_type: Option<String>,
|
||||||
/// Site name
|
/// Site name
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
site_name: Option<String>,
|
pub site_name: Option<String>,
|
||||||
/// URL to site icon
|
/// URL to site icon
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
icon_url: Option<String>,
|
pub icon_url: Option<String>,
|
||||||
/// CSS Colour
|
/// CSS Colour
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
colour: Option<String>,
|
pub colour: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text Embed
|
/// Text Embed
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ auto_derived_partial!(
|
|||||||
pub channel: String,
|
pub channel: String,
|
||||||
/// Id of the user or webhook that sent this message
|
/// Id of the user or webhook that sent this message
|
||||||
pub author: String,
|
pub author: String,
|
||||||
|
/// The user that sent this message
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub user: Option<User>,
|
||||||
|
/// The member that sent this message
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub member: Option<Member>,
|
||||||
/// The webhook that sent this message
|
/// The webhook that sent this message
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub webhook: Option<MessageWebhook>,
|
pub webhook: Option<MessageWebhook>,
|
||||||
@@ -64,6 +70,18 @@ auto_derived_partial!(
|
|||||||
/// Name and / or avatar overrides for this message
|
/// Name and / or avatar overrides for this message
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub masquerade: Option<Masquerade>,
|
pub masquerade: Option<Masquerade>,
|
||||||
|
/// Whether or not the message in pinned
|
||||||
|
#[serde(skip_serializing_if = "crate::if_option_false")]
|
||||||
|
pub pinned: Option<bool>,
|
||||||
|
|
||||||
|
/// Bitfield of message flags
|
||||||
|
///
|
||||||
|
/// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html
|
||||||
|
#[cfg_attr(
|
||||||
|
feature = "serde",
|
||||||
|
serde(skip_serializing_if = "crate::if_zero_u32", default)
|
||||||
|
)]
|
||||||
|
pub flags: u32,
|
||||||
},
|
},
|
||||||
"PartialMessage"
|
"PartialMessage"
|
||||||
);
|
);
|
||||||
@@ -112,6 +130,10 @@ auto_derived!(
|
|||||||
ChannelIconChanged { by: String },
|
ChannelIconChanged { by: String },
|
||||||
#[serde(rename = "channel_ownership_changed")]
|
#[serde(rename = "channel_ownership_changed")]
|
||||||
ChannelOwnershipChanged { from: String, to: String },
|
ChannelOwnershipChanged { from: String, to: String },
|
||||||
|
#[serde(rename = "message_pinned")]
|
||||||
|
MessagePinned { id: String, by: String },
|
||||||
|
#[serde(rename = "message_unpinned")]
|
||||||
|
MessageUnpinned { id: String, by: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Name and / or avatar override information
|
/// Name and / or avatar override information
|
||||||
@@ -185,6 +207,8 @@ auto_derived!(
|
|||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
/// URL to open when clicking notification
|
/// URL to open when clicking notification
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
/// The message object itself, to send to clients for processing
|
||||||
|
pub message: Message,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Representation of a text embed before it is sent.
|
/// Representation of a text embed before it is sent.
|
||||||
@@ -241,6 +265,11 @@ auto_derived!(
|
|||||||
pub masquerade: Option<Masquerade>,
|
pub masquerade: Option<Masquerade>,
|
||||||
/// Information about how this message should be interacted with
|
/// Information about how this message should be interacted with
|
||||||
pub interactions: Option<Interactions>,
|
pub interactions: Option<Interactions>,
|
||||||
|
|
||||||
|
/// Bitfield of message flags
|
||||||
|
///
|
||||||
|
/// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.MessageFlags.html
|
||||||
|
pub flags: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Options for querying messages
|
/// Options for querying messages
|
||||||
@@ -278,7 +307,9 @@ auto_derived!(
|
|||||||
///
|
///
|
||||||
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
|
/// See [MongoDB documentation](https://docs.mongodb.com/manual/text-search/#-text-operator) for more information.
|
||||||
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
|
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 64)))]
|
||||||
pub query: String,
|
pub query: Option<String>,
|
||||||
|
/// Whether to only search for pinned messages, cannot be sent with `query`.
|
||||||
|
pub pinned: Option<bool>,
|
||||||
|
|
||||||
/// Maximum number of messages to fetch
|
/// Maximum number of messages to fetch
|
||||||
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
|
||||||
@@ -328,6 +359,18 @@ auto_derived!(
|
|||||||
/// Remove all reactions
|
/// Remove all reactions
|
||||||
pub remove_all: Option<bool>,
|
pub remove_all: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Message flag bitfield
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum MessageFlags {
|
||||||
|
/// Message will not send push / desktop notifications
|
||||||
|
SuppressNotifications = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional fields on message
|
||||||
|
pub enum FieldsMessage {
|
||||||
|
Pinned
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Message Author Abstraction
|
/// Message Author Abstraction
|
||||||
@@ -391,6 +434,8 @@ impl From<SystemMessage> for String {
|
|||||||
SystemMessage::ChannelOwnershipChanged { .. } => {
|
SystemMessage::ChannelOwnershipChanged { .. } => {
|
||||||
"Channel ownership changed.".to_string()
|
"Channel ownership changed.".to_string()
|
||||||
}
|
}
|
||||||
|
SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(),
|
||||||
|
SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,15 +455,30 @@ impl PushNotification {
|
|||||||
format!("{}/assets/logo.png", config.hosts.app)
|
format!("{}/assets/logo.png", config.hosts.app)
|
||||||
};
|
};
|
||||||
|
|
||||||
let image = msg.attachments.and_then(|attachments| {
|
let image = msg.attachments.as_ref().and_then(|attachments| {
|
||||||
attachments
|
attachments
|
||||||
.first()
|
.first()
|
||||||
.map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
|
.map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
|
||||||
});
|
});
|
||||||
|
|
||||||
let body = if let Some(sys) = msg.system {
|
let body = if let Some(ref sys) = msg.system {
|
||||||
sys.into()
|
sys.clone().into()
|
||||||
} else if let Some(text) = msg.content {
|
} else if let Some(ref text) = msg.content {
|
||||||
|
text.clone()
|
||||||
|
} else if let Some(text) = msg.embeds.as_ref().and_then(|embeds| match embeds.first() {
|
||||||
|
Some(Embed::Image(_)) => Some("Sent an image".to_string()),
|
||||||
|
Some(Embed::Video(_)) => Some("Sent a video".to_string()),
|
||||||
|
Some(Embed::Text(e)) => e
|
||||||
|
.description
|
||||||
|
.clone()
|
||||||
|
.or(e.title.clone().or(Some("Empty Embed".to_string()))),
|
||||||
|
Some(Embed::Website(e)) => e.title.clone().or(e
|
||||||
|
.description
|
||||||
|
.clone()
|
||||||
|
.or(e.site_name.clone().or(Some("Empty Embed".to_string())))),
|
||||||
|
Some(Embed::None) => Some("Empty Message".to_string()), // ???
|
||||||
|
None => Some("Empty Message".to_string()), // ??
|
||||||
|
}) {
|
||||||
text
|
text
|
||||||
} else {
|
} else {
|
||||||
"Empty Message".to_string()
|
"Empty Message".to_string()
|
||||||
@@ -439,6 +499,7 @@ impl PushNotification {
|
|||||||
tag: channel_id.to_string(),
|
tag: channel_id.to_string(),
|
||||||
timestamp,
|
timestamp,
|
||||||
url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id),
|
url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id),
|
||||||
|
message: msg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ auto_derived_partial!(
|
|||||||
pub relations: Vec<Relationship>,
|
pub relations: Vec<Relationship>,
|
||||||
|
|
||||||
/// Bitfield of user badges
|
/// Bitfield of user badges
|
||||||
|
///
|
||||||
|
/// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.UserBadges.html
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "serde",
|
feature = "serde",
|
||||||
serde(skip_serializing_if = "crate::if_zero_u32", default)
|
serde(skip_serializing_if = "crate::if_zero_u32", default)
|
||||||
@@ -52,6 +54,8 @@ auto_derived_partial!(
|
|||||||
pub status: Option<UserStatus>,
|
pub status: Option<UserStatus>,
|
||||||
|
|
||||||
/// Enum of user flags
|
/// Enum of user flags
|
||||||
|
///
|
||||||
|
/// https://docs.rs/revolt-models/latest/revolt_models/v0/enum.UserFlags.html
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "serde",
|
feature = "serde",
|
||||||
serde(skip_serializing_if = "crate::if_zero_u32", default)
|
serde(skip_serializing_if = "crate::if_zero_u32", default)
|
||||||
@@ -83,6 +87,7 @@ auto_derived!(
|
|||||||
StatusPresence,
|
StatusPresence,
|
||||||
ProfileContent,
|
ProfileContent,
|
||||||
ProfileBackground,
|
ProfileBackground,
|
||||||
|
DisplayName,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// User's relationship with another user (or themselves)
|
/// User's relationship with another user (or themselves)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-permissions"
|
name = "revolt-permissions"
|
||||||
version = "0.7.9"
|
version = "0.7.16"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-result = { version = "0.7.9", path = "../result" }
|
revolt-result = { version = "0.7.16", path = "../result" }
|
||||||
|
|
||||||
# Utility
|
# Utility
|
||||||
auto_ops = "0.3.0"
|
auto_ops = "0.3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-presence"
|
name = "revolt-presence"
|
||||||
version = "0.7.9"
|
version = "0.7.16"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-result"
|
name = "revolt-result"
|
||||||
version = "0.7.9"
|
version = "0.7.16"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Paul Makles <me@insrt.uk>"]
|
authors = ["Paul Makles <me@insrt.uk>"]
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ pub enum ErrorType {
|
|||||||
},
|
},
|
||||||
AlreadyInGroup,
|
AlreadyInGroup,
|
||||||
NotInGroup,
|
NotInGroup,
|
||||||
|
AlreadyPinned,
|
||||||
|
NotPinned,
|
||||||
|
|
||||||
// ? Server related errors
|
// ? Server related errors
|
||||||
UnknownServer,
|
UnknownServer,
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ impl<'r> Responder<'r, 'static> for Error {
|
|||||||
ErrorType::GroupTooLarge { .. } => Status::Forbidden,
|
ErrorType::GroupTooLarge { .. } => Status::Forbidden,
|
||||||
ErrorType::AlreadyInGroup => Status::Conflict,
|
ErrorType::AlreadyInGroup => Status::Conflict,
|
||||||
ErrorType::NotInGroup => Status::NotFound,
|
ErrorType::NotInGroup => Status::NotFound,
|
||||||
|
ErrorType::AlreadyPinned => Status::BadRequest,
|
||||||
|
ErrorType::NotPinned => Status::BadRequest,
|
||||||
|
|
||||||
ErrorType::UnknownServer => Status::NotFound,
|
ErrorType::UnknownServer => Status::NotFound,
|
||||||
ErrorType::InvalidRole => Status::NotFound,
|
ErrorType::InvalidRole => Status::NotFound,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt-delta"
|
name = "revolt-delta"
|
||||||
version = "0.7.9"
|
version = "0.7.16"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -77,6 +77,7 @@ revolt-models = { path = "../core/models", features = [
|
|||||||
"validator",
|
"validator",
|
||||||
"rocket",
|
"rocket",
|
||||||
] }
|
] }
|
||||||
|
revolt-presence = { path = "../core/presence" }
|
||||||
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
|
revolt-result = { path = "../core/result", features = ["rocket", "okapi"] }
|
||||||
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
|
revolt-permissions = { path = "../core/permissions", features = ["schemas"] }
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,11 @@
|
|||||||
FROM ghcr.io/revoltchat/base:latest AS builder
|
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||||
|
|
||||||
# Bundle Stage
|
# Bundle Stage
|
||||||
FROM debian:bullseye-slim
|
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y ca-certificates && \
|
|
||||||
apt-get clean
|
|
||||||
COPY --from=builder /home/rust/src/target/release/revolt-delta ./
|
COPY --from=builder /home/rust/src/target/release/revolt-delta ./
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
ENV ROCKET_ADDRESS 0.0.0.0
|
ENV ROCKET_ADDRESS 0.0.0.0
|
||||||
ENV ROCKET_PORT 8000
|
ENV ROCKET_PORT 8000
|
||||||
|
USER nonroot
|
||||||
CMD ["./revolt-delta"]
|
CMD ["./revolt-delta"]
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ pub async fn create_bot(
|
|||||||
db: &State<Database>,
|
db: &State<Database>,
|
||||||
user: User,
|
user: User,
|
||||||
info: Json<v0::DataCreateBot>,
|
info: Json<v0::DataCreateBot>,
|
||||||
) -> Result<Json<v0::Bot>> {
|
) -> Result<Json<v0::BotWithUserResponse>> {
|
||||||
let info = info.into_inner();
|
let info = info.into_inner();
|
||||||
info.validate().map_err(|error| {
|
info.validate().map_err(|error| {
|
||||||
create_error!(FailedValidation {
|
create_error!(FailedValidation {
|
||||||
@@ -22,8 +22,11 @@ pub async fn create_bot(
|
|||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let bot = Bot::create(db, info.name, &user, None).await?;
|
let (bot, user) = Bot::create(db, info.name, &user, None).await?;
|
||||||
Ok(Json(bot.into()))
|
Ok(Json(v0::BotWithUserResponse {
|
||||||
|
bot: bot.into(),
|
||||||
|
user: user.into_self(false).await,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ mod test {
|
|||||||
let mut harness = TestHarness::new().await;
|
let mut harness = TestHarness::new().await;
|
||||||
let (_, session, user) = harness.new_user().await;
|
let (_, session, user) = harness.new_user().await;
|
||||||
|
|
||||||
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
||||||
.await
|
.await
|
||||||
.expect("`Bot`");
|
.expect("`Bot`");
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub async fn edit_bot(
|
|||||||
user: User,
|
user: User,
|
||||||
target: Reference,
|
target: Reference,
|
||||||
data: Json<DataEditBot>,
|
data: Json<DataEditBot>,
|
||||||
) -> Result<Json<v0::Bot>> {
|
) -> Result<Json<v0::BotWithUserResponse>> {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
data.validate().map_err(|error| {
|
data.validate().map_err(|error| {
|
||||||
create_error!(FailedValidation {
|
create_error!(FailedValidation {
|
||||||
@@ -29,8 +29,8 @@ pub async fn edit_bot(
|
|||||||
return Err(create_error!(NotFound));
|
return Err(create_error!(NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut user = db.fetch_user(&bot.id).await?;
|
||||||
if let Some(name) = data.name {
|
if let Some(name) = data.name {
|
||||||
let mut user = db.fetch_user(&bot.id).await?;
|
|
||||||
user.update_username(db, name).await?;
|
user.update_username(db, name).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,10 @@ pub async fn edit_bot(
|
|||||||
&& data.interactions_url.is_none()
|
&& data.interactions_url.is_none()
|
||||||
&& data.remove.is_none()
|
&& data.remove.is_none()
|
||||||
{
|
{
|
||||||
return Ok(Json(bot.into()));
|
return Ok(Json(v0::BotWithUserResponse {
|
||||||
|
bot: bot.into(),
|
||||||
|
user: user.into_self(false).await,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
let DataEditBot {
|
let DataEditBot {
|
||||||
@@ -68,7 +71,10 @@ pub async fn edit_bot(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(bot.into()))
|
Ok(Json(v0::BotWithUserResponse {
|
||||||
|
bot: bot.into(),
|
||||||
|
user: user.into_self(false).await,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -83,7 +89,7 @@ mod test {
|
|||||||
let harness = TestHarness::new().await;
|
let harness = TestHarness::new().await;
|
||||||
let (_, session, user) = harness.new_user().await;
|
let (_, session, user) = harness.new_user().await;
|
||||||
|
|
||||||
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
||||||
.await
|
.await
|
||||||
.expect("`Bot`");
|
.expect("`Bot`");
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ mod test {
|
|||||||
let harness = TestHarness::new().await;
|
let harness = TestHarness::new().await;
|
||||||
let (_, session, user) = harness.new_user().await;
|
let (_, session, user) = harness.new_user().await;
|
||||||
|
|
||||||
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
||||||
.await
|
.await
|
||||||
.expect("`Bot`");
|
.expect("`Bot`");
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub async fn fetch_owned_bots(db: &State<Database>, user: User) -> Result<Json<O
|
|||||||
users.sort_by(|a, b| a.id.cmp(&b.id));
|
users.sort_by(|a, b| a.id.cmp(&b.id));
|
||||||
|
|
||||||
Ok(Json(OwnedBotsResponse {
|
Ok(Json(OwnedBotsResponse {
|
||||||
users: join_all(users.into_iter().map(|user| user.into_self())).await,
|
users: join_all(users.into_iter().map(|user| user.into_self(false))).await,
|
||||||
bots: bots.into_iter().map(|bot| bot.into()).collect(),
|
bots: bots.into_iter().map(|bot| bot.into()).collect(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ mod test {
|
|||||||
let harness = TestHarness::new().await;
|
let harness = TestHarness::new().await;
|
||||||
let (_, session, user) = harness.new_user().await;
|
let (_, session, user) = harness.new_user().await;
|
||||||
|
|
||||||
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
||||||
.await
|
.await
|
||||||
.expect("`Bot`");
|
.expect("`Bot`");
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ mod test {
|
|||||||
let harness = TestHarness::new().await;
|
let harness = TestHarness::new().await;
|
||||||
let (_, _, user) = harness.new_user().await;
|
let (_, _, user) = harness.new_user().await;
|
||||||
|
|
||||||
let mut bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
let (mut bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
||||||
.await
|
.await
|
||||||
.expect("`Bot`");
|
.expect("`Bot`");
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ mod test {
|
|||||||
let mut harness = TestHarness::new().await;
|
let mut harness = TestHarness::new().await;
|
||||||
let (_, session, user) = harness.new_user().await;
|
let (_, session, user) = harness.new_user().await;
|
||||||
|
|
||||||
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
||||||
.await
|
.await
|
||||||
.expect("`Bot`");
|
.expect("`Bot`");
|
||||||
|
|
||||||
@@ -93,7 +93,9 @@ mod test {
|
|||||||
.client
|
.client
|
||||||
.post(format!("/bots/{}/invite", bot.id))
|
.post(format!("/bots/{}/invite", bot.id))
|
||||||
.header(ContentType::JSON)
|
.header(ContentType::JSON)
|
||||||
.body(json!(v0::InviteBotDestination::Group { group: group.id() }).to_string())
|
.body(json!(v0::InviteBotDestination::Group {
|
||||||
|
group: group.id().to_string()
|
||||||
|
}).to_string())
|
||||||
.header(Header::new("x-session-token", session.token.to_string()))
|
.header(Header::new("x-session-token", session.token.to_string()))
|
||||||
.dispatch()
|
.dispatch()
|
||||||
.await;
|
.await;
|
||||||
@@ -102,8 +104,8 @@ mod test {
|
|||||||
drop(response);
|
drop(response);
|
||||||
|
|
||||||
let event = harness
|
let event = harness
|
||||||
.wait_for_event(&group.id(), |event| match event {
|
.wait_for_event(group.id(), |event| match event {
|
||||||
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
|
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -121,7 +123,7 @@ mod test {
|
|||||||
let mut harness = TestHarness::new().await;
|
let mut harness = TestHarness::new().await;
|
||||||
let (_, session, user) = harness.new_user().await;
|
let (_, session, user) = harness.new_user().await;
|
||||||
|
|
||||||
let bot = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
let (bot, _) = Bot::create(&harness.db, TestHarness::rand_string(), &user, None)
|
||||||
.await
|
.await
|
||||||
.expect("`Bot`");
|
.expect("`Bot`");
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ mod test {
|
|||||||
|
|
||||||
let event = harness
|
let event = harness
|
||||||
.wait_for_event(&format!("{}!", user.id), |event| match event {
|
.wait_for_event(&format!("{}!", user.id), |event| match event {
|
||||||
EventV1::ChannelAck { id, .. } => id == &group.id(),
|
EventV1::ChannelAck { id, .. } => id == group.id(),
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ mod test {
|
|||||||
drop(response);
|
drop(response);
|
||||||
|
|
||||||
harness
|
harness
|
||||||
.wait_for_event(&group.id(), |event| match event {
|
.wait_for_event(group.id(), |event| match event {
|
||||||
EventV1::ChannelDelete { id, .. } => id == &group.id(),
|
EventV1::ChannelDelete { id, .. } => id == group.id(),
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ pub async fn edit(
|
|||||||
return Err(create_error!(InvalidOperation));
|
return Err(create_error!(InvalidOperation));
|
||||||
}
|
}
|
||||||
.into_message(channel.id().to_string())
|
.into_message(channel.id().to_string())
|
||||||
.send(db, user.as_author_for_system(), &channel, false)
|
.send(db, user.as_author_for_system(), None, None, &channel, false)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ pub async fn edit(
|
|||||||
by: user.id.clone(),
|
by: user.id.clone(),
|
||||||
}
|
}
|
||||||
.into_message(channel.id().to_string())
|
.into_message(channel.id().to_string())
|
||||||
.send(db, user.as_author_for_system(), &channel, false)
|
.send(db, user.as_author_for_system(), None, None, &channel, false)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -161,7 +161,7 @@ pub async fn edit(
|
|||||||
by: user.id.clone(),
|
by: user.id.clone(),
|
||||||
}
|
}
|
||||||
.into_message(channel.id().to_string())
|
.into_message(channel.id().to_string())
|
||||||
.send(db, user.as_author_for_system(), &channel, false)
|
.send(db, user.as_author_for_system(), None, None, &channel, false)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ pub async fn edit(
|
|||||||
by: user.id.clone(),
|
by: user.id.clone(),
|
||||||
}
|
}
|
||||||
.into_message(channel.id().to_string())
|
.into_message(channel.id().to_string())
|
||||||
.send(db, user.as_author_for_system(), &channel, false)
|
.send(db, user.as_author_for_system(), None, None, &channel, false)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ mod test {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let event = harness
|
let event = harness
|
||||||
.wait_for_event(&group.id(), |event| match event {
|
.wait_for_event(group.id(), |event| match event {
|
||||||
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
|
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -113,7 +113,7 @@ mod test {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let message = harness.wait_for_message(&group.id()).await;
|
let message = harness.wait_for_message(group.id()).await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
message.system,
|
message.system,
|
||||||
|
|||||||
@@ -106,8 +106,8 @@ mod test {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let event = harness
|
let event = harness
|
||||||
.wait_for_event(&group.id(), |event| match event {
|
.wait_for_event(group.id(), |event| match event {
|
||||||
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
|
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
|
||||||
_ => false,
|
_ => false,
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -117,7 +117,7 @@ mod test {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let message = harness.wait_for_message(&group.id()).await;
|
let message = harness.wait_for_message(group.id()).await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
message.system,
|
message.system,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub async fn clear_reactions(
|
|||||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||||
|
|
||||||
// Fetch relevant message
|
// Fetch relevant message
|
||||||
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
|
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||||
|
|
||||||
// Clear reactions
|
// Clear reactions
|
||||||
message
|
message
|
||||||
@@ -37,6 +37,7 @@ pub async fn clear_reactions(
|
|||||||
reactions: Some(Default::default()),
|
reactions: Some(Default::default()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
vec![]
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(|_| EmptyResponse)
|
.map(|_| EmptyResponse)
|
||||||
|
|||||||
@@ -30,11 +30,10 @@ pub async fn edit(
|
|||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let config = config().await;
|
|
||||||
Message::validate_sum(
|
Message::validate_sum(
|
||||||
&edit.content,
|
&edit.content,
|
||||||
edit.embeds.as_deref().unwrap_or_default(),
|
edit.embeds.as_deref().unwrap_or_default(),
|
||||||
config.features.limits.default.message_length,
|
user.limits().await.message_length,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Ensure we have permissions to send a message
|
// Ensure we have permissions to send a message
|
||||||
@@ -44,7 +43,7 @@ pub async fn edit(
|
|||||||
|
|
||||||
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?;
|
permissions.throw_if_lacking_channel_permission(ChannelPermission::SendMessage)?;
|
||||||
|
|
||||||
let mut message = msg.as_message_in_channel(db, &channel.id()).await?;
|
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||||
if message.author != user.id {
|
if message.author != user.id {
|
||||||
return Err(create_error!(CannotEditMessage));
|
return Err(create_error!(CannotEditMessage));
|
||||||
}
|
}
|
||||||
@@ -84,7 +83,7 @@ pub async fn edit(
|
|||||||
|
|
||||||
partial.embeds = Some(new_embeds);
|
partial.embeds = Some(new_embeds);
|
||||||
|
|
||||||
message.update(db, partial).await?;
|
message.update(db, partial, vec![]).await?;
|
||||||
|
|
||||||
// Queue up a task for processing embeds if the we have sufficient permissions
|
// Queue up a task for processing embeds if the we have sufficient permissions
|
||||||
if permissions.has_channel_permission(ChannelPermission::SendEmbeds) {
|
if permissions.has_channel_permission(ChannelPermission::SendEmbeds) {
|
||||||
@@ -98,5 +97,5 @@ pub async fn edit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(message.into()))
|
Ok(Json(message.into_model(None, None)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,5 +29,5 @@ pub async fn fetch(
|
|||||||
return Err(create_error!(NotFound));
|
return Err(create_error!(NotFound));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(message.into()))
|
Ok(Json(message.into_model(None, None)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
use revolt_database::{
|
||||||
|
util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, PartialMessage, SystemMessage, User
|
||||||
|
};
|
||||||
|
use revolt_models::v0::MessageAuthor;
|
||||||
|
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||||
|
use revolt_result::{create_error, Result};
|
||||||
|
use rocket::State;
|
||||||
|
use rocket_empty::EmptyResponse;
|
||||||
|
|
||||||
|
/// # Pins a message
|
||||||
|
///
|
||||||
|
/// Pins a message by its id.
|
||||||
|
#[openapi(tag = "Messaging")]
|
||||||
|
#[post("/<target>/messages/<msg>/pin")]
|
||||||
|
pub async fn message_pin(
|
||||||
|
db: &State<Database>,
|
||||||
|
user: User,
|
||||||
|
target: Reference,
|
||||||
|
msg: Reference,
|
||||||
|
) -> Result<EmptyResponse> {
|
||||||
|
let channel = target.as_channel(db).await?;
|
||||||
|
|
||||||
|
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||||
|
calculate_channel_permissions(&mut query)
|
||||||
|
.await
|
||||||
|
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||||
|
|
||||||
|
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||||
|
|
||||||
|
if message.pinned.unwrap_or_default() {
|
||||||
|
return Err(create_error!(AlreadyPinned))
|
||||||
|
}
|
||||||
|
|
||||||
|
message.update(db, PartialMessage {
|
||||||
|
pinned: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}, vec![]).await?;
|
||||||
|
|
||||||
|
SystemMessage::MessagePinned {
|
||||||
|
id: message.id.clone(),
|
||||||
|
by: user.id.clone()
|
||||||
|
}
|
||||||
|
.into_message(channel.id().to_string())
|
||||||
|
.send(
|
||||||
|
db,
|
||||||
|
MessageAuthor::System {
|
||||||
|
username: &user.username,
|
||||||
|
avatar: user.avatar.as_ref().map(|file| file.id.as_ref())
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&channel,
|
||||||
|
false
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(EmptyResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{rocket, util::test::TestHarness};
|
||||||
|
use revolt_database::{events::client::EventV1, util::{idempotency::IdempotencyKey, reference::Reference}, Member, Message, Server};
|
||||||
|
use revolt_models::v0::{self, SystemMessage};
|
||||||
|
use rocket::http::{Header, Status};
|
||||||
|
|
||||||
|
#[rocket::async_test]
|
||||||
|
async fn pin_message() {
|
||||||
|
let mut harness = TestHarness::new().await;
|
||||||
|
let (_, session, user) = harness.new_user().await;
|
||||||
|
|
||||||
|
let (server, channels) = Server::create(
|
||||||
|
&harness.db,
|
||||||
|
v0::DataCreateServer {
|
||||||
|
name: "Test Server".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&user,
|
||||||
|
true
|
||||||
|
).await.expect("Failed to create test server");
|
||||||
|
|
||||||
|
let (member, channels) = Member::create(&harness.db, &server, &user, Some(channels)).await.expect("Failed to create member");
|
||||||
|
let channel = &channels[0];
|
||||||
|
|
||||||
|
let message = Message::create_from_api(
|
||||||
|
&harness.db,
|
||||||
|
channel.clone(),
|
||||||
|
v0::DataMessageSend {
|
||||||
|
content:Some("Test message".to_string()),
|
||||||
|
nonce: None,
|
||||||
|
attachments: None,
|
||||||
|
replies: None,
|
||||||
|
embeds: None,
|
||||||
|
masquerade: None,
|
||||||
|
interactions: None,
|
||||||
|
flags: None
|
||||||
|
},
|
||||||
|
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
|
||||||
|
Some(user.clone().into(&harness.db, Some(&user)).await),
|
||||||
|
Some(member.into()),
|
||||||
|
user.limits().await,
|
||||||
|
IdempotencyKey::unchecked_from_string("0".to_string()),
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create message");
|
||||||
|
|
||||||
|
let response = harness
|
||||||
|
.client
|
||||||
|
.post(format!("/channels/{}/messages/{}/pin", channel.id(), &message.id))
|
||||||
|
.header(Header::new("x-session-token", session.token.to_string()))
|
||||||
|
.dispatch()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), Status::NoContent);
|
||||||
|
drop(response);
|
||||||
|
|
||||||
|
harness.wait_for_event(channel.id(), |event| {
|
||||||
|
match event {
|
||||||
|
EventV1::Message(message) => {
|
||||||
|
match &message.system {
|
||||||
|
Some(SystemMessage::MessagePinned { by, .. }) => {
|
||||||
|
assert_eq!(by, &user.id);
|
||||||
|
|
||||||
|
true
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
harness.wait_for_event(channel.id(), |event| {
|
||||||
|
match event {
|
||||||
|
EventV1::MessageUpdate { id, channel: channel_id, data, .. } => {
|
||||||
|
assert_eq!(id, &message.id);
|
||||||
|
assert_eq!(channel_id, channel.id());
|
||||||
|
assert_eq!(data.pinned, Some(true));
|
||||||
|
|
||||||
|
true
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let updated_message = Reference::from_unchecked(message.id)
|
||||||
|
.as_message(&harness.db)
|
||||||
|
.await
|
||||||
|
.expect("Failed to find updated message");
|
||||||
|
|
||||||
|
assert_eq!(updated_message.pinned, Some(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ pub async fn react_message(
|
|||||||
.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
.throw_if_lacking_channel_permission(ChannelPermission::React)?;
|
||||||
|
|
||||||
// Fetch relevant message
|
// Fetch relevant message
|
||||||
let message = msg.as_message_in_channel(db, &channel.id()).await?;
|
let message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||||
|
|
||||||
// Add the reaction
|
// Add the reaction
|
||||||
message
|
message
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ pub async fn search(
|
|||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if options.query.is_some() && options.pinned.is_some() {
|
||||||
|
return Err(create_error!(InvalidOperation))
|
||||||
|
}
|
||||||
|
|
||||||
let channel = target.as_channel(db).await?;
|
let channel = target.as_channel(db).await?;
|
||||||
|
|
||||||
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||||
@@ -39,6 +43,7 @@ pub async fn search(
|
|||||||
|
|
||||||
let v0::DataMessageSearch {
|
let v0::DataMessageSearch {
|
||||||
query,
|
query,
|
||||||
|
pinned,
|
||||||
limit,
|
limit,
|
||||||
before,
|
before,
|
||||||
after,
|
after,
|
||||||
@@ -51,7 +56,8 @@ pub async fn search(
|
|||||||
MessageQuery {
|
MessageQuery {
|
||||||
filter: MessageFilter {
|
filter: MessageFilter {
|
||||||
channel: Some(channel.id().to_string()),
|
channel: Some(channel.id().to_string()),
|
||||||
query: Some(query),
|
query,
|
||||||
|
pinned,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
time_period: MessageTimePeriod::Absolute {
|
time_period: MessageTimePeriod::Absolute {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use revolt_database::{
|
|||||||
};
|
};
|
||||||
use revolt_database::{Interactions, Message};
|
use revolt_database::{Interactions, Message};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
|
use revolt_permissions::PermissionQuery;
|
||||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
@@ -75,17 +76,34 @@ pub async fn message_send(
|
|||||||
|
|
||||||
// Create the message
|
// Create the message
|
||||||
let author: v0::User = user.clone().into(db, Some(&user)).await;
|
let author: v0::User = user.clone().into(db, Some(&user)).await;
|
||||||
|
|
||||||
|
// Make sure we have server member (edge case if server owner)
|
||||||
|
query.are_we_a_member().await;
|
||||||
|
|
||||||
|
// Create model user / members
|
||||||
|
let model_user = user
|
||||||
|
.clone()
|
||||||
|
.into_known_static(revolt_presence::is_online(&user.id).await);
|
||||||
|
|
||||||
|
let model_member: Option<v0::Member> = query
|
||||||
|
.member_ref()
|
||||||
|
.as_ref()
|
||||||
|
.map(|member| member.clone().into_owned().into());
|
||||||
|
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
Message::create_from_api(
|
Message::create_from_api(
|
||||||
db,
|
db,
|
||||||
channel,
|
channel,
|
||||||
data,
|
data,
|
||||||
v0::MessageAuthor::User(&author),
|
v0::MessageAuthor::User(&author),
|
||||||
|
Some(model_user.clone()),
|
||||||
|
model_member.clone(),
|
||||||
|
user.limits().await,
|
||||||
idempotency,
|
idempotency,
|
||||||
permissions.has_channel_permission(ChannelPermission::SendEmbeds),
|
permissions.has_channel_permission(ChannelPermission::SendEmbeds),
|
||||||
allow_mentions,
|
allow_mentions,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into(),
|
.into_model(Some(model_user), model_member),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
use revolt_database::{
|
||||||
|
util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, FieldsMessage, PartialMessage, SystemMessage, User
|
||||||
|
};
|
||||||
|
use revolt_models::v0::MessageAuthor;
|
||||||
|
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||||
|
use revolt_result::{create_error, Result};
|
||||||
|
use rocket::State;
|
||||||
|
use rocket_empty::EmptyResponse;
|
||||||
|
|
||||||
|
/// # Unpins a message
|
||||||
|
///
|
||||||
|
/// Unpins a message by its id.
|
||||||
|
#[openapi(tag = "Messaging")]
|
||||||
|
#[delete("/<target>/messages/<msg>/pin")]
|
||||||
|
pub async fn message_unpin(
|
||||||
|
db: &State<Database>,
|
||||||
|
user: User,
|
||||||
|
target: Reference,
|
||||||
|
msg: Reference,
|
||||||
|
) -> Result<EmptyResponse> {
|
||||||
|
let channel = target.as_channel(db).await?;
|
||||||
|
|
||||||
|
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
|
||||||
|
calculate_channel_permissions(&mut query)
|
||||||
|
.await
|
||||||
|
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
|
||||||
|
|
||||||
|
let mut message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||||
|
|
||||||
|
if !message.pinned.unwrap_or_default() {
|
||||||
|
return Err(create_error!(NotPinned))
|
||||||
|
}
|
||||||
|
|
||||||
|
message.update(db, PartialMessage::default(), vec![FieldsMessage::Pinned]).await?;
|
||||||
|
|
||||||
|
SystemMessage::MessageUnpinned {
|
||||||
|
id: message.id.clone(),
|
||||||
|
by: user.id.clone()
|
||||||
|
}
|
||||||
|
.into_message(channel.id().to_string())
|
||||||
|
.send(
|
||||||
|
db,
|
||||||
|
MessageAuthor::System {
|
||||||
|
username: &user.username,
|
||||||
|
avatar: user.avatar.as_ref().map(|file| file.id.as_ref())
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
&channel,
|
||||||
|
false
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(EmptyResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{rocket, util::test::TestHarness};
|
||||||
|
use revolt_database::{events::client::EventV1, util::{idempotency::IdempotencyKey, reference::Reference}, Member, Message, PartialMessage, Server};
|
||||||
|
use revolt_models::v0::{self, FieldsMessage, SystemMessage};
|
||||||
|
use rocket::http::{Header, Status};
|
||||||
|
|
||||||
|
#[rocket::async_test]
|
||||||
|
async fn unpin_message() {
|
||||||
|
let mut harness = TestHarness::new().await;
|
||||||
|
let (_, session, user) = harness.new_user().await;
|
||||||
|
|
||||||
|
let (server, channels) = Server::create(
|
||||||
|
&harness.db,
|
||||||
|
v0::DataCreateServer {
|
||||||
|
name: "Test Server".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
&user,
|
||||||
|
true
|
||||||
|
).await.expect("Failed to create test server");
|
||||||
|
|
||||||
|
let channel = &channels[0];
|
||||||
|
|
||||||
|
Member::create(&harness.db, &server, &user, Some(channels.clone())).await.expect("Failed to create member");
|
||||||
|
let member = Reference::from_unchecked(user.id.clone()).as_member(&harness.db, &server.id).await.expect("Failed to get member");
|
||||||
|
|
||||||
|
let message = Message::create_from_api(
|
||||||
|
&harness.db,
|
||||||
|
channel.clone(),
|
||||||
|
v0::DataMessageSend {
|
||||||
|
content:Some("Test message".to_string()),
|
||||||
|
nonce: None,
|
||||||
|
attachments: None,
|
||||||
|
replies: None,
|
||||||
|
embeds: None,
|
||||||
|
masquerade: None,
|
||||||
|
interactions: None,
|
||||||
|
flags: None
|
||||||
|
},
|
||||||
|
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
|
||||||
|
Some(user.clone().into(&harness.db, Some(&user)).await),
|
||||||
|
Some(member.into()),
|
||||||
|
user.limits().await,
|
||||||
|
IdempotencyKey::unchecked_from_string("0".to_string()),
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create message");
|
||||||
|
|
||||||
|
harness.db.update_message(
|
||||||
|
&message.id,
|
||||||
|
&PartialMessage {
|
||||||
|
pinned: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
vec![]
|
||||||
|
).await.expect("Failed to update message");
|
||||||
|
|
||||||
|
let response = harness
|
||||||
|
.client
|
||||||
|
.delete(format!("/channels/{}/messages/{}/pin", channel.id(), &message.id))
|
||||||
|
.header(Header::new("x-session-token", session.token.to_string()))
|
||||||
|
.dispatch()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(response.status(), Status::NoContent);
|
||||||
|
drop(response);
|
||||||
|
|
||||||
|
harness.wait_for_event(channel.id(), |event| {
|
||||||
|
match event {
|
||||||
|
EventV1::Message(message) => {
|
||||||
|
match &message.system {
|
||||||
|
Some(SystemMessage::MessageUnpinned { by, .. }) => {
|
||||||
|
assert_eq!(by, &user.id);
|
||||||
|
|
||||||
|
true
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
harness.wait_for_event(channel.id(), |event| {
|
||||||
|
match event {
|
||||||
|
EventV1::MessageUpdate { id, clear, .. } => {
|
||||||
|
assert_eq!(&message.id, id);
|
||||||
|
assert_eq!(clear, &[FieldsMessage::Pinned]);
|
||||||
|
|
||||||
|
true
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let updated_message = Reference::from_unchecked(message.id)
|
||||||
|
.as_message(&harness.db)
|
||||||
|
.await
|
||||||
|
.expect("Failed to find updated message");
|
||||||
|
|
||||||
|
assert_eq!(updated_message.pinned, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,7 @@ pub async fn unreact_message(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch relevant message
|
// Fetch relevant message
|
||||||
let message = msg.as_message_in_channel(db, &channel.id()).await?;
|
let message = msg.as_message_in_channel(db, channel.id()).await?;
|
||||||
|
|
||||||
// Check if we should wipe all of this reaction
|
// Check if we should wipe all of this reaction
|
||||||
if remove_all {
|
if remove_all {
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ mod message_clear_reactions;
|
|||||||
mod message_delete;
|
mod message_delete;
|
||||||
mod message_edit;
|
mod message_edit;
|
||||||
mod message_fetch;
|
mod message_fetch;
|
||||||
|
mod message_pin;
|
||||||
mod message_query;
|
mod message_query;
|
||||||
mod message_react;
|
mod message_react;
|
||||||
mod message_search;
|
mod message_search;
|
||||||
mod message_send;
|
mod message_send;
|
||||||
|
mod message_unpin;
|
||||||
mod message_unreact;
|
mod message_unreact;
|
||||||
mod permissions_set;
|
mod permissions_set;
|
||||||
mod permissions_set_default;
|
mod permissions_set_default;
|
||||||
@@ -37,10 +39,12 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
|
|||||||
message_send::message_send,
|
message_send::message_send,
|
||||||
message_query::query,
|
message_query::query,
|
||||||
message_search::search,
|
message_search::search,
|
||||||
|
message_pin::message_pin,
|
||||||
message_fetch::fetch,
|
message_fetch::fetch,
|
||||||
message_edit::edit,
|
message_edit::edit,
|
||||||
message_bulk_delete::bulk_delete_messages,
|
message_bulk_delete::bulk_delete_messages,
|
||||||
message_delete::delete,
|
message_delete::delete,
|
||||||
|
message_unpin::message_unpin,
|
||||||
group_create::create_group,
|
group_create::create_group,
|
||||||
group_add_member::add_member,
|
group_add_member::add_member,
|
||||||
group_remove_member::remove_member,
|
group_remove_member::remove_member,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ pub async fn create_webhook(
|
|||||||
id: webhook_id,
|
id: webhook_id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
avatar,
|
avatar,
|
||||||
channel_id: channel.id(),
|
channel_id: channel.id().to_string(),
|
||||||
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
|
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
|
||||||
token: Some(nanoid::nanoid!(64)),
|
token: Some(nanoid::nanoid!(64)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub async fn fetch_webhooks(
|
|||||||
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
|
||||||
|
|
||||||
Ok(Json(
|
Ok(Json(
|
||||||
db.fetch_webhooks_for_channel(&channel.id())
|
db.fetch_webhooks_for_channel(channel.id())
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|v| v.into())
|
.map(|v| v.into())
|
||||||
|
|||||||
@@ -45,9 +45,9 @@ pub async fn create_emoji(
|
|||||||
|
|
||||||
// Check that we haven't hit the emoji limit
|
// Check that we haven't hit the emoji limit
|
||||||
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
|
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
|
||||||
if emojis.len() >= config.features.limits.default.server_emoji {
|
if emojis.len() >= config.features.limits.global.server_emoji {
|
||||||
return Err(create_error!(TooManyEmoji {
|
return Err(create_error!(TooManyEmoji {
|
||||||
max: config.features.limits.default.server_emoji,
|
max: config.features.limits.global.server_emoji,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ pub async fn join(
|
|||||||
match &invite {
|
match &invite {
|
||||||
Invite::Server { server, .. } => {
|
Invite::Server { server, .. } => {
|
||||||
let server = db.fetch_server(server).await?;
|
let server = db.fetch_server(server).await?;
|
||||||
let channels = Member::create(db, &server, &user, None).await?;
|
let (_, channels) = Member::create(db, &server, &user, None).await?;
|
||||||
|
|
||||||
Ok(Json(InviteJoinResponse::Server {
|
Ok(Json(InviteJoinResponse::Server {
|
||||||
channels: channels.into_iter().map(|c| c.into()).collect(),
|
channels: channels.into_iter().map(|c| c.into()).collect(),
|
||||||
server: server.into(),
|
server: server.into(),
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub async fn complete(
|
|||||||
Ok(Json(
|
Ok(Json(
|
||||||
User::create(db, data.username, session.user_id, None)
|
User::create(db, data.username, session.user_id, None)
|
||||||
.await?
|
.await?
|
||||||
.into_self()
|
.into_self(false)
|
||||||
.await,
|
.await,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub async fn list(
|
|||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|u| u.into_self()),
|
.map(|u| u.into_self(false)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ pub async fn create(
|
|||||||
.throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?;
|
.throw_if_lacking_channel_permission(ChannelPermission::ManageRole)?;
|
||||||
|
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
if server.roles.len() >= config.features.limits.default.server_roles {
|
if server.roles.len() >= config.features.limits.global.server_roles {
|
||||||
return Err(create_error!(TooManyRoles {
|
return Err(create_error!(TooManyRoles {
|
||||||
max: config.features.limits.default.server_roles,
|
max: config.features.limits.global.server_roles,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
use revolt_database::{util::reference::Reference, Database};
|
use revolt_database::{
|
||||||
|
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||||
|
Database, User,
|
||||||
|
};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
|
use revolt_permissions::PermissionQuery;
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
use rocket::{serde::json::Json, State};
|
use rocket::{serde::json::Json, State};
|
||||||
|
|
||||||
@@ -10,10 +14,16 @@ use rocket::{serde::json::Json, State};
|
|||||||
#[get("/<target>/roles/<role_id>")]
|
#[get("/<target>/roles/<role_id>")]
|
||||||
pub async fn fetch(
|
pub async fn fetch(
|
||||||
db: &State<Database>,
|
db: &State<Database>,
|
||||||
|
user: User,
|
||||||
target: Reference,
|
target: Reference,
|
||||||
role_id: String,
|
role_id: String,
|
||||||
) -> Result<Json<v0::Role>> {
|
) -> Result<Json<v0::Role>> {
|
||||||
let mut server = target.as_server(db).await?;
|
let mut server = target.as_server(db).await?;
|
||||||
|
let mut query = DatabasePermissionQuery::new(db, &user).server(&server);
|
||||||
|
if !query.are_we_a_member().await {
|
||||||
|
return Err(create_error!(NotFound));
|
||||||
|
}
|
||||||
|
|
||||||
let role = server.roles.remove(&role_id);
|
let role = server.roles.remove(&role_id);
|
||||||
|
|
||||||
if let Some(role) = role {
|
if let Some(role) = role {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user