Compare commits

..

90 Commits
0.3.0 ... 0.4.0

Author SHA1 Message Date
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
74 changed files with 3440 additions and 951 deletions

View File

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

773
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,45 +1,44 @@
[package]
name = "revolt"
version = "0.3.0-alpha"
version = "0.4.0-alpha.6"
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"
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,22 @@
# 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 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

View File

@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
enum Metadata {
File,
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,
filename: String,
metadata: Metadata,
content_type: String,
message_id: Option<String>,
}

View File

@@ -1,42 +1,20 @@
use crate::{
database::get_collection,
util::result::{Error, Result},
};
use mongodb::bson::to_document;
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, from_document, 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")]
@@ -48,19 +26,49 @@ pub enum Channel {
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")]
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 +83,61 @@ impl Channel {
with: "channel",
})?;
let channel_id = self.id().to_string();
ClientboundNotification::ChannelCreate(self)
.publish(channel_id)
.await
.ok();
Ok(())
}
pub async fn publish_update(&self, data: JsonValue) -> Result<()> {
let id = self.id().to_string();
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data,
}
.publish(id)
.await
.ok();
Ok(())
}
pub async fn delete(&self) -> Result<()> {
let id = self.id();
get_collection("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())
.await
.ok();
Ok(())
}
}

View File

@@ -1,22 +1,281 @@
// use mongodb::bson::DateTime;
// use serde::{Deserialize, Serialize};
use crate::util::variables::VAPID_PRIVATE_KEY;
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 MessageEmbed {
Dummy,
}
#[derive(Serialize, Deserialize, Debug)]
#[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 },
}
#[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<MessageEmbed>,
}
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",
})?;
}
}
_ => {}
}
let enc = serde_json::to_string(&self).unwrap();
ClientboundNotification::Message(self)
.publish(channel.id().to_string())
.await
.unwrap();
/*
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());
}
}
}
_ => {}
}
// 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)
.await
.ok();
Ok(())
}
pub async fn delete(&self) -> Result<()> {
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",
})?;
}
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)
.await
.ok();
Ok(())
}
}

View File

@@ -1,8 +1,10 @@
mod autumn;
mod channel;
mod guild;
mod message;
mod user;
pub use autumn::*;
pub use channel::*;
pub use guild::*;
pub use message::*;

View File

@@ -1,12 +1,14 @@
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 mongodb::bson::doc;
use mongodb::options::{Collation, FindOneOptions};
use serde::{Deserialize, Serialize};
#[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};
use validator::Validate;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus {
None,
User,
@@ -24,6 +26,37 @@ 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")]
text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
presence: Option<Presence>
}
#[derive(Validate, Serialize, Deserialize, Debug)]
pub struct UserProfile {
#[validate(length(min = 1, max = 2000))]
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
#[serde(rename = "_id")]
@@ -31,42 +64,75 @@ pub struct User {
pub username: String,
#[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 {
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));
}
self.profile = None;
self
}
/// 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)
}
}
}

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,14 +21,6 @@ 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.");
@@ -43,6 +39,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;
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)]
@@ -12,7 +10,7 @@ struct MigrationInfo {
revision: i32,
}
pub const LATEST_REVISION: i32 = 3;
pub const LATEST_REVISION: i32 = 0;
pub async fn migrate_database() {
let migrations = get_collection("migrations");
@@ -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;
@@ -55,99 +53,6 @@ pub async fn run_migrations(revision: i32) -> i32 {
info!("Running migration [revision 0]: Test migration system.");
}
if revision <= 1 {
info!("Running migration [revision 1]: Add channels to guild object.");
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 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
}
},
None,
)
.await
.expect("Failed to update guild.");
}
}
if revision <= 2 {
info!("Running migration [revision 2]: Add username index to users.");
get_db()
.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1
},
"name": "username",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
}
]
},
None,
)
.await
.expect("Failed to create username index.");
}
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION
}

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)
} 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,49 @@
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>,
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,
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_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,102 @@
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 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

@@ -18,17 +18,23 @@ pub mod notifications;
pub mod routes;
pub mod util;
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"));
@@ -59,10 +65,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,8 @@ pub enum WebSocketError {
#[serde(tag = "type")]
pub enum ServerboundNotification {
Authenticate(Session),
BeginTyping { channel: String },
EndTyping { channel: String },
}
#[derive(Serialize, Deserialize, Debug)]
@@ -33,73 +36,112 @@ 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,
},
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,
},
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,
status: RelationshipStatus,
},
UserPresence {
id: String,
online: bool,
},
}
impl ClientboundNotification {
pub async fn publish(self, topic: String) -> Result<(), String> {
prehandle_hook(&self); // ! TODO: this should be moved to pubsub
hive_pubsub::backend::mongo::publish(get_hive(), &topic, self).await
}
}
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.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.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,112 @@
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},
options::FindOptions,
};
pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
let mut users = vec![];
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);
if user_ids.len() > 0 {
let mut cursor = get_collection("users")
.find(
doc! {
"_id": {
"$in": user_ids.into_iter().collect::<Vec<String>>()
}
},
FindOptions::builder()
.projection(doc! { "_id": 1, "username": 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(&user)
.with_mutual_connection()
.with_user(&other)
.for_user_given()
.await?;
users.push(other.from(&user).with(permissions));
}
}
}
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,152 @@ 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)
.await
.ok();
}
}
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)
.await
.ok();
} 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)
.await
.ok();
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
}
}
}
future::ok(())
Ok(())
});
pin_mut!(fwd, incoming);
@@ -135,7 +220,8 @@ async fn accept(stream: TcpStream) {
info!("User {} disconnected.", &addr);
CONNECTIONS.lock().unwrap().remove(&addr);
if let Some(session) = session {
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() {
@@ -173,3 +259,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,114 @@
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())
.await
.ok();
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::UserLeft { id: user.id }),
)
.publish(&target)
.await
.ok();
Ok(())
}
}
}

View File

@@ -0,0 +1,76 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
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>,
}
#[patch("/<target>", data = "<info>")]
pub async fn req(user: User, target: Ref, info: Json<Data>) -> Result<()> {
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
if info.name.is_none() && info.description.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, .. } => {
get_collection("channels")
.update_one(
doc! { "_id": &id },
doc! { "$set": to_document(&info.0).map_err(|_| Error::DatabaseError { operation: "to_document", with: "data" })? },
None
)
.await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "channel" })?;
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!(info.0),
}
.publish(id.clone())
.await
.ok();
if let Some(name) = &info.name {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::ChannelRenamed {
name: name.clone(),
by: user.id,
}),
)
.publish(&target)
.await
.ok();
}
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,75 @@
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())
.await
.ok();
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,79 @@
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>>(),
last_message: None,
};
channel.clone().publish().await?;
Ok(json!(channel))
}

View File

@@ -0,0 +1,72 @@
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())
.await
.ok();
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,79 @@
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(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>,
}
#[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 mut cursor = get_collection("messages")
.find(
query,
FindOptions::builder()
.limit(options.limit.unwrap_or(50))
.sort(doc! {
"_id": -1
})
.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,128 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::{
bson::{doc, from_document},
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 attachments = get_collection("attachments");
let attachment = if let Some(attachment_id) = &message.attachment {
if let Some(doc) = attachments
.find_one(
doc! {
"_id": attachment_id,
"message_id": {
"$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": {
"message_id": &id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
Some(attachment)
} else {
return Err(Error::UnknownAttachment);
}
} 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,33 @@
use rocket::Route;
mod delete_channel;
mod edit_channel;
mod fetch_channel;
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,
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, AUTUMN_URL, DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY,
USE_AUTUMN, 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": "0.4.0-alpha.6",
"features": {
"registration": !*DISABLE_REGISTRATION,
"captcha": {
@@ -14,6 +17,19 @@ pub async fn root() -> JsonValue {
"key": HCAPTCHA_SITEKEY.to_string()
},
"email": *USE_EMAIL,
}
"invite_only": *INVITE_ONLY,
"autumn": {
"enabled": *USE_AUTUMN,
"url": *AUTUMN_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,37 @@
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() }) {
match get_relationship(&user, &target_id) {
RelationshipStatus::User => return Err(Error::NoEffect),
RelationshipStatus::Friend => return Err(Error::AlreadyFriends),
RelationshipStatus::Outgoing => return Err(Error::AlreadySentRequest),
@@ -141,9 +137,6 @@ pub async fn req(user: User, username: String) -> Result<JsonValue> {
)
.ok();
hive::subscribe_if_exists(user.id.clone(), target_id.to_string()).ok();
hive::subscribe_if_exists(target_id.to_string(), user.id.clone()).ok();
Ok(json!({ "status": "Outgoing" }))
}
Err(_) => Err(Error::DatabaseError {

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,7 @@ 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) {
match get_relationship(&user, &target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
RelationshipStatus::BlockedOther => {
col.update_one(
@@ -94,9 +90,6 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
)
.ok();
hive::subscribe_if_exists(user.id.clone(), target.id.clone()).ok();
hive::subscribe_if_exists(target.id.clone(), user.id.clone()).ok();
Ok(json!({ "status": "Blocked" }))
}
Err(_) => Err(Error::DatabaseError {

View File

@@ -0,0 +1,67 @@
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),
}
.publish(user.id.clone())
.await
.ok();
Ok(())
}

View File

@@ -0,0 +1,41 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
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 {
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<UserStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
profile: Option<UserProfile>,
}
#[patch("/<_ignore_id>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<()> {
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
get_collection("users")
.update_one(
doc! { "_id": &user.id },
doc! { "$set": to_document(&data.0).map_err(|_| Error::DatabaseError { operation: "to_document", with: "data" })? },
None
)
.await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "user" })?;
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!(data.0),
}
.publish(user.id.clone())
.await
.ok();
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._id": &user.id, "relations.status": "Friend" },
{ "relations._id": &target.id, "relations.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 +1,49 @@
use rocket::response::NamedFile;
use std::path::Path;
use md5;
use mongodb::bson::doc;
use mongodb::options::FindOneOptions;
use rocket::response::Redirect;
use urlencoding;
#[get("/<_target>/avatar")]
pub async fn req(_target: String) -> Option<NamedFile> {
NamedFile::open(Path::new("avatar.png")).await.ok()
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::PUBLIC_URL;
#[get("/<target>/avatar")]
pub async fn req(target: Ref) -> Result<Redirect> {
let doc = get_collection("accounts")
.find_one(
doc! {
"_id": &target.id
},
FindOneOptions::builder()
.projection(doc! { "email": 1 })
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.ok_or_else(|| Error::UnknownUser)?;
let email = doc
.get_str("email")
.map_err(|_| Error::DatabaseError {
operation: "get_str(email)",
with: "user",
})?
.to_lowercase();
let url = format!(
"https://www.gravatar.com/avatar/{:x}?s=128&d={}",
md5::compute(email),
urlencoding::encode(&format!(
"{}/users/{}/default_avatar",
*PUBLIC_URL, &target.id
))
);
dbg!(&url);
Ok(Redirect::to(url))
}

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,17 @@
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_avatar;
mod get_default_avatar;
mod open_dm;
mod remove_friend;
mod unblock_user;
@@ -15,13 +20,16 @@ pub fn routes() -> Vec<Route> {
routes![
// User Information
fetch_user::req,
edit_user::req,
change_username::req,
get_default_avatar::req,
get_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,7 @@ 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),
match get_relationship(&user, &target.id) {
RelationshipStatus::Friend
| RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => {
@@ -68,10 +59,6 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
)
.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 {
@@ -80,5 +67,6 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
}),
}
}
_ => Err(Error::NoEffect),
}
}

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,15 +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::None
| RelationshipStatus::User
| RelationshipStatus::BlockedOther
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing
| RelationshipStatus::Friend => Err(Error::NoEffect),
match get_relationship(&user, &target.id) {
RelationshipStatus::Blocked => {
match get_relationship(&target.fetch_user().await?, &user.as_ref()) {
match get_relationship(&target.fetch_user().await?, &user.id) {
RelationshipStatus::Blocked => {
col.update_one(
doc! {
@@ -100,10 +89,6 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
)
.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 {
@@ -115,5 +100,6 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
_ => 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,36 @@ 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 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 +45,37 @@ 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_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 +84,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)]