forked from jmug/stoatchat
Compare commits
103
Commits
0.3.0-rc.0
...
0.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fae0198343 | ||
|
|
10f116b4a3 | ||
|
|
6716a2d32b | ||
|
|
6cc92b877e | ||
|
|
cc0307f702 | ||
|
|
274d6f2b5a | ||
|
|
0f100213ba | ||
|
|
7293abfc53 | ||
|
|
cf7bb832da | ||
|
|
efc3794f3d | ||
|
|
a319e72655 | ||
|
|
2173b1e9f8 | ||
|
|
c086fe7ac4 | ||
|
|
fa960ebc94 | ||
|
|
b47067b311 | ||
|
|
2e996a487b | ||
|
|
92bface6ae | ||
|
|
8cfa5d7091 | ||
|
|
59b18fd376 | ||
|
|
f135a57a9b | ||
|
|
5da26cb833 | ||
|
|
c8981ac695 | ||
|
|
33b0658680 | ||
|
|
81002c75d2 | ||
|
|
f163cb65de | ||
|
|
f7bcd3ad93 | ||
|
|
9e2250783c | ||
|
|
2050a0609f | ||
|
|
a547177325 | ||
|
|
c3fa7bf74e | ||
|
|
7f5d6f2312 | ||
|
|
9492e145f9 | ||
|
|
78c42fd412 | ||
|
|
3797878d20 | ||
|
|
d2bf477fd8 | ||
|
|
c9bff789de | ||
|
|
56bd763820 | ||
|
|
32cd9d8a13 | ||
|
|
60731e1c70 | ||
|
|
37df18dbf7 | ||
|
|
0a9d5de369 | ||
|
|
d2864ac025 | ||
|
|
4eb76fd154 | ||
|
|
3c7852271a | ||
|
|
dadad271b4 | ||
|
|
db6047f2d3 | ||
|
|
5baf85a8e9 | ||
|
|
10cac358a9 | ||
|
|
64d2707366 | ||
|
|
78cfbf9d21 | ||
|
|
02077a4024 | ||
|
|
52e070c21c | ||
|
|
a2f14d2d37 | ||
|
|
a7ea29d821 | ||
|
|
b6b51bca26 | ||
|
|
2bafffbbc1 | ||
|
|
bdf5fb95d0 | ||
|
|
ea8de2400a | ||
|
|
f5e0b49bbb | ||
|
|
b0d07ec58f | ||
|
|
4c99d079e0 | ||
|
|
38bedbaaea | ||
|
|
ea276c3517 | ||
|
|
648e33a13e | ||
|
|
a76ec207b3 | ||
|
|
687b300bfc | ||
|
|
6586d8f02c | ||
|
|
a8a8c08c29 | ||
|
|
1806ea824b | ||
|
|
222a417fff | ||
|
|
3b85dcce14 | ||
|
|
f42480886b | ||
|
|
23ec2d61f1 | ||
|
|
abd8b1821d | ||
|
|
75a35831da | ||
|
|
99e2f874a1 | ||
|
|
11f7092fcd | ||
|
|
cb882ce1b2 | ||
|
|
5486a68bfa | ||
|
|
4aaf328435 | ||
|
|
9d251f794b | ||
|
|
1d390d483d | ||
|
|
81de29e723 | ||
|
|
984017eb61 | ||
|
|
5ab329dfdd | ||
|
|
3a63d502d9 | ||
|
|
8bb694a1c8 | ||
|
|
c38977e026 | ||
|
|
8cbba0671d | ||
|
|
c21d7c4620 | ||
|
|
901b29f49e | ||
|
|
34ac8f54ef | ||
|
|
c562d33c8f | ||
|
|
accd6d7789 | ||
|
|
ba08746e92 | ||
|
|
a1a921bbcb | ||
|
|
bd789b6825 | ||
|
|
c6ba72d924 | ||
|
|
c401663c47 | ||
|
|
3d3db80e61 | ||
|
|
15357008d6 | ||
|
|
124aa8a9bf | ||
|
|
af8731ac38 |
No files matched your search
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
assets
|
|
||||||
target
|
target
|
||||||
|
.mongo
|
||||||
.env
|
.env
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"rust-analyzer.diagnostics.disabled": [
|
||||||
|
"unresolved-macro-call"
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+532
-248
File diff suppressed because it is too large.
Load diff
+37
-33
@@ -1,44 +1,48 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "revolt"
|
name = "revolt"
|
||||||
version = "0.3.0-alpha"
|
# To help optimise CI and Docker builds.
|
||||||
|
# Version here is left as 0.0.0, please
|
||||||
|
# adjust and run ./set_version.sh instead.
|
||||||
|
version = "0.0.0"
|
||||||
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
authors = ["Paul Makles <paulmakles@gmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
futures = "0.3.8"
|
regex = "1"
|
||||||
many-to-many = "0.1.2"
|
md5 = "0.7.0"
|
||||||
impl_ops = "0.1.1"
|
log = "0.4.11"
|
||||||
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", "attributes"] }
|
|
||||||
|
|
||||||
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 }
|
|
||||||
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
|
|
||||||
|
|
||||||
once_cell = "1.4.1"
|
|
||||||
dotenv = "0.15.0"
|
|
||||||
ulid = "0.4.1"
|
ulid = "0.4.1"
|
||||||
|
rand = "0.7.3"
|
||||||
|
time = "0.2.16"
|
||||||
|
base64 = "0.13.0"
|
||||||
|
linkify = "0.6.0"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
futures = "0.3.8"
|
||||||
|
chrono = "0.4.15"
|
||||||
|
num_enum = "0.5.1"
|
||||||
|
impl_ops = "0.1.1"
|
||||||
|
web-push = "0.7.2"
|
||||||
|
once_cell = "1.4.1"
|
||||||
|
bitfield = "0.13.2"
|
||||||
|
env_logger = "0.7.1"
|
||||||
|
serde_json = "1.0.57"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
urlencoding = "1.1.1"
|
||||||
|
many-to-many = "0.1.2"
|
||||||
|
lettre = "0.10.0-alpha.1"
|
||||||
|
snafu = { version = "0.6.9" }
|
||||||
|
reqwest = { version = "0.10.8", features = ["json"] }
|
||||||
serde = { version = "1.0.115", features = ["derive"] }
|
serde = { version = "1.0.115", features = ["derive"] }
|
||||||
validator = { version = "0.11", features = ["derive"] }
|
validator = { version = "0.11", features = ["derive"] }
|
||||||
snafu = { version = "0.6.9" }
|
ctrlc = { version = "3.0", features = ["termination"] }
|
||||||
serde_json = "1.0.57"
|
hive_pubsub = { version = "0.4.4", features = ["mongo"] }
|
||||||
bitfield = "0.13.2"
|
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
|
||||||
|
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
|
||||||
reqwest = { version = "0.10.8", features = ["json"] }
|
rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" }
|
||||||
lazy_static = "1.4.0"
|
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
|
||||||
num_enum = "0.5.1"
|
rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "c52758a5087cd035b0ed9c6eacc942ba5468d2ce" }
|
||||||
chrono = "0.4.15"
|
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
|
||||||
time = "0.2.16"
|
rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
|
||||||
rand = "0.7.3"
|
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false }
|
||||||
regex = "1"
|
|
||||||
|
|
||||||
lettre = "0.10.0-alpha.1"
|
|
||||||
env_logger = "0.7.1"
|
|
||||||
log = "0.4.11"
|
|
||||||
+12
-3
@@ -1,16 +1,25 @@
|
|||||||
# Build Stage
|
# 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
|
WORKDIR /home/rust/src
|
||||||
|
|
||||||
RUN USER=root cargo new --bin revolt
|
RUN USER=root cargo new --bin revolt
|
||||||
WORKDIR ./revolt
|
WORKDIR /home/rust/src/revolt
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY src/bin/dummy.rs ./src/bin/dummy.rs
|
||||||
|
RUN cargo build --release --bin dummy
|
||||||
|
|
||||||
|
COPY assets/templates ./assets/templates
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
# Bundle Stage
|
# 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 --from=builder /home/rust/src/revolt/target/x86_64-unknown-linux-musl/release/revolt ./
|
||||||
|
COPY assets ./assets
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
EXPOSE 9000
|
EXPOSE 9000
|
||||||
|
ENV ROCKET_ADDRESS 0.0.0.0
|
||||||
|
ENV ROCKET_PORT 8000
|
||||||
CMD ["./revolt"]
|
CMD ["./revolt"]
|
||||||
@@ -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. · <a style="color: white;" href="https://revolt.chat">Website</a> · <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>
|
||||||
@@ -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. · <a style="color: white;" href="https://revolt.chat">Website</a> · <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>
|
||||||
+1
-1
@@ -14,5 +14,5 @@ services:
|
|||||||
- REVOLT_UNSAFE_NO_CAPTCHA=1
|
- REVOLT_UNSAFE_NO_CAPTCHA=1
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
- "9999:9999"
|
- "9000:9000"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
source set_version.sh
|
||||||
|
|
||||||
|
docker build -t revoltchat/server:${version} . &&
|
||||||
|
docker push revoltchat/server:${version}
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
export version=0.4.1-alpha.11
|
||||||
|
echo "pub const VERSION: &str = \"${version}\";" > src/version.rs
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
fn main() {}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
use mongodb::bson::{doc, from_document};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::database::*;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
enum Metadata {
|
||||||
|
File,
|
||||||
|
Text,
|
||||||
|
Image { width: isize, height: isize },
|
||||||
|
Video { width: isize, height: isize },
|
||||||
|
Audio,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct File {
|
||||||
|
#[serde(rename = "_id")]
|
||||||
|
pub id: String,
|
||||||
|
tag: String,
|
||||||
|
filename: String,
|
||||||
|
metadata: Metadata,
|
||||||
|
content_type: String,
|
||||||
|
size: isize,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
deleted: Option<bool>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
message_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
user_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
server_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
object_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
pub async fn find_and_use(
|
||||||
|
attachment_id: &str,
|
||||||
|
tag: &str,
|
||||||
|
parent_type: &str,
|
||||||
|
parent_id: &str,
|
||||||
|
) -> Result<File> {
|
||||||
|
let attachments = get_collection("attachments");
|
||||||
|
let key = format!("{}_id", parent_type);
|
||||||
|
if let Some(doc) = attachments
|
||||||
|
.find_one(
|
||||||
|
doc! {
|
||||||
|
"_id": attachment_id,
|
||||||
|
"tag": &tag,
|
||||||
|
key.clone(): {
|
||||||
|
"$exists": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "find_one",
|
||||||
|
with: "attachment",
|
||||||
|
})?
|
||||||
|
{
|
||||||
|
let attachment = from_document::<File>(doc).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "from_document",
|
||||||
|
with: "attachment",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
attachments
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &attachment.id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
key: &parent_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "attachment",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(attachment)
|
||||||
|
} else {
|
||||||
|
Err(Error::UnknownAttachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self) -> Result<()> {
|
||||||
|
get_collection("attachments")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &self.id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"deleted": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "attachment",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,24 @@
|
|||||||
use crate::{
|
use crate::database::*;
|
||||||
database::get_collection,
|
use crate::notifications::events::ClientboundNotification;
|
||||||
util::result::{Error, Result},
|
use crate::util::result::{Error, Result};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use mongodb::{
|
||||||
|
bson::{doc, from_document, to_document, Document},
|
||||||
|
options::FindOptions,
|
||||||
};
|
};
|
||||||
use mongodb::bson::to_document;
|
use rocket_contrib::json::JsonValue;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/*#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct LastMessage {
|
pub struct LastMessage {
|
||||||
|
#[serde(rename = "_id")]
|
||||||
id: String,
|
id: String,
|
||||||
user_id: String,
|
author: String,
|
||||||
short_content: String,
|
short: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Channel {
|
#[serde(tag = "channel_type")]
|
||||||
#[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")]
|
|
||||||
pub enum Channel {
|
pub enum Channel {
|
||||||
SavedMessages {
|
SavedMessages {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
@@ -46,21 +28,56 @@ pub enum Channel {
|
|||||||
DirectMessage {
|
DirectMessage {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
id: String,
|
id: String,
|
||||||
|
|
||||||
active: bool,
|
active: bool,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
last_message: Option<LastMessage>,
|
||||||
},
|
},
|
||||||
Group {
|
Group {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
id: String,
|
id: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
nonce: Option<String>,
|
||||||
|
|
||||||
name: String,
|
name: String,
|
||||||
owner: String,
|
owner: String,
|
||||||
description: String,
|
description: String,
|
||||||
recipients: Vec<String>,
|
recipients: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
icon: Option<File>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
last_message: Option<LastMessage>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
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")
|
get_collection("channels")
|
||||||
.insert_one(
|
.insert_one(
|
||||||
to_document(&self).map_err(|_| Error::DatabaseError {
|
to_document(&self).map_err(|_| Error::DatabaseError {
|
||||||
@@ -75,6 +92,109 @@ impl Channel {
|
|||||||
with: "channel",
|
with: "channel",
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let channel_id = self.id().to_string();
|
||||||
|
ClientboundNotification::ChannelCreate(self).publish(channel_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_update(&self, data: JsonValue) -> Result<()> {
|
||||||
|
let id = self.id().to_string();
|
||||||
|
ClientboundNotification::ChannelUpdate {
|
||||||
|
id: id.clone(),
|
||||||
|
data,
|
||||||
|
clear: None,
|
||||||
|
}
|
||||||
|
.publish(id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self) -> Result<()> {
|
||||||
|
let id = self.id();
|
||||||
|
let messages = get_collection("messages");
|
||||||
|
|
||||||
|
// Check if there are any attachments we need to delete.
|
||||||
|
let message_ids = messages
|
||||||
|
.find(
|
||||||
|
doc! {
|
||||||
|
"channel": id,
|
||||||
|
"attachment": {
|
||||||
|
"$exists": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "fetch_many",
|
||||||
|
with: "messages",
|
||||||
|
})?
|
||||||
|
.filter_map(async move |s| s.ok())
|
||||||
|
.collect::<Vec<Document>>()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
// If we found any, mark them as deleted.
|
||||||
|
if message_ids.len() > 0 {
|
||||||
|
get_collection("attachments")
|
||||||
|
.update_many(
|
||||||
|
doc! {
|
||||||
|
"message_id": {
|
||||||
|
"$in": message_ids
|
||||||
|
}
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"deleted": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_many",
|
||||||
|
with: "attachments",
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// And then delete said messages.
|
||||||
|
messages
|
||||||
|
.delete_many(
|
||||||
|
doc! {
|
||||||
|
"channel": id
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "delete_many",
|
||||||
|
with: "messages",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
get_collection("channels")
|
||||||
|
.delete_one(
|
||||||
|
doc! {
|
||||||
|
"_id": id
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "delete_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
ClientboundNotification::ChannelDelete { id: id.to_string() }.publish(id.to_string());
|
||||||
|
|
||||||
|
if let Channel::Group { icon, .. } = self {
|
||||||
|
if let Some(attachment) = icon {
|
||||||
|
attachment.delete().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
use crate::util::{
|
||||||
|
result::{Error, Result},
|
||||||
|
variables::JANUARY_URL,
|
||||||
|
};
|
||||||
|
use linkify::{LinkFinder, LinkKind};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum ImageSize {
|
||||||
|
Large,
|
||||||
|
Preview,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct Image {
|
||||||
|
pub url: String,
|
||||||
|
pub width: isize,
|
||||||
|
pub height: isize,
|
||||||
|
pub size: ImageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct Video {
|
||||||
|
pub url: String,
|
||||||
|
pub width: isize,
|
||||||
|
pub height: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum TwitchType {
|
||||||
|
Channel,
|
||||||
|
Video,
|
||||||
|
Clip,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub enum BandcampType {
|
||||||
|
Album,
|
||||||
|
Track,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum Special {
|
||||||
|
None,
|
||||||
|
YouTube {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
Twitch {
|
||||||
|
content_type: TwitchType,
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
Spotify {
|
||||||
|
content_type: String,
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
Soundcloud,
|
||||||
|
Bandcamp {
|
||||||
|
content_type: BandcampType,
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct Metadata {
|
||||||
|
url: Option<String>,
|
||||||
|
special: Option<Special>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
title: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
description: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
image: Option<Image>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
video: Option<Video>,
|
||||||
|
|
||||||
|
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
// opengraph_type: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
site_name: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
icon_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum Embed {
|
||||||
|
Website(Metadata),
|
||||||
|
Image(Image),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Embed {
|
||||||
|
pub async fn generate(content: String) -> Result<Vec<Embed>> {
|
||||||
|
let content = content
|
||||||
|
.split("\n")
|
||||||
|
.map(|v| {
|
||||||
|
// Ignore quoted lines.
|
||||||
|
if let Some(c) = v.chars().next() {
|
||||||
|
if c == '>' {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
// ! FIXME: allow multiple links
|
||||||
|
// ! FIXME: prevent generation if link is surrounded with < >
|
||||||
|
let mut finder = LinkFinder::new();
|
||||||
|
finder.kinds(&[LinkKind::Url]);
|
||||||
|
let links: Vec<_> = finder.links(&content).collect();
|
||||||
|
|
||||||
|
if links.len() == 0 {
|
||||||
|
return Err(Error::LabelMe);
|
||||||
|
}
|
||||||
|
|
||||||
|
let link = &links[0];
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let result = client
|
||||||
|
.get(&format!("{}/embed", *JANUARY_URL))
|
||||||
|
.query(&[("url", link.as_str())])
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Err(_) => return Err(Error::LabelMe),
|
||||||
|
Ok(result) => match result.status() {
|
||||||
|
reqwest::StatusCode::OK => {
|
||||||
|
let res: Embed = result.json().await.map_err(|_| Error::InvalidOperation)?;
|
||||||
|
|
||||||
|
Ok(vec![res])
|
||||||
|
}
|
||||||
|
_ => return Err(Error::LabelMe),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,22 +1,321 @@
|
|||||||
// use mongodb::bson::DateTime;
|
use crate::util::variables::{VAPID_PRIVATE_KEY, USE_JANUARY};
|
||||||
// use serde::{Deserialize, Serialize};
|
use crate::{
|
||||||
|
database::*,
|
||||||
|
notifications::{events::ClientboundNotification, websocket::is_online},
|
||||||
|
util::result::{Error, Result},
|
||||||
|
};
|
||||||
|
|
||||||
/*#[derive(Serialize, Deserialize, Debug)]
|
use futures::StreamExt;
|
||||||
pub struct PreviousEntry {
|
use mongodb::{
|
||||||
pub content: String,
|
bson::{doc, to_bson, DateTime},
|
||||||
pub time: DateTime,
|
options::FindOptions,
|
||||||
|
};
|
||||||
|
use rocket_contrib::json::JsonValue;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ulid::Ulid;
|
||||||
|
use web_push::{
|
||||||
|
ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum SystemMessage {
|
||||||
|
#[serde(rename = "text")]
|
||||||
|
Text { content: String },
|
||||||
|
#[serde(rename = "user_added")]
|
||||||
|
UserAdded { id: String, by: String },
|
||||||
|
#[serde(rename = "user_remove")]
|
||||||
|
UserRemove { id: String, by: String },
|
||||||
|
#[serde(rename = "user_left")]
|
||||||
|
UserLeft { id: String },
|
||||||
|
#[serde(rename = "channel_renamed")]
|
||||||
|
ChannelRenamed { name: String, by: String },
|
||||||
|
#[serde(rename = "channel_description_changed")]
|
||||||
|
ChannelDescriptionChanged { by: String },
|
||||||
|
#[serde(rename = "channel_icon_changed")]
|
||||||
|
ChannelIconChanged { by: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum Content {
|
||||||
|
Text(String),
|
||||||
|
SystemMessage(SystemMessage),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Message {
|
pub struct Message {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub nonce: Option<String>,
|
pub nonce: Option<String>,
|
||||||
pub channel: String,
|
pub channel: String,
|
||||||
pub author: 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>,
|
pub edited: Option<DateTime>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub embeds: Option<Vec<Embed>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub previous_content: Vec<PreviousEntry>,
|
impl Message {
|
||||||
}*/
|
pub fn create(author: String, channel: String, content: Content) -> Message {
|
||||||
|
Message {
|
||||||
|
id: Ulid::new().to_string(),
|
||||||
|
nonce: None,
|
||||||
|
channel,
|
||||||
|
author,
|
||||||
|
|
||||||
|
content,
|
||||||
|
attachment: None,
|
||||||
|
edited: None,
|
||||||
|
embeds: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish(self, channel: &Channel) -> Result<()> {
|
||||||
|
get_collection("messages")
|
||||||
|
.insert_one(to_bson(&self).unwrap().as_document().unwrap().clone(), None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "insert_one",
|
||||||
|
with: "message",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut set = if let Content::Text(text) = &self.content {
|
||||||
|
doc! {
|
||||||
|
"last_message": {
|
||||||
|
"_id": self.id.clone(),
|
||||||
|
"author": self.author.clone(),
|
||||||
|
"short": text.chars().take(24).collect::<String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doc! {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ! FIXME: temp code
|
||||||
|
let channels = get_collection("channels");
|
||||||
|
match &channel {
|
||||||
|
Channel::DirectMessage { id, .. } => {
|
||||||
|
set.insert("active", true);
|
||||||
|
channels
|
||||||
|
.update_one(
|
||||||
|
doc! { "_id": id },
|
||||||
|
doc! {
|
||||||
|
"$set": set
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Channel::Group { id, .. } => {
|
||||||
|
if let Content::Text(_) = &self.content {
|
||||||
|
channels
|
||||||
|
.update_one(
|
||||||
|
doc! { "_id": id },
|
||||||
|
doc! {
|
||||||
|
"$set": set
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.process_embed();
|
||||||
|
|
||||||
|
let enc = serde_json::to_string(&self).unwrap();
|
||||||
|
ClientboundNotification::Message(self).publish(channel.id().to_string());
|
||||||
|
|
||||||
|
/*
|
||||||
|
Web Push Test Code
|
||||||
|
! FIXME: temp code
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Find all offline users.
|
||||||
|
let mut target_ids = vec![];
|
||||||
|
match &channel {
|
||||||
|
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
|
||||||
|
for recipient in recipients {
|
||||||
|
if !is_online(recipient) {
|
||||||
|
target_ids.push(recipient.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async_std::task::spawn(async move {
|
||||||
|
// Fetch their corresponding sessions.
|
||||||
|
if let Ok(mut cursor) = get_collection("accounts")
|
||||||
|
.find(
|
||||||
|
doc! {
|
||||||
|
"_id": {
|
||||||
|
"$in": target_ids
|
||||||
|
},
|
||||||
|
"sessions.subscription": {
|
||||||
|
"$exists": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FindOptions::builder()
|
||||||
|
.projection(doc! { "sessions": 1 })
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let mut subscriptions = vec![];
|
||||||
|
while let Some(result) = cursor.next().await {
|
||||||
|
if let Ok(doc) = result {
|
||||||
|
if let Ok(sessions) = doc.get_array("sessions") {
|
||||||
|
for session in sessions {
|
||||||
|
if let Some(doc) = session.as_document() {
|
||||||
|
if let Ok(sub) = doc.get_document("subscription") {
|
||||||
|
let endpoint = sub.get_str("endpoint").unwrap().to_string();
|
||||||
|
let p256dh = sub.get_str("p256dh").unwrap().to_string();
|
||||||
|
let auth = sub.get_str("auth").unwrap().to_string();
|
||||||
|
|
||||||
|
subscriptions
|
||||||
|
.push(SubscriptionInfo::new(endpoint, p256dh, auth));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if subscriptions.len() > 0 {
|
||||||
|
let client = WebPushClient::new();
|
||||||
|
let key =
|
||||||
|
base64::decode_config(VAPID_PRIVATE_KEY.clone(), base64::URL_SAFE).unwrap();
|
||||||
|
|
||||||
|
for subscription in subscriptions {
|
||||||
|
let mut builder = WebPushMessageBuilder::new(&subscription).unwrap();
|
||||||
|
let sig_builder = VapidSignatureBuilder::from_pem(
|
||||||
|
std::io::Cursor::new(&key),
|
||||||
|
&subscription,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let signature = sig_builder.build().unwrap();
|
||||||
|
builder.set_vapid_signature(signature);
|
||||||
|
builder.set_payload(ContentEncoding::AesGcm, enc.as_bytes());
|
||||||
|
let m = builder.build().unwrap();
|
||||||
|
client.send(m).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_update(self, data: JsonValue) -> Result<()> {
|
||||||
|
let channel = self.channel.clone();
|
||||||
|
ClientboundNotification::MessageUpdate {
|
||||||
|
id: self.id.clone(),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
.publish(channel);
|
||||||
|
self.process_embed();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_embed(&self) {
|
||||||
|
if !*USE_JANUARY {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Content::Text(text) = &self.content {
|
||||||
|
let id = self.id.clone();
|
||||||
|
let content = text.clone();
|
||||||
|
let channel = self.channel.clone();
|
||||||
|
async_std::task::spawn(async move {
|
||||||
|
if let Ok(embeds) = Embed::generate(content).await {
|
||||||
|
if let Ok(bson) = to_bson(&embeds) {
|
||||||
|
if let Ok(_) = get_collection("messages")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"embeds": bson
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
ClientboundNotification::MessageUpdate {
|
||||||
|
id,
|
||||||
|
data: json!({ "embeds": embeds }),
|
||||||
|
}
|
||||||
|
.publish(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(&self) -> Result<()> {
|
||||||
|
if let Some(attachment) = &self.attachment {
|
||||||
|
attachment.delete().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_collection("messages")
|
||||||
|
.delete_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &self.id
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "delete_one",
|
||||||
|
with: "message",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let channel = self.channel.clone();
|
||||||
|
ClientboundNotification::MessageDelete {
|
||||||
|
id: self.id.clone(),
|
||||||
|
}
|
||||||
|
.publish(channel);
|
||||||
|
|
||||||
|
if let Some(attachment) = &self.attachment {
|
||||||
|
get_collection("attachments")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &attachment.id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"deleted": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "attachment",
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
|
mod autumn;
|
||||||
mod channel;
|
mod channel;
|
||||||
mod guild;
|
mod january;
|
||||||
mod message;
|
mod message;
|
||||||
|
mod server;
|
||||||
mod user;
|
mod user;
|
||||||
|
|
||||||
|
pub use autumn::*;
|
||||||
pub use channel::*;
|
pub use channel::*;
|
||||||
pub use guild::*;
|
pub use january::*;
|
||||||
pub use message::*;
|
pub use message::*;
|
||||||
|
pub use server::*;
|
||||||
pub use user::*;
|
pub use user::*;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/*#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct MemberCompositeKey {
|
pub struct MemberCompositeKey {
|
||||||
pub guild: String,
|
pub guild: String,
|
||||||
pub user: String,
|
pub user: String,
|
||||||
@@ -27,12 +27,11 @@ pub struct Ban {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct Guild {
|
pub struct Server {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
// pub nonce: String, used internally
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub name: String,
|
pub nonce: Option<String>,
|
||||||
pub description: String,
|
|
||||||
pub owner: String,
|
pub owner: String,
|
||||||
|
|
||||||
pub channels: Vec<String>,
|
pub channels: Vec<String>,
|
||||||
@@ -40,4 +39,4 @@ pub struct Guild {
|
|||||||
pub bans: Vec<Ban>,
|
pub bans: Vec<Ban>,
|
||||||
|
|
||||||
pub default_permissions: u32,
|
pub default_permissions: u32,
|
||||||
}*/
|
}
|
||||||
@@ -1,6 +1,18 @@
|
|||||||
|
use futures::StreamExt;
|
||||||
|
use mongodb::options::{Collation, FindOneOptions};
|
||||||
|
use mongodb::{
|
||||||
|
bson::{doc, from_document},
|
||||||
|
options::FindOptions,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
use crate::database::permissions::user::UserPermissions;
|
||||||
|
use crate::database::*;
|
||||||
|
use crate::notifications::websocket::is_online;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
pub enum RelationshipStatus {
|
pub enum RelationshipStatus {
|
||||||
None,
|
None,
|
||||||
User,
|
User,
|
||||||
@@ -18,17 +30,180 @@ pub struct Relationship {
|
|||||||
pub status: RelationshipStatus,
|
pub status: RelationshipStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub enum Badge {
|
||||||
|
Developer = 1,
|
||||||
|
Translator = 2,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub enum Presence {
|
||||||
|
Online,
|
||||||
|
Idle,
|
||||||
|
Busy,
|
||||||
|
Invisible,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct UserStatus {
|
||||||
|
#[validate(length(min = 1, max = 128))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub text: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub presence: Option<Presence>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct UserProfile {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub content: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub background: Option<File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// When changing this struct, update notifications/payload.rs#80
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
#[serde(rename = "_id")]
|
#[serde(rename = "_id")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub avatar: Option<File>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub relations: Option<Vec<Relationship>>,
|
pub relations: Option<Vec<Relationship>>,
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
|
||||||
// ? This should never be pushed to the collection.
|
// ? This should never be pushed to the collection.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub relationship: Option<RelationshipStatus>,
|
pub relationship: Option<RelationshipStatus>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub online: Option<bool>,
|
pub online: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
/// Mutate the user object to include relationship as seen by user.
|
||||||
|
pub fn from(mut self, user: &User) -> User {
|
||||||
|
self.relationship = Some(RelationshipStatus::None);
|
||||||
|
|
||||||
|
if self.id == user.id {
|
||||||
|
self.relationship = Some(RelationshipStatus::User);
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.relations = None;
|
||||||
|
if let Some(relations) = &user.relations {
|
||||||
|
if let Some(relationship) = relations.iter().find(|x| self.id == x.id) {
|
||||||
|
self.relationship = Some(relationship.status.clone());
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mutate the user object to appear as seen by user.
|
||||||
|
pub fn with(mut self, permissions: UserPermissions<[u32; 1]>) -> User {
|
||||||
|
if permissions.get_view_profile() {
|
||||||
|
self.online = Some(is_online(&self.id));
|
||||||
|
} else {
|
||||||
|
self.status = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.profile = None;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mutate the user object to appear as seen by user.
|
||||||
|
/// Also overrides the relationship status.
|
||||||
|
pub async fn from_override(
|
||||||
|
mut self,
|
||||||
|
user: &User,
|
||||||
|
relationship: RelationshipStatus,
|
||||||
|
) -> Result<User> {
|
||||||
|
let permissions = PermissionCalculator::new(&user)
|
||||||
|
.with_relationship(&relationship)
|
||||||
|
.for_user(&self.id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.relations = None;
|
||||||
|
self.relationship = Some(relationship);
|
||||||
|
Ok(self.with(permissions))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility function for checking claimed usernames.
|
||||||
|
pub async fn is_username_taken(username: &str) -> Result<bool> {
|
||||||
|
if username.to_lowercase() == "revolt" && username.to_lowercase() == "admin" {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if get_collection("users")
|
||||||
|
.find_one(
|
||||||
|
doc! {
|
||||||
|
"username": username
|
||||||
|
},
|
||||||
|
FindOneOptions::builder()
|
||||||
|
.collation(Collation::builder().locale("en").strength(2).build())
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "find_one",
|
||||||
|
with: "user",
|
||||||
|
})?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utility function for fetching multiple users from the perspective of one.
|
||||||
|
pub async fn fetch_multiple_users(&self, user_ids: Vec<String>) -> Result<Vec<User>> {
|
||||||
|
let mut users = vec![];
|
||||||
|
let mut cursor = get_collection("users")
|
||||||
|
.find(
|
||||||
|
doc! {
|
||||||
|
"_id": {
|
||||||
|
"$in": user_ids
|
||||||
|
}
|
||||||
|
},
|
||||||
|
FindOptions::builder()
|
||||||
|
.projection(
|
||||||
|
doc! { "_id": 1, "username": 1, "avatar": 1, "badges": 1, "status": 1 },
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "find",
|
||||||
|
with: "users",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
while let Some(result) = cursor.next().await {
|
||||||
|
if let Ok(doc) = result {
|
||||||
|
let other: User = from_document(doc).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "from_document",
|
||||||
|
with: "user",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let permissions = PermissionCalculator::new(&self)
|
||||||
|
.with_mutual_connection()
|
||||||
|
.with_user(&other)
|
||||||
|
.for_user_given()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
users.push(other.from(&self).with(permissions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,4 +2,3 @@ pub mod reference;
|
|||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
pub use reference::Ref;
|
pub use reference::Ref;
|
||||||
// pub use user::*;
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::util::result::{Error, Result};
|
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::http::RawStr;
|
||||||
use rocket::request::FromParam;
|
use rocket::request::FromParam;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
#[derive(Validate, Serialize, Deserialize)]
|
#[derive(Validate, Serialize, Deserialize)]
|
||||||
@@ -18,8 +18,8 @@ impl Ref {
|
|||||||
Ok(Ref { id })
|
Ok(Ref { id })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_user(&self) -> Result<User> {
|
pub async fn fetch<T: DeserializeOwned>(&self, collection: &'static str) -> Result<T> {
|
||||||
let doc = get_collection("users")
|
let doc = get_collection(&collection)
|
||||||
.find_one(
|
.find_one(
|
||||||
doc! {
|
doc! {
|
||||||
"_id": &self.id
|
"_id": &self.id
|
||||||
@@ -29,16 +29,31 @@ impl Ref {
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| Error::DatabaseError {
|
.map_err(|_| Error::DatabaseError {
|
||||||
operation: "find_one",
|
operation: "find_one",
|
||||||
with: "user",
|
with: &collection,
|
||||||
})?
|
})?
|
||||||
.ok_or_else(|| Error::UnknownUser)?;
|
.ok_or_else(|| Error::UnknownUser)?;
|
||||||
|
|
||||||
Ok(
|
Ok(from_document::<T>(doc).map_err(|_| Error::DatabaseError {
|
||||||
from_bson(Bson::Document(doc)).map_err(|_| Error::DatabaseError {
|
operation: "from_document",
|
||||||
operation: "from_bson",
|
with: &collection,
|
||||||
with: "user",
|
})?)
|
||||||
})?,
|
}
|
||||||
)
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
|
|
||||||
use mongodb::bson::{doc, from_bson, Bson};
|
use mongodb::bson::{doc, from_document};
|
||||||
use rauth::auth::Session;
|
use rauth::auth::Session;
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
use rocket::request::{self, FromRequest, Outcome, Request};
|
use rocket::request::{self, FromRequest, Outcome, Request};
|
||||||
@@ -22,14 +22,17 @@ impl<'a, 'r> FromRequest<'a, 'r> for User {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if let Some(doc) = result {
|
if let Some(doc) = result {
|
||||||
Outcome::Success(from_bson(Bson::Document(doc)).unwrap())
|
Outcome::Success(from_document(doc).unwrap())
|
||||||
} else {
|
} else {
|
||||||
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
|
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Outcome::Failure((
|
Outcome::Failure((
|
||||||
Status::InternalServerError,
|
Status::InternalServerError,
|
||||||
rauth::util::Error::DatabaseError,
|
rauth::util::Error::DatabaseError {
|
||||||
|
operation: "find_one",
|
||||||
|
with: "user",
|
||||||
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ pub async fn create_database() {
|
|||||||
info!("Creating database.");
|
info!("Creating database.");
|
||||||
let db = get_db();
|
let db = get_db();
|
||||||
|
|
||||||
|
db.create_collection("accounts", None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create accounts collection.");
|
||||||
|
|
||||||
db.create_collection("users", None)
|
db.create_collection("users", None)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create users collection.");
|
.expect("Failed to create users collection.");
|
||||||
@@ -17,22 +21,22 @@ pub async fn create_database() {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create channels collection.");
|
.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)
|
db.create_collection("messages", None)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create messages collection.");
|
.expect("Failed to create messages collection.");
|
||||||
|
|
||||||
|
db.create_collection("servers", None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create servers collection.");
|
||||||
|
|
||||||
db.create_collection("migrations", None)
|
db.create_collection("migrations", None)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create migrations collection.");
|
.expect("Failed to create migrations collection.");
|
||||||
|
|
||||||
|
db.create_collection("attachments", None)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create attachments collection.");
|
||||||
|
|
||||||
db.create_collection(
|
db.create_collection(
|
||||||
"pubsub",
|
"pubsub",
|
||||||
CreateCollectionOptions::builder()
|
CreateCollectionOptions::builder()
|
||||||
@@ -43,6 +47,39 @@ pub async fn create_database() {
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create pubsub collection.");
|
.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(
|
db.run_command(
|
||||||
doc! {
|
doc! {
|
||||||
"createIndexes": "users",
|
"createIndexes": "users",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
use super::super::{get_collection, get_db};
|
use crate::database::{get_collection, get_db};
|
||||||
|
|
||||||
use crate::rocket::futures::StreamExt;
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use mongodb::bson::{doc, from_bson, Bson};
|
use mongodb::bson::{doc, from_document};
|
||||||
use mongodb::options::FindOptions;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -23,7 +21,7 @@ pub async fn migrate_database() {
|
|||||||
|
|
||||||
if let Some(doc) = data {
|
if let Some(doc) = data {
|
||||||
let info: MigrationInfo =
|
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;
|
let revision = run_migrations(info.revision).await;
|
||||||
|
|
||||||
@@ -56,96 +54,37 @@ pub async fn run_migrations(revision: i32) -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if revision <= 1 {
|
if revision <= 1 {
|
||||||
info!("Running migration [revision 1]: Add channels to guild object.");
|
info!("Running migration [revision 1 / 2021-04-24]: Migrate to Autumn v1.0.0.");
|
||||||
|
|
||||||
let col = get_collection("guilds");
|
let messages = get_collection("messages");
|
||||||
let mut guilds = col
|
let attachments = get_collection("attachments");
|
||||||
.find(
|
|
||||||
None,
|
|
||||||
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("Failed to fetch guilds.");
|
|
||||||
|
|
||||||
let mut result = get_collection("channels")
|
messages
|
||||||
.find(
|
.update_many(
|
||||||
doc! {
|
doc! { "attachment": { "$exists": 1 } },
|
||||||
"type": 2
|
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0 } },
|
||||||
},
|
|
||||||
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,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to update guild.");
|
.expect("Failed to update messages.");
|
||||||
}
|
|
||||||
|
attachments
|
||||||
|
.update_many(
|
||||||
|
doc! {},
|
||||||
|
doc! { "$set": { "tag": "attachments", "size": 0 } },
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to update attachments.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if revision <= 2 {
|
if revision <= 2 {
|
||||||
info!("Running migration [revision 2]: Add username index to users.");
|
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
|
||||||
|
|
||||||
get_db()
|
get_db()
|
||||||
.run_command(
|
.create_collection("servers", None)
|
||||||
doc! {
|
|
||||||
"createIndexes": "users",
|
|
||||||
"indexes": [
|
|
||||||
{
|
|
||||||
"key": {
|
|
||||||
"username": 1
|
|
||||||
},
|
|
||||||
"name": "username",
|
|
||||||
"unique": true,
|
|
||||||
"collation": {
|
|
||||||
"locale": "en",
|
|
||||||
"strength": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create username index.");
|
.expect("Failed to create servers collection.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
use crate::database::*;
|
||||||
|
use crate::util::result::Result;
|
||||||
|
|
||||||
|
use super::PermissionCalculator;
|
||||||
|
|
||||||
|
use num_enum::TryFromPrimitive;
|
||||||
|
use std::ops;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
|
||||||
|
#[repr(u32)]
|
||||||
|
pub enum ChannelPermission {
|
||||||
|
View = 1,
|
||||||
|
SendMessage = 2,
|
||||||
|
ManageMessages = 4,
|
||||||
|
ManageChannel = 8,
|
||||||
|
VoiceCall = 16,
|
||||||
|
}
|
||||||
|
|
||||||
|
bitfield! {
|
||||||
|
pub struct ChannelPermissions(MSB0 [u32]);
|
||||||
|
u32;
|
||||||
|
pub get_view, _: 31;
|
||||||
|
pub get_send_message, _: 30;
|
||||||
|
pub get_manage_messages, _: 29;
|
||||||
|
pub get_manage_channel, _: 28;
|
||||||
|
pub get_voice_call, _: 27;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 });
|
||||||
|
impl_op_ex_commutative!(+ |a: &u32, b: &ChannelPermission| -> u32 { *a | *b as u32 });
|
||||||
|
|
||||||
|
impl<'a> PermissionCalculator<'a> {
|
||||||
|
pub async fn calculate_channel(self) -> Result<u32> {
|
||||||
|
let channel = if let Some(channel) = self.channel {
|
||||||
|
channel
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
match channel {
|
||||||
|
Channel::SavedMessages { user: owner, .. } => {
|
||||||
|
if &self.perspective.id == owner {
|
||||||
|
Ok(u32::MAX - ChannelPermission::VoiceCall as u32)
|
||||||
|
} else {
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Channel::DirectMessage { recipients, .. } => {
|
||||||
|
if recipients
|
||||||
|
.iter()
|
||||||
|
.find(|x| *x == &self.perspective.id)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
if let Some(recipient) = recipients.iter().find(|x| *x != &self.perspective.id)
|
||||||
|
{
|
||||||
|
let perms = self.for_user(recipient).await?;
|
||||||
|
|
||||||
|
if perms.get_send_message() {
|
||||||
|
return Ok(ChannelPermission::View
|
||||||
|
+ ChannelPermission::SendMessage
|
||||||
|
+ ChannelPermission::VoiceCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ChannelPermission::View as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
Channel::Group { recipients, .. } => {
|
||||||
|
if recipients
|
||||||
|
.iter()
|
||||||
|
.find(|x| *x == &self.perspective.id)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
Ok(ChannelPermission::View
|
||||||
|
+ ChannelPermission::SendMessage
|
||||||
|
+ ChannelPermission::ManageChannel
|
||||||
|
+ ChannelPermission::VoiceCall)
|
||||||
|
} else {
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn for_channel(self) -> Result<ChannelPermissions<[u32; 1]>> {
|
||||||
|
Ok(ChannelPermissions([self.calculate_channel().await?]))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,58 @@
|
|||||||
|
pub use crate::database::*;
|
||||||
|
|
||||||
|
pub mod channel;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
||||||
pub use user::get_relationship;
|
pub use user::get_relationship;
|
||||||
|
|
||||||
|
pub struct PermissionCalculator<'a> {
|
||||||
|
perspective: &'a User,
|
||||||
|
|
||||||
|
user: Option<&'a User>,
|
||||||
|
relationship: Option<&'a RelationshipStatus>,
|
||||||
|
channel: Option<&'a Channel>,
|
||||||
|
|
||||||
|
has_mutual_connection: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PermissionCalculator<'a> {
|
||||||
|
pub fn new(perspective: &'a User) -> PermissionCalculator {
|
||||||
|
PermissionCalculator {
|
||||||
|
perspective,
|
||||||
|
|
||||||
|
user: None,
|
||||||
|
relationship: None,
|
||||||
|
channel: None,
|
||||||
|
|
||||||
|
has_mutual_connection: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_user(self, user: &'a User) -> PermissionCalculator {
|
||||||
|
PermissionCalculator {
|
||||||
|
user: Some(&user),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_relationship(self, relationship: &'a RelationshipStatus) -> PermissionCalculator {
|
||||||
|
PermissionCalculator {
|
||||||
|
relationship: Some(&relationship),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_channel(self, channel: &'a Channel) -> PermissionCalculator {
|
||||||
|
PermissionCalculator {
|
||||||
|
channel: Some(&channel),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_mutual_connection(self) -> PermissionCalculator<'a> {
|
||||||
|
PermissionCalculator {
|
||||||
|
has_mutual_connection: true,
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
|
use super::PermissionCalculator;
|
||||||
|
|
||||||
|
use mongodb::bson::doc;
|
||||||
use num_enum::TryFromPrimitive;
|
use num_enum::TryFromPrimitive;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
@@ -6,35 +11,23 @@ use std::ops;
|
|||||||
#[repr(u32)]
|
#[repr(u32)]
|
||||||
pub enum UserPermission {
|
pub enum UserPermission {
|
||||||
Access = 1,
|
Access = 1,
|
||||||
SendMessage = 2,
|
ViewProfile = 2,
|
||||||
Invite = 4,
|
SendMessage = 4,
|
||||||
|
Invite = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
bitfield! {
|
bitfield! {
|
||||||
pub struct UserPermissions(MSB0 [u32]);
|
pub struct UserPermissions(MSB0 [u32]);
|
||||||
u32;
|
u32;
|
||||||
pub get_access, _: 31;
|
pub get_access, _: 31;
|
||||||
pub get_send_message, _: 30;
|
pub get_view_profile, _: 30;
|
||||||
pub get_invite, _: 29;
|
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!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 });
|
||||||
impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 });
|
impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 });
|
||||||
|
|
||||||
pub async fn calculate(user: &User, target: &str) -> 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
|
|
||||||
|
|
||||||
if let RelationshipStatus::Friend = get_relationship(&user, &target) {
|
|
||||||
UserPermissions([UserPermission::Access + UserPermission::SendMessage + UserPermission::Invite])
|
|
||||||
} else {
|
|
||||||
UserPermissions([ 0 ])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus {
|
pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus {
|
||||||
if a.id == b {
|
if a.id == b {
|
||||||
return RelationshipStatus::Friend;
|
return RelationshipStatus::Friend;
|
||||||
@@ -48,3 +41,67 @@ pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus {
|
|||||||
|
|
||||||
RelationshipStatus::None
|
RelationshipStatus::None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> PermissionCalculator<'a> {
|
||||||
|
pub async fn calculate_user(self, target: &str) -> Result<u32> {
|
||||||
|
if &self.perspective.id == target {
|
||||||
|
return Ok(u32::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut permissions: u32 = 0;
|
||||||
|
match self
|
||||||
|
.relationship
|
||||||
|
.clone()
|
||||||
|
.map(|v| v.to_owned())
|
||||||
|
.unwrap_or_else(|| get_relationship(&self.perspective, &target))
|
||||||
|
{
|
||||||
|
RelationshipStatus::Friend => return Ok(u32::MAX),
|
||||||
|
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
|
||||||
|
return Ok(UserPermission::Access as u32)
|
||||||
|
}
|
||||||
|
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
|
||||||
|
permissions = UserPermission::Access as u32;
|
||||||
|
// ! INFO: if we add boolean switch for permission to
|
||||||
|
// ! message people who have mutual, we need to get
|
||||||
|
// ! rid of this return statement.
|
||||||
|
return Ok(permissions);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.has_mutual_connection
|
||||||
|
|| get_collection("channels")
|
||||||
|
.find_one(
|
||||||
|
doc! {
|
||||||
|
"channel_type": {
|
||||||
|
"$in": ["Group", "DirectMessage"]
|
||||||
|
},
|
||||||
|
"recipients": {
|
||||||
|
"$all": [ &self.perspective.id, target ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "find",
|
||||||
|
with: "channels",
|
||||||
|
})?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
// ! FIXME: add privacy settings
|
||||||
|
return Ok(UserPermission::Access + UserPermission::ViewProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn for_user(self, target: &str) -> Result<UserPermissions<[u32; 1]>> {
|
||||||
|
Ok(UserPermissions([self.calculate_user(&target).await?]))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn for_user_given(self) -> Result<UserPermissions<[u32; 1]>> {
|
||||||
|
let id = &self.user.unwrap().id;
|
||||||
|
Ok(UserPermissions([self.calculate_user(&id).await?]))
|
||||||
|
}
|
||||||
|
}
|
||||||
+81
-7
@@ -17,18 +17,33 @@ pub mod database;
|
|||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
pub mod version;
|
||||||
|
|
||||||
|
use async_std::task;
|
||||||
|
use chrono::Duration;
|
||||||
use futures::join;
|
use futures::join;
|
||||||
use log::info;
|
use log::info;
|
||||||
use rauth;
|
use rauth::options::{EmailVerification, Options, SMTP};
|
||||||
|
use rauth::{
|
||||||
|
auth::Auth,
|
||||||
|
options::{Template, Templates},
|
||||||
|
};
|
||||||
use rocket_cors::AllowedOrigins;
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
#[async_std::main]
|
#[async_std::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "info"));
|
env_logger::init_from_env(env_logger::Env::default().filter_or("RUST_LOG", "info"));
|
||||||
|
|
||||||
info!("Starting REVOLT server.");
|
info!(
|
||||||
|
"Starting REVOLT server [version {}].",
|
||||||
|
crate::version::VERSION
|
||||||
|
);
|
||||||
|
|
||||||
util::variables::preflight_checks();
|
util::variables::preflight_checks();
|
||||||
database::connect().await;
|
database::connect().await;
|
||||||
@@ -40,10 +55,13 @@ async fn main() {
|
|||||||
})
|
})
|
||||||
.expect("Error setting Ctrl-C handler");
|
.expect("Error setting Ctrl-C handler");
|
||||||
|
|
||||||
|
let web_task = task::spawn(launch_web());
|
||||||
|
let hive_task = task::spawn(notifications::hive::listen());
|
||||||
|
|
||||||
join!(
|
join!(
|
||||||
launch_web(),
|
web_task,
|
||||||
notifications::websocket::launch_server(),
|
hive_task,
|
||||||
notifications::hive::listen(),
|
notifications::websocket::launch_server()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,10 +73,66 @@ async fn launch_web() {
|
|||||||
.to_cors()
|
.to_cors()
|
||||||
.expect("Failed to create 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("/", rocket_cors::catch_all_options_routes())
|
||||||
|
.mount("/auth", rauth::routes::routes())
|
||||||
|
.manage(auth)
|
||||||
.manage(cors.clone())
|
.manage(cors.clone())
|
||||||
.attach(cors)
|
.attach(cors)
|
||||||
.launch()
|
.launch()
|
||||||
|
|||||||
+109
-51
@@ -1,8 +1,10 @@
|
|||||||
|
use hive_pubsub::PubSub;
|
||||||
use rauth::auth::Session;
|
use rauth::auth::Session;
|
||||||
|
use rocket_contrib::json::JsonValue;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
use super::hive::get_hive;
|
use super::hive::{get_hive, subscribe_if_exists};
|
||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Snafu)]
|
#[derive(Serialize, Deserialize, Debug, Snafu)]
|
||||||
@@ -24,6 +26,21 @@ pub enum WebSocketError {
|
|||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum ServerboundNotification {
|
pub enum ServerboundNotification {
|
||||||
Authenticate(Session),
|
Authenticate(Session),
|
||||||
|
BeginTyping { channel: String },
|
||||||
|
EndTyping { channel: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub enum RemoveUserField {
|
||||||
|
ProfileContent,
|
||||||
|
ProfileBackground,
|
||||||
|
StatusText,
|
||||||
|
Avatar,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub enum RemoveChannelField {
|
||||||
|
Icon,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -33,78 +50,119 @@ pub enum ClientboundNotification {
|
|||||||
Authenticated,
|
Authenticated,
|
||||||
Ready {
|
Ready {
|
||||||
users: Vec<User>,
|
users: Vec<User>,
|
||||||
|
channels: Vec<Channel>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/*MessageCreate {
|
Message(Message),
|
||||||
|
MessageUpdate {
|
||||||
id: String,
|
id: String,
|
||||||
nonce: Option<String>,
|
data: JsonValue,
|
||||||
channel: String,
|
|
||||||
author: String,
|
|
||||||
content: String,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
MessageEdit {
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
author: String,
|
|
||||||
content: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
MessageDelete {
|
MessageDelete {
|
||||||
id: String,
|
id: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
GroupUserJoin {
|
ChannelCreate(Channel),
|
||||||
|
ChannelUpdate {
|
||||||
|
id: String,
|
||||||
|
data: JsonValue,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
clear: Option<RemoveChannelField>,
|
||||||
|
},
|
||||||
|
ChannelGroupJoin {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
ChannelGroupLeave {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
ChannelDelete {
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
ChannelStartTyping {
|
||||||
|
id: String,
|
||||||
|
user: String,
|
||||||
|
},
|
||||||
|
ChannelStopTyping {
|
||||||
id: String,
|
id: String,
|
||||||
user: String,
|
user: String,
|
||||||
},
|
},
|
||||||
|
|
||||||
GroupUserLeave {
|
UserUpdate {
|
||||||
id: String,
|
id: String,
|
||||||
user: String,
|
data: JsonValue,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
clear: Option<RemoveUserField>,
|
||||||
},
|
},
|
||||||
|
|
||||||
GuildUserJoin {
|
|
||||||
id: String,
|
|
||||||
user: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
GuildUserLeave {
|
|
||||||
id: String,
|
|
||||||
user: String,
|
|
||||||
banned: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
GuildChannelCreate {
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
name: String,
|
|
||||||
description: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
GuildChannelDelete {
|
|
||||||
id: String,
|
|
||||||
channel: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
GuildDelete {
|
|
||||||
id: String,
|
|
||||||
},*/
|
|
||||||
|
|
||||||
UserRelationship {
|
UserRelationship {
|
||||||
id: String,
|
id: String,
|
||||||
user: String,
|
user: User,
|
||||||
status: RelationshipStatus,
|
status: RelationshipStatus,
|
||||||
},
|
},
|
||||||
|
|
||||||
UserPresence {
|
UserPresence {
|
||||||
id: String,
|
id: String,
|
||||||
online: bool
|
online: bool,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientboundNotification {
|
impl ClientboundNotification {
|
||||||
pub async fn publish(self, topic: String) -> Result<(), String> {
|
pub fn publish(self, topic: String) {
|
||||||
hive_pubsub::backend::mongo::publish(get_hive(), &topic, self).await
|
async_std::task::spawn(async move {
|
||||||
|
prehandle_hook(&self); // ! TODO: this should be moved to pubsub
|
||||||
|
hive_pubsub::backend::mongo::publish(get_hive(), &topic, self)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prehandle_hook(notification: &ClientboundNotification) {
|
||||||
|
match ¬ification {
|
||||||
|
ClientboundNotification::ChannelGroupJoin { id, user } => {
|
||||||
|
subscribe_if_exists(user.clone(), id.clone()).ok();
|
||||||
|
}
|
||||||
|
ClientboundNotification::ChannelCreate(channel) => {
|
||||||
|
let channel_id = channel.id();
|
||||||
|
match &channel {
|
||||||
|
Channel::SavedMessages { user, .. } => {
|
||||||
|
subscribe_if_exists(user.clone(), channel_id.to_string()).ok();
|
||||||
|
}
|
||||||
|
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
|
||||||
|
for recipient in recipients {
|
||||||
|
subscribe_if_exists(recipient.clone(), channel_id.to_string()).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientboundNotification::UserRelationship { id, user, status } => {
|
||||||
|
if status != &RelationshipStatus::None {
|
||||||
|
subscribe_if_exists(id.clone(), user.id.clone()).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn posthandle_hook(notification: &ClientboundNotification) {
|
||||||
|
match ¬ification {
|
||||||
|
ClientboundNotification::ChannelDelete { id } => {
|
||||||
|
get_hive().hive.drop_topic(&id).ok();
|
||||||
|
}
|
||||||
|
ClientboundNotification::UserRelationship { id, user, status } => {
|
||||||
|
if status == &RelationshipStatus::None {
|
||||||
|
get_hive()
|
||||||
|
.hive
|
||||||
|
.unsubscribe(&id.to_string(), &user.id.to_string())
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientboundNotification::ChannelGroupLeave { id, user } => {
|
||||||
|
get_hive()
|
||||||
|
.hive
|
||||||
|
.unsubscribe(&user.to_string(), &id.to_string())
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,8 @@ static HIVE: OnceCell<Hive> = OnceCell::new();
|
|||||||
pub async fn init_hive() {
|
pub async fn init_hive() {
|
||||||
let hive = MongodbPubSub::new(
|
let hive = MongodbPubSub::new(
|
||||||
|ids, notification| {
|
|ids, notification| {
|
||||||
|
super::events::posthandle_hook(¬ification);
|
||||||
|
|
||||||
if let Ok(data) = to_string(¬ification) {
|
if let Ok(data) = to_string(¬ification) {
|
||||||
debug!("Pushing out notification. {}", data);
|
debug!("Pushing out notification. {}", data);
|
||||||
websocket::publish(ids, notification);
|
websocket::publish(ids, notification);
|
||||||
@@ -36,8 +38,6 @@ pub async fn listen() {
|
|||||||
.fuse()
|
.fuse()
|
||||||
.await
|
.await
|
||||||
.expect("Hive hit an error");
|
.expect("Hive hit an error");
|
||||||
|
|
||||||
dbg!("a");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscribe_multiple(user: String, topics: Vec<String>) -> Result<(), String> {
|
pub fn subscribe_multiple(user: String, topics: Vec<String>) -> Result<(), String> {
|
||||||
|
|||||||
@@ -1,68 +1,79 @@
|
|||||||
use crate::notifications::events::ClientboundNotification;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{database::*, notifications::events::ClientboundNotification};
|
||||||
use crate::{
|
use crate::{
|
||||||
database::{entities::User, get_collection},
|
database::{entities::User, get_collection},
|
||||||
util::result::{Error, Result},
|
util::result::{Error, Result},
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use mongodb::{
|
use mongodb::bson::{doc, from_document};
|
||||||
bson::{doc, from_bson, Bson},
|
|
||||||
options::FindOptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::websocket::is_online;
|
|
||||||
|
|
||||||
pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
|
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 {
|
if let Some(relationships) = &user.relations {
|
||||||
let user_ids: Vec<String> = relationships
|
user_ids.extend(
|
||||||
|
relationships
|
||||||
.iter()
|
.iter()
|
||||||
.map(|relationship| relationship.id.clone())
|
.map(|relationship| relationship.id.clone()),
|
||||||
.collect();
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut cursor = get_collection("users")
|
let mut cursor = get_collection("channels")
|
||||||
.find(
|
.find(
|
||||||
doc! {
|
doc! {
|
||||||
"_id": {
|
"$or": [
|
||||||
"$in": user_ids
|
{
|
||||||
}
|
"channel_type": "SavedMessages",
|
||||||
|
"user": &user.id
|
||||||
},
|
},
|
||||||
FindOptions::builder()
|
{
|
||||||
.projection(doc! { "_id": 1, "username": 1 })
|
"channel_type": "DirectMessage",
|
||||||
.build(),
|
"recipients": &user.id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"channel_type": "Group",
|
||||||
|
"recipients": &user.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| Error::DatabaseError {
|
.map_err(|_| Error::DatabaseError {
|
||||||
operation: "find",
|
operation: "find",
|
||||||
with: "users",
|
with: "channels",
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let mut channels = vec![];
|
||||||
while let Some(result) = cursor.next().await {
|
while let Some(result) = cursor.next().await {
|
||||||
if let Ok(doc) = result {
|
if let Ok(doc) = result {
|
||||||
let mut user: User =
|
let channel = from_document(doc).map_err(|_| Error::DatabaseError {
|
||||||
from_bson(Bson::Document(doc)).map_err(|_| Error::DatabaseError {
|
operation: "from_document",
|
||||||
operation: "from_bson",
|
with: "channel",
|
||||||
with: "user",
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
user.relationship = Some(
|
if let Channel::Group { recipients, .. } = &channel {
|
||||||
relationships
|
user_ids.extend(recipients.iter().cloned());
|
||||||
.iter()
|
} else if let Channel::DirectMessage { recipients, .. } = &channel {
|
||||||
.find(|x| user.id == x.id)
|
user_ids.extend(recipients.iter().cloned());
|
||||||
.ok_or_else(|| Error::InternalError)?
|
|
||||||
.status
|
|
||||||
.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
user.online = Some(is_online(&user.id));
|
|
||||||
|
|
||||||
users.push(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channels.push(channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user.online = Some(is_online(&user.id));
|
user_ids.remove(&user.id);
|
||||||
|
let mut users = if user_ids.len() > 0 {
|
||||||
|
user.fetch_multiple_users(user_ids.into_iter().collect::<Vec<String>>())
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
user.relationship = Some(RelationshipStatus::User);
|
||||||
|
user.online = Some(true);
|
||||||
users.push(user);
|
users.push(user);
|
||||||
|
|
||||||
Ok(ClientboundNotification::Ready { users })
|
Ok(ClientboundNotification::Ready { users, channels })
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
|
|
||||||
use super::hive::get_hive;
|
use super::hive::get_hive;
|
||||||
|
use futures::StreamExt;
|
||||||
use hive_pubsub::PubSub;
|
use hive_pubsub::PubSub;
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use mongodb::options::FindOptions;
|
||||||
|
|
||||||
pub async fn generate_subscriptions(user: &User) -> Result<(), String> {
|
pub async fn generate_subscriptions(user: &User) -> Result<(), String> {
|
||||||
let hive = get_hive();
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,10 @@ use futures::{pin_mut, prelude::*};
|
|||||||
use hive_pubsub::PubSub;
|
use hive_pubsub::PubSub;
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use many_to_many::ManyToMany;
|
use many_to_many::ManyToMany;
|
||||||
use rauth::auth::{Auth, Session};
|
use rauth::{
|
||||||
|
auth::{Auth, Session},
|
||||||
|
options::Options,
|
||||||
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
@@ -64,12 +67,10 @@ async fn accept(stream: TcpStream) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let session: Arc<Mutex<Option<Session>>> = Arc::new(Mutex::new(None));
|
let session: Arc<Mutex<Option<Session>>> = Arc::new(Mutex::new(None));
|
||||||
let mutex_generator = || { session.clone() };
|
let mutex_generator = || session.clone();
|
||||||
let fwd = rx.map(Ok).forward(write);
|
let fwd = rx.map(Ok).forward(write);
|
||||||
let incoming = read.try_for_each(async move |msg| {
|
let incoming = read.try_for_each(async move |msg| {
|
||||||
let mutex = mutex_generator();
|
let mutex = mutex_generator();
|
||||||
//dbg!(&mutex.lock().unwrap());
|
|
||||||
|
|
||||||
if let Message::Text(text) = msg {
|
if let Message::Text(text) = msg {
|
||||||
if let Ok(notification) = serde_json::from_str::<ServerboundNotification>(&text) {
|
if let Ok(notification) = serde_json::from_str::<ServerboundNotification>(&text) {
|
||||||
match notification {
|
match notification {
|
||||||
@@ -80,21 +81,17 @@ async fn accept(stream: TcpStream) {
|
|||||||
WebSocketError::AlreadyAuthenticated,
|
WebSocketError::AlreadyAuthenticated,
|
||||||
));
|
));
|
||||||
|
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(validated_session) = Auth::new(get_collection("accounts"))
|
if let Ok(validated_session) =
|
||||||
|
Auth::new(get_collection("accounts"), Options::new())
|
||||||
.verify_session(new_session)
|
.verify_session(new_session)
|
||||||
.await {
|
.await
|
||||||
|
{
|
||||||
let id = validated_session.user_id.clone();
|
let id = validated_session.user_id.clone();
|
||||||
if let Ok(user) = (
|
if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await {
|
||||||
Ref {
|
|
||||||
id: id.clone()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.fetch_user()
|
|
||||||
.await {
|
|
||||||
let was_online = is_online(&id);
|
let was_online = is_online(&id);
|
||||||
{
|
{
|
||||||
match USERS.write() {
|
match USERS.write() {
|
||||||
@@ -103,10 +100,12 @@ async fn accept(stream: TcpStream) {
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
send(ClientboundNotification::Error(
|
send(ClientboundNotification::Error(
|
||||||
WebSocketError::InternalError { at: "Writing users map.".to_string() },
|
WebSocketError::InternalError {
|
||||||
|
at: "Writing users map.".to_string(),
|
||||||
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,10 +114,12 @@ async fn accept(stream: TcpStream) {
|
|||||||
|
|
||||||
if let Err(_) = subscriptions::generate_subscriptions(&user).await {
|
if let Err(_) = subscriptions::generate_subscriptions(&user).await {
|
||||||
send(ClientboundNotification::Error(
|
send(ClientboundNotification::Error(
|
||||||
WebSocketError::InternalError { at: "Generating subscriptions.".to_string() },
|
WebSocketError::InternalError {
|
||||||
|
at: "Generating subscriptions.".to_string(),
|
||||||
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
send(ClientboundNotification::Authenticated);
|
send(ClientboundNotification::Authenticated);
|
||||||
@@ -130,19 +131,19 @@ async fn accept(stream: TcpStream) {
|
|||||||
if !was_online {
|
if !was_online {
|
||||||
ClientboundNotification::UserPresence {
|
ClientboundNotification::UserPresence {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
online: true
|
online: true,
|
||||||
}
|
}
|
||||||
.publish(id)
|
.publish(id);
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
send(ClientboundNotification::Error(
|
send(ClientboundNotification::Error(
|
||||||
WebSocketError::InternalError { at: "Generating payload.".to_string() },
|
WebSocketError::InternalError {
|
||||||
|
at: "Generating payload.".to_string(),
|
||||||
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -156,6 +157,50 @@ async fn accept(stream: TcpStream) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ! TEMP: verify user part of channel
|
||||||
|
// ! Could just run permission check here.
|
||||||
|
ServerboundNotification::BeginTyping { channel } => {
|
||||||
|
if mutex.lock().unwrap().is_some() {
|
||||||
|
let user = {
|
||||||
|
let mutex = mutex.lock().unwrap();
|
||||||
|
let session = mutex.as_ref().unwrap();
|
||||||
|
session.user_id.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
ClientboundNotification::ChannelStartTyping {
|
||||||
|
id: channel.clone(),
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
.publish(channel);
|
||||||
|
} else {
|
||||||
|
send(ClientboundNotification::Error(
|
||||||
|
WebSocketError::AlreadyAuthenticated,
|
||||||
|
));
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerboundNotification::EndTyping { channel } => {
|
||||||
|
if mutex.lock().unwrap().is_some() {
|
||||||
|
let user = {
|
||||||
|
let mutex = mutex.lock().unwrap();
|
||||||
|
let session = mutex.as_ref().unwrap();
|
||||||
|
session.user_id.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
ClientboundNotification::ChannelStopTyping {
|
||||||
|
id: channel.clone(),
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
.publish(channel);
|
||||||
|
} else {
|
||||||
|
send(ClientboundNotification::Error(
|
||||||
|
WebSocketError::AlreadyAuthenticated,
|
||||||
|
));
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,16 +214,28 @@ async fn accept(stream: TcpStream) {
|
|||||||
info!("User {} disconnected.", &addr);
|
info!("User {} disconnected.", &addr);
|
||||||
CONNECTIONS.lock().unwrap().remove(&addr);
|
CONNECTIONS.lock().unwrap().remove(&addr);
|
||||||
|
|
||||||
|
let mut offline = None;
|
||||||
|
{
|
||||||
let session = session.lock().unwrap();
|
let session = session.lock().unwrap();
|
||||||
if let Some(session) = session.as_ref() {
|
if let Some(session) = session.as_ref() {
|
||||||
let mut users = USERS.write().unwrap();
|
let mut users = USERS.write().unwrap();
|
||||||
users.remove(&session.user_id, &addr);
|
users.remove(&session.user_id, &addr);
|
||||||
if users.get_left(&session.user_id).is_none() {
|
if users.get_left(&session.user_id).is_none() {
|
||||||
get_hive().drop_client(&session.user_id).unwrap();
|
get_hive().drop_client(&session.user_id).unwrap();
|
||||||
|
offline = Some(session.user_id.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(id) = offline {
|
||||||
|
ClientboundNotification::UserPresence {
|
||||||
|
id: id.clone(),
|
||||||
|
online: false,
|
||||||
|
}
|
||||||
|
.publish(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
|
pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
|
||||||
let mut targets = vec![];
|
let mut targets = vec![];
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
use crate::{database::*, notifications::events::ClientboundNotification};
|
||||||
|
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
|
||||||
|
#[delete("/<target>")]
|
||||||
|
pub async fn req(user: User, target: Ref) -> Result<()> {
|
||||||
|
let target = target.fetch_channel().await?;
|
||||||
|
|
||||||
|
let perm = permissions::PermissionCalculator::new(&user)
|
||||||
|
.with_channel(&target)
|
||||||
|
.for_channel()
|
||||||
|
.await?;
|
||||||
|
if !perm.get_view() {
|
||||||
|
Err(Error::MissingPermission)?
|
||||||
|
}
|
||||||
|
|
||||||
|
match &target {
|
||||||
|
Channel::SavedMessages { .. } => Err(Error::NoEffect),
|
||||||
|
Channel::DirectMessage { .. } => {
|
||||||
|
get_collection("channels")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": target.id()
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Channel::Group {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
recipients,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if &user.id == owner {
|
||||||
|
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
|
||||||
|
get_collection("channels")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$set": {
|
||||||
|
"owner": new_owner
|
||||||
|
},
|
||||||
|
"$pull": {
|
||||||
|
"recipients": &user.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
target.publish_update(json!({ "owner": new_owner })).await?;
|
||||||
|
} else {
|
||||||
|
return target.delete().await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
get_collection("channels")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$pull": {
|
||||||
|
"recipients": &user.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientboundNotification::ChannelGroupLeave {
|
||||||
|
id: id.clone(),
|
||||||
|
user: user.id.clone(),
|
||||||
|
}
|
||||||
|
.publish(id.clone());
|
||||||
|
|
||||||
|
Message::create(
|
||||||
|
"00000000000000000000000000".to_string(),
|
||||||
|
id.clone(),
|
||||||
|
Content::SystemMessage(SystemMessage::UserLeft { id: user.id }),
|
||||||
|
)
|
||||||
|
.publish(&target)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use crate::notifications::events::ClientboundNotification;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
use crate::{database::*, notifications::events::RemoveChannelField};
|
||||||
|
|
||||||
|
use mongodb::bson::{doc, to_document};
|
||||||
|
use rocket_contrib::json::Json;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize)]
|
||||||
|
pub struct Data {
|
||||||
|
#[validate(length(min = 1, max = 32))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
name: Option<String>,
|
||||||
|
#[validate(length(min = 0, max = 1024))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
description: Option<String>,
|
||||||
|
#[validate(length(min = 1, max = 128))]
|
||||||
|
icon: Option<String>,
|
||||||
|
remove: Option<RemoveChannelField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("/<target>", data = "<data>")]
|
||||||
|
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<()> {
|
||||||
|
let data = data.into_inner();
|
||||||
|
data.validate()
|
||||||
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
|
if data.name.is_none()
|
||||||
|
&& data.description.is_none()
|
||||||
|
&& data.icon.is_none()
|
||||||
|
&& data.remove.is_none()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = target.fetch_channel().await?;
|
||||||
|
let perm = permissions::PermissionCalculator::new(&user)
|
||||||
|
.with_channel(&target)
|
||||||
|
.for_channel()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !perm.get_manage_channel() {
|
||||||
|
Err(Error::MissingPermission)?
|
||||||
|
}
|
||||||
|
|
||||||
|
match &target {
|
||||||
|
Channel::Group { id, icon, .. } => {
|
||||||
|
let mut set = doc! {};
|
||||||
|
let mut unset = doc! {};
|
||||||
|
|
||||||
|
let mut remove_icon = false;
|
||||||
|
if let Some(remove) = &data.remove {
|
||||||
|
match remove {
|
||||||
|
RemoveChannelField::Icon => {
|
||||||
|
unset.insert("icon", 1);
|
||||||
|
remove_icon = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(name) = &data.name {
|
||||||
|
set.insert("name", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(description) = &data.description {
|
||||||
|
set.insert("description", description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(attachment_id) = &data.icon {
|
||||||
|
let attachment =
|
||||||
|
File::find_and_use(&attachment_id, "icons", "object", &user.id).await?;
|
||||||
|
set.insert(
|
||||||
|
"icon",
|
||||||
|
to_document(&attachment).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "to_document",
|
||||||
|
with: "attachment",
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_icon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut operations = doc! {};
|
||||||
|
if set.len() > 0 {
|
||||||
|
operations.insert("$set", &set);
|
||||||
|
}
|
||||||
|
|
||||||
|
if unset.len() > 0 {
|
||||||
|
operations.insert("$unset", unset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if operations.len() > 0 {
|
||||||
|
get_collection("channels")
|
||||||
|
.update_one(doc! { "_id": &id }, operations, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientboundNotification::ChannelUpdate {
|
||||||
|
id: id.clone(),
|
||||||
|
data: json!(set),
|
||||||
|
clear: data.remove,
|
||||||
|
}
|
||||||
|
.publish(id.clone());
|
||||||
|
|
||||||
|
if let Some(name) = data.name {
|
||||||
|
Message::create(
|
||||||
|
"00000000000000000000000000".to_string(),
|
||||||
|
id.clone(),
|
||||||
|
Content::SystemMessage(SystemMessage::ChannelRenamed { name, by: user.id.clone() }),
|
||||||
|
)
|
||||||
|
.publish(&target)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(_) = data.description {
|
||||||
|
Message::create(
|
||||||
|
"00000000000000000000000000".to_string(),
|
||||||
|
id.clone(),
|
||||||
|
Content::SystemMessage(SystemMessage::ChannelDescriptionChanged { by: user.id.clone() }),
|
||||||
|
)
|
||||||
|
.publish(&target)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(_) = data.icon {
|
||||||
|
Message::create(
|
||||||
|
"00000000000000000000000000".to_string(),
|
||||||
|
id.clone(),
|
||||||
|
Content::SystemMessage(SystemMessage::ChannelIconChanged { by: user.id }),
|
||||||
|
)
|
||||||
|
.publish(&target)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if remove_icon {
|
||||||
|
if let Some(old_icon) = icon {
|
||||||
|
old_icon.delete().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(Error::InvalidOperation),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
use crate::database::*;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
|
use rocket_contrib::json::JsonValue;
|
||||||
|
|
||||||
|
#[get("/<target>/members")]
|
||||||
|
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||||
|
let target = target.fetch_channel().await?;
|
||||||
|
|
||||||
|
let perm = permissions::PermissionCalculator::new(&user)
|
||||||
|
.with_channel(&target)
|
||||||
|
.for_channel()
|
||||||
|
.await?;
|
||||||
|
if !perm.get_view() {
|
||||||
|
Err(Error::MissingPermission)?
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Channel::Group { recipients, .. } = target {
|
||||||
|
Ok(json!(user.fetch_multiple_users(recipients).await?))
|
||||||
|
} else {
|
||||||
|
Err(Error::InvalidOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
use crate::util::variables::MAX_GROUP_SIZE;
|
||||||
|
use crate::{database::*, notifications::events::ClientboundNotification};
|
||||||
|
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
|
||||||
|
#[put("/<target>/recipients/<member>")]
|
||||||
|
pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
|
||||||
|
if get_relationship(&user, &member.id) != RelationshipStatus::Friend {
|
||||||
|
Err(Error::NotFriends)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = target.fetch_channel().await?;
|
||||||
|
let perm = permissions::PermissionCalculator::new(&user)
|
||||||
|
.with_channel(&channel)
|
||||||
|
.for_channel()
|
||||||
|
.await?;
|
||||||
|
if !perm.get_view() {
|
||||||
|
Err(Error::MissingPermission)?
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Channel::Group { id, recipients, .. } = &channel {
|
||||||
|
if recipients.len() >= *MAX_GROUP_SIZE {
|
||||||
|
Err(Error::GroupTooLarge {
|
||||||
|
max: *MAX_GROUP_SIZE,
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipients.iter().find(|x| *x == &member.id).is_some() {
|
||||||
|
Err(Error::AlreadyInGroup)?
|
||||||
|
}
|
||||||
|
|
||||||
|
get_collection("channels")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$push": {
|
||||||
|
"recipients": &member.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
ClientboundNotification::ChannelGroupJoin {
|
||||||
|
id: id.clone(),
|
||||||
|
user: member.id.clone(),
|
||||||
|
}
|
||||||
|
.publish(id.clone());
|
||||||
|
|
||||||
|
Message::create(
|
||||||
|
"00000000000000000000000000".to_string(),
|
||||||
|
id.clone(),
|
||||||
|
Content::SystemMessage(SystemMessage::UserAdded {
|
||||||
|
id: member.id,
|
||||||
|
by: user.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.publish(&channel)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::InvalidOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
use crate::database::*;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
use crate::util::variables::MAX_GROUP_SIZE;
|
||||||
|
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use rocket_contrib::json::{Json, JsonValue};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
use ulid::Ulid;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize)]
|
||||||
|
pub struct Data {
|
||||||
|
#[validate(length(min = 1, max = 32))]
|
||||||
|
name: String,
|
||||||
|
#[validate(length(min = 0, max = 1024))]
|
||||||
|
description: Option<String>,
|
||||||
|
// Maximum length of 36 allows both ULIDs and UUIDs.
|
||||||
|
#[validate(length(min = 1, max = 36))]
|
||||||
|
nonce: String,
|
||||||
|
users: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/create", data = "<info>")]
|
||||||
|
pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> {
|
||||||
|
info.validate()
|
||||||
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
|
let mut set: HashSet<String> = HashSet::from_iter(info.users.iter().cloned());
|
||||||
|
set.insert(user.id.clone());
|
||||||
|
|
||||||
|
if set.len() > *MAX_GROUP_SIZE {
|
||||||
|
Err(Error::GroupTooLarge {
|
||||||
|
max: *MAX_GROUP_SIZE,
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
|
||||||
|
for target in &set {
|
||||||
|
if get_relationship(&user, target) != RelationshipStatus::Friend {
|
||||||
|
Err(Error::NotFriends)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if get_collection("channels")
|
||||||
|
.find_one(
|
||||||
|
doc! {
|
||||||
|
"nonce": &info.nonce
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "find_one",
|
||||||
|
with: "channel",
|
||||||
|
})?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
Err(Error::DuplicateNonce)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = Ulid::new().to_string();
|
||||||
|
let channel = Channel::Group {
|
||||||
|
id,
|
||||||
|
nonce: Some(info.nonce.clone()),
|
||||||
|
name: info.name.clone(),
|
||||||
|
description: info
|
||||||
|
.description
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "A group.".to_string()),
|
||||||
|
owner: user.id,
|
||||||
|
recipients: set.into_iter().collect::<Vec<String>>(),
|
||||||
|
icon: None,
|
||||||
|
last_message: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
channel.clone().publish().await?;
|
||||||
|
|
||||||
|
Ok(json!(channel))
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
use crate::{database::*, notifications::events::ClientboundNotification};
|
||||||
|
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
|
||||||
|
#[delete("/<target>/recipients/<member>")]
|
||||||
|
pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
|
||||||
|
if &user.id == &member.id {
|
||||||
|
Err(Error::CannotRemoveYourself)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = target.fetch_channel().await?;
|
||||||
|
|
||||||
|
if let Channel::Group {
|
||||||
|
id,
|
||||||
|
owner,
|
||||||
|
recipients,
|
||||||
|
..
|
||||||
|
} = &channel
|
||||||
|
{
|
||||||
|
if &user.id != owner {
|
||||||
|
// figure out if we want to use perm system here
|
||||||
|
Err(Error::MissingPermission)?
|
||||||
|
}
|
||||||
|
|
||||||
|
if recipients.iter().find(|x| *x == &member.id).is_none() {
|
||||||
|
Err(Error::NotInGroup)?
|
||||||
|
}
|
||||||
|
|
||||||
|
get_collection("channels")
|
||||||
|
.update_one(
|
||||||
|
doc! {
|
||||||
|
"_id": &id
|
||||||
|
},
|
||||||
|
doc! {
|
||||||
|
"$pull": {
|
||||||
|
"recipients": &member.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "channel",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
ClientboundNotification::ChannelGroupLeave {
|
||||||
|
id: id.clone(),
|
||||||
|
user: member.id.clone(),
|
||||||
|
}
|
||||||
|
.publish(id.clone());
|
||||||
|
|
||||||
|
Message::create(
|
||||||
|
"00000000000000000000000000".to_string(),
|
||||||
|
id.clone(),
|
||||||
|
Content::SystemMessage(SystemMessage::UserRemove {
|
||||||
|
id: member.id,
|
||||||
|
by: user.id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.publish(&channel)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::InvalidOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use crate::database::*;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
use mongodb::{
|
||||||
|
bson::{doc, from_document},
|
||||||
|
options::FindOptions,
|
||||||
|
};
|
||||||
|
use rocket::request::Form;
|
||||||
|
use rocket_contrib::json::JsonValue;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, FromFormValue)]
|
||||||
|
pub enum Sort {
|
||||||
|
Latest,
|
||||||
|
Oldest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize, FromForm)]
|
||||||
|
pub struct Options {
|
||||||
|
#[validate(range(min = 1, max = 100))]
|
||||||
|
limit: Option<i64>,
|
||||||
|
#[validate(length(min = 26, max = 26))]
|
||||||
|
before: Option<String>,
|
||||||
|
#[validate(length(min = 26, max = 26))]
|
||||||
|
after: Option<String>,
|
||||||
|
sort: Option<Sort>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/<target>/messages?<options..>")]
|
||||||
|
pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<JsonValue> {
|
||||||
|
options
|
||||||
|
.validate()
|
||||||
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
|
let target = target.fetch_channel().await?;
|
||||||
|
|
||||||
|
let perm = permissions::PermissionCalculator::new(&user)
|
||||||
|
.with_channel(&target)
|
||||||
|
.for_channel()
|
||||||
|
.await?;
|
||||||
|
if !perm.get_view() {
|
||||||
|
Err(Error::MissingPermission)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut query = doc! { "channel": target.id() };
|
||||||
|
|
||||||
|
if let Some(before) = &options.before {
|
||||||
|
query.insert("_id", doc! { "$lt": before });
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(after) = &options.after {
|
||||||
|
query.insert("_id", doc! { "$gt": after });
|
||||||
|
}
|
||||||
|
|
||||||
|
let sort = if let Sort::Latest = options.sort.as_ref().unwrap_or_else(|| &Sort::Latest) {
|
||||||
|
-1
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
let mut cursor = get_collection("messages")
|
||||||
|
.find(
|
||||||
|
query,
|
||||||
|
FindOptions::builder()
|
||||||
|
.limit(options.limit.unwrap_or(50))
|
||||||
|
.sort(doc! {
|
||||||
|
"_id": sort
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "find",
|
||||||
|
with: "messages",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut messages = vec![];
|
||||||
|
while let Some(result) = cursor.next().await {
|
||||||
|
if let Ok(doc) = result {
|
||||||
|
messages.push(
|
||||||
|
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "from_document",
|
||||||
|
with: "message",
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!(messages))
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
use crate::database::*;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
|
use mongodb::{bson::doc, options::FindOneOptions};
|
||||||
|
use rocket_contrib::json::{Json, JsonValue};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use ulid::Ulid;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize)]
|
||||||
|
pub struct Data {
|
||||||
|
#[validate(length(min = 0, max = 2000))]
|
||||||
|
content: String,
|
||||||
|
// Maximum length of 36 allows both ULIDs and UUIDs.
|
||||||
|
#[validate(length(min = 1, max = 36))]
|
||||||
|
nonce: String,
|
||||||
|
#[validate(length(min = 1, max = 128))]
|
||||||
|
attachment: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/<target>/messages", data = "<message>")]
|
||||||
|
pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonValue> {
|
||||||
|
message
|
||||||
|
.validate()
|
||||||
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
|
if message.content.len() == 0 && message.attachment.is_none() {
|
||||||
|
return Err(Error::EmptyMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
let target = target.fetch_channel().await?;
|
||||||
|
let perm = permissions::PermissionCalculator::new(&user)
|
||||||
|
.with_channel(&target)
|
||||||
|
.for_channel()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !perm.get_send_message() {
|
||||||
|
Err(Error::MissingPermission)?
|
||||||
|
}
|
||||||
|
|
||||||
|
if get_collection("messages")
|
||||||
|
.find_one(
|
||||||
|
doc! {
|
||||||
|
"nonce": &message.nonce
|
||||||
|
},
|
||||||
|
FindOneOptions::builder()
|
||||||
|
.projection(doc! { "_id": 1 })
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "find_one",
|
||||||
|
with: "message",
|
||||||
|
})?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
Err(Error::DuplicateNonce)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = Ulid::new().to_string();
|
||||||
|
let attachment = if let Some(attachment_id) = &message.attachment {
|
||||||
|
Some(File::find_and_use(attachment_id, "attachments", "message", &id).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let msg = Message {
|
||||||
|
id,
|
||||||
|
channel: target.id().to_string(),
|
||||||
|
author: user.id,
|
||||||
|
|
||||||
|
content: Content::Text(message.content.clone()),
|
||||||
|
attachment,
|
||||||
|
nonce: Some(message.nonce.clone()),
|
||||||
|
edited: None,
|
||||||
|
embeds: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
msg.clone().publish(&target).await?;
|
||||||
|
|
||||||
|
Ok(json!(msg))
|
||||||
|
}
|
||||||
@@ -1,5 +1,35 @@
|
|||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
|
|
||||||
|
mod delete_channel;
|
||||||
|
mod edit_channel;
|
||||||
|
mod fetch_channel;
|
||||||
|
mod fetch_members;
|
||||||
|
mod group_add_member;
|
||||||
|
mod group_create;
|
||||||
|
mod group_remove_member;
|
||||||
|
mod join_call;
|
||||||
|
mod message_delete;
|
||||||
|
mod message_edit;
|
||||||
|
mod message_fetch;
|
||||||
|
mod message_query;
|
||||||
|
mod message_query_stale;
|
||||||
|
mod message_send;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![]
|
routes![
|
||||||
|
fetch_channel::req,
|
||||||
|
fetch_members::req,
|
||||||
|
delete_channel::req,
|
||||||
|
edit_channel::req,
|
||||||
|
message_send::req,
|
||||||
|
message_query::req,
|
||||||
|
message_query_stale::req,
|
||||||
|
message_fetch::req,
|
||||||
|
message_edit::req,
|
||||||
|
message_delete::req,
|
||||||
|
group_create::req,
|
||||||
|
group_add_member::req,
|
||||||
|
group_remove_member::req,
|
||||||
|
join_call::req,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ use rocket::Rocket;
|
|||||||
mod channels;
|
mod channels;
|
||||||
mod guild;
|
mod guild;
|
||||||
mod onboard;
|
mod onboard;
|
||||||
|
mod push;
|
||||||
mod root;
|
mod root;
|
||||||
mod users;
|
mod users;
|
||||||
|
|
||||||
@@ -15,4 +16,5 @@ pub fn mount(rocket: Rocket) -> Rocket {
|
|||||||
.mount("/users", users::routes())
|
.mount("/users", users::routes())
|
||||||
.mount("/channels", channels::routes())
|
.mount("/channels", channels::routes())
|
||||||
.mount("/guild", guild::routes())
|
.mount("/guild", guild::routes())
|
||||||
|
.mount("/push", push::routes())
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ use crate::database::*;
|
|||||||
use crate::util::result::{Error, Result};
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
use mongodb::bson::doc;
|
use mongodb::bson::doc;
|
||||||
use mongodb::options::{Collation, FindOneOptions};
|
|
||||||
use rauth::auth::Session;
|
use rauth::auth::Session;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
@@ -10,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
lazy_static! {
|
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)]
|
#[derive(Validate, Serialize, Deserialize)]
|
||||||
@@ -28,27 +27,12 @@ pub async fn req(session: Session, user: Option<User>, data: Json<Data>) -> Resu
|
|||||||
data.validate()
|
data.validate()
|
||||||
.map_err(|error| Error::FailedValidation { error })?;
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
let col = get_collection("users");
|
if User::is_username_taken(&data.username).await? {
|
||||||
if col
|
return Err(Error::UsernameTaken);
|
||||||
.find_one(
|
|
||||||
doc! {
|
|
||||||
"username": &data.username
|
|
||||||
},
|
|
||||||
FindOneOptions::builder()
|
|
||||||
.collation(Collation::builder().locale("en").strength(2).build())
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::DatabaseError {
|
|
||||||
operation: "find_one",
|
|
||||||
with: "user",
|
|
||||||
})?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
Err(Error::UsernameTaken)?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
col.insert_one(
|
get_collection("users")
|
||||||
|
.insert_one(
|
||||||
doc! {
|
doc! {
|
||||||
"_id": session.user_id,
|
"_id": session.user_id,
|
||||||
"username": &data.username
|
"username": &data.username
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use rocket::Route;
|
||||||
|
|
||||||
|
mod subscribe;
|
||||||
|
mod unsubscribe;
|
||||||
|
|
||||||
|
pub fn routes() -> Vec<Route> {
|
||||||
|
routes![subscribe::req, unsubscribe::req]
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
+19
-2
@@ -1,5 +1,6 @@
|
|||||||
use crate::util::variables::{
|
use crate::util::variables::{
|
||||||
DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, USE_EMAIL, USE_HCAPTCHA,
|
APP_URL, JANUARY_URL, AUTUMN_URL, DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY,
|
||||||
|
USE_AUTUMN, USE_JANUARY, USE_EMAIL, USE_HCAPTCHA, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL, VOSO_WS_HOST,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mongodb::bson::doc;
|
use mongodb::bson::doc;
|
||||||
@@ -8,7 +9,7 @@ use rocket_contrib::json::JsonValue;
|
|||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn root() -> JsonValue {
|
pub async fn root() -> JsonValue {
|
||||||
json!({
|
json!({
|
||||||
"revolt": "0.3.0-alpha",
|
"revolt": crate::version::VERSION,
|
||||||
"features": {
|
"features": {
|
||||||
"registration": !*DISABLE_REGISTRATION,
|
"registration": !*DISABLE_REGISTRATION,
|
||||||
"captcha": {
|
"captcha": {
|
||||||
@@ -16,7 +17,23 @@ pub async fn root() -> JsonValue {
|
|||||||
"key": HCAPTCHA_SITEKEY.to_string()
|
"key": HCAPTCHA_SITEKEY.to_string()
|
||||||
},
|
},
|
||||||
"email": *USE_EMAIL,
|
"email": *USE_EMAIL,
|
||||||
|
"invite_only": *INVITE_ONLY,
|
||||||
|
"autumn": {
|
||||||
|
"enabled": *USE_AUTUMN,
|
||||||
|
"url": *AUTUMN_URL
|
||||||
|
},
|
||||||
|
"january": {
|
||||||
|
"enabled": *USE_JANUARY,
|
||||||
|
"url": *JANUARY_URL
|
||||||
|
},
|
||||||
|
"voso": {
|
||||||
|
"enabled": *USE_VOSO,
|
||||||
|
"url": *VOSO_URL,
|
||||||
|
"ws": *VOSO_WS_HOST
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"ws": *EXTERNAL_WS_URL,
|
"ws": *EXTERNAL_WS_URL,
|
||||||
|
"app": *APP_URL,
|
||||||
|
"vapid": *VAPID_PUBLIC_KEY
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::notifications::{events::ClientboundNotification, hive};
|
use crate::notifications::events::ClientboundNotification;
|
||||||
use crate::util::result::{Error, Result};
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
use futures::try_join;
|
use futures::try_join;
|
||||||
@@ -31,10 +31,9 @@ pub async fn req(user: User, username: String) -> Result<JsonValue> {
|
|||||||
with: "user",
|
with: "user",
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match get_relationship(
|
let target_user = Ref::from(target_id.to_string())?.fetch_user().await?;
|
||||||
&user,
|
|
||||||
&target_id,
|
match get_relationship(&user, &target_id) {
|
||||||
) {
|
|
||||||
RelationshipStatus::User => return Err(Error::NoEffect),
|
RelationshipStatus::User => return Err(Error::NoEffect),
|
||||||
RelationshipStatus::Friend => return Err(Error::AlreadyFriends),
|
RelationshipStatus::Friend => return Err(Error::AlreadyFriends),
|
||||||
RelationshipStatus::Outgoing => return Err(Error::AlreadySentRequest),
|
RelationshipStatus::Outgoing => return Err(Error::AlreadySentRequest),
|
||||||
@@ -68,21 +67,26 @@ pub async fn req(user: User, username: String) -> Result<JsonValue> {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
try_join!(
|
let target_user = target_user
|
||||||
|
.from_override(&user, RelationshipStatus::Friend)
|
||||||
|
.await?;
|
||||||
|
let user = user
|
||||||
|
.from_override(&target_user, RelationshipStatus::Friend)
|
||||||
|
.await?;
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
user: target_id.to_string(),
|
user: target_user,
|
||||||
status: RelationshipStatus::Friend
|
status: RelationshipStatus::Friend,
|
||||||
}
|
}
|
||||||
.publish(user.id.clone()),
|
.publish(user.id.clone());
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: target_id.to_string(),
|
id: target_id.to_string(),
|
||||||
user: user.id.clone(),
|
user,
|
||||||
status: RelationshipStatus::Friend
|
status: RelationshipStatus::Friend,
|
||||||
}
|
}
|
||||||
.publish(target_id.to_string())
|
.publish(target_id.to_string());
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(json!({ "status": "Friend" }))
|
Ok(json!({ "status": "Friend" }))
|
||||||
}
|
}
|
||||||
@@ -124,24 +128,26 @@ pub async fn req(user: User, username: String) -> Result<JsonValue> {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
try_join!(
|
let target_user = target_user
|
||||||
|
.from_override(&user, RelationshipStatus::Outgoing)
|
||||||
|
.await?;
|
||||||
|
let user = user
|
||||||
|
.from_override(&target_user, RelationshipStatus::Incoming)
|
||||||
|
.await?;
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
user: target_id.to_string(),
|
user: target_user,
|
||||||
status: RelationshipStatus::Outgoing
|
status: RelationshipStatus::Outgoing,
|
||||||
}
|
}
|
||||||
.publish(user.id.clone()),
|
.publish(user.id.clone());
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: target_id.to_string(),
|
id: target_id.to_string(),
|
||||||
user: user.id.clone(),
|
user,
|
||||||
status: RelationshipStatus::Incoming
|
status: RelationshipStatus::Incoming,
|
||||||
}
|
}
|
||||||
.publish(target_id.to_string())
|
.publish(target_id.to_string());
|
||||||
)
|
|
||||||
.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" }))
|
Ok(json!({ "status": "Outgoing" }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::notifications::{events::ClientboundNotification, hive};
|
use crate::notifications::events::ClientboundNotification;
|
||||||
use crate::util::result::{Error, Result};
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
use futures::try_join;
|
use futures::try_join;
|
||||||
@@ -10,6 +10,8 @@ use rocket_contrib::json::JsonValue;
|
|||||||
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||||
let col = get_collection("users");
|
let col = get_collection("users");
|
||||||
|
|
||||||
|
let target = target.fetch_user().await?;
|
||||||
|
|
||||||
match get_relationship(&user, &target.id) {
|
match get_relationship(&user, &target.id) {
|
||||||
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
|
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
|
||||||
RelationshipStatus::BlockedOther => {
|
RelationshipStatus::BlockedOther => {
|
||||||
@@ -33,12 +35,10 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
|||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
user: target.id.clone(),
|
user: target,
|
||||||
status: RelationshipStatus::Blocked,
|
status: RelationshipStatus::Blocked,
|
||||||
}
|
}
|
||||||
.publish(user.id.clone())
|
.publish(user.id.clone());
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(json!({ "status": "Blocked" }))
|
Ok(json!({ "status": "Blocked" }))
|
||||||
}
|
}
|
||||||
@@ -74,24 +74,27 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
try_join!(
|
let target = target
|
||||||
|
.from_override(&user, RelationshipStatus::Blocked)
|
||||||
|
.await?;
|
||||||
|
let user = user
|
||||||
|
.from_override(&target, RelationshipStatus::BlockedOther)
|
||||||
|
.await?;
|
||||||
|
let target_id = target.id.clone();
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
user: target.id.clone(),
|
user: target,
|
||||||
status: RelationshipStatus::Blocked
|
status: RelationshipStatus::Blocked,
|
||||||
}
|
}
|
||||||
.publish(user.id.clone()),
|
.publish(user.id.clone());
|
||||||
ClientboundNotification::UserRelationship {
|
|
||||||
id: target.id.clone(),
|
|
||||||
user: user.id.clone(),
|
|
||||||
status: RelationshipStatus::BlockedOther
|
|
||||||
}
|
|
||||||
.publish(target.id.clone())
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
hive::subscribe_if_exists(user.id.clone(), target.id.clone()).ok();
|
ClientboundNotification::UserRelationship {
|
||||||
hive::subscribe_if_exists(target.id.clone(), user.id.clone()).ok();
|
id: target_id.clone(),
|
||||||
|
user,
|
||||||
|
status: RelationshipStatus::BlockedOther,
|
||||||
|
}
|
||||||
|
.publish(target_id);
|
||||||
|
|
||||||
Ok(json!({ "status": "Blocked" }))
|
Ok(json!({ "status": "Blocked" }))
|
||||||
}
|
}
|
||||||
@@ -131,21 +134,27 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
try_join!(
|
let target = target
|
||||||
|
.from_override(&user, RelationshipStatus::Blocked)
|
||||||
|
.await?;
|
||||||
|
let user = user
|
||||||
|
.from_override(&target, RelationshipStatus::BlockedOther)
|
||||||
|
.await?;
|
||||||
|
let target_id = target.id.clone();
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
user: target.id.clone(),
|
user: target,
|
||||||
status: RelationshipStatus::Blocked
|
status: RelationshipStatus::Blocked,
|
||||||
}
|
}
|
||||||
.publish(user.id.clone()),
|
.publish(user.id.clone());
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: target.id.clone(),
|
id: target_id.clone(),
|
||||||
user: user.id.clone(),
|
user,
|
||||||
status: RelationshipStatus::BlockedOther
|
status: RelationshipStatus::BlockedOther,
|
||||||
}
|
}
|
||||||
.publish(target.id.clone())
|
.publish(target_id);
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(json!({ "status": "Blocked" }))
|
Ok(json!({ "status": "Blocked" }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
use crate::database::*;
|
||||||
|
use crate::notifications::events::ClientboundNotification;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
|
use mongodb::bson::doc;
|
||||||
|
use rauth::auth::{Auth, Session};
|
||||||
|
use regex::Regex;
|
||||||
|
use rocket::State;
|
||||||
|
use rocket_contrib::json::Json;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
// ! FIXME: should be global somewhere; maybe use config(?)
|
||||||
|
lazy_static! {
|
||||||
|
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize)]
|
||||||
|
pub struct Data {
|
||||||
|
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
|
||||||
|
username: Option<String>,
|
||||||
|
#[validate(length(min = 8, max = 72))]
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("/<_ignore_id>/username", data = "<data>")]
|
||||||
|
pub async fn req(
|
||||||
|
auth: State<'_, Auth>,
|
||||||
|
session: Session,
|
||||||
|
user: User,
|
||||||
|
data: Json<Data>,
|
||||||
|
_ignore_id: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
data.validate()
|
||||||
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
|
auth.verify_password(&session, data.password.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::InvalidCredentials)?;
|
||||||
|
|
||||||
|
let mut set = doc! {};
|
||||||
|
if let Some(username) = &data.username {
|
||||||
|
if User::is_username_taken(&username).await? {
|
||||||
|
return Err(Error::UsernameTaken);
|
||||||
|
}
|
||||||
|
|
||||||
|
set.insert("username", username.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
get_collection("users")
|
||||||
|
.update_one(doc! { "_id": &user.id }, doc! { "$set": set }, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "user",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
ClientboundNotification::UserUpdate {
|
||||||
|
id: user.id.clone(),
|
||||||
|
data: json!(data.0),
|
||||||
|
clear: None,
|
||||||
|
}
|
||||||
|
.publish(user.id.clone());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
use crate::notifications::events::ClientboundNotification;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
use crate::{database::*, notifications::events::RemoveUserField};
|
||||||
|
|
||||||
|
use mongodb::bson::{doc, to_document};
|
||||||
|
use rocket_contrib::json::Json;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct UserProfileData {
|
||||||
|
#[validate(length(min = 0, max = 2000))]
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
content: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[validate(length(min = 1, max = 128))]
|
||||||
|
background: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Validate, Serialize, Deserialize)]
|
||||||
|
pub struct Data {
|
||||||
|
#[validate]
|
||||||
|
status: Option<UserStatus>,
|
||||||
|
#[validate]
|
||||||
|
profile: Option<UserProfileData>,
|
||||||
|
#[validate(length(min = 1, max = 128))]
|
||||||
|
avatar: Option<String>,
|
||||||
|
remove: Option<RemoveUserField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[patch("/<_ignore_id>", data = "<data>")]
|
||||||
|
pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<()> {
|
||||||
|
let mut data = data.into_inner();
|
||||||
|
|
||||||
|
data.validate()
|
||||||
|
.map_err(|error| Error::FailedValidation { error })?;
|
||||||
|
|
||||||
|
if data.status.is_none()
|
||||||
|
&& data.profile.is_none()
|
||||||
|
&& data.avatar.is_none()
|
||||||
|
&& data.remove.is_none()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut unset = doc! {};
|
||||||
|
let mut set = doc! {};
|
||||||
|
|
||||||
|
let mut remove_background = false;
|
||||||
|
let mut remove_avatar = false;
|
||||||
|
|
||||||
|
if let Some(remove) = &data.remove {
|
||||||
|
match remove {
|
||||||
|
RemoveUserField::ProfileContent => {
|
||||||
|
unset.insert("profile.content", 1);
|
||||||
|
}
|
||||||
|
RemoveUserField::ProfileBackground => {
|
||||||
|
unset.insert("profile.background", 1);
|
||||||
|
remove_background = true;
|
||||||
|
}
|
||||||
|
RemoveUserField::StatusText => {
|
||||||
|
unset.insert("status.text", 1);
|
||||||
|
}
|
||||||
|
RemoveUserField::Avatar => {
|
||||||
|
unset.insert("avatar", 1);
|
||||||
|
remove_avatar = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(status) = &data.status {
|
||||||
|
set.insert(
|
||||||
|
"status",
|
||||||
|
to_document(&status).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "to_document",
|
||||||
|
with: "status",
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(profile) = data.profile {
|
||||||
|
if let Some(content) = profile.content {
|
||||||
|
set.insert("profile.content", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(attachment_id) = profile.background {
|
||||||
|
let attachment =
|
||||||
|
File::find_and_use(&attachment_id, "backgrounds", "user", &user.id).await?;
|
||||||
|
set.insert(
|
||||||
|
"profile.background",
|
||||||
|
to_document(&attachment).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "to_document",
|
||||||
|
with: "attachment",
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_background = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatar = std::mem::replace(&mut data.avatar, None);
|
||||||
|
let attachment = if let Some(attachment_id) = avatar {
|
||||||
|
let attachment = File::find_and_use(&attachment_id, "avatars", "user", &user.id).await?;
|
||||||
|
set.insert(
|
||||||
|
"avatar",
|
||||||
|
to_document(&attachment).map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "to_document",
|
||||||
|
with: "attachment",
|
||||||
|
})?,
|
||||||
|
);
|
||||||
|
|
||||||
|
remove_avatar = true;
|
||||||
|
Some(attachment)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut operations = doc! {};
|
||||||
|
if set.len() > 0 {
|
||||||
|
operations.insert("$set", set);
|
||||||
|
}
|
||||||
|
|
||||||
|
if unset.len() > 0 {
|
||||||
|
operations.insert("$unset", unset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if operations.len() > 0 {
|
||||||
|
get_collection("users")
|
||||||
|
.update_one(doc! { "_id": &user.id }, operations, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "update_one",
|
||||||
|
with: "user",
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(status) = &data.status {
|
||||||
|
ClientboundNotification::UserUpdate {
|
||||||
|
id: user.id.clone(),
|
||||||
|
data: json!({ "status": status }),
|
||||||
|
clear: None,
|
||||||
|
}
|
||||||
|
.publish(user.id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(avatar) = attachment {
|
||||||
|
ClientboundNotification::UserUpdate {
|
||||||
|
id: user.id.clone(),
|
||||||
|
data: json!({ "avatar": avatar }),
|
||||||
|
clear: None,
|
||||||
|
}
|
||||||
|
.publish(user.id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(clear) = data.remove {
|
||||||
|
ClientboundNotification::UserUpdate {
|
||||||
|
id: user.id.clone(),
|
||||||
|
data: json!({}),
|
||||||
|
clear: Some(clear),
|
||||||
|
}
|
||||||
|
.publish(user.id.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if remove_avatar {
|
||||||
|
if let Some(old_avatar) = user.avatar {
|
||||||
|
old_avatar.delete().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if remove_background {
|
||||||
|
if let Some(profile) = user.profile {
|
||||||
|
if let Some(old_background) = profile.background {
|
||||||
|
old_background.delete().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -12,11 +12,11 @@ pub async fn req(user: User) -> Result<JsonValue> {
|
|||||||
doc! {
|
doc! {
|
||||||
"$or": [
|
"$or": [
|
||||||
{
|
{
|
||||||
"type": "DirectMessage",
|
"channel_type": "DirectMessage",
|
||||||
"active": true
|
"active": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "Group"
|
"channel_type": "Group"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"recipients": user.id
|
"recipients": user.id
|
||||||
|
|||||||
@@ -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!({}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +1,20 @@
|
|||||||
use crate::{database::*, notifications::websocket::is_online};
|
use crate::database::*;
|
||||||
use crate::util::result::{Error, Result};
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
use rocket_contrib::json::JsonValue;
|
use rocket_contrib::json::JsonValue;
|
||||||
|
|
||||||
#[get("/<target>")]
|
#[get("/<target>")]
|
||||||
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||||
let mut target = target.fetch_user().await?;
|
let target = target.fetch_user().await?;
|
||||||
|
|
||||||
|
let perm = permissions::PermissionCalculator::new(&user)
|
||||||
|
.with_user(&target)
|
||||||
|
.for_user_given()
|
||||||
|
.await?;
|
||||||
|
|
||||||
if user.id != target.id {
|
|
||||||
// Check whether we are allowed to fetch this user.
|
|
||||||
let perm = permissions::user::calculate(&user, &target.id).await;
|
|
||||||
if !perm.get_access() {
|
if !perm.get_access() {
|
||||||
Err(Error::LabelMe)?
|
Err(Error::MissingPermission)?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only return user relationships if the target is the caller.
|
Ok(json!(target.from(&user).with(perm)))
|
||||||
target.relations = None;
|
|
||||||
|
|
||||||
// Add relevant relationship
|
|
||||||
if let Some(relationships) = &user.relations {
|
|
||||||
target.relationship = relationships
|
|
||||||
.iter()
|
|
||||||
.find(|x| x.id == user.id)
|
|
||||||
.map(|x| x.status.clone())
|
|
||||||
.or_else(|| Some(RelationshipStatus::None));
|
|
||||||
} else {
|
|
||||||
target.relationship = Some(RelationshipStatus::None);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
target.relationship = Some(RelationshipStatus::User);
|
|
||||||
}
|
|
||||||
|
|
||||||
target.online = Some(is_online(&target.id));
|
|
||||||
|
|
||||||
Ok(json!(target))
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use crate::database::*;
|
||||||
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
|
use futures::StreamExt;
|
||||||
|
use mongodb::bson::{doc, Document};
|
||||||
|
use mongodb::options::FindOptions;
|
||||||
|
use rocket_contrib::json::JsonValue;
|
||||||
|
|
||||||
|
#[get("/<target>/mutual")]
|
||||||
|
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||||
|
let users = get_collection("users")
|
||||||
|
.find(
|
||||||
|
doc! {
|
||||||
|
"$and": [
|
||||||
|
{ "relations": { "$elemMatch": { "_id": &user.id, "status": "Friend" } } },
|
||||||
|
{ "relations": { "$elemMatch": { "_id": &target.id, "status": "Friend" } } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::DatabaseError {
|
||||||
|
operation: "find",
|
||||||
|
with: "users",
|
||||||
|
})?
|
||||||
|
.filter_map(async move |s| s.ok())
|
||||||
|
.collect::<Vec<Document>>()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
Ok(json!({ "users": users }))
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
use rocket::response::NamedFile;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::database::Ref;
|
|
||||||
|
|
||||||
#[get("/<target>/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!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-2
@@ -2,11 +2,15 @@ use rocket::Route;
|
|||||||
|
|
||||||
mod add_friend;
|
mod add_friend;
|
||||||
mod block_user;
|
mod block_user;
|
||||||
|
mod change_username;
|
||||||
|
mod edit_user;
|
||||||
mod fetch_dms;
|
mod fetch_dms;
|
||||||
|
mod fetch_profile;
|
||||||
mod fetch_relationship;
|
mod fetch_relationship;
|
||||||
mod fetch_relationships;
|
mod fetch_relationships;
|
||||||
mod fetch_user;
|
mod fetch_user;
|
||||||
mod get_avatar;
|
mod find_mutual;
|
||||||
|
mod get_default_avatar;
|
||||||
mod open_dm;
|
mod open_dm;
|
||||||
mod remove_friend;
|
mod remove_friend;
|
||||||
mod unblock_user;
|
mod unblock_user;
|
||||||
@@ -15,11 +19,15 @@ pub fn routes() -> Vec<Route> {
|
|||||||
routes![
|
routes![
|
||||||
// User Information
|
// User Information
|
||||||
fetch_user::req,
|
fetch_user::req,
|
||||||
get_avatar::req,
|
edit_user::req,
|
||||||
|
change_username::req,
|
||||||
|
get_default_avatar::req,
|
||||||
|
fetch_profile::req,
|
||||||
// Direct Messaging
|
// Direct Messaging
|
||||||
fetch_dms::req,
|
fetch_dms::req,
|
||||||
open_dm::req,
|
open_dm::req,
|
||||||
// Relationships
|
// Relationships
|
||||||
|
find_mutual::req,
|
||||||
fetch_relationships::req,
|
fetch_relationships::req,
|
||||||
fetch_relationship::req,
|
fetch_relationship::req,
|
||||||
add_friend::req,
|
add_friend::req,
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ use ulid::Ulid;
|
|||||||
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||||
let query = if user.id == target.id {
|
let query = if user.id == target.id {
|
||||||
doc! {
|
doc! {
|
||||||
"type": "SavedMessages",
|
"channel_type": "SavedMessages",
|
||||||
"user": &user.id
|
"user": &user.id
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
doc! {
|
doc! {
|
||||||
"type": "DirectMessage",
|
"channel_type": "DirectMessage",
|
||||||
"recipients": {
|
"recipients": {
|
||||||
"$all": [ &user.id, &target.id ]
|
"$all": [ &user.id, &target.id ]
|
||||||
}
|
}
|
||||||
@@ -40,10 +40,11 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
|||||||
id,
|
id,
|
||||||
active: false,
|
active: false,
|
||||||
recipients: vec![user.id, target.id],
|
recipients: vec![user.id, target.id],
|
||||||
|
last_message: None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
channel.save().await?;
|
channel.clone().publish().await?;
|
||||||
Ok(json!(channel))
|
Ok(json!(channel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::notifications::{events::ClientboundNotification, hive};
|
use crate::notifications::events::ClientboundNotification;
|
||||||
use crate::util::result::{Error, Result};
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
use futures::try_join;
|
use futures::try_join;
|
||||||
use hive_pubsub::PubSub;
|
|
||||||
use mongodb::bson::doc;
|
use mongodb::bson::doc;
|
||||||
use rocket_contrib::json::JsonValue;
|
use rocket_contrib::json::JsonValue;
|
||||||
|
|
||||||
@@ -11,6 +10,8 @@ use rocket_contrib::json::JsonValue;
|
|||||||
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||||
let col = get_collection("users");
|
let col = get_collection("users");
|
||||||
|
|
||||||
|
let target = target.fetch_user().await?;
|
||||||
|
|
||||||
match get_relationship(&user, &target.id) {
|
match get_relationship(&user, &target.id) {
|
||||||
RelationshipStatus::Friend
|
RelationshipStatus::Friend
|
||||||
| RelationshipStatus::Outgoing
|
| RelationshipStatus::Outgoing
|
||||||
@@ -44,25 +45,27 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
try_join!(
|
let target = target
|
||||||
|
.from_override(&user, RelationshipStatus::None)
|
||||||
|
.await?;
|
||||||
|
let user = user
|
||||||
|
.from_override(&target, RelationshipStatus::None)
|
||||||
|
.await?;
|
||||||
|
let target_id = target.id.clone();
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
user: target.id.clone(),
|
user: target,
|
||||||
status: RelationshipStatus::None
|
status: RelationshipStatus::None,
|
||||||
}
|
}
|
||||||
.publish(user.id.clone()),
|
.publish(user.id.clone());
|
||||||
ClientboundNotification::UserRelationship {
|
|
||||||
id: target.id.clone(),
|
|
||||||
user: user.id.clone(),
|
|
||||||
status: RelationshipStatus::None
|
|
||||||
}
|
|
||||||
.publish(target.id.clone())
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let hive = hive::get_hive();
|
ClientboundNotification::UserRelationship {
|
||||||
hive.unsubscribe(&user.id, &target.id).ok();
|
id: target_id.clone(),
|
||||||
hive.unsubscribe(&target.id, &user.id).ok();
|
user,
|
||||||
|
status: RelationshipStatus::None,
|
||||||
|
}
|
||||||
|
.publish(target_id);
|
||||||
|
|
||||||
Ok(json!({ "status": "None" }))
|
Ok(json!({ "status": "None" }))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
use crate::database::*;
|
use crate::database::*;
|
||||||
use crate::notifications::{events::ClientboundNotification, hive};
|
use crate::notifications::events::ClientboundNotification;
|
||||||
use crate::util::result::{Error, Result};
|
use crate::util::result::{Error, Result};
|
||||||
|
|
||||||
use futures::try_join;
|
use futures::try_join;
|
||||||
use hive_pubsub::PubSub;
|
|
||||||
use mongodb::bson::doc;
|
use mongodb::bson::doc;
|
||||||
use rocket_contrib::json::JsonValue;
|
use rocket_contrib::json::JsonValue;
|
||||||
|
|
||||||
#[delete("/<target>/block")]
|
#[delete("/<target>/block")]
|
||||||
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
||||||
let col = get_collection("users");
|
let col = get_collection("users");
|
||||||
|
let target = target.fetch_user().await?;
|
||||||
|
|
||||||
match get_relationship(&user, &target.id) {
|
match get_relationship(&user, &target.id) {
|
||||||
RelationshipStatus::Blocked => {
|
RelationshipStatus::Blocked => match get_relationship(&target, &user.id) {
|
||||||
match get_relationship(&target.fetch_user().await?, &user.id) {
|
|
||||||
RelationshipStatus::Blocked => {
|
RelationshipStatus::Blocked => {
|
||||||
col.update_one(
|
col.update_one(
|
||||||
doc! {
|
doc! {
|
||||||
@@ -33,14 +32,15 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
|||||||
with: "user",
|
with: "user",
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let target = target
|
||||||
|
.from_override(&user, RelationshipStatus::BlockedOther)
|
||||||
|
.await?;
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
user: target.id.clone(),
|
user: target,
|
||||||
status: RelationshipStatus::BlockedOther,
|
status: RelationshipStatus::BlockedOther,
|
||||||
}
|
}
|
||||||
.publish(user.id.clone())
|
.publish(user.id.clone());
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(json!({ "status": "BlockedOther" }))
|
Ok(json!({ "status": "BlockedOther" }))
|
||||||
}
|
}
|
||||||
@@ -74,25 +74,27 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
try_join!(
|
let target = target
|
||||||
|
.from_override(&user, RelationshipStatus::None)
|
||||||
|
.await?;
|
||||||
|
let user = user
|
||||||
|
.from_override(&target, RelationshipStatus::None)
|
||||||
|
.await?;
|
||||||
|
let target_id = target.id.clone();
|
||||||
|
|
||||||
ClientboundNotification::UserRelationship {
|
ClientboundNotification::UserRelationship {
|
||||||
id: user.id.clone(),
|
id: user.id.clone(),
|
||||||
user: target.id.clone(),
|
user: target,
|
||||||
status: RelationshipStatus::None
|
status: RelationshipStatus::None,
|
||||||
}
|
}
|
||||||
.publish(user.id.clone()),
|
.publish(user.id.clone());
|
||||||
ClientboundNotification::UserRelationship {
|
|
||||||
id: target.id.clone(),
|
|
||||||
user: user.id.clone(),
|
|
||||||
status: RelationshipStatus::None
|
|
||||||
}
|
|
||||||
.publish(target.id.clone())
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let hive = hive::get_hive();
|
ClientboundNotification::UserRelationship {
|
||||||
hive.unsubscribe(&user.id, &target.id).ok();
|
id: target_id.clone(),
|
||||||
hive.unsubscribe(&target.id, &user.id).ok();
|
user: user,
|
||||||
|
status: RelationshipStatus::None,
|
||||||
|
}
|
||||||
|
.publish(target_id);
|
||||||
|
|
||||||
Ok(json!({ "status": "None" }))
|
Ok(json!({ "status": "None" }))
|
||||||
}
|
}
|
||||||
@@ -103,8 +105,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => Err(Error::InternalError),
|
_ => Err(Error::InternalError),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
_ => Err(Error::NoEffect),
|
_ => Err(Error::NoEffect),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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 result;
|
||||||
pub mod variables;
|
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>()
|
|
||||||
}
|
|
||||||
@@ -30,8 +30,30 @@ pub enum Error {
|
|||||||
Blocked,
|
Blocked,
|
||||||
#[snafu(display("You have been blocked by this user."))]
|
#[snafu(display("You have been blocked by this user."))]
|
||||||
BlockedByOther,
|
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.
|
// ? General errors.
|
||||||
|
#[snafu(display("Trying to fetch too much data."))]
|
||||||
|
TooManyIds,
|
||||||
#[snafu(display("Failed to validate fields."))]
|
#[snafu(display("Failed to validate fields."))]
|
||||||
FailedValidation { error: ValidationErrors },
|
FailedValidation { error: ValidationErrors },
|
||||||
#[snafu(display("Encountered a database error."))]
|
#[snafu(display("Encountered a database error."))]
|
||||||
@@ -41,6 +63,16 @@ pub enum Error {
|
|||||||
},
|
},
|
||||||
#[snafu(display("Internal server error."))]
|
#[snafu(display("Internal server error."))]
|
||||||
InternalError,
|
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."))]
|
#[snafu(display("This request had no effect."))]
|
||||||
NoEffect,
|
NoEffect,
|
||||||
}
|
}
|
||||||
@@ -61,10 +93,26 @@ impl<'r> Responder<'r, 'static> for Error {
|
|||||||
Error::AlreadySentRequest => Status::Conflict,
|
Error::AlreadySentRequest => Status::Conflict,
|
||||||
Error::Blocked => Status::Conflict,
|
Error::Blocked => Status::Conflict,
|
||||||
Error::BlockedByOther => Status::Forbidden,
|
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::FailedValidation { .. } => Status::UnprocessableEntity,
|
||||||
Error::DatabaseError { .. } => Status::InternalServerError,
|
Error::DatabaseError { .. } => Status::InternalServerError,
|
||||||
Error::InternalError => 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,
|
Error::NoEffect => Status::Ok,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+41
-6
@@ -7,19 +7,38 @@ lazy_static! {
|
|||||||
// Application Settings
|
// Application Settings
|
||||||
pub static ref MONGO_URI: String =
|
pub static ref MONGO_URI: String =
|
||||||
env::var("REVOLT_MONGO_URI").expect("Missing REVOLT_MONGO_URI environment variable.");
|
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 =
|
pub static ref PUBLIC_URL: String =
|
||||||
env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable.");
|
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 =
|
pub static ref EXTERNAL_WS_URL: String =
|
||||||
env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable.");
|
env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable.");
|
||||||
|
|
||||||
|
pub static ref AUTUMN_URL: String =
|
||||||
|
env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
|
||||||
|
pub static ref JANUARY_URL: String =
|
||||||
|
env::var("JANUARY_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
|
||||||
|
pub static ref VOSO_URL: String =
|
||||||
|
env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
|
||||||
|
pub static ref VOSO_WS_HOST: String =
|
||||||
|
env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string());
|
||||||
|
pub static ref VOSO_MANAGE_TOKEN: String =
|
||||||
|
env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string());
|
||||||
|
|
||||||
pub static ref HCAPTCHA_KEY: String =
|
pub static ref HCAPTCHA_KEY: String =
|
||||||
env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string());
|
env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string());
|
||||||
pub static ref HCAPTCHA_SITEKEY: String =
|
pub static ref HCAPTCHA_SITEKEY: String =
|
||||||
env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string());
|
env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string());
|
||||||
pub static ref WS_HOST: String =
|
pub static ref VAPID_PRIVATE_KEY: String =
|
||||||
env::var("REVOLT_WS_HOST").unwrap_or_else(|_| "0.0.0.0:9000".to_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
|
// 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(
|
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_HOST").is_ok()
|
||||||
&& env::var("REVOLT_SMTP_USERNAME").is_ok()
|
&& env::var("REVOLT_SMTP_USERNAME").is_ok()
|
||||||
@@ -28,18 +47,34 @@ lazy_static! {
|
|||||||
|v| v == *"1"
|
|v| v == *"1"
|
||||||
);
|
);
|
||||||
pub static ref USE_HCAPTCHA: bool = env::var("REVOLT_HCAPTCHA_KEY").is_ok();
|
pub static ref USE_HCAPTCHA: bool = env::var("REVOLT_HCAPTCHA_KEY").is_ok();
|
||||||
|
pub static ref USE_PROMETHEUS: bool = env::var("REVOLT_ENABLE_PROMETHEUS").map_or(false, |v| v == "1");
|
||||||
|
pub static ref USE_AUTUMN: bool = env::var("AUTUMN_PUBLIC_URL").is_ok();
|
||||||
|
pub static ref USE_JANUARY: bool = env::var("JANUARY_PUBLIC_URL").is_ok();
|
||||||
|
pub static ref USE_VOSO: bool = env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok();
|
||||||
|
|
||||||
// SMTP Settings
|
// SMTP Settings
|
||||||
pub static ref SMTP_HOST: String =
|
pub static ref SMTP_HOST: String =
|
||||||
env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string());
|
env::var("REVOLT_SMTP_HOST").unwrap_or_else(|_| "".to_string());
|
||||||
pub static ref SMTP_USERNAME: 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 =
|
pub static ref SMTP_PASSWORD: String =
|
||||||
env::var("SMTP_PASSWORD").unwrap_or_else(|_| "".to_string());
|
env::var("REVOLT_SMTP_PASSWORD").unwrap_or_else(|_| "".to_string());
|
||||||
pub static ref SMTP_FROM: String = env::var("SMTP_FROM").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() {
|
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 {
|
if *USE_EMAIL == false {
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
|
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
pub const VERSION: &str = "0.4.1-alpha.11";
|
||||||
Reference in new issue
Block a user