Compare commits

...
43 Commits
Author SHA1 Message Date
Paul Makles 5c8ece0727 fix: add new crates to docker build 2024-08-29 20:26:16 +01:00
Paul Makles ab6b92aa2a chore(core/files): update toml 2024-08-29 20:22:09 +01:00
Paul Makles 239710951a chore: add revolt-files to justfile publish cmd 2024-08-29 20:20:47 +01:00
Paul Makles 28960979ec chore: bump version to 0.7.16 2024-08-29 20:18:34 +01:00
Paul Makles 1938ebc3fa feat(services/autumn): scaffold implementation of api 2024-08-29 20:18:17 +01:00
Paul Makles 1fb4032d9e refactor: switch from fcm to fcm_v1 crate 2024-08-29 20:17:52 +01:00
Paul Makles ae2194cce8 chore: (wip) suspend bindings 2024-08-29 20:17:35 +01:00
Paul Makles 949a0cf649 chore(core/config): add configuration options 2024-08-29 20:17:10 +01:00
Paul Makles f6c57b23b4 feat: bindings for node.js 2024-08-26 20:01:10 +01:00
Paul Makles 12ae781621 fix: detach rather than delete emoji during server deletion 2024-08-20 13:19:23 +01:00
Paul Makles a52d610e6c ci: use docker compose instead of docker-compose
Signed-off-by: Paul Makles <me@insrt.uk>
2024-08-05 16:32:21 +01:00
Paul Makles f513a9cb5e chore: bump version to 0.7.15 2024-08-05 16:22:18 +01:00
Paul Makles 535f01604b fix(core/database): include users from pin events 2024-08-05 16:21:00 +01:00
Paul Makles f8ec6ba5ff fix(delta): add check to roles_fetch route 2024-08-05 16:20:21 +01:00
Paul Makles b8fdc07508 refactor: remove inspect_err debug code 2024-08-05 16:20:21 +01:00
Paul Makles 5ce7530206 chore: bump nixpkgs and MSRV 2024-08-05 16:20:21 +01:00
Paul Makles b36bac3225 merge: pull request #333 from revoltchat/feat/apns
APNS fixes
2024-08-05 17:17:29 +02:00
infi beef06ebe3 chore: applemusic option for special embed
See https://github.com/revoltchat/january/pull/41

Signed-off-by: Infi <infi@infi.sh>
2024-08-05 17:16:24 +02:00
Zomatree 4fc46f765b refactor: change Channel::id to return a reference 2024-07-31 00:05:40 +01:00
Zomatree b45ae2cd4d fix: include user for pinned system messages 2024-07-30 18:09:35 +01:00
Zomatree 0a8089787b chore: remove debugging logging 2024-07-30 18:06:54 +01:00
Paul Makles 475fa6f2e6 chore: add optional fields for CI as template [skip ci]
Signed-off-by: Paul Makles <me@insrt.uk>
2024-07-28 10:43:01 +01:00
Zomatree 96bf73cb57 fix: send pin system messages 2024-07-22 00:09:27 +01:00
Zomatree 87a9fb79f2 chore: Write tests for pinning 2024-07-21 22:22:46 +01:00
Zomatree c50435d499 fix: Switch to remove for unpinning 2024-07-21 22:16:23 +01:00
Zomatree d2f4823ceb chore: Add index and migration 2024-07-21 22:12:55 +01:00
Zomatree 39230c559a fix: Bump revolt_a2 dep 2024-07-20 22:43:12 +01:00
Zomatree 32d1d5df2e feat: Send badge updates from message acks 2024-07-20 20:49:22 +01:00
Zomatree 7547fbe245 Merge branch 'IAmTomahawkx-main' 2024-07-20 19:51:41 +01:00
IAmTomahawkx 9f13cb6f47 add Badge update capabilities
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-19 13:13:13 -07:00
Tom d94571fbf9 Merge branch 'main' into main
Signed-off-by: Tom <iamtomahawkx@gmail.com>
2024-07-19 09:20:42 -07:00
Zomatree 389ecc0e5c feat: Message pinning 2024-07-18 18:37:41 +01:00
Paul Makles 412f4a99d7 feat(core/events): add user settings / unreads to Ready payload
refactor(core/events): make all Ready payload items optional
2024-07-14 17:03:40 +01:00
Paul Makles caa0795d07 fix(core/database); never include relations on known users 2024-07-14 16:27:11 +01:00
Paul Makles 5c82343bb3 chore: bump version to 0.7.14
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
Paul Makles e17af1c064 feat: add reason to ServerMemberLeave event
closes #314

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
Paul Makles a9e309395e fix: execute query to remove users from groups
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
IAmTomahawkx 8468ce19cb fix apple push notifications
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
IAmTomahawkx 27f15f7b02 change apns to use sandbox, and (somewhat) provide a custom payload
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2024-07-12 15:25:33 -07:00
Paul Makles 78b9fa168b chore: bump version to 0.7.14 2024-07-10 14:17:31 +01:00
Paul Makles e290d168ac feat: add reason to ServerMemberLeave event
closes #314
2024-07-10 14:12:53 +01:00
Paul Makles 78cd89ec32 fix: execute query to remove users from groups 2024-07-10 14:04:17 +01:00
Paul Makles f9d9059e73 fix: bot model test did not destruct tuple 2024-06-29 18:20:01 +01:00
84 changed files with 3589 additions and 486 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ jobs:
- name: Run services in background
run: |
docker-compose -f docker-compose.db.yml up -d
docker compose -f docker-compose.db.yml up -d
- name: Copy .env.example
run: |
Generated
+1484 -264
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -1,6 +1,12 @@
[workspace]
resolver = "2"
members = ["crates/delta", "crates/bonfire", "crates/core/*"]
members = [
"crates/delta",
"crates/bonfire",
"crates/core/*",
"crates/services/*",
"crates/bindings/*",
]
[patch.crates-io]
# mobc-redis = { git = "https://github.com/insertish/mobc", rev = "8b880bb59f2ba80b4c7bc40c649c113d8857a186" }
+3
View File
@@ -17,14 +17,17 @@ RUN sh /tmp/build-image-layer.sh tools
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
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/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
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
# Build all apps
+3
View File
@@ -13,14 +13,17 @@ COPY scripts/build-image-layer.sh /tmp/
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
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/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
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
# Build all apps
+2 -2
View File
@@ -17,7 +17,7 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
## Minimum Supported Rust Version
Rust 1.70 or higher.
Rust 1.76 or higher.
## Development Guide
@@ -143,7 +143,7 @@ Now run tests for whichever database:
```sh
TEST_DB=REFERENCE cargo nextest run
TEST_DB=MONGOBD cargo nextest run
TEST_DB=MONGODB cargo nextest run
```
## License
+7
View File
@@ -0,0 +1,7 @@
target
index.node
**/node_modules
**/.DS_Store
npm-debug.log*
cargo.log
cross.log
+24
View File
@@ -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" }
+92
View File
@@ -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).
+66
View File
@@ -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 }>;
+36
View File
@@ -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"
}
}
+117
View File
@@ -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
+190
View File
@@ -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(())
}
+53
View File
@@ -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())
);
+6
View File
@@ -0,0 +1,6 @@
{
"name": "bindings",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "0.7.13"
version = "0.7.16"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -41,7 +41,7 @@ revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.7.13", path = "../core/permissions" }
revolt-permissions = { version = "0.7.16", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis
+12
View File
@@ -1,5 +1,6 @@
use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender;
use revolt_database::events::client::ReadyPayloadFields;
use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize};
@@ -83,6 +84,17 @@ impl ProtocolConfiguration {
pub fn get_protocol_format(&self) -> &ProtocolFormat {
&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
+62 -17
View File
@@ -1,8 +1,9 @@
use std::collections::HashSet;
use revolt_database::{
events::client::EventV1, util::permissions::DatabasePermissionQuery, Channel, Database, Member,
MemberCompositeKey, Presence, RelationshipStatus,
events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery,
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
};
use revolt_models::v0;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
@@ -90,7 +91,11 @@ impl Cache {
/// State Manager
impl State {
/// 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();
self.cache.is_bot = user.bot.is_some();
@@ -150,14 +155,36 @@ impl State {
.await?;
// Fetch customisations.
let emojis = db
.fetch_emoji_by_parent_ids(
&servers
.iter()
.map(|x| x.id.to_string())
.collect::<Vec<String>>(),
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
Some(
db.fetch_emoji_by_parent_ids(
&servers
.iter()
.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.
self.cache.users = users.iter().cloned().map(|x| (x.id.clone(), x)).collect();
@@ -201,13 +228,31 @@ impl State {
for channel in &channels {
self.insert_subscription(channel.id().to_string()).await;
}
Ok(EventV1::Ready {
users,
servers: servers.into_iter().map(Into::into).collect(),
channels: channels.into_iter().map(Into::into).collect(),
members: members.into_iter().map(Into::into).collect(),
emojis: emojis.into_iter().map(Into::into).collect(),
users: if fields.contains(&ReadyPayloadFields::Users) {
Some(users)
} else {
None
},
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 { .. } => {
// We will always receive ServerCreate when joining a new server.
}
EventV1::ServerMemberLeave { id, user } => {
EventV1::ServerMemberLeave { id, user, .. } => {
if user == &self.cache.user_id {
self.remove_subscription(id).await;
+4 -1
View File
@@ -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.
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,
Err(err) => {
sentry::capture_error(&err);
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-config"
version = "0.7.13"
version = "0.7.16"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
+103 -14
View File
@@ -1,8 +1,17 @@
[database]
# MongoDB connection URL
# Defaults to the container name specified in self-hosted
mongodb = "mongodb://database"
# Redis connection URL
# Defaults to the container name specified in self-hosted
redis = "redis://redis/"
[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"
api = "http://local.revolt.chat/api"
events = "ws://local.revolt.chat/ws"
@@ -14,39 +23,113 @@ voso_legacy_ws = ""
[api]
[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
[api.smtp]
# Email server configuration for verification
# Defaults to no email verification (host field is empty)
host = ""
username = ""
password = ""
from_address = ""
from_address = "noreply@example.com"
reply_to = "noreply@example.com"
port = 587
use_tls = true
[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"
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
[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]
# Authifier Shield API key
authifier_shield_key = ""
# Legacy voice server management token
voso_legacy_token = ""
# Whether services are behind the Cloudflare network
trust_cloudflare = false
[api.security.captcha]
# hCaptcha configuration
hcaptcha_key = ""
hcaptcha_sitekey = ""
[api.workers]
# Maximum concurrent connections (to proxy server)
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]
# Feature gate options
webhooks_enabled = false
[features.limits]
@@ -60,8 +143,13 @@ server_emoji = 100
server_roles = 200
server_channels = 200
# How many days since creation a user is considered new
new_user_days = 3
# Maximum permissible body size in bytes for uploads
# (should be greater than any one file upload limit)
body_limit_size = 20_000_000
[features.limits.new_user]
outgoing_friend_requests = 5
@@ -70,12 +158,12 @@ message_length = 2000
message_attachments = 5
servers = 100
attachment_size = 20000000
avatar_size = 4000000
background_size = 6000000
icon_size = 2500000
banner_size = 6000000
emoji_size = 500000
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
@@ -85,13 +173,14 @@ message_length = 2000
message_attachments = 5
servers = 100
attachment_size = 20000000
avatar_size = 4000000
background_size = 6000000
icon_size = 2500000
banner_size = 6000000
emoji_size = 500000
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]
# Configuration for Sentry error reporting
api = ""
events = ""
+13 -1
View File
@@ -74,11 +74,21 @@ pub struct ApiVapid {
#[derive(Deserialize, Debug, Clone)]
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,
@@ -125,6 +135,8 @@ pub struct GlobalLimits {
pub server_channels: usize,
pub new_user_days: usize,
pub body_limit_size: usize,
}
#[derive(Deserialize, Debug, Clone)]
+8 -10
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-database"
version = "0.7.13"
version = "0.7.16"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,13 +23,13 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.7.13", path = "../config" }
revolt-result = { version = "0.7.13", path = "../result" }
revolt-models = { version = "0.7.13", path = "../models", features = [
revolt-config = { version = "0.7.16", path = "../config" }
revolt-result = { version = "0.7.16", path = "../result" }
revolt-models = { version = "0.7.16", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.7.13", path = "../presence" }
revolt-permissions = { version = "0.7.13", path = "../permissions", features = [
revolt-presence = { version = "0.7.16", path = "../presence" }
revolt-permissions = { version = "0.7.16", path = "../permissions", features = [
"serde",
"bson",
] }
@@ -85,11 +85,9 @@ revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.9.1", optional = true }
# Notifications
fcm = "0.9.2"
fcm_v1 = "0.3.0"
web-push = "0.10.0"
revolt_a2 = { version = "0.10.0", default-features = false, features = [
"ring",
] }
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.8" }
+35 -10
View File
@@ -2,10 +2,7 @@ use authifier::AuthifierEvent;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{
AppendMessage, Channel, Emoji, FieldsChannel, FieldsMember, FieldsRole, FieldsServer,
FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember,
PartialMessage, PartialRole, PartialServer, PartialUser, PartialWebhook, Report, Server, User,
UserSettings, Webhook,
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
};
use revolt_result::Error;
@@ -39,6 +36,19 @@ pub enum ErrorEvent {
APIError(Error),
}
/// Fields provided in Ready payload
#[derive(PartialEq)]
pub enum ReadyPayloadFields {
Users,
Servers,
Channels,
Members,
Emoji,
UserSettings(Vec<String>),
ChannelUnreads,
}
/// Protocol Events
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
@@ -52,11 +62,21 @@ pub enum EventV1 {
Logout,
/// Basic data to cache
Ready {
users: Vec<User>,
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
emojis: Vec<Emoji>,
#[serde(skip_serializing_if = "Option::is_none")]
users: Option<Vec<User>>,
#[serde(skip_serializing_if = "Option::is_none")]
servers: Option<Vec<Server>>,
#[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
@@ -69,6 +89,7 @@ pub enum EventV1 {
id: String,
channel: String,
data: PartialMessage,
clear: Vec<FieldsMessage>,
},
/// Append information to existing message
@@ -136,7 +157,11 @@ pub enum EventV1 {
ServerMemberJoin { id: String, user: String },
/// User left server
ServerMemberLeave { id: String, user: String },
ServerMemberLeave {
id: String,
user: String,
reason: RemovalIntention,
},
/// Server role created or updated
ServerRoleUpdate {
+5
View File
@@ -87,3 +87,8 @@ pub mod tasks;
pub fn if_false(t: &bool) -> bool {
!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
},
"name": "author"
}
},
{
"key": {
"channel": 1_i32,
"pinned": 1_i32
},
"name": "channel_pinned_compound"
},
]
},
None,
@@ -20,7 +20,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 27;
pub const LATEST_REVISION: i32 = 28;
pub async fn migrate_database(db: &MongoDb) {
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`.
// Reminder to update LATEST_REVISION when adding new migrations.
@@ -172,7 +172,7 @@ mod tests {
.await
.unwrap();
let bot = Bot::create(
let (bot, _) = Bot::create(
&db,
"Bot Name".to_string(),
&owner,
@@ -7,13 +7,13 @@ mod reference;
#[async_trait]
pub trait AbstractChannelUnreads: Sync + Send {
/// Acknowledge a message.
/// Acknowledge a message, and returns updated channel unread.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()>;
) -> Result<Option<ChannelUnread>>;
/// Acknowledge many channels.
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.
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 mongodb::options::FindOneAndUpdateOptions;
use mongodb::options::ReturnDocument;
use mongodb::options::UpdateOptions;
use revolt_result::Result;
use ulid::Ulid;
@@ -12,31 +14,35 @@ static COL: &str = "channel_unreads";
#[async_trait]
impl AbstractChannelUnreads for MongoDb {
/// Acknowledge a message.
/// Acknowledge a message, and returns updated channel unread.
async fn acknowledge_message(
&self,
channel_id: &str,
user_id: &str,
message_id: &str,
) -> Result<()> {
self.col::<Document>(COL)
.update_one(
) -> Result<Option<ChannelUnread>> {
self.col::<ChannelUnread>(COL)
.find_one_and_update(
doc! {
"_id.channel": channel_id,
"_id.user": user_id,
},
doc! {
"$unset": {
"mentions": 1_i32
"$pull": {
"mentions": {
"$lt": message_id
}
},
"$set": {
"last_id": message_id
}
},
UpdateOptions::builder().upsert(true).build(),
FindOneAndUpdateOptions::builder()
.upsert(true)
.return_document(ReturnDocument::After)
.build(),
)
.await
.map(|_| ())
.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,
user_id: &str,
message_id: &str,
) -> Result<()> {
) -> Result<Option<ChannelUnread>> {
let mut unreads = self.channel_unreads.lock().await;
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
@@ -27,14 +27,14 @@ impl AbstractChannelUnreads for ReferenceDb {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
id: key.clone(),
last_id: Some(message_id.to_string()),
mentions: None,
},
);
}
Ok(())
Ok(unreads.get(&key).cloned())
}
/// Acknowledge many channels.
@@ -87,4 +87,14 @@ impl AbstractChannelUnreads for ReferenceDb {
.cloned()
.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())
}
}
@@ -417,13 +417,13 @@ impl Channel {
}
/// Clone this channel's id
pub fn id(&self) -> String {
pub fn id(&self) -> &str {
match self {
Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::SavedMessages { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id.clone(),
| Channel::VoiceChannel { id, .. } => id,
}
}
@@ -702,6 +702,8 @@ impl Channel {
}
}
db.remove_user_from_group(id, &user.id).await?;
EventV1::ChannelGroupLeave {
id: id.to_string(),
user: user.id.to_string(),
@@ -266,7 +266,7 @@ impl AbstractChannels for MongoDb {
.await?;
// 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
async fn insert_channel(&self, channel: &Channel) -> Result<()> {
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());
Ok(())
} else {
@@ -148,7 +148,7 @@ impl AbstractChannels for ReferenceDb {
// Delete a channel
async fn delete_channel(&self, channel: &Channel) -> Result<()> {
let mut channels = self.channels.lock().await;
if channels.remove(&channel.id()).is_some() {
if channels.remove(channel.id()).is_some() {
Ok(())
} else {
Err(create_error!(NotFound))
@@ -65,6 +65,9 @@ auto_derived_partial!(
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
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")]
@@ -99,6 +102,10 @@ auto_derived!(
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
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
@@ -168,6 +175,8 @@ auto_derived!(
pub author: Option<String>,
/// Search query
pub query: Option<String>,
/// Search for pinned
pub pinned: Option<bool>,
}
/// Message Query
@@ -183,6 +192,11 @@ auto_derived!(
#[serde(flatten)]
pub time_period: MessageTimePeriod,
}
/// Optional fields on message
pub enum FieldsMessage {
Pinned,
}
);
#[allow(clippy::derivable_impls)]
@@ -205,6 +219,7 @@ impl Default for Message {
interactions: Default::default(),
masquerade: None,
flags: None,
pinned: None,
}
}
}
@@ -278,7 +293,7 @@ impl Message {
let message_id = Ulid::new().to_string();
let mut message = Message {
id: message_id.clone(),
channel: channel.id(),
channel: channel.id().to_string(),
masquerade: data.masquerade.map(|masquerade| masquerade.into()),
interactions: data
.interactions
@@ -444,8 +459,8 @@ impl Message {
) -> Result<()> {
self.send_without_notifications(
db,
user,
member,
user.clone(),
member.clone(),
matches!(channel, Channel::DirectMessage { .. }),
generate_embeds,
)
@@ -463,9 +478,9 @@ impl Message {
}
},
PushNotification::from(
self.clone().into_model(None, None),
self.clone().into_model(user, member),
Some(author),
&channel.id(),
channel.id(),
)
.await,
)
@@ -513,14 +528,26 @@ impl Message {
}
/// 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());
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 {
id: self.id.clone(),
channel: self.channel.clone(),
data: partial.into(),
clear: remove.into_iter().map(|field| field.into()).collect(),
}
.p(self.channel.clone())
.await;
@@ -571,6 +598,12 @@ impl Message {
users.push(id.clone());
}
v0::SystemMessage::Text { .. } => {}
v0::SystemMessage::MessagePinned { by, .. } => {
users.push(by.clone());
}
v0::SystemMessage::MessageUnpinned { by, .. } => {
users.push(by.clone());
}
}
}
users
@@ -800,6 +833,12 @@ impl Message {
// Write to database
db.clear_reaction(&self.id, emoji).await
}
pub fn remove_field(&mut self, field: &FieldsMessage) {
match field {
FieldsMessage::Pinned => self.pinned = None,
}
}
}
impl SystemMessage {
@@ -1,6 +1,6 @@
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage};
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage};
mod mongodb;
mod reference;
@@ -20,7 +20,7 @@ pub trait AbstractMessages: Sync + Send {
async fn fetch_messages_by_id(&self, ids: &[String]) -> Result<Vec<Message>>;
/// 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
async fn append_message(&self, id: &str, append: &AppendMessage) -> Result<()>;
@@ -5,7 +5,8 @@ use revolt_models::v0::MessageSort;
use revolt_result::Result;
use crate::{
AppendMessage, DocumentId, Message, MessageQuery, MessageTimePeriod, MongoDb, PartialMessage,
AppendMessage, DocumentId, FieldsMessage, IntoDocumentPath, Message, MessageQuery,
MessageTimePeriod, MongoDb, PartialMessage,
};
use super::AbstractMessages;
@@ -50,6 +51,10 @@ impl AbstractMessages for MongoDb {
false
};
if let Some(pinned) = query.filter.pinned {
filter.insert("pinned", pinned);
};
// 2. Find query limit
let limit = query.limit.unwrap_or(50);
@@ -177,8 +182,22 @@ impl AbstractMessages for MongoDb {
}
/// Update a given message with new information
async fn update_message(&self, id: &str, message: &PartialMessage) -> Result<()> {
query!(self, update_one_by_id, COL, id, message, vec![], None).map(|_| ())
async fn update_message(
&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
@@ -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 {
pub async fn delete_bulk_messages(&self, projection: Document) -> Result<()> {
let mut for_attachments = projection.clone();
@@ -2,7 +2,7 @@ use futures::future::try_join_all;
use indexmap::IndexSet;
use revolt_result::Result;
use crate::{AppendMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
use crate::{AppendMessage, FieldsMessage, Message, MessageQuery, PartialMessage, ReferenceDb};
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
})
.cloned()
@@ -183,10 +189,15 @@ impl AbstractMessages for ReferenceDb {
}
/// 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;
if let Some(message_data) = messages.get_mut(id) {
message_data.apply_options(message.to_owned());
for field in remove {
#[allow(clippy::disallowed_methods)]
message_data.remove_field(&field);
}
Ok(())
} else {
Err(create_error!(NotFound))
@@ -81,7 +81,7 @@ impl Member {
server: &Server,
user: &User,
channels: Option<Vec<Channel>>,
) -> Result<Vec<Channel>> {
) -> Result<(Member, Vec<Channel>)> {
if db.fetch_ban(&server.id, &user.id).await.is_ok() {
return Err(create_error!(Banned));
}
@@ -155,7 +155,7 @@ impl Member {
.ok();
}
Ok(channels)
Ok((member, channels))
}
/// Update member data
@@ -229,6 +229,7 @@ impl Member {
EventV1::ServerMemberLeave {
id: self.id.server.to_string(),
user: self.id.user.to_string(),
reason: intention.clone().into(),
}
.p(self.id.server.to_string())
.await;
@@ -175,7 +175,7 @@ impl Server {
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?;
Ok((server, channels))
}
@@ -214,14 +214,21 @@ impl MongoDb {
// Delete all emoji.
self.col::<Document>("emojis")
.delete_many(
.update_many(
doc! {
"parent.id": &server_id
},
doc! {
"$set": {
"parent": {
"type": "Detached"
}
}
},
None,
)
.await
.map_err(|_| create_database_error!("delete_many", "emojis"))?;
.map_err(|_| create_database_error!("update_many", "emojis"))?;
// Delete all channels.
self.col::<Document>("channels")
@@ -662,6 +662,14 @@ impl User {
}
}
/// 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
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
self.update(
+40 -9
View File
@@ -5,7 +5,9 @@ use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use std::{collections::HashMap, time::Duration};
use super::DelayedTask;
use revolt_result::Result;
use super::{apple_notifications::{self, ApnJob}, DelayedTask};
/// Enumeration of possible events
#[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());
}
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
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 keys = vec![];
@@ -71,13 +108,7 @@ pub async fn worker(db: Database) {
let Task { event } = task.data;
let (user, channel) = key;
if let Err(err) = match &event {
#[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
}
} {
if let Err(err) = handle_ack_event(&event, &db, &authifier_db, user, channel).await {
error!("{err:?} for {event:?}. ({user}, {channel})");
} else {
info!("User {user} ack in {channel} with {event:?}");
@@ -6,22 +6,64 @@ use base64::{
};
use deadqueue::limited::Queue;
use once_cell::sync::Lazy;
use revolt_a2::{Client, ClientConfig, DefaultNotificationBuilder};
use revolt_a2::{Error, ErrorBody, ErrorReason, NotificationBuilder, Response};
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::PushNotification;
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 ApnTask {
pub struct AlertJob {
/// Session Id
session_id: String,
/// Device token
device_token: String,
/// User Id
user_id: String,
/// Title
title: String,
@@ -30,32 +72,116 @@ pub struct ApnTask {
/// 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 ApnTask {
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,
) -> ApnTask {
ApnTask {
session_id,
device_token,
title: notification.author.to_string(),
body: notification.body.to_string(),
thread_id: notification.tag.to_string(),
) -> 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,
}),
}
}
}
static Q: Lazy<Queue<ApnTask>> = Lazy::new(|| Queue::new(10_000));
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: ApnTask) {
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;
@@ -67,27 +193,97 @@ pub async fn worker(db: Database) {
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,
ClientConfig::default(),
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 = DefaultNotificationBuilder::new()
.set_title(&task.title)
.set_body(&task.body)
.set_thread_id(&task.thread_id)
.build(&task.device_token, Default::default());
let payload: AssembledPayload;
if let Err(err) = client.send(payload).await {
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:
@@ -98,7 +294,10 @@ pub async fn worker(db: Database) {
..
}) => {
if let Err(err) = db
.remove_push_subscription_by_session_id(&task.session_id)
.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);
+1 -1
View File
@@ -18,7 +18,7 @@ pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
task::spawn(apple_notifications::worker(db.clone()));
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(process_embeds::worker(db.clone()));
task::spawn(web_push::worker(authifier_db.clone()));
+58 -26
View File
@@ -1,4 +1,7 @@
use std::collections::HashSet;
use std::{
collections::{HashMap, HashSet},
time::Duration,
};
use authifier::Database;
use base64::{
@@ -6,7 +9,7 @@ use base64::{
Engine as _,
};
use deadqueue::limited::Queue;
use fcm::FcmError;
use fcm_v1::auth::{Authenticator, ServiceAccountKey};
use once_cell::sync::Lazy;
use revolt_config::config;
use revolt_models::v0::PushNotification;
@@ -55,10 +58,28 @@ pub async fn worker(db: Database) {
let config = config().await;
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
} 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
@@ -77,26 +98,38 @@ pub async fn worker(db: Database) {
let PushNotification {
author,
icon,
image: _,
image,
body,
tag,
timestamp: _,
tag: _,
timestamp,
url: _,
message,
} = &task.payload;
let mut notification = fcm::NotificationBuilder::new();
notification.title(author);
notification.icon(icon);
notification.body(body);
notification.tag(tag);
// TODO: expand support for fields
let notification = notification.finalize();
let message = fcm_v1::message::Message {
token: Some(sub.auth),
data: Some(HashMap::from([
(
"author".to_owned(),
serde_json::Value::String(author.clone()),
),
("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 =
fcm::MessageBuilder::new(&config.api.fcm.api_key, &sub.auth);
message_builder.notification(notification);
if let Err(err) = client.send(message_builder.finalize()).await {
if let Err(err) = client.send(&message).await {
error!("Failed to send FCM notification! {:?}", err);
} else {
info!("Sent FCM notification to {:?}.", session.id);
@@ -105,13 +138,12 @@ pub async fn worker(db: Database) {
info!("No FCM token was specified!");
}
} else if sub.endpoint == "apn" {
apple_notifications::queue(
apple_notifications::ApnTask::from_notification(
session.id,
sub.auth,
&task.payload,
),
)
apple_notifications::queue(apple_notifications::ApnJob::from_notification(
session.id,
session.user_id,
sub.auth,
&task.payload,
))
.await;
} else {
// Use Web Push Standard
+22 -13
View File
@@ -482,6 +482,7 @@ impl crate::Message {
interactions: self.interactions.into(),
masquerade: self.masquerade.map(Into::into),
flags: self.flags.map(|flags| flags as u32).unwrap_or_default(),
pinned: self.pinned,
}
}
}
@@ -509,6 +510,7 @@ impl From<crate::PartialMessage> for PartialMessage {
interactions: value.interactions.map(Into::into),
masquerade: value.masquerade.map(Into::into),
flags: value.flags.map(|flags| flags as u32),
pinned: value.pinned,
}
}
}
@@ -531,6 +533,8 @@ impl From<crate::SystemMessage> for SystemMessage {
crate::SystemMessage::UserKicked { id } => Self::UserKicked { id },
crate::SystemMessage::UserLeft { id } => Self::UserLeft { id },
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 },
}
}
}
@@ -1024,6 +1028,8 @@ impl crate::User {
}
/// 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
where
P: Into<Option<&'a crate::User>>,
@@ -1059,19 +1065,7 @@ impl crate::User {
discriminator: self.discriminator,
display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()),
relations: if let Some(crate::User { id, .. }) = perspective {
if id == &self.id {
self.relations
.unwrap_or_default()
.into_iter()
.map(|relation| relation.into())
.collect()
} else {
vec![]
}
} else {
vec![]
},
relations: vec![],
badges: self.badges.unwrap_or_default() as u32,
online: can_see_profile
&& is_online
@@ -1330,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())));
impl IdempotencyKey {
pub fn unchecked_from_string(key: String) -> Self {
Self { key }
}
// Backwards compatibility.
// Issue #109
pub async fn consume_nonce(&mut self, v: Option<String>) -> Result<()> {
+13
View File
@@ -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"] }
+3
View File
@@ -0,0 +1,3 @@
// pub fn
pub fn decrypt(data: &mut [u8]) {}
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-models"
version = "0.7.13"
version = "0.7.16"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -19,8 +19,8 @@ default = ["serde", "partials", "rocket"]
[dependencies]
# Core
revolt-config = { version = "0.7.13", path = "../config" }
revolt-permissions = { version = "0.7.13", path = "../permissions" }
revolt-config = { version = "0.7.16", path = "../config" }
revolt-permissions = { version = "0.7.16", path = "../permissions" }
# Utility
regex = "1"
+5
View File
@@ -66,3 +66,8 @@ pub fn if_false(t: &bool) -> bool {
pub fn if_zero_u32(t: &u32) -> bool {
t == &0
}
/// Utility function to check if an option doesnt contain true
pub fn if_option_false(t: &Option<bool>) -> bool {
t != &Some(true)
}
+16 -10
View File
@@ -84,6 +84,12 @@ auto_derived!(
content_type: BandcampType,
id: String,
},
AppleMusic {
album_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
track_id: Option<String>,
},
/// Streamable Video
Streamable { id: String },
}
@@ -92,38 +98,38 @@ auto_derived!(
pub struct WebsiteMetadata {
/// Direct URL to web page
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
pub url: Option<String>,
/// Original direct URL
#[serde(skip_serializing_if = "Option::is_none")]
original_url: Option<String>,
pub original_url: Option<String>,
/// Remote content
#[serde(skip_serializing_if = "Option::is_none")]
special: Option<Special>,
pub special: Option<Special>,
/// Title of website
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
pub title: Option<String>,
/// Description of website
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
pub description: Option<String>,
/// Embedded image
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<Image>,
pub image: Option<Image>,
/// Embedded video
#[serde(skip_serializing_if = "Option::is_none")]
video: Option<Video>,
pub video: Option<Video>,
// #[serde(skip_serializing_if = "Option::is_none")]
// opengraph_type: Option<String>,
/// Site name
#[serde(skip_serializing_if = "Option::is_none")]
site_name: Option<String>,
pub site_name: Option<String>,
/// URL to site icon
#[serde(skip_serializing_if = "Option::is_none")]
icon_url: Option<String>,
pub icon_url: Option<String>,
/// CSS Colour
#[serde(skip_serializing_if = "Option::is_none")]
colour: Option<String>,
pub colour: Option<String>,
}
/// Text Embed
+39 -5
View File
@@ -70,6 +70,9 @@ auto_derived_partial!(
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
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
///
@@ -127,6 +130,10 @@ auto_derived!(
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
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
@@ -200,6 +207,8 @@ auto_derived!(
pub timestamp: u64,
/// URL to open when clicking notification
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.
@@ -298,7 +307,9 @@ auto_derived!(
///
/// 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)))]
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
#[cfg_attr(feature = "validator", validate(range(min = 1, max = 100)))]
@@ -355,6 +366,11 @@ auto_derived!(
/// Message will not send push / desktop notifications
SuppressNotifications = 1,
}
/// Optional fields on message
pub enum FieldsMessage {
Pinned
}
);
/// Message Author Abstraction
@@ -418,6 +434,8 @@ impl From<SystemMessage> for String {
SystemMessage::ChannelOwnershipChanged { .. } => {
"Channel ownership changed.".to_string()
}
SystemMessage::MessagePinned { .. } => "Message pinned.".to_string(),
SystemMessage::MessageUnpinned { .. } => "Message unpinned.".to_string(),
}
}
}
@@ -437,15 +455,30 @@ impl PushNotification {
format!("{}/assets/logo.png", config.hosts.app)
};
let image = msg.attachments.and_then(|attachments| {
let image = msg.attachments.as_ref().and_then(|attachments| {
attachments
.first()
.map(|v| format!("{}/attachments/{}", config.hosts.autumn, v.id))
});
let body = if let Some(sys) = msg.system {
sys.into()
} else if let Some(text) = msg.content {
let body = if let Some(ref sys) = msg.system {
sys.clone().into()
} 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
} else {
"Empty Message".to_string()
@@ -466,6 +499,7 @@ impl PushNotification {
tag: channel_id.to_string(),
timestamp,
url: format!("{}/channel/{}/{}", config.hosts.app, channel_id, msg.id),
message: msg,
}
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-permissions"
version = "0.7.13"
version = "0.7.16"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies]
# Core
revolt-result = { version = "0.7.13", path = "../result" }
revolt-result = { version = "0.7.16", path = "../result" }
# Utility
auto_ops = "0.3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-presence"
version = "0.7.13"
version = "0.7.16"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-result"
version = "0.7.13"
version = "0.7.16"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
+2
View File
@@ -90,6 +90,8 @@ pub enum ErrorType {
},
AlreadyInGroup,
NotInGroup,
AlreadyPinned,
NotPinned,
// ? Server related errors
UnknownServer,
+2
View File
@@ -40,6 +40,8 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::GroupTooLarge { .. } => Status::Forbidden,
ErrorType::AlreadyInGroup => Status::Conflict,
ErrorType::NotInGroup => Status::NotFound,
ErrorType::AlreadyPinned => Status::BadRequest,
ErrorType::NotPinned => Status::BadRequest,
ErrorType::UnknownServer => Status::NotFound,
ErrorType::InvalidRole => Status::NotFound,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.7.13"
version = "0.7.16"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
+5 -3
View File
@@ -93,7 +93,9 @@ mod test {
.client
.post(format!("/bots/{}/invite", bot.id))
.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()))
.dispatch()
.await;
@@ -102,8 +104,8 @@ mod test {
drop(response);
let event = harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
.wait_for_event(group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
_ => false,
})
.await;
@@ -69,7 +69,7 @@ mod test {
let event = harness
.wait_for_event(&format!("{}!", user.id), |event| match event {
EventV1::ChannelAck { id, .. } => id == &group.id(),
EventV1::ChannelAck { id, .. } => id == group.id(),
_ => false,
})
.await;
@@ -82,8 +82,8 @@ mod test {
drop(response);
harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelDelete { id, .. } => id == &group.id(),
.wait_for_event(group.id(), |event| match event {
EventV1::ChannelDelete { id, .. } => id == group.id(),
_ => false,
})
.await;
@@ -102,8 +102,8 @@ mod test {
.await;
let event = harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
.wait_for_event(group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
_ => false,
})
.await;
@@ -113,7 +113,7 @@ mod test {
_ => unreachable!(),
};
let message = harness.wait_for_message(&group.id()).await;
let message = harness.wait_for_message(group.id()).await;
assert_eq!(
message.system,
@@ -106,8 +106,8 @@ mod test {
.await;
let event = harness
.wait_for_event(&group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == &group.id(),
.wait_for_event(group.id(), |event| match event {
EventV1::ChannelGroupJoin { id, .. } => id == group.id(),
_ => false,
})
.await;
@@ -117,7 +117,7 @@ mod test {
_ => unreachable!(),
};
let message = harness.wait_for_message(&group.id()).await;
let message = harness.wait_for_message(group.id()).await;
assert_eq!(
message.system,
@@ -27,7 +27,7 @@ pub async fn clear_reactions(
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
// 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
message
@@ -37,6 +37,7 @@ pub async fn clear_reactions(
reactions: Some(Default::default()),
..Default::default()
},
vec![]
)
.await
.map(|_| EmptyResponse)
@@ -43,7 +43,7 @@ pub async fn edit(
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 {
return Err(create_error!(CannotEditMessage));
}
@@ -83,7 +83,7 @@ pub async fn edit(
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
if permissions.has_channel_permission(ChannelPermission::SendEmbeds) {
@@ -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)?;
// 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
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 mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
@@ -39,6 +43,7 @@ pub async fn search(
let v0::DataMessageSearch {
query,
pinned,
limit,
before,
after,
@@ -51,7 +56,8 @@ pub async fn search(
MessageQuery {
filter: MessageFilter {
channel: Some(channel.id().to_string()),
query: Some(query),
query,
pinned,
..Default::default()
},
time_period: MessageTimePeriod::Absolute {
@@ -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
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
if remove_all {
+4
View File
@@ -15,10 +15,12 @@ mod message_clear_reactions;
mod message_delete;
mod message_edit;
mod message_fetch;
mod message_pin;
mod message_query;
mod message_react;
mod message_search;
mod message_send;
mod message_unpin;
mod message_unreact;
mod permissions_set;
mod permissions_set_default;
@@ -37,10 +39,12 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
message_send::message_send,
message_query::query,
message_search::search,
message_pin::message_pin,
message_fetch::fetch,
message_edit::edit,
message_bulk_delete::bulk_delete_messages,
message_delete::delete,
message_unpin::message_unpin,
group_create::create_group,
group_add_member::add_member,
group_remove_member::remove_member,
@@ -54,7 +54,7 @@ pub async fn create_webhook(
id: webhook_id,
name: data.name,
avatar,
channel_id: channel.id(),
channel_id: channel.id().to_string(),
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
token: Some(nanoid::nanoid!(64)),
};
@@ -25,7 +25,7 @@ pub async fn fetch_webhooks(
.throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?;
Ok(Json(
db.fetch_webhooks_for_channel(&channel.id())
db.fetch_webhooks_for_channel(channel.id())
.await?
.into_iter()
.map(|v| v.into())
@@ -23,7 +23,8 @@ pub async fn join(
match &invite {
Invite::Server { server, .. } => {
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 {
channels: channels.into_iter().map(|c| c.into()).collect(),
server: server.into(),
+11 -1
View File
@@ -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_permissions::PermissionQuery;
use revolt_result::{create_error, Result};
use rocket::{serde::json::Json, State};
@@ -10,10 +14,16 @@ use rocket::{serde::json::Json, State};
#[get("/<target>/roles/<role_id>")]
pub async fn fetch(
db: &State<Database>,
user: User,
target: Reference,
role_id: String,
) -> Result<Json<v0::Role>> {
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);
if let Some(role) = role {
@@ -30,7 +30,7 @@ pub async fn create_server(
user.can_acquire_server(db).await?;
let (server, channels) = Server::create(db, data, &user, true).await?;
let channels = Member::create(db, &server, &user, Some(channels)).await?;
let (_, channels) = Member::create(db, &server, &user, Some(channels)).await?;
Ok(Json(v0::CreateServerLegacyResponse {
server: server.into(),
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "revolt-autumn"
version = "0.7.14"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
revolt-config = { version = "0.7.16", path = "../../core/config" }
tempfile = "3.12.0"
axum_typed_multipart = "0.12.1"
axum = { version = "0.7.5", features = ["multipart"] }
utoipa-scalar = { version = "0.1.0", features = ["axum"] }
utoipa = { version = "4.2.3", features = ["axum_extras", "ulid"] }
+160
View File
@@ -0,0 +1,160 @@
use axum::{
extract::{DefaultBodyLimit, Multipart, Path},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
use revolt_config::config;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use tempfile::NamedTempFile;
pub async fn router() -> Router {
let config = config().await;
Router::new().route("/", get(root)).route(
"/:tag",
post(upload_file).layer(DefaultBodyLimit::max(
config.features.limits.global.body_limit_size,
)),
)
}
/// Successful root response
#[derive(Serialize, Debug, ToSchema)]
pub struct RootResponse {
autumn: &'static str,
version: &'static str,
}
/// Capture crate version from Cargo
static CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Root response from service
#[utoipa::path(
get,
path = "/",
responses(
(status = 200, description = "Echo response", body = RootResponse)
)
)]
async fn root() -> Json<RootResponse> {
Json(RootResponse {
autumn: "Hello, I am a file server!",
version: CRATE_VERSION,
})
}
/// Available tags to upload to
#[derive(Deserialize, Debug, ToSchema)]
#[allow(non_camel_case_types)]
pub enum Tag {
attachments,
avatars,
backgrounds,
icons,
banners,
emojis,
}
/// Request body for upload
#[derive(ToSchema, TryFromMultipart)]
pub struct UploadPayload {
#[schema(format = Binary)]
#[allow(dead_code)]
#[form_data(limit = "unlimited")] // handled by axum
file: FieldData<NamedTempFile>,
}
/// Successful upload response
#[derive(Serialize, Debug, ToSchema)]
pub struct UploadResponse {
/// ID to attach uploaded file to object
id: &'static str,
}
/// Upload a file
///
/// Available tags and restrictions:
///
/// | Tag | Size | Resolution | Type |
/// | :-: | --: | :-- | :-: |
/// | attachments | 20 MB | - | Any |
/// | avatars | 4 MB | 40 MP or 10,000px | Image |
/// | backgrounds | 6 MB | 40 MP or 10,000px | Image |
/// | icons | 2.5 MB | 40 MP or 10,000px | Image |
/// | banners | 6 MB | 40 MP or 10,000px | Image |
/// | emojis | 500 KB | 40 MP or 10,000px | Image |
#[utoipa::path(
post,
path = "/{tag}",
responses(
(status = 200, description = "Upload was successful", body = UploadResponse)
),
params(
("tag" = Tag, Path, description = "Tag to upload to (e.g. attachments, icons, ...)")
),
request_body(content_type = "multipart/form-data", content = UploadPayload),
)]
async fn upload_file(
Path(tag): Path<Tag>,
TypedMultipart(UploadPayload { file }): TypedMultipart<UploadPayload>,
) -> axum::response::Result<Json<UploadResponse>> {
Ok(Json(UploadResponse { id: "aaa" }))
}
/// Fetch preview of file
///
/// Depending on the given tag, the file will be re-processed to fit the criteria:
///
/// | Tag | Image Resolution <sup>†</sup> |
/// | :-: | --- |
/// | attachments | Up to 1280px on any axis |
/// | avatars | Up to 128px on any axis |
/// | backgrounds | Up to 1280x720px |
/// | icons | Up to 128px on any axis |
/// | banners | Up to 480px on any axis |
/// | emojis | Up to 128px on any axis |
///
/// <sup>†</sup> aspect ratio will always be preserved
#[utoipa::path(
get,
path = "/{tag}/{file_id}",
responses(
(status = 200, description = "Generated preview", body = Vec<u8>)
),
params(
("tag" = Tag, Path, description = "Tag to fetch from (e.g. attachments, icons, ...)"),
("file_id" = String, Path, description = "File identifier")
),
)]
async fn fetch_preview(
Path(tag): Path<Tag>,
Path(file_id): Path<String>,
) -> axum::response::Result<Response> {
todo!()
}
/// Fetch original file
#[utoipa::path(
get,
path = "/{tag}/{file_id}/{file_name}",
responses(
(status = 200, description = "Generated preview", body = Vec<u8>)
),
params(
("tag" = Tag, Path, description = "Tag to fetch from (e.g. attachments, icons, ...)"),
("file_id" = String, Path, description = "File identifier"),
("file_name" = String, Path, description = "File name")
),
)]
async fn fetch_file(
Path(tag): Path<Tag>,
Path(file_id): Path<String>,
Path(file_name): Path<String>,
) -> axum::response::Result<Response> {
todo!()
}
+65
View File
@@ -0,0 +1,65 @@
use std::net::{Ipv4Addr, SocketAddr};
use axum::Router;
use tokio::net::TcpListener;
use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
Modify, OpenApi,
};
use utoipa_scalar::{Scalar, Servable as ScalarServable};
mod api;
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
// Configure logging and environment
revolt_config::configure!(api);
// Configure API schema
#[derive(OpenApi)]
#[openapi(
modifiers(&SecurityAddon),
paths(
api::root,
api::upload_file,
api::fetch_preview,
api::fetch_file
),
components(
schemas(
api::RootResponse,
api::Tag,
api::UploadPayload,
api::UploadResponse
)
),
tags(
// (name = "Files", description = "File uploads API")
)
)]
struct ApiDoc;
struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_scheme(
"api_key",
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
)
}
}
}
// Configure Axum and router
let app = Router::new()
.merge(Scalar::with_url("/scalar", ApiDoc::openapi()))
.nest("/", api::router().await);
// Configure TCP listener and bind
let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 3000));
let listener = TcpListener::bind(&address).await?;
axum::serve(listener, app.into_make_service()).await
}
+3 -3
View File
@@ -1,6 +1,6 @@
let
# Pinned nixpkgs, deterministic. Last updated: 11-08-2023.
pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/bb9707ef2ea4a5b749b362d5cf81ada3ded2c53f.tar.gz")) {};
# Pinned nixpkgs, deterministic. Last updated: 28-07-2024.
pkgs = import (fetchTarball("https://github.com/NixOS/nixpkgs/archive/9b34ca580417e1ebc56c4df57d8b387dad686665.tar.gz")) {};
# Rolling updates, not deterministic.
# pkgs = import (fetchTarball("channel:nixpkgs-unstable")) {};
@@ -29,7 +29,7 @@ in pkgs.mkShell {
pkgs.rustc
pkgs.clippy
pkgs.rustfmt
pkgs.pkgconfig
pkgs.pkg-config
pkgs.openssl.dev
# mdbook
+1
View File
@@ -1,6 +1,7 @@
publish:
cargo publish --package revolt-config
cargo publish --package revolt-result
cargo publish --package revolt-files
cargo publish --package revolt-permissions
cargo publish --package revolt-models
cargo publish --package revolt-presence
+8 -2
View File
@@ -22,20 +22,26 @@ tools() {
deps() {
mkdir -p \
crates/bindings/node/src \
crates/bonfire/src \
crates/delta/src \
crates/core/config/src \
crates/core/database/src \
crates/core/files/src \
crates/core/models/src \
crates/core/permissions/src \
crates/core/presence/src \
crates/core/result/src
crates/core/result/src \
crates/services/autumn/src
echo 'fn main() { panic!("stub"); }' |
tee crates/bonfire/src/main.rs |
tee crates/delta/src/main.rs
tee crates/delta/src/main.rs |
tee crates/services/autumn/src/main.rs
echo '' |
tee crates/bindings/node/src/lib.rs |
tee crates/core/config/src/lib.rs |
tee crates/core/database/src/lib.rs |
tee crates/core/files/src/lib.rs |
tee crates/core/models/src/lib.rs |
tee crates/core/permissions/src/lib.rs |
tee crates/core/presence/src/lib.rs |