Compare commits

...

24 Commits

Author SHA1 Message Date
Paul
9e2250783c Allow people with mutual connections to see profile / status; fix username check. 2021-04-07 19:21:25 +01:00
Paul
2050a0609f Fix update_one failing on sys message. 2021-04-03 21:37:24 +01:00
Paul
a547177325 Add status presence indicator. 2021-04-03 20:50:14 +01:00
Paul
c3fa7bf74e Change route paths for user edits. 2021-04-03 15:02:29 +01:00
Paul
7f5d6f2312 Format code. 2021-04-03 14:44:01 +01:00
Paul
9492e145f9 User status / profile change route, username change route. 2021-04-03 14:41:40 +01:00
Paul
78c42fd412 Fetch profile route, push badges and status. 2021-04-02 21:55:02 +01:00
Paul
3797878d20 Fixes bug with distributed nodes. 2021-04-02 13:25:51 +01:00
Paul
d2bf477fd8 Expose voso host. 2021-04-01 15:11:19 +01:00
Martin Loffler
c9bff789de Fixed room creation failure in join_call 2021-04-01 15:59:06 +02:00
Paul
56bd763820 Prevent setting null in edit. Preflight for variables. 2021-04-01 14:01:07 +01:00
Paul
32cd9d8a13 Strict typing for system messages; add a way to rename group. 2021-03-31 20:54:47 +01:00
Paul
60731e1c70 Integrate voso, join_call route for channels. 2021-03-31 11:48:52 +01:00
Paul
37df18dbf7 Fixes mutual connections not being seen and hence requests failing. 2021-03-30 22:09:36 +01:00
Paul
0a9d5de369 Don't unwrap on subscription errors. 2021-03-29 10:14:25 +01:00
Paul
d2864ac025 Make sure to check that they are friends. 2021-02-19 15:49:55 +00:00
Paul
4eb76fd154 Fix mutex issues. 2021-02-19 15:24:19 +00:00
Paul
3c7852271a Add temporary typing indicator impl. 2021-02-19 14:50:23 +00:00
Paul
dadad271b4 Mark attachments as deleted when deleting messages. 2021-02-19 14:12:21 +00:00
Paul
db6047f2d3 Allow empty messages with attachment. 2021-02-19 14:09:19 +00:00
Paul
5baf85a8e9 JSON system messages. 2021-02-19 14:03:41 +00:00
Paul
10cac358a9 Label permission errors, and too many ids for /stale. 2021-02-19 13:26:04 +00:00
Paul
64d2707366 Find mutual for users; allow dots in usernames. 2021-02-19 13:11:33 +00:00
Paul
78cfbf9d21 Clean up subscription code; handle error properly. 2021-02-19 12:48:52 +00:00
36 changed files with 709 additions and 185 deletions

8
Cargo.lock generated
View File

@@ -1,5 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.14.1"
@@ -1065,9 +1067,9 @@ checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
[[package]]
name = "hive_pubsub"
version = "0.4.3"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83760410241f6db418bb15e54b506a0887e7240286e29e5b0d2d88f5d1659b24"
checksum = "2ac4635b1e9faf304ccb33c2f0edbc615030e40d249183281fc788ef20ef5c76"
dependencies = [
"futures",
"many-to-many",
@@ -2473,7 +2475,7 @@ dependencies = [
[[package]]
name = "revolt"
version = "0.3.3-alpha.6"
version = "0.4.0-alpha.6"
dependencies = [
"async-std",
"async-tungstenite",

View File

@@ -1,49 +1,44 @@
[package]
name = "revolt"
version = "0.3.3-alpha.6"
version = "0.4.0-alpha.6"
authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
regex = "1"
md5 = "0.7.0"
log = "0.4.11"
ulid = "0.4.1"
rand = "0.7.3"
time = "0.2.16"
base64 = "0.13.0"
dotenv = "0.15.0"
futures = "0.3.8"
chrono = "0.4.15"
num_enum = "0.5.1"
impl_ops = "0.1.1"
web-push = "0.7.2"
many-to-many = "0.1.2"
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"
bitfield = "0.13.2"
env_logger = "0.7.1"
serde_json = "1.0.57"
lazy_static = "1.4.0"
urlencoding = "1.1.1"
many-to-many = "0.1.2"
lettre = "0.10.0-alpha.1"
snafu = { version = "0.6.9" }
reqwest = { version = "0.10.8", features = ["json"] }
serde = { version = "1.0.115", features = ["derive"] }
validator = { version = "0.11", features = ["derive"] }
snafu = { version = "0.6.9" }
serde_json = "1.0.57"
bitfield = "0.13.2"
reqwest = { version = "0.10.8", features = ["json"] }
urlencoding = "1.1.1"
lazy_static = "1.4.0"
num_enum = "0.5.1"
chrono = "0.4.15"
time = "0.2.16"
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"
ctrlc = { version = "3.0", features = ["termination"] }
hive_pubsub = { version = "0.4.4", features = ["mongo"] }
async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" }
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "c52758a5087cd035b0ed9c6eacc942ba5468d2ce" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false }

View File

@@ -1,5 +1,6 @@
# Build Stage
FROM ekidd/rust-musl-builder:nightly-2021-01-01 AS builder
USER 0:0
WORKDIR /home/rust/src
RUN USER=root cargo new --bin revolt

View File

@@ -13,7 +13,37 @@ use mongodb::{
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use web_push::{ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder};
use web_push::{
ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum MessageEmbed {
Dummy,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum SystemMessage {
#[serde(rename = "text")]
Text { content: String },
#[serde(rename = "user_added")]
UserAdded { id: String, by: String },
#[serde(rename = "user_remove")]
UserRemove { id: String, by: String },
#[serde(rename = "user_left")]
UserLeft { id: String },
#[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String },
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Content {
Text(String),
SystemMessage(SystemMessage),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
@@ -24,15 +54,17 @@ pub struct Message {
pub channel: String,
pub author: String,
pub content: String,
pub content: Content,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachment: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<DateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<MessageEmbed>,
}
impl Message {
pub fn create(author: String, channel: String, content: String) -> Message {
pub fn create(author: String, channel: String, content: Content) -> Message {
Message {
id: Ulid::new().to_string(),
nonce: None,
@@ -42,6 +74,7 @@ impl Message {
content,
attachment: None,
edited: None,
embeds: None,
}
}
@@ -54,22 +87,28 @@ impl 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
let channels = get_collection("channels");
match &channel {
Channel::DirectMessage { id, .. } => {
set.insert("active", true);
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": {
"active": true,
"last_message": {
"_id": self.id.clone(),
"author": self.author.clone(),
"short": self.content.chars().take(24).collect::<String>()
}
}
"$set": set
},
None,
)
@@ -80,25 +119,21 @@ impl Message {
})?;
}
Channel::Group { id, .. } => {
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": {
"last_message": {
"_id": self.id.clone(),
"author": self.author.clone(),
"short": self.content.chars().take(24).collect::<String>()
}
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
if let Content::Text(_) = &self.content {
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": set
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
}
_ => {}
}
@@ -107,7 +142,7 @@ impl Message {
ClientboundNotification::Message(self)
.publish(channel.id().to_string())
.await
.ok();
.unwrap();
/*
Web Push Test Code
@@ -128,7 +163,7 @@ impl Message {
}
// Fetch their corresponding sessions.
let mut cursor = get_collection("accounts")
if let Ok(mut cursor) = get_collection("accounts")
.find(
doc! {
"_id": {
@@ -143,41 +178,43 @@ impl Message {
.build(),
)
.await
.unwrap(); // !FIXME
{
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();
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));
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();
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();
let response = client.send(m).await.unwrap();
dbg!(response);
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();
}
}
}
@@ -198,6 +235,26 @@ impl Message {
}
pub async fn delete(&self) -> Result<()> {
if let Some(attachment) = &self.attachment {
get_collection("attachments")
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
}
get_collection("messages")
.delete_one(
doc! {

View File

@@ -1,6 +1,12 @@
use mongodb::bson::doc;
use mongodb::options::{Collation, FindOneOptions};
use serde::{Deserialize, Serialize};
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};
use validator::Validate;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus {
@@ -20,6 +26,37 @@ pub struct Relationship {
pub status: RelationshipStatus,
}
/*
pub enum Badge {
Developer = 1,
Translator = 2,
}
*/
#[derive(Serialize, Deserialize, Debug)]
pub enum Presence {
Online,
Idle,
Busy,
Invisible
}
#[derive(Validate, Serialize, Deserialize, Debug)]
pub struct UserStatus {
#[validate(length(min = 1, max = 128))]
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
presence: Option<Presence>
}
#[derive(Validate, Serialize, Deserialize, Debug)]
pub struct UserProfile {
#[validate(length(min = 1, max = 2000))]
#[serde(skip_serializing_if = "Option::is_none")]
content: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
#[serde(rename = "_id")]
@@ -28,6 +65,13 @@ pub struct User {
#[serde(skip_serializing_if = "Option::is_none")]
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.
#[serde(skip_serializing_if = "Option::is_none")]
pub relationship: Option<RelationshipStatus>,
@@ -60,6 +104,35 @@ impl User {
self.online = Some(is_online(&self.id));
}
self.profile = None;
self
}
/// Utility function for checking claimed usernames.
pub async fn is_username_taken(username: &str) -> Result<bool> {
if username.to_lowercase() == "revolt" && username.to_lowercase() == "admin" {
return Ok(true);
}
if get_collection("users")
.find_one(
doc! {
"username": username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.is_some()
{
Ok(true)
} else {
Ok(false)
}
}
}

View File

@@ -12,6 +12,8 @@ pub enum ChannelPermission {
View = 1,
SendMessage = 2,
ManageMessages = 4,
ManageChannel = 8,
VoiceCall = 16,
}
bitfield! {
@@ -20,6 +22,8 @@ bitfield! {
pub get_view, _: 31;
pub get_send_message, _: 30;
pub get_manage_messages, _: 29;
pub get_manage_channel, _: 28;
pub get_voice_call, _: 27;
}
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 });
@@ -36,9 +40,7 @@ impl<'a> PermissionCalculator<'a> {
match channel {
Channel::SavedMessages { user: owner, .. } => {
if &self.perspective.id == owner {
Ok(ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::ManageMessages)
Ok(u32::MAX)
} else {
Ok(0)
}
@@ -54,7 +56,9 @@ impl<'a> PermissionCalculator<'a> {
let perms = self.for_user(recipient).await?;
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);
@@ -69,7 +73,10 @@ impl<'a> PermissionCalculator<'a> {
.find(|x| *x == &self.perspective.id)
.is_some()
{
Ok(ChannelPermission::View + ChannelPermission::SendMessage)
Ok(ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::ManageChannel
+ ChannelPermission::VoiceCall)
} else {
Ok(0)
}

View File

@@ -11,7 +11,7 @@ pub struct PermissionCalculator<'a> {
user: Option<&'a User>,
channel: Option<&'a Channel>,
has_mutual_conncetion: bool,
has_mutual_connection: bool,
}
impl<'a> PermissionCalculator<'a> {
@@ -22,7 +22,7 @@ impl<'a> PermissionCalculator<'a> {
user: None,
channel: None,
has_mutual_conncetion: false,
has_mutual_connection: false,
}
}
@@ -42,7 +42,7 @@ impl<'a> PermissionCalculator<'a> {
pub fn with_mutual_connection(self) -> PermissionCalculator<'a> {
PermissionCalculator {
has_mutual_conncetion: true,
has_mutual_connection: true,
..self
}
}

View File

@@ -64,18 +64,16 @@ impl<'a> PermissionCalculator<'a> {
_ => {}
}
if self.has_mutual_conncetion
if self.has_mutual_connection
|| get_collection("channels")
.find_one(
doc! {
"$or": [
{ "type": "Group" },
{ "type": "DirectMessage" },
],
"$and": [
{ "recipients": &self.perspective.id },
{ "recipients": target }
]
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ &self.perspective.id, target ]
}
},
None,
)
@@ -86,7 +84,8 @@ impl<'a> PermissionCalculator<'a> {
})?
.is_some()
{
return Ok(UserPermission::Access as u32);
// ! FIXME: add privacy settings
return Ok(UserPermission::Access + UserPermission::ViewProfile);
}
Ok(permissions)

View File

@@ -26,6 +26,8 @@ pub enum WebSocketError {
#[serde(tag = "type")]
pub enum ServerboundNotification {
Authenticate(Session),
BeginTyping { channel: String },
EndTyping { channel: String },
}
#[derive(Serialize, Deserialize, Debug)]
@@ -63,7 +65,19 @@ pub enum ClientboundNotification {
ChannelDelete {
id: String,
},
ChannelStartTyping {
id: String,
user: String,
},
ChannelStopTyping {
id: String,
user: String,
},
UserUpdate {
id: String,
data: JsonValue,
},
UserRelationship {
id: String,
user: String,

View File

@@ -77,7 +77,7 @@ pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
}
},
FindOptions::builder()
.projection(doc! { "_id": 1, "username": 1 })
.projection(doc! { "_id": 1, "username": 1, "badges": 1, "status": 1 })
.build(),
)
.await

View File

@@ -71,8 +71,6 @@ async fn accept(stream: TcpStream) {
let fwd = rx.map(Ok).forward(write);
let incoming = read.try_for_each(async move |msg| {
let mutex = mutex_generator();
//dbg!(&mutex.lock().unwrap());
if let Message::Text(text) = msg {
if let Ok(notification) = serde_json::from_str::<ServerboundNotification>(&text) {
match notification {
@@ -161,6 +159,54 @@ 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)
.await
.ok();
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
ServerboundNotification::EndTyping { channel } => {
if mutex.lock().unwrap().is_some() {
let user = {
let mutex = mutex.lock().unwrap();
let session = mutex.as_ref().unwrap();
session.user_id.clone()
};
ClientboundNotification::ChannelStopTyping {
id: channel.clone(),
user,
}
.publish(channel)
.await
.ok();
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
}
}
}

View File

@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
match &target {
@@ -102,7 +102,7 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
format!("<@{}> left the group.", user.id),
Content::SystemMessage(SystemMessage::UserLeft { id: user.id }),
)
.publish(&target)
.await

View File

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

View File

@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
Ok(json!(target))

View File

@@ -16,7 +16,7 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
if let Channel::Group { id, recipients, .. } = &channel {
@@ -59,7 +59,10 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
format!("<@{}> added <@{}> to the group.", user.id, member.id),
Content::SystemMessage(SystemMessage::UserAdded {
id: member.id,
by: user.id,
}),
)
.publish(&channel)
.await

View File

@@ -20,7 +20,7 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
{
if &user.id != owner {
// 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() {
@@ -56,7 +56,10 @@ pub async fn req(user: User, target: Ref, member: Ref) -> Result<()> {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
format!("<@{}> removed <@{}> from the group.", user.id, member.id),
Content::SystemMessage(SystemMessage::UserRemove {
id: member.id,
by: user.id,
}),
)
.publish(&channel)
.await

View File

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

View File

@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref, msg: Ref) -> Result<()> {
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let message = msg.fetch_message(&channel).await?;

View File

@@ -24,7 +24,7 @@ pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let message = msg.fetch_message(&channel).await?;

View File

@@ -12,7 +12,7 @@ pub async fn req(user: User, target: Ref, msg: Ref) -> Result<JsonValue> {
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let message = msg.fetch_message(&channel).await?;

View File

@@ -34,7 +34,7 @@ pub async fn req(user: User, target: Ref, options: Form<Options>) -> Result<Json
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let mut query = doc! { "channel": target.id() };

View File

@@ -14,7 +14,7 @@ pub struct Options {
#[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::LabelMe);
return Err(Error::TooManyIds);
}
let target = target.fetch_channel().await?;
@@ -24,7 +24,7 @@ pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<JsonVal
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
let mut cursor = get_collection("messages")

View File

@@ -12,7 +12,7 @@ use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 2000))]
#[validate(length(min = 0, max = 2000))]
content: String,
// Maximum length of 36 allows both ULIDs and UUIDs.
#[validate(length(min = 1, max = 36))]
@@ -27,14 +27,18 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
.validate()
.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)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_send_message() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
if get_collection("messages")
@@ -111,10 +115,11 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
channel: target.id().to_string(),
author: user.id,
content: message.content.clone(),
content: Content::Text(message.content.clone()),
attachment,
nonce: Some(message.nonce.clone()),
edited: None,
embeds: None,
};
msg.clone().publish(&target).await?;

View File

@@ -1,10 +1,12 @@
use rocket::Route;
mod delete_channel;
mod edit_channel;
mod fetch_channel;
mod group_add_member;
mod group_create;
mod group_remove_member;
mod join_call;
mod message_delete;
mod message_edit;
mod message_fetch;
@@ -16,6 +18,7 @@ pub fn routes() -> Vec<Route> {
routes![
fetch_channel::req,
delete_channel::req,
edit_channel::req,
message_send::req,
message_query::req,
message_query_stale::req,
@@ -24,6 +27,7 @@ pub fn routes() -> Vec<Route> {
message_delete::req,
group_create::req,
group_add_member::req,
group_remove_member::req
group_remove_member::req,
join_call::req,
]
}

View File

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

View File

@@ -16,7 +16,7 @@ pub struct Subscription {
#[post("/subscribe", data = "<data>")]
pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> {
let data = data.into_inner();
let col = get_collection("accounts")
get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
@@ -24,13 +24,14 @@ pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> {
},
doc! {
"$set": {
"sessions.$.subscription": to_document(&data).unwrap()
"sessions.$.subscription": to_document(&data)
.map_err(|_| Error::DatabaseError { operation: "to_document", with: "subscription" })?
}
},
None,
)
.await
.unwrap();
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "account" })?;
Ok(())
}

View File

@@ -6,7 +6,7 @@ use rauth::auth::Session;
#[post("/unsubscribe")]
pub async fn req(session: Session) -> Result<()> {
let col = get_collection("accounts")
get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
@@ -20,7 +20,10 @@ pub async fn req(session: Session) -> Result<()> {
None,
)
.await
.unwrap();
.map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "subscription",
})?;
Ok(())
}

View File

@@ -1,6 +1,6 @@
use crate::util::variables::{
AUTUMN_URL, DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, USE_AUTUMN,
USE_EMAIL, USE_HCAPTCHA, VAPID_PUBLIC_KEY,
APP_URL, AUTUMN_URL, DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY,
USE_AUTUMN, USE_EMAIL, USE_HCAPTCHA, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL, VOSO_WS_HOST,
};
use mongodb::bson::doc;
@@ -9,7 +9,7 @@ use rocket_contrib::json::JsonValue;
#[get("/")]
pub async fn root() -> JsonValue {
json!({
"revolt": "0.3.3-alpha.6",
"revolt": "0.4.0-alpha.6",
"features": {
"registration": !*DISABLE_REGISTRATION,
"captcha": {
@@ -21,9 +21,15 @@ pub async fn root() -> JsonValue {
"autumn": {
"enabled": *USE_AUTUMN,
"url": *AUTUMN_URL
},
"voso": {
"enabled": *USE_VOSO,
"url": *VOSO_URL,
"ws": *VOSO_WS_HOST
}
},
"ws": *EXTERNAL_WS_URL,
"app": *APP_URL,
"vapid": *VAPID_PUBLIC_KEY
})
}

View File

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

View File

@@ -0,0 +1,41 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, to_document};
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<UserStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
profile: Option<UserProfile>,
}
#[patch("/<_ignore_id>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<()> {
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
get_collection("users")
.update_one(
doc! { "_id": &user.id },
doc! { "$set": to_document(&data.0).map_err(|_| Error::DatabaseError { operation: "to_document", with: "data" })? },
None
)
.await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "user" })?;
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!(data.0),
}
.publish(user.id.clone())
.await
.ok();
Ok(())
}

View File

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

View File

@@ -13,7 +13,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.await?;
if !perm.get_access() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
Ok(json!(target.from(&user).with(perm)))

View File

@@ -8,15 +8,12 @@ use rocket_contrib::json::JsonValue;
#[get("/<target>/mutual")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
let channels = get_collection("channels")
let users = get_collection("users")
.find(
doc! {
"$or": [
{ "type": "Group" },
],
"$and": [
{ "recipients": &user.id },
{ "recipients": &target.id }
{ "relations._id": &user.id, "relations.status": "Friend" },
{ "relations._id": &target.id, "relations.status": "Friend" }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
@@ -24,7 +21,7 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "channels",
with: "users",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
@@ -33,5 +30,5 @@ pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
Ok(json!({ "channels": channels }))
Ok(json!({ "users": users }))
}

View File

@@ -2,7 +2,10 @@ use rocket::Route;
mod add_friend;
mod block_user;
mod change_username;
mod edit_user;
mod fetch_dms;
mod fetch_profile;
mod fetch_relationship;
mod fetch_relationships;
mod fetch_user;
@@ -17,8 +20,11 @@ pub fn routes() -> Vec<Route> {
routes![
// User Information
fetch_user::req,
edit_user::req,
change_username::req,
get_default_avatar::req,
get_avatar::req,
fetch_profile::req,
// Direct Messaging
fetch_dms::req,
open_dm::req,

View File

@@ -40,6 +40,8 @@ pub enum Error {
UnknownAttachment,
#[snafu(display("Cannot edit someone else's message."))]
CannotEditMessage,
#[snafu(display("Cannot send empty message."))]
EmptyMessage,
#[snafu(display("Cannot remove yourself from a group, use delete channel instead."))]
CannotRemoveYourself,
#[snafu(display("Group size is too large."))]
@@ -50,6 +52,8 @@ pub enum Error {
NotInGroup,
// ? General errors.
#[snafu(display("Trying to fetch too much data."))]
TooManyIds,
#[snafu(display("Failed to validate fields."))]
FailedValidation { error: ValidationErrors },
#[snafu(display("Encountered a database error."))]
@@ -59,10 +63,16 @@ pub enum Error {
},
#[snafu(display("Internal server error."))]
InternalError,
#[snafu(display("Missing permission."))]
MissingPermission,
#[snafu(display("Operation cannot be performed on this object."))]
InvalidOperation,
#[snafu(display("Email or password is incorrect."))]
InvalidCredentials,
#[snafu(display("Already created an object with this nonce."))]
DuplicateNonce,
#[snafu(display("Voso is not enabled on this instance."))]
VosoUnavailable,
#[snafu(display("This request had no effect."))]
NoEffect,
}
@@ -88,6 +98,7 @@ impl<'r> Responder<'r, 'static> for Error {
Error::UnknownChannel => Status::NotFound,
Error::UnknownAttachment => Status::BadRequest,
Error::CannotEditMessage => Status::Forbidden,
Error::EmptyMessage => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => Status::Conflict,
@@ -96,8 +107,12 @@ impl<'r> Responder<'r, 'static> for Error {
Error::FailedValidation { .. } => Status::UnprocessableEntity,
Error::DatabaseError { .. } => Status::InternalServerError,
Error::InternalError => Status::InternalServerError,
Error::MissingPermission => Status::Forbidden,
Error::InvalidOperation => Status::BadRequest,
Error::TooManyIds => Status::BadRequest,
Error::InvalidCredentials => Status::Forbidden,
Error::DuplicateNonce => Status::Conflict,
Error::VosoUnavailable => Status::BadRequest,
Error::NoEffect => Status::Ok,
};

View File

@@ -12,11 +12,19 @@ lazy_static! {
pub static ref PUBLIC_URL: String =
env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable.");
pub static ref APP_URL: String =
env::var("REVOLT_APP_URL").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 =
env::var("REVOLT_EXTERNAL_WS_URL").expect("Missing REVOLT_EXTERNAL_WS_URL environment variable.");
pub static ref AUTUMN_URL: String =
env::var("AUTUMN_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
pub static ref VOSO_URL: String =
env::var("VOSO_PUBLIC_URL").unwrap_or_else(|_| "https://example.com".to_string());
pub static ref VOSO_WS_HOST: String =
env::var("VOSO_WS_HOST").unwrap_or_else(|_| "wss://example.com".to_string());
pub static ref VOSO_MANAGE_TOKEN: String =
env::var("VOSO_MANAGE_TOKEN").unwrap_or_else(|_| "0".to_string());
pub static ref HCAPTCHA_KEY: String =
env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string());
pub static ref HCAPTCHA_SITEKEY: String =
@@ -39,6 +47,7 @@ lazy_static! {
pub static ref USE_HCAPTCHA: bool = env::var("REVOLT_HCAPTCHA_KEY").is_ok();
pub static ref USE_PROMETHEUS: bool = env::var("REVOLT_ENABLE_PROMETHEUS").map_or(false, |v| v == "1");
pub static ref USE_AUTUMN: bool = env::var("AUTUMN_PUBLIC_URL").is_ok();
pub static ref USE_VOSO: bool = env::var("VOSO_PUBLIC_URL").is_ok() && env::var("VOSO_MANAGE_TOKEN").is_ok();
// SMTP Settings
pub static ref SMTP_HOST: String =
@@ -52,11 +61,17 @@ lazy_static! {
// Application Logic Settings
pub static ref MAX_GROUP_SIZE: usize =
env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap();
pub static ref PUSH_LIMIT: usize =
env::var("REVOLT_PUSH_LIMIT").unwrap_or_else(|_| "50".to_string()).parse().unwrap();
}
pub fn preflight_checks() {
format!("{}", *APP_URL);
format!("{}", *MONGO_URI);
format!("{}", *PUBLIC_URL);
format!("{}", *EXTERNAL_WS_URL);
format!("{}", *VAPID_PRIVATE_KEY);
format!("{}", *VAPID_PUBLIC_KEY);
if *USE_EMAIL == false {
#[cfg(not(debug_assertions))]
if !env::var("REVOLT_UNSAFE_NO_EMAIL").map_or(false, |v| v == *"1") {