Compare commits

..
55 Commits
Author SHA1 Message Date
Paul fae0198343 Bump version to 0.4.1-alpha.11 2021-05-14 22:54:27 +01:00
Paul 10f116b4a3 Block voice calls in saved messages channel.
Add messages for desc / icon change for group chats.
2021-05-14 22:44:35 +01:00
Paul 6716a2d32b Join with newlines for embed generation.
Server January URL endpoint on server configuration.
2021-05-14 22:39:16 +01:00
Paul 6cc92b877e Fix block user route, send correct user struct.
Add route for fetching members.
Cargo fmt on accident.
2021-05-14 22:29:43 +01:00
Paul cc0307f702 Fix find mutual query. 2021-05-13 17:19:43 +01:00
Paul 274d6f2b5a Update January definitions. (special embeds) 2021-05-09 19:30:55 +01:00
Paul 0f100213ba Update January definitions; add servers collection 2021-05-08 17:08:45 +01:00
Paul 7293abfc53 Add january support. 2021-05-07 18:00:21 +01:00
Paul cf7bb832da Really make sure MongoDB driver can't screw up. 2021-05-03 21:02:12 +01:00
Paul efc3794f3d Emergency patch; Rust MDB driver deletes doc. with empty update. 2021-05-03 20:48:37 +01:00
Paul a319e72655 Add a way to choose message fetch sort. 2021-05-03 16:35:59 +01:00
Paul 2173b1e9f8 Fix type. 2021-05-03 15:04:05 +01:00
Paul c086fe7ac4 Move versioning to source. 2021-05-03 15:03:30 +01:00
Paul fa960ebc94 Send out unset in event. 2021-05-03 11:17:28 +01:00
Paul b47067b311 Cache Cargo dependencies for Docker build. 2021-05-03 09:59:40 +01:00
Paul 2e996a487b Add event when user goes offline. 2021-05-03 08:57:49 +01:00
Paul 92bface6ae Add group icons / profile backgrounds. 2021-05-01 22:55:37 +01:00
Paul 8cfa5d7091 Run cargo fmt. 2021-05-01 17:29:31 +01:00
Paul 59b18fd376 Delete old avatar; fix database migration. 2021-05-01 17:12:51 +01:00
Paul f135a57a9b API change: deprecate Gravatar.
Include avatar information in user object.
2021-05-01 16:38:06 +01:00
Paul 5da26cb833 Breaking: Provide new user object instead of id.
Fix rustup complaining about join macros.
2021-05-01 15:54:29 +01:00
Paul c8981ac695 Merge branch 'master' of https://gitlab.insrt.uk/revolt/delta 2021-05-01 13:08:55 +01:00
Martin Loffler 33b0658680 Multi-thread rocket and hive 2021-04-30 13:19:31 +02:00
Paul 81002c75d2 Only push out status; mark attachments as deleted. 2021-04-28 17:12:14 +01:00
Paul f163cb65de AAAAAAAAAAAAA
We should be setting message_id not message.
2021-04-24 18:01:08 +01:00
Paul f7bcd3ad93 Migrate to Autumn 1.0.0 2021-04-24 16:00:13 +01:00
Paul 9e2250783c Allow people with mutual connections to see profile / status; fix username check. 2021-04-07 19:21:25 +01:00
Paul 2050a0609f Fix update_one failing on sys message. 2021-04-03 21:37:24 +01:00
Paul a547177325 Add status presence indicator. 2021-04-03 20:50:14 +01:00
Paul c3fa7bf74e Change route paths for user edits. 2021-04-03 15:02:29 +01:00
Paul 7f5d6f2312 Format code. 2021-04-03 14:44:01 +01:00
Paul 9492e145f9 User status / profile change route, username change route. 2021-04-03 14:41:40 +01:00
Paul 78c42fd412 Fetch profile route, push badges and status. 2021-04-02 21:55:02 +01:00
Paul 3797878d20 Fixes bug with distributed nodes. 2021-04-02 13:25:51 +01:00
Paul d2bf477fd8 Expose voso host. 2021-04-01 15:11:19 +01:00
Martin Loffler c9bff789de Fixed room creation failure in join_call 2021-04-01 15:59:06 +02:00
Paul 56bd763820 Prevent setting null in edit. Preflight for variables. 2021-04-01 14:01:07 +01:00
Paul 32cd9d8a13 Strict typing for system messages; add a way to rename group. 2021-03-31 20:54:47 +01:00
Paul 60731e1c70 Integrate voso, join_call route for channels. 2021-03-31 11:48:52 +01:00
Paul 37df18dbf7 Fixes mutual connections not being seen and hence requests failing. 2021-03-30 22:09:36 +01:00
Paul 0a9d5de369 Don't unwrap on subscription errors. 2021-03-29 10:14:25 +01:00
Paul d2864ac025 Make sure to check that they are friends. 2021-02-19 15:49:55 +00:00
Paul 4eb76fd154 Fix mutex issues. 2021-02-19 15:24:19 +00:00
Paul 3c7852271a Add temporary typing indicator impl. 2021-02-19 14:50:23 +00:00
Paul dadad271b4 Mark attachments as deleted when deleting messages. 2021-02-19 14:12:21 +00:00
Paul db6047f2d3 Allow empty messages with attachment. 2021-02-19 14:09:19 +00:00
Paul 5baf85a8e9 JSON system messages. 2021-02-19 14:03:41 +00:00
Paul 10cac358a9 Label permission errors, and too many ids for /stale. 2021-02-19 13:26:04 +00:00
Paul 64d2707366 Find mutual for users; allow dots in usernames. 2021-02-19 13:11:33 +00:00
Paul 78cfbf9d21 Clean up subscription code; handle error properly. 2021-02-19 12:48:52 +00:00
Paul 02077a4024 Check if online. 2021-02-18 16:47:59 +00:00
Paul 52e070c21c Basic Web Push support. 2021-02-18 16:21:34 +00:00
Paul Makles a2f14d2d37 Bump version. 2021-02-16 15:26:31 +00:00
Paul Makles a7ea29d821 Add a way to send messages with attachments. 2021-02-16 15:25:33 +00:00
Paul Makles b6b51bca26 Route which lets clients determine messages that have updated / deleted. 2021-02-16 09:34:24 +00:00
59 changed files with 2111 additions and 467 deletions
+1
View File
@@ -1,2 +1,3 @@
target target
.mongo
.env .env
+5
View File
@@ -0,0 +1,5 @@
{
"rust-analyzer.diagnostics.disabled": [
"unresolved-macro-call"
]
}
Generated
+76 -6
View File
@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.14.1" version = "0.14.1"
@@ -243,6 +245,16 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base64"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5032d51da2741729bfdaeb2664d9b8c6d9fd1e2b90715c660b6def36628499c2"
dependencies = [
"byteorder",
"safemem",
]
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.11.0" version = "0.11.0"
@@ -686,6 +698,15 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "erased-serde"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0465971a8cc1fa2455c8465aaa377131e1f1cf4983280f474a13e68793aa770c"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "err-derive" name = "err-derive"
version = "0.2.4" version = "0.2.4"
@@ -1046,9 +1067,9 @@ checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
[[package]] [[package]]
name = "hive_pubsub" name = "hive_pubsub"
version = "0.4.3" version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83760410241f6db418bb15e54b506a0887e7240286e29e5b0d2d88f5d1659b24" checksum = "2ac4635b1e9faf304ccb33c2f0edbc615030e40d249183281fc788ef20ef5c76"
dependencies = [ dependencies = [
"futures", "futures",
"many-to-many", "many-to-many",
@@ -1368,6 +1389,15 @@ version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "linkify"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1986921c3c13e81df623c66a298d4b130c061bcb98a01f5b2d3ac402b1649a7f"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.3.4" version = "0.3.4"
@@ -2454,10 +2484,11 @@ dependencies = [
[[package]] [[package]]
name = "revolt" name = "revolt"
version = "0.3.3-alpha.3" version = "0.0.0"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
"base64 0.13.0",
"bitfield", "bitfield",
"chrono", "chrono",
"ctrlc", "ctrlc",
@@ -2468,6 +2499,7 @@ dependencies = [
"impl_ops", "impl_ops",
"lazy_static", "lazy_static",
"lettre", "lettre",
"linkify",
"log", "log",
"many-to-many", "many-to-many",
"md5", "md5",
@@ -2489,6 +2521,7 @@ dependencies = [
"ulid", "ulid",
"urlencoding", "urlencoding",
"validator", "validator",
"web-push",
] ]
[[package]] [[package]]
@@ -2501,7 +2534,7 @@ dependencies = [
"libc", "libc",
"once_cell", "once_cell",
"spin", "spin",
"untrusted", "untrusted 0.7.1",
"web-sys", "web-sys",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@@ -2673,6 +2706,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "safemem"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.19" version = "0.1.19"
@@ -2705,7 +2744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [ dependencies = [
"ring", "ring",
"untrusted", "untrusted 0.7.1",
] ]
[[package]] [[package]]
@@ -3477,6 +3516,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "untrusted"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"
@@ -3678,6 +3723,31 @@ version = "0.2.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64"
[[package]]
name = "web-push"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8353dd6c7cfb9a02737fd6dc9a66a80dca2a93fb690f6ad264d2a7672e6f1c0"
dependencies = [
"base64 0.7.0",
"chrono",
"erased-serde",
"futures",
"http",
"hyper",
"hyper-tls",
"lazy_static",
"log",
"native-tls",
"openssl",
"ring",
"serde",
"serde_derive",
"serde_json",
"time 0.1.44",
"untrusted 0.6.2",
]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.47" version = "0.3.47"
@@ -3695,7 +3765,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [ dependencies = [
"ring", "ring",
"untrusted", "untrusted 0.7.1",
] ]
[[package]] [[package]]
+37 -36
View File
@@ -1,47 +1,48 @@
[package] [package]
name = "revolt" name = "revolt"
version = "0.3.3-alpha.3" # 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"
impl_ops = "0.1.1" md5 = "0.7.0"
many-to-many = "0.1.2" log = "0.4.11"
ctrlc = { version = "3.0", features = ["termination"] }
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "c52758a5087cd035b0ed9c6eacc942ba5468d2ce" }
hive_pubsub = { version = "0.4.3", features = ["mongo"] }
rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" }
rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", 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" }
urlencoding = "1.1.1" mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
lazy_static = "1.4.0" rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "c52758a5087cd035b0ed9c6eacc942ba5468d2ce" }
num_enum = "0.5.1" rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
chrono = "0.4.15" rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
time = "0.2.16" rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false }
rand = "0.7.3"
md5 = "0.7.0"
regex = "1"
lettre = "0.10.0-alpha.1"
env_logger = "0.7.1"
log = "0.4.11"
+4
View File
@@ -1,10 +1,14 @@
# Build Stage # Build Stage
FROM ekidd/rust-musl-builder:nightly-2021-01-01 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 /home/rust/src/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 assets/templates ./assets/templates
COPY src ./src COPY src ./src
RUN cargo build --release RUN cargo build --release
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
source set_version.sh
docker build -t revoltchat/server:${version} . &&
docker push revoltchat/server:${version}
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
export version=0.4.1-alpha.11
echo "pub const VERSION: &str = \"${version}\";" > src/version.rs
+1
View File
@@ -0,0 +1 @@
fn main() {}
+115
View File
@@ -0,0 +1,115 @@
use mongodb::bson::{doc, from_document};
use serde::{Deserialize, Serialize};
use crate::database::*;
use crate::util::result::{Error, Result};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
enum Metadata {
File,
Text,
Image { width: isize, height: isize },
Video { width: isize, height: isize },
Audio,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct File {
#[serde(rename = "_id")]
pub id: String,
tag: String,
filename: String,
metadata: Metadata,
content_type: String,
size: isize,
#[serde(skip_serializing_if = "Option::is_none")]
deleted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
object_id: Option<String>,
}
impl File {
pub async fn find_and_use(
attachment_id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
) -> Result<File> {
let attachments = get_collection("attachments");
let key = format!("{}_id", parent_type);
if let Some(doc) = attachments
.find_one(
doc! {
"_id": attachment_id,
"tag": &tag,
key.clone(): {
"$exists": false
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "attachment",
})?
{
let attachment = from_document::<File>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "attachment",
})?;
attachments
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
key: &parent_id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
Ok(attachment)
} else {
Err(Error::UnknownAttachment)
}
}
pub async fn delete(&self) -> Result<()> {
get_collection("attachments")
.update_one(
doc! {
"_id": &self.id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})
}
}
+73 -16
View File
@@ -1,7 +1,11 @@
use crate::database::*; use crate::database::*;
use crate::notifications::events::ClientboundNotification; use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use mongodb::bson::{doc, from_document, to_document}; use futures::StreamExt;
use mongodb::{
bson::{doc, from_document, to_document, Document},
options::FindOptions,
};
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -24,6 +28,7 @@ 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")] #[serde(skip_serializing_if = "Option::is_none")]
@@ -34,10 +39,14 @@ pub enum Channel {
id: String, id: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<String>, 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")] #[serde(skip_serializing_if = "Option::is_none")]
last_message: Option<LastMessage>, last_message: Option<LastMessage>,
}, },
@@ -46,9 +55,9 @@ pub enum Channel {
impl Channel { impl Channel {
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
match self { match self {
Channel::SavedMessages { id, .. } => id, Channel::SavedMessages { id, .. }
Channel::DirectMessage { id, .. } => id, | Channel::DirectMessage { id, .. }
Channel::Group { id, .. } => id, | Channel::Group { id, .. } => id,
} }
} }
@@ -84,10 +93,7 @@ impl Channel {
})?; })?;
let channel_id = self.id().to_string(); let channel_id = self.id().to_string();
ClientboundNotification::ChannelCreate(self) ClientboundNotification::ChannelCreate(self).publish(channel_id);
.publish(channel_id)
.await
.ok();
Ok(()) Ok(())
} }
@@ -97,17 +103,65 @@ impl Channel {
ClientboundNotification::ChannelUpdate { ClientboundNotification::ChannelUpdate {
id: id.clone(), id: id.clone(),
data, data,
clear: None,
} }
.publish(id) .publish(id);
.await
.ok();
Ok(()) Ok(())
} }
pub async fn delete(&self) -> Result<()> { pub async fn delete(&self) -> Result<()> {
let id = self.id(); let id = self.id();
get_collection("messages") 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( .delete_many(
doc! { doc! {
"channel": id "channel": id
@@ -133,10 +187,13 @@ impl Channel {
with: "channel", with: "channel",
})?; })?;
ClientboundNotification::ChannelDelete { id: id.to_string() } ClientboundNotification::ChannelDelete { id: id.to_string() }.publish(id.to_string());
.publish(id.to_string())
.await if let Channel::Group { icon, .. } = self {
.ok(); if let Some(attachment) = icon {
attachment.delete().await?;
}
}
Ok(()) Ok(())
} }
+144
View File
@@ -0,0 +1,144 @@
use crate::util::{
result::{Error, Result},
variables::JANUARY_URL,
};
use linkify::{LinkFinder, LinkKind};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ImageSize {
Large,
Preview,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Image {
pub url: String,
pub width: isize,
pub height: isize,
pub size: ImageSize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Video {
pub url: String,
pub width: isize,
pub height: isize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum TwitchType {
Channel,
Video,
Clip,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum BandcampType {
Album,
Track,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Special {
None,
YouTube {
id: String,
},
Twitch {
content_type: TwitchType,
id: String,
},
Spotify {
content_type: String,
id: String,
},
Soundcloud,
Bandcamp {
content_type: BandcampType,
id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Metadata {
url: Option<String>,
special: Option<Special>,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<Image>,
#[serde(skip_serializing_if = "Option::is_none")]
video: Option<Video>,
// #[serde(skip_serializing_if = "Option::is_none")]
// opengraph_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
site_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
color: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Embed {
Website(Metadata),
Image(Image),
None,
}
impl Embed {
pub async fn generate(content: String) -> Result<Vec<Embed>> {
let content = content
.split("\n")
.map(|v| {
// Ignore quoted lines.
if let Some(c) = v.chars().next() {
if c == '>' {
return "";
}
}
v
})
.collect::<Vec<&str>>()
.join("\n");
// ! FIXME: allow multiple links
// ! FIXME: prevent generation if link is surrounded with < >
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
let links: Vec<_> = finder.links(&content).collect();
if links.len() == 0 {
return Err(Error::LabelMe);
}
let link = &links[0];
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/embed", *JANUARY_URL))
.query(&[("url", link.as_str())])
.send()
.await;
match result {
Err(_) => return Err(Error::LabelMe),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => {
let res: Embed = result.json().await.map_err(|_| Error::InvalidOperation)?;
Ok(vec![res])
}
_ => return Err(Error::LabelMe),
},
}
}
}
+209 -29
View File
@@ -1,12 +1,47 @@
use crate::util::variables::{VAPID_PRIVATE_KEY, USE_JANUARY};
use crate::{ use crate::{
database::*, database::*,
notifications::events::ClientboundNotification, notifications::{events::ClientboundNotification, websocket::is_online},
util::result::{Error, Result}, util::result::{Error, Result},
}; };
use mongodb::bson::{doc, to_bson, DateTime};
use futures::StreamExt;
use mongodb::{
bson::{doc, to_bson, DateTime},
options::FindOptions,
};
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ulid::Ulid; 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, Clone)]
#[serde(untagged)]
pub enum Content {
Text(String),
SystemMessage(SystemMessage),
}
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message { pub struct Message {
@@ -17,13 +52,17 @@ pub struct Message {
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")] #[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>>,
} }
impl Message { impl Message {
pub fn create(author: String, channel: String, content: String) -> Message { pub fn create(author: String, channel: String, content: Content) -> Message {
Message { Message {
id: Ulid::new().to_string(), id: Ulid::new().to_string(),
nonce: None, nonce: None,
@@ -31,7 +70,9 @@ impl Message {
author, author,
content, content,
attachment: None,
edited: None, edited: None,
embeds: None,
} }
} }
@@ -44,22 +85,28 @@ impl Message {
with: "message", 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 // ! FIXME: temp code
let channels = get_collection("channels"); let channels = get_collection("channels");
match channel { match &channel {
Channel::DirectMessage { id, .. } => { Channel::DirectMessage { id, .. } => {
set.insert("active", true);
channels channels
.update_one( .update_one(
doc! { "_id": id }, doc! { "_id": id },
doc! { doc! {
"$set": { "$set": set
"active": true,
"last_message": {
"_id": self.id.clone(),
"author": self.author.clone(),
"short": self.content.chars().take(24).collect::<String>()
}
}
}, },
None, None,
) )
@@ -70,17 +117,12 @@ impl Message {
})?; })?;
} }
Channel::Group { id, .. } => { Channel::Group { id, .. } => {
if let Content::Text(_) = &self.content {
channels channels
.update_one( .update_one(
doc! { "_id": id }, doc! { "_id": id },
doc! { doc! {
"$set": { "$set": set
"last_message": {
"_id": self.id.clone(),
"author": self.author.clone(),
"short": self.content.chars().take(24).collect::<String>()
}
}
}, },
None, None,
) )
@@ -90,31 +132,151 @@ impl Message {
with: "channel", with: "channel",
})?; })?;
} }
}
_ => {} _ => {}
} }
ClientboundNotification::Message(self) self.process_embed();
.publish(channel.id().to_string())
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 .await
.ok(); {
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(()) Ok(())
} }
pub async fn publish_update(&self, data: JsonValue) -> Result<()> { pub async fn publish_update(self, data: JsonValue) -> Result<()> {
let channel = self.channel.clone(); let channel = self.channel.clone();
ClientboundNotification::MessageUpdate { ClientboundNotification::MessageUpdate {
id: self.id.clone(), id: self.id.clone(),
data, data,
} }
.publish(channel) .publish(channel);
.await self.process_embed();
.ok();
Ok(()) 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<()> { pub async fn delete(&self) -> Result<()> {
if let Some(attachment) = &self.attachment {
attachment.delete().await?;
}
get_collection("messages") get_collection("messages")
.delete_one( .delete_one(
doc! { doc! {
@@ -132,9 +294,27 @@ impl Message {
ClientboundNotification::MessageDelete { ClientboundNotification::MessageDelete {
id: self.id.clone(), id: self.id.clone(),
} }
.publish(channel) .publish(channel);
if let Some(attachment) = &self.attachment {
get_collection("attachments")
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await .await
.ok(); .map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
}
Ok(()) Ok(())
} }
+6 -2
View File
@@ -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,
}*/ }
+145 -1
View File
@@ -1,6 +1,16 @@
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;
use crate::{database::permissions::user::UserPermissions, notifications::websocket::is_online}; 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)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus { pub enum RelationshipStatus {
@@ -20,14 +30,56 @@ 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>,
@@ -38,6 +90,8 @@ pub struct User {
impl User { impl User {
/// Mutate the user object to include relationship as seen by user. /// Mutate the user object to include relationship as seen by user.
pub fn from(mut self, user: &User) -> User { pub fn from(mut self, user: &User) -> User {
self.relationship = Some(RelationshipStatus::None);
if self.id == user.id { if self.id == user.id {
self.relationship = Some(RelationshipStatus::User); self.relationship = Some(RelationshipStatus::User);
return self; return self;
@@ -58,8 +112,98 @@ impl User {
pub fn with(mut self, permissions: UserPermissions<[u32; 1]>) -> User { pub fn with(mut self, permissions: UserPermissions<[u32; 1]>) -> User {
if permissions.get_view_profile() { if permissions.get_view_profile() {
self.online = Some(is_online(&self.id)); self.online = Some(is_online(&self.id));
} else {
self.status = None;
} }
self.profile = None;
self 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)
}
} }
+8
View File
@@ -25,10 +25,18 @@ pub async fn create_database() {
.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()
+36 -2
View File
@@ -1,4 +1,4 @@
use crate::database::get_collection; use crate::database::{get_collection, get_db};
use log::info; use log::info;
use mongodb::bson::{doc, from_document}; use mongodb::bson::{doc, from_document};
@@ -10,7 +10,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 0; pub const LATEST_REVISION: i32 = 3;
pub async fn migrate_database() { pub async fn migrate_database() {
let migrations = get_collection("migrations"); let migrations = get_collection("migrations");
@@ -53,6 +53,40 @@ pub async fn run_migrations(revision: i32) -> i32 {
info!("Running migration [revision 0]: Test migration system."); info!("Running migration [revision 0]: Test migration system.");
} }
if revision <= 1 {
info!("Running migration [revision 1 / 2021-04-24]: Migrate to Autumn v1.0.0.");
let messages = get_collection("messages");
let attachments = get_collection("attachments");
messages
.update_many(
doc! { "attachment": { "$exists": 1 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0 } },
None,
)
.await
.expect("Failed to update messages.");
attachments
.update_many(
doc! {},
doc! { "$set": { "tag": "attachments", "size": 0 } },
None,
)
.await
.expect("Failed to update attachments.");
}
if revision <= 2 {
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
get_db()
.create_collection("servers", None)
.await
.expect("Failed to create servers collection.");
}
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION LATEST_REVISION
} }
+12 -5
View File
@@ -12,6 +12,8 @@ pub enum ChannelPermission {
View = 1, View = 1,
SendMessage = 2, SendMessage = 2,
ManageMessages = 4, ManageMessages = 4,
ManageChannel = 8,
VoiceCall = 16,
} }
bitfield! { bitfield! {
@@ -20,6 +22,8 @@ bitfield! {
pub get_view, _: 31; pub get_view, _: 31;
pub get_send_message, _: 30; pub get_send_message, _: 30;
pub get_manage_messages, _: 29; 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!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 });
@@ -36,9 +40,7 @@ impl<'a> PermissionCalculator<'a> {
match channel { match channel {
Channel::SavedMessages { user: owner, .. } => { Channel::SavedMessages { user: owner, .. } => {
if &self.perspective.id == owner { if &self.perspective.id == owner {
Ok(ChannelPermission::View Ok(u32::MAX - ChannelPermission::VoiceCall as u32)
+ ChannelPermission::SendMessage
+ ChannelPermission::ManageMessages)
} else { } else {
Ok(0) Ok(0)
} }
@@ -54,7 +56,9 @@ impl<'a> PermissionCalculator<'a> {
let perms = self.for_user(recipient).await?; let perms = self.for_user(recipient).await?;
if perms.get_send_message() { if perms.get_send_message() {
return Ok(ChannelPermission::View + ChannelPermission::SendMessage); return Ok(ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::VoiceCall);
} }
return Ok(ChannelPermission::View as u32); return Ok(ChannelPermission::View as u32);
@@ -69,7 +73,10 @@ impl<'a> PermissionCalculator<'a> {
.find(|x| *x == &self.perspective.id) .find(|x| *x == &self.perspective.id)
.is_some() .is_some()
{ {
Ok(ChannelPermission::View + ChannelPermission::SendMessage) Ok(ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::ManageChannel
+ ChannelPermission::VoiceCall)
} else { } else {
Ok(0) Ok(0)
} }
+12 -3
View File
@@ -9,9 +9,10 @@ pub struct PermissionCalculator<'a> {
perspective: &'a User, perspective: &'a User,
user: Option<&'a User>, user: Option<&'a User>,
relationship: Option<&'a RelationshipStatus>,
channel: Option<&'a Channel>, channel: Option<&'a Channel>,
has_mutual_conncetion: bool, has_mutual_connection: bool,
} }
impl<'a> PermissionCalculator<'a> { impl<'a> PermissionCalculator<'a> {
@@ -20,9 +21,10 @@ impl<'a> PermissionCalculator<'a> {
perspective, perspective,
user: None, user: None,
relationship: None,
channel: None, channel: None,
has_mutual_conncetion: false, has_mutual_connection: false,
} }
} }
@@ -33,6 +35,13 @@ impl<'a> PermissionCalculator<'a> {
} }
} }
pub fn with_relationship(self, relationship: &'a RelationshipStatus) -> PermissionCalculator {
PermissionCalculator {
relationship: Some(&relationship),
..self
}
}
pub fn with_channel(self, channel: &'a Channel) -> PermissionCalculator { pub fn with_channel(self, channel: &'a Channel) -> PermissionCalculator {
PermissionCalculator { PermissionCalculator {
channel: Some(&channel), channel: Some(&channel),
@@ -42,7 +51,7 @@ impl<'a> PermissionCalculator<'a> {
pub fn with_mutual_connection(self) -> PermissionCalculator<'a> { pub fn with_mutual_connection(self) -> PermissionCalculator<'a> {
PermissionCalculator { PermissionCalculator {
has_mutual_conncetion: true, has_mutual_connection: true,
..self ..self
} }
} }
+15 -11
View File
@@ -49,7 +49,12 @@ impl<'a> PermissionCalculator<'a> {
} }
let mut permissions: u32 = 0; let mut permissions: u32 = 0;
match get_relationship(&self.perspective, &target) { match self
.relationship
.clone()
.map(|v| v.to_owned())
.unwrap_or_else(|| get_relationship(&self.perspective, &target))
{
RelationshipStatus::Friend => return Ok(u32::MAX), RelationshipStatus::Friend => return Ok(u32::MAX),
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => { RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
return Ok(UserPermission::Access as u32) return Ok(UserPermission::Access as u32)
@@ -64,18 +69,16 @@ impl<'a> PermissionCalculator<'a> {
_ => {} _ => {}
} }
if self.has_mutual_conncetion if self.has_mutual_connection
|| get_collection("channels") || get_collection("channels")
.find_one( .find_one(
doc! { doc! {
"$or": [ "channel_type": {
{ "type": "Group" }, "$in": ["Group", "DirectMessage"]
{ "type": "DirectMessage" }, },
], "recipients": {
"$and": [ "$all": [ &self.perspective.id, target ]
{ "recipients": &self.perspective.id }, }
{ "recipients": target }
]
}, },
None, None,
) )
@@ -86,7 +89,8 @@ impl<'a> PermissionCalculator<'a> {
})? })?
.is_some() .is_some()
{ {
return Ok(UserPermission::Access as u32); // ! FIXME: add privacy settings
return Ok(UserPermission::Access + UserPermission::ViewProfile);
} }
Ok(permissions) Ok(permissions)
+12 -4
View File
@@ -17,7 +17,9 @@ 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 chrono::Duration;
use futures::join; use futures::join;
use log::info; use log::info;
@@ -38,7 +40,10 @@ 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;
@@ -50,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()
); );
} }
+41 -6
View File
@@ -26,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)]
@@ -51,6 +66,8 @@ pub enum ClientboundNotification {
ChannelUpdate { ChannelUpdate {
id: String, id: String,
data: JsonValue, data: JsonValue,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveChannelField>,
}, },
ChannelGroupJoin { ChannelGroupJoin {
id: String, id: String,
@@ -63,10 +80,24 @@ pub enum ClientboundNotification {
ChannelDelete { ChannelDelete {
id: String, id: String,
}, },
ChannelStartTyping {
UserRelationship {
id: String, id: String,
user: String, user: String,
},
ChannelStopTyping {
id: String,
user: String,
},
UserUpdate {
id: String,
data: JsonValue,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveUserField>,
},
UserRelationship {
id: String,
user: User,
status: RelationshipStatus, status: RelationshipStatus,
}, },
UserPresence { UserPresence {
@@ -76,9 +107,13 @@ pub enum ClientboundNotification {
} }
impl ClientboundNotification { impl ClientboundNotification {
pub async fn publish(self, topic: String) -> Result<(), String> { pub fn publish(self, topic: String) {
async_std::task::spawn(async move {
prehandle_hook(&self); // ! TODO: this should be moved to pubsub prehandle_hook(&self); // ! TODO: this should be moved to pubsub
hive_pubsub::backend::mongo::publish(get_hive(), &topic, self).await hive_pubsub::backend::mongo::publish(get_hive(), &topic, self)
.await
.ok();
});
} }
} }
@@ -102,7 +137,7 @@ pub fn prehandle_hook(notification: &ClientboundNotification) {
} }
ClientboundNotification::UserRelationship { id, user, status } => { ClientboundNotification::UserRelationship { id, user, status } => {
if status != &RelationshipStatus::None { if status != &RelationshipStatus::None {
subscribe_if_exists(id.clone(), user.clone()).ok(); subscribe_if_exists(id.clone(), user.id.clone()).ok();
} }
} }
_ => {} _ => {}
@@ -118,7 +153,7 @@ pub fn posthandle_hook(notification: &ClientboundNotification) {
if status == &RelationshipStatus::None { if status == &RelationshipStatus::None {
get_hive() get_hive()
.hive .hive
.unsubscribe(&id.to_string(), &user.to_string()) .unsubscribe(&id.to_string(), &user.id.to_string())
.ok(); .ok();
} }
} }
+7 -40
View File
@@ -6,13 +6,9 @@ use crate::{
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_document},
options::FindOptions,
};
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(); let mut user_ids: HashSet<String> = HashSet::new();
if let Some(relationships) = &user.relations { if let Some(relationships) = &user.relations {
@@ -68,41 +64,12 @@ pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
} }
user_ids.remove(&user.id); user_ids.remove(&user.id);
if user_ids.len() > 0 { let mut users = if user_ids.len() > 0 {
let mut cursor = get_collection("users") user.fetch_multiple_users(user_ids.into_iter().collect::<Vec<String>>())
.find( .await?
doc! { } else {
"_id": { vec![]
"$in": user_ids.into_iter().collect::<Vec<String>>() };
}
},
FindOptions::builder()
.projection(doc! { "_id": 1, "username": 1 })
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let other: User = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "user",
})?;
let permissions = PermissionCalculator::new(&user)
.with_mutual_connection()
.with_user(&other)
.for_user_given()
.await?;
users.push(other.from(&user).with(permissions));
}
}
}
user.relationship = Some(RelationshipStatus::User); user.relationship = Some(RelationshipStatus::User);
user.online = Some(true); user.online = Some(true);
+57 -5
View File
@@ -71,8 +71,6 @@ async fn accept(stream: TcpStream) {
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 {
@@ -135,9 +133,7 @@ async fn accept(stream: TcpStream) {
id: id.clone(), id: id.clone(),
online: true, online: true,
} }
.publish(id) .publish(id);
.await
.ok();
} }
} }
Err(_) => { Err(_) => {
@@ -161,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(());
}
}
} }
} }
} }
@@ -174,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![];
{ {
+3 -5
View File
@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
match &target { match &target {
@@ -95,14 +95,12 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
id: id.clone(), id: id.clone(),
user: user.id.clone(), user: user.id.clone(),
} }
.publish(id.clone()) .publish(id.clone());
.await
.ok();
Message::create( Message::create(
"00000000000000000000000000".to_string(), "00000000000000000000000000".to_string(),
id.clone(), id.clone(),
format!("<@{}> left the group.", user.id), Content::SystemMessage(SystemMessage::UserLeft { id: user.id }),
) )
.publish(&target) .publish(&target)
.await .await
+153
View File
@@ -0,0 +1,153 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use crate::{database::*, notifications::events::RemoveChannelField};
use mongodb::bson::{doc, to_document};
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 32))]
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[validate(length(min = 0, max = 1024))]
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
remove: Option<RemoveChannelField>,
}
#[patch("/<target>", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<()> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.name.is_none()
&& data.description.is_none()
&& data.icon.is_none()
&& data.remove.is_none()
{
return Ok(());
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_manage_channel() {
Err(Error::MissingPermission)?
}
match &target {
Channel::Group { id, icon, .. } => {
let mut set = doc! {};
let mut unset = doc! {};
let mut remove_icon = false;
if let Some(remove) = &data.remove {
match remove {
RemoveChannelField::Icon => {
unset.insert("icon", 1);
remove_icon = true;
}
}
}
if let Some(name) = &data.name {
set.insert("name", name);
}
if let Some(description) = &data.description {
set.insert("description", description);
}
if let Some(attachment_id) = &data.icon {
let attachment =
File::find_and_use(&attachment_id, "icons", "object", &user.id).await?;
set.insert(
"icon",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_icon = true;
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("channels")
.update_one(doc! { "_id": &id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!(set),
clear: data.remove,
}
.publish(id.clone());
if let Some(name) = data.name {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::ChannelRenamed { name, by: user.id.clone() }),
)
.publish(&target)
.await
.ok();
}
if let Some(_) = data.description {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::ChannelDescriptionChanged { by: user.id.clone() }),
)
.publish(&target)
.await
.ok();
}
if let Some(_) = data.icon {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
Content::SystemMessage(SystemMessage::ChannelIconChanged { by: user.id }),
)
.publish(&target)
.await
.ok();
}
if remove_icon {
if let Some(old_icon) = icon {
old_icon.delete().await?;
}
}
Ok(())
}
_ => Err(Error::InvalidOperation),
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
Ok(json!(target)) Ok(json!(target))
+23
View File
@@ -0,0 +1,23 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use rocket_contrib::json::JsonValue;
#[get("/<target>/members")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
if let Channel::Group { recipients, .. } = target {
Ok(json!(user.fetch_multiple_users(recipients).await?))
} else {
Err(Error::InvalidOperation)
}
}
+6 -5
View File
@@ -16,7 +16,7 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
if let Channel::Group { id, recipients, .. } = &channel { if let Channel::Group { id, recipients, .. } = &channel {
@@ -52,14 +52,15 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
id: id.clone(), id: id.clone(),
user: member.id.clone(), user: member.id.clone(),
} }
.publish(id.clone()) .publish(id.clone());
.await
.ok();
Message::create( Message::create(
"00000000000000000000000000".to_string(), "00000000000000000000000000".to_string(),
id.clone(), id.clone(),
format!("<@{}> added <@{}> to the group.", user.id, member.id), Content::SystemMessage(SystemMessage::UserAdded {
id: member.id,
by: user.id,
}),
) )
.publish(&channel) .publish(&channel)
.await .await
+1
View File
@@ -70,6 +70,7 @@ pub async fn req(user: User, info: Json<Data>) -> Result<JsonValue> {
.unwrap_or_else(|| "A group.".to_string()), .unwrap_or_else(|| "A group.".to_string()),
owner: user.id, owner: user.id,
recipients: set.into_iter().collect::<Vec<String>>(), recipients: set.into_iter().collect::<Vec<String>>(),
icon: None,
last_message: None, last_message: None,
}; };
+6 -5
View File
@@ -20,7 +20,7 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
{ {
if &user.id != owner { if &user.id != owner {
// figure out if we want to use perm system here // figure out if we want to use perm system here
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
if recipients.iter().find(|x| *x == &member.id).is_none() { if recipients.iter().find(|x| *x == &member.id).is_none() {
@@ -49,14 +49,15 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
id: id.clone(), id: id.clone(),
user: member.id.clone(), user: member.id.clone(),
} }
.publish(id.clone()) .publish(id.clone());
.await
.ok();
Message::create( Message::create(
"00000000000000000000000000".to_string(), "00000000000000000000000000".to_string(),
id.clone(), id.clone(),
format!("<@{}> removed <@{}> from the group.", user.id, member.id), Content::SystemMessage(SystemMessage::UserRemove {
id: member.id,
by: user.id,
}),
) )
.publish(&channel) .publish(&channel)
.await .await
+84
View File
@@ -0,0 +1,84 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL};
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct CreateUserResponse {
token: String,
}
#[post("/<target>/join_call")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
if !*USE_VOSO {
return Err(Error::VosoUnavailable);
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_voice_call() {
return Err(Error::MissingPermission);
}
// To join a call:
// - Check if the room exists.
// - If not, create it.
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/room/{}", *VOSO_URL, target.id()))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await;
match result {
Err(_) => return Err(Error::VosoUnavailable),
Ok(result) => match result.status() {
reqwest::StatusCode::OK => (),
reqwest::StatusCode::NOT_FOUND => {
if let Err(_) = client
.post(&format!("{}/room/{}", *VOSO_URL, target.id()))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await
{
return Err(Error::VosoUnavailable);
}
}
_ => return Err(Error::VosoUnavailable),
},
}
// Then create a user for the room.
if let Ok(response) = client
.post(&format!(
"{}/room/{}/user/{}",
*VOSO_URL,
target.id(),
user.id
))
.header(
reqwest::header::AUTHORIZATION,
VOSO_MANAGE_TOKEN.to_string(),
)
.send()
.await
{
let res: CreateUserResponse = response.json().await.map_err(|_| Error::InvalidOperation)?;
Ok(json!(res))
} else {
Err(Error::VosoUnavailable)
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref, msg: Ref) -> Result<()> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let message = msg.fetch_message(&channel).await?; let message = msg.fetch_message(&channel).await?;
+1 -1
View File
@@ -24,7 +24,7 @@ pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let message = msg.fetch_message(&channel).await?; let message = msg.fetch_message(&channel).await?;
+1 -1
View File
@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref, msg: Ref) -> Result<JsonValue> {
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let message = msg.fetch_message(&channel).await?; let message = msg.fetch_message(&channel).await?;
+14 -2
View File
@@ -11,6 +11,12 @@ use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use validator::Validate; use validator::Validate;
#[derive(Serialize, Deserialize, FromFormValue)]
pub enum Sort {
Latest,
Oldest,
}
#[derive(Validate, Serialize, Deserialize, FromForm)] #[derive(Validate, Serialize, Deserialize, FromForm)]
pub struct Options { pub struct Options {
#[validate(range(min = 1, max = 100))] #[validate(range(min = 1, max = 100))]
@@ -19,6 +25,7 @@ pub struct Options {
before: Option<String>, before: Option<String>,
#[validate(length(min = 26, max = 26))] #[validate(length(min = 26, max = 26))]
after: Option<String>, after: Option<String>,
sort: Option<Sort>,
} }
#[get("/<target>/messages?<options..>")] #[get("/<target>/messages?<options..>")]
@@ -34,7 +41,7 @@ pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<Json
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_view() { if !perm.get_view() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
let mut query = doc! { "channel": target.id() }; let mut query = doc! { "channel": target.id() };
@@ -47,13 +54,18 @@ pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<Json
query.insert("_id", doc! { "$gt": 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") let mut cursor = get_collection("messages")
.find( .find(
query, query,
FindOptions::builder() FindOptions::builder()
.limit(options.limit.unwrap_or(50)) .limit(options.limit.unwrap_or(50))
.sort(doc! { .sort(doc! {
"_id": -1 "_id": sort
}) })
.build(), .build(),
) )
@@ -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
}))
}
+24 -7
View File
@@ -1,7 +1,7 @@
use crate::database::*; use crate::database::*;
use crate::util::result::{Error, Result}; use crate::util::result::{Error, Result};
use mongodb::bson::doc; use mongodb::{bson::doc, options::FindOneOptions};
use rocket_contrib::json::{Json, JsonValue}; use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ulid::Ulid; use ulid::Ulid;
@@ -9,11 +9,13 @@ use validator::Validate;
#[derive(Validate, Serialize, Deserialize)] #[derive(Validate, Serialize, Deserialize)]
pub struct Data { pub struct Data {
#[validate(length(min = 1, max = 2000))] #[validate(length(min = 0, max = 2000))]
content: String, content: String,
// Maximum length of 36 allows both ULIDs and UUIDs. // Maximum length of 36 allows both ULIDs and UUIDs.
#[validate(length(min = 1, max = 36))] #[validate(length(min = 1, max = 36))]
nonce: String, nonce: String,
#[validate(length(min = 1, max = 128))]
attachment: Option<String>,
} }
#[post("/<target>/messages", data = "<message>")] #[post("/<target>/messages", data = "<message>")]
@@ -22,14 +24,18 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
.validate() .validate()
.map_err(|error| Error::FailedValidation { error })?; .map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_channel().await?; 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) let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target) .with_channel(&target)
.for_channel() .for_channel()
.await?; .await?;
if !perm.get_send_message() { if !perm.get_send_message() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
if get_collection("messages") if get_collection("messages")
@@ -37,7 +43,9 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
doc! { doc! {
"nonce": &message.nonce "nonce": &message.nonce
}, },
None, FindOneOptions::builder()
.projection(doc! { "_id": 1 })
.build(),
) )
.await .await
.map_err(|_| Error::DatabaseError { .map_err(|_| Error::DatabaseError {
@@ -49,14 +57,23 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
Err(Error::DuplicateNonce)? 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 { let msg = Message {
id: Ulid::new().to_string(), id,
channel: target.id().to_string(), channel: target.id().to_string(),
author: user.id, author: user.id,
content: message.content.clone(), content: Content::Text(message.content.clone()),
attachment,
nonce: Some(message.nonce.clone()), nonce: Some(message.nonce.clone()),
edited: None, edited: None,
embeds: None,
}; };
msg.clone().publish(&target).await?; msg.clone().publish(&target).await?;
+9 -1
View File
@@ -1,27 +1,35 @@
use rocket::Route; use rocket::Route;
mod delete_channel; mod delete_channel;
mod edit_channel;
mod fetch_channel; mod fetch_channel;
mod fetch_members;
mod group_add_member; mod group_add_member;
mod group_create; mod group_create;
mod group_remove_member; mod group_remove_member;
mod join_call;
mod message_delete; mod message_delete;
mod message_edit; mod message_edit;
mod message_fetch; mod message_fetch;
mod message_query; mod message_query;
mod message_query_stale;
mod message_send; mod message_send;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ routes![
fetch_channel::req, fetch_channel::req,
fetch_members::req,
delete_channel::req, delete_channel::req,
edit_channel::req,
message_send::req, message_send::req,
message_query::req, message_query::req,
message_query_stale::req,
message_fetch::req, message_fetch::req,
message_edit::req, message_edit::req,
message_delete::req, message_delete::req,
group_create::req, group_create::req,
group_add_member::req, group_add_member::req,
group_remove_member::req group_remove_member::req,
join_call::req,
] ]
} }
+2
View File
@@ -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())
} }
+5 -25
View File
@@ -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,31 +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 })?;
if data.username == "revolt" { if User::is_username_taken(&data.username).await? {
Err(Error::UsernameTaken)? return Err(Error::UsernameTaken);
} }
let col = get_collection("users"); get_collection("users")
if col .insert_one(
.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(
doc! { doc! {
"_id": session.user_id, "_id": session.user_id,
"username": &data.username "username": &data.username
+8
View File
@@ -0,0 +1,8 @@
use rocket::Route;
mod subscribe;
mod unsubscribe;
pub fn routes() -> Vec<Route> {
routes![subscribe::req, unsubscribe::req]
}
+37
View File
@@ -0,0 +1,37 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, to_document};
use rauth::auth::Session;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Subscription {
endpoint: String,
p256dh: String,
auth: String,
}
#[post("/subscribe", data = "<data>")]
pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> {
let data = data.into_inner();
get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$set": {
"sessions.$.subscription": to_document(&data)
.map_err(|_| Error::DatabaseError { operation: "to_document", with: "subscription" })?
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "account" })?;
Ok(())
}
+29
View File
@@ -0,0 +1,29 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rauth::auth::Session;
#[post("/unsubscribe")]
pub async fn req(session: Session) -> Result<()> {
get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$unset": {
"sessions.$.subscription": 1
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "subscription",
})?;
Ok(())
}
+19 -3
View File
@@ -1,5 +1,6 @@
use crate::util::variables::{ use crate::util::variables::{
DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, 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.3-alpha.2", "revolt": crate::version::VERSION,
"features": { "features": {
"registration": !*DISABLE_REGISTRATION, "registration": !*DISABLE_REGISTRATION,
"captcha": { "captcha": {
@@ -16,8 +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 "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
}) })
} }
+30 -18
View File
@@ -31,6 +31,8 @@ pub async fn req(user: User, username: String) -> Result<JsonValue> {
with: "user", with: "user",
})?; })?;
let target_user = Ref::from(target_id.to_string())?.fetch_user().await?;
match get_relationship(&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),
@@ -65,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" }))
} }
@@ -121,21 +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();
Ok(json!({ "status": "Outgoing" })) Ok(json!({ "status": "Outgoing" }))
} }
+36 -24
View File
@@ -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,21 +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 { 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" }))
} }
@@ -128,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" }))
} }
+66
View File
@@ -0,0 +1,66 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rauth::auth::{Auth, Session};
use regex::Regex;
use rocket::State;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
// ! FIXME: should be global somewhere; maybe use config(?)
lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap();
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
username: Option<String>,
#[validate(length(min = 8, max = 72))]
password: String,
}
#[patch("/<_ignore_id>/username", data = "<data>")]
pub async fn req(
auth: State<'_, Auth>,
session: Session,
user: User,
data: Json<Data>,
_ignore_id: String,
) -> Result<()> {
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
auth.verify_password(&session, data.password.clone())
.await
.map_err(|_| Error::InvalidCredentials)?;
let mut set = doc! {};
if let Some(username) = &data.username {
if User::is_username_taken(&username).await? {
return Err(Error::UsernameTaken);
}
set.insert("username", username.clone());
}
get_collection("users")
.update_one(doc! { "_id": &user.id }, doc! { "$set": set }, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!(data.0),
clear: None,
}
.publish(user.id.clone());
Ok(())
}
+179
View File
@@ -0,0 +1,179 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use crate::{database::*, notifications::events::RemoveUserField};
use mongodb::bson::{doc, to_document};
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize, Debug)]
pub struct UserProfileData {
#[validate(length(min = 0, max = 2000))]
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))]
background: Option<String>,
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate]
status: Option<UserStatus>,
#[validate]
profile: Option<UserProfileData>,
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
remove: Option<RemoveUserField>,
}
#[patch("/<_ignore_id>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<()> {
let mut data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.status.is_none()
&& data.profile.is_none()
&& data.avatar.is_none()
&& data.remove.is_none()
{
return Ok(());
}
let mut unset = doc! {};
let mut set = doc! {};
let mut remove_background = false;
let mut remove_avatar = false;
if let Some(remove) = &data.remove {
match remove {
RemoveUserField::ProfileContent => {
unset.insert("profile.content", 1);
}
RemoveUserField::ProfileBackground => {
unset.insert("profile.background", 1);
remove_background = true;
}
RemoveUserField::StatusText => {
unset.insert("status.text", 1);
}
RemoveUserField::Avatar => {
unset.insert("avatar", 1);
remove_avatar = true;
}
}
}
if let Some(status) = &data.status {
set.insert(
"status",
to_document(&status).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "status",
})?,
);
}
if let Some(profile) = data.profile {
if let Some(content) = profile.content {
set.insert("profile.content", content);
}
if let Some(attachment_id) = profile.background {
let attachment =
File::find_and_use(&attachment_id, "backgrounds", "user", &user.id).await?;
set.insert(
"profile.background",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_background = true;
}
}
let avatar = std::mem::replace(&mut data.avatar, None);
let attachment = if let Some(attachment_id) = avatar {
let attachment = File::find_and_use(&attachment_id, "avatars", "user", &user.id).await?;
set.insert(
"avatar",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_avatar = true;
Some(attachment)
} else {
None
};
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("users")
.update_one(doc! { "_id": &user.id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
}
if let Some(status) = &data.status {
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!({ "status": status }),
clear: None,
}
.publish(user.id.clone());
}
if let Some(avatar) = attachment {
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!({ "avatar": avatar }),
clear: None,
}
.publish(user.id.clone());
}
if let Some(clear) = data.remove {
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!({}),
clear: Some(clear),
}
.publish(user.id.clone());
}
if remove_avatar {
if let Some(old_avatar) = user.avatar {
old_avatar.delete().await?;
}
}
if remove_background {
if let Some(profile) = user.profile {
if let Some(old_background) = profile.background {
old_background.delete().await?;
}
}
}
Ok(())
}
+24
View File
@@ -0,0 +1,24 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket_contrib::json::JsonValue;
#[get("/<target>/profile")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let target = target.fetch_user().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_user(&target)
.for_user_given()
.await?;
if !perm.get_view_profile() {
Err(Error::MissingPermission)?
}
if target.profile.is_some() {
Ok(json!(target.profile))
} else {
Ok(json!({}))
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.await?; .await?;
if !perm.get_access() { if !perm.get_access() {
Err(Error::LabelMe)? Err(Error::MissingPermission)?
} }
Ok(json!(target.from(&user).with(perm))) Ok(json!(target.from(&user).with(perm)))
+34
View File
@@ -0,0 +1,34 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, Document};
use mongodb::options::FindOptions;
use rocket_contrib::json::JsonValue;
#[get("/<target>/mutual")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let users = get_collection("users")
.find(
doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user.id, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &target.id, "status": "Friend" } } }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
Ok(json!({ "users": users }))
}
-49
View File
@@ -1,49 +0,0 @@
use md5;
use mongodb::bson::doc;
use mongodb::options::FindOneOptions;
use rocket::response::Redirect;
use urlencoding;
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::PUBLIC_URL;
#[get("/<target>/avatar")]
pub async fn req(target: Ref) -> Result<Redirect> {
let doc = get_collection("accounts")
.find_one(
doc! {
"_id": &target.id
},
FindOneOptions::builder()
.projection(doc! { "email": 1 })
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.ok_or_else(|| Error::UnknownUser)?;
let email = doc
.get_str("email")
.map_err(|_| Error::DatabaseError {
operation: "get_str(email)",
with: "user",
})?
.to_lowercase();
let url = format!(
"https://www.gravatar.com/avatar/{:x}?s=128&d={}",
md5::compute(email),
urlencoding::encode(&format!(
"{}/users/{}/default_avatar",
*PUBLIC_URL, &target.id
))
);
dbg!(&url);
Ok(Redirect::to(url))
}
+8 -2
View File
@@ -2,11 +2,14 @@ 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 get_default_avatar;
mod open_dm; mod open_dm;
mod remove_friend; mod remove_friend;
@@ -16,12 +19,15 @@ pub fn routes() -> Vec<Route> {
routes![ routes![
// User Information // User Information
fetch_user::req, fetch_user::req,
edit_user::req,
change_username::req,
get_default_avatar::req, get_default_avatar::req,
get_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,
+18 -10
View File
@@ -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::Friend RelationshipStatus::Friend
| RelationshipStatus::Outgoing | RelationshipStatus::Outgoing
@@ -43,21 +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 { ClientboundNotification::UserRelationship {
id: target.id.clone(), id: target_id.clone(),
user: user.id.clone(), user,
status: RelationshipStatus::None status: RelationshipStatus::None,
} }
.publish(target.id.clone()) .publish(target_id);
)
.ok();
Ok(json!({ "status": "None" })) Ok(json!({ "status": "None" }))
} }
+24 -18
View File
@@ -9,10 +9,10 @@ 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! {
@@ -32,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" }))
} }
@@ -73,21 +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 { ClientboundNotification::UserRelationship {
id: target.id.clone(), id: target_id.clone(),
user: user.id.clone(), user: user,
status: RelationshipStatus::None status: RelationshipStatus::None,
} }
.publish(target.id.clone()) .publish(target_id);
)
.ok();
Ok(json!({ "status": "None" })) Ok(json!({ "status": "None" }))
} }
@@ -98,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),
} }
} }
+18
View File
@@ -36,8 +36,12 @@ pub enum Error {
// ? Channel related errors. // ? Channel related errors.
#[snafu(display("This channel does not exist!"))] #[snafu(display("This channel does not exist!"))]
UnknownChannel, UnknownChannel,
#[snafu(display("Attachment does not exist!"))]
UnknownAttachment,
#[snafu(display("Cannot edit someone else's message."))] #[snafu(display("Cannot edit someone else's message."))]
CannotEditMessage, CannotEditMessage,
#[snafu(display("Cannot send empty message."))]
EmptyMessage,
#[snafu(display("Cannot remove yourself from a group, use delete channel instead."))] #[snafu(display("Cannot remove yourself from a group, use delete channel instead."))]
CannotRemoveYourself, CannotRemoveYourself,
#[snafu(display("Group size is too large."))] #[snafu(display("Group size is too large."))]
@@ -48,6 +52,8 @@ pub enum Error {
NotInGroup, 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."))]
@@ -57,10 +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."))] #[snafu(display("Operation cannot be performed on this object."))]
InvalidOperation, InvalidOperation,
#[snafu(display("Email or password is incorrect."))]
InvalidCredentials,
#[snafu(display("Already created an object with this nonce."))] #[snafu(display("Already created an object with this nonce."))]
DuplicateNonce, 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,
} }
@@ -84,7 +96,9 @@ impl<'r> Responder<'r, 'static> for Error {
Error::NotFriends => Status::Forbidden, Error::NotFriends => Status::Forbidden,
Error::UnknownChannel => Status::NotFound, Error::UnknownChannel => Status::NotFound,
Error::UnknownAttachment => Status::BadRequest,
Error::CannotEditMessage => Status::Forbidden, Error::CannotEditMessage => Status::Forbidden,
Error::EmptyMessage => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest, Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden, Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => Status::Conflict, Error::AlreadyInGroup => Status::Conflict,
@@ -93,8 +107,12 @@ impl<'r> Responder<'r, 'static> for Error {
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::InvalidOperation => Status::BadRequest,
Error::TooManyIds => Status::BadRequest,
Error::InvalidCredentials => Status::Forbidden,
Error::DuplicateNonce => Status::Conflict, Error::DuplicateNonce => Status::Conflict,
Error::VosoUnavailable => Status::BadRequest,
Error::NoEffect => Status::Ok, Error::NoEffect => Status::Ok,
}; };
+30 -3
View File
@@ -7,18 +7,34 @@ 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 = pub static ref APP_URL: String =
env::var("REVOLT_APP_URL").unwrap_or_else(|_| "https://app.revolt.chat".to_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");
@@ -32,6 +48,9 @@ lazy_static! {
); );
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_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 =
@@ -48,6 +67,14 @@ lazy_static! {
} }
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") {
+1
View File
@@ -0,0 +1 @@
pub const VERSION: &str = "0.4.1-alpha.11";