Compare commits

...

46 Commits

Author SHA1 Message Date
Paul Makles
f5c95f9d12 chore: bump rauth for lockout reset on pwd reset 2023-01-20 15:57:58 +00:00
Paul Makles
fed1da72fd feat: support rauth events
feat: add platform user wipe event
2023-01-20 15:43:45 +00:00
Paul Makles
cf9c0bc5e1 chore: bump rauth 2023-01-20 14:00:19 +00:00
Paul Makles
cc2054838a chore: swap okapi for revolt_okapi 2023-01-20 14:00:19 +00:00
Paul Makles
d9d2735efa fix(ratelimiter): re-calculate reset time
closes #204

Co-authored-by: TheBobBobs <84781603+TheBobBobs@users.noreply.github.com>
2023-01-18 18:26:00 +00:00
Ryan
94b0472934 feat: pass server flags with invite data (#203) 2023-01-18 18:17:59 +00:00
Sophie L
4a7917e9ea feat: enable reactions by default in servers/DMs 2023-01-18 18:17:37 +00:00
Lea
ce6b454874 fix: raise masquerade avatar URL length limit 2023-01-18 18:17:05 +00:00
ToastXC
fc32996b51 chore: remove clippy warnings (#213) 2023-01-18 18:16:27 +00:00
Paul Makles
32d3683013 fix(ci): re-order docker-compose arguments
chore(ci): prevent running gen on PR
2023-01-14 15:53:09 +00:00
Paul Makles
0cebb69e02 feat: automate generating OpenAPI specification 2023-01-14 15:42:30 +00:00
Paul Makles
117958d8cd feat: include build information in root api call 2023-01-14 15:42:06 +00:00
Paul Makles
c5b823ad13 chore: clippy warnings 2023-01-03 21:06:43 +00:00
Paul Makles
27b4699299 fix: unify foreign user fetch into one method 2023-01-03 21:04:04 +00:00
Paul Makles
ee1c4cc2d5 chore: bump quark
feat: update email templates to include recipient
2022-11-19 16:52:51 +00:00
Paul Makles
0e0daf86f7 feat: update emails to not look garbage 2022-10-23 21:23:29 +01:00
Paul Makles
572e08c733 chore: update rauth 2022-10-23 21:08:08 +01:00
Paul Makles
9011939344 chore: remove .env file 2022-10-23 20:55:45 +01:00
Paul Makles
f4dcd091cc feat: add 'Focus' presence status 2022-09-18 12:12:31 +01:00
Paul Makles
86ad72b426 fix: strip unknown channels from categories (fixes #201) 2022-09-12 21:39:29 +01:00
Paul Makles
044d82d566 fix: use correct validation for masq role colour 2022-09-03 10:06:49 +01:00
Paul Makles
deffe663cb feat: add ability to leave groups / servers silently 2022-09-02 15:32:11 +01:00
Paul Makles
e7f6433d77 fix: actually transfer group ownership 2022-09-01 09:45:17 +01:00
Paul Makles
345e4d5f13 fix: validate categories 2022-09-01 09:41:00 +01:00
Sophie L
81fc3e7760 fix: set the cap to 100 2022-09-01 09:01:43 +01:00
Paul Makles
ca8d2e6118 fix: reaction ordering by using indexmap 2022-08-14 11:47:30 +02:00
Paul Makles
c1227e8e33 fix: check React permission on interactions 2022-08-14 11:31:36 +02:00
Paul Makles
0e0c6f2c3a fix: validate number of reactions on message when adding more 2022-08-13 20:26:09 +02:00
Paul Makles
fa82be74b5 fix: adjust permission checks for member_edit 2022-07-26 14:06:53 +01:00
Paul Makles
8491ced13d fix: ensure list matches 2022-07-18 12:46:33 +01:00
Paul Makles
229d4e2e1d fix: correct ordering for creating ownership change event 2022-07-15 21:46:41 +01:00
Paul Makles
7bdd2d69d6 feat: add message reactions + interactions object 2022-07-15 21:24:49 +01:00
Paul Makles
ab2af9b5e5 feat: add group ownership transfer 2022-07-15 17:58:08 +01:00
Paul Makles
5aebda2d16 chore: restrict emojis to lowercase 2022-07-15 17:57:53 +01:00
Paul Makles
a7273d0556 feat: allow servers to set discoverable value 2022-07-15 17:57:30 +01:00
Paul Makles
72aed64e5b chore: allow banning users not in server 2022-07-15 16:47:27 +01:00
Paul Makles
00ad4d54e7 feat: add nsfw flag to emotes 2022-07-15 16:34:08 +01:00
Paul Makles
cba8c4ef3b fix: add case-insensitive flag to colour regex 2022-07-15 16:13:04 +01:00
Paul Makles
b6e395fe0d fix: use default for roles key 2022-07-15 16:02:58 +01:00
Paul Makles
8d07457c58 chore: make joined_at migration shorter 2022-07-15 16:02:46 +01:00
Paul Makles
f1171e5358 feat: restrict role colours to regex (increase length limit) 2022-07-15 15:36:00 +01:00
Paul Makles
d96c9f62c4 chore: detach emojis on delete 2022-07-15 14:01:59 +01:00
Paul Makles
4f73e43a03 feat: restrict permissions for users in timeout 2022-07-15 13:55:55 +01:00
Paul Makles
741b8ee8fd feat: add role colours to masquerades
feat: add `joined_at` property to members
feat: add `timeout` property to members (non-functional)
2022-07-15 13:42:01 +01:00
Paul Makles
692081b7f0 chore: bump rauth for verification fix 2022-07-14 14:01:10 +01:00
Paul Makles
943b1f08f0 fix: prevent short-circuit on permissions 2022-07-10 18:27:22 +01:00
97 changed files with 3737 additions and 632 deletions

79
.env
View File

@@ -1,79 +0,0 @@
# MongoDB URI
MONGODB=mongodb://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 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

View File

@@ -1,10 +1,10 @@
name: Rust build and test
name: Rust build, test, and generate specification
on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]
env:
CARGO_TERM_COLOR: always
@@ -18,16 +18,58 @@ jobs:
- name: Install latest nightly
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
components: rustfmt, clippy
toolchain: nightly
override: true
components: rustfmt, clippy
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
command: test
- name: Copy .env.example
if: github.event_name != 'pull_request'
run: |
cp .env.example .env
- name: Run services in background
if: github.event_name != 'pull_request'
run: |
docker-compose -f docker-compose.db.yml up -d
- name: Start API in background
if: github.event_name != 'pull_request'
run: |
cargo run --bin revolt-delta &
- name: Wait for API to go up
if: github.event_name != 'pull_request'
uses: nev7n/wait_for_response@v1
with:
url: "http://localhost:8000/"
- name: Checkout API repository
if: github.event_name != 'pull_request'
uses: actions/checkout@v3
with:
repository: revoltchat/api
path: api
- name: Download OpenAPI specification
if: github.event_name != 'pull_request'
run: curl http://localhost:8000/openapi.json -o api/OpenAPI.json
- name: Commit changes
if: github.event_name != 'pull_request'
uses: EndBug/add-and-commit@v4
with:
cwd: "api"
add: "*.json"
message: "chore: generate OpenAPI specification"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
Rocket.toml
target
.data
.env
.vercel

317
Cargo.lock generated
View File

@@ -81,6 +81,12 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "anyhow"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
[[package]]
name = "arrayref"
version = "0.3.6"
@@ -517,6 +523,9 @@ name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
@@ -606,7 +615,7 @@ dependencies = [
"rand 0.8.5",
"sha2",
"subtle",
"time 0.3.9",
"time 0.3.17",
"version_check",
]
@@ -875,6 +884,26 @@ dependencies = [
"syn 1.0.96",
]
[[package]]
name = "enum-iterator"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91a4ec26efacf4aeff80887a175a419493cb6f8b5480d26387eb0bd038976187"
dependencies = [
"enum-iterator-derive",
]
[[package]]
name = "enum-iterator-derive"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce"
dependencies = [
"proc-macro2",
"quote 1.0.18",
"syn 1.0.96",
]
[[package]]
name = "env_logger"
version = "0.7.1"
@@ -1138,6 +1167,18 @@ dependencies = [
"wasi 0.10.0+wasi-snapshot-preview1",
]
[[package]]
name = "getset"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote 1.0.18",
"syn 1.0.96",
]
[[package]]
name = "ghash"
version = "0.4.4"
@@ -1154,6 +1195,19 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4"
[[package]]
name = "git2"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
dependencies = [
"bitflags",
"libc",
"libgit2-sys",
"log",
"url",
]
[[package]]
name = "glob"
version = "0.3.0"
@@ -1454,12 +1508,12 @@ checksum = "90f97a5f38dd3ccfbe7aa80f4a0c00930f21b922c74195be0201c51028f22dcf"
[[package]]
name = "indexmap"
version = "1.8.2"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg 1.1.0",
"hashbrown 0.11.2",
"hashbrown 0.12.1",
"serde",
]
@@ -1514,7 +1568,7 @@ dependencies = [
"generic-array 0.14.5",
"schemars",
"serde",
"time 0.3.9",
"time 0.3.17",
]
[[package]]
@@ -1529,6 +1583,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]]
name = "jobserver"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.57"
@@ -1590,6 +1653,30 @@ version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "libgit2-sys"
version = "0.14.1+1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a07fb2692bc3593bda59de45a502bb3071659f2c515e28c71e728306b038e17"
dependencies = [
"cc",
"libc",
"libz-sys",
"pkg-config",
]
[[package]]
name = "libz-sys"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linked-hash-map"
version = "0.5.4"
@@ -1914,6 +2001,15 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "ntapi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "num-integer"
version = "0.1.45"
@@ -1964,15 +2060,6 @@ dependencies = [
"syn 1.0.96",
]
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.28.4"
@@ -1982,22 +2069,11 @@ dependencies = [
"memchr",
]
[[package]]
name = "okapi"
version = "0.7.0-rc.1"
source = "git+https://github.com/insertish/okapi?rev=a1048d0c8cd771e424ec97d33d825c32e06aa120#a1048d0c8cd771e424ec97d33d825c32e06aa120"
dependencies = [
"log",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "once_cell"
version = "1.12.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]]
name = "opaque-debug"
@@ -2504,8 +2580,8 @@ dependencies = [
[[package]]
name = "rauth"
version = "1.0.0"
source = "git+https://github.com/insertish/rauth?rev=c2acaf6b31213e3969f8b48f9deed3daa125d394#c2acaf6b31213e3969f8b48f9deed3daa125d394"
version = "1.0.3"
source = "git+https://github.com/insertish/rauth?tag=1.0.3#a62eae5076d94730d06e76d438cbca0d70161856"
dependencies = [
"async-std",
"async-trait",
@@ -2520,12 +2596,12 @@ dependencies = [
"log",
"mongodb",
"nanoid",
"okapi",
"rand 0.8.5",
"regex",
"reqwest",
"revolt_okapi",
"revolt_rocket_okapi",
"rocket",
"rocket_okapi",
"rust-argon2",
"schemars",
"serde",
@@ -2714,7 +2790,7 @@ dependencies = [
[[package]]
name = "revolt-bonfire"
version = "1.0.6-patch.2"
version = "0.5.6"
dependencies = [
"async-std",
"async-tungstenite",
@@ -2730,7 +2806,7 @@ dependencies = [
[[package]]
name = "revolt-delta"
version = "0.5.4"
version = "0.5.6"
dependencies = [
"async-channel",
"async-std",
@@ -2755,9 +2831,9 @@ dependencies = [
"regex",
"reqwest",
"revolt-quark",
"revolt_rocket_okapi",
"rocket",
"rocket_empty",
"rocket_okapi",
"rocket_rauth",
"schemars",
"serde",
@@ -2765,11 +2841,12 @@ dependencies = [
"ulid 0.4.1",
"url",
"validator 0.14.0",
"vergen",
]
[[package]]
name = "revolt-quark"
version = "0.1.0"
version = "0.5.6"
dependencies = [
"async-recursion",
"async-std",
@@ -2783,6 +2860,7 @@ dependencies = [
"dotenv",
"futures",
"impl_ops",
"indexmap",
"iso8601-timestamp",
"lazy_static",
"linkify 0.8.1",
@@ -2791,18 +2869,19 @@ dependencies = [
"mongodb",
"nanoid",
"num_enum",
"okapi",
"once_cell",
"optional_struct",
"pretty_env_logger",
"rauth",
"redis-kiss",
"regex",
"reqwest",
"revolt_okapi",
"revolt_rocket_okapi",
"rocket",
"rocket_cors",
"rocket_empty",
"rocket_http",
"rocket_okapi",
"schemars",
"sentry",
"serde",
@@ -2812,6 +2891,47 @@ dependencies = [
"web-push",
]
[[package]]
name = "revolt_okapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23bfdf7ae769c3042fe727f6e5c17363b02a64b4b33ad60c3e5f73b26df7835b"
dependencies = [
"log",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "revolt_rocket_okapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "275e1e9bd3343f75225cafa64f4bfb939c8b21c5f861141180fc0e24769ff6cf"
dependencies = [
"either",
"log",
"revolt_okapi",
"revolt_rocket_okapi_codegen",
"rocket",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "revolt_rocket_okapi_codegen"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc6620569d8ac8f0a1690fcca13f488503807a60e96ebf729749b59aca1dbef9"
dependencies = [
"darling",
"proc-macro2",
"quote 1.0.18",
"rocket_http",
"syn 1.0.96",
]
[[package]]
name = "ring"
version = "0.16.20"
@@ -2879,7 +2999,7 @@ dependencies = [
"serde_json",
"state",
"tempfile",
"time 0.3.9",
"time 0.3.17",
"tokio 1.18.2",
"tokio-stream",
"tokio-util 0.7.2",
@@ -2906,9 +3026,10 @@ dependencies = [
[[package]]
name = "rocket_cors"
version = "0.5.2"
source = "git+https://github.com/lawliet89/rocket_cors?rev=5843861a88958c16bfaa0b40f0d8910772bcd2f6#5843861a88958c16bfaa0b40f0d8910772bcd2f6"
version = "0.6.0-alpha1"
source = "git+https://github.com/lawliet89/rocket_cors?rev=c17e8145baa4790319fdb6a473e465b960f55e7c#c17e8145baa4790319fdb6a473e465b960f55e7c"
dependencies = [
"http",
"log",
"regex",
"rocket",
@@ -2921,12 +3042,13 @@ dependencies = [
[[package]]
name = "rocket_empty"
version = "0.1.0"
source = "git+https://github.com/insertish/rocket_empty?branch=master#9d8234fc3bb215ea241b787301e8cff68ae53652"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c0922e47f981204fee38578a8efcf47a5c1a4ac0eb7f59e6bdfa3e61c8e3d69"
dependencies = [
"okapi",
"revolt_okapi",
"revolt_rocket_okapi",
"rocket",
"rocket_okapi",
]
[[package]]
@@ -2951,48 +3073,22 @@ dependencies = [
"smallvec",
"stable-pattern",
"state",
"time 0.3.9",
"time 0.3.17",
"tokio 1.18.2",
"uncased",
]
[[package]]
name = "rocket_okapi"
version = "0.8.0-rc.1"
source = "git+https://github.com/insertish/okapi?rev=a1048d0c8cd771e424ec97d33d825c32e06aa120#a1048d0c8cd771e424ec97d33d825c32e06aa120"
dependencies = [
"either",
"log",
"okapi",
"rocket",
"rocket_okapi_codegen",
"schemars",
"serde",
"serde_json",
]
[[package]]
name = "rocket_okapi_codegen"
version = "0.8.0-rc.1"
source = "git+https://github.com/insertish/okapi?rev=a1048d0c8cd771e424ec97d33d825c32e06aa120#a1048d0c8cd771e424ec97d33d825c32e06aa120"
dependencies = [
"darling",
"proc-macro2",
"quote 1.0.18",
"rocket_http",
"syn 1.0.96",
]
[[package]]
name = "rocket_rauth"
version = "1.0.0"
source = "git+https://github.com/insertish/rauth?rev=c2acaf6b31213e3969f8b48f9deed3daa125d394#c2acaf6b31213e3969f8b48f9deed3daa125d394"
version = "1.0.3"
source = "git+https://github.com/insertish/rauth?tag=1.0.3#a62eae5076d94730d06e76d438cbca0d70161856"
dependencies = [
"okapi",
"iso8601-timestamp",
"rauth",
"revolt_okapi",
"revolt_rocket_okapi",
"rocket",
"rocket_empty",
"rocket_okapi",
"schemars",
"serde",
]
@@ -3066,9 +3162,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.6"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
[[package]]
name = "ryu"
@@ -3258,7 +3354,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror",
"time 0.3.9",
"time 0.3.17",
"url",
"uuid",
]
@@ -3542,6 +3638,20 @@ dependencies = [
"unicode-xid 0.0.4",
]
[[package]]
name = "sysinfo"
version = "0.27.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c215311383d25d03753375c53ab9fad8fc0cf46953c409211e065edeabf577ee"
dependencies = [
"cfg-if 1.0.0",
"core-foundation-sys",
"libc",
"ntapi",
"once_cell",
"winapi 0.3.9",
]
[[package]]
name = "take_mut"
version = "0.2.2"
@@ -3573,18 +3683,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.31"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a"
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.31"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a"
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
dependencies = [
"proc-macro2",
"quote 1.0.18",
@@ -3613,21 +3723,30 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.9"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
dependencies = [
"itoa 1.0.2",
"libc",
"num_threads",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-macros"
version = "0.2.4"
name = "time-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "time-macros"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
dependencies = [
"time-core",
]
[[package]]
name = "tinyvec"
@@ -4201,6 +4320,24 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vergen"
version = "7.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "571b69f690c855821462709b6f41d42ceccc316fbd17b60bd06d06928cfe6a99"
dependencies = [
"anyhow",
"cfg-if 1.0.0",
"enum-iterator",
"getset",
"git2",
"rustc_version 0.4.0",
"rustversion",
"sysinfo",
"thiserror",
"time 0.3.17",
]
[[package]]
name = "version_check"
version = "0.9.4"

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-bonfire"
version = "1.0.6-patch.2"
version = "0.5.6"
license = "AGPL-3.0-or-later"
edition = "2021"

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-delta"
version = "0.5.4"
version = "0.5.6"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
@@ -51,13 +51,15 @@ mobc-redis = { version = "0.7.0", default-features = false, features = ["async-s
# web
rocket = { version = "0.5.0-rc.2", default-features = false, features = ["json"] }
rocket_empty = { git = "https://github.com/insertish/rocket_empty", branch = "master" }
rocket_rauth = { git = "https://github.com/insertish/rauth", rev = "c2acaf6b31213e3969f8b48f9deed3daa125d394" }
rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_rauth = { git = "https://github.com/insertish/rauth", tag = "1.0.3" }
# spec generation
schemars = "0.8.8"
# rocket_okapi = "0.8.0-rc.1"
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "a1048d0c8cd771e424ec97d33d825c32e06aa120", features = [ "swagger" ] }
revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
# quark
revolt-quark = { path = "../quark" }
[build-dependencies]
vergen = "7.5.0"

16
crates/delta/build.rs Normal file
View File

@@ -0,0 +1,16 @@
use vergen::{vergen, Config};
use std::process::Command;
fn main() {
if let Ok(output) = Command::new("git")
.args(["config", "--get", "remote.origin.url"])
.output()
{
if let Ok(git_origin) = String::from_utf8(output.stdout) {
println!("cargo:rustc-env=GIT_ORIGIN_URL={}", git_origin);
}
}
vergen(Config::default()).unwrap();
}

View File

@@ -1,7 +1,7 @@
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_okapi;
extern crate revolt_rocket_okapi;
#[macro_use]
extern crate serde_json;
#[macro_use]
@@ -10,7 +10,9 @@ extern crate lazy_static;
pub mod routes;
pub mod util;
use revolt_quark::rauth::RAuth;
use async_std::channel::unbounded;
use revolt_quark::events::client::EventV1;
use revolt_quark::rauth::{RAuth, RAuthEvent};
use revolt_quark::DatabaseInfo;
#[launch]
@@ -25,13 +27,33 @@ async fn rocket() -> _ {
let db = DatabaseInfo::Auto.connect().await.unwrap();
db.migrate_database().await.unwrap();
// Setup rAuth event channel
let (sender, receiver) = unbounded();
// Setup rAuth
let rauth = RAuth {
database: db.clone().into(),
config: revolt_quark::util::rauth::config(),
event_channel: Some(sender),
};
// Launch background task workers.
// Launch a listener for rAuth events
async_std::task::spawn(async move {
while let Ok(event) = receiver.recv().await {
match &event {
RAuthEvent::CreateSession { .. } | RAuthEvent::CreateAccount { .. } => {
EventV1::Auth(event).global().await
}
RAuthEvent::DeleteSession { user_id, .. }
| RAuthEvent::DeleteAllSessions { user_id, .. } => {
let id = user_id.to_string();
EventV1::Auth(event).private(id).await
}
}
}
});
// Launch background task workers
async_std::task::spawn(revolt_quark::tasks::start_workers(db.clone()));
// Configure CORS

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod create;
mod delete;

View File

@@ -2,13 +2,27 @@ use revolt_quark::{
models::{channel::PartialChannel, Channel, User},
perms, Db, EmptyResponse, Error, Permission, Ref, Result,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsChannelDelete {
/// Whether to not send a leave message
leave_silently: Option<bool>,
}
/// # Close Channel
///
/// Deletes a server channel, leaves a group or closes a group.
#[openapi(tag = "Channel Information")]
#[delete("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
#[delete("/<target>?<options..>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
options: OptionsChannelDelete,
) -> Result<EmptyResponse> {
let mut channel = target.as_channel(db).await?;
let mut perms = perms(&user).channel(&channel);
perms.throw_permission(db, Permission::ViewChannel).await?;
@@ -27,7 +41,12 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
.await
.map(|_| EmptyResponse),
Channel::Group { .. } => channel
.remove_user_from_group(db, &user.id, None)
.remove_user_from_group(
db,
&user.id,
None,
options.leave_silently.unwrap_or_default(),
)
.await
.map(|_| EmptyResponse),
Channel::TextChannel { .. } | Channel::VoiceChannel { .. } => {

View File

@@ -16,20 +16,21 @@ use validator::Validate;
pub struct DataEditChannel {
/// Channel name
#[validate(length(min = 1, max = 32))]
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
/// Channel description
#[validate(length(min = 0, max = 1024))]
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
/// Group owner
owner: Option<String>,
/// Icon
///
/// Provide an Autumn attachment Id.
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
/// Whether this channel is age-restricted
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>,
/// Whether this channel is archived
archived: Option<bool>,
#[validate(length(min = 1))]
remove: Option<Vec<FieldsChannel>>,
}
@@ -59,12 +60,48 @@ pub async fn req(
&& data.description.is_none()
&& data.icon.is_none()
&& data.nsfw.is_none()
&& data.owner.is_none()
&& data.remove.is_none()
{
return Ok(Json(channel));
}
let mut partial: PartialChannel = Default::default();
// Transfer group ownership
if let Some(new_owner) = data.owner {
if let Channel::Group {
owner, recipients, ..
} = &mut channel
{
// Make sure we are the owner of this group
if owner != &user.id {
return Err(Error::NotOwner);
}
// Ensure user is part of group
if !recipients.contains(&new_owner) {
return Err(Error::NotInGroup);
}
// Transfer ownership
partial.owner = Some(new_owner.to_string());
let old_owner = std::mem::replace(owner, new_owner.to_string());
// Notify clients
SystemMessage::ChannelOwnershipChanged {
from: old_owner,
to: new_owner,
}
} else {
return Err(Error::InvalidOperation);
}
.into_message(channel.id().to_string())
.create(db, &channel, None)
.await
.ok();
}
match &mut channel {
Channel::Group {
id,

View File

@@ -33,7 +33,7 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyR
}
channel
.remove_user_from_group(db, &member.id, Some(&user.id))
.remove_user_from_group(db, &member.id, Some(&user.id), false)
.await
.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,34 @@
use revolt_quark::{
models::{message::PartialMessage, User},
perms, Db, EmptyResponse, Permission, Ref, Result,
};
/// # Remove All Reactions from Message
///
/// Remove your own, someone else's or all of a given reaction.
///
/// Requires `ManageMessages` permission.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions")]
pub async fn clear_reactions(db: &Db, user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::ManageMessages)
.await?;
// Fetch relevant message
let mut message = msg.as_message_in(db, channel.id()).await?;
// Clear reactions
message
.update(
db,
PartialMessage {
reactions: Some(Default::default()),
..Default::default()
},
)
.await
.map(|_| EmptyResponse)
}

View File

@@ -0,0 +1,29 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
/// # Add Reaction to Message
///
/// React to a given message.
#[openapi(tag = "Interactions")]
#[put("/<target>/messages/<msg>/reactions/<emoji>")]
pub async fn react_message(
db: &Db,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
perms(&user)
.channel(&channel)
.throw_permission_and_view_channel(db, Permission::React)
.await?;
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
// Add the reaction
message
.add_reaction(db, &user, &emoji.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use revolt_quark::{
models::{
message::{Masquerade, Reply, SendableEmbed},
message::{Interactions, Masquerade, Reply, SendableEmbed},
Message, User,
},
perms,
@@ -40,6 +40,8 @@ pub struct DataMessageSend {
/// Masquerade to apply to this message
#[validate]
masquerade: Option<Masquerade>,
/// Information about how this message should be interacted with
interactions: Option<Interactions>,
}
lazy_static! {
@@ -86,6 +88,7 @@ pub async fn message_send(
channel: channel.id().to_string(),
author: user.id.clone(),
masquerade: data.masquerade,
interactions: data.interactions.unwrap_or_default(),
..Default::default()
};
@@ -100,13 +103,22 @@ pub async fn message_send(
}
// 2. Verify permissions for masquerade.
if message.masquerade.is_some() {
if let Some(masq) = &message.masquerade {
permissions
.throw_permission(db, Permission::Masquerade)
.await?;
if masq.colour.is_some() {
permissions
.throw_permission(db, Permission::ManageRole)
.await?;
}
}
// 3. Verify replies are valid.
// 3. Ensure interactions information is correct
message.interactions.validate(db, &mut permissions).await?;
// 4. Verify replies are valid.
let mut replies = HashSet::new();
if let Some(entries) = data.replies {
if entries.len() > 5 {
@@ -139,7 +151,7 @@ pub async fn message_send(
.replace(replies.into_iter().collect::<Vec<String>>());
}
// 4. Process included embeds.
// 5. Process included embeds.
let mut embeds = vec![];
if let Some(sendable_embeds) = data.embeds {
for sendable_embed in sendable_embeds {
@@ -151,7 +163,7 @@ pub async fn message_send(
message.embeds.replace(embeds);
}
// 5. Add attachments to message.
// 6. Add attachments to message.
let mut attachments = vec![];
if let Some(ids) = &data.attachments {
if !ids.is_empty() {
@@ -177,10 +189,10 @@ pub async fn message_send(
message.attachments.replace(attachments);
}
// 6. Set content
// 7. Set content
message.content = data.content;
// 7. Pass-through nonce value for clients
// 8. Pass-through nonce value for clients
message.nonce = Some(idempotency.into_key());
message.create(db, &channel, Some(&user)).await?;

View File

@@ -0,0 +1,58 @@
use revolt_quark::{models::User, perms, Db, EmptyResponse, Permission, Ref, Result};
use serde::{Deserialize, Serialize};
/// # Query Parameters
#[derive(Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsUnreact {
/// Remove a specific user's reaction
user_id: Option<String>,
/// Remove all reactions
remove_all: Option<bool>,
}
/// # Remove Reaction(s) to Message
///
/// Remove your own, someone else's or all of a given reaction.
///
/// Requires `ManageMessages` if changing others' reactions.
#[openapi(tag = "Interactions")]
#[delete("/<target>/messages/<msg>/reactions/<emoji>?<options..>")]
pub async fn unreact_message(
db: &Db,
user: User,
target: Ref,
msg: Ref,
emoji: Ref,
options: OptionsUnreact,
) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?;
let mut permissions = perms(&user).channel(&channel);
permissions
.throw_permission_and_view_channel(db, Permission::React)
.await?;
// Check if we need to escalate permissions
let remove_all = options.remove_all.unwrap_or_default();
if options.user_id.is_some() || remove_all {
permissions
.throw_permission(db, Permission::ManageMessages)
.await?;
}
// Fetch relevant message
let message = msg.as_message_in(db, channel.id()).await?;
// Check if we should wipe all of this reaction
if remove_all {
return message
.clear_reaction(db, &emoji.id)
.await
.map(|_| EmptyResponse);
}
// Remove the reaction
message
.remove_reaction(db, options.user_id.as_ref().unwrap_or(&user.id), &emoji.id)
.await
.map(|_| EmptyResponse)
}

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod channel_ack;
mod channel_delete;
@@ -11,13 +11,16 @@ mod group_remove_member;
mod invite_create;
mod members_fetch;
mod message_bulk_delete;
mod message_clear_reactions;
mod message_delete;
mod message_edit;
mod message_fetch;
mod message_query;
mod message_query_stale;
mod message_react;
mod message_search;
mod message_send;
mod message_unreact;
mod permissions_set;
mod permissions_set_default;
mod voice_join;
@@ -44,5 +47,8 @@ pub fn routes() -> (Vec<Route>, OpenApi) {
voice_join::req,
permissions_set::req,
permissions_set_default::req,
message_react::react_message,
message_unreact::unreact_message,
message_clear_reactions::clear_reactions
]
}

View File

@@ -14,7 +14,11 @@ pub struct DataCreateEmoji {
/// Server name
#[validate(length(min = 1, max = 32), regex = "RE_EMOJI")]
name: String,
/// Parent information
parent: EmojiParent,
/// Whether the emoji is mature
#[serde(default)]
nsfw: bool,
}
/// # Create New Emoji
@@ -51,10 +55,11 @@ pub async fn create_emoji(
// Check that there are no more than 100 emoji
// ! FIXME: hardcoded upper limit
let emojis = db.fetch_emoji_by_parent_id(&server.id).await?;
if emojis.len() > 100 {
if emojis.len() > 99 {
return Err(Error::TooManyEmoji);
}
}
EmojiParent::Detached => return Err(Error::InvalidOperation),
};
// Find the relevant attachment
@@ -67,6 +72,7 @@ pub async fn create_emoji(
creator_id: user.id,
name: data.name,
animated: "image/gif" == &attachment.content_type,
nsfw: data.nsfw,
};
// Save emoji

View File

@@ -28,6 +28,7 @@ pub async fn delete_emoji(db: &Db, user: User, id: Ref) -> Result<EmptyResponse>
.throw_permission(db, Permission::ManageCustomisation)
.await?;
}
EmojiParent::Detached => return Ok(EmptyResponse),
};
}

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod emoji_create;
mod emoji_delete;

View File

@@ -23,6 +23,9 @@ pub enum InviteResponse {
/// Attachment for server banner
#[serde(skip_serializing_if = "Option::is_none")]
server_banner: Option<File>,
/// Enum of server flags
#[serde(skip_serializing_if = "Option::is_none")]
server_flags: Option<i32>,
/// Id of server channel
channel_id: String,
/// Name of server channel
@@ -94,6 +97,7 @@ pub async fn req(db: &Db, target: Ref) -> Result<Json<InviteResponse>> {
server_name: server.name,
server_icon: server.icon,
server_banner: server.banner,
server_flags: server.flags,
channel_id: id,
channel_name: name,
channel_description: description,

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod invite_delete;
mod invite_fetch;

View File

@@ -1,7 +1,7 @@
use revolt_rocket_okapi::{revolt_okapi::openapi3::OpenApi, settings::OpenApiSettings};
pub use rocket::http::Status;
pub use rocket::response::Redirect;
use rocket::{Build, Rocket};
use rocket_okapi::{okapi::openapi3::OpenApi, settings::OpenApiSettings};
mod bots;
mod channels;
@@ -39,7 +39,7 @@ pub fn mount(mut rocket: Rocket<Build>) -> Rocket<Build> {
}
fn custom_openapi_spec() -> OpenApi {
use rocket_okapi::okapi::openapi3::*;
use revolt_rocket_okapi::revolt_okapi::openapi3::*;
let mut extensions = schemars::Map::new();
extensions.insert(
@@ -80,6 +80,7 @@ fn custom_openapi_spec() -> OpenApi {
"Channel Invites",
"Channel Permissions",
"Messaging",
"Interactions",
"Groups",
"Voice"
]

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod complete;
mod hello;

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod subscribe;
mod unsubscribe;

View File

@@ -22,7 +22,7 @@ pub async fn req(
) -> Result<EmptyResponse> {
session.subscription = Some(data.into_inner());
session
.save(&rauth)
.save(rauth)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {

View File

@@ -13,7 +13,7 @@ use rocket::State;
pub async fn req(rauth: &State<RAuth>, mut session: Session) -> Result<EmptyResponse> {
session.subscription = None;
session
.save(&rauth)
.save(rauth)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {

View File

@@ -54,6 +54,21 @@ pub struct RevoltFeatures {
pub voso: VoiceFeature,
}
/// # Build Information
#[derive(Serialize, JsonSchema, Debug)]
pub struct BuildInformation {
/// Commit Hash
pub commit_sha: String,
/// Commit Timestamp
pub commit_timestamp: String,
/// Git Semver
pub semver: String,
/// Git Origin URL
pub origin_url: String,
/// Build Timestamp
pub timestamp: String,
}
/// # Server Configuration
#[derive(Serialize, JsonSchema, Debug)]
pub struct RevoltConfig {
@@ -67,6 +82,8 @@ pub struct RevoltConfig {
pub app: String,
/// Web Push VAPID public key
pub vapid: String,
/// Build information
pub build: BuildInformation,
}
/// # Query Node
@@ -101,6 +118,13 @@ pub async fn root() -> Result<Json<RevoltConfig>> {
ws: EXTERNAL_WS_URL.to_string(),
app: APP_URL.to_string(),
vapid: VAPID_PUBLIC_KEY.to_string(),
build: BuildInformation {
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
semver: env!("VERGEN_GIT_SEMVER").to_string(),
origin_url: env!("GIT_ORIGIN_URL", "<missing>").to_string(),
timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
},
}))
}

View File

@@ -1,5 +1,5 @@
use revolt_quark::{
models::{ServerBan, User},
models::{server_member::MemberCompositeKey, ServerBan, User},
perms, Db, Error, Permission, Ref, Result,
};
@@ -47,13 +47,27 @@ pub async fn req(
.throw_permission(db, Permission::BanMembers)
.await?;
let member = target.as_member(db, &server.id).await?;
// If member exists, check privileges against them
if let Ok(member) = target.as_member(db, &server.id).await {
if member.get_ranking(permissions.server.get().unwrap())
<= permissions.get_member_rank().unwrap_or(i64::MIN)
{
return Err(Error::NotElevated);
}
if member.get_ranking(permissions.server.get().unwrap())
<= permissions.get_member_rank().unwrap_or(i64::MIN)
{
return Err(Error::NotElevated);
server.ban_member(db, member, data.reason).await.map(Json)
} else {
let server_id = server.id.to_string();
server
.ban_user(
db,
MemberCompositeKey {
server: server_id,
user: target.id,
},
data.reason,
)
.await
.map(Json)
}
server.ban_member(db, member, data.reason).await.map(Json)
}

View File

@@ -5,7 +5,7 @@ use revolt_quark::{
server_member::{FieldsMember, PartialMember},
File, Member, User,
},
perms, Db, Error, Permission, Ref, Result,
perms, Db, Error, Permission, Ref, Result, Timestamp,
};
use rocket::serde::json::Json;
@@ -22,6 +22,8 @@ pub struct DataMemberEdit {
avatar: Option<String>,
/// Array of role ids
roles: Option<Vec<String>>,
/// Timestamp this member is timed out until
timeout: Option<Timestamp>,
/// Fields to remove from channel object
#[validate(length(min = 1))]
remove: Option<Vec<FieldsMember>>,
@@ -89,6 +91,16 @@ pub async fn req(
required.push(Permission::AssignRoles);
}
if data.timeout.is_some()
|| data
.remove
.as_ref()
.map(|x| x.contains(&FieldsMember::Timeout))
.unwrap_or_default()
{
required.push(Permission::TimeoutMembers);
}
for permission in required {
permissions.throw_permission(db, permission).await?;
}
@@ -105,13 +117,7 @@ pub async fn req(
// Check permissions against roles in diff
if let Some(roles) = &data.roles {
let fallback = vec![];
let current_roles = member
.roles
.as_ref()
.unwrap_or(&fallback)
.iter()
.collect::<HashSet<&String>>();
let current_roles = member.roles.iter().collect::<HashSet<&String>>();
let new_roles = roles.iter().collect::<HashSet<&String>>();
let added_roles: Vec<&&String> = new_roles.difference(&current_roles).collect();
@@ -132,12 +138,14 @@ pub async fn req(
nickname,
avatar,
roles,
timeout,
remove,
} = data;
let mut partial = PartialMember {
nickname,
roles,
timeout,
..Default::default()
};

View File

@@ -1,8 +1,6 @@
use revolt_quark::{
models::{Member, User},
perms,
presence::presence_filter_online,
Db, Ref, Result,
perms, Db, Ref, Result,
};
use rocket::serde::json::Json;
@@ -47,16 +45,7 @@ pub async fn req(
user_ids.push(member.id.user.clone());
}
let online_ids = presence_filter_online(&user_ids).await;
let mut users = db
.fetch_users(&user_ids)
.await?
.into_iter()
.map(|mut user| {
user.online = Some(online_ids.contains(&user.id));
user.foreign()
})
.collect::<Vec<User>>();
let mut users = User::fetch_foreign_users(db, &user_ids).await?;
// Ensure the lists match up exactly.
members.sort_by(|a, b| a.id.user.cmp(&b.id.user));

View File

@@ -34,7 +34,7 @@ pub async fn req(db: &Db, user: User, target: Ref, member: Ref) -> Result<EmptyR
}
server
.remove_member(db, member, RemovalIntention::Kick)
.remove_member(db, member, RemovalIntention::Kick, false)
.await
.map(|_| EmptyResponse)
}

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod ban_create;
mod ban_list;

View File

@@ -3,7 +3,9 @@ use revolt_quark::{
server::{FieldsRole, PartialRole, Role},
User,
},
perms, Db, Error, Permission, Ref, Result,
perms,
util::regex::RE_COLOUR,
Db, Error, Permission, Ref, Result,
};
use rocket::serde::json::Json;
@@ -17,7 +19,7 @@ pub struct DataEditRole {
#[validate(length(min = 1, max = 32))]
name: Option<String>,
/// Role colour
#[validate(length(min = 1, max = 32))]
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
colour: Option<String>,
/// Whether this role should be displayed separately
hoist: Option<bool>,

View File

@@ -2,13 +2,27 @@ use revolt_quark::{
models::{server_member::RemovalIntention, User},
Db, EmptyResponse, Ref, Result,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
/// # Query Parameters
#[derive(Validate, Serialize, Deserialize, JsonSchema, FromForm)]
pub struct OptionsServerDelete {
/// Whether to not send a leave message
leave_silently: Option<bool>,
}
/// # Delete / Leave Server
///
/// Deletes a server if owner otherwise leaves.
#[openapi(tag = "Server Information")]
#[delete("/<target>")]
pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
#[delete("/<target>?<options..>")]
pub async fn req(
db: &Db,
user: User,
target: Ref,
options: OptionsServerDelete,
) -> Result<EmptyResponse> {
let server = target.as_server(db).await?;
let member = db.fetch_member(&target.id, &user.id).await?;
@@ -16,7 +30,12 @@ pub async fn req(db: &Db, user: User, target: Ref) -> Result<EmptyResponse> {
server.delete(db).await
} else {
server
.remove_member(db, member, RemovalIntention::Leave)
.remove_member(
db,
member,
RemovalIntention::Leave,
options.leave_silently.unwrap_or_default(),
)
.await
}
.map(|_| EmptyResponse)

View File

@@ -1,3 +1,5 @@
use std::collections::HashSet;
use revolt_quark::{
models::{
server::{Category, FieldsServer, PartialServer, SystemMessageChannels},
@@ -33,6 +35,8 @@ pub struct DataEditServer {
// Whether this server is age-restricted
// nsfw: Option<bool>,
/// Whether this server is public and should show up on [Revolt Discover](https://rvlt.gg)
discoverable: Option<bool>,
/// Whether analytics should be collected for this server
///
/// Must be enabled in order to show up on [Revolt Discover](https://rvlt.gg).
@@ -102,6 +106,7 @@ pub async fn req(
categories,
system_messages,
// nsfw,
discoverable,
analytics,
remove,
} = data;
@@ -112,6 +117,7 @@ pub async fn req(
categories,
system_messages,
// nsfw,
discoverable,
analytics,
..Default::default()
};
@@ -131,29 +137,44 @@ pub async fn req(
}
}
// 2. Apply new icon
// 2. Validate changes
if let Some(system_messages) = &partial.system_messages {
for id in system_messages.clone().into_channel_ids() {
if !server.channels.contains(&id) {
return Err(Error::NotFound);
}
}
}
if let Some(categories) = &mut partial.categories {
let mut channel_ids = HashSet::new();
for category in categories {
for channel in &category.channels {
if channel_ids.contains(channel) {
return Err(Error::InvalidOperation);
}
channel_ids.insert(channel.to_string());
}
category
.channels
.retain(|item| server.channels.contains(item));
}
}
// 3. Apply new icon
if let Some(icon) = icon {
partial.icon = Some(File::use_server_icon(db, &icon, &server.id).await?);
server.icon = partial.icon.clone();
}
// 3. Apply new banner
// 4. Apply new banner
if let Some(banner) = banner {
partial.banner = Some(File::use_banner(db, &banner, &server.id).await?);
server.banner = partial.banner.clone();
}
// 4. Validate changes
if let Some(system_messages) = &partial.system_messages {
let channels = system_messages.clone().into_channel_ids();
if !db
.check_channels_exist(&channels.into_iter().collect())
.await?
{
return Err(Error::NotFound);
}
}
server
.update(db, partial, remove.unwrap_or_default())
.await?;

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod get_settings;
mod get_unreads;

View File

@@ -1,7 +1,7 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::{self, MediaType, RefOr};
use rocket::http::ContentType;
use rocket::response::{self, Responder};
use rocket::{Request, Response};
use rocket_okapi::okapi::openapi3::{self, MediaType, RefOr};
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
pub struct CachedFile((ContentType, Vec<u8>));
@@ -16,10 +16,10 @@ impl<'r> Responder<'r, 'static> for CachedFile {
}
}
impl rocket_okapi::response::OpenApiResponderInner for CachedFile {
impl revolt_rocket_okapi::response::OpenApiResponderInner for CachedFile {
fn responses(
_gen: &mut rocket_okapi::gen::OpenApiGenerator,
) -> std::result::Result<openapi3::Responses, rocket_okapi::OpenApiError> {
_gen: &mut revolt_rocket_okapi::gen::OpenApiGenerator,
) -> std::result::Result<openapi3::Responses, revolt_rocket_okapi::OpenApiError> {
let mut responses = schemars::Map::new();
let mut content = schemars::Map::new();

View File

@@ -1,5 +1,5 @@
use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi;
use rocket::Route;
use rocket_okapi::okapi::openapi3::OpenApi;
mod add_friend;
mod block_user;

View File

@@ -11,4 +11,4 @@ pub static RE_USERNAME: Lazy<Regex> =
/// Regex for valid emoji names
///
/// Alphanumeric and underscores
pub static RE_EMOJI: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_]+$").unwrap());
pub static RE_EMOJI: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-z0-9_]+$").unwrap());

View File

@@ -1,6 +1,6 @@
[package]
name = "revolt-quark"
version = "0.1.0"
version = "0.5.6"
license = "AGPL-3.0-or-later"
edition = "2021"
@@ -38,10 +38,8 @@ bson = { version = "2.1.0", features = ["chrono-0_4"] }
# Spec Generation
schemars = "0.8.8"
okapi = { git = "https://github.com/insertish/okapi", rev = "a1048d0c8cd771e424ec97d33d825c32e06aa120" }
rocket_okapi = { git = "https://github.com/insertish/okapi", rev = "a1048d0c8cd771e424ec97d33d825c32e06aa120" }
# okapi = "0.7.0-rc.1"
# rocket_okapi = "0.8.0-rc.1"
revolt_okapi = "0.9.1"
revolt_rocket_okapi = { version = "0.9.1", features = [ "swagger" ] }
# Databases
redis-kiss = { version = "0.1.3" }
@@ -64,10 +62,12 @@ regex = "1.5.5"
nanoid = "0.4.0"
linkify = "0.8.1"
dotenv = "0.15.0"
indexmap = "1.9.1"
impl_ops = "0.1.1"
num_enum = "0.5.6"
reqwest = "0.11.10"
bitfield = "0.13.2"
once_cell = "1.13.0"
lazy_static = "1.4.0"
lru = { version = "0.7.6", optional = true }
@@ -80,11 +80,11 @@ web-push = "0.7.2"
# Implementations
rocket_http = { optional = true, version = "0.5.0-rc.2" }
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false, features = ["json"] }
rocket_empty = { optional = true, git = "https://github.com/insertish/rocket_empty", branch = "master" }
rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "5843861a88958c16bfaa0b40f0d8910772bcd2f6" }
rocket_empty = { version = "0.1.1", optional = true, features = [ "schema" ] }
rocket_cors = { optional = true, git = "https://github.com/lawliet89/rocket_cors", rev = "c17e8145baa4790319fdb6a473e465b960f55e7c" }
# rAuth
rauth = { git = "https://github.com/insertish/rauth", rev = "c2acaf6b31213e3969f8b48f9deed3daa125d394", features = [ "async-std-runtime" ] }
rauth = { git = "https://github.com/insertish/rauth", tag = "1.0.3", features = [ "async-std-runtime" ] }
# Sentry
sentry = "0.25.0"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type="text/css">
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
</style>
</head>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<tbody>
<tr>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
</table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Account Deletion</h1>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately.</p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Confirm</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Account Deletion</h1>
<p>
You requested to have your account deleted, if you did not perform
this action please take measures to secure your account immediately.
</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
</div>
</div>
</body>
</html>

View File

@@ -1,5 +1,7 @@
You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately.
Confirm here: {{url}}
Please navigate to: {{url}}
Sent by Revolt.
This email is intended for {{email}}
Sent by Revolt
Made in the EU

View File

@@ -1,63 +1,138 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-GB">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Reset your password.</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style type="text/css">
a[x-apple-data-detectors] {color: inherit !important;}
</style>
</head>
<body style="margin: 0; padding: 0;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 20px 0 30px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
<tr>
<td align="center" bgcolor="#ff4654">
<img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
</td>
</tr>
<tr>
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #153643; font-family: Arial, sans-serif;">
<h1 style="font-size: 24px; margin: 0;">Reset your password!</h1>
</td>
</tr>
<tr>
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
<p>
You requested a password reset, if you didn't perform this action you can safely ignore this email.
</p>
<p style="margin: 0;">
Reset your password by navigating to <a href="{{url}}">{{url}}</a>.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ff4654" style="padding: 30px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
<p style="margin: 0;">Sent by Revolt. &middot; Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
<p>Revolt is a user-first chat platform built with modern web technologies.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type="text/css">
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
</style>
</head>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<tbody>
<tr>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
</table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Password Reset</h1>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">You requested a password reset, click below to continue.</p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Reset</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Password Reset</h1>
<p>You requested a password reset, click below to continue.</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Reset</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
</div>
</div>
</body>
</html>

View File

@@ -1,5 +1,7 @@
You requested a password reset, if you did not perform this action you can safely ignore this email.
Reset your password here: {{url}}
Please navigate to: {{url}}
Sent by Revolt.
This email is intended for {{email}}
Sent by Revolt
Made in the EU

View File

@@ -1,14 +0,0 @@
<h2>Reset your password.</h2>
<p>
You requested a password reset, if you did not perform this action you can safely ignore this email.
</p>
<p>
Reset your password here: <a href="{{url}}">{{url}}</a>
</p>
<br/>
<p>
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
</p>
<p>
Revolt is a user-first chat platform built with modern web technologies.
</p>

View File

@@ -1,63 +1,138 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-GB">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Verify your account.</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style type="text/css">
a[x-apple-data-detectors] {color: inherit !important;}
</style>
</head>
<body style="margin: 0; padding: 0;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 20px 0 30px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
<tr>
<td align="center" bgcolor="#ff4654">
<img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
</td>
</tr>
<tr>
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #153643; font-family: Arial, sans-serif;">
<h1 style="font-size: 24px; margin: 0;">You're almost there!</h1>
</td>
</tr>
<tr>
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
<p>
Verify your account to be able to log into the platform. If you didn't perform this action you can safely ignore this email.
</p>
<p style="margin: 0;">
Please verify your account by navigating to <a href="{{url}}">{{url}}</a>.
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ff4654" style="padding: 30px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr>
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
<p style="margin: 0;">Sent by Revolt. &middot; Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
<p>Revolt is a user-first chat platform built with modern web technologies.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type="text/css">
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}}
</style>
</head>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc">
<tbody>
<tr>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc">
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;">
<tbody>
<tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96">
</td>
</tr>
</tbody>
</table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff">
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Almost there!</h1>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">To complete your sign up, we just need to verify your email.</p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd">
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Confirm</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Almost there!</h1>
<p>To complete your sign up, we just need to verify your email.</p>
<a class="btn btn-primary p-3 fw-700" href="{{url}}">Confirm</a>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
</div>
</div>
</body>
</html>

View File

@@ -1,6 +1,8 @@
You're almost there!
If you did not perform this action you can safely ignore this email.
Almost there!
To complete your sign up, we just need to verify your email.
Please verify your account here: {{url}}
Please navigate to: {{url}}
Sent by Revolt.
This email is intended for {{email}}
Sent by Revolt
Made in the EU

View File

@@ -1,15 +0,0 @@
<h2>You're almost there!</h2>
<p>
Verify your account to be able to log into the platform.<br/>
If you did not perform this action you can safely ignore this email.
</p>
<p>
Please verify your account here: <a href="{{url}}">{{url}}</a>
</p>
<br/>
<p>
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
</p>
<p>
Revolt is a user-first chat platform built with modern web technologies.
</p>

View File

@@ -1,3 +1,4 @@
use rauth::RAuthEvent;
use serde::{Deserialize, Serialize};
use crate::models::channel::{FieldsChannel, PartialChannel};
@@ -78,6 +79,29 @@ pub enum EventV1 {
/// Delete message
MessageDelete { id: String, channel: String },
/// New reaction to a message
MessageReact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove user's reaction from message
MessageUnreact {
id: String,
channel_id: String,
user_id: String,
emoji_id: String,
},
/// Remove a reaction from message
MessageRemoveReaction {
id: String,
channel_id: String,
emoji_id: String,
},
/// Bulk delete messages
BulkMessageDelete { channel: String, ids: Vec<String> },
@@ -172,9 +196,23 @@ pub enum EventV1 {
/// Settings updated remotely
UserSettingsUpdate { id: String, update: UserSettings },
/// User has been platform banned or deleted their account
///
/// Clients should remove the following associated data:
/// - Messages
/// - DM Channels
/// - Relationships
/// - Server Memberships
///
/// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear.
UserPlatformWipe { user_id: String, flags: i32 },
/// New emoji
EmojiCreate(Emoji),
/// Delete emoji
EmojiDelete { id: String },
/// Auth events
Auth(RAuthEvent),
}

View File

@@ -23,11 +23,7 @@ impl Cache {
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
match &channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
let member = self
.members
.iter()
.map(|(_, x)| x)
.find(|x| &x.id.server == server);
let member = self.members.values().find(|x| &x.id.server == server);
let server = self.servers.get(server);
let mut perms = perms(self.users.get(&self.user_id).unwrap()).channel(channel);
@@ -111,6 +107,7 @@ impl State {
// Fetch all memberships with their corresponding servers.
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
let servers = db.fetch_servers(&server_ids).await?;
@@ -484,10 +481,8 @@ impl State {
if data.rank.is_some() || data.permissions.is_some() {
if let Some(member) = self.cache.members.get(id) {
if let Some(roles) = &member.roles {
if roles.contains(role_id) {
queue_server = Some(id.clone());
}
if member.roles.contains(role_id) {
queue_server = Some(id.clone());
}
}
}
@@ -498,10 +493,8 @@ impl State {
}
if let Some(member) = self.cache.members.get(id) {
if let Some(roles) = &member.roles {
if roles.contains(role_id) {
queue_server = Some(id.clone());
}
if member.roles.contains(role_id) {
queue_server = Some(id.clone());
}
}
}
@@ -566,4 +559,9 @@ impl EventV1 {
pub async fn private(self, id: String) {
self.p(format!("{}!", id)).await;
}
/// Publish internal global event
pub async fn global(self) {
self.p("global".to_string()).await;
}
}

View File

@@ -1,5 +1,3 @@
use std::collections::HashSet;
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
use crate::{AbstractAttachment, AbstractChannel, Error, OverrideField, Result};
@@ -83,8 +81,4 @@ impl AbstractChannel for DummyDb {
info!("Updating permissions for role {role} in {channel} with {permissions:?}");
Ok(())
}
async fn check_channels_exist(&self, _channels: &HashSet<String>) -> Result<bool> {
Ok(true)
}
}

View File

@@ -64,4 +64,22 @@ impl AbstractMessage for DummyDb {
) -> Result<Vec<Message>> {
Ok(vec![self.fetch_message(channel).await.unwrap()])
}
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
info!("Add to {id} with {emoji} and {user}");
Ok(())
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
info!("Remove {emoji} from {id} for {user}");
Ok(())
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
info!("Clear {emoji} on {id}");
Ok(())
}
}

View File

@@ -14,6 +14,7 @@ impl AbstractEmoji for DummyDb {
parent: EmojiParent::Server { id: id.into() },
creator_id: id.into(),
animated: false,
nsfw: false,
})
}
@@ -33,9 +34,9 @@ impl AbstractEmoji for DummyDb {
Ok(())
}
/// Delete an emoji by its id
async fn delete_emoji(&self, emoji: &Emoji) -> Result<()> {
info!("Delete {emoji:?}");
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
info!("Detach {emoji:?}");
Ok(())
}
}

View File

@@ -3,6 +3,8 @@ use crate::{AbstractServerMember, Result};
use super::super::DummyDb;
use iso8601_timestamp::Timestamp;
#[async_trait]
impl AbstractServerMember for DummyDb {
async fn fetch_member(&self, server: &str, user: &str) -> Result<Member> {
@@ -11,9 +13,11 @@ impl AbstractServerMember for DummyDb {
server: server.into(),
user: user.into(),
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: None,
roles: vec![],
timeout: None,
})
}

View File

@@ -286,6 +286,7 @@ impl Channel {
db: &Database,
user: &str,
by: Option<&str>,
silent: bool,
) -> Result<()> {
match &self {
Channel::Group {
@@ -305,6 +306,15 @@ impl Channel {
vec![],
)
.await?;
SystemMessage::ChannelOwnershipChanged {
from: owner.to_string(),
to: new_owner.into(),
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok();
} else {
db.delete_channel(self).await?;
return Ok(());
@@ -320,20 +330,22 @@ impl Channel {
.p(id.to_string())
.await;
if let Some(by) = by {
SystemMessage::UserRemove {
id: user.to_string(),
by: by.to_string(),
}
} else {
SystemMessage::UserLeft {
id: user.to_string(),
if !silent {
if let Some(by) = by {
SystemMessage::UserRemove {
id: user.to_string(),
by: by.to_string(),
}
} else {
SystemMessage::UserLeft {
id: user.to_string(),
}
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok();
}
.into_message(id.to_string())
.create(db, self, None)
.await
.ok();
Ok(())
}

View File

@@ -8,17 +8,19 @@ use crate::{
events::client::EventV1,
models::{
message::{
AppendMessage, BulkMessageResponse, PartialMessage, SendableEmbed, SystemMessage,
AppendMessage, BulkMessageResponse, Interactions, PartialMessage, SendableEmbed,
SystemMessage,
},
Channel, Message, User,
Channel, Emoji, Message, User,
},
permissions::PermissionCalculator,
presence::presence_filter_online,
tasks::ack::AckEvent,
types::{
january::{Embed, Text},
push::PushNotification,
},
Database, Error, Result,
Database, Error, Permission, Result,
};
impl Message {
@@ -191,6 +193,84 @@ impl Message {
Err(Error::PayloadTooLarge)
}
}
/// Add a reaction to a message
pub async fn add_reaction(&self, db: &Database, user: &User, emoji: &str) -> Result<()> {
// Check how many reactions are already on the message
if self.reactions.len() >= 20 {
return Err(Error::InvalidOperation);
}
// Check if the emoji is whitelisted
if !self.interactions.can_use(emoji) {
return Err(Error::InvalidOperation);
}
// Check if the emoji is usable by us
if !Emoji::can_use(db, emoji).await? {
return Err(Error::InvalidOperation);
}
// Send reaction event
EventV1::MessageReact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.id.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
// Add emoji
db.add_reaction(&self.id, emoji, &user.id).await
}
/// Remove a reaction from a message
pub async fn remove_reaction(&self, db: &Database, user: &str, emoji: &str) -> Result<()> {
// Check if it actually exists
let empty = if let Some(users) = self.reactions.get(emoji) {
if !users.contains(user) {
return Err(Error::NotFound);
}
users.len() == 1
} else {
return Err(Error::NotFound);
};
// Send reaction event
EventV1::MessageUnreact {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
user_id: user.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
if empty {
// If empty, remove the reaction entirely
db.clear_reaction(&self.id, emoji).await
} else {
// Otherwise only remove that one reaction
db.remove_reaction(&self.id, emoji, user).await
}
}
/// Remove a reaction from a message
pub async fn clear_reaction(&self, db: &Database, emoji: &str) -> Result<()> {
// Send reaction event
EventV1::MessageRemoveReaction {
id: self.id.to_string(),
channel_id: self.channel.to_string(),
emoji_id: emoji.to_string(),
}
.p(self.channel.to_string())
.await;
// Write to database
db.clear_reaction(&self.id, emoji).await
}
}
pub trait IntoUsers {
@@ -262,6 +342,9 @@ impl From<SystemMessage> for String {
"Channel description changed.".to_string()
}
SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string(),
SystemMessage::ChannelOwnershipChanged { .. } => {
"Channel ownership changed.".to_string()
}
}
}
}
@@ -300,7 +383,7 @@ impl BulkMessageResponse {
) -> Result<BulkMessageResponse> {
if let Some(true) = include_users {
let user_ids = messages.get_user_ids();
let users = db.fetch_users(&user_ids).await?;
let users = User::fetch_foreign_users(db, &user_ids).await?;
Ok(match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
@@ -321,3 +404,46 @@ impl BulkMessageResponse {
}
}
}
impl Interactions {
/// Validate interactions info is correct
pub async fn validate(
&self,
db: &Database,
permissions: &mut PermissionCalculator<'_>,
) -> Result<()> {
if let Some(reactions) = &self.reactions {
permissions.throw_permission(db, Permission::React).await?;
if reactions.len() > 20 {
return Err(Error::InvalidOperation);
}
for reaction in reactions {
if !Emoji::can_use(db, reaction).await? {
return Err(Error::InvalidOperation);
}
}
}
Ok(())
}
/// Check if we can use a given emoji to react
pub fn can_use(&self, emoji: &str) -> bool {
if self.restrict_reactions {
if let Some(reactions) = &self.reactions {
reactions.contains(emoji)
} else {
false
}
} else {
true
}
}
/// Check if default initialisation of fields
pub fn is_default(&self) -> bool {
!self.restrict_reactions && self.reactions.is_none()
}
}

View File

@@ -1,14 +1,27 @@
use std::{collections::HashSet, str::FromStr};
use ulid::Ulid;
use crate::{
events::client::EventV1,
models::{emoji::EmojiParent, Emoji},
Database, Result,
};
lazy_static! {
/// Permissible emojis
static ref PERMISSIBLE_EMOJIS: HashSet<String> = include_str!(crate::asset!("emojis.txt"))
.split('\n')
.map(|x| x.into())
.collect();
}
impl Emoji {
/// Get parent id
fn parent(&self) -> &str {
match &self.parent {
EmojiParent::Server { id } => id,
EmojiParent::Detached => "",
}
}
@@ -30,7 +43,16 @@ impl Emoji {
.p(self.parent().to_string())
.await;
db.mark_attachment_as_deleted(&self.id).await?;
db.delete_emoji(&self).await
db.detach_emoji(&self).await
}
/// Check whether we can use a given emoji
pub async fn can_use(db: &Database, emoji: &str) -> Result<bool> {
if Ulid::from_str(emoji).is_ok() {
db.fetch_emoji(emoji).await?;
Ok(true)
} else {
Ok(PERMISSIBLE_EMOJIS.contains(emoji))
}
}
}

View File

@@ -1,3 +1,6 @@
use std::collections::HashSet;
use iso8601_timestamp::Timestamp;
use ulid::Ulid;
use crate::{
@@ -188,7 +191,11 @@ impl Server {
server: self.id.clone(),
user: user.id.clone(),
},
..Default::default()
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: vec![],
timeout: None,
};
db.insert_member(&member).await?;
@@ -249,6 +256,7 @@ impl Server {
db: &Database,
member: Member,
intention: RemovalIntention,
silent: bool,
) -> Result<()> {
db.delete_member(&member.id).await?;
@@ -259,25 +267,39 @@ impl Server {
.p(member.id.server)
.await;
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
RemovalIntention::Leave => x.user_left.as_ref(),
RemovalIntention::Kick => x.user_kicked.as_ref(),
RemovalIntention::Ban => x.user_banned.as_ref(),
}) {
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
if !silent {
if let Some(id) = self.system_messages.as_ref().and_then(|x| match intention {
RemovalIntention::Leave => x.user_left.as_ref(),
RemovalIntention::Kick => x.user_kicked.as_ref(),
RemovalIntention::Ban => x.user_banned.as_ref(),
}) {
match intention {
RemovalIntention::Leave => SystemMessage::UserLeft { id: member.id.user },
RemovalIntention::Kick => SystemMessage::UserKicked { id: member.id.user },
RemovalIntention::Ban => SystemMessage::UserBanned { id: member.id.user },
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
.into_message(id.to_string())
.create_no_web_push(db, id, false)
.await
.ok();
}
Ok(())
}
/// Create ban
pub async fn ban_user(
self,
db: &Database,
id: MemberCompositeKey,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan { id, reason };
db.insert_ban(&ban).await?;
Ok(ban)
}
/// Ban a member from a server
pub async fn ban_member(
self,
@@ -285,37 +307,31 @@ impl Server {
member: Member,
reason: Option<String>,
) -> Result<ServerBan> {
let ban = ServerBan {
id: member.id.clone(),
reason,
};
self.remove_member(db, member, RemovalIntention::Ban)
self.remove_member(db, member.clone(), RemovalIntention::Ban, false)
.await?;
db.insert_ban(&ban).await?;
Ok(ban)
self.ban_user(db, member.id, reason).await
}
}
impl SystemMessageChannels {
pub fn into_channel_ids(self) -> Vec<String> {
let mut ids = vec![];
pub fn into_channel_ids(self) -> HashSet<String> {
let mut ids = HashSet::new();
if let Some(id) = self.user_joined {
ids.push(id);
ids.insert(id);
}
if let Some(id) = self.user_left {
ids.push(id);
ids.insert(id);
}
if let Some(id) = self.user_kicked {
ids.push(id);
ids.insert(id);
}
if let Some(id) = self.user_banned {
ids.push(id);
ids.insert(id);
}
ids

View File

@@ -1,3 +1,5 @@
use iso8601_timestamp::Timestamp;
use crate::{
events::client::EventV1,
models::{
@@ -36,19 +38,24 @@ impl Member {
/// Get this user's current ranking
pub fn get_ranking(&self, server: &Server) -> i64 {
if let Some(roles) = &self.roles {
let mut value = i64::MAX;
for role in roles {
if let Some(role) = server.roles.get(role) {
if role.rank < value {
value = role.rank;
}
let mut value = i64::MAX;
for role in &self.roles {
if let Some(role) = server.roles.get(role) {
if role.rank < value {
value = role.rank;
}
}
}
value
value
}
/// Check whether this member is in timeout
pub fn in_timeout(&self) -> bool {
if let Some(timeout) = self.timeout {
*timeout > *Timestamp::now_utc()
} else {
i64::MAX
false
}
}
@@ -56,7 +63,8 @@ impl Member {
match field {
FieldsMember::Avatar => self.avatar = None,
FieldsMember::Nickname => self.nickname = None,
FieldsMember::Roles => self.roles = None,
FieldsMember::Roles => self.roles.clear(),
FieldsMember::Timeout => self.timeout = None,
}
}
}

View File

@@ -4,6 +4,7 @@ use crate::models::user::{
};
use crate::permissions::defn::UserPerms;
use crate::permissions::r#impl::user::get_relationship;
use crate::presence::presence_filter_online;
use crate::{perms, Database, Error, Result};
use futures::try_join;
@@ -67,6 +68,7 @@ impl User {
}
/// Mutate the user object to remove redundant information
#[must_use]
pub fn foreign(mut self) -> User {
self.profile = None;
self.relations = None;
@@ -94,6 +96,21 @@ impl User {
self
}
/// Fetch foreign users by a list of IDs
pub async fn fetch_foreign_users(db: &Database, user_ids: &[String]) -> Result<Vec<User>> {
let online_ids = presence_filter_online(user_ids).await;
Ok(db
.fetch_users(user_ids)
.await?
.into_iter()
.map(|mut user| {
user.online = Some(online_ids.contains(&user.id));
user.foreign()
})
.collect::<Vec<User>>())
}
/// Mutate the user object to include relationship (if it does not already exist)
#[must_use]
pub fn with_relationship(self, perspective: &User) -> User {

View File

@@ -1,6 +1,6 @@
use std::time::Duration;
use bson::Bson;
use bson::{Bson, DateTime};
use futures::StreamExt;
use mongodb::{
bson::{doc, from_bson, from_document, to_document, Document},
@@ -16,7 +16,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 17;
pub const LATEST_REVISION: i32 = 18;
pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations");
@@ -644,6 +644,23 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.expect("Failed to create emoji parent index.");
}
if revision <= 17 {
info!("Running migration [revision 17 / 15-07-2022]: Initialise `joined_at` property on server members.");
db.col::<Document>("server_members")
.update_many(
doc! {},
doc! {
"$set": {
"joined_at": DateTime::now().to_rfc3339_string()
}
},
None,
)
.await
.expect("Failed to update server members.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.

View File

@@ -1,5 +1,3 @@
use std::collections::HashSet;
use bson::{Bson, Document};
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
@@ -291,25 +289,6 @@ impl AbstractChannel for MongoDb {
with: "channel",
})
}
async fn check_channels_exist(&self, channels: &HashSet<String>) -> Result<bool> {
let count = channels.len() as u64;
self.col::<Document>(COL)
.count_documents(
doc! {
"_id": {
"$in": channels.iter().cloned().collect::<Vec<String>>()
}
},
None,
)
.await
.map(|x| x == count)
.map_err(|_| Error::DatabaseError {
operation: "count_documents",
with: "channel",
})
}
}
impl IntoDocumentPath for FieldsChannel {

View File

@@ -279,4 +279,70 @@ impl AbstractMessage for MongoDb {
)
.await
}
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$addToSet": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$pull": {
format!("reactions.{emoji}"): user
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": id
},
doc! {
"$unset": {
format!("reactions.{emoji}"): 1
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})
}
}

View File

@@ -1,5 +1,7 @@
use bson::Document;
use crate::models::Emoji;
use crate::{AbstractEmoji, Result};
use crate::{AbstractEmoji, Error, Result};
use super::super::MongoDb;
@@ -42,7 +44,26 @@ impl AbstractEmoji for MongoDb {
}
/// Delete an emoji by its id
async fn delete_emoji(&self, emoji: &Emoji) -> Result<()> {
self.delete_one_by_id(COL, &emoji.id).await.map(|_| ())
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> {
self.col::<Document>(COL)
.update_one(
doc! {
"_id": &emoji.id
},
doc! {
"$set": {
"parent": {
"type": "Detached"
}
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "emojis",
})
}
}

View File

@@ -129,6 +129,7 @@ impl IntoDocumentPath for FieldsMember {
FieldsMember::Avatar => "avatar",
FieldsMember::Nickname => "nickname",
FieldsMember::Roles => "roles",
FieldsMember::Timeout => "timeout",
})
}
}

View File

@@ -1,7 +1,7 @@
use okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use rauth::models::Session;
use rocket_okapi::gen::OpenApiGenerator;
use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use revolt_rocket_okapi::gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
@@ -53,7 +53,7 @@ impl<'r> OpenApiFromRequest<'r> for User {
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
let mut requirements = schemars::Map::new();
requirements.insert("Session Token".to_owned(), vec![]);

View File

@@ -161,7 +161,7 @@ pub struct PartialChannel {
}
/// Optional fields on channel object
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
pub enum FieldsChannel {
Description,
Icon,

View File

@@ -1,8 +1,10 @@
use crate::util::regex::RE_COLOUR;
use indexmap::{IndexMap, IndexSet};
use iso8601_timestamp::Timestamp;
use serde::{Deserialize, Serialize};
use validator::Validate;
use iso8601_timestamp::Timestamp;
#[cfg(feature = "rocket_impl")]
use rocket::FromFormField;
@@ -11,6 +13,11 @@ use crate::{
types::january::Embed,
};
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}
/// # Reply
///
/// Representation of a message reply before it is sent.
@@ -33,7 +40,7 @@ pub struct SendableEmbed {
#[validate(length(min = 1, max = 2000))]
pub description: Option<String>,
pub media: Option<String>,
#[validate(length(min = 1, max = 64))]
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
pub colour: Option<String>,
}
@@ -61,6 +68,8 @@ pub enum SystemMessage {
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
#[serde(rename = "channel_ownership_changed")]
ChannelOwnershipChanged { from: String, to: String },
}
/// Name and / or avatar override information
@@ -72,8 +81,25 @@ pub struct Masquerade {
pub name: Option<String>,
/// Replace the avatar shown on this message (URL to image file)
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))]
#[validate(length(min = 1, max = 256))]
pub avatar: Option<String>,
/// Replace the display role colour shown on this message
///
/// Must have `ManageRole` permission to use
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128), regex = "RE_COLOUR")]
pub colour: Option<String>,
}
/// Information to guide interactions on this message
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, Validate, Default)]
pub struct Interactions {
/// Reactions which should always appear and be distinct
#[serde(skip_serializing_if = "Option::is_none", default)]
pub reactions: Option<IndexSet<String>>,
/// Whether reactions should be restricted to the given list
#[serde(skip_serializing_if = "if_false", default)]
pub restrict_reactions: bool,
}
/// Representation of a Message on Revolt
@@ -115,6 +141,12 @@ pub struct Message {
/// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>,
/// Hashmap of emoji IDs to array of user IDs
#[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub reactions: IndexMap<String, IndexSet<String>>,
/// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions,
/// Name and / or avatar overrides for this message
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>,

View File

@@ -1,10 +1,16 @@
use serde::{Deserialize, Serialize};
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t
}
/// Information about what owns this emoji
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[serde(tag = "type")]
pub enum EmojiParent {
Server { id: String },
Detached,
}
/// Representation of an Emoji on Revolt
@@ -20,5 +26,9 @@ pub struct Emoji {
/// Emoji name
pub name: String,
/// Whether the emoji is animated
#[serde(skip_serializing_if = "if_false", default)]
pub animated: bool,
/// Whether the emoji is marked as nsfw
#[serde(skip_serializing_if = "if_false", default)]
pub nsfw: bool,
}

View File

@@ -134,7 +134,7 @@ pub struct Server {
}
/// Optional fields on server object
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
pub enum FieldsServer {
Description,
Categories,
@@ -144,7 +144,7 @@ pub enum FieldsServer {
}
/// Optional fields on server object
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
pub enum FieldsRole {
Colour,
}

View File

@@ -1,3 +1,4 @@
use iso8601_timestamp::Timestamp;
use serde::{Deserialize, Serialize};
use crate::models::attachment::File;
@@ -12,7 +13,7 @@ pub struct MemberCompositeKey {
}
/// Representation of a member of a server on Revolt
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct, Default)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct)]
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
#[optional_name = "PartialMember"]
#[opt_skip_serializing_none]
@@ -22,6 +23,9 @@ pub struct Member {
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Time at which this user joined the server
pub joined_at: Timestamp,
/// Member's nickname
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
@@ -30,16 +34,20 @@ pub struct Member {
pub avatar: Option<File>,
/// Member's roles
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub roles: Vec<String>,
/// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>,
pub timeout: Option<Timestamp>,
}
/// Optional fields on server member object
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
pub enum FieldsMember {
Nickname,
Avatar,
Roles,
Timeout,
}
/// Member removal intention

View File

@@ -56,7 +56,7 @@ pub struct Bot {
}
/// Optional fields on bot object
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq)]
pub enum FieldsBot {
Token,
InteractionsURL,

View File

@@ -10,7 +10,7 @@ pub fn if_false(t: &bool) -> bool {
}
/// User's relationship with another user (or themselves)
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, PartialEq)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, PartialEq, Eq)]
pub enum RelationshipStatus {
None,
User,
@@ -30,11 +30,17 @@ pub struct Relationship {
}
/// Presence status
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, PartialEq)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, PartialEq, Eq)]
pub enum Presence {
/// User is online
Online,
/// User is not currently available
Idle,
/// User is focusing / will only receive mentions
Focus,
/// User is busy / will not receive any notifications
Busy,
/// User appears to be offline
Invisible,
}
@@ -99,6 +105,8 @@ pub enum Flags {
Deleted = 2,
/// User was banned off the platform
Banned = 4,
/// User was marked as spam and removed from platform
Spam = 8,
}
/// Bot information for if the user is a bot
@@ -157,7 +165,7 @@ pub struct User {
}
/// Optional fields on user object
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)]
#[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq, Eq, Clone)]
pub enum FieldsUser {
Avatar,
StatusText,

View File

@@ -45,8 +45,23 @@ impl Override {
impl PermissionValue {
/// Apply a given override to this value
pub fn apply(&mut self, v: Override) {
self.0 |= v.allow;
self.0 &= !v.deny;
self.allow(v.allow);
self.revoke(v.deny);
}
/// Allow given permissions
pub fn allow(&mut self, v: u64) {
self.0 |= v;
}
/// Revoke given permissions
pub fn revoke(&mut self, v: u64) {
self.0 &= !v;
}
/// Restrict to given permissions
pub fn restrict(&mut self, v: u64) {
self.0 &= v;
}
}
@@ -82,7 +97,7 @@ impl From<i64> for PermissionValue {
impl From<u64> for PermissionValue {
fn from(v: u64) -> Self {
Self(v as u64)
Self(v)
}
}

View File

@@ -24,7 +24,7 @@ pub enum Permission {
/// Manage server customisation (includes emoji)
ManageCustomisation = 1 << 4,
// % 1 bits reserved
// % 1 bit reserved
// * Member permissions
/// Kick other members below their ranking
@@ -65,8 +65,8 @@ pub enum Permission {
UploadFiles = 1 << 27,
/// Masquerade messages using custom nickname and avatar
Masquerade = 1 << 28,
// % 1 bits reserved
/// React to messages with emojis
React = 1 << 29,
// * Voice permissions
/// Connect to a voice channel
@@ -98,6 +98,7 @@ impl_op_ex!(+ |a: &Permission, b: &Permission| -> u64 { *a as u64 | *b as u64 })
impl_op_ex_commutative!(+ |a: &u64, b: &Permission| -> u64 { *a | *b as u64 });
lazy_static! {
pub static ref ALLOW_IN_TIMEOUT: u64 = Permission::ViewChannel + Permission::ReadMessageHistory;
pub static ref DEFAULT_PERMISSION_VIEW_ONLY: u64 =
Permission::ViewChannel + Permission::ReadMessageHistory;
pub static ref DEFAULT_PERMISSION: u64 = *DEFAULT_PERMISSION_VIEW_ONLY
@@ -108,10 +109,11 @@ lazy_static! {
+ Permission::Connect
+ Permission::Speak;
pub static ref DEFAULT_PERMISSION_SAVED_MESSAGES: u64 = Permission::GrantAllSafe as u64;
pub static ref DEFAULT_PERMISSION_DIRECT_MESSAGE: u64 =
*DEFAULT_PERMISSION + Permission::ManageChannel;
pub static ref DEFAULT_PERMISSION_DIRECT_MESSAGE: u64 = *DEFAULT_PERMISSION
+ Permission::ManageChannel
+ Permission::React;
pub static ref DEFAULT_PERMISSION_SERVER: u64 =
*DEFAULT_PERMISSION + Permission::ChangeNickname + Permission::ChangeAvatar;
*DEFAULT_PERMISSION + Permission::React + Permission::ChangeNickname + Permission::ChangeAvatar;
}
bitfield! {

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use crate::{
models::Channel, permissions::PermissionCalculator, Override, Permission, PermissionValue,
Permissions, Perms, Result, DEFAULT_PERMISSION_DIRECT_MESSAGE,
Permissions, Perms, Result, ALLOW_IN_TIMEOUT, DEFAULT_PERMISSION_DIRECT_MESSAGE,
DEFAULT_PERMISSION_SAVED_MESSAGES, DEFAULT_PERMISSION_VIEW_ONLY,
};
@@ -55,11 +55,7 @@ async fn calculate_server_permission(
let mut permissions: PermissionValue = server.default_permissions.into();
// 4. Resolve each role in order.
let member_roles: HashSet<&String> = if let Some(roles) = member.roles.as_ref() {
roles.iter().collect()
} else {
HashSet::new()
};
let member_roles: HashSet<&String> = member.roles.iter().collect();
if !member_roles.is_empty() {
let mut roles = server
@@ -80,6 +76,11 @@ async fn calculate_server_permission(
}
}
// 5. Revoke permissions if member is timed out.
if member.in_timeout() {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
Ok(permissions)
}
@@ -110,25 +111,36 @@ async fn calculate_channel_permission(
// 1. Check channel type.
let value: PermissionValue = match channel {
Channel::SavedMessages { .. } => (*DEFAULT_PERMISSION_SAVED_MESSAGES).into(),
Channel::DirectMessage { recipients, .. } => {
// 2. Fetch user.
let other_user = recipients
.iter()
.find(|x| x != &&data.perspective.id)
.unwrap();
let user = db.fetch_user(other_user).await?;
data.user.set(user);
// 3. Calculate user permissions.
let perms = data.calc_user(db).await;
// 4. Check if the user can send messages.
if perms.get_send_message() {
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
Channel::SavedMessages { user, .. } => {
if user == &data.perspective.id {
(*DEFAULT_PERMISSION_SAVED_MESSAGES).into()
} else {
(*DEFAULT_PERMISSION_VIEW_ONLY).into()
0_u64.into()
}
}
Channel::DirectMessage { recipients, .. } => {
// 2. Ensure we are a recipient.
if recipients.contains(&data.perspective.id) {
// 3. Fetch user.
let other_user = recipients
.iter()
.find(|x| x != &&data.perspective.id)
.unwrap();
let user = db.fetch_user(other_user).await?;
data.user.set(user);
// 4. Calculate user permissions.
let perms = data.calc_user(db).await;
// 5. Check if the user can send messages.
if perms.get_send_message() {
(*DEFAULT_PERMISSION_DIRECT_MESSAGE).into()
} else {
(*DEFAULT_PERMISSION_VIEW_ONLY).into()
}
} else {
0_u64.into()
}
}
Channel::Group {
@@ -182,11 +194,7 @@ async fn calculate_channel_permission(
}
// 4. Resolve each role in order.
let member_roles: HashSet<&String> = if let Some(roles) = member.roles.as_ref() {
roles.iter().collect()
} else {
HashSet::new()
};
let member_roles: HashSet<&String> = member.roles.iter().collect();
if !member_roles.is_empty() {
let mut roles = role_permissions
@@ -208,6 +216,11 @@ async fn calculate_channel_permission(
}
}
// 5. Revoke permissions if member is timed out.
if member.in_timeout() {
permissions.restrict(*ALLOW_IN_TIMEOUT);
}
permissions
} else {
(Permission::GrantAllSafe as u64).into()

View File

@@ -35,7 +35,7 @@ pub async fn presence_create_session(user_id: &str, flags: u8) -> (bool, u8) {
__set_key_presence_entry(&mut conn, user_id, entry).await;
// Add to region set in case of failure.
__add_to_set_sessions(&mut conn, &*REGION_KEY, user_id, session_id).await;
__add_to_set_sessions(&mut conn, &REGION_KEY, user_id, session_id).await;
(was_empty, session_id)
}
@@ -74,7 +74,7 @@ async fn presence_delete_session_internal(
// Remove from region set.
if !skip_region {
__remove_from_set_sessions(&mut conn, &*REGION_KEY, user_id, session_id).await;
__remove_from_set_sessions(&mut conn, &REGION_KEY, user_id, session_id).await;
}
}
@@ -147,7 +147,7 @@ pub async fn presence_clear_region(region_id: Option<&str>) {
// also send out any relevant events.
for session in sessions {
let parts = session.split(':').collect::<Vec<&str>>();
if let (Some(user_id), Some(session_id)) = (parts.get(0), parts.get(1)) {
if let (Some(user_id), Some(session_id)) = (parts.first(), parts.get(1)) {
if let Ok(session_id) = session_id.parse() {
presence_delete_session_internal(user_id, session_id, true).await;
}

View File

@@ -53,5 +53,5 @@ pub async fn __get_set_sessions(conn: &mut Conn, region_id: &str) -> Vec<String>
/// Delete region session set
pub async fn __delete_set_sessions(conn: &mut Conn, region_id: &str) {
let _: () = conn.del(region_id).await.unwrap();
conn.del::<_, ()>(region_id).await.unwrap();
}

View File

@@ -38,7 +38,7 @@ pub async fn queue(channel: String, id: String, content: String) {
pub async fn worker(db: Database) {
loop {
let task = Q.pop().await;
if let Ok(embeds) = Embed::generate(task.content, &*JANUARY_URL, *MAX_EMBED_COUNT).await {
if let Ok(embeds) = Embed::generate(task.content, &JANUARY_URL, *MAX_EMBED_COUNT).await {
if let Err(err) = Message::append(
&db,
task.id,

View File

@@ -1,5 +1,3 @@
use std::collections::HashSet;
use crate::models::channel::{Channel, FieldsChannel, PartialChannel};
use crate::{OverrideField, Result};
@@ -55,7 +53,4 @@ pub trait AbstractChannel: Sync + Send {
role: &str,
permissions: OverrideField,
) -> Result<()>;
/// Validate existence of channels
async fn check_channels_exist(&self, channels: &HashSet<String>) -> Result<bool>;
}

View File

@@ -42,4 +42,13 @@ pub trait AbstractMessage: Sync + Send {
after: Option<String>,
sort: MessageSort,
) -> Result<Vec<Message>>;
/// Add a new reaction to a message
async fn add_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove a reaction from a message
async fn remove_reaction(&self, id: &str, emoji: &str, user: &str) -> Result<()>;
/// Remove reaction from a message
async fn clear_reaction(&self, id: &str, emoji: &str) -> Result<()>;
}

View File

@@ -15,6 +15,6 @@ pub trait AbstractEmoji: Sync + Send {
/// Insert emoji into database.
async fn insert_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Delete an emoji by its id
async fn delete_emoji(&self, emoji: &Emoji) -> Result<()>;
/// Detach an emoji by its id
async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>;
}

View File

@@ -3,6 +3,7 @@ pub mod manipulation;
pub mod pfp;
pub mod rauth;
pub mod r#ref;
pub mod regex;
pub mod result;
pub mod value;
pub mod variables;

View File

@@ -26,19 +26,19 @@ pub fn config() -> Config {
title: "Verify your Revolt account.".into(),
text: include_str!(crate::asset!("templates/verify.txt")).into(),
url: format!("{}/login/verify/", *APP_URL),
html: None,
html: Some(include_str!(crate::asset!("templates/verify.html")).into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!(crate::asset!("templates/reset.txt")).into(),
url: format!("{}/login/reset/", *APP_URL),
html: None,
html: Some(include_str!(crate::asset!("templates/reset.html")).into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!(crate::asset!("templates/deletion.txt")).into(),
url: format!("{}/delete/", *APP_URL),
html: None,
html: Some(include_str!(crate::asset!("templates/deletion.html")).into()),
},
welcome: None,
},

View File

@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::models::{Bot, Channel, Emoji, Invite, Member, Message, Server, ServerBan, User};
use crate::presence::presence_is_online;
use crate::{Database, Result};
use crate::{Database, Error, Result};
/// Reference to some object in the database
#[derive(Serialize, Deserialize)]
@@ -44,6 +44,16 @@ impl Ref {
db.fetch_message(&self.id).await
}
/// Fetch message in channel from Ref
pub async fn as_message_in(&self, db: &Database, channel: &str) -> Result<Message> {
let message = self.as_message(db).await?;
if message.channel != channel {
return Err(Error::NotFound);
}
Ok(message)
}
/// Fetch bot from Ref
pub async fn as_bot(&self, db: &Database) -> Result<Bot> {
db.fetch_bot(&self.id).await

View File

@@ -0,0 +1,21 @@
use once_cell::sync::Lazy;
use regex::Regex;
/// Regex for valid role colours
///
/// Allows the use of named colours, rgb(a), variables and all gradients.
///
/// Flags:
/// - Case-insensitive (`i`)
///
/// Source:
/// ```regex
/// VALUE = [a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+
/// ADDITIONAL_VALUE = \d+deg
/// STOP = ([ ]+(\d{1,3}%|0))?
///
/// ^(?:VALUE|(repeating-)?(linear|conic|radial)-gradient\((VALUE|ADDITIONAL_VALUE)STOP(,[ ]*(VALUE)STOP)+\))$
/// ```
pub static RE_COLOUR: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)^(?:[a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|(repeating-)?(linear|conic|radial)-gradient\(([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+|\d+deg)([ ]+(\d{1,3}%|0))?(,[ ]*([a-z ]+|var\(--[a-z\d-]+\)|rgba?\([\d, ]+\)|#[a-f0-9]+)([ ]+(\d{1,3}%|0))?)+\))$").unwrap()
});

View File

@@ -1,4 +1,5 @@
use okapi::openapi3::{self, SchemaObject};
use revolt_okapi::openapi3::SchemaObject;
use revolt_rocket_okapi::revolt_okapi::openapi3;
use rocket::{
http::{ContentType, Status},
response::{self, Responder},
@@ -72,6 +73,7 @@ pub enum Error {
},
NotElevated,
CannotGiveMissingPermissions,
NotOwner,
// ? General errors.
DatabaseError {
@@ -167,6 +169,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::MissingUserPermission { .. } => Status::Forbidden,
Error::NotElevated => Status::Forbidden,
Error::CannotGiveMissingPermissions => Status::Forbidden,
Error::NotOwner => Status::Forbidden,
Error::DatabaseError { .. } => Status::InternalServerError,
Error::InternalError => Status::InternalServerError,
@@ -192,11 +195,11 @@ impl<'r> Responder<'r, 'static> for Error {
}
}
impl rocket_okapi::response::OpenApiResponderInner for Error {
impl revolt_rocket_okapi::response::OpenApiResponderInner for Error {
fn responses(
gen: &mut rocket_okapi::gen::OpenApiGenerator,
) -> std::result::Result<openapi3::Responses, rocket_okapi::OpenApiError> {
let mut content = okapi::Map::new();
gen: &mut revolt_rocket_okapi::gen::OpenApiGenerator,
) -> std::result::Result<openapi3::Responses, revolt_rocket_okapi::OpenApiError> {
let mut content = revolt_okapi::Map::new();
let settings = schemars::gen::SchemaSettings::default().with(|s| {
s.option_nullable = true;

View File

@@ -1,11 +1,11 @@
use crate::{Error, Result};
use async_std::sync::Mutex;
use revolt_rocket_okapi::gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use revolt_rocket_okapi::revolt_okapi::openapi3::{Parameter, ParameterValue};
use rocket::http::Status;
use rocket::request::{FromRequest, Outcome};
use rocket_okapi::gen::OpenApiGenerator;
use rocket_okapi::okapi::openapi3::{Parameter, ParameterValue};
use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use schemars::schema::{InstanceType, SchemaObject, SingleOrVec};
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -47,7 +47,7 @@ impl<'r> OpenApiFromRequest<'r> for IdempotencyKey {
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::Parameter(Parameter {
name: "Idempotency-Key".to_string(),
description: Some("Unique key to prevent duplicate requests".to_string()),

View File

@@ -16,8 +16,8 @@ use rocket::request::{FromRequest, Outcome};
use rocket::serde::json::Json;
use rocket::{Data, Request, Response};
use rocket_okapi::gen::OpenApiGenerator;
use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use revolt_rocket_okapi::gen::OpenApiGenerator;
use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
use serde::Serialize;
@@ -51,7 +51,7 @@ impl Entry {
}
/// Deduct one unit from the bucket and save
pub fn deduct(mut self, key: u64) {
pub fn deduct(mut self) {
let current_time = now().as_millis();
if current_time > self.reset {
self.used = 1;
@@ -59,7 +59,10 @@ impl Entry {
} else {
self.used += 1;
}
}
/// Save information
pub fn save(self, key: u64) {
MAP.insert(key, self);
}
@@ -189,10 +192,12 @@ impl Ratelimiter {
let entry = Entry::from(key);
let remaining = entry.get_remaining(limit);
let reset = entry.left_until_reset();
if remaining > 0 {
entry.deduct(key);
entry.deduct();
let reset = entry.left_until_reset();
entry.save(key);
Ok(Ratelimiter {
key,
limit,
@@ -200,7 +205,7 @@ impl Ratelimiter {
reset,
})
} else {
Err(reset)
Err(entry.left_until_reset())
}
}
}
@@ -236,7 +241,7 @@ impl<'r> OpenApiFromRequest<'r> for Ratelimiter {
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> rocket_okapi::Result<RequestHeaderInput> {
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
Ok(RequestHeaderInput::None)
}
}

View File

@@ -1,7 +1,7 @@
use rocket::Route;
pub fn routes() -> Vec<Route> {
rocket_okapi::swagger_ui::make_swagger_ui(&rocket_okapi::swagger_ui::SwaggerUIConfig {
revolt_rocket_okapi::swagger_ui::make_swagger_ui(&revolt_rocket_okapi::swagger_ui::SwaggerUIConfig {
url: "../openapi.json".to_owned(),
..Default::default()
})

9
docker-compose.db.yml Normal file
View File

@@ -0,0 +1,9 @@
version: "3.3"
services:
# MongoDB
database:
image: mongo
ports:
- "27017:27017"
volumes:
- ./.data/db:/data/db

View File

@@ -50,6 +50,7 @@ services:
/usr/bin/mc mb minio/backgrounds;
/usr/bin/mc mb minio/icons;
/usr/bin/mc mb minio/banners;
/usr/bin/mc mb minio/emojis;
exit 0;
"
# File server (autumn)