forked from jmug/stoatchat
Compare commits
17
Commits
20240909-1
...
20240929-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a061bb3c6 | ||
|
|
6c3b8eaa92 | ||
|
|
5e1b2e165f | ||
|
|
e270b5df6a | ||
|
|
cbf9e81256 | ||
|
|
3080ec1f5a | ||
|
|
4fd66b2719 | ||
|
|
916f47e2f5 | ||
|
|
a3c5b1bf87 | ||
|
|
ace6c30ba5 | ||
|
|
f4104612b2 | ||
|
|
6209bc7152 | ||
|
|
1a6a8a809b | ||
|
|
5485781e7c | ||
|
|
8b21825ffe | ||
|
|
530ff4f9c7 | ||
|
|
2eb596fe99 |
No files matched your search
-107
@@ -1,107 +0,0 @@
|
|||||||
# MongoDB URI
|
|
||||||
MONGODB=mongodb://localhost
|
|
||||||
AUTUMN_MONGO_URI=mongodb://database
|
|
||||||
REDIS_URI=redis://localhost/
|
|
||||||
|
|
||||||
# URL to where the Revolt app is publicly accessible
|
|
||||||
REVOLT_APP_URL=http://local.revolt.chat:5000
|
|
||||||
|
|
||||||
# URL to where the API is publicly accessible
|
|
||||||
REVOLT_PUBLIC_URL=http://local.revolt.chat:8000
|
|
||||||
VITE_API_URL=http://local.revolt.chat:8000
|
|
||||||
|
|
||||||
# URL to where the WebSocket server is publicly accessible
|
|
||||||
REVOLT_EXTERNAL_WS_URL=ws://local.revolt.chat:9000
|
|
||||||
|
|
||||||
# URL to where Autumn is publicly available
|
|
||||||
AUTUMN_PUBLIC_URL=http://local.revolt.chat:3000
|
|
||||||
|
|
||||||
# URL to where January is publicly available
|
|
||||||
JANUARY_PUBLIC_URL=http://local.revolt.chat:7000
|
|
||||||
|
|
||||||
# URL to where Vortex is publicly available
|
|
||||||
# VOSO_PUBLIC_URL=https://voso.revolt.chat
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
## hCaptcha Settings
|
|
||||||
##
|
|
||||||
|
|
||||||
# If you are sure that you don't want to use hCaptcha, set to 1.
|
|
||||||
REVOLT_UNSAFE_NO_CAPTCHA=1
|
|
||||||
|
|
||||||
# hCaptcha API key
|
|
||||||
# REVOLT_HCAPTCHA_KEY=0x0000000000000000000000000000000000000000
|
|
||||||
|
|
||||||
# hCaptcha site key
|
|
||||||
# REVOLT_HCAPTCHA_SITEKEY=10000000-ffff-ffff-ffff-000000000001
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
## Email Settings
|
|
||||||
##
|
|
||||||
|
|
||||||
# If you are sure that you don't want to use email verification, set to 1.
|
|
||||||
REVOLT_UNSAFE_NO_EMAIL=1
|
|
||||||
|
|
||||||
# SMTP host
|
|
||||||
# REVOLT_SMTP_HOST=smtp.example.com
|
|
||||||
|
|
||||||
# SMTP username
|
|
||||||
# REVOLT_SMTP_USERNAME=noreply@example.com
|
|
||||||
|
|
||||||
# SMTP password
|
|
||||||
# REVOLT_SMTP_PASSWORD=CHANGEME
|
|
||||||
|
|
||||||
# SMTP From header
|
|
||||||
# REVOLT_SMTP_FROM=Revolt <noreply@example.com>
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
## Application Settings
|
|
||||||
##
|
|
||||||
|
|
||||||
# Whether to enable staging only features
|
|
||||||
REVOLT_IS_STAGING=1
|
|
||||||
|
|
||||||
# Whether to only allow users to sign up if they have an invite code
|
|
||||||
REVOLT_INVITE_ONLY=0
|
|
||||||
|
|
||||||
# Maximum number of people that can be in a group chat
|
|
||||||
REVOLT_MAX_GROUP_SIZE=150
|
|
||||||
|
|
||||||
# VAPID keys for push notifications
|
|
||||||
# Generate using this guide: https://gitlab.insrt.uk/revolt/delta/-/wikis/vapid
|
|
||||||
# --> Please replace these keys before going into production! <--
|
|
||||||
REVOLT_VAPID_PRIVATE_KEY=LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo=
|
|
||||||
REVOLT_VAPID_PUBLIC_KEY=BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw=
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
## Vortex configuration
|
|
||||||
##
|
|
||||||
|
|
||||||
# VOSO_MANAGE_TOKEN=CHANGEME
|
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
## Autumn configuration
|
|
||||||
##
|
|
||||||
|
|
||||||
# S3 Region
|
|
||||||
AUTUMN_S3_REGION=minio
|
|
||||||
|
|
||||||
# S3 Endpoint
|
|
||||||
AUTUMN_S3_ENDPOINT=http://minio:9000
|
|
||||||
|
|
||||||
# MinIO Root User
|
|
||||||
MINIO_ROOT_USER=minioautumn
|
|
||||||
|
|
||||||
# MinIO Root Password
|
|
||||||
MINIO_ROOT_PASSWORD=minioautumn
|
|
||||||
|
|
||||||
# AWS Access Key ID
|
|
||||||
AWS_ACCESS_KEY_ID=minioautumn
|
|
||||||
|
|
||||||
# AWS Secret Key
|
|
||||||
AWS_SECRET_ACCESS_KEY=minioautumn
|
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
project: [delta, bonfire]
|
project: [delta, bonfire, autumn, january]
|
||||||
name: Build ${{ matrix.project }} image
|
name: Build ${{ matrix.project }} image
|
||||||
steps:
|
steps:
|
||||||
# Configure build environment
|
# Configure build environment
|
||||||
@@ -98,6 +98,14 @@ jobs:
|
|||||||
"bonfire": {
|
"bonfire": {
|
||||||
"path": "crates/bonfire",
|
"path": "crates/bonfire",
|
||||||
"tag": "${{ github.repository_owner }}/bonfire"
|
"tag": "${{ github.repository_owner }}/bonfire"
|
||||||
|
},
|
||||||
|
"autumn": {
|
||||||
|
"path": "crates/services/autumn",
|
||||||
|
"tag": "${{ github.repository_owner }}/autumn"
|
||||||
|
},
|
||||||
|
"january": {
|
||||||
|
"path": "crates/services/january",
|
||||||
|
"tag": "${{ github.repository_owner }}/january"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export_to: output
|
export_to: output
|
||||||
|
|||||||
Generated
+61
-16
@@ -602,9 +602,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-s3"
|
name = "aws-sdk-s3"
|
||||||
version = "1.48.0"
|
version = "1.46.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "00a545b16c05af9302b0b4b38a7584f6f323749e407169aa3e9b210e7c0a808d"
|
checksum = "4abf69a87be33b6f125a93d5046b5f7395c26d1f449bf8d3927f5577463b6de0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.11",
|
"ahash 0.8.11",
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
@@ -637,9 +637,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-sso"
|
name = "aws-sdk-sso"
|
||||||
version = "1.41.0"
|
version = "1.39.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af0a3f676cba2c079c9563acc9233998c8951cdbe38629a0bef3c8c1b02f3658"
|
checksum = "11822090cf501c316c6f75711d77b96fba30658e3867a7762e5e2f5d32d31e81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-runtime",
|
"aws-runtime",
|
||||||
@@ -659,9 +659,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-ssooidc"
|
name = "aws-sdk-ssooidc"
|
||||||
version = "1.42.0"
|
version = "1.39.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c91b6a04495547162cf52b075e3c15a17ab6608bf9c5785d3e5a5509b3f09f5c"
|
checksum = "bc3e48ec239bb734db029ceef83599f4c9b3ce5d25c961b5bcd3f031c15bed54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-runtime",
|
"aws-runtime",
|
||||||
@@ -681,9 +681,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-sdk-sts"
|
name = "aws-sdk-sts"
|
||||||
version = "1.41.0"
|
version = "1.39.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "99c56bcd6a56cab7933980a54148b476a5a69a7694e3874d9aa2a566f150447d"
|
checksum = "a20a91795850826a6f456f4a48eff1dfa59a0e69bdbf5b8c50518fd372106574"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-credential-types",
|
"aws-credential-types",
|
||||||
"aws-runtime",
|
"aws-runtime",
|
||||||
@@ -1139,9 +1139,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitstream-io"
|
name = "bitstream-io"
|
||||||
version = "2.5.3"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452"
|
checksum = "7c12d1856e42f0d817a835fe55853957c85c8c8a470114029143d3f12671446e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "blake2b_simd"
|
name = "blake2b_simd"
|
||||||
@@ -2515,8 +2515,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
|
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3773,6 +3775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
|
checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"rayon",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5003,15 +5006,17 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ravif"
|
name = "ravif"
|
||||||
version = "0.11.10"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd"
|
checksum = "85be49d628515bb99a01c44200009f0a4167c252f036445b975b35daf952258c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"avif-serialize",
|
"avif-serialize",
|
||||||
|
"bitstream-io",
|
||||||
"imgref",
|
"imgref",
|
||||||
"loop9",
|
"loop9",
|
||||||
"quick-error 2.0.1",
|
"quick-error 2.0.1",
|
||||||
"rav1e",
|
"rav1e",
|
||||||
|
"rayon",
|
||||||
"rgb",
|
"rgb",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5305,10 +5310,12 @@ dependencies = [
|
|||||||
"kamadak-exif",
|
"kamadak-exif",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"moka",
|
"moka",
|
||||||
|
"nanoid",
|
||||||
"revolt-config",
|
"revolt-config",
|
||||||
"revolt-database",
|
"revolt-database",
|
||||||
"revolt-files",
|
"revolt-files",
|
||||||
"revolt-result",
|
"revolt-result",
|
||||||
|
"revolt_clamav-client",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -5316,8 +5323,10 @@ dependencies = [
|
|||||||
"strum_macros",
|
"strum_macros",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio 1.35.1",
|
"tokio 1.35.1",
|
||||||
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"ulid 1.1.3",
|
||||||
"utoipa",
|
"utoipa",
|
||||||
"utoipa-scalar",
|
"utoipa-scalar",
|
||||||
"webp",
|
"webp",
|
||||||
@@ -5365,6 +5374,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
|
"revolt-result",
|
||||||
"sentry",
|
"sentry",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -5378,6 +5388,7 @@ dependencies = [
|
|||||||
"async-std",
|
"async-std",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"authifier",
|
"authifier",
|
||||||
|
"axum",
|
||||||
"base64 0.21.3",
|
"base64 0.21.3",
|
||||||
"bson",
|
"bson",
|
||||||
"deadqueue",
|
"deadqueue",
|
||||||
@@ -5409,7 +5420,7 @@ dependencies = [
|
|||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ulid 1.0.0",
|
"ulid 1.1.3",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url-escape",
|
"url-escape",
|
||||||
"validator 0.16.0",
|
"validator 0.16.0",
|
||||||
@@ -5593,6 +5604,12 @@ dependencies = [
|
|||||||
"tokio 1.35.1",
|
"tokio 1.35.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "revolt_clamav-client"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d719e9fe861e2e05cbf01fd82d310fdeb2160c74fee21689a5c223eeb65cea8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "revolt_okapi"
|
name = "revolt_okapi"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
@@ -7106,6 +7123,22 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"bytes 1.5.0",
|
||||||
|
"http 1.1.0",
|
||||||
|
"http-body 1.0.0",
|
||||||
|
"http-body-util",
|
||||||
|
"pin-project-lite 0.2.13",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -7321,11 +7354,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ulid"
|
name = "ulid"
|
||||||
version = "1.0.0"
|
version = "1.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13a3aaa69b04e5b66cc27309710a569ea23593612387d67daaf102e73aa974fd"
|
checksum = "04f903f293d11f31c0c29e4148f6dc0d033a7f80cebc0282bea147611667d289"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"web-time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7506,7 +7541,7 @@ dependencies = [
|
|||||||
"quote 1.0.37",
|
"quote 1.0.37",
|
||||||
"regex",
|
"regex",
|
||||||
"syn 2.0.76",
|
"syn 2.0.76",
|
||||||
"ulid 1.0.0",
|
"ulid 1.1.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7788,6 +7823,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webp"
|
name = "webp"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
# Revolt Backend
|
<div align="center">
|
||||||
|
<h1>
|
||||||
|
Revolt Backend
|
||||||
|
|
||||||
|
[](https://github.com/revoltchat/backend/stargazers)
|
||||||
|
[](https://github.com/revoltchat/backend/network/members)
|
||||||
|
[](https://github.com/revoltchat/backend/pulls)
|
||||||
|
[](https://github.com/revoltchat/backend/issues)
|
||||||
|
[](https://github.com/revoltchat/backend/graphs/contributors)
|
||||||
|
[](https://github.com/revoltchat/backend/blob/main/LICENSE)
|
||||||
|
</h1>
|
||||||
|
The services and libraries that power the Revolt service.<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
This is a monorepo for the Revolt backend.
|
| Crate | Path | Description | |
|
||||||
|
| ------------------ | -------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |     |
|
||||||
|
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |     |
|
||||||
|
| `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines |     |
|
||||||
|
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |     |
|
||||||
|
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |     |
|
||||||
|
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |     |
|
||||||
|
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |     |
|
||||||
|
| `delta` | [crates/delta](crates/delta) | REST API server |  |
|
||||||
|
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |  |
|
||||||
|
| `services/january` | [crates/services/january](crates/services/january) | Proxy server |  |
|
||||||
|
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server |  |
|
||||||
|
| `bindings/node` | [crates/bindings/node](crates/bindings/node) | Node.js bindings for the Revolt software |  |
|
||||||
|
|
||||||
| Crate | Path | Description |
|
</div>
|
||||||
| ------------------ | -------------------------------------------------- | ----------------------------- |
|
<br/>
|
||||||
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration |
|
|
||||||
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation |
|
|
||||||
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models |
|
|
||||||
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic |
|
|
||||||
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence |
|
|
||||||
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types |
|
|
||||||
| `delta` | [crates/delta](crates/delta) | REST API server |
|
|
||||||
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server |
|
|
||||||
|
|
||||||
Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
|
|
||||||
|
|
||||||
## Minimum Supported Rust Version
|
## Minimum Supported Rust Version
|
||||||
|
|
||||||
@@ -60,13 +75,19 @@ cargo build
|
|||||||
|
|
||||||
A default configuration `Revolt.toml` is present in this project that is suited for development.
|
A default configuration `Revolt.toml` is present in this project that is suited for development.
|
||||||
|
|
||||||
If you'd like to change anything, create a `Revolt.overrides.toml` file to overwrite it.
|
If you'd like to change anything, create a `Revolt.overrides.toml` file and specify relevant variables.
|
||||||
|
|
||||||
You may need to configure the legacy environment options:
|
> [!TIP]
|
||||||
|
> Use Sentry to catch unexpected service errors:
|
||||||
```bash
|
>
|
||||||
cp .env.example .env
|
> ```toml
|
||||||
```
|
> # Revolt.overrides.toml
|
||||||
|
> [sentry]
|
||||||
|
> api = "https://abc@your.sentry/1"
|
||||||
|
> events = "https://abc@your.sentry/1"
|
||||||
|
> files = "https://abc@your.sentry/1"
|
||||||
|
> proxy = "https://abc@your.sentry/1"
|
||||||
|
> ```
|
||||||
|
|
||||||
Then continue:
|
Then continue:
|
||||||
|
|
||||||
@@ -98,10 +119,13 @@ git clone --recursive https://github.com/revoltchat/revite
|
|||||||
cd revite
|
cd revite
|
||||||
yarn
|
yarn
|
||||||
yarn build:deps
|
yarn build:deps
|
||||||
|
echo "VITE_API_URL=http://local.revolt.chat:14702" > .env.local
|
||||||
yarn dev --port 14701
|
yarn dev --port 14701
|
||||||
```
|
```
|
||||||
|
|
||||||
Then go to https://local.revolt.chat:14701
|
Then go to http://local.revolt.chat:14701 to create an account/login.
|
||||||
|
|
||||||
|
When signing up, go to http://localhost:14080 to find confirmation/password reset emails.
|
||||||
|
|
||||||
## Deployment Guide
|
## Deployment Guide
|
||||||
|
|
||||||
|
|||||||
+3
-153
@@ -4,10 +4,10 @@
|
|||||||
[database]
|
[database]
|
||||||
# MongoDB connection URL
|
# MongoDB connection URL
|
||||||
# Defaults to the container name specified in self-hosted
|
# Defaults to the container name specified in self-hosted
|
||||||
mongodb = "mongodb://localhost:14017"
|
mongodb = "mongodb://127.0.0.1:14017"
|
||||||
# Redis connection URL
|
# Redis connection URL
|
||||||
# Defaults to the container name specified in self-hosted
|
# Defaults to the container name specified in self-hosted
|
||||||
redis = "redis://localhost:14079/"
|
redis = "redis://127.0.0.1:14079/"
|
||||||
|
|
||||||
[hosts]
|
[hosts]
|
||||||
# Web locations of various services
|
# Web locations of various services
|
||||||
@@ -25,11 +25,6 @@ voso_legacy_ws = ""
|
|||||||
|
|
||||||
[api]
|
[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]
|
[api.smtp]
|
||||||
# Email server configuration for verification
|
# Email server configuration for verification
|
||||||
# Defaults to no email verification (host field is empty)
|
# Defaults to no email verification (host field is empty)
|
||||||
@@ -41,97 +36,9 @@ reply_to = "support@revolt.chat"
|
|||||||
port = 14025
|
port = 14025
|
||||||
use_tls = false
|
use_tls = false
|
||||||
|
|
||||||
[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]
|
|
||||||
# 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="
|
|
||||||
# Quality used for lossy WebP previews (set to 100 for lossless)
|
|
||||||
webp_quality = 80.0
|
|
||||||
# Mime types that cannot be uploaded or served
|
|
||||||
#
|
|
||||||
# Example for Windows executables and Android installation files:
|
|
||||||
# ["application/vnd.microsoft.portable-executable", "application/vnd.android.package-archive"]
|
|
||||||
blocked_mime_types = []
|
|
||||||
# ClamAV service
|
|
||||||
# hostname:port
|
|
||||||
clamd_host = ""
|
|
||||||
|
|
||||||
[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]
|
[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
|
# S3 protocol endpoint
|
||||||
endpoint = "http://localhost:14009"
|
endpoint = "http://127.0.0.1:14009"
|
||||||
# S3 region name
|
# S3 region name
|
||||||
region = "minio"
|
region = "minio"
|
||||||
# S3 protocol key ID
|
# S3 protocol key ID
|
||||||
@@ -140,60 +47,3 @@ access_key_id = "minioautumn"
|
|||||||
secret_access_key = "minioautumn"
|
secret_access_key = "minioautumn"
|
||||||
# Bucket to upload to by default
|
# Bucket to upload to by default
|
||||||
default_bucket = "revolt-uploads"
|
default_bucket = "revolt-uploads"
|
||||||
|
|
||||||
[features]
|
|
||||||
# Feature gate options
|
|
||||||
webhooks_enabled = false
|
|
||||||
|
|
||||||
[features.limits]
|
|
||||||
|
|
||||||
[features.limits.global]
|
|
||||||
group_size = 100
|
|
||||||
message_embeds = 5
|
|
||||||
message_replies = 5
|
|
||||||
message_reactions = 20
|
|
||||||
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
|
|
||||||
|
|
||||||
bots = 2
|
|
||||||
message_length = 2000
|
|
||||||
message_attachments = 5
|
|
||||||
servers = 100
|
|
||||||
|
|
||||||
attachment_size = 20_000_000
|
|
||||||
avatar_size = 4_000_000
|
|
||||||
background_size = 6_000_000
|
|
||||||
icon_size = 2_500_000
|
|
||||||
banner_size = 6_000_000
|
|
||||||
emoji_size = 500_000
|
|
||||||
|
|
||||||
[features.limits.default]
|
|
||||||
outgoing_friend_requests = 10
|
|
||||||
|
|
||||||
bots = 5
|
|
||||||
message_length = 2000
|
|
||||||
message_attachments = 5
|
|
||||||
servers = 100
|
|
||||||
|
|
||||||
attachment_size = 20_000_000
|
|
||||||
avatar_size = 4_000_000
|
|
||||||
background_size = 6_000_000
|
|
||||||
icon_size = 2_500_000
|
|
||||||
banner_size = 6_000_000
|
|
||||||
emoji_size = 500_000
|
|
||||||
|
|
||||||
[sentry]
|
|
||||||
# Configuration for Sentry error reporting
|
|
||||||
api = ""
|
|
||||||
events = ""
|
|
||||||
+2
-1
@@ -24,6 +24,7 @@ services:
|
|||||||
- ./.data/minio:/data
|
- ./.data/minio:/data
|
||||||
ports:
|
ports:
|
||||||
- "14009:9000"
|
- "14009:9000"
|
||||||
|
- "14010:9001"
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
# Create buckets for minio.
|
# Create buckets for minio.
|
||||||
@@ -46,7 +47,7 @@ services:
|
|||||||
maildev:
|
maildev:
|
||||||
image: soulteary/maildev
|
image: soulteary/maildev
|
||||||
ports:
|
ports:
|
||||||
- "14025:8080"
|
- "14025:25"
|
||||||
- "14080:8080"
|
- "14080:8080"
|
||||||
environment:
|
environment:
|
||||||
MAILDEV_SMTP_PORT: 25
|
MAILDEV_SMTP_PORT: 25
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ FROM ghcr.io/revoltchat/base:latest AS builder
|
|||||||
FROM gcr.io/distroless/cc-debian12:nonroot
|
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||||
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
|
||||||
|
|
||||||
EXPOSE 9000
|
EXPOSE 14703
|
||||||
USER nonroot
|
USER nonroot
|
||||||
ENV HOST=0.0.0.0:9000
|
|
||||||
CMD ["./revolt-bonfire"]
|
CMD ["./revolt-bonfire"]
|
||||||
@@ -14,6 +14,7 @@ use futures::{
|
|||||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||||
};
|
};
|
||||||
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI};
|
||||||
|
use revolt_config::report_internal_error;
|
||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
events::{client::EventV1, server::ClientMessage},
|
events::{client::EventV1, server::ClientMessage},
|
||||||
Database, User, UserHint,
|
Database, User, UserHint,
|
||||||
@@ -99,27 +100,21 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
|
|||||||
let user_id = state.cache.user_id.clone();
|
let user_id = state.cache.user_id.clone();
|
||||||
|
|
||||||
// Notify socket we have authenticated.
|
// Notify socket we have authenticated.
|
||||||
if let Err(err) = write.send(config.encode(&EventV1::Authenticated)).await {
|
if report_internal_error!(write.send(config.encode(&EventV1::Authenticated)).await).is_err() {
|
||||||
error!("Failed to write: {err:?}");
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download required data to local cache and send Ready payload.
|
// Download required data to local cache and send Ready payload.
|
||||||
let ready_payload = match state
|
let ready_payload = match report_internal_error!(
|
||||||
.generate_ready_payload(db, config.get_ready_payload_fields())
|
state
|
||||||
.await
|
.generate_ready_payload(db, config.get_ready_payload_fields())
|
||||||
{
|
.await
|
||||||
|
) {
|
||||||
Ok(ready_payload) => ready_payload,
|
Ok(ready_payload) => ready_payload,
|
||||||
Err(err) => {
|
Err(_) => return,
|
||||||
sentry::capture_error(&err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = write.send(config.encode(&ready_payload)).await {
|
if report_internal_error!(write.send(config.encode(&ready_payload)).await).is_err() {
|
||||||
error!("Failed to write: {err:?}");
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,21 +209,16 @@ async fn listener(
|
|||||||
write: &Mutex<WsWriter>,
|
write: &Mutex<WsWriter>,
|
||||||
) {
|
) {
|
||||||
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
|
||||||
let subscriber = match fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
let subscriber = match report_internal_error!(
|
||||||
{
|
fred::types::Builder::from_config(redis_config).build_subscriber_client()
|
||||||
|
) {
|
||||||
Ok(subscriber) => subscriber,
|
Ok(subscriber) => subscriber,
|
||||||
Err(err) => {
|
Err(_) => return,
|
||||||
error!("Failed to build a subscriber: {err:?}");
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = subscriber.init().await {
|
if report_internal_error!(subscriber.init().await).is_err() {
|
||||||
error!("Failed to init subscriber: {err:?}");
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
// Handle Redis connection dropping
|
// Handle Redis connection dropping
|
||||||
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
|
||||||
@@ -249,17 +239,13 @@ async fn listener(
|
|||||||
// Check for state changes for subscriptions.
|
// Check for state changes for subscriptions.
|
||||||
match state.apply_state().await {
|
match state.apply_state().await {
|
||||||
SubscriptionStateChange::Reset => {
|
SubscriptionStateChange::Reset => {
|
||||||
if let Err(err) = subscriber.unsubscribe_all().await {
|
if report_internal_error!(subscriber.unsubscribe_all().await).is_err() {
|
||||||
error!("Unsubscribe all failed: {err:?}");
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
break 'out;
|
break 'out;
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscribed = state.subscribed.read().await;
|
let subscribed = state.subscribed.read().await;
|
||||||
for id in subscribed.iter() {
|
for id in subscribed.iter() {
|
||||||
if let Err(err) = subscriber.subscribe(id).await {
|
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
|
||||||
error!("Subscribe failed: {err:?}");
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
break 'out;
|
break 'out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,9 +258,7 @@ async fn listener(
|
|||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
info!("{addr:?} unsubscribing from {id}");
|
info!("{addr:?} unsubscribing from {id}");
|
||||||
|
|
||||||
if let Err(err) = subscriber.unsubscribe(id).await {
|
if report_internal_error!(subscriber.unsubscribe(id).await).is_err() {
|
||||||
error!("Unsubscribe failed: {err:?}");
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
break 'out;
|
break 'out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,9 +267,7 @@ async fn listener(
|
|||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
info!("{addr:?} subscribing to {id}");
|
info!("{addr:?} subscribing to {id}");
|
||||||
|
|
||||||
if let Err(err) = subscriber.subscribe(id).await {
|
if report_internal_error!(subscriber.subscribe(id).await).is_err() {
|
||||||
error!("Subscribe failed: {err:?}");
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
break 'out;
|
break 'out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,13 +292,9 @@ async fn listener(
|
|||||||
_ = t2 => {},
|
_ = t2 => {},
|
||||||
message = t1 => {
|
message = t1 => {
|
||||||
// Handle incoming events.
|
// Handle incoming events.
|
||||||
let message = match message {
|
let message = match report_internal_error!(message) {
|
||||||
Ok(message) => message,
|
Ok(message) => message,
|
||||||
Err(e) => {
|
Err(_) => break 'out
|
||||||
error!("Error while consuming pub/sub messages: {e:?}");
|
|
||||||
sentry::capture_error(&e);
|
|
||||||
break 'out;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let event = match *REDIS_PAYLOAD_TYPE {
|
let event = match *REDIS_PAYLOAD_TYPE {
|
||||||
@@ -393,10 +371,7 @@ async fn listener(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(err) = subscriber.quit().await {
|
report_internal_error!(subscriber.quit().await).ok();
|
||||||
error!("{}", err);
|
|
||||||
sentry::capture_error(&err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ description = "Revolt Backend: Configuration"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
report-macros = ["revolt-result"]
|
||||||
test = ["async-std"]
|
test = ["async-std"]
|
||||||
default = ["test"]
|
default = ["test"]
|
||||||
|
|
||||||
@@ -32,3 +33,6 @@ pretty_env_logger = "0.4.0"
|
|||||||
|
|
||||||
# Sentry
|
# Sentry
|
||||||
sentry = "0.31.5"
|
sentry = "0.31.5"
|
||||||
|
|
||||||
|
# Core
|
||||||
|
revolt-result = { version = "0.7.16", path = "../result", optional = true }
|
||||||
@@ -98,8 +98,18 @@ blocked_mime_types = []
|
|||||||
# ClamAV service
|
# ClamAV service
|
||||||
# hostname:port
|
# hostname:port
|
||||||
clamd_host = ""
|
clamd_host = ""
|
||||||
|
# Mime types that should be virus scanned
|
||||||
|
#
|
||||||
|
# Leave empty to scan all file types
|
||||||
|
scan_mime_types = [
|
||||||
|
"application/vnd.microsoft.portable-executable",
|
||||||
|
"application/vnd.android.package-archive",
|
||||||
|
"application/zip",
|
||||||
|
]
|
||||||
|
|
||||||
[files.limit]
|
[files.limit]
|
||||||
|
# Minimum file size (in bytes)
|
||||||
|
min_file_size = 1
|
||||||
# Minimum image resolution
|
# Minimum image resolution
|
||||||
min_resolution = [1, 1]
|
min_resolution = [1, 1]
|
||||||
# Maximum MP of images
|
# Maximum MP of images
|
||||||
@@ -153,44 +163,70 @@ server_emoji = 100
|
|||||||
server_roles = 200
|
server_roles = 200
|
||||||
server_channels = 200
|
server_channels = 200
|
||||||
|
|
||||||
# How many days since creation a user is considered new
|
# How many hours since creation a user is considered new
|
||||||
new_user_days = 3
|
new_user_hours = 72
|
||||||
|
|
||||||
# Maximum permissible body size in bytes for uploads
|
# Maximum permissible body size in bytes for uploads
|
||||||
# (should be greater than any one file upload limit)
|
# (should be greater than any one file upload limit)
|
||||||
body_limit_size = 20_000_000
|
body_limit_size = 20_000_000
|
||||||
|
|
||||||
[features.limits.new_user]
|
[features.limits.new_user]
|
||||||
|
# Limits imposed on new users
|
||||||
|
|
||||||
|
# Number of outgoing friend requests permitted at any time
|
||||||
outgoing_friend_requests = 5
|
outgoing_friend_requests = 5
|
||||||
|
|
||||||
|
# Maximum number of owned bots
|
||||||
bots = 2
|
bots = 2
|
||||||
message_length = 2000
|
|
||||||
message_attachments = 5
|
|
||||||
servers = 100
|
|
||||||
|
|
||||||
attachment_size = 20_000_000
|
# Message content length
|
||||||
avatar_size = 4_000_000
|
message_length = 2000
|
||||||
background_size = 6_000_000
|
|
||||||
icon_size = 2_500_000
|
# Number of attachments that can be included
|
||||||
banner_size = 6_000_000
|
message_attachments = 5
|
||||||
emoji_size = 500_000
|
|
||||||
|
# Maximum number of servers the user can create/join
|
||||||
|
servers = 50
|
||||||
|
|
||||||
|
[features.limits.new_user.file_upload_size_limit]
|
||||||
|
# Maximum file size limits (in bytes)
|
||||||
|
attachments = 20_000_000
|
||||||
|
avatars = 4_000_000
|
||||||
|
backgrounds = 6_000_000
|
||||||
|
icons = 2_500_000
|
||||||
|
banners = 6_000_000
|
||||||
|
emojis = 500_000
|
||||||
|
|
||||||
[features.limits.default]
|
[features.limits.default]
|
||||||
|
# Limits imposed on users by default
|
||||||
|
|
||||||
|
# Number of outgoing friend requests permitted at any time
|
||||||
outgoing_friend_requests = 10
|
outgoing_friend_requests = 10
|
||||||
|
|
||||||
|
# Maximum number of owned bots
|
||||||
bots = 5
|
bots = 5
|
||||||
|
|
||||||
|
# Message content length
|
||||||
message_length = 2000
|
message_length = 2000
|
||||||
|
|
||||||
|
# Number of attachments that can be included
|
||||||
message_attachments = 5
|
message_attachments = 5
|
||||||
|
|
||||||
|
# Maximum number of servers the user can create/join
|
||||||
servers = 100
|
servers = 100
|
||||||
|
|
||||||
attachment_size = 20_000_000
|
[features.limits.default.file_upload_size_limit]
|
||||||
avatar_size = 4_000_000
|
# Maximum file size limits (in bytes)
|
||||||
background_size = 6_000_000
|
attachments = 20_000_000
|
||||||
icon_size = 2_500_000
|
avatars = 4_000_000
|
||||||
banner_size = 6_000_000
|
backgrounds = 6_000_000
|
||||||
emoji_size = 500_000
|
icons = 2_500_000
|
||||||
|
banners = 6_000_000
|
||||||
|
emojis = 500_000
|
||||||
|
|
||||||
[sentry]
|
[sentry]
|
||||||
# Configuration for Sentry error reporting
|
# Configuration for Sentry error reporting
|
||||||
api = ""
|
api = ""
|
||||||
events = ""
|
events = ""
|
||||||
|
files = ""
|
||||||
|
proxy = ""
|
||||||
@@ -6,11 +6,49 @@ use futures_locks::RwLock;
|
|||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
pub use sentry::capture_error;
|
pub use sentry::{capture_error, capture_message, Level};
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(feature = "report-macros")]
|
||||||
use std::env;
|
#[macro_export]
|
||||||
|
macro_rules! report_error {
|
||||||
|
( $expr: expr, $error: ident $( $tt:tt )? ) => {
|
||||||
|
$expr
|
||||||
|
.inspect_err(|err| {
|
||||||
|
$crate::capture_message(
|
||||||
|
&format!("{err:?} ({}:{}:{})", file!(), line!(), column!()),
|
||||||
|
$crate::Level::Error,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map_err(|_| ::revolt_result::create_error!($error))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "report-macros")]
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! report_internal_error {
|
||||||
|
( $expr: expr ) => {
|
||||||
|
$expr
|
||||||
|
.inspect_err(|err| {
|
||||||
|
$crate::capture_message(
|
||||||
|
&format!("{err:?} ({}:{}:{})", file!(), line!(), column!()),
|
||||||
|
$crate::Level::Error,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map_err(|_| ::revolt_result::create_error!(InternalError))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paths to search for configuration
|
||||||
|
static CONFIG_SEARCH_PATHS: [&str; 3] = [
|
||||||
|
// current working directory
|
||||||
|
"Revolt.toml",
|
||||||
|
// current working directory - overrides file
|
||||||
|
"Revolt.overrides.toml",
|
||||||
|
// root directory, for Docker containers
|
||||||
|
"/Revolt.toml",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Configuration builder
|
||||||
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
||||||
RwLock::new({
|
RwLock::new({
|
||||||
let mut builder = Config::builder().add_source(File::from_str(
|
let mut builder = Config::builder().add_source(File::from_str(
|
||||||
@@ -23,18 +61,18 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
|
|||||||
include_str!("../Revolt.test.toml"),
|
include_str!("../Revolt.test.toml"),
|
||||||
FileFormat::Toml,
|
FileFormat::Toml,
|
||||||
));
|
));
|
||||||
} else if std::path::Path::new("Revolt.toml").exists() {
|
}
|
||||||
builder = builder.add_source(File::new("Revolt.toml", FileFormat::Toml));
|
|
||||||
} else if std::path::Path::new("/Revolt.toml").exists() {
|
for path in CONFIG_SEARCH_PATHS {
|
||||||
builder = builder.add_source(File::new("/Revolt.toml", FileFormat::Toml));
|
if std::path::Path::new(path).exists() {
|
||||||
|
builder = builder.add_source(File::new(path, FileFormat::Toml));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.build().unwrap()
|
builder.build().unwrap()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
// https://gifbox.me/view/gT5mqxYKCZv-twilight-meow
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pub mongodb: String,
|
pub mongodb: String,
|
||||||
@@ -128,6 +166,7 @@ pub struct Api {
|
|||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct FilesLimit {
|
pub struct FilesLimit {
|
||||||
|
pub min_file_size: usize,
|
||||||
pub min_resolution: [usize; 2],
|
pub min_resolution: [usize; 2],
|
||||||
pub max_mega_pixels: usize,
|
pub max_mega_pixels: usize,
|
||||||
pub max_pixel_side: usize,
|
pub max_pixel_side: usize,
|
||||||
@@ -148,6 +187,7 @@ pub struct Files {
|
|||||||
pub webp_quality: f32,
|
pub webp_quality: f32,
|
||||||
pub blocked_mime_types: Vec<String>,
|
pub blocked_mime_types: Vec<String>,
|
||||||
pub clamd_host: String,
|
pub clamd_host: String,
|
||||||
|
pub scan_mime_types: Vec<String>,
|
||||||
|
|
||||||
pub limit: FilesLimit,
|
pub limit: FilesLimit,
|
||||||
pub preview: HashMap<String, [usize; 2]>,
|
pub preview: HashMap<String, [usize; 2]>,
|
||||||
@@ -164,7 +204,7 @@ pub struct GlobalLimits {
|
|||||||
pub server_roles: usize,
|
pub server_roles: usize,
|
||||||
pub server_channels: usize,
|
pub server_channels: usize,
|
||||||
|
|
||||||
pub new_user_days: usize,
|
pub new_user_hours: usize,
|
||||||
|
|
||||||
pub body_limit_size: usize,
|
pub body_limit_size: usize,
|
||||||
}
|
}
|
||||||
@@ -178,12 +218,7 @@ pub struct FeaturesLimits {
|
|||||||
pub message_attachments: usize,
|
pub message_attachments: usize,
|
||||||
pub servers: usize,
|
pub servers: usize,
|
||||||
|
|
||||||
pub attachment_size: usize,
|
pub file_upload_size_limit: HashMap<String, usize>,
|
||||||
pub avatar_size: usize,
|
|
||||||
pub background_size: usize,
|
|
||||||
pub icon_size: usize,
|
|
||||||
pub banner_size: usize,
|
|
||||||
pub emoji_size: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
@@ -207,6 +242,8 @@ pub struct Features {
|
|||||||
pub struct Sentry {
|
pub struct Sentry {
|
||||||
pub api: String,
|
pub api: String,
|
||||||
pub events: String,
|
pub events: String,
|
||||||
|
pub files: String,
|
||||||
|
pub proxy: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
@@ -222,22 +259,10 @@ pub struct Settings {
|
|||||||
impl Settings {
|
impl Settings {
|
||||||
pub fn preflight_checks(&self) {
|
pub fn preflight_checks(&self) {
|
||||||
if self.api.smtp.host.is_empty() {
|
if self.api.smtp.host.is_empty() {
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
|
|
||||||
panic!("Running in production without email is not recommended, set REVOLT_UNSAFE_NO_EMAIL=1 to override.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
log::warn!("No SMTP settings specified! Remember to configure email.");
|
log::warn!("No SMTP settings specified! Remember to configure email.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api.security.captcha.hcaptcha_key.is_empty() {
|
if self.api.security.captcha.hcaptcha_key.is_empty() {
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
|
|
||||||
panic!("Running in production without CAPTCHA is not recommended, set REVOLT_UNSAFE_NO_CAPTCHA=1 to override.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
|
log::warn!("No Captcha key specified! Remember to add hCaptcha key.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,6 +296,12 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
|
|||||||
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
|
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if std::env::var("REDIS_URL").is_err() {
|
||||||
|
// Configure redis-kiss library
|
||||||
|
let config = config().await;
|
||||||
|
std::env::set_var("REDIS_URI", config.database.redis);
|
||||||
|
}
|
||||||
|
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
log::info!("Starting {release}");
|
log::info!("Starting {release}");
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ mongodb = ["dep:mongodb", "bson"]
|
|||||||
tasks = ["isahc", "linkify", "url-escape"]
|
tasks = ["isahc", "linkify", "url-escape"]
|
||||||
async-std-runtime = ["async-std"]
|
async-std-runtime = ["async-std"]
|
||||||
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
|
||||||
|
axum-impl = ["axum"]
|
||||||
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
redis-is-patched = ["revolt-presence/redis-is-patched"]
|
||||||
|
|
||||||
# Default Features
|
# Default Features
|
||||||
@@ -23,7 +24,9 @@ default = ["mongodb", "async-std-runtime", "tasks"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Core
|
# Core
|
||||||
revolt-config = { version = "0.7.16", path = "../config" }
|
revolt-config = { version = "0.7.16", path = "../config", features = [
|
||||||
|
"report-macros",
|
||||||
|
] }
|
||||||
revolt-result = { version = "0.7.16", path = "../result" }
|
revolt-result = { version = "0.7.16", path = "../result" }
|
||||||
revolt-models = { version = "0.7.16", path = "../models", features = [
|
revolt-models = { version = "0.7.16", path = "../models", features = [
|
||||||
"validator",
|
"validator",
|
||||||
@@ -76,6 +79,9 @@ async-recursion = "1.0.4"
|
|||||||
# Async
|
# Async
|
||||||
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
|
||||||
|
|
||||||
|
# Axum Impl
|
||||||
|
axum = { version = "0.7.5", optional = true }
|
||||||
|
|
||||||
# Rocket Impl
|
# Rocket Impl
|
||||||
schemars = { version = "0.8.8", optional = true }
|
schemars = { version = "0.8.8", optional = true }
|
||||||
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
|
rocket = { version = "0.5.0-rc.2", default-features = false, features = [
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ extern crate revolt_optional_struct;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate revolt_result;
|
extern crate revolt_result;
|
||||||
|
|
||||||
|
pub use iso8601_timestamp;
|
||||||
|
|
||||||
#[cfg(feature = "mongodb")]
|
#[cfg(feature = "mongodb")]
|
||||||
pub use mongodb;
|
pub use mongodb;
|
||||||
|
|
||||||
@@ -91,4 +93,4 @@ pub fn if_false(t: &bool) -> bool {
|
|||||||
/// Utility function to check if an option doesnt contain true
|
/// Utility function to check if an option doesnt contain true
|
||||||
pub fn if_option_false(t: &Option<bool>) -> bool {
|
pub fn if_option_false(t: &Option<bool>) -> bool {
|
||||||
t != &Some(true)
|
t != &Some(true)
|
||||||
}
|
}
|
||||||
@@ -56,6 +56,10 @@ pub async fn create_database(db: &MongoDb) {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create attachments collection.");
|
.expect("Failed to create attachments collection.");
|
||||||
|
|
||||||
|
db.create_collection("attachment_hashes", None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create attachment_hashes collection.");
|
||||||
|
|
||||||
db.create_collection("user_settings", None)
|
db.create_collection("user_settings", None)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create user_settings collection.");
|
.expect("Failed to create user_settings collection.");
|
||||||
@@ -209,6 +213,46 @@ pub async fn create_database(db: &MongoDb) {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create server_members index.");
|
.expect("Failed to create server_members index.");
|
||||||
|
|
||||||
|
db.run_command(
|
||||||
|
doc! {
|
||||||
|
"createIndexes": "attachments",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"hash": 1_i32
|
||||||
|
},
|
||||||
|
"name": "hash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"used_for.id": 1_i32
|
||||||
|
},
|
||||||
|
"name": "used_for_id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create attachments index.");
|
||||||
|
|
||||||
|
db.run_command(
|
||||||
|
doc! {
|
||||||
|
"createIndexes": "attachment_hashes",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"processed_hash": 1_i32
|
||||||
|
},
|
||||||
|
"name": "processed_hash"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create attachment_hashes index.");
|
||||||
|
|
||||||
db.collection("migrations")
|
db.collection("migrations")
|
||||||
.insert_one(
|
.insert_one(
|
||||||
doc! {
|
doc! {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::{
|
|||||||
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
bson::{doc, from_bson, from_document, to_document, Bson, DateTime, Document},
|
||||||
options::FindOptions,
|
options::FindOptions,
|
||||||
},
|
},
|
||||||
Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
AbstractChannels, AbstractServers, Channel, Invite, MongoDb, DISCRIMINATOR_SEARCH_SPACE,
|
||||||
};
|
};
|
||||||
use bson::oid::ObjectId;
|
use bson::oid::ObjectId;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
@@ -20,7 +20,7 @@ struct MigrationInfo {
|
|||||||
revision: i32,
|
revision: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const LATEST_REVISION: i32 = 28;
|
pub const LATEST_REVISION: i32 = 30;
|
||||||
|
|
||||||
pub async fn migrate_database(db: &MongoDb) {
|
pub async fn migrate_database(db: &MongoDb) {
|
||||||
let migrations = db.col::<Document>("migrations");
|
let migrations = db.col::<Document>("migrations");
|
||||||
@@ -1094,6 +1094,121 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
|
|||||||
.expect("Failed to create message index.");
|
.expect("Failed to create message index.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if revision <= 28 {
|
||||||
|
info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn.");
|
||||||
|
|
||||||
|
db.db()
|
||||||
|
.create_collection("attachment_hashes", None)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
db.db()
|
||||||
|
.run_command(
|
||||||
|
doc! {
|
||||||
|
"createIndexes": "attachments",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"hash": 1_i32
|
||||||
|
},
|
||||||
|
"name": "hash"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create attachments index.");
|
||||||
|
|
||||||
|
db.db()
|
||||||
|
.run_command(
|
||||||
|
doc! {
|
||||||
|
"createIndexes": "attachment_hashes",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"processed_hash": 1_i32
|
||||||
|
},
|
||||||
|
"name": "processed_hash"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create attachment_hashes index.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if revision <= 29 {
|
||||||
|
info!("Running migration [revision 29 / 29-09-2024]: Add creator_id to webhooks.");
|
||||||
|
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
struct WebhookShell {
|
||||||
|
_id: String,
|
||||||
|
channel_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let invites = db
|
||||||
|
.db()
|
||||||
|
.collection::<WebhookShell>("channel_webhooks")
|
||||||
|
.find(doc! {}, None)
|
||||||
|
.await
|
||||||
|
.expect("webhooks")
|
||||||
|
.filter_map(|s| async { s.ok() })
|
||||||
|
.collect::<Vec<WebhookShell>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
for invite in invites {
|
||||||
|
let channel = db.fetch_channel(&invite.channel_id).await.expect("channel");
|
||||||
|
let creator_id = match channel {
|
||||||
|
Channel::Group { owner, .. } => owner,
|
||||||
|
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
|
||||||
|
let server = db.fetch_server(&server).await.expect("server");
|
||||||
|
server.owner
|
||||||
|
}
|
||||||
|
_ => unreachable!("not server or group channel!"),
|
||||||
|
};
|
||||||
|
|
||||||
|
db.db()
|
||||||
|
.collection::<Document>("channel_webhooks")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": invite._id,
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set" : {
|
||||||
|
"creator_id": creator_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("update webhook");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if revision <= 30 {
|
||||||
|
info!("Running migration [revision 30 / 29-09-2024]: Add index for used_for.id to attachments.");
|
||||||
|
|
||||||
|
db.db()
|
||||||
|
.run_command(
|
||||||
|
doc! {
|
||||||
|
"createIndexes": "attachments",
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"used_for.id": 1_i32
|
||||||
|
},
|
||||||
|
"name": "used_for_id"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create attachments index.");
|
||||||
|
}
|
||||||
|
|
||||||
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
|
||||||
|
|
||||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ auto_derived_partial!(
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub avatar: Option<File>,
|
pub avatar: Option<File>,
|
||||||
|
|
||||||
|
/// User that created this webhook
|
||||||
|
pub creator_id: String,
|
||||||
|
|
||||||
/// The channel this webhook belongs to
|
/// The channel this webhook belongs to
|
||||||
pub channel_id: String,
|
pub channel_id: String,
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ impl Default for Webhook {
|
|||||||
id: Default::default(),
|
id: Default::default(),
|
||||||
name: Default::default(),
|
name: Default::default(),
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
creator_id: Default::default(),
|
||||||
channel_id: Default::default(),
|
channel_id: Default::default(),
|
||||||
permissions: Default::default(),
|
permissions: Default::default(),
|
||||||
token: Default::default(),
|
token: Default::default(),
|
||||||
@@ -70,7 +74,7 @@ impl Webhook {
|
|||||||
if self.token.as_deref() == Some(token) {
|
if self.token.as_deref() == Some(token) {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(create_error!(InvalidCredentials))
|
Err(create_error!(NotAuthenticated))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ impl AbstractChannels for MongoDb {
|
|||||||
|
|
||||||
// Delete associated attachments
|
// Delete associated attachments
|
||||||
self.delete_many_attachments(doc! {
|
self.delete_many_attachments(doc! {
|
||||||
"object_id": &id
|
"used_for.id": &id
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use iso8601_timestamp::Timestamp;
|
use iso8601_timestamp::Timestamp;
|
||||||
|
|
||||||
|
use crate::File;
|
||||||
|
|
||||||
auto_derived_partial!(
|
auto_derived_partial!(
|
||||||
/// File hash
|
/// File hash
|
||||||
pub struct FileHash {
|
pub struct FileHash {
|
||||||
@@ -51,3 +53,40 @@ auto_derived!(
|
|||||||
Audio,
|
Audio,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
impl FileHash {
|
||||||
|
/// Create a file from a file hash
|
||||||
|
pub fn into_file(
|
||||||
|
&self,
|
||||||
|
id: String,
|
||||||
|
tag: String,
|
||||||
|
filename: String,
|
||||||
|
uploader_id: String,
|
||||||
|
) -> File {
|
||||||
|
File {
|
||||||
|
id,
|
||||||
|
tag,
|
||||||
|
filename,
|
||||||
|
hash: Some(self.id.clone()),
|
||||||
|
|
||||||
|
uploaded_at: Some(Timestamp::now_utc()),
|
||||||
|
uploader_id: Some(uploader_id),
|
||||||
|
|
||||||
|
used_for: None,
|
||||||
|
|
||||||
|
deleted: None,
|
||||||
|
reported: None,
|
||||||
|
|
||||||
|
// TODO: remove this data
|
||||||
|
metadata: self.metadata.clone(),
|
||||||
|
content_type: self.content_type.clone(),
|
||||||
|
size: self.size,
|
||||||
|
|
||||||
|
// TODO: superseded by "used_for"
|
||||||
|
message_id: None,
|
||||||
|
object_id: None,
|
||||||
|
server_id: None,
|
||||||
|
user_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,12 @@ mod reference;
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait AbstractAttachmentHashes: Sync + Send {
|
pub trait AbstractAttachmentHashes: Sync + Send {
|
||||||
|
/// Insert a new attachment hash into the database.
|
||||||
|
async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()>;
|
||||||
|
|
||||||
/// Fetch an attachment hash entry by sha256 hash.
|
/// Fetch an attachment hash entry by sha256 hash.
|
||||||
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash>;
|
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash>;
|
||||||
|
|
||||||
|
/// Update an attachment hash nonce value.
|
||||||
|
async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()>;
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,11 @@ static COL: &str = "attachment_hashes";
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AbstractAttachmentHashes for MongoDb {
|
impl AbstractAttachmentHashes for MongoDb {
|
||||||
|
/// Insert a new attachment hash into the database.
|
||||||
|
async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()> {
|
||||||
|
query!(self, insert_one, COL, &hash).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch an attachment hash entry by sha256 hash.
|
/// Fetch an attachment hash entry by sha256 hash.
|
||||||
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash> {
|
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash> {
|
||||||
query!(
|
query!(
|
||||||
@@ -16,9 +21,31 @@ impl AbstractAttachmentHashes for MongoDb {
|
|||||||
find_one,
|
find_one,
|
||||||
COL,
|
COL,
|
||||||
doc! {
|
doc! {
|
||||||
"_id": hash
|
"$or": [
|
||||||
|
{"_id": hash},
|
||||||
|
{"processed_hash": hash}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
)?
|
)?
|
||||||
.ok_or_else(|| create_error!(NotFound))
|
.ok_or_else(|| create_error!(NotFound))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update an attachment hash nonce value.
|
||||||
|
async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()> {
|
||||||
|
self.col::<FileHash>(COL)
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": hash
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"iv": nonce
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| create_database_error!("update_one", COL))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,15 +7,33 @@ use super::AbstractAttachmentHashes;
|
|||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AbstractAttachmentHashes for ReferenceDb {
|
impl AbstractAttachmentHashes for ReferenceDb {
|
||||||
|
/// Insert a new attachment hash into the database.
|
||||||
|
async fn insert_attachment_hash(&self, hash: &FileHash) -> Result<()> {
|
||||||
|
let mut hashes = self.file_hashes.lock().await;
|
||||||
|
if hashes.contains_key(&hash.id) {
|
||||||
|
Err(create_database_error!("insert", "attachment"))
|
||||||
|
} else {
|
||||||
|
hashes.insert(hash.id.to_string(), hash.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fetch an attachment hash entry by sha256 hash.
|
/// Fetch an attachment hash entry by sha256 hash.
|
||||||
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash> {
|
async fn fetch_attachment_hash(&self, hash_value: &str) -> Result<FileHash> {
|
||||||
let hashes = self.file_hashes.lock().await;
|
let hashes = self.file_hashes.lock().await;
|
||||||
if let Some(file) = hashes.get(hash) {
|
hashes
|
||||||
if file.id == hash {
|
.values()
|
||||||
Ok(file.clone())
|
.cloned()
|
||||||
} else {
|
.find(|hash| hash.id == hash_value || hash.processed_hash == hash_value)
|
||||||
Err(create_error!(NotFound))
|
.ok_or(create_error!(NotFound))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update an attachment hash nonce value.
|
||||||
|
async fn set_attachment_hash_nonce(&self, hash: &str, nonce: &str) -> Result<()> {
|
||||||
|
let mut hashes = self.file_hashes.lock().await;
|
||||||
|
if let Some(file) = hashes.get_mut(hash) {
|
||||||
|
file.iv = nonce.to_owned();
|
||||||
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(create_error!(NotFound))
|
Err(create_error!(NotFound))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ auto_derived_partial!(
|
|||||||
/// When this file was uploaded
|
/// When this file was uploaded
|
||||||
pub uploaded_at: Option<Timestamp>, // these are Option<>s to not break file uploads on legacy Autumn
|
pub uploaded_at: Option<Timestamp>, // these are Option<>s to not break file uploads on legacy Autumn
|
||||||
/// ID of user who uploaded this file
|
/// ID of user who uploaded this file
|
||||||
pub uploaded_id: Option<String>, // these are Option<>s to not break file uploads on legacy Autumn
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub uploader_id: Option<String>, // these are Option<>s to not break file uploads on legacy Autumn
|
||||||
|
|
||||||
/// What the file was used for
|
/// What the file was used for
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub used_for: Option<FileUsedFor>,
|
pub used_for: Option<FileUsedFor>,
|
||||||
|
|
||||||
/// Whether this file was deleted
|
/// Whether this file was deleted
|
||||||
@@ -59,15 +61,15 @@ auto_derived_partial!(
|
|||||||
auto_derived!(
|
auto_derived!(
|
||||||
/// Type of object file was used for
|
/// Type of object file was used for
|
||||||
pub enum FileUsedForType {
|
pub enum FileUsedForType {
|
||||||
// TODO: changing this requires a db migration
|
Message,
|
||||||
message,
|
ServerBanner,
|
||||||
serverBanner,
|
Emoji,
|
||||||
emoji,
|
UserAvatar,
|
||||||
userAvatar,
|
WebhookAvatar,
|
||||||
userProfileBackground,
|
UserProfileBackground,
|
||||||
legacyGroupIcon,
|
LegacyGroupIcon,
|
||||||
channelIcon,
|
ChannelIcon,
|
||||||
serverIcon,
|
ServerIcon,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about what the file was used for
|
/// Information about what the file was used for
|
||||||
@@ -87,44 +89,154 @@ impl File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Use a file for a message attachment
|
/// Use a file for a message attachment
|
||||||
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
|
pub async fn use_attachment(
|
||||||
db.find_and_use_attachment(id, "attachments", "message", parent)
|
db: &Database,
|
||||||
.await
|
id: &str,
|
||||||
|
parent: &str,
|
||||||
|
uploader_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
db.find_and_use_attachment(
|
||||||
|
id,
|
||||||
|
"attachments",
|
||||||
|
FileUsedFor {
|
||||||
|
id: parent.to_owned(),
|
||||||
|
object_type: FileUsedForType::Message,
|
||||||
|
},
|
||||||
|
uploader_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use a file for a user profile background
|
/// Use a file for a user profile background
|
||||||
pub async fn use_background(db: &Database, id: &str, parent: &str) -> Result<File> {
|
pub async fn use_background(
|
||||||
db.find_and_use_attachment(id, "backgrounds", "user", parent)
|
db: &Database,
|
||||||
.await
|
id: &str,
|
||||||
|
parent: &str,
|
||||||
|
uploader_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
db.find_and_use_attachment(
|
||||||
|
id,
|
||||||
|
"backgrounds",
|
||||||
|
FileUsedFor {
|
||||||
|
id: parent.to_owned(),
|
||||||
|
object_type: FileUsedForType::UserProfileBackground,
|
||||||
|
},
|
||||||
|
uploader_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use a file for a user avatar
|
/// Use a file for a user avatar
|
||||||
pub async fn use_avatar(db: &Database, id: &str, parent: &str) -> Result<File> {
|
pub async fn use_user_avatar(
|
||||||
db.find_and_use_attachment(id, "avatars", "user", parent)
|
db: &Database,
|
||||||
.await
|
id: &str,
|
||||||
|
parent: &str,
|
||||||
|
uploader_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
db.find_and_use_attachment(
|
||||||
|
id,
|
||||||
|
"avatars",
|
||||||
|
FileUsedFor {
|
||||||
|
id: parent.to_owned(),
|
||||||
|
object_type: FileUsedForType::UserAvatar,
|
||||||
|
},
|
||||||
|
uploader_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use a file for an icon
|
/// Use a file for a webhook avatar
|
||||||
pub async fn use_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
pub async fn use_webhook_avatar(
|
||||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
db: &Database,
|
||||||
.await
|
id: &str,
|
||||||
|
parent: &str,
|
||||||
|
uploader_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
db.find_and_use_attachment(
|
||||||
|
id,
|
||||||
|
"avatars",
|
||||||
|
FileUsedFor {
|
||||||
|
id: parent.to_owned(),
|
||||||
|
object_type: FileUsedForType::WebhookAvatar,
|
||||||
|
},
|
||||||
|
uploader_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use a file for a server icon
|
/// Use a file for a server icon
|
||||||
pub async fn use_server_icon(db: &Database, id: &str, parent: &str) -> Result<File> {
|
pub async fn use_server_icon(
|
||||||
db.find_and_use_attachment(id, "icons", "object", parent)
|
db: &Database,
|
||||||
.await
|
id: &str,
|
||||||
|
parent: &str,
|
||||||
|
uploader_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
db.find_and_use_attachment(
|
||||||
|
id,
|
||||||
|
"icons",
|
||||||
|
FileUsedFor {
|
||||||
|
id: parent.to_owned(),
|
||||||
|
object_type: FileUsedForType::ServerIcon,
|
||||||
|
},
|
||||||
|
uploader_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use a file for a channel icon
|
||||||
|
pub async fn use_channel_icon(
|
||||||
|
db: &Database,
|
||||||
|
id: &str,
|
||||||
|
parent: &str,
|
||||||
|
uploader_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
db.find_and_use_attachment(
|
||||||
|
id,
|
||||||
|
"icons",
|
||||||
|
FileUsedFor {
|
||||||
|
id: parent.to_owned(),
|
||||||
|
object_type: FileUsedForType::ChannelIcon,
|
||||||
|
},
|
||||||
|
uploader_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use a file for a server banner
|
/// Use a file for a server banner
|
||||||
pub async fn use_banner(db: &Database, id: &str, parent: &str) -> Result<File> {
|
pub async fn use_server_banner(
|
||||||
db.find_and_use_attachment(id, "banners", "server", parent)
|
db: &Database,
|
||||||
.await
|
id: &str,
|
||||||
|
parent: &str,
|
||||||
|
uploader_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
db.find_and_use_attachment(
|
||||||
|
id,
|
||||||
|
"banners",
|
||||||
|
FileUsedFor {
|
||||||
|
id: parent.to_owned(),
|
||||||
|
object_type: FileUsedForType::ServerBanner,
|
||||||
|
},
|
||||||
|
uploader_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use a file for an emoji
|
/// Use a file for an emoji
|
||||||
pub async fn use_emoji(db: &Database, id: &str, parent: &str) -> Result<File> {
|
pub async fn use_emoji(
|
||||||
db.find_and_use_attachment(id, "emojis", "object", parent)
|
db: &Database,
|
||||||
.await
|
id: &str,
|
||||||
|
parent: &str,
|
||||||
|
uploader_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
db.find_and_use_attachment(
|
||||||
|
id,
|
||||||
|
"emojis",
|
||||||
|
FileUsedFor {
|
||||||
|
id: parent.to_owned(),
|
||||||
|
object_type: FileUsedForType::Emoji,
|
||||||
|
},
|
||||||
|
uploader_id.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@ use revolt_result::Result;
|
|||||||
|
|
||||||
use crate::File;
|
use crate::File;
|
||||||
|
|
||||||
|
use super::FileUsedFor;
|
||||||
|
|
||||||
mod mongodb;
|
mod mongodb;
|
||||||
mod reference;
|
mod reference;
|
||||||
|
|
||||||
@@ -18,8 +20,8 @@ pub trait AbstractAttachments: Sync + Send {
|
|||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
tag: &str,
|
tag: &str,
|
||||||
parent_type: &str,
|
used_for: FileUsedFor,
|
||||||
parent_id: &str,
|
uploader_id: String,
|
||||||
) -> Result<File>;
|
) -> Result<File>;
|
||||||
|
|
||||||
/// Mark an attachment as having been reported.
|
/// Mark an attachment as having been reported.
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
use bson::to_document;
|
||||||
use bson::Document;
|
use bson::Document;
|
||||||
|
use revolt_config::report_internal_error;
|
||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::File;
|
use crate::File;
|
||||||
|
use crate::FileUsedFor;
|
||||||
use crate::MongoDb;
|
use crate::MongoDb;
|
||||||
|
|
||||||
use super::AbstractAttachments;
|
use super::AbstractAttachments;
|
||||||
@@ -34,10 +37,9 @@ impl AbstractAttachments for MongoDb {
|
|||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
tag: &str,
|
tag: &str,
|
||||||
parent_type: &str,
|
used_for: FileUsedFor,
|
||||||
parent_id: &str,
|
uploader_id: String,
|
||||||
) -> Result<File> {
|
) -> Result<File> {
|
||||||
let key = format!("{parent_type}_id");
|
|
||||||
let file = query!(
|
let file = query!(
|
||||||
self,
|
self,
|
||||||
find_one,
|
find_one,
|
||||||
@@ -45,7 +47,7 @@ impl AbstractAttachments for MongoDb {
|
|||||||
doc! {
|
doc! {
|
||||||
"_id": id,
|
"_id": id,
|
||||||
"tag": tag,
|
"tag": tag,
|
||||||
&key: {
|
"used_for": {
|
||||||
"$exists": false
|
"$exists": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +61,8 @@ impl AbstractAttachments for MongoDb {
|
|||||||
},
|
},
|
||||||
doc! {
|
doc! {
|
||||||
"$set": {
|
"$set": {
|
||||||
key: parent_id
|
"used_for": report_internal_error!(to_document(&used_for))?,
|
||||||
|
"uploader_id": uploader_id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use revolt_result::Result;
|
use revolt_result::Result;
|
||||||
|
|
||||||
use crate::File;
|
use crate::File;
|
||||||
|
use crate::FileUsedFor;
|
||||||
use crate::ReferenceDb;
|
use crate::ReferenceDb;
|
||||||
|
|
||||||
use super::AbstractAttachments;
|
use super::AbstractAttachments;
|
||||||
@@ -37,19 +38,14 @@ impl AbstractAttachments for ReferenceDb {
|
|||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
tag: &str,
|
tag: &str,
|
||||||
parent_type: &str,
|
used_for: FileUsedFor,
|
||||||
parent_id: &str,
|
uploader_id: String,
|
||||||
) -> Result<File> {
|
) -> Result<File> {
|
||||||
let mut files = self.files.lock().await;
|
let mut files = self.files.lock().await;
|
||||||
if let Some(file) = files.get_mut(id) {
|
if let Some(file) = files.get_mut(id) {
|
||||||
if file.tag == tag {
|
if file.tag == tag {
|
||||||
match parent_type {
|
file.uploader_id = Some(uploader_id);
|
||||||
"message" => file.message_id = Some(parent_id.to_owned()),
|
file.used_for = Some(used_for);
|
||||||
"user" => file.user_id = Some(parent_id.to_owned()),
|
|
||||||
"object" => file.object_id = Some(parent_id.to_owned()),
|
|
||||||
"server" => file.server_id = Some(parent_id.to_owned()),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(file.clone())
|
Ok(file.clone())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -370,10 +370,8 @@ impl Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
|
for attachment_id in data.attachments.as_deref().unwrap_or_default() {
|
||||||
attachments.push(
|
attachments
|
||||||
db.find_and_use_attachment(attachment_id, "attachments", "message", &message_id)
|
.push(File::use_attachment(db, attachment_id, &message_id, author.id()).await?);
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !attachments.is_empty() {
|
if !attachments.is_empty() {
|
||||||
@@ -499,10 +497,7 @@ impl Message {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let media = if let Some(id) = embed.media {
|
let media = if let Some(id) = embed.media {
|
||||||
Some(
|
Some(File::use_attachment(db, &id, &self.id, &self.author).await?)
|
||||||
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -661,7 +656,7 @@ impl Message {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let media: Option<v0::File> = if let Some(id) = embed.media {
|
let media: Option<v0::File> = if let Some(id) = embed.media {
|
||||||
Some(
|
Some(
|
||||||
db.find_and_use_attachment(&id, "attachments", "message", &self.id)
|
File::use_attachment(db, &id, &self.id, &self.author)
|
||||||
.await?
|
.await?
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ impl MongoDb {
|
|||||||
|
|
||||||
// Update many attachments with parent id.
|
// Update many attachments with parent id.
|
||||||
self.delete_many_attachments(doc! {
|
self.delete_many_attachments(doc! {
|
||||||
"object_id": &server_id
|
"used_for.id": &server_id
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||||
|
|
||||||
|
use revolt_result::{create_error, Error, Result};
|
||||||
|
|
||||||
|
use crate::{Database, User};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl FromRequestParts<Database> for User {
|
||||||
|
type Rejection = Error;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, db: &Database) -> Result<User> {
|
||||||
|
if let Some(Ok(bot_token)) = parts.headers.get("x-bot-token").map(|v| v.to_str()) {
|
||||||
|
let bot = db.fetch_bot_by_token(bot_token).await?;
|
||||||
|
db.fetch_user(&bot.id).await
|
||||||
|
} else if let Some(Ok(session_token)) =
|
||||||
|
parts.headers.get("x-session-token").map(|v| v.to_str())
|
||||||
|
{
|
||||||
|
let session = db.fetch_session_by_token(session_token).await?;
|
||||||
|
db.fetch_user(&session.user_id).await
|
||||||
|
} else {
|
||||||
|
Err(create_error!(NotAuthenticated))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#[cfg(feature = "axum-impl")]
|
||||||
|
mod axum;
|
||||||
mod model;
|
mod model;
|
||||||
mod ops;
|
mod ops;
|
||||||
#[cfg(feature = "rocket-impl")]
|
#[cfg(feature = "rocket-impl")]
|
||||||
@@ -5,6 +7,8 @@ mod rocket;
|
|||||||
#[cfg(feature = "rocket-impl")]
|
#[cfg(feature = "rocket-impl")]
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
|
#[cfg(feature = "axum-impl")]
|
||||||
|
pub use self::axum::*;
|
||||||
#[cfg(feature = "rocket-impl")]
|
#[cfg(feature = "rocket-impl")]
|
||||||
pub use self::rocket::*;
|
pub use self::rocket::*;
|
||||||
#[cfg(feature = "rocket-impl")]
|
#[cfg(feature = "rocket-impl")]
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ impl User {
|
|||||||
.datetime()
|
.datetime()
|
||||||
.elapsed()
|
.elapsed()
|
||||||
.expect("time went backwards")
|
.expect("time went backwards")
|
||||||
<= Duration::from_secs(86400u64 * config.features.limits.global.new_user_days as u64)
|
<= Duration::from_secs(3600u64 * config.features.limits.global.new_user_hours as u64)
|
||||||
{
|
{
|
||||||
config.features.limits.new_user
|
config.features.limits.new_user
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use crate::Database;
|
|||||||
|
|
||||||
/// Payload information, before assembly
|
/// Payload information, before assembly
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
pub struct ApnPayload {
|
pub struct ApnPayload {
|
||||||
message: Message,
|
message: Message,
|
||||||
url: String,
|
url: String,
|
||||||
@@ -29,6 +30,7 @@ pub struct ApnPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
struct Payload<'a> {
|
struct Payload<'a> {
|
||||||
aps: APS<'a>,
|
aps: APS<'a>,
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use base64::{
|
|||||||
use deadqueue::limited::Queue;
|
use deadqueue::limited::Queue;
|
||||||
use fcm_v1::auth::{Authenticator, ServiceAccountKey};
|
use fcm_v1::auth::{Authenticator, ServiceAccountKey};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use revolt_config::config;
|
use revolt_config::{config, report_internal_error};
|
||||||
use revolt_models::v0::PushNotification;
|
use revolt_models::v0::PushNotification;
|
||||||
use revolt_presence::filter_online;
|
use revolt_presence::filter_online;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -116,12 +116,11 @@ pub async fn worker(db: Database, authifier_db: AuthifierDatabase) {
|
|||||||
if fcm_error.contains("404 (Not Found)") {
|
if fcm_error.contains("404 (Not Found)") {
|
||||||
println!("Unregistering {:?}", session.id);
|
println!("Unregistering {:?}", session.id);
|
||||||
|
|
||||||
if let Err(err) = db
|
report_internal_error!(
|
||||||
.remove_push_subscription_by_session_id(&session.id)
|
db.remove_push_subscription_by_session_id(&session.id)
|
||||||
.await
|
.await
|
||||||
{
|
)
|
||||||
revolt_config::capture_error(&err);
|
.ok();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ impl From<crate::Webhook> for Webhook {
|
|||||||
id: value.id,
|
id: value.id,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
avatar: value.avatar.map(|file| file.into()),
|
avatar: value.avatar.map(|file| file.into()),
|
||||||
|
creator_id: value.creator_id,
|
||||||
channel_id: value.channel_id,
|
channel_id: value.channel_id,
|
||||||
token: value.token,
|
token: value.token,
|
||||||
permissions: value.permissions,
|
permissions: value.permissions,
|
||||||
@@ -121,6 +122,7 @@ impl From<crate::PartialWebhook> for PartialWebhook {
|
|||||||
id: value.id,
|
id: value.id,
|
||||||
name: value.name,
|
name: value.name,
|
||||||
avatar: value.avatar.map(|file| file.into()),
|
avatar: value.avatar.map(|file| file.into()),
|
||||||
|
creator_id: value.creator_id,
|
||||||
channel_id: value.channel_id,
|
channel_id: value.channel_id,
|
||||||
token: value.token,
|
token: value.token,
|
||||||
permissions: value.permissions,
|
permissions: value.permissions,
|
||||||
@@ -421,7 +423,7 @@ impl From<File> for crate::File {
|
|||||||
object_id: value.object_id,
|
object_id: value.object_id,
|
||||||
hash: None,
|
hash: None,
|
||||||
uploaded_at: None,
|
uploaded_at: None,
|
||||||
uploaded_id: None,
|
uploader_id: None,
|
||||||
used_for: None,
|
used_for: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ typenum = "1.17.0"
|
|||||||
aws-config = "1.5.5"
|
aws-config = "1.5.5"
|
||||||
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
|
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
|
||||||
|
|
||||||
revolt-config = { version = "0.7.16", path = "../config" }
|
revolt-config = { version = "0.7.16", path = "../config", features = [
|
||||||
|
"report-macros",
|
||||||
|
] }
|
||||||
revolt-result = { version = "0.7.16", path = "../result" }
|
revolt-result = { version = "0.7.16", path = "../result" }
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use aes_gcm::{aead::AeadMutInPlace, Aes256Gcm, Key, KeyInit, Nonce};
|
use aes_gcm::{
|
||||||
use revolt_config::{config, FilesS3};
|
aead::{AeadCore, AeadMutInPlace, OsRng},
|
||||||
|
Aes256Gcm, Key, KeyInit, Nonce,
|
||||||
|
};
|
||||||
|
use revolt_config::{config, report_internal_error, FilesS3};
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
|
|
||||||
use aws_sdk_s3::{
|
use aws_sdk_s3::{
|
||||||
@@ -11,7 +14,8 @@ use aws_sdk_s3::{
|
|||||||
|
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
|
|
||||||
pub static AUTHENTICATION_TAG_SIZE_BYTES: usize = 16;
|
/// Size of the authentication tag in the buffer
|
||||||
|
pub const AUTHENTICATION_TAG_SIZE_BYTES: usize = 16;
|
||||||
|
|
||||||
/// Create an S3 client
|
/// Create an S3 client
|
||||||
pub fn create_client(s3_config: FilesS3) -> Client {
|
pub fn create_client(s3_config: FilesS3) -> Client {
|
||||||
@@ -35,7 +39,7 @@ pub fn create_client(s3_config: FilesS3) -> Client {
|
|||||||
|
|
||||||
/// Create an AES-256-GCM cipher
|
/// Create an AES-256-GCM cipher
|
||||||
pub fn create_cipher(key: &str) -> Aes256Gcm {
|
pub fn create_cipher(key: &str) -> Aes256Gcm {
|
||||||
let key = &BASE64_STANDARD.decode(key).unwrap()[..];
|
let key = &BASE64_STANDARD.decode(key).expect("valid base64 string")[..];
|
||||||
let key: &Key<Aes256Gcm> = key.into();
|
let key: &Key<Aes256Gcm> = key.into();
|
||||||
Aes256Gcm::new(key)
|
Aes256Gcm::new(key)
|
||||||
}
|
}
|
||||||
@@ -46,27 +50,23 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
|
|||||||
let client = create_client(config.files.s3);
|
let client = create_client(config.files.s3);
|
||||||
|
|
||||||
// Send a request for the file
|
// Send a request for the file
|
||||||
let mut obj = client
|
let mut obj =
|
||||||
.get_object()
|
report_internal_error!(client.get_object().bucket(bucket_id).key(path).send().await)?;
|
||||||
.bucket(bucket_id)
|
|
||||||
.key(path)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| {
|
|
||||||
revolt_config::capture_error(err);
|
|
||||||
})
|
|
||||||
.map_err(|_| create_error!(InternalError))?;
|
|
||||||
|
|
||||||
// Read the file from remote
|
// Read the file from remote
|
||||||
let mut buf = vec![];
|
let mut buf = vec![];
|
||||||
while let Some(bytes) = obj.body.next().await {
|
while let Some(bytes) = obj.body.next().await {
|
||||||
let data = bytes.map_err(|_| create_error!(InternalError))?;
|
let data = report_internal_error!(bytes)?;
|
||||||
buf.write_all(&data)
|
report_internal_error!(buf.write_all(&data))?;
|
||||||
.map_err(|_| create_error!(InternalError))?;
|
|
||||||
// is there a more efficient way to do this?
|
// is there a more efficient way to do this?
|
||||||
// we just want the Vec<u8>
|
// we just want the Vec<u8>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File is not encrypted
|
||||||
|
if nonce.is_empty() {
|
||||||
|
return Ok(buf);
|
||||||
|
}
|
||||||
|
|
||||||
// Recover nonce as bytes
|
// Recover nonce as bytes
|
||||||
let nonce = &BASE64_STANDARD.decode(nonce).unwrap()[..];
|
let nonce = &BASE64_STANDARD.decode(nonce).unwrap()[..];
|
||||||
let nonce: &Nonce<typenum::consts::U12> = nonce.into();
|
let nonce: &Nonce<typenum::consts::U12> = nonce.into();
|
||||||
@@ -78,3 +78,33 @@ pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<V
|
|||||||
|
|
||||||
Ok(buf)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encrypt and upload a file to S3 (returning its nonce/IV)
|
||||||
|
pub async fn upload_to_s3(bucket_id: &str, path: &str, buf: &[u8]) -> Result<String> {
|
||||||
|
let config = config().await;
|
||||||
|
let client = create_client(config.files.s3);
|
||||||
|
|
||||||
|
// Generate a nonce
|
||||||
|
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||||
|
|
||||||
|
// Extend the buffer for in-place encryption
|
||||||
|
let mut buf = [buf, &[0; AUTHENTICATION_TAG_SIZE_BYTES]].concat();
|
||||||
|
|
||||||
|
// Encrypt the file in place
|
||||||
|
create_cipher(&config.files.encryption_key)
|
||||||
|
.encrypt_in_place(&nonce, b"", &mut buf)
|
||||||
|
.map_err(|_| create_error!(InternalError))?;
|
||||||
|
|
||||||
|
// Upload the file to remote
|
||||||
|
report_internal_error!(
|
||||||
|
client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket_id)
|
||||||
|
.key(path)
|
||||||
|
.body(buf.into())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(BASE64_STANDARD.encode(nonce))
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@ auto_derived_partial!(
|
|||||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||||
pub avatar: Option<File>,
|
pub avatar: Option<File>,
|
||||||
|
|
||||||
|
/// User that created this webhook
|
||||||
|
pub creator_id: String,
|
||||||
|
|
||||||
/// The channel this webhook belongs to
|
/// The channel this webhook belongs to
|
||||||
pub channel_id: String,
|
pub channel_id: String,
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ impl IntoResponse for Error {
|
|||||||
ErrorType::InvalidCredentials => StatusCode::UNAUTHORIZED,
|
ErrorType::InvalidCredentials => StatusCode::UNAUTHORIZED,
|
||||||
ErrorType::InvalidProperty => StatusCode::BAD_REQUEST,
|
ErrorType::InvalidProperty => StatusCode::BAD_REQUEST,
|
||||||
ErrorType::InvalidSession => StatusCode::UNAUTHORIZED,
|
ErrorType::InvalidSession => StatusCode::UNAUTHORIZED,
|
||||||
|
ErrorType::NotAuthenticated => StatusCode::UNAUTHORIZED,
|
||||||
ErrorType::DuplicateNonce => StatusCode::CONFLICT,
|
ErrorType::DuplicateNonce => StatusCode::CONFLICT,
|
||||||
ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST,
|
ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST,
|
||||||
ErrorType::NotFound => StatusCode::NOT_FOUND,
|
ErrorType::NotFound => StatusCode::NOT_FOUND,
|
||||||
@@ -74,6 +75,9 @@ impl IntoResponse for Error {
|
|||||||
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
|
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
|
||||||
|
|
||||||
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
|
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
|
||||||
|
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
ErrorType::FileTooLarge { .. } => StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
ErrorType::FileTypeNotAllowed => StatusCode::BAD_REQUEST,
|
||||||
};
|
};
|
||||||
|
|
||||||
(status, Json(&self)).into_response()
|
(status, Json(&self)).into_response()
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ pub enum ErrorType {
|
|||||||
InvalidCredentials,
|
InvalidCredentials,
|
||||||
InvalidProperty,
|
InvalidProperty,
|
||||||
InvalidSession,
|
InvalidSession,
|
||||||
|
NotAuthenticated,
|
||||||
DuplicateNonce,
|
DuplicateNonce,
|
||||||
NotFound,
|
NotFound,
|
||||||
NoEffect,
|
NoEffect,
|
||||||
@@ -156,6 +157,11 @@ pub enum ErrorType {
|
|||||||
|
|
||||||
// ? Micro-service errors
|
// ? Micro-service errors
|
||||||
ProxyError,
|
ProxyError,
|
||||||
|
FileTooSmall,
|
||||||
|
FileTooLarge {
|
||||||
|
max: usize,
|
||||||
|
},
|
||||||
|
FileTypeNotAllowed,
|
||||||
|
|
||||||
// ? Legacy errors
|
// ? Legacy errors
|
||||||
VosoUnavailable,
|
VosoUnavailable,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ impl<'r> Responder<'r, 'static> for Error {
|
|||||||
ErrorType::InvalidCredentials => Status::Unauthorized,
|
ErrorType::InvalidCredentials => Status::Unauthorized,
|
||||||
ErrorType::InvalidProperty => Status::BadRequest,
|
ErrorType::InvalidProperty => Status::BadRequest,
|
||||||
ErrorType::InvalidSession => Status::Unauthorized,
|
ErrorType::InvalidSession => Status::Unauthorized,
|
||||||
|
ErrorType::NotAuthenticated => Status::Unauthorized,
|
||||||
ErrorType::DuplicateNonce => Status::Conflict,
|
ErrorType::DuplicateNonce => Status::Conflict,
|
||||||
ErrorType::VosoUnavailable => Status::BadRequest,
|
ErrorType::VosoUnavailable => Status::BadRequest,
|
||||||
ErrorType::NotFound => Status::NotFound,
|
ErrorType::NotFound => Status::NotFound,
|
||||||
@@ -80,6 +81,9 @@ impl<'r> Responder<'r, 'static> for Error {
|
|||||||
ErrorType::FailedValidation { .. } => Status::BadRequest,
|
ErrorType::FailedValidation { .. } => Status::BadRequest,
|
||||||
|
|
||||||
ErrorType::ProxyError => Status::BadRequest,
|
ErrorType::ProxyError => Status::BadRequest,
|
||||||
|
ErrorType::FileTooSmall => Status::UnprocessableEntity,
|
||||||
|
ErrorType::FileTooLarge { .. } => Status::UnprocessableEntity,
|
||||||
|
ErrorType::FileTypeNotAllowed => Status::BadRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Serialize the error data structure into JSON.
|
// Serialize the error data structure into JSON.
|
||||||
|
|||||||
@@ -7,6 +7,5 @@ COPY --from=builder /home/rust/src/target/release/revolt-delta ./
|
|||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
ENV ROCKET_ADDRESS 0.0.0.0
|
ENV ROCKET_ADDRESS 0.0.0.0
|
||||||
ENV ROCKET_PORT 8000
|
|
||||||
USER nonroot
|
USER nonroot
|
||||||
CMD ["./revolt-delta"]
|
CMD ["./revolt-delta"]
|
||||||
@@ -124,7 +124,7 @@ pub async fn edit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(icon_id) = data.icon {
|
if let Some(icon_id) = data.icon {
|
||||||
partial.icon = Some(File::use_icon(db, &icon_id, id).await?);
|
partial.icon = Some(File::use_channel_icon(db, &icon_id, id, &user.id).await?);
|
||||||
*icon = partial.icon.clone();
|
*icon = partial.icon.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||||
Channel, Database, User, Webhook,
|
Channel, Database, File, User, Webhook,
|
||||||
};
|
};
|
||||||
use revolt_models::v0;
|
use revolt_models::v0;
|
||||||
use revolt_permissions::{
|
use revolt_permissions::{
|
||||||
@@ -43,10 +43,7 @@ pub async fn create_webhook(
|
|||||||
let webhook_id = Ulid::new().to_string();
|
let webhook_id = Ulid::new().to_string();
|
||||||
|
|
||||||
let avatar = match &data.avatar {
|
let avatar = match &data.avatar {
|
||||||
Some(id) => Some(
|
Some(id) => Some(File::use_webhook_avatar(db, id, &webhook_id, &user.id).await?),
|
||||||
db.find_and_use_attachment(id, "avatars", "user", &webhook_id)
|
|
||||||
.await?,
|
|
||||||
),
|
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,6 +51,7 @@ pub async fn create_webhook(
|
|||||||
id: webhook_id,
|
id: webhook_id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
avatar,
|
avatar,
|
||||||
|
creator_id: user.id,
|
||||||
channel_id: channel.id().to_string(),
|
channel_id: channel.id().to_string(),
|
||||||
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
|
permissions: *DEFAULT_WEBHOOK_PERMISSIONS,
|
||||||
token: Some(nanoid::nanoid!(64)),
|
token: Some(nanoid::nanoid!(64)),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pub async fn create_emoji(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Find the relevant attachment
|
// Find the relevant attachment
|
||||||
let attachment = File::use_emoji(db, &id, &id).await?;
|
let attachment = File::use_emoji(db, &id, &id, &user.id).await?;
|
||||||
|
|
||||||
// Create the emoji object
|
// Create the emoji object
|
||||||
let emoji = Emoji {
|
let emoji = Emoji {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ pub async fn edit(
|
|||||||
|
|
||||||
// 2. Apply new avatar
|
// 2. Apply new avatar
|
||||||
if let Some(avatar) = avatar {
|
if let Some(avatar) = avatar {
|
||||||
partial.avatar = Some(File::use_avatar(db, &avatar, &user.id).await?);
|
partial.avatar = Some(File::use_user_avatar(db, &avatar, &user.id, &user.id).await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
member
|
member
|
||||||
|
|||||||
@@ -138,13 +138,13 @@ pub async fn edit(
|
|||||||
|
|
||||||
// 3. Apply new icon
|
// 3. Apply new icon
|
||||||
if let Some(icon) = icon {
|
if let Some(icon) = icon {
|
||||||
partial.icon = Some(File::use_server_icon(db, &icon, &server.id).await?);
|
partial.icon = Some(File::use_server_icon(db, &icon, &server.id, &user.id).await?);
|
||||||
server.icon = partial.icon.clone();
|
server.icon = partial.icon.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Apply new banner
|
// 4. Apply new banner
|
||||||
if let Some(banner) = banner {
|
if let Some(banner) = banner {
|
||||||
partial.banner = Some(File::use_banner(db, &banner, &server.id).await?);
|
partial.banner = Some(File::use_server_banner(db, &banner, &server.id, &user.id).await?);
|
||||||
server.banner = partial.banner.clone();
|
server.banner = partial.banner.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ pub async fn edit(
|
|||||||
|
|
||||||
// 2. Apply new avatar
|
// 2. Apply new avatar
|
||||||
if let Some(avatar) = data.avatar {
|
if let Some(avatar) = data.avatar {
|
||||||
partial.avatar = Some(File::use_avatar(db, &avatar, &user.id).await?);
|
partial.avatar = Some(File::use_user_avatar(db, &avatar, &user.id, &user.id).await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Apply new status
|
// 3. Apply new status
|
||||||
@@ -114,7 +114,8 @@ pub async fn edit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(background) = profile.background {
|
if let Some(background) = profile.background {
|
||||||
new_profile.background = Some(File::use_background(db, &background, &user.id).await?);
|
new_profile.background =
|
||||||
|
Some(File::use_background(db, &background, &user.id, &user.id).await?);
|
||||||
}
|
}
|
||||||
|
|
||||||
partial.profile = Some(new_profile);
|
partial.profile = Some(new_profile);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use revolt_database::{
|
use revolt_database::{
|
||||||
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
util::{permissions::DatabasePermissionQuery, reference::Reference},
|
||||||
Database, PartialWebhook, User,
|
Database, File, PartialWebhook, User,
|
||||||
};
|
};
|
||||||
use revolt_models::v0::{DataEditWebhook, Webhook};
|
use revolt_models::v0::{DataEditWebhook, Webhook};
|
||||||
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
|
||||||
@@ -52,10 +52,7 @@ pub async fn webhook_edit(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(avatar) = avatar {
|
if let Some(avatar) = avatar {
|
||||||
let file = db
|
let file = File::use_webhook_avatar(db, &avatar, &webhook.id, &webhook.creator_id).await?;
|
||||||
.find_and_use_attachment(&avatar, "avatars", "user", &webhook.id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
partial.avatar = Some(file)
|
partial.avatar = Some(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use revolt_database::util::reference::Reference;
|
use revolt_database::util::reference::Reference;
|
||||||
use revolt_database::{Database, PartialWebhook};
|
use revolt_database::{Database, File, PartialWebhook};
|
||||||
use revolt_models::v0::{DataEditWebhook, Webhook};
|
use revolt_models::v0::{DataEditWebhook, Webhook};
|
||||||
use revolt_models::validator::Validate;
|
use revolt_models::validator::Validate;
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
@@ -34,7 +34,7 @@ pub async fn webhook_edit_token(
|
|||||||
name,
|
name,
|
||||||
avatar,
|
avatar,
|
||||||
permissions,
|
permissions,
|
||||||
remove
|
remove,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
let mut partial = PartialWebhook {
|
let mut partial = PartialWebhook {
|
||||||
@@ -44,10 +44,7 @@ pub async fn webhook_edit_token(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(avatar) = avatar {
|
if let Some(avatar) = avatar {
|
||||||
let file = db
|
let file = File::use_webhook_avatar(db, &avatar, &webhook.id, &webhook.creator_id).await?;
|
||||||
.find_and_use_attachment(&avatar, "avatars", "user", &webhook.id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
partial.avatar = Some(file)
|
partial.avatar = Some(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ version = "0.7.14"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# ID generation
|
||||||
|
ulid = "1.1.3"
|
||||||
|
nanoid = "0.4.0"
|
||||||
|
|
||||||
# Media processing
|
# Media processing
|
||||||
webp = "0.3.0"
|
webp = "0.3.0"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
jxl-oxide = "0.8.1"
|
jxl-oxide = "0.8.1"
|
||||||
kamadak-exif = "0.5.4"
|
kamadak-exif = "0.5.4"
|
||||||
# revolt_little_exif = "0.4.0"
|
# revolt_little_exif = "0.4.0"
|
||||||
|
image = { version = "0.25.2" } # avif encode requires dav1d system library: features = ["avif-native"]
|
||||||
|
|
||||||
|
# File processing
|
||||||
|
revolt_clamav-client = { version = "0.1.5" }
|
||||||
simdutf8 = { version = "0.1.4", features = ["aarch64_neon"] }
|
simdutf8 = { version = "0.1.4", features = ["aarch64_neon"] }
|
||||||
image = { version = "0.25.2" } # requires dav1d system library: features = ["avif-native"]
|
|
||||||
|
|
||||||
# Content type processing
|
# Content type processing
|
||||||
infer = "0.16.0"
|
infer = "0.16.0"
|
||||||
@@ -37,7 +44,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||||||
# Core crates
|
# Core crates
|
||||||
revolt-files = { version = "0.7.16", path = "../../core/files" }
|
revolt-files = { version = "0.7.16", path = "../../core/files" }
|
||||||
revolt-config = { version = "0.7.16", path = "../../core/config" }
|
revolt-config = { version = "0.7.16", path = "../../core/config" }
|
||||||
revolt-database = { version = "0.7.16", path = "../../core/database" }
|
revolt-database = { version = "0.7.16", path = "../../core/database", features = [
|
||||||
|
"axum-impl",
|
||||||
|
] }
|
||||||
revolt-result = { version = "0.7.16", path = "../../core/result", features = [
|
revolt-result = { version = "0.7.16", path = "../../core/result", features = [
|
||||||
"utoipa",
|
"utoipa",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -48,6 +57,7 @@ tempfile = "3.12.0"
|
|||||||
axum-macros = "0.4.1"
|
axum-macros = "0.4.1"
|
||||||
axum_typed_multipart = "0.12.1"
|
axum_typed_multipart = "0.12.1"
|
||||||
axum = { version = "0.7.5", features = ["multipart"] }
|
axum = { version = "0.7.5", features = ["multipart"] }
|
||||||
|
tower-http = { version = "0.5.2", features = ["cors"] }
|
||||||
|
|
||||||
# OpenAPI & documentation generation
|
# OpenAPI & documentation generation
|
||||||
utoipa-scalar = { version = "0.1.0", features = ["axum"] }
|
utoipa-scalar = { version = "0.1.0", features = ["axum"] }
|
||||||
|
|||||||
@@ -1 +1,12 @@
|
|||||||
# TODO: https://github.com/wader/static-ffmpeg
|
# Build Stage
|
||||||
|
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||||
|
|
||||||
|
# Bundle Stage
|
||||||
|
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||||
|
COPY --from=builder /home/rust/src/target/release/revolt-autumn ./
|
||||||
|
COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/local/bin/
|
||||||
|
COPY --from=mwader/static-ffmpeg:7.0.2 /ffprobe /usr/local/bin/
|
||||||
|
|
||||||
|
EXPOSE 14704
|
||||||
|
USER nonroot
|
||||||
|
CMD ["./revolt-autumn"]
|
||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{DefaultBodyLimit, Path, State},
|
extract::{DefaultBodyLimit, Path, State},
|
||||||
http::header,
|
http::{header, Method},
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
@@ -13,14 +13,15 @@ use axum::{
|
|||||||
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
|
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
|
||||||
use image::ImageReader;
|
use image::ImageReader;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use revolt_config::config;
|
use revolt_config::{config, report_internal_error};
|
||||||
use revolt_database::{Database, FileHash, Metadata};
|
use revolt_database::{iso8601_timestamp::Timestamp, Database, FileHash, Metadata, User};
|
||||||
use revolt_files::{fetch_from_s3, AUTHENTICATION_TAG_SIZE_BYTES};
|
use revolt_files::{fetch_from_s3, upload_to_s3, AUTHENTICATION_TAG_SIZE_BYTES};
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
|
use tower_http::cors::{AllowHeaders, Any, CorsLayer};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use crate::{exif::strip_metadata, metadata::generate_metadata, mime_type::determine_mime_type};
|
use crate::{exif::strip_metadata, metadata::generate_metadata, mime_type::determine_mime_type};
|
||||||
@@ -29,16 +30,24 @@ use crate::{exif::strip_metadata, metadata::generate_metadata, mime_type::determ
|
|||||||
pub async fn router() -> Router<Database> {
|
pub async fn router() -> Router<Database> {
|
||||||
let config = config().await;
|
let config = config().await;
|
||||||
|
|
||||||
|
let cors = CorsLayer::new()
|
||||||
|
.allow_methods([Method::POST])
|
||||||
|
.allow_headers(AllowHeaders::mirror_request())
|
||||||
|
.allow_origin(Any);
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
.route(
|
.route(
|
||||||
"/:tag",
|
"/:tag",
|
||||||
post(upload_file).layer(DefaultBodyLimit::max(
|
post(upload_file)
|
||||||
config.features.limits.global.body_limit_size,
|
.options(options)
|
||||||
)),
|
.layer(DefaultBodyLimit::max(
|
||||||
|
config.features.limits.global.body_limit_size,
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
.route("/:tag/:file_id", get(fetch_preview))
|
.route("/:tag/:file_id", get(fetch_preview))
|
||||||
.route("/:tag/:file_id/:file_name", get(fetch_file))
|
.route("/:tag/:file_id/:file_name", get(fetch_file))
|
||||||
|
.layer(cors)
|
||||||
}
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -86,8 +95,11 @@ async fn root() -> Json<RootResponse> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Empty handler for OPTIONS routes
|
||||||
|
async fn options() {}
|
||||||
|
|
||||||
/// Available tags to upload to
|
/// Available tags to upload to
|
||||||
#[derive(Deserialize, Debug, ToSchema, strum_macros::IntoStaticStr)]
|
#[derive(Clone, Deserialize, Debug, ToSchema, strum_macros::IntoStaticStr)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum Tag {
|
pub enum Tag {
|
||||||
attachments,
|
attachments,
|
||||||
@@ -111,7 +123,7 @@ pub struct UploadPayload {
|
|||||||
#[derive(Serialize, Debug, ToSchema)]
|
#[derive(Serialize, Debug, ToSchema)]
|
||||||
pub struct UploadResponse {
|
pub struct UploadResponse {
|
||||||
/// ID to attach uploaded file to object
|
/// ID to attach uploaded file to object
|
||||||
id: &'static str,
|
id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload a file
|
/// Upload a file
|
||||||
@@ -136,29 +148,48 @@ pub struct UploadResponse {
|
|||||||
("tag" = Tag, Path, description = "Tag to upload to (e.g. attachments, icons, ...)")
|
("tag" = Tag, Path, description = "Tag to upload to (e.g. attachments, icons, ...)")
|
||||||
),
|
),
|
||||||
request_body(content_type = "multipart/form-data", content = UploadPayload),
|
request_body(content_type = "multipart/form-data", content = UploadPayload),
|
||||||
|
security(
|
||||||
|
("session_token" = []),
|
||||||
|
("bot_token" = [])
|
||||||
|
)
|
||||||
)]
|
)]
|
||||||
async fn upload_file(
|
async fn upload_file(
|
||||||
|
State(db): State<Database>,
|
||||||
|
user: User,
|
||||||
Path(tag): Path<Tag>,
|
Path(tag): Path<Tag>,
|
||||||
TypedMultipart(UploadPayload { mut file }): TypedMultipart<UploadPayload>,
|
TypedMultipart(UploadPayload { mut file }): TypedMultipart<UploadPayload>,
|
||||||
) -> axum::response::Result<Json<UploadResponse>> {
|
) -> Result<Json<UploadResponse>> {
|
||||||
|
// Fetch configuration
|
||||||
|
let config = config().await;
|
||||||
|
|
||||||
// Keep track of processing time
|
// Keep track of processing time
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
// TODO: authenticate the user
|
|
||||||
|
|
||||||
// Extract the filename, or give it a generic name
|
// Extract the filename, or give it a generic name
|
||||||
let file_name = file.metadata.file_name.unwrap_or("unnamed-file".to_owned());
|
let filename = file.metadata.file_name.unwrap_or("unnamed-file".to_owned());
|
||||||
|
|
||||||
// Load file to memory
|
// Load file to memory
|
||||||
let mut buf = Vec::<u8>::new();
|
let mut buf = Vec::<u8>::new();
|
||||||
file.contents
|
report_internal_error!(file.contents.read_to_end(&mut buf))?;
|
||||||
.read_to_end(&mut buf)
|
|
||||||
.map_err(|_| create_error!(InternalError))?;
|
|
||||||
|
|
||||||
// Take note of original file size
|
// Take note of original file size
|
||||||
let original_file_size = buf.len();
|
let original_file_size = buf.len();
|
||||||
|
|
||||||
// TODO: check against tag
|
// Ensure the file is not empty
|
||||||
|
if original_file_size < config.files.limit.min_file_size {
|
||||||
|
return Err(create_error!(FileTooSmall));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's file upload limits
|
||||||
|
let limits = user.limits().await;
|
||||||
|
let size_limit = *limits
|
||||||
|
.file_upload_size_limit
|
||||||
|
.get(tag.clone().into())
|
||||||
|
.expect("size limit");
|
||||||
|
|
||||||
|
if original_file_size > size_limit {
|
||||||
|
return Err(create_error!(FileTooLarge { max: size_limit }));
|
||||||
|
}
|
||||||
|
|
||||||
// Generate sha256 hash
|
// Generate sha256 hash
|
||||||
let original_hash = {
|
let original_hash = {
|
||||||
@@ -167,13 +198,37 @@ async fn upload_file(
|
|||||||
hasher.finalize()
|
hasher.finalize()
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: find existing `hash` (or match `processed`) and use that if possible
|
// Generate an ID for this file
|
||||||
// then: create attachment and return UploadResponse { id }
|
let id = if matches!(tag, Tag::emojis) {
|
||||||
|
ulid::Ulid::new().to_string()
|
||||||
|
} else {
|
||||||
|
nanoid::nanoid!(42)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find an existing hash and use that if possible
|
||||||
|
if let Ok(file_hash) = db
|
||||||
|
.fetch_attachment_hash(&format!("{original_hash:02x}"))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let tag: &'static str = tag.into();
|
||||||
|
db.insert_attachment(&file_hash.into_file(id.clone(), tag.to_owned(), filename, user.id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
return Ok(Json(UploadResponse { id }));
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the mime type for the file
|
// Determine the mime type for the file
|
||||||
let mime_type = determine_mime_type(&mut file.contents, &buf, &file_name);
|
let mime_type = determine_mime_type(&mut file.contents, &buf, &filename);
|
||||||
|
|
||||||
// TODO: block mime types here
|
// Check blocklist for mime type
|
||||||
|
if config
|
||||||
|
.files
|
||||||
|
.blocked_mime_types
|
||||||
|
.iter()
|
||||||
|
.any(|m| m == mime_type)
|
||||||
|
{
|
||||||
|
return Err(create_error!(FileTypeNotAllowed));
|
||||||
|
}
|
||||||
|
|
||||||
// Determine metadata for the file
|
// Determine metadata for the file
|
||||||
let metadata = generate_metadata(&file.contents, mime_type);
|
let metadata = generate_metadata(&file.contents, mime_type);
|
||||||
@@ -181,7 +236,14 @@ async fn upload_file(
|
|||||||
// Strip metadata
|
// Strip metadata
|
||||||
let (buf, metadata) = strip_metadata(file.contents, buf, metadata, mime_type).await?;
|
let (buf, metadata) = strip_metadata(file.contents, buf, metadata, mime_type).await?;
|
||||||
|
|
||||||
// TODO: Virus scan Metadata::File
|
// Virus scan files if ClamAV is configured
|
||||||
|
if matches!(metadata, Metadata::File)
|
||||||
|
&& (config.files.scan_mime_types.is_empty()
|
||||||
|
|| config.files.scan_mime_types.iter().any(|v| v == mime_type))
|
||||||
|
&& crate::clamav::is_malware(&buf).await?
|
||||||
|
{
|
||||||
|
return Err(create_error!(InternalError));
|
||||||
|
}
|
||||||
|
|
||||||
// Print file information for debug purposes
|
// Print file information for debug purposes
|
||||||
let new_file_size = buf.len() + AUTHENTICATION_TAG_SIZE_BYTES;
|
let new_file_size = buf.len() + AUTHENTICATION_TAG_SIZE_BYTES;
|
||||||
@@ -193,15 +255,41 @@ async fn upload_file(
|
|||||||
let process_ratio = new_file_size as f32 / original_file_size as f32;
|
let process_ratio = new_file_size as f32 / original_file_size as f32;
|
||||||
let time_to_process = Instant::now() - now;
|
let time_to_process = Instant::now() - now;
|
||||||
|
|
||||||
tracing::info!("Received file {file_name}\nOriginal hash: {original_hash:02X}\nOriginal size: {original_file_size} bytes\nMime type: {mime_type}\nMetadata: {metadata:?}\nProcessed file size: {new_file_size} bytes ({:.2}%).\nProcessed hash: {processed_hash:02X}\nProcessing took {time_to_process:?}", process_ratio * 100.0);
|
tracing::info!("Received file {filename}\nOriginal hash: {original_hash:02x}\nOriginal size: {original_file_size} bytes\nMime type: {mime_type}\nMetadata: {metadata:?}\nProcessed file size: {new_file_size} bytes ({:.2}%).\nProcessed hash: {processed_hash:02x}\nProcessing took {time_to_process:?}", process_ratio * 100.0);
|
||||||
|
|
||||||
// TODO: encrypt the file and generate FileHash
|
// Create hash entry in database
|
||||||
// note: file_size is processed file's size (incl. authTag in encrypted buffer)
|
let file_hash = FileHash {
|
||||||
// TODO: insert FileHash
|
id: format!("{original_hash:02x}"),
|
||||||
// TODO: upload to S3
|
processed_hash: format!("{processed_hash:02x}"),
|
||||||
// TODO: create attachment
|
|
||||||
|
|
||||||
Ok(Json(UploadResponse { id: "aaa" }))
|
created_at: Timestamp::now_utc(),
|
||||||
|
|
||||||
|
bucket_id: config.files.s3.default_bucket,
|
||||||
|
path: format!("{original_hash:02x}"),
|
||||||
|
iv: String::new(), // indicates file is not uploaded yet
|
||||||
|
|
||||||
|
metadata,
|
||||||
|
content_type: mime_type.to_owned(),
|
||||||
|
size: new_file_size as isize,
|
||||||
|
};
|
||||||
|
|
||||||
|
db.insert_attachment_hash(&file_hash).await?;
|
||||||
|
|
||||||
|
// Upload the file to S3 and commit nonce to database
|
||||||
|
let upload_start = Instant::now();
|
||||||
|
let nonce = upload_to_s3(&file_hash.bucket_id, &file_hash.id, &buf).await?;
|
||||||
|
db.set_attachment_hash_nonce(&file_hash.id, &nonce).await?;
|
||||||
|
|
||||||
|
// Debug information
|
||||||
|
let time_to_upload = Instant::now() - upload_start;
|
||||||
|
tracing::info!("Took {time_to_upload:?} to upload {new_file_size} bytes to S3.");
|
||||||
|
|
||||||
|
// Finally, create the file and return its ID
|
||||||
|
let tag: &'static str = tag.into();
|
||||||
|
db.insert_attachment(&file_hash.into_file(id.clone(), tag.to_owned(), filename, user.id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(UploadResponse { id }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Header value used for cache control
|
/// Header value used for cache control
|
||||||
@@ -270,16 +358,16 @@ async fn fetch_preview(
|
|||||||
|
|
||||||
// Read the image and resize it
|
// Read the image and resize it
|
||||||
// TODO: use jxl_oxide to process image/jxl files
|
// TODO: use jxl_oxide to process image/jxl files
|
||||||
let image = ImageReader::new(Cursor::new(data))
|
let image = report_internal_error!(report_internal_error!(ImageReader::new(Cursor::new(
|
||||||
.with_guessed_format()
|
data
|
||||||
.map_err(|_| create_error!(InternalError))?
|
))
|
||||||
.decode()
|
.with_guessed_format())?
|
||||||
.map_err(|_| create_error!(InternalError))?
|
.decode())?
|
||||||
//.resize(width as u32, height as u32, image::imageops::FilterType::Gaussian)
|
//.resize(width as u32, height as u32, image::imageops::FilterType::Gaussian)
|
||||||
// resize is about 2.5x slower,
|
// resize is about 2.5x slower,
|
||||||
// thumbnail doesn't have terrible quality
|
// thumbnail doesn't have terrible quality
|
||||||
// so we use thumbnail
|
// so we use thumbnail
|
||||||
.thumbnail(*w as u32, *h as u32);
|
.thumbnail(*w as u32, *h as u32);
|
||||||
|
|
||||||
// Encode it into WEBP
|
// Encode it into WEBP
|
||||||
let encoder = webp::Encoder::from_image(&image).expect("Could not create encoder.");
|
let encoder = webp::Encoder::from_image(&image).expect("Could not create encoder.");
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use revolt_config::{config, report_internal_error};
|
||||||
|
use revolt_result::Result;
|
||||||
|
|
||||||
|
/// Initialise ClamAV
|
||||||
|
pub async fn init() {
|
||||||
|
let config = config().await;
|
||||||
|
|
||||||
|
if !config.files.clamd_host.is_empty() {
|
||||||
|
tracing::info!("Waiting for clamd to be ready...");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let clamd_available =
|
||||||
|
match revolt_clamav_client::ping_tcp(config.files.clamd_host.clone()) {
|
||||||
|
Ok(ping_response) => ping_response == b"PONG\0",
|
||||||
|
Err(_) => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if clamd_available {
|
||||||
|
tracing::info!("clamd is ready, virus protection enabled!");
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
tracing::error!(
|
||||||
|
"Could not ping clamd host at {}, retrying in 10 seconds...",
|
||||||
|
config.files.clamd_host
|
||||||
|
);
|
||||||
|
|
||||||
|
std::thread::sleep(Duration::from_secs(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan for malware
|
||||||
|
pub async fn is_malware(buf: &[u8]) -> Result<bool> {
|
||||||
|
let config = config().await;
|
||||||
|
if config.files.clamd_host.is_empty() {
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
let scan_response = report_internal_error!(revolt_clamav_client::scan_buffer_tcp(
|
||||||
|
buf,
|
||||||
|
config.files.clamd_host,
|
||||||
|
None
|
||||||
|
))?;
|
||||||
|
|
||||||
|
report_internal_error!(revolt_clamav_client::clean(&scan_response)).map(|v| !v)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ use std::io::{Cursor, Read};
|
|||||||
|
|
||||||
use exif::Reader;
|
use exif::Reader;
|
||||||
use image::{ImageFormat, ImageReader};
|
use image::{ImageFormat, ImageReader};
|
||||||
|
use revolt_config::report_internal_error;
|
||||||
use revolt_database::Metadata;
|
use revolt_database::Metadata;
|
||||||
use revolt_result::{create_error, Result};
|
use revolt_result::{create_error, Result};
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
@@ -39,11 +40,11 @@ pub async fn strip_metadata(
|
|||||||
let mut cursor = Cursor::new(buf);
|
let mut cursor = Cursor::new(buf);
|
||||||
|
|
||||||
// Decode the image
|
// Decode the image
|
||||||
let image = ImageReader::new(&mut cursor)
|
let image = report_internal_error!(report_internal_error!(ImageReader::new(
|
||||||
.with_guessed_format()
|
&mut cursor
|
||||||
.map_err(|_| create_error!(InternalError))?
|
)
|
||||||
.decode()
|
.with_guessed_format())?
|
||||||
.map_err(|_| create_error!(InternalError));
|
.decode());
|
||||||
|
|
||||||
// Reset read position
|
// Reset read position
|
||||||
cursor.set_position(0);
|
cursor.set_position(0);
|
||||||
@@ -64,7 +65,7 @@ pub async fn strip_metadata(
|
|||||||
|
|
||||||
// Apply the EXIF rotation
|
// Apply the EXIF rotation
|
||||||
// See https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
|
// See https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
|
||||||
match &rotation {
|
report_internal_error!(match &rotation {
|
||||||
2 => image?.fliph(),
|
2 => image?.fliph(),
|
||||||
3 => image?.rotate180(),
|
3 => image?.rotate180(),
|
||||||
4 => image?.rotate180().fliph(),
|
4 => image?.rotate180().fliph(),
|
||||||
@@ -83,8 +84,7 @@ pub async fn strip_metadata(
|
|||||||
"image/tiff" => ImageFormat::Tiff,
|
"image/tiff" => ImageFormat::Tiff,
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
},
|
},
|
||||||
)
|
))?;
|
||||||
.map_err(|_| create_error!(InternalError))?;
|
|
||||||
|
|
||||||
// Calculate dimensions after rotation.
|
// Calculate dimensions after rotation.
|
||||||
let (width, height) = match &rotation {
|
let (width, height) = match &rotation {
|
||||||
@@ -112,47 +112,44 @@ pub async fn strip_metadata(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Temporary output file
|
// Temporary output file
|
||||||
let mut out_file =
|
let mut out_file = report_internal_error!(NamedTempFile::new())?;
|
||||||
NamedTempFile::new().map_err(|_| create_error!(InternalError))?;
|
|
||||||
|
|
||||||
// Process the file with ffmpeg
|
// Process the file with ffmpeg
|
||||||
Command::new("ffmpeg")
|
report_internal_error!(
|
||||||
.args([
|
Command::new("ffmpeg")
|
||||||
// Overwrite the temporary file
|
.args([
|
||||||
"-y",
|
// Overwrite the temporary file
|
||||||
// Read original uploaded file
|
"-y",
|
||||||
"-i",
|
// Read original uploaded file
|
||||||
file.path().to_str().ok_or(create_error!(InternalError))?,
|
"-i",
|
||||||
// Strip any metadata
|
file.path().to_str().ok_or(create_error!(InternalError))?,
|
||||||
"-map_metadata",
|
// Strip any metadata
|
||||||
"-1",
|
"-map_metadata",
|
||||||
// Copy video / audio data to new file
|
"-1",
|
||||||
"-c:v",
|
// Copy video / audio data to new file
|
||||||
"copy",
|
"-c:v",
|
||||||
"-c:a",
|
"copy",
|
||||||
"copy",
|
"-c:a",
|
||||||
// Select correct file format
|
"copy",
|
||||||
"-f",
|
// Select correct file format
|
||||||
ext,
|
"-f",
|
||||||
// Save to new temporary file
|
ext,
|
||||||
out_file
|
// Save to new temporary file
|
||||||
.path()
|
out_file
|
||||||
.to_str()
|
.path()
|
||||||
.ok_or(create_error!(InternalError))?,
|
.to_str()
|
||||||
])
|
.ok_or(create_error!(InternalError))?,
|
||||||
.output()
|
])
|
||||||
.await
|
.output()
|
||||||
.inspect_err(|err| tracing::error!("{err:?}"))
|
.await
|
||||||
.map_err(|_| create_error!(InternalError))?;
|
)?;
|
||||||
|
|
||||||
// Probe the file again
|
// Probe the file again
|
||||||
let metadata = crate::metadata::generate_metadata(&out_file, mime);
|
let metadata = crate::metadata::generate_metadata(&out_file, mime);
|
||||||
|
|
||||||
// Read the file from disk
|
// Read the file from disk
|
||||||
let mut buf = Vec::<u8>::new();
|
let mut buf = Vec::<u8>::new();
|
||||||
out_file
|
report_internal_error!(out_file.read_to_end(&mut buf))?;
|
||||||
.read_to_end(&mut buf)
|
|
||||||
.map_err(|_| create_error!(InternalError))?;
|
|
||||||
|
|
||||||
Ok((buf, metadata))
|
Ok((buf, metadata))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use utoipa::{
|
|||||||
use utoipa_scalar::{Scalar, Servable as ScalarServable};
|
use utoipa_scalar::{Scalar, Servable as ScalarServable};
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
|
pub mod clamav;
|
||||||
pub mod exif;
|
pub mod exif;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
pub mod mime_type;
|
pub mod mime_type;
|
||||||
@@ -18,7 +19,10 @@ pub mod mime_type;
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), std::io::Error> {
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
// Configure logging and environment
|
// Configure logging and environment
|
||||||
revolt_config::configure!(api);
|
revolt_config::configure!(files);
|
||||||
|
|
||||||
|
// Wait for ClamAV
|
||||||
|
clamav::init().await;
|
||||||
|
|
||||||
// Configure API schema
|
// Configure API schema
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
@@ -52,9 +56,13 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||||
if let Some(components) = openapi.components.as_mut() {
|
if let Some(components) = openapi.components.as_mut() {
|
||||||
components.add_security_scheme(
|
components.add_security_scheme(
|
||||||
"api_key",
|
"bot_token",
|
||||||
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
|
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Bot-Token"))),
|
||||||
)
|
);
|
||||||
|
components.add_security_scheme(
|
||||||
|
"session_token",
|
||||||
|
SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("X-Session-Token"))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::{io::Cursor, os::unix::fs::MetadataExt};
|
use std::io::Cursor;
|
||||||
|
|
||||||
use revolt_database::Metadata;
|
use revolt_database::Metadata;
|
||||||
use revolt_result::{create_error, Result};
|
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
/// Intersection of what infer can detect and what image-rs supports
|
/// Intersection of what infer can detect and what image-rs supports
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
/// Determine the mime type of the given temporary file and filename
|
/// Determine the mime type of the given temporary file and filename
|
||||||
pub fn determine_mime_type(f: &mut NamedTempFile, buf: &Vec<u8>, file_name: &str) -> &'static str {
|
pub fn determine_mime_type(f: &mut NamedTempFile, buf: &[u8], file_name: &str) -> &'static str {
|
||||||
|
// Force certain extensions into particular mime types
|
||||||
|
if file_name.to_lowercase().ends_with(".apk") {
|
||||||
|
return "application/vnd.android.package-archive";
|
||||||
|
} else if file_name.to_lowercase().ends_with(".exe") {
|
||||||
|
return "application/vnd.microsoft.portable-executable";
|
||||||
|
}
|
||||||
|
|
||||||
// Use magic signatures to determine mime type
|
// Use magic signatures to determine mime type
|
||||||
let kind = infer::get_from_path(f.path()).expect("file read successfully");
|
let kind = infer::get_from_path(f.path()).expect("file read successfully");
|
||||||
let mime_type = if let Some(kind) = kind {
|
let mime_type = if let Some(kind) = kind {
|
||||||
@@ -10,14 +17,6 @@ pub fn determine_mime_type(f: &mut NamedTempFile, buf: &Vec<u8>, file_name: &str
|
|||||||
"application/octet-stream"
|
"application/octet-stream"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map any known conflicts where appropriate
|
|
||||||
let mime_type = if mime_type == "application/zip" && file_name.to_lowercase().ends_with(".apk")
|
|
||||||
{
|
|
||||||
"application/vnd.android.package-archive"
|
|
||||||
} else {
|
|
||||||
mime_type
|
|
||||||
};
|
|
||||||
|
|
||||||
// See if the file is actually just plain Unicode/ASCII text
|
// See if the file is actually just plain Unicode/ASCII text
|
||||||
if mime_type == "application/octet-stream" && simdutf8::basic::from_utf8(buf).is_ok() {
|
if mime_type == "application/octet-stream" && simdutf8::basic::from_utf8(buf).is_ok() {
|
||||||
return "plain/text";
|
return "plain/text";
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Build Stage
|
||||||
|
FROM ghcr.io/revoltchat/base:latest AS builder
|
||||||
|
|
||||||
|
# Bundle Stage
|
||||||
|
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||||
|
COPY --from=builder /home/rust/src/target/release/revolt-january ./
|
||||||
|
COPY --from=mwader/static-ffmpeg:7.0.2 /ffmpeg /usr/local/bin/
|
||||||
|
COPY --from=mwader/static-ffmpeg:7.0.2 /ffprobe /usr/local/bin/
|
||||||
|
|
||||||
|
EXPOSE 14705
|
||||||
|
USER nonroot
|
||||||
|
CMD ["./revolt-january"]
|
||||||
@@ -15,7 +15,7 @@ pub mod requests;
|
|||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), std::io::Error> {
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
// Configure logging and environment
|
// Configure logging and environment
|
||||||
revolt_config::configure!(api);
|
revolt_config::configure!(proxy);
|
||||||
|
|
||||||
// Configure API schema
|
// Configure API schema
|
||||||
#[derive(OpenApi)]
|
#[derive(OpenApi)]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ echo "Building images, will tag for ghcr.io with $TAG!"
|
|||||||
docker build -t ghcr.io/revoltchat/base:latest -f Dockerfile.useCurrentArch .
|
docker build -t ghcr.io/revoltchat/base:latest -f Dockerfile.useCurrentArch .
|
||||||
docker build -t ghcr.io/revoltchat/server:$TAG - < crates/delta/Dockerfile
|
docker build -t ghcr.io/revoltchat/server:$TAG - < crates/delta/Dockerfile
|
||||||
docker build -t ghcr.io/revoltchat/bonfire:$TAG - < crates/bonfire/Dockerfile
|
docker build -t ghcr.io/revoltchat/bonfire:$TAG - < crates/bonfire/Dockerfile
|
||||||
|
docker build -t ghcr.io/revoltchat/autumn:$TAG - < crates/services/autumn/Dockerfile
|
||||||
|
|
||||||
if [ "$DEBUG" = "true" ]; then
|
if [ "$DEBUG" = "true" ]; then
|
||||||
git restore Cargo.toml
|
git restore Cargo.toml
|
||||||
@@ -30,3 +31,4 @@ fi
|
|||||||
|
|
||||||
docker push ghcr.io/revoltchat/server:$TAG
|
docker push ghcr.io/revoltchat/server:$TAG
|
||||||
docker push ghcr.io/revoltchat/bonfire:$TAG
|
docker push ghcr.io/revoltchat/bonfire:$TAG
|
||||||
|
docker push ghcr.io/revoltchat/autumn:$TAG
|
||||||
Reference in new issue
Block a user