Compare commits

..

116 Commits
0.3.0 ... 0.4.1

Author SHA1 Message Date
Paul
fae0198343 Bump version to 0.4.1-alpha.11 2021-05-14 22:54:27 +01:00
Paul
10f116b4a3 Block voice calls in saved messages channel.
Add messages for desc / icon change for group chats.
2021-05-14 22:44:35 +01:00
Paul
6716a2d32b Join with newlines for embed generation.
Server January URL endpoint on server configuration.
2021-05-14 22:39:16 +01:00
Paul
6cc92b877e Fix block user route, send correct user struct.
Add route for fetching members.
Cargo fmt on accident.
2021-05-14 22:29:43 +01:00
Paul
cc0307f702 Fix find mutual query. 2021-05-13 17:19:43 +01:00
Paul
274d6f2b5a Update January definitions. (special embeds) 2021-05-09 19:30:55 +01:00
Paul
0f100213ba Update January definitions; add servers collection 2021-05-08 17:08:45 +01:00
Paul
7293abfc53 Add january support. 2021-05-07 18:00:21 +01:00
Paul
cf7bb832da Really make sure MongoDB driver can't screw up. 2021-05-03 21:02:12 +01:00
Paul
efc3794f3d Emergency patch; Rust MDB driver deletes doc. with empty update. 2021-05-03 20:48:37 +01:00
Paul
a319e72655 Add a way to choose message fetch sort. 2021-05-03 16:35:59 +01:00
Paul
2173b1e9f8 Fix type. 2021-05-03 15:04:05 +01:00
Paul
c086fe7ac4 Move versioning to source. 2021-05-03 15:03:30 +01:00
Paul
fa960ebc94 Send out unset in event. 2021-05-03 11:17:28 +01:00
Paul
b47067b311 Cache Cargo dependencies for Docker build. 2021-05-03 09:59:40 +01:00
Paul
2e996a487b Add event when user goes offline. 2021-05-03 08:57:49 +01:00
Paul
92bface6ae Add group icons / profile backgrounds. 2021-05-01 22:55:37 +01:00
Paul
8cfa5d7091 Run cargo fmt. 2021-05-01 17:29:31 +01:00
Paul
59b18fd376 Delete old avatar; fix database migration. 2021-05-01 17:12:51 +01:00
Paul
f135a57a9b API change: deprecate Gravatar.
Include avatar information in user object.
2021-05-01 16:38:06 +01:00
Paul
5da26cb833 Breaking: Provide new user object instead of id.
Fix rustup complaining about join macros.
2021-05-01 15:54:29 +01:00
Paul
c8981ac695 Merge branch 'master' of https://gitlab.insrt.uk/revolt/delta 2021-05-01 13:08:55 +01:00
Martin Loffler
33b0658680 Multi-thread rocket and hive 2021-04-30 13:19:31 +02:00
Paul
81002c75d2 Only push out status; mark attachments as deleted. 2021-04-28 17:12:14 +01:00
Paul
f163cb65de AAAAAAAAAAAAA
We should be setting message_id not message.
2021-04-24 18:01:08 +01:00
Paul
f7bcd3ad93 Migrate to Autumn 1.0.0 2021-04-24 16:00:13 +01:00
Paul
9e2250783c Allow people with mutual connections to see profile / status; fix username check. 2021-04-07 19:21:25 +01:00
Paul
2050a0609f Fix update_one failing on sys message. 2021-04-03 21:37:24 +01:00
Paul
a547177325 Add status presence indicator. 2021-04-03 20:50:14 +01:00
Paul
c3fa7bf74e Change route paths for user edits. 2021-04-03 15:02:29 +01:00
Paul
7f5d6f2312 Format code. 2021-04-03 14:44:01 +01:00
Paul
9492e145f9 User status / profile change route, username change route. 2021-04-03 14:41:40 +01:00
Paul
78c42fd412 Fetch profile route, push badges and status. 2021-04-02 21:55:02 +01:00
Paul
3797878d20 Fixes bug with distributed nodes. 2021-04-02 13:25:51 +01:00
Paul
d2bf477fd8 Expose voso host. 2021-04-01 15:11:19 +01:00
Martin Loffler
c9bff789de Fixed room creation failure in join_call 2021-04-01 15:59:06 +02:00
Paul
56bd763820 Prevent setting null in edit. Preflight for variables. 2021-04-01 14:01:07 +01:00
Paul
32cd9d8a13 Strict typing for system messages; add a way to rename group. 2021-03-31 20:54:47 +01:00
Paul
60731e1c70 Integrate voso, join_call route for channels. 2021-03-31 11:48:52 +01:00
Paul
37df18dbf7 Fixes mutual connections not being seen and hence requests failing. 2021-03-30 22:09:36 +01:00
Paul
0a9d5de369 Don't unwrap on subscription errors. 2021-03-29 10:14:25 +01:00
Paul
d2864ac025 Make sure to check that they are friends. 2021-02-19 15:49:55 +00:00
Paul
4eb76fd154 Fix mutex issues. 2021-02-19 15:24:19 +00:00
Paul
3c7852271a Add temporary typing indicator impl. 2021-02-19 14:50:23 +00:00
Paul
dadad271b4 Mark attachments as deleted when deleting messages. 2021-02-19 14:12:21 +00:00
Paul
db6047f2d3 Allow empty messages with attachment. 2021-02-19 14:09:19 +00:00
Paul
5baf85a8e9 JSON system messages. 2021-02-19 14:03:41 +00:00
Paul
10cac358a9 Label permission errors, and too many ids for /stale. 2021-02-19 13:26:04 +00:00
Paul
64d2707366 Find mutual for users; allow dots in usernames. 2021-02-19 13:11:33 +00:00
Paul
78cfbf9d21 Clean up subscription code; handle error properly. 2021-02-19 12:48:52 +00:00
Paul
02077a4024 Check if online. 2021-02-18 16:47:59 +00:00
Paul
52e070c21c Basic Web Push support. 2021-02-18 16:21:34 +00:00
Paul Makles
a2f14d2d37 Bump version. 2021-02-16 15:26:31 +00:00
Paul Makles
a7ea29d821 Add a way to send messages with attachments. 2021-02-16 15:25:33 +00:00
Paul Makles
b6b51bca26 Route which lets clients determine messages that have updated / deleted. 2021-02-16 09:34:24 +00:00
Paul Makles
2bafffbbc1 Case-insensitive search for emails. 2021-02-14 16:28:23 +00:00
Paul Makles
bdf5fb95d0 Add rAuth captcha support. 2021-02-13 20:18:38 +00:00
Paul Makles
ea8de2400a Add support for rAuth invite-only mode. 2021-02-12 20:37:24 +00:00
Paul Makles
f5e0b49bbb Update rAuth, add templates; add APP_URL variable. 2021-02-12 15:09:42 +00:00
Paul Makles
b0d07ec58f Include DM users in payload; fix permission query. + cargo fmt 2021-02-09 16:41:08 +00:00
Paul Makles
4c99d079e0 Remove ViewAll; check for mutual DMs. 2021-02-09 15:51:35 +00:00
Paul Makles
38bedbaaea Add Gravatar support. 2021-02-02 19:09:13 +00:00
Paul Makles
ea276c3517 Fix assets fr this time 2021-01-30 18:40:58 +00:00
Paul Makles
648e33a13e Include assets in docker image. 2021-01-30 18:23:23 +00:00
Paul Makles
a76ec207b3 Add Prometheus metrics. 2021-01-29 21:56:37 +00:00
Paul Makles
687b300bfc Include assets in Docker image. 2021-01-28 18:51:46 +00:00
Paul Makles
6586d8f02c Fix SSL certificates in container. 2021-01-27 19:37:21 +00:00
Paul Makles
a8a8c08c29 Update Docker image and ports. 2021-01-27 18:31:27 +00:00
Paul Makles
1806ea824b Bump rAuth revision. 2021-01-27 11:08:37 +00:00
Paul Makles
222a417fff Add utility function for including relationship on User. 2021-01-26 22:33:14 +00:00
Paul Makles
3b85dcce14 Include group members in payload. 2021-01-26 22:05:32 +00:00
Paul Makles
f42480886b Re-structure Permissions; add perm to view users from mutual groups. 2021-01-26 21:12:23 +00:00
Paul Makles
23ec2d61f1 Push env variables to rAuth + cargo fmt. 2021-01-26 17:29:03 +00:00
Paul Makles
abd8b1821d Reset migrations; [point of no return for old databases] 2021-01-26 09:30:29 +00:00
Paul Makles
75a35831da Update rauth, pin git repositories in Cargo.toml. 2021-01-25 21:35:47 +00:00
Paul Makles
99e2f874a1 $set instead of overwriting object 2021-01-24 10:28:24 +00:00
Paul Makles
11f7092fcd Add last_message to channels, mark DMs as active. 2021-01-24 10:24:44 +00:00
Paul Makles
cb882ce1b2 Block @revolt from being registered. 2021-01-24 09:29:42 +00:00
Paul Makles
5486a68bfa Move group leave into posthandle hook. 2021-01-24 09:20:43 +00:00
Paul Makles
4aaf328435 Include all channels in payload. 2021-01-24 09:20:07 +00:00
Paul Makles
9d251f794b Update tag in enums. 2021-01-19 21:27:18 +00:00
Paul Makles
1d390d483d Add a database migration for channel tag enum. 2021-01-19 21:11:38 +00:00
Paul Makles
81de29e723 Backtracking again, and adding the channel_type tag. 2021-01-19 20:55:25 +00:00
Paul Makles
984017eb61 Revert tag change, update partials to include ids. 2021-01-19 20:18:29 +00:00
Paul Makles
5ab329dfdd Prevent fetching messages from other channels. Change channel tag to channel_type. 2021-01-19 19:54:37 +00:00
Paul Makles
3a63d502d9 Cargo fmt. 2021-01-19 13:07:11 +00:00
Paul Makles
8bb694a1c8 Post-handle hook, partial updates and group delete. 2021-01-19 13:06:22 +00:00
Paul Makles
c38977e026 Run cargo fmt. 2021-01-19 11:47:36 +00:00
Paul Makles
8cbba0671d Move new subscriptions to a unfied prehandle function. 2021-01-19 11:46:22 +00:00
Paul Makles
c21d7c4620 Group member add and remove routes. 2021-01-19 09:02:18 +00:00
Paul Makles
901b29f49e Run cargo fmt 2021-01-18 22:02:46 +00:00
Paul Makles
34ac8f54ef Add group creation. Sync channels to clients on creation. 2021-01-18 22:02:32 +00:00
Paul Makles
c562d33c8f Add message delete route. 2021-01-18 21:02:04 +00:00
Paul Makles
accd6d7789 Add edit message route. 2021-01-18 20:54:08 +00:00
Paul Makles
ba08746e92 Rename fetch to query, add fetch by id. 2021-01-18 20:30:55 +00:00
Paul Makles
a1a921bbcb Run cargo fmt. 2021-01-18 20:26:26 +00:00
Paul Makles
bd789b6825 Fetch messages route. 2021-01-18 20:26:09 +00:00
Paul Makles
c6ba72d924 Add message sending. 2021-01-18 20:07:42 +00:00
Paul Makles
c401663c47 Catch database error for send message. 2021-01-18 14:52:15 +00:00
Paul Makles
3d3db80e61 Channel subscription, message sending, channel delete. 2021-01-18 14:50:17 +00:00
Paul Makles
15357008d6 Push relevant channels in Ready payload. 2021-01-18 14:14:56 +00:00
Paul Makles
124aa8a9bf Fix user permissions. 2021-01-18 14:01:27 +00:00
Paul Makles
af8731ac38 Start development sprint 3. 2021-01-18 13:41:48 +00:00
Paul Makles
a9f258de6b Randomly select an avatar from id. 2021-01-16 11:17:18 +00:00
Paul Makles
f6c52de171 Add basic presence tracking. 2021-01-09 20:49:36 +00:00
Paul Makles
5e70ceea01 Write proper user permission code. 2021-01-09 16:50:11 +00:00
Paul Makles
bb3667a83b Move guard and reference code out of entities folder. 2021-01-09 13:59:45 +00:00
Paul Makles
80e3baaa15 Clean up imports for database. 2021-01-09 13:57:37 +00:00
Paul Makles
0d3ef9a3b7 Refractor WS; remove - fom names. 2021-01-09 12:58:15 +00:00
insert
4f9029f7b9 Merge branch 'master' into 'master'
Code cleanup/formatting

See merge request revolt/delta!2
2021-01-05 14:29:42 +00:00
Martin Loffler
bb73f905e6 Formatted code 2021-01-05 15:01:24 +01:00
Martin Loffler
66a2930a6f Code cleanup
Cleaned up
- matches in routes remove_friend and unblock_user
- production warnings in util/variables
2021-01-05 14:49:56 +01:00
Martin Loffler
f24d478454 async_std main attribute
Closes #1
2021-01-04 15:01:18 +01:00
Paul Makles
1b711a88ef Include relationship when fetching a user. 2021-01-03 20:27:23 +00:00
Paul Makles
b87f396f40 Make ready payload send all users we have a relationship with. 2021-01-03 17:43:20 +00:00
Paul Makles
ffff620508 Advertise WebSocket address at root. Add REVOLT_EXTERNAL_WS_URL. 2020-12-31 16:05:39 +00:00
82 changed files with 4290 additions and 1110 deletions

View File

@@ -1,3 +1,3 @@
assets
target
.mongo
.env

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"rust-analyzer.diagnostics.disabled": [
"unresolved-macro-call"
]
}

783
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +1,48 @@
[package]
name = "revolt"
version = "0.3.0-alpha"
# To help optimise CI and Docker builds.
# Version here is left as 0.0.0, please
# adjust and run ./set_version.sh instead.
version = "0.0.0"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
futures = "0.3.8"
many-to-many = "0.1.2"
impl_ops = "0.1.1"
ctrlc = { version = "3.0", features = ["termination"] }
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rauth = { git = "https://gitlab.insrt.uk/insert/rauth" }
async-std = { version = "1.8.0", features = ["tokio02"] }
hive_pubsub = { version = "0.4.3", features = ["mongo"] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", branch = "master" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", branch = "master", default-features = false }
# ! FIXME: Switch to async-std runtime.
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
once_cell = "1.4.1"
dotenv = "0.15.0"
regex = "1"
md5 = "0.7.0"
log = "0.4.11"
ulid = "0.4.1"
rand = "0.7.3"
time = "0.2.16"
base64 = "0.13.0"
linkify = "0.6.0"
dotenv = "0.15.0"
futures = "0.3.8"
chrono = "0.4.15"
num_enum = "0.5.1"
impl_ops = "0.1.1"
web-push = "0.7.2"
once_cell = "1.4.1"
bitfield = "0.13.2"
env_logger = "0.7.1"
serde_json = "1.0.57"
lazy_static = "1.4.0"
urlencoding = "1.1.1"
many-to-many = "0.1.2"
lettre = "0.10.0-alpha.1"
snafu = { version = "0.6.9" }
reqwest = { version = "0.10.8", features = ["json"] }
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.11", features = ["derive"] }
snafu = { version = "0.6.9" }
serde_json = "1.0.57"
bitfield = "0.13.2"
reqwest = { version = "0.10.8", features = ["json"] }
lazy_static = "1.4.0"
num_enum = "0.5.1"
chrono = "0.4.15"
time = "0.2.16"
rand = "0.7.3"
regex = "1"
lettre = "0.10.0-alpha.1"
env_logger = "0.7.1"
log = "0.4.11"
ctrlc = { version = "3.0", features = ["termination"] }
hive_pubsub = { version = "0.4.4", features = ["mongo"] }
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" }
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "c52758a5087cd035b0ed9c6eacc942ba5468d2ce" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false }

View File

@@ -1,16 +1,25 @@
# Build Stage
FROM ekidd/rust-musl-builder:nightly-2020-11-19 AS builder
FROM ekidd/rust-musl-builder:nightly-2021-01-01 AS builder
USER 0:0
WORKDIR /home/rust/src
RUN USER=root cargo new --bin revolt
WORKDIR ./revolt
WORKDIR /home/rust/src/revolt
COPY Cargo.toml Cargo.lock ./
COPY src/bin/dummy.rs ./src/bin/dummy.rs
RUN cargo build --release --bin dummy
COPY assets/templates ./assets/templates
COPY src ./src
RUN cargo build --release
# Bundle Stage
FROM scratch
FROM alpine:latest
RUN apk update && apk add ca-certificates && rm -rf /var/cache/apk/*
COPY --from=builder /home/rust/src/revolt/target/x86_64-unknown-linux-musl/release/revolt ./
COPY assets ./assets
EXPOSE 8000
EXPOSE 9000
ENV ROCKET_ADDRESS 0.0.0.0
ENV ROCKET_PORT 8000
CMD ["./revolt"]

View File

@@ -0,0 +1,62 @@
<!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 role="presentation" 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 style="margin: 0;">
Reset your password by <a href="{{url}}">clicking here</a>.
</p>
<p>
Or by manually navigating to the URL: {{url}}
</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; <a style="color: white;" href="https://revolt.chat">Website</a> &middot; <a style="color: white;" href="https://gitlab.insrt.uk/revolt">Source Code</a></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,62 @@
<!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 role="presentation" 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;">Verify your account!</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 style="margin: 0;">
Please verify your account by <a href="{{url}}">clicking here</a>.
</p>
<p>
Or by manually navigating to the URL: {{url}}
</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; <a style="color: white;" href="https://revolt.chat">Website</a> &middot; <a style="color: white;" href="https://gitlab.insrt.uk/revolt">Source Code</a></p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

BIN
assets/user_blue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/user_green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
assets/user_red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
assets/user_yellow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -14,5 +14,5 @@ services:
- REVOLT_UNSAFE_NO_CAPTCHA=1
ports:
- "8000:8000"
- "9999:9999"
- "9000:9000"
restart: unless-stopped

5
publish.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
source set_version.sh
docker build -t revoltchat/server:${version} . &&
docker push revoltchat/server:${version}

3
set_version.sh Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
export version=0.4.1-alpha.11
echo "pub const VERSION: &str = \"${version}\";" > src/version.rs

1
src/bin/dummy.rs Normal file
View File

@@ -0,0 +1 @@
fn main() {}

View File

@@ -0,0 +1,115 @@
use mongodb::bson::{doc, from_document};
use serde::{Deserialize, Serialize};
use crate::database::*;
use crate::util::result::{Error, Result};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
enum Metadata {
File,
Text,
Image { width: isize, height: isize },
Video { width: isize, height: isize },
Audio,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct File {
#[serde(rename = "_id")]
pub id: String,
tag: String,
filename: String,
metadata: Metadata,
content_type: String,
size: isize,
#[serde(skip_serializing_if = "Option::is_none")]
deleted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
object_id: Option<String>,
}
impl File {
pub async fn find_and_use(
attachment_id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
) -> Result<File> {
let attachments = get_collection("attachments");
let key = format!("{}_id", parent_type);
if let Some(doc) = attachments
.find_one(
doc! {
"_id": attachment_id,
"tag": &tag,
key.clone(): {
"$exists": false
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "attachment",
})?
{
let attachment = from_document::<File>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "attachment",
})?;
attachments
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
key: &parent_id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
Ok(attachment)
} else {
Err(Error::UnknownAttachment)
}
}
pub async fn delete(&self) -> Result<()> {
get_collection("attachments")
.update_one(
doc! {
"_id": &self.id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})
}
}

View File

@@ -1,42 +1,24 @@
use crate::{
database::get_collection,
util::result::{Error, Result},
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::{
bson::{doc, from_document, to_document, Document},
options::FindOptions,
};
use mongodb::bson::to_document;
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
/*#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LastMessage {
#[serde(rename = "_id")]
id: String,
user_id: String,
short_content: String,
author: String,
short: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Channel {
#[serde(rename = "_id")]
pub id: String,
#[serde(rename = "type")]
pub channel_type: u8,
// DM: whether the DM is active
pub active: Option<bool>,
// DM + GDM: last message in channel
pub last_message: Option<LastMessage>,
// DM + GDM: recipients for channel
pub recipients: Option<Vec<String>>,
// GDM: owner of group
pub owner: Option<String>,
// GUILD: channel parent
pub guild: Option<String>,
// GUILD + GDM: channel name
pub name: Option<String>,
// GUILD + GDM: channel description
pub description: Option<String>,
}*/
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
#[serde(tag = "channel_type")]
pub enum Channel {
SavedMessages {
#[serde(rename = "_id")]
@@ -46,21 +28,56 @@ pub enum Channel {
DirectMessage {
#[serde(rename = "_id")]
id: String,
active: bool,
recipients: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
last_message: Option<LastMessage>,
},
Group {
#[serde(rename = "_id")]
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<String>,
name: String,
owner: String,
description: String,
recipients: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
last_message: Option<LastMessage>,
},
}
impl Channel {
pub async fn save(&self) -> Result<()> {
pub fn id(&self) -> &str {
match self {
Channel::SavedMessages { id, .. }
| Channel::DirectMessage { id, .. }
| Channel::Group { id, .. } => id,
}
}
pub async fn get(id: &str) -> Result<Channel> {
let doc = get_collection("channels")
.find_one(doc! { "_id": id }, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "channel",
})?
.ok_or_else(|| Error::UnknownChannel)?;
from_document::<Channel>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "channel",
})
}
pub async fn publish(self) -> Result<()> {
get_collection("channels")
.insert_one(
to_document(&self).map_err(|_| Error::DatabaseError {
@@ -75,6 +92,109 @@ impl Channel {
with: "channel",
})?;
let channel_id = self.id().to_string();
ClientboundNotification::ChannelCreate(self).publish(channel_id);
Ok(())
}
pub async fn publish_update(&self, data: JsonValue) -> Result<()> {
let id = self.id().to_string();
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data,
clear: None,
}
.publish(id);
Ok(())
}
pub async fn delete(&self) -> Result<()> {
let id = self.id();
let messages = get_collection("messages");
// Check if there are any attachments we need to delete.
let message_ids = messages
.find(
doc! {
"channel": id,
"attachment": {
"$exists": 1
}
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "fetch_many",
with: "messages",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
// If we found any, mark them as deleted.
if message_ids.len() > 0 {
get_collection("attachments")
.update_many(
doc! {
"message_id": {
"$in": message_ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "attachments",
})?;
}
// And then delete said messages.
messages
.delete_many(
doc! {
"channel": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "messages",
})?;
get_collection("channels")
.delete_one(
doc! {
"_id": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "channel",
})?;
ClientboundNotification::ChannelDelete { id: id.to_string() }.publish(id.to_string());
if let Channel::Group { icon, .. } = self {
if let Some(attachment) = icon {
attachment.delete().await?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,144 @@
use crate::util::{
result::{Error, Result},
variables::JANUARY_URL,
};
use linkify::{LinkFinder, LinkKind};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ImageSize {
Large,
Preview,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Image {
pub url: String,
pub width: isize,
pub height: isize,
pub size: ImageSize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Video {
pub url: String,
pub width: isize,
pub height: isize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum TwitchType {
Channel,
Video,
Clip,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum BandcampType {
Album,
Track,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Special {
None,
YouTube {
id: String,
},
Twitch {
content_type: TwitchType,
id: String,
},
Spotify {
content_type: String,
id: String,
},
Soundcloud,
Bandcamp {
content_type: BandcampType,
id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Metadata {
url: Option<String>,
special: Option<Special>,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<Image>,
#[serde(skip_serializing_if = "Option::is_none")]
video: Option<Video>,
// #[serde(skip_serializing_if = "Option::is_none")]
// opengraph_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
site_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
color: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Embed {
Website(Metadata),
Image(Image),
None,
}
impl Embed {
pub async fn generate(content: String) -> Result<Vec<Embed>> {
let content = content
.split("\n")
.map(|v| {
// Ignore quoted lines.
if let Some(c) = v.chars().next() {
if c == '>' {
return "";
}
}
v
})
.collect::<Vec<&str>>()
.join("\n");
// ! FIXME: allow multiple links
// ! FIXME: prevent generation if link is surrounded with < >
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
let links: Vec<_> = finder.links(&content).collect();
if links.len() == 0 {
return Err(Error::LabelMe);
}
let link = &links[0];
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/embed", *JANUARY_URL))
.query(&[("url", link.as_str())])
.send()
.await;
match result {
Err(_) => return Err(Error::LabelMe),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => {
let res: Embed = result.json().await.map_err(|_| Error::InvalidOperation)?;
Ok(vec![res])
}
_ => return Err(Error::LabelMe),
},
}
}
}

View File

@@ -1,22 +1,321 @@
// use mongodb::bson::DateTime;
// use serde::{Deserialize, Serialize};
use crate::util::variables::{VAPID_PRIVATE_KEY, USE_JANUARY};
use crate::{
database::*,
notifications::{events::ClientboundNotification, websocket::is_online},
util::result::{Error, Result},
};
/*#[derive(Serialize, Deserialize, Debug)]
pub struct PreviousEntry {
pub content: String,
pub time: DateTime,
use futures::StreamExt;
use mongodb::{
bson::{doc, to_bson, DateTime},
options::FindOptions,
};
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use web_push::{
ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum SystemMessage {
#[serde(rename = "text")]
Text { content: String },
#[serde(rename = "user_added")]
UserAdded { id: String, by: String },
#[serde(rename = "user_remove")]
UserRemove { id: String, by: String },
#[serde(rename = "user_left")]
UserLeft { id: String },
#[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String },
#[serde(rename = "channel_description_changed")]
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Content {
Text(String),
SystemMessage(SystemMessage),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
#[serde(rename = "_id")]
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
pub channel: String,
pub author: String,
pub content: String,
pub content: Content,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachment: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<DateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>,
}
pub previous_content: Vec<PreviousEntry>,
}*/
impl Message {
pub fn create(author: String, channel: String, content: Content) -> Message {
Message {
id: Ulid::new().to_string(),
nonce: None,
channel,
author,
content,
attachment: None,
edited: None,
embeds: None,
}
}
pub async fn publish(self, channel: &Channel) -> Result<()> {
get_collection("messages")
.insert_one(to_bson(&self).unwrap().as_document().unwrap().clone(), None)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "message",
})?;
let mut set = if let Content::Text(text) = &self.content {
doc! {
"last_message": {
"_id": self.id.clone(),
"author": self.author.clone(),
"short": text.chars().take(24).collect::<String>()
}
}
} else {
doc! {}
};
// ! FIXME: temp code
let channels = get_collection("channels");
match &channel {
Channel::DirectMessage { id, .. } => {
set.insert("active", true);
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": set
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
Channel::Group { id, .. } => {
if let Content::Text(_) = &self.content {
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": set
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
}
_ => {}
}
self.process_embed();
let enc = serde_json::to_string(&self).unwrap();
ClientboundNotification::Message(self).publish(channel.id().to_string());
/*
Web Push Test Code
! FIXME: temp code
*/
// Find all offline users.
let mut target_ids = vec![];
match &channel {
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
for recipient in recipients {
if !is_online(recipient) {
target_ids.push(recipient.clone());
}
}
}
_ => {}
}
async_std::task::spawn(async move {
// Fetch their corresponding sessions.
if let Ok(mut cursor) = get_collection("accounts")
.find(
doc! {
"_id": {
"$in": target_ids
},
"sessions.subscription": {
"$exists": true
}
},
FindOptions::builder()
.projection(doc! { "sessions": 1 })
.build(),
)
.await
{
let mut subscriptions = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(sessions) = doc.get_array("sessions") {
for session in sessions {
if let Some(doc) = session.as_document() {
if let Ok(sub) = doc.get_document("subscription") {
let endpoint = sub.get_str("endpoint").unwrap().to_string();
let p256dh = sub.get_str("p256dh").unwrap().to_string();
let auth = sub.get_str("auth").unwrap().to_string();
subscriptions
.push(SubscriptionInfo::new(endpoint, p256dh, auth));
}
}
}
}
}
}
if subscriptions.len() > 0 {
let client = WebPushClient::new();
let key =
base64::decode_config(VAPID_PRIVATE_KEY.clone(), base64::URL_SAFE).unwrap();
for subscription in subscriptions {
let mut builder = WebPushMessageBuilder::new(&subscription).unwrap();
let sig_builder = VapidSignatureBuilder::from_pem(
std::io::Cursor::new(&key),
&subscription,
)
.unwrap();
let signature = sig_builder.build().unwrap();
builder.set_vapid_signature(signature);
builder.set_payload(ContentEncoding::AesGcm, enc.as_bytes());
let m = builder.build().unwrap();
client.send(m).await.ok();
}
}
}
});
Ok(())
}
pub async fn publish_update(self, data: JsonValue) -> Result<()> {
let channel = self.channel.clone();
ClientboundNotification::MessageUpdate {
id: self.id.clone(),
data,
}
.publish(channel);
self.process_embed();
Ok(())
}
pub fn process_embed(&self) {
if !*USE_JANUARY {
return;
}
if let Content::Text(text) = &self.content {
let id = self.id.clone();
let content = text.clone();
let channel = self.channel.clone();
async_std::task::spawn(async move {
if let Ok(embeds) = Embed::generate(content).await {
if let Ok(bson) = to_bson(&embeds) {
if let Ok(_) = get_collection("messages")
.update_one(
doc! {
"_id": &id
},
doc! {
"$set": {
"embeds": bson
}
},
None,
)
.await
{
ClientboundNotification::MessageUpdate {
id,
data: json!({ "embeds": embeds }),
}
.publish(channel);
}
}
}
});
}
}
pub async fn delete(&self) -> Result<()> {
if let Some(attachment) = &self.attachment {
attachment.delete().await?;
}
get_collection("messages")
.delete_one(
doc! {
"_id": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "message",
})?;
let channel = self.channel.clone();
ClientboundNotification::MessageDelete {
id: self.id.clone(),
}
.publish(channel);
if let Some(attachment) = &self.attachment {
get_collection("attachments")
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
}
Ok(())
}
}

View File

@@ -1,9 +1,13 @@
mod autumn;
mod channel;
mod guild;
mod january;
mod message;
mod server;
mod user;
pub use autumn::*;
pub use channel::*;
pub use guild::*;
pub use january::*;
pub use message::*;
pub use server::*;
pub use user::*;

View File

@@ -1,6 +1,6 @@
// use serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize};
/*#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MemberCompositeKey {
pub guild: String,
pub user: String,
@@ -27,12 +27,11 @@ pub struct Ban {
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Guild {
pub struct Server {
#[serde(rename = "_id")]
pub id: String,
// pub nonce: String, used internally
pub name: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
pub owner: String,
pub channels: Vec<String>,
@@ -40,4 +39,4 @@ pub struct Guild {
pub bans: Vec<Ban>,
pub default_permissions: u32,
}*/
}

View File

@@ -1,12 +1,18 @@
use crate::database::get_collection;
use crate::database::guards::reference::Ref;
use mongodb::bson::{doc, from_bson, Bson};
use rauth::auth::Session;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
use futures::StreamExt;
use mongodb::options::{Collation, FindOneOptions};
use mongodb::{
bson::{doc, from_document},
options::FindOptions,
};
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Serialize, Deserialize, Debug, Clone)]
use crate::database::permissions::user::UserPermissions;
use crate::database::*;
use crate::notifications::websocket::is_online;
use crate::util::result::{Error, Result};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus {
None,
User,
@@ -24,49 +30,180 @@ pub struct Relationship {
pub status: RelationshipStatus,
}
/*
pub enum Badge {
Developer = 1,
Translator = 2,
}
*/
#[derive(Serialize, Deserialize, Debug)]
pub enum Presence {
Online,
Idle,
Busy,
Invisible,
}
#[derive(Validate, Serialize, Deserialize, Debug)]
pub struct UserStatus {
#[validate(length(min = 1, max = 128))]
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence: Option<Presence>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UserProfile {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<File>,
}
// When changing this struct, update notifications/payload.rs#80
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
#[serde(rename = "_id")]
pub id: String,
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relations: Option<Vec<Relationship>>,
}
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = rauth::util::Error;
#[serde(skip_serializing_if = "Option::is_none")]
pub badges: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<UserStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<UserProfile>,
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let session: Session = try_outcome!(request.guard::<Session>().await);
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &session.user_id
},
None,
)
.await
{
if let Some(doc) = result {
Outcome::Success(from_bson(Bson::Document(doc)).unwrap())
} else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
}
} else {
Outcome::Failure((
Status::InternalServerError,
rauth::util::Error::DatabaseError,
))
}
}
// ? This should never be pushed to the collection.
#[serde(skip_serializing_if = "Option::is_none")]
pub relationship: Option<RelationshipStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub online: Option<bool>,
}
impl User {
pub fn as_ref(&self) -> Ref {
Ref {
id: self.id.to_string(),
/// Mutate the user object to include relationship as seen by user.
pub fn from(mut self, user: &User) -> User {
self.relationship = Some(RelationshipStatus::None);
if self.id == user.id {
self.relationship = Some(RelationshipStatus::User);
return self;
}
self.relations = None;
if let Some(relations) = &user.relations {
if let Some(relationship) = relations.iter().find(|x| self.id == x.id) {
self.relationship = Some(relationship.status.clone());
return self;
}
}
self
}
/// Mutate the user object to appear as seen by user.
pub fn with(mut self, permissions: UserPermissions<[u32; 1]>) -> User {
if permissions.get_view_profile() {
self.online = Some(is_online(&self.id));
} else {
self.status = None;
}
self.profile = None;
self
}
/// Mutate the user object to appear as seen by user.
/// Also overrides the relationship status.
pub async fn from_override(
mut self,
user: &User,
relationship: RelationshipStatus,
) -> Result<User> {
let permissions = PermissionCalculator::new(&user)
.with_relationship(&relationship)
.for_user(&self.id)
.await?;
self.relations = None;
self.relationship = Some(relationship);
Ok(self.with(permissions))
}
/// Utility function for checking claimed usernames.
pub async fn is_username_taken(username: &str) -> Result<bool> {
if username.to_lowercase() == "revolt" && username.to_lowercase() == "admin" {
return Ok(true);
}
if get_collection("users")
.find_one(
doc! {
"username": username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.is_some()
{
Ok(true)
} else {
Ok(false)
}
}
/// Utility function for fetching multiple users from the perspective of one.
pub async fn fetch_multiple_users(&self, user_ids: Vec<String>) -> Result<Vec<User>> {
let mut users = vec![];
let mut cursor = get_collection("users")
.find(
doc! {
"_id": {
"$in": user_ids
}
},
FindOptions::builder()
.projection(
doc! { "_id": 1, "username": 1, "avatar": 1, "badges": 1, "status": 1 },
)
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let other: User = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "user",
})?;
let permissions = PermissionCalculator::new(&self)
.with_mutual_connection()
.with_user(&other)
.for_user_given()
.await?;
users.push(other.from(&self).with(permissions));
}
}
Ok(users)
}
}

View File

@@ -1,2 +1,4 @@
pub mod reference;
pub mod user;
pub use reference::Ref;

View File

@@ -1,10 +1,10 @@
use crate::database::entities::*;
use crate::database::get_collection;
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, from_bson, Bson};
use mongodb::bson::{doc, from_document};
use rocket::http::RawStr;
use rocket::request::FromParam;
use serde::{Deserialize, Serialize};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
@@ -18,8 +18,8 @@ impl Ref {
Ok(Ref { id })
}
pub async fn fetch_user(&self) -> Result<User> {
let doc = get_collection("users")
pub async fn fetch<T: DeserializeOwned>(&self, collection: &'static str) -> Result<T> {
let doc = get_collection(&collection)
.find_one(
doc! {
"_id": &self.id
@@ -29,16 +29,39 @@ impl Ref {
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
with: &collection,
})?
.ok_or_else(|| Error::UnknownUser)?;
Ok(
from_bson(Bson::Document(doc)).map_err(|_| Error::DatabaseError {
operation: "from_bson",
with: "user",
})?,
)
Ok(from_document::<T>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: &collection,
})?)
}
pub async fn fetch_user(&self) -> Result<User> {
self.fetch("users").await
}
pub async fn fetch_channel(&self) -> Result<Channel> {
self.fetch("channels").await
}
pub async fn fetch_message(&self, channel: &Channel) -> Result<Message> {
let message: Message = self.fetch("messages").await?;
if &message.channel != channel.id() {
Err(Error::InvalidOperation)
} else {
Ok(message)
}
}
}
impl User {
pub fn as_ref(&self) -> Ref {
Ref {
id: self.id.to_string(),
}
}
}

View File

@@ -1 +1,39 @@
use crate::database::*;
use mongodb::bson::{doc, from_document};
use rauth::auth::Session;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
#[rocket::async_trait]
impl<'a, 'r> FromRequest<'a, 'r> for User {
type Error = rauth::util::Error;
async fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let session: Session = try_outcome!(request.guard::<Session>().await);
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &session.user_id
},
None,
)
.await
{
if let Some(doc) = result {
Outcome::Success(from_document(doc).unwrap())
} else {
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
}
} else {
Outcome::Failure((
Status::InternalServerError,
rauth::util::Error::DatabaseError {
operation: "find_one",
with: "user",
},
))
}
}
}

View File

@@ -9,6 +9,10 @@ pub async fn create_database() {
info!("Creating database.");
let db = get_db();
db.create_collection("accounts", None)
.await
.expect("Failed to create accounts collection.");
db.create_collection("users", None)
.await
.expect("Failed to create users collection.");
@@ -17,22 +21,22 @@ pub async fn create_database() {
.await
.expect("Failed to create channels collection.");
db.create_collection("guilds", None)
.await
.expect("Failed to create guilds collection.");
db.create_collection("members", None)
.await
.expect("Failed to create members collection.");
db.create_collection("messages", None)
.await
.expect("Failed to create messages collection.");
db.create_collection("servers", None)
.await
.expect("Failed to create servers collection.");
db.create_collection("migrations", None)
.await
.expect("Failed to create migrations collection.");
db.create_collection("attachments", None)
.await
.expect("Failed to create attachments collection.");
db.create_collection(
"pubsub",
CreateCollectionOptions::builder()
@@ -43,6 +47,39 @@ pub async fn create_database() {
.await
.expect("Failed to create pubsub collection.");
db.run_command(
doc! {
"createIndexes": "accounts",
"indexes": [
{
"key": {
"email": 1
},
"name": "email",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
},
{
"key": {
"email_normalised": 1
},
"name": "email_normalised",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
}
]
},
None,
)
.await
.expect("Failed to create account index.");
db.run_command(
doc! {
"createIndexes": "users",

View File

@@ -1,9 +1,7 @@
use super::super::{get_collection, get_db};
use crate::database::{get_collection, get_db};
use crate::rocket::futures::StreamExt;
use log::info;
use mongodb::bson::{doc, from_bson, Bson};
use mongodb::options::FindOptions;
use mongodb::bson::{doc, from_document};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
@@ -23,7 +21,7 @@ pub async fn migrate_database() {
if let Some(doc) = data {
let info: MigrationInfo =
from_bson(Bson::Document(doc)).expect("Failed to read migration information.");
from_document(doc).expect("Failed to read migration information.");
let revision = run_migrations(info.revision).await;
@@ -56,96 +54,37 @@ pub async fn run_migrations(revision: i32) -> i32 {
}
if revision <= 1 {
info!("Running migration [revision 1]: Add channels to guild object.");
info!("Running migration [revision 1 / 2021-04-24]: Migrate to Autumn v1.0.0.");
let col = get_collection("guilds");
let mut guilds = col
.find(
None,
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.expect("Failed to fetch guilds.");
let messages = get_collection("messages");
let attachments = get_collection("attachments");
let mut result = get_collection("channels")
.find(
doc! {
"type": 2
},
FindOptions::builder()
.projection(doc! { "_id": 1, "guild": 1 })
.build(),
)
.await
.expect("Failed to fetch channels.");
let mut channels = vec![];
while let Some(doc) = result.next().await {
let channel = doc.expect("Failed to fetch channel.");
let id = channel
.get_str("_id")
.expect("Failed to get channel id.")
.to_string();
let gid = channel
.get_str("guild")
.expect("Failed to get guild id.")
.to_string();
channels.push((id, gid));
}
while let Some(doc) = guilds.next().await {
let guild = doc.expect("Failed to fetch guild.");
let id = guild.get_str("_id").expect("Failed to get guild id.");
let list: Vec<String> = channels
.iter()
.filter(|x| x.1 == id)
.map(|x| x.0.clone())
.collect();
col.update_one(
doc! {
"_id": id
},
doc! {
"$set": {
"channels": list
}
},
messages
.update_many(
doc! { "attachment": { "$exists": 1 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0 } },
None,
)
.await
.expect("Failed to update guild.");
}
.expect("Failed to update messages.");
attachments
.update_many(
doc! {},
doc! { "$set": { "tag": "attachments", "size": 0 } },
None,
)
.await
.expect("Failed to update attachments.");
}
if revision <= 2 {
info!("Running migration [revision 2]: Add username index to users.");
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
get_db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1
},
"name": "username",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
}
]
},
None,
)
.create_collection("servers", None)
.await
.expect("Failed to create username index.");
.expect("Failed to create servers collection.");
}
// Reminder to update LATEST_REVISION when adding new migrations.

View File

@@ -30,3 +30,7 @@ pub mod entities;
pub mod guards;
pub mod migrations;
pub mod permissions;
pub use entities::*;
pub use guards::*;
pub use permissions::*;

View File

@@ -0,0 +1,90 @@
use crate::database::*;
use crate::util::result::Result;
use super::PermissionCalculator;
use num_enum::TryFromPrimitive;
use std::ops;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum ChannelPermission {
View = 1,
SendMessage = 2,
ManageMessages = 4,
ManageChannel = 8,
VoiceCall = 16,
}
bitfield! {
pub struct ChannelPermissions(MSB0 [u32]);
u32;
pub get_view, _: 31;
pub get_send_message, _: 30;
pub get_manage_messages, _: 29;
pub get_manage_channel, _: 28;
pub get_voice_call, _: 27;
}
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &ChannelPermission| -> u32 { *a | *b as u32 });
impl<'a> PermissionCalculator<'a> {
pub async fn calculate_channel(self) -> Result<u32> {
let channel = if let Some(channel) = self.channel {
channel
} else {
unreachable!()
};
match channel {
Channel::SavedMessages { user: owner, .. } => {
if &self.perspective.id == owner {
Ok(u32::MAX - ChannelPermission::VoiceCall as u32)
} else {
Ok(0)
}
}
Channel::DirectMessage { recipients, .. } => {
if recipients
.iter()
.find(|x| *x == &self.perspective.id)
.is_some()
{
if let Some(recipient) = recipients.iter().find(|x| *x != &self.perspective.id)
{
let perms = self.for_user(recipient).await?;
if perms.get_send_message() {
return Ok(ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::VoiceCall);
}
return Ok(ChannelPermission::View as u32);
}
}
Ok(0)
}
Channel::Group { recipients, .. } => {
if recipients
.iter()
.find(|x| *x == &self.perspective.id)
.is_some()
{
Ok(ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::ManageChannel
+ ChannelPermission::VoiceCall)
} else {
Ok(0)
}
}
}
}
pub async fn for_channel(self) -> Result<ChannelPermissions<[u32; 1]>> {
Ok(ChannelPermissions([self.calculate_channel().await?]))
}
}

View File

@@ -1,49 +1,58 @@
use crate::database::entities::User;
use num_enum::TryFromPrimitive;
use std::ops;
pub use crate::database::*;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum UserPermission {
Access = 1,
SendMessage = 2,
Invite = 4,
pub mod channel;
pub mod user;
pub use user::get_relationship;
pub struct PermissionCalculator<'a> {
perspective: &'a User,
user: Option<&'a User>,
relationship: Option<&'a RelationshipStatus>,
channel: Option<&'a Channel>,
has_mutual_connection: bool,
}
bitfield! {
pub struct UserPermissions(MSB0 [u32]);
u32;
pub get_access, _: 31;
pub get_send_message, _: 30;
pub get_invite, _: 29;
}
impl<'a> PermissionCalculator<'a> {
pub fn new(perspective: &'a User) -> PermissionCalculator {
PermissionCalculator {
perspective,
impl_op_ex!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 });
user: None,
relationship: None,
channel: None,
pub async fn temp_calc_perm(_user: &User, _target: &User) -> UserPermissions<[u32; 1]> {
// if friends; Access + Message + Invite
// if mutually know each other:
// and has DMs from users enabled -> Access + Message
// otherwise -> Access
// otherwise; None
UserPermissions([UserPermission::Access + UserPermission::SendMessage + UserPermission::Invite])
}
use crate::database::entities::RelationshipStatus;
use crate::database::guards::reference::Ref;
pub fn get_relationship(a: &User, b: &Ref) -> RelationshipStatus {
if a.id == b.id {
return RelationshipStatus::Friend;
}
if let Some(relations) = &a.relations {
if let Some(relationship) = relations.iter().find(|x| x.id == b.id) {
return relationship.status.clone();
has_mutual_connection: false,
}
}
RelationshipStatus::None
pub fn with_user(self, user: &'a User) -> PermissionCalculator {
PermissionCalculator {
user: Some(&user),
..self
}
}
pub fn with_relationship(self, relationship: &'a RelationshipStatus) -> PermissionCalculator {
PermissionCalculator {
relationship: Some(&relationship),
..self
}
}
pub fn with_channel(self, channel: &'a Channel) -> PermissionCalculator {
PermissionCalculator {
channel: Some(&channel),
..self
}
}
pub fn with_mutual_connection(self) -> PermissionCalculator<'a> {
PermissionCalculator {
has_mutual_connection: true,
..self
}
}
}

View File

@@ -0,0 +1,107 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use super::PermissionCalculator;
use mongodb::bson::doc;
use num_enum::TryFromPrimitive;
use std::ops;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum UserPermission {
Access = 1,
ViewProfile = 2,
SendMessage = 4,
Invite = 8,
}
bitfield! {
pub struct UserPermissions(MSB0 [u32]);
u32;
pub get_access, _: 31;
pub get_view_profile, _: 30;
pub get_send_message, _: 29;
pub get_invite, _: 28;
}
impl_op_ex!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 });
pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus {
if a.id == b {
return RelationshipStatus::Friend;
}
if let Some(relations) = &a.relations {
if let Some(relationship) = relations.iter().find(|x| x.id == b) {
return relationship.status.clone();
}
}
RelationshipStatus::None
}
impl<'a> PermissionCalculator<'a> {
pub async fn calculate_user(self, target: &str) -> Result<u32> {
if &self.perspective.id == target {
return Ok(u32::MAX);
}
let mut permissions: u32 = 0;
match self
.relationship
.clone()
.map(|v| v.to_owned())
.unwrap_or_else(|| get_relationship(&self.perspective, &target))
{
RelationshipStatus::Friend => return Ok(u32::MAX),
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
return Ok(UserPermission::Access as u32)
}
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
permissions = UserPermission::Access as u32;
// ! INFO: if we add boolean switch for permission to
// ! message people who have mutual, we need to get
// ! rid of this return statement.
return Ok(permissions);
}
_ => {}
}
if self.has_mutual_connection
|| get_collection("channels")
.find_one(
doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ &self.perspective.id, target ]
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "channels",
})?
.is_some()
{
// ! FIXME: add privacy settings
return Ok(UserPermission::Access + UserPermission::ViewProfile);
}
Ok(permissions)
}
pub async fn for_user(self, target: &str) -> Result<UserPermissions<[u32; 1]>> {
Ok(UserPermissions([self.calculate_user(&target).await?]))
}
pub async fn for_user_given(self) -> Result<UserPermissions<[u32; 1]>> {
let id = &self.user.unwrap().id;
Ok(UserPermissions([self.calculate_user(&id).await?]))
}
}

View File

@@ -17,22 +17,33 @@ pub mod database;
pub mod notifications;
pub mod routes;
pub mod util;
pub mod version;
use async_std::task;
use chrono::Duration;
use futures::join;
use log::info;
use rauth;
use rauth::options::{EmailVerification, Options, SMTP};
use rauth::{
auth::Auth,
options::{Template, Templates},
};
use rocket_cors::AllowedOrigins;
use rocket_prometheus::PrometheusMetrics;
use util::variables::{
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, PUBLIC_URL, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD,
SMTP_USERNAME, USE_EMAIL, USE_HCAPTCHA, USE_PROMETHEUS,
};
fn main() {
task::block_on(entry())
}
async fn entry() {
#[async_std::main]
async fn main() {
dotenv::dotenv().ok();
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "info"));
info!("Starting REVOLT server.");
info!(
"Starting REVOLT server [version {}].",
crate::version::VERSION
);
util::variables::preflight_checks();
database::connect().await;
@@ -44,10 +55,13 @@ async fn entry() {
})
.expect("Error setting Ctrl-C handler");
let web_task = task::spawn(launch_web());
let hive_task = task::spawn(notifications::hive::listen());
join!(
launch_web(),
notifications::websocket::launch_server(),
notifications::hive::listen(),
web_task,
hive_task,
notifications::websocket::launch_server()
);
}
@@ -59,10 +73,66 @@ async fn launch_web() {
.to_cors()
.expect("Failed to create CORS.");
let auth = rauth::auth::Auth::new(database::get_collection("accounts"));
let mut options = Options::new()
.base_url(format!("{}/auth", *PUBLIC_URL))
.email_verification(if *USE_EMAIL {
EmailVerification::Enabled {
success_redirect_uri: format!("{}/login", *APP_URL),
welcome_redirect_uri: format!("{}/welcome", *APP_URL),
password_reset_url: Some(format!("{}/login/reset", *APP_URL)),
routes::mount(rauth::routes::mount(rocket::ignite(), "/auth", auth))
verification_expiry: Duration::days(1),
password_reset_expiry: Duration::hours(1),
templates: Templates {
verify_email: Template {
title: "Verify your REVOLT account.",
text: "Verify your email here: {{url}}",
html: include_str!("../assets/templates/verify.html"),
},
reset_password: Template {
title: "Reset your REVOLT password.",
text: "Reset your password here: {{url}}",
html: include_str!("../assets/templates/reset.html"),
},
welcome: None,
},
smtp: SMTP {
from: (*SMTP_FROM).to_string(),
host: (*SMTP_HOST).to_string(),
username: (*SMTP_USERNAME).to_string(),
password: (*SMTP_PASSWORD).to_string(),
},
}
} else {
EmailVerification::Disabled
});
if *INVITE_ONLY {
options = options.invite_only_collection(database::get_collection("invites"))
}
if *USE_HCAPTCHA {
options = options.hcaptcha_secret(HCAPTCHA_KEY.clone());
}
let auth = Auth::new(database::get_collection("accounts"), options);
let mut rocket = rocket::ignite();
if *USE_PROMETHEUS {
info!("Enabled Prometheus metrics!");
let prometheus = PrometheusMetrics::new();
rocket = rocket
.attach(prometheus.clone())
.mount("/metrics", prometheus);
}
routes::mount(rocket)
.mount("/", rocket_cors::catch_all_options_routes())
.mount("/auth", rauth::routes::routes())
.manage(auth)
.manage(cors.clone())
.attach(cors)
.launch()

View File

@@ -1,10 +1,11 @@
use hive_pubsub::PubSub;
use rauth::auth::Session;
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use crate::database::entities::{RelationshipStatus, User};
use super::hive::get_hive;
use super::hive::{get_hive, subscribe_if_exists};
use crate::database::*;
#[derive(Serialize, Deserialize, Debug, Snafu)]
#[serde(tag = "error")]
@@ -12,7 +13,7 @@ pub enum WebSocketError {
#[snafu(display("This error has not been labelled."))]
LabelMe,
#[snafu(display("Internal server error."))]
InternalError,
InternalError { at: String },
#[snafu(display("Invalid session."))]
InvalidSession,
#[snafu(display("User hasn't completed onboarding."))]
@@ -25,6 +26,21 @@ pub enum WebSocketError {
#[serde(tag = "type")]
pub enum ServerboundNotification {
Authenticate(Session),
BeginTyping { channel: String },
EndTyping { channel: String },
}
#[derive(Serialize, Deserialize, Debug)]
pub enum RemoveUserField {
ProfileContent,
ProfileBackground,
StatusText,
Avatar,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum RemoveChannelField {
Icon,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -33,73 +49,120 @@ pub enum ClientboundNotification {
Error(WebSocketError),
Authenticated,
Ready {
user: User
users: Vec<User>,
channels: Vec<Channel>,
},
/*MessageCreate {
Message(Message),
MessageUpdate {
id: String,
nonce: Option<String>,
channel: String,
author: String,
content: String,
data: JsonValue,
},
MessageEdit {
id: String,
channel: String,
author: String,
content: String,
},
MessageDelete {
id: String,
},
GroupUserJoin {
ChannelCreate(Channel),
ChannelUpdate {
id: String,
data: JsonValue,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveChannelField>,
},
ChannelGroupJoin {
id: String,
user: String,
},
ChannelGroupLeave {
id: String,
user: String,
},
ChannelDelete {
id: String,
},
ChannelStartTyping {
id: String,
user: String,
},
ChannelStopTyping {
id: String,
user: String,
},
GroupUserLeave {
UserUpdate {
id: String,
user: String,
data: JsonValue,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveUserField>,
},
GuildUserJoin {
id: String,
user: String,
},
GuildUserLeave {
id: String,
user: String,
banned: bool,
},
GuildChannelCreate {
id: String,
channel: String,
name: String,
description: String,
},
GuildChannelDelete {
id: String,
channel: String,
},
GuildDelete {
id: String,
},*/
UserRelationship {
id: String,
user: String,
user: User,
status: RelationshipStatus,
},
UserPresence {
id: String,
online: bool,
},
}
impl ClientboundNotification {
pub async fn publish(self, topic: String) -> Result<(), String> {
hive_pubsub::backend::mongo::publish(get_hive(), &topic, self).await
pub fn publish(self, topic: String) {
async_std::task::spawn(async move {
prehandle_hook(&self); // ! TODO: this should be moved to pubsub
hive_pubsub::backend::mongo::publish(get_hive(), &topic, self)
.await
.ok();
});
}
}
pub fn prehandle_hook(notification: &ClientboundNotification) {
match &notification {
ClientboundNotification::ChannelGroupJoin { id, user } => {
subscribe_if_exists(user.clone(), id.clone()).ok();
}
ClientboundNotification::ChannelCreate(channel) => {
let channel_id = channel.id();
match &channel {
Channel::SavedMessages { user, .. } => {
subscribe_if_exists(user.clone(), channel_id.to_string()).ok();
}
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
for recipient in recipients {
subscribe_if_exists(recipient.clone(), channel_id.to_string()).ok();
}
}
}
}
ClientboundNotification::UserRelationship { id, user, status } => {
if status != &RelationshipStatus::None {
subscribe_if_exists(id.clone(), user.id.clone()).ok();
}
}
_ => {}
}
}
pub fn posthandle_hook(notification: &ClientboundNotification) {
match &notification {
ClientboundNotification::ChannelDelete { id } => {
get_hive().hive.drop_topic(&id).ok();
}
ClientboundNotification::UserRelationship { id, user, status } => {
if status == &RelationshipStatus::None {
get_hive()
.hive
.unsubscribe(&id.to_string(), &user.id.to_string())
.ok();
}
}
ClientboundNotification::ChannelGroupLeave { id, user } => {
get_hive()
.hive
.unsubscribe(&user.to_string(), &id.to_string())
.ok();
}
_ => {}
}
}

View File

@@ -1,5 +1,5 @@
use super::{events::ClientboundNotification, websocket};
use crate::database::get_collection;
use crate::database::*;
use futures::FutureExt;
use hive_pubsub::backend::mongo::MongodbPubSub;
@@ -14,6 +14,8 @@ static HIVE: OnceCell<Hive> = OnceCell::new();
pub async fn init_hive() {
let hive = MongodbPubSub::new(
|ids, notification| {
super::events::posthandle_hook(&notification);
if let Ok(data) = to_string(&notification) {
debug!("Pushing out notification. {}", data);
websocket::publish(ids, notification);
@@ -36,8 +38,6 @@ pub async fn listen() {
.fuse()
.await
.expect("Hive hit an error");
dbg!("a");
}
pub fn subscribe_multiple(user: String, topics: Vec<String>) -> Result<(), String> {

View File

@@ -1,4 +1,5 @@
pub mod events;
pub mod hive;
pub mod payload;
pub mod subscriptions;
pub mod websocket;

View File

@@ -0,0 +1,79 @@
use std::collections::HashSet;
use crate::{database::*, notifications::events::ClientboundNotification};
use crate::{
database::{entities::User, get_collection},
util::result::{Error, Result},
};
use futures::StreamExt;
use mongodb::bson::{doc, from_document};
pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
let mut user_ids: HashSet<String> = HashSet::new();
if let Some(relationships) = &user.relations {
user_ids.extend(
relationships
.iter()
.map(|relationship| relationship.id.clone()),
);
}
let mut cursor = get_collection("channels")
.find(
doc! {
"$or": [
{
"channel_type": "SavedMessages",
"user": &user.id
},
{
"channel_type": "DirectMessage",
"recipients": &user.id
},
{
"channel_type": "Group",
"recipients": &user.id
}
]
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "channels",
})?;
let mut channels = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let channel = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "channel",
})?;
if let Channel::Group { recipients, .. } = &channel {
user_ids.extend(recipients.iter().cloned());
} else if let Channel::DirectMessage { recipients, .. } = &channel {
user_ids.extend(recipients.iter().cloned());
}
channels.push(channel);
}
}
user_ids.remove(&user.id);
let mut users = if user_ids.len() > 0 {
user.fetch_multiple_users(user_ids.into_iter().collect::<Vec<String>>())
.await?
} else {
vec![]
};
user.relationship = Some(RelationshipStatus::User);
user.online = Some(true);
users.push(user);
Ok(ClientboundNotification::Ready { users, channels })
}

View File

@@ -1,7 +1,10 @@
use crate::database::entities::User;
use crate::database::*;
use super::hive::get_hive;
use futures::StreamExt;
use hive_pubsub::PubSub;
use mongodb::bson::doc;
use mongodb::options::FindOptions;
pub async fn generate_subscriptions(user: &User) -> Result<(), String> {
let hive = get_hive();
@@ -13,5 +16,34 @@ pub async fn generate_subscriptions(user: &User) -> Result<(), String> {
}
}
let mut cursor = get_collection("channels")
.find(
doc! {
"$or": [
{
"channel_type": "SavedMessages",
"user": &user.id
},
{
"channel_type": "DirectMessage",
"recipients": &user.id
},
{
"channel_type": "Group",
"recipients": &user.id
}
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| "Failed to fetch channels.".to_string())?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
hive.subscribe(user.id.clone(), doc.get_str("_id").unwrap().to_string())?;
}
}
Ok(())
}

View File

@@ -1,5 +1,4 @@
use crate::database::get_collection;
use crate::database::guards::reference::Ref;
use crate::database::*;
use crate::util::variables::WS_HOST;
use super::subscriptions;
@@ -13,7 +12,10 @@ use futures::{pin_mut, prelude::*};
use hive_pubsub::PubSub;
use log::{debug, info};
use many_to_many::ManyToMany;
use rauth::auth::{Auth, Session};
use rauth::{
auth::{Auth, Session},
options::Options,
};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex, RwLock};
@@ -64,69 +66,146 @@ async fn accept(stream: TcpStream) {
}
};
let mut session: Option<Session> = None;
let session: Arc<Mutex<Option<Session>>> = Arc::new(Mutex::new(None));
let mutex_generator = || session.clone();
let fwd = rx.map(Ok).forward(write);
let incoming = read.try_for_each(|msg| {
let incoming = read.try_for_each(async move |msg| {
let mutex = mutex_generator();
if let Message::Text(text) = msg {
if let Ok(notification) = serde_json::from_str::<ServerboundNotification>(&text) {
match notification {
ServerboundNotification::Authenticate(new_session) => {
if session.is_some() {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return future::ok(());
{
if mutex.lock().unwrap().is_some() {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
match task::block_on(
Auth::new(get_collection("accounts")).verify_session(new_session),
) {
Ok(validated_session) => {
match task::block_on(
Ref {
id: validated_session.user_id.clone(),
}
.fetch_user(),
) {
Ok(user) => {
if let Ok(mut map) = USERS.write() {
map.insert(validated_session.user_id.clone(), addr);
session = Some(validated_session);
if let Ok(_) = task::block_on(
subscriptions::generate_subscriptions(&user),
) {
send(ClientboundNotification::Authenticated);
send(ClientboundNotification::Ready { user });
} else {
send(ClientboundNotification::Error(
WebSocketError::InternalError,
));
}
} else {
if let Ok(validated_session) =
Auth::new(get_collection("accounts"), Options::new())
.verify_session(new_session)
.await
{
let id = validated_session.user_id.clone();
if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await {
let was_online = is_online(&id);
{
match USERS.write() {
Ok(mut map) => {
map.insert(id.clone(), addr);
}
Err(_) => {
send(ClientboundNotification::Error(
WebSocketError::InternalError,
WebSocketError::InternalError {
at: "Writing users map.".to_string(),
},
));
return Ok(());
}
}
}
*mutex.lock().unwrap() = Some(validated_session);
if let Err(_) = subscriptions::generate_subscriptions(&user).await {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Generating subscriptions.".to_string(),
},
));
return Ok(());
}
send(ClientboundNotification::Authenticated);
match super::payload::generate_ready(user).await {
Ok(payload) => {
send(payload);
if !was_online {
ClientboundNotification::UserPresence {
id: id.clone(),
online: true,
}
.publish(id);
}
}
Err(_) => {
send(ClientboundNotification::Error(
WebSocketError::OnboardingNotFinished,
WebSocketError::InternalError {
at: "Generating payload.".to_string(),
},
));
return Ok(());
}
}
}
Err(_) => {
} else {
send(ClientboundNotification::Error(
WebSocketError::InvalidSession,
WebSocketError::OnboardingNotFinished,
));
}
} else {
send(ClientboundNotification::Error(
WebSocketError::InvalidSession,
));
}
}
// ! TEMP: verify user part of channel
// ! Could just run permission check here.
ServerboundNotification::BeginTyping { channel } => {
if mutex.lock().unwrap().is_some() {
let user = {
let mutex = mutex.lock().unwrap();
let session = mutex.as_ref().unwrap();
session.user_id.clone()
};
ClientboundNotification::ChannelStartTyping {
id: channel.clone(),
user,
}
.publish(channel);
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
ServerboundNotification::EndTyping { channel } => {
if mutex.lock().unwrap().is_some() {
let user = {
let mutex = mutex.lock().unwrap();
let session = mutex.as_ref().unwrap();
session.user_id.clone()
};
ClientboundNotification::ChannelStopTyping {
id: channel.clone(),
user,
}
.publish(channel);
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
}
}
}
future::ok(())
Ok(())
});
pin_mut!(fwd, incoming);
@@ -135,13 +214,26 @@ async fn accept(stream: TcpStream) {
info!("User {} disconnected.", &addr);
CONNECTIONS.lock().unwrap().remove(&addr);
if let Some(session) = session {
let mut users = USERS.write().unwrap();
users.remove(&session.user_id, &addr);
if users.get_left(&session.user_id).is_none() {
get_hive().drop_client(&session.user_id).unwrap();
let mut offline = None;
{
let session = session.lock().unwrap();
if let Some(session) = session.as_ref() {
let mut users = USERS.write().unwrap();
users.remove(&session.user_id, &addr);
if users.get_left(&session.user_id).is_none() {
get_hive().drop_client(&session.user_id).unwrap();
offline = Some(session.user_id.clone());
}
}
}
if let Some(id) = offline {
ClientboundNotification::UserPresence {
id: id.clone(),
online: false,
}
.publish(id);
}
}
pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
@@ -173,3 +265,7 @@ pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
}
}
}
pub fn is_online(user: &String) -> bool {
USERS.read().unwrap().get_left(&user).is_some()
}

View File

@@ -0,0 +1,112 @@
use crate::util::result::{Error, Result};
use crate::{database::*, notifications::events::ClientboundNotification};
use mongodb::bson::doc;
#[delete("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<()> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
match &target {
Channel::SavedMessages { .. } => Err(Error::NoEffect),
Channel::DirectMessage { .. } => {
get_collection("channels")
.update_one(
doc! {
"_id": target.id()
},
doc! {
"$set": {
"active": false
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
Ok(())
}
Channel::Group {
id,
owner,
recipients,
..
} => {
if &user.id == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$set": {
"owner": new_owner
},
"$pull": {
"recipients": &user.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
target.publish_update(json!({ "owner": new_owner })).await?;
} else {
return target.delete().await;
}
} else {
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$pull": {
"recipients": &user.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
ClientboundNotification::ChannelGroupLeave {
id: id.clone(),
user: user.id.clone(),
}
.publish(id.clone());
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::UserLeft { id: user.id }),
)
.publish(&target)
.await
.ok();
Ok(())
}
}
}

View File

@@ -0,0 +1,153 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use crate::{database::*, notifications::events::RemoveChannelField};
use mongodb::bson::{doc, to_document};
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 32))]
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[validate(length(min = 0, max = 1024))]
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
remove: Option<RemoveChannelField>,
}
#[patch("/<target>", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<()> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.name.is_none()
&& data.description.is_none()
&& data.icon.is_none()
&& data.remove.is_none()
{
return Ok(());
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_manage_channel() {
Err(Error::MissingPermission)?
}
match &target {
Channel::Group { id, icon, .. } => {
let mut set = doc! {};
let mut unset = doc! {};
let mut remove_icon = false;
if let Some(remove) = &data.remove {
match remove {
RemoveChannelField::Icon => {
unset.insert("icon", 1);
remove_icon = true;
}
}
}
if let Some(name) = &data.name {
set.insert("name", name);
}
if let Some(description) = &data.description {
set.insert("description", description);
}
if let Some(attachment_id) = &data.icon {
let attachment =
File::find_and_use(&attachment_id, "icons", "object", &user.id).await?;
set.insert(
"icon",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_icon = true;
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("channels")
.update_one(doc! { "_id": &id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!(set),
clear: data.remove,
}
.publish(id.clone());
if let Some(name) = data.name {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::ChannelRenamed { name, by: user.id.clone() }),
)
.publish(&target)
.await
.ok();
}
if let Some(_) = data.description {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::ChannelDescriptionChanged { by: user.id.clone() }),
)
.publish(&target)
.await
.ok();
}
if let Some(_) = data.icon {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::ChannelIconChanged { by: user.id }),
)
.publish(&target)
.await
.ok();
}
if remove_icon {
if let Some(old_icon) = icon {
old_icon.delete().await?;
}
}
Ok(())
}
_ => Err(Error::InvalidOperation),
}
}

View File

@@ -0,0 +1,19 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue;
#[get("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
Ok(json!(target))
}

View File

@@ -0,0 +1,23 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue;
#[get("/<target>/members")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
if let Channel::Group { recipients, .. } = target {
Ok(json!(user.fetch_multiple_users(recipients).await?))
} else {
Err(Error::InvalidOperation)
}
}

View File

@@ -0,0 +1,73 @@
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_GROUP_SIZE;
use crate::{database::*, notifications::events::ClientboundNotification};
use mongodb::bson::doc;
#[put("/<target>/recipients/<member>")]
pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
if get_relationship(&user, &member.id) != RelationshipStatus::Friend {
Err(Error::NotFriends)?
}
let channel = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
if let Channel::Group { id, recipients, .. } = &channel {
if recipients.len() >= *MAX_GROUP_SIZE {
Err(Error::GroupTooLarge {
max: *MAX_GROUP_SIZE,
})?
}
if recipients.iter().find(|x| *x == &member.id).is_some() {
Err(Error::AlreadyInGroup)?
}
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$push": {
"recipients": &member.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
ClientboundNotification::ChannelGroupJoin {
id: id.clone(),
user: member.id.clone(),
}
.publish(id.clone());
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::UserAdded {
id: member.id,
by: user.id,
}),
)
.publish(&channel)
.await
.ok();
Ok(())
} else {
Err(Error::InvalidOperation)
}
}

View File

@@ -0,0 +1,80 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_GROUP_SIZE;
use mongodb::bson::doc;
use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::iter::FromIterator;
use ulid::Ulid;
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 32))]
name: String,
#[validate(length(min = 0, max = 1024))]
description: Option<String>,
// Maximum length of 36 allows both ULIDs and UUIDs.
#[validate(length(min = 1, max = 36))]
nonce: String,
users: Vec<String>,
}
#[post("/create", data = "<info>")]
pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> {
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let mut set: HashSet<String> = HashSet::from_iter(info.users.iter().cloned());
set.insert(user.id.clone());
if set.len() > *MAX_GROUP_SIZE {
Err(Error::GroupTooLarge {
max: *MAX_GROUP_SIZE,
})?
}
for target in &set {
if get_relationship(&user, target) != RelationshipStatus::Friend {
Err(Error::NotFriends)?
}
}
if get_collection("channels")
.find_one(
doc! {
"nonce": &info.nonce
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "channel",
})?
.is_some()
{
Err(Error::DuplicateNonce)?
}
let id = Ulid::new().to_string();
let channel = Channel::Group {
id,
nonce: Some(info.nonce.clone()),
name: info.name.clone(),
description: info
.description
.clone()
.unwrap_or_else(|| "A group.".to_string()),
owner: user.id,
recipients: set.into_iter().collect::<Vec<String>>(),
icon: None,
last_message: None,
};
channel.clone().publish().await?;
Ok(json!(channel))
}

View File

@@ -0,0 +1,70 @@
use crate::util::result::{Error, Result};
use crate::{database::*, notifications::events::ClientboundNotification};
use mongodb::bson::doc;
#[delete("/<target>/recipients/<member>")]
pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
if &user.id == &member.id {
Err(Error::CannotRemoveYourself)?
}
let channel = target.fetch_channel().await?;
if let Channel::Group {
id,
owner,
recipients,
..
} = &channel
{
if &user.id != owner {
// figure out if we want to use perm system here
Err(Error::MissingPermission)?
}
if recipients.iter().find(|x| *x == &member.id).is_none() {
Err(Error::NotInGroup)?
}
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$pull": {
"recipients": &member.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
ClientboundNotification::ChannelGroupLeave {
id: id.clone(),
user: member.id.clone(),
}
.publish(id.clone());
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::UserRemove {
id: member.id,
by: user.id,
}),
)
.publish(&channel)
.await
.ok();
Ok(())
} else {
Err(Error::InvalidOperation)
}
}

View File

@@ -0,0 +1,84 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL};
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct CreateUserResponse {
token: String,
}
#[post("/<target>/join_call")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
if !*USE_VOSO {
return Err(Error::VosoUnavailable);
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_voice_call() {
return Err(Error::MissingPermission);
}
// To join a call:
// - Check if the room exists.
// - If not, create it.
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/room/{}", *VOSO_URL, target.id()))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await;
match result {
Err(_) => return Err(Error::VosoUnavailable),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => (),
reqwest::StatusCode::NOT_FOUND => {
if let Err(_) = client
.post(&format!("{}/room/{}", *VOSO_URL, target.id()))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await
{
return Err(Error::VosoUnavailable);
}
}
_ => return Err(Error::VosoUnavailable),
},
}
// Then create a user for the room.
if let Ok(response) = client
.post(&format!(
"{}/room/{}/user/{}",
*VOSO_URL,
target.id(),
user.id
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await
{
let res: CreateUserResponse = response.json().await.map_err(|_| Error::InvalidOperation)?;
Ok(json!(res))
} else {
Err(Error::VosoUnavailable)
}
}

View File

@@ -0,0 +1,27 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
#[delete("/<target>/messages/<msg>")]
pub async fn req(user: User, target: Ref, msg: Ref) -> Result<()> {
let channel = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let message = msg.fetch_message(&channel).await?;
if message.author != user.id && !perm.get_manage_messages() {
match channel {
Channel::SavedMessages { .. } => unreachable!(),
_ => Err(Error::CannotEditMessage)?,
}
}
message.delete().await
}

View File

@@ -0,0 +1,58 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use chrono::Utc;
use mongodb::bson::{doc, Bson, DateTime};
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 2000))]
content: String,
}
#[patch("/<target>/messages/<msg>", data = "<edit>")]
pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<()> {
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
let channel = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let message = msg.fetch_message(&channel).await?;
if message.author != user.id {
Err(Error::CannotEditMessage)?
}
let edited = Utc::now();
get_collection("messages")
.update_one(
doc! {
"_id": &message.id
},
doc! {
"$set": {
"content": &edit.content,
"edited": Bson::DateTime(edited)
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})?;
message
.publish_update(json!({ "content": edit.content, "edited": DateTime(edited) }))
.await
}

View File

@@ -0,0 +1,20 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue;
#[get("/<target>/messages/<msg>")]
pub async fn req(user: User, target: Ref, msg: Ref) -> Result<JsonValue> {
let channel = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let message = msg.fetch_message(&channel).await?;
Ok(json!(message))
}

View File

@@ -0,0 +1,91 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::{
bson::{doc, from_document},
options::FindOptions,
};
use rocket::request::Form;
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Serialize, Deserialize, FromFormValue)]
pub enum Sort {
Latest,
Oldest,
}
#[derive(Validate, Serialize, Deserialize, FromForm)]
pub struct Options {
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
#[validate(length(min = 26, max = 26))]
before: Option<String>,
#[validate(length(min = 26, max = 26))]
after: Option<String>,
sort: Option<Sort>,
}
#[get("/<target>/messages?<options..>")]
pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<JsonValue> {
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let mut query = doc! { "channel": target.id() };
if let Some(before) = &options.before {
query.insert("_id", doc! { "$lt": before });
}
if let Some(after) = &options.after {
query.insert("_id", doc! { "$gt": after });
}
let sort = if let Sort::Latest = options.sort.as_ref().unwrap_or_else(|| &Sort::Latest) {
-1
} else {
1
};
let mut cursor = get_collection("messages")
.find(
query,
FindOptions::builder()
.limit(options.limit.unwrap_or(50))
.sort(doc! {
"_id": sort
})
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
let mut messages = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
Ok(json!(messages))
}

View File

@@ -0,0 +1,73 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, from_document};
use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Options {
ids: Vec<String>,
}
#[post("/<target>/messages/stale", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonValue> {
if data.ids.len() > 150 {
return Err(Error::TooManyIds);
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let mut cursor = get_collection("messages")
.find(
doc! {
"_id": {
"$in": &data.ids
},
"channel": target.id()
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
let mut updated = vec![];
let mut found_ids = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let msg = from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?;
found_ids.push(msg.id.clone());
if msg.edited.is_some() {
updated.push(msg);
}
}
}
let mut deleted = vec![];
for id in &data.ids {
if found_ids.iter().find(|x| *x == id).is_none() {
deleted.push(id);
}
}
Ok(json!({
"updated": updated,
"deleted": deleted
}))
}

View File

@@ -0,0 +1,82 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::{bson::doc, options::FindOneOptions};
use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 0, max = 2000))]
content: String,
// Maximum length of 36 allows both ULIDs and UUIDs.
#[validate(length(min = 1, max = 36))]
nonce: String,
#[validate(length(min = 1, max = 128))]
attachment: Option<String>,
}
#[post("/<target>/messages", data = "<message>")]
pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonValue> {
message
.validate()
.map_err(|error| Error::FailedValidation { error })?;
if message.content.len() == 0 && message.attachment.is_none() {
return Err(Error::EmptyMessage);
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_send_message() {
Err(Error::MissingPermission)?
}
if get_collection("messages")
.find_one(
doc! {
"nonce": &message.nonce
},
FindOneOptions::builder()
.projection(doc! { "_id": 1 })
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "message",
})?
.is_some()
{
Err(Error::DuplicateNonce)?
}
let id = Ulid::new().to_string();
let attachment = if let Some(attachment_id) = &message.attachment {
Some(File::find_and_use(attachment_id, "attachments", "message", &id).await?)
} else {
None
};
let msg = Message {
id,
channel: target.id().to_string(),
author: user.id,
content: Content::Text(message.content.clone()),
attachment,
nonce: Some(message.nonce.clone()),
edited: None,
embeds: None,
};
msg.clone().publish(&target).await?;
Ok(json!(msg))
}

View File

@@ -1,5 +1,35 @@
use rocket::Route;
mod delete_channel;
mod edit_channel;
mod fetch_channel;
mod fetch_members;
mod group_add_member;
mod group_create;
mod group_remove_member;
mod join_call;
mod message_delete;
mod message_edit;
mod message_fetch;
mod message_query;
mod message_query_stale;
mod message_send;
pub fn routes() -> Vec<Route> {
routes![]
routes![
fetch_channel::req,
fetch_members::req,
delete_channel::req,
edit_channel::req,
message_send::req,
message_query::req,
message_query_stale::req,
message_fetch::req,
message_edit::req,
message_delete::req,
group_create::req,
group_add_member::req,
group_remove_member::req,
join_call::req,
]
}

View File

@@ -5,6 +5,7 @@ use rocket::Rocket;
mod channels;
mod guild;
mod onboard;
mod push;
mod root;
mod users;
@@ -15,4 +16,5 @@ pub fn mount(rocket: Rocket) -> Rocket {
.mount("/users", users::routes())
.mount("/channels", channels::routes())
.mount("/guild", guild::routes())
.mount("/push", push::routes())
}

View File

@@ -1,8 +1,7 @@
use crate::database::entities::User;
use crate::database::get_collection;
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use mongodb::options::{Collation, FindOneOptions};
use rauth::auth::Session;
use regex::Regex;
use rocket_contrib::json::Json;
@@ -10,7 +9,7 @@ use serde::{Deserialize, Serialize};
use validator::Validate;
lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9-_]+$").unwrap();
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap();
}
#[derive(Validate, Serialize, Deserialize)]
@@ -28,38 +27,23 @@ pub async fn req(session: Session, user: Option<User>, data: Json<Data>) -> Resu
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let col = get_collection("users");
if col
.find_one(
if User::is_username_taken(&data.username).await? {
return Err(Error::UsernameTaken);
}
get_collection("users")
.insert_one(
doc! {
"_id": session.user_id,
"username": &data.username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
operation: "insert_one",
with: "user",
})?
.is_some()
{
Err(Error::UsernameTaken)?
}
col.insert_one(
doc! {
"_id": session.user_id,
"username": &data.username
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "user",
})?;
})?;
Ok(())
}

View File

@@ -1,4 +1,5 @@
use crate::database::entities::User;
use crate::database::*;
use rauth::auth::Session;
use rocket_contrib::json::JsonValue;

8
src/routes/push/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
use rocket::Route;
mod subscribe;
mod unsubscribe;
pub fn routes() -> Vec<Route> {
routes![subscribe::req, unsubscribe::req]
}

View File

@@ -0,0 +1,37 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, to_document};
use rauth::auth::Session;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Subscription {
endpoint: String,
p256dh: String,
auth: String,
}
#[post("/subscribe", data = "<data>")]
pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> {
let data = data.into_inner();
get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$set": {
"sessions.$.subscription": to_document(&data)
.map_err(|_| Error::DatabaseError { operation: "to_document", with: "subscription" })?
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "account" })?;
Ok(())
}

View File

@@ -0,0 +1,29 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rauth::auth::Session;
#[post("/unsubscribe")]
pub async fn req(session: Session) -> Result<()> {
get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$unset": {
"sessions.$.subscription": 1
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "subscription",
})?;
Ok(())
}

View File

@@ -1,4 +1,7 @@
use crate::util::variables::{DISABLE_REGISTRATION, HCAPTCHA_SITEKEY, USE_EMAIL, USE_HCAPTCHA};
use crate::util::variables::{
APP_URL, JANUARY_URL, AUTUMN_URL, DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY,
USE_AUTUMN, USE_JANUARY, USE_EMAIL, USE_HCAPTCHA, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL, VOSO_WS_HOST,
};
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
@@ -6,7 +9,7 @@ use rocket_contrib::json::JsonValue;
#[get("/")]
pub async fn root() -> JsonValue {
json!({
"revolt": "0.3.0-alpha",
"revolt": crate::version::VERSION,
"features": {
"registration": !*DISABLE_REGISTRATION,
"captcha": {
@@ -14,6 +17,23 @@ pub async fn root() -> JsonValue {
"key": HCAPTCHA_SITEKEY.to_string()
},
"email": *USE_EMAIL,
}
"invite_only": *INVITE_ONLY,
"autumn": {
"enabled": *USE_AUTUMN,
"url": *AUTUMN_URL
},
"january": {
"enabled": *USE_JANUARY,
"url": *JANUARY_URL
},
"voso": {
"enabled": *USE_VOSO,
"url": *VOSO_URL,
"ws": *VOSO_WS_HOST
}
},
"ws": *EXTERNAL_WS_URL,
"app": *APP_URL,
"vapid": *VAPID_PUBLIC_KEY
})
}

View File

@@ -1,41 +1,39 @@
use crate::{
database::{
entities::{RelationshipStatus, User},
get_collection,
guards::reference::Ref,
permissions::get_relationship,
},
util::result::Error,
};
use crate::{
notifications::{events::ClientboundNotification, hive},
util::result::Result,
};
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
use mongodb::options::{FindOneOptions, Collation};
use mongodb::options::{Collation, FindOneOptions};
use rocket_contrib::json::JsonValue;
#[put("/<username>/friend")]
pub async fn req(user: User, username: String) -> Result<JsonValue> {
let col = get_collection("users");
let doc = col.find_one(
doc! {
"username": username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError { operation: "find_one", with: "user" })?
.ok_or_else(|| Error::UnknownUser)?;
let doc = col
.find_one(
doc! {
"username": username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.ok_or_else(|| Error::UnknownUser)?;
let target_id = doc
.get_str("_id")
.map_err(|_| Error::DatabaseError { operation: "get_str(_id)", with: "user" })?;
let target_id = doc.get_str("_id").map_err(|_| Error::DatabaseError {
operation: "get_str(_id)",
with: "user",
})?;
match get_relationship(&user, &Ref { id: target_id.to_string() }) {
let target_user = Ref::from(target_id.to_string())?.fetch_user().await?;
match get_relationship(&user, &target_id) {
RelationshipStatus::User => return Err(Error::NoEffect),
RelationshipStatus::Friend => return Err(Error::AlreadyFriends),
RelationshipStatus::Outgoing => return Err(Error::AlreadySentRequest),
@@ -69,21 +67,26 @@ pub async fn req(user: User, username: String) -> Result<JsonValue> {
)
) {
Ok(_) => {
try_join!(
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target_id.to_string(),
status: RelationshipStatus::Friend
}
.publish(user.id.clone()),
ClientboundNotification::UserRelationship {
id: target_id.to_string(),
user: user.id.clone(),
status: RelationshipStatus::Friend
}
.publish(target_id.to_string())
)
.ok();
let target_user = target_user
.from_override(&user, RelationshipStatus::Friend)
.await?;
let user = user
.from_override(&target_user, RelationshipStatus::Friend)
.await?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target_user,
status: RelationshipStatus::Friend,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.to_string(),
user,
status: RelationshipStatus::Friend,
}
.publish(target_id.to_string());
Ok(json!({ "status": "Friend" }))
}
@@ -125,24 +128,26 @@ pub async fn req(user: User, username: String) -> Result<JsonValue> {
)
) {
Ok(_) => {
try_join!(
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target_id.to_string(),
status: RelationshipStatus::Outgoing
}
.publish(user.id.clone()),
ClientboundNotification::UserRelationship {
id: target_id.to_string(),
user: user.id.clone(),
status: RelationshipStatus::Incoming
}
.publish(target_id.to_string())
)
.ok();
let target_user = target_user
.from_override(&user, RelationshipStatus::Outgoing)
.await?;
let user = user
.from_override(&target_user, RelationshipStatus::Incoming)
.await?;
hive::subscribe_if_exists(user.id.clone(), target_id.to_string()).ok();
hive::subscribe_if_exists(target_id.to_string(), user.id.clone()).ok();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target_user,
status: RelationshipStatus::Outgoing,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.to_string(),
user,
status: RelationshipStatus::Incoming,
}
.publish(target_id.to_string());
Ok(json!({ "status": "Outgoing" }))
}

View File

@@ -1,11 +1,7 @@
use crate::{
database::entities::RelationshipStatus, database::entities::User, database::get_collection,
database::guards::reference::Ref, database::permissions::get_relationship, util::result::Error,
};
use crate::{
notifications::{events::ClientboundNotification, hive},
util::result::Result,
};
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
@@ -14,7 +10,9 @@ use rocket_contrib::json::JsonValue;
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let col = get_collection("users");
match get_relationship(&user, &target) {
let target = target.fetch_user().await?;
match get_relationship(&user, &target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
RelationshipStatus::BlockedOther => {
col.update_one(
@@ -37,12 +35,10 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone())
.await
.ok();
.publish(user.id.clone());
Ok(json!({ "status": "Blocked" }))
}
@@ -78,24 +74,27 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
)
) {
Ok(_) => {
try_join!(
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target.id.clone(),
status: RelationshipStatus::Blocked
}
.publish(user.id.clone()),
ClientboundNotification::UserRelationship {
id: target.id.clone(),
user: user.id.clone(),
status: RelationshipStatus::BlockedOther
}
.publish(target.id.clone())
)
.ok();
let target = target
.from_override(&user, RelationshipStatus::Blocked)
.await?;
let user = user
.from_override(&target, RelationshipStatus::BlockedOther)
.await?;
let target_id = target.id.clone();
hive::subscribe_if_exists(user.id.clone(), target.id.clone()).ok();
hive::subscribe_if_exists(target.id.clone(), user.id.clone()).ok();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::BlockedOther,
}
.publish(target_id);
Ok(json!({ "status": "Blocked" }))
}
@@ -135,21 +134,27 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
)
) {
Ok(_) => {
try_join!(
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target.id.clone(),
status: RelationshipStatus::Blocked
}
.publish(user.id.clone()),
ClientboundNotification::UserRelationship {
id: target.id.clone(),
user: user.id.clone(),
status: RelationshipStatus::BlockedOther
}
.publish(target.id.clone())
)
.ok();
let target = target
.from_override(&user, RelationshipStatus::Blocked)
.await?;
let user = user
.from_override(&target, RelationshipStatus::BlockedOther)
.await?;
let target_id = target.id.clone();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::BlockedOther,
}
.publish(target_id);
Ok(json!({ "status": "Blocked" }))
}

View File

@@ -0,0 +1,66 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rauth::auth::{Auth, Session};
use regex::Regex;
use rocket::State;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
// ! FIXME: should be global somewhere; maybe use config(?)
lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap();
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
username: Option<String>,
#[validate(length(min = 8, max = 72))]
password: String,
}
#[patch("/<_ignore_id>/username", data = "<data>")]
pub async fn req(
auth: State<'_, Auth>,
session: Session,
user: User,
data: Json<Data>,
_ignore_id: String,
) -> Result<()> {
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
auth.verify_password(&session, data.password.clone())
.await
.map_err(|_| Error::InvalidCredentials)?;
let mut set = doc! {};
if let Some(username) = &data.username {
if User::is_username_taken(&username).await? {
return Err(Error::UsernameTaken);
}
set.insert("username", username.clone());
}
get_collection("users")
.update_one(doc! { "_id": &user.id }, doc! { "$set": set }, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!(data.0),
clear: None,
}
.publish(user.id.clone());
Ok(())
}

View File

@@ -0,0 +1,179 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use crate::{database::*, notifications::events::RemoveUserField};
use mongodb::bson::{doc, to_document};
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize, Debug)]
pub struct UserProfileData {
#[validate(length(min = 0, max = 2000))]
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))]
background: Option<String>,
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate]
status: Option<UserStatus>,
#[validate]
profile: Option<UserProfileData>,
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
remove: Option<RemoveUserField>,
}
#[patch("/<_ignore_id>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<()> {
let mut data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.status.is_none()
&& data.profile.is_none()
&& data.avatar.is_none()
&& data.remove.is_none()
{
return Ok(());
}
let mut unset = doc! {};
let mut set = doc! {};
let mut remove_background = false;
let mut remove_avatar = false;
if let Some(remove) = &data.remove {
match remove {
RemoveUserField::ProfileContent => {
unset.insert("profile.content", 1);
}
RemoveUserField::ProfileBackground => {
unset.insert("profile.background", 1);
remove_background = true;
}
RemoveUserField::StatusText => {
unset.insert("status.text", 1);
}
RemoveUserField::Avatar => {
unset.insert("avatar", 1);
remove_avatar = true;
}
}
}
if let Some(status) = &data.status {
set.insert(
"status",
to_document(&status).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "status",
})?,
);
}
if let Some(profile) = data.profile {
if let Some(content) = profile.content {
set.insert("profile.content", content);
}
if let Some(attachment_id) = profile.background {
let attachment =
File::find_and_use(&attachment_id, "backgrounds", "user", &user.id).await?;
set.insert(
"profile.background",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_background = true;
}
}
let avatar = std::mem::replace(&mut data.avatar, None);
let attachment = if let Some(attachment_id) = avatar {
let attachment = File::find_and_use(&attachment_id, "avatars", "user", &user.id).await?;
set.insert(
"avatar",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_avatar = true;
Some(attachment)
} else {
None
};
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("users")
.update_one(doc! { "_id": &user.id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
}
if let Some(status) = &data.status {
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!({ "status": status }),
clear: None,
}
.publish(user.id.clone());
}
if let Some(avatar) = attachment {
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!({ "avatar": avatar }),
clear: None,
}
.publish(user.id.clone());
}
if let Some(clear) = data.remove {
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!({}),
clear: Some(clear),
}
.publish(user.id.clone());
}
if remove_avatar {
if let Some(old_avatar) = user.avatar {
old_avatar.delete().await?;
}
}
if remove_background {
if let Some(profile) = user.profile {
if let Some(old_background) = profile.background {
old_background.delete().await?;
}
}
}
Ok(())
}

View File

@@ -1,6 +1,6 @@
use crate::database::entities::User;
use crate::database::get_collection;
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
@@ -12,11 +12,11 @@ pub async fn req(user: User) -> Result<JsonValue> {
doc! {
"$or": [
{
"type": "DirectMessage",
"channel_type": "DirectMessage",
"active": true
},
{
"type": "Group"
"channel_type": "Group"
}
],
"recipients": user.id

View File

@@ -0,0 +1,24 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
#[get("/<target>/profile")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let target = target.fetch_user().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_user(&target)
.for_user_given()
.await?;
if !perm.get_view_profile() {
Err(Error::MissingPermission)?
}
if target.profile.is_some() {
Ok(json!(target.profile))
} else {
Ok(json!({}))
}
}

View File

@@ -1,8 +1,9 @@
use crate::database::{entities::User, guards::reference::Ref, permissions::get_relationship};
use crate::database::*;
use crate::util::result::Result;
use rocket_contrib::json::JsonValue;
#[get("/<target>/relationship")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
Ok(json!({ "status": get_relationship(&user, &target) }))
Ok(json!({ "status": get_relationship(&user, &target.id) }))
}

View File

@@ -1,5 +1,6 @@
use crate::database::entities::User;
use crate::database::*;
use crate::util::result::Result;
use rocket_contrib::json::JsonValue;
#[get("/relationships")]

View File

@@ -1,22 +1,20 @@
use crate::database::entities::User;
use crate::database::guards::reference::Ref;
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue;
#[get("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let mut target = target.fetch_user().await?;
let target = target.fetch_user().await?;
if user.id != target.id {
// Check whether we are allowed to fetch this user.
let perm = crate::database::permissions::temp_calc_perm(&user, &target).await;
if !perm.get_access() {
Err(Error::LabelMe)?
}
let perm = permissions::PermissionCalculator::new(&user)
.with_user(&target)
.for_user_given()
.await?;
// Only return user relationships if the target is the caller.
target.relations = None;
if !perm.get_access() {
Err(Error::MissingPermission)?
}
Ok(json!(target))
Ok(json!(target.from(&user).with(perm)))
}

View File

@@ -0,0 +1,34 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, Document};
use mongodb::options::FindOptions;
use rocket_contrib::json::JsonValue;
#[get("/<target>/mutual")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let users = get_collection("users")
.find(
doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user.id, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &target.id, "status": "Friend" } } }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
Ok(json!({ "users": users }))
}

View File

@@ -1,7 +0,0 @@
use rocket::response::NamedFile;
use std::path::Path;
#[get("/<_target>/avatar")]
pub async fn req(_target: String) -> Option<NamedFile> {
NamedFile::open(Path::new("avatar.png")).await.ok()
}

View File

@@ -0,0 +1,29 @@
use rocket::response::NamedFile;
use std::path::Path;
use crate::database::Ref;
#[get("/<target>/default_avatar")]
pub async fn req(target: Ref) -> Option<NamedFile> {
match target.id.chars().nth(25).unwrap() {
'0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' => {
NamedFile::open(Path::new("assets/user_red.png")).await.ok()
}
'8' | '9' | 'A' | 'C' | 'B' | 'D' | 'E' | 'F' => {
NamedFile::open(Path::new("assets/user_green.png"))
.await
.ok()
}
'G' | 'H' | 'J' | 'K' | 'M' | 'N' | 'P' | 'Q' => {
NamedFile::open(Path::new("assets/user_blue.png"))
.await
.ok()
}
'R' | 'S' | 'T' | 'V' | 'W' | 'X' | 'Y' | 'Z' => {
NamedFile::open(Path::new("assets/user_yellow.png"))
.await
.ok()
}
_ => unreachable!(),
}
}

View File

@@ -1,12 +1,16 @@
use rocket::Route;
mod add_friend;
mod get_avatar;
mod block_user;
mod change_username;
mod edit_user;
mod fetch_dms;
mod fetch_profile;
mod fetch_relationship;
mod fetch_relationships;
mod fetch_user;
mod find_mutual;
mod get_default_avatar;
mod open_dm;
mod remove_friend;
mod unblock_user;
@@ -15,13 +19,15 @@ pub fn routes() -> Vec<Route> {
routes![
// User Information
fetch_user::req,
get_avatar::req,
edit_user::req,
change_username::req,
get_default_avatar::req,
fetch_profile::req,
// Direct Messaging
fetch_dms::req,
open_dm::req,
// Relationships
find_mutual::req,
fetch_relationships::req,
fetch_relationship::req,
add_friend::req,

View File

@@ -1,7 +1,6 @@
use crate::database::entities::{Channel, User};
use crate::database::get_collection;
use crate::database::guards::reference::Ref;
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
use ulid::Ulid;
@@ -10,12 +9,12 @@ use ulid::Ulid;
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let query = if user.id == target.id {
doc! {
"type": "SavedMessages",
"channel_type": "SavedMessages",
"user": &user.id
}
} else {
doc! {
"type": "DirectMessage",
"channel_type": "DirectMessage",
"recipients": {
"$all": [ &user.id, &target.id ]
}
@@ -41,10 +40,11 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
id,
active: false,
recipients: vec![user.id, target.id],
last_message: None,
}
};
channel.save().await?;
channel.clone().publish().await?;
Ok(json!(channel))
}
}

View File

@@ -1,13 +1,8 @@
use crate::{
database::entities::RelationshipStatus, database::entities::User, database::get_collection,
database::guards::reference::Ref, database::permissions::get_relationship, util::result::Error,
};
use crate::{
notifications::{events::ClientboundNotification, hive},
util::result::Result,
};
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::try_join;
use hive_pubsub::PubSub;
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
@@ -15,11 +10,9 @@ use rocket_contrib::json::JsonValue;
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let col = get_collection("users");
match get_relationship(&user, &target) {
RelationshipStatus::Blocked
| RelationshipStatus::BlockedOther
| RelationshipStatus::User
| RelationshipStatus::None => Err(Error::NoEffect),
let target = target.fetch_user().await?;
match get_relationship(&user, &target.id) {
RelationshipStatus::Friend
| RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => {
@@ -52,25 +45,27 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
)
) {
Ok(_) => {
try_join!(
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target.id.clone(),
status: RelationshipStatus::None
}
.publish(user.id.clone()),
ClientboundNotification::UserRelationship {
id: target.id.clone(),
user: user.id.clone(),
status: RelationshipStatus::None
}
.publish(target.id.clone())
)
.ok();
let target = target
.from_override(&user, RelationshipStatus::None)
.await?;
let user = user
.from_override(&target, RelationshipStatus::None)
.await?;
let target_id = target.id.clone();
let hive = hive::get_hive();
hive.unsubscribe(&user.id, &target.id).ok();
hive.unsubscribe(&target.id, &user.id).ok();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::None,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::None,
}
.publish(target_id);
Ok(json!({ "status": "None" }))
}
@@ -80,5 +75,6 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
}),
}
}
_ => Err(Error::NoEffect),
}
}

View File

@@ -1,119 +1,111 @@
use crate::{
database::entities::RelationshipStatus, database::entities::User, database::get_collection,
database::guards::reference::Ref, database::permissions::get_relationship, util::result::Error,
};
use crate::{
notifications::{events::ClientboundNotification, hive},
util::result::Result,
};
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::try_join;
use hive_pubsub::PubSub;
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
#[delete("/<target>/block")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let col = get_collection("users");
let target = target.fetch_user().await?;
match get_relationship(&user, &target) {
RelationshipStatus::None
| RelationshipStatus::User
| RelationshipStatus::BlockedOther
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing
| RelationshipStatus::Friend => Err(Error::NoEffect),
RelationshipStatus::Blocked => {
match get_relationship(&target.fetch_user().await?, &user.as_ref()) {
RelationshipStatus::Blocked => {
match get_relationship(&user, &target.id) {
RelationshipStatus::Blocked => match get_relationship(&target, &user.id) {
RelationshipStatus::Blocked => {
col.update_one(
doc! {
"_id": &user.id,
"relations._id": &target.id
},
doc! {
"$set": {
"relations.$.status": "BlockedOther"
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
let target = target
.from_override(&user, RelationshipStatus::BlockedOther)
.await?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::BlockedOther,
}
.publish(user.id.clone());
Ok(json!({ "status": "BlockedOther" }))
}
RelationshipStatus::BlockedOther => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id,
"relations._id": &target.id
"_id": &user.id
},
doc! {
"$set": {
"relations.$.status": "BlockedOther"
"$pull": {
"relations": {
"_id": &target.id
}
}
},
None,
None
),
col.update_one(
doc! {
"_id": &target.id
},
doc! {
"$pull": {
"relations": {
"_id": &user.id
}
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
) {
Ok(_) => {
let target = target
.from_override(&user, RelationshipStatus::None)
.await?;
let user = user
.from_override(&target, RelationshipStatus::None)
.await?;
let target_id = target.id.clone();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::None,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user: user,
status: RelationshipStatus::None,
}
.publish(target_id);
Ok(json!({ "status": "None" }))
}
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target.id.clone(),
status: RelationshipStatus::BlockedOther,
}
.publish(user.id.clone())
.await
.ok();
Ok(json!({ "status": "BlockedOther" }))
}),
}
RelationshipStatus::BlockedOther => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id
},
doc! {
"$pull": {
"relations": {
"_id": &target.id
}
}
},
None
),
col.update_one(
doc! {
"_id": &target.id
},
doc! {
"$pull": {
"relations": {
"_id": &user.id
}
}
},
None
)
) {
Ok(_) => {
try_join!(
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target.id.clone(),
status: RelationshipStatus::None
}
.publish(user.id.clone()),
ClientboundNotification::UserRelationship {
id: target.id.clone(),
user: user.id.clone(),
status: RelationshipStatus::None
}
.publish(target.id.clone())
)
.ok();
let hive = hive::get_hive();
hive.unsubscribe(&user.id, &target.id).ok();
hive.unsubscribe(&target.id, &user.id).ok();
Ok(json!({ "status": "None" }))
}
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
}
}
_ => Err(Error::InternalError),
}
}
_ => Err(Error::InternalError),
},
_ => Err(Error::NoEffect),
}
}

View File

@@ -1,45 +0,0 @@
use crate::util::variables::{HCAPTCHA_KEY, USE_HCAPTCHA};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
struct CaptchaResponse {
success: bool,
}
pub async fn verify(user_token: &Option<String>) -> Result<(), String> {
if *USE_HCAPTCHA {
if let Some(token) = user_token {
let mut map = HashMap::new();
map.insert("secret", HCAPTCHA_KEY.to_string());
map.insert("response", token.to_string());
let client = Client::new();
if let Ok(response) = client
.post("https://hcaptcha.com/siteverify")
.form(&map)
.send()
.await
{
let result: CaptchaResponse = response
.json()
.await
.map_err(|_| "Failed to deserialise captcha result.".to_string())?;
if result.success {
Ok(())
} else {
Err("Unsuccessful captcha verification".to_string())
}
} else {
Err("Failed to verify with hCaptcha".to_string())
}
} else {
Err("Missing hCaptcha token!".to_string())
}
} else {
Ok(())
}
}

View File

@@ -1,96 +0,0 @@
use lettre::message::{header, MultiPart, SinglePart};
use lettre::transport::smtp::authentication::Credentials;
use lettre::{Message, SmtpTransport, Transport};
use super::variables::{PUBLIC_URL, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME};
lazy_static! {
static ref MAILER: lettre::transport::smtp::SmtpTransport =
SmtpTransport::relay(SMTP_HOST.as_ref())
.unwrap()
.credentials(Credentials::new(
SMTP_USERNAME.to_string(),
SMTP_PASSWORD.to_string()
))
.build();
}
fn send(message: Message) -> Result<(), String> {
MAILER
.send(&message)
.map_err(|err| format!("Failed to send email! {}", err.to_string()))?;
Ok(())
}
fn generate_multipart(text: &str, html: &str) -> MultiPart {
MultiPart::mixed().multipart(
MultiPart::alternative()
.singlepart(
SinglePart::quoted_printable()
.header(header::ContentType(
"text/plain; charset=utf8".parse().unwrap(),
))
.body(text),
)
.multipart(
MultiPart::related().singlepart(
SinglePart::eight_bit()
.header(header::ContentType(
"text/html; charset=utf8".parse().unwrap(),
))
.body(html),
),
),
)
}
pub fn send_verification_email(email: String, code: String) -> Result<(), String> {
let url = format!("{}/api/account/verify/{}", PUBLIC_URL.to_string(), code);
let email = Message::builder()
.from(SMTP_FROM.to_string().parse().unwrap())
.to(email.parse().unwrap())
.subject("Verify your email!")
.multipart(generate_multipart(
&format!("Verify your email here: {}", url),
&format!("<a href=\"{}\">Click to verify your email!</a>", url),
))
.unwrap();
send(email)
}
pub fn send_password_reset(email: String, code: String) -> Result<(), String> {
let url = format!("{}/api/account/reset/{}", PUBLIC_URL.to_string(), code);
let email = Message::builder()
.from(SMTP_FROM.to_string().parse().unwrap())
.to(email.parse().unwrap())
.subject("Reset your password.")
.multipart(generate_multipart(
&format!("Reset your password here: {}", url),
&format!("<a href=\"{}\">Click to reset your password!</a>", url),
))
.unwrap();
send(email)
}
pub fn send_welcome_email(email: String, username: String) -> Result<(), String> {
let email = Message::builder()
.from(SMTP_FROM.to_string().parse().unwrap())
.to(email.parse().unwrap())
.subject("Welcome to REVOLT!")
.multipart(
generate_multipart(
&format!("Welcome, {}! You can now use REVOLT.", username),
&format!(
"<b>Welcome, {}!</b><br/>You can now use REVOLT.<br/><a href=\"{}\">Go to REVOLT</a>",
username,
PUBLIC_URL.to_string()
)
)
)
.unwrap();
send(email)
}

View File

@@ -1,19 +1,2 @@
use rand::{distributions::Alphanumeric, Rng};
use std::collections::HashSet;
use std::iter::FromIterator;
pub mod captcha;
pub mod email;
pub mod result;
pub mod variables;
pub fn vec_to_set<T: Clone + Eq + std::hash::Hash>(data: &[T]) -> HashSet<T> {
HashSet::from_iter(data.iter().cloned())
}
pub fn gen_token(l: usize) -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(l)
.collect::<String>()
}

View File

@@ -30,8 +30,30 @@ pub enum Error {
Blocked,
#[snafu(display("You have been blocked by this user."))]
BlockedByOther,
#[snafu(display("Not friends with target user."))]
NotFriends,
// ? Channel related errors.
#[snafu(display("This channel does not exist!"))]
UnknownChannel,
#[snafu(display("Attachment does not exist!"))]
UnknownAttachment,
#[snafu(display("Cannot edit someone else's message."))]
CannotEditMessage,
#[snafu(display("Cannot send empty message."))]
EmptyMessage,
#[snafu(display("Cannot remove yourself from a group, use delete channel instead."))]
CannotRemoveYourself,
#[snafu(display("Group size is too large."))]
GroupTooLarge { max: usize },
#[snafu(display("User already part of group."))]
AlreadyInGroup,
#[snafu(display("User is not part of the group."))]
NotInGroup,
// ? General errors.
#[snafu(display("Trying to fetch too much data."))]
TooManyIds,
#[snafu(display("Failed to validate fields."))]
FailedValidation { error: ValidationErrors },
#[snafu(display("Encountered a database error."))]
@@ -41,6 +63,16 @@ pub enum Error {
},
#[snafu(display("Internal server error."))]
InternalError,
#[snafu(display("Missing permission."))]
MissingPermission,
#[snafu(display("Operation cannot be performed on this object."))]
InvalidOperation,
#[snafu(display("Email or password is incorrect."))]
InvalidCredentials,
#[snafu(display("Already created an object with this nonce."))]
DuplicateNonce,
#[snafu(display("Voso is not enabled on this instance."))]
VosoUnavailable,
#[snafu(display("This request had no effect."))]
NoEffect,
}
@@ -61,10 +93,26 @@ impl<'r> Responder<'r, 'static> for Error {
Error::AlreadySentRequest => Status::Conflict,
Error::Blocked => Status::Conflict,
Error::BlockedByOther => Status::Forbidden,
Error::NotFriends => Status::Forbidden,
Error::UnknownChannel => Status::NotFound,
Error::UnknownAttachment => Status::BadRequest,
Error::CannotEditMessage => Status::Forbidden,
Error::EmptyMessage => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => Status::Conflict,
Error::NotInGroup => Status::NotFound,
Error::FailedValidation { .. } => Status::UnprocessableEntity,
Error::DatabaseError { .. } => Status::InternalServerError,
Error::InternalError => Status::InternalServerError,
Error::MissingPermission => Status::Forbidden,
Error::InvalidOperation => Status::BadRequest,
Error::TooManyIds => Status::BadRequest,
Error::InvalidCredentials => Status::Forbidden,
Error::DuplicateNonce => Status::Conflict,
Error::VosoUnavailable => Status::BadRequest,
Error::NoEffect => Status::Ok,
};

View File

@@ -7,17 +7,38 @@ lazy_static! {
// Application Settings
pub static ref MONGO_URI: String =
env::var("REVOLT_MONGO_URI").expect("Missing REVOLT_MONGO_URI environment variable.");
pub static ref WS_HOST: String =
env::var("REVOLT_WS_HOST").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
pub static ref PUBLIC_URL: String =
env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable.");
pub static ref APP_URL: String =
env::var("REVOLT_APP_URL").expect("Missing REVOLT_APP_URL environment variable.");
pub static ref EXTERNAL_WS_URL: String =
env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable.");
pub static ref AUTUMN_URL: String =
env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
pub static ref JANUARY_URL: String =
env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
pub static ref VOSO_URL: String =
env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
pub static ref VOSO_WS_HOST: String =
env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string());
pub static ref VOSO_MANAGE_TOKEN: String =
env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string());
pub static ref HCAPTCHA_KEY: String =
env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string());
pub static ref HCAPTCHA_SITEKEY: String =
env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string());
pub static ref WS_HOST: String =
env::var("REVOLT_WS_HOST").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
pub static ref VAPID_PRIVATE_KEY: String =
env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable.");
pub static ref VAPID_PUBLIC_KEY: String =
env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable.");
// Application Flags
pub static ref DISABLE_REGISTRATION: bool = env::var("REVOLT_DISABLE_REGISTRATION").map_or(false, |v| v == "*1");
pub static ref DISABLE_REGISTRATION: bool = env::var("REVOLT_DISABLE_REGISTRATION").map_or(false, |v| v == "1");
pub static ref INVITE_ONLY: bool = env::var("REVOLT_INVITE_ONLY").map_or(false, |v| v == "1");
pub static ref USE_EMAIL: bool = env::var("REVOLT_USE_EMAIL_VERIFICATION").map_or(
env::var("REVOLT_SMTP_HOST").is_ok()
&& env::var("REVOLT_SMTP_USERNAME").is_ok()
@@ -26,26 +47,38 @@ lazy_static! {
|v| v == *"1"
);
pub static ref USE_HCAPTCHA: bool = env::var("REVOLT_HCAPTCHA_KEY").is_ok();
pub static ref USE_PROMETHEUS: bool = env::var("REVOLT_ENABLE_PROMETHEUS").map_or(false, |v| v == "1");
pub static ref USE_AUTUMN: bool = env::var("AUTUMN_PUBLIC_URL").is_ok();
pub static ref USE_JANUARY: bool = env::var("JANUARY_PUBLIC_URL").is_ok();
pub static ref USE_VOSO: bool = env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok();
// SMTP Settings
pub static ref SMTP_HOST: String =
env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string());
pub static ref SMTP_USERNAME: String =
env::var("SMTP_USERNAME").unwrap_or_else(|_| "".to_string());
env::var("REVOLT_SMTP_USERNAME").unwrap_or_else(|_| "".to_string());
pub static ref SMTP_PASSWORD: String =
env::var("SMTP_PASSWORD").unwrap_or_else(|_| "".to_string());
pub static ref SMTP_FROM: String = env::var("SMTP_FROM").unwrap_or_else(|_| "".to_string());
env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string());
pub static ref SMTP_FROM: String = env::var("REVOLT_SMTP_FROM").unwrap_or_else(|_| "".to_string());
// Application Logic Settings
pub static ref MAX_GROUP_SIZE: usize =
env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap();
}
pub fn preflight_checks() {
format!("{}", *APP_URL);
format!("{}", *MONGO_URI);
format!("{}", *PUBLIC_URL);
format!("{}", *EXTERNAL_WS_URL);
format!("{}", *VAPID_PRIVATE_KEY);
format!("{}", *VAPID_PUBLIC_KEY);
if *USE_EMAIL == false {
#[cfg(not(debug_assertions))]
{
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
panic!(
"Not letting you run this in production, set REVOLT_UNSAFE_NO_EMAIL=1 to run."
);
}
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
panic!("Running in production without email is not recommended, set REVOLT_UNSAFE_NO_EMAIL=1 to override.");
}
#[cfg(debug_assertions)]
@@ -54,10 +87,8 @@ pub fn preflight_checks() {
if *USE_HCAPTCHA == false {
#[cfg(not(debug_assertions))]
{
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
panic!("Not letting you run this in production, set REVOLT_UNSAFE_NO_CAPTCHA=1 to run.");
}
if !env::var("REVOLT_UNSAFE_NO_CAPTCHA").map_or(false, |v| v == *"1") {
panic!("Running in production without CAPTCHA is not recommended, set REVOLT_UNSAFE_NO_CAPTCHA=1 to override.");
}
#[cfg(debug_assertions)]

1
src/version.rs Normal file
View File

@@ -0,0 +1 @@
pub const VERSION: &str = "0.4.1-alpha.11";