Compare commits

..

16 Commits

Author SHA1 Message Date
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
34 changed files with 608 additions and 69 deletions

View File

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

68
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"
@@ -243,6 +245,16 @@ version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "base64"
version = "0.11.0"
@@ -686,6 +698,15 @@ dependencies = [
"termcolor",
]
[[package]]
name = "erased-serde"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0465971a8cc1fa2455c8465aaa377131e1f1cf4983280f474a13e68793aa770c"
dependencies = [
"serde",
]
[[package]]
name = "err-derive"
version = "0.2.4"
@@ -2454,10 +2475,11 @@ dependencies = [
[[package]]
name = "revolt"
version = "0.3.3-alpha.3"
version = "0.3.3-alpha.8"
dependencies = [
"async-std",
"async-tungstenite",
"base64 0.13.0",
"bitfield",
"chrono",
"ctrlc",
@@ -2489,6 +2511,7 @@ dependencies = [
"ulid",
"urlencoding",
"validator",
"web-push",
]
[[package]]
@@ -2501,7 +2524,7 @@ dependencies = [
"libc",
"once_cell",
"spin",
"untrusted",
"untrusted 0.7.1",
"web-sys",
"winapi 0.3.9",
]
@@ -2673,6 +2696,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "safemem"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
[[package]]
name = "schannel"
version = "0.1.19"
@@ -2705,7 +2734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [
"ring",
"untrusted",
"untrusted 0.7.1",
]
[[package]]
@@ -3477,6 +3506,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "untrusted"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cd1f4b4e96b46aeb8d4855db4a7a9bd96eeeb5c6a1ab54593328761642ce2f"
[[package]]
name = "untrusted"
version = "0.7.1"
@@ -3678,6 +3713,31 @@ version = "0.2.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "web-sys"
version = "0.3.47"
@@ -3695,7 +3755,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [
"ring",
"untrusted",
"untrusted 0.7.1",
]
[[package]]

View File

@@ -1,14 +1,16 @@
[package]
name = "revolt"
version = "0.3.3-alpha.3"
version = "0.3.3-alpha.8"
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]
base64 = "0.13.0"
futures = "0.3.8"
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"] }

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

@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
enum Metadata {
File,
Image { width: isize, height: isize },
Video { width: isize, height: isize },
Audio,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct File {
#[serde(rename = "_id")]
pub id: String,
filename: String,
metadata: Metadata,
content_type: String,
message_id: Option<String>,
}

View File

@@ -46,9 +46,9 @@ pub enum Channel {
impl Channel {
pub fn id(&self) -> &str {
match self {
Channel::SavedMessages { id, .. } => id,
Channel::DirectMessage { id, .. } => id,
Channel::Group { id, .. } => id,
Channel::SavedMessages { id, .. }
| Channel::DirectMessage { id, .. }
| Channel::Group { id, .. } => id,
}
}

View File

@@ -1,12 +1,21 @@
use crate::util::variables::VAPID_PRIVATE_KEY;
use crate::{
database::*,
notifications::events::ClientboundNotification,
notifications::{events::ClientboundNotification, websocket::is_online},
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 serde::{Deserialize, Serialize};
use ulid::Ulid;
use web_push::{
ContentEncoding, SubscriptionInfo, VapidSignatureBuilder, WebPushClient, WebPushMessageBuilder,
};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
@@ -19,6 +28,8 @@ pub struct Message {
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachment: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<DateTime>,
}
@@ -31,6 +42,7 @@ impl Message {
author,
content,
attachment: None,
edited: None,
}
}
@@ -46,7 +58,7 @@ impl Message {
// ! FIXME: temp code
let channels = get_collection("channels");
match channel {
match &channel {
Channel::DirectMessage { id, .. } => {
channels
.update_one(
@@ -93,11 +105,84 @@ impl Message {
_ => {}
}
let enc = serde_json::to_string(&self).unwrap();
ClientboundNotification::Message(self)
.publish(channel.id().to_string())
.await
.ok();
/*
Web Push Test Code
! FIXME: temp code
*/
// Find all offline users.
let mut target_ids = vec![];
match &channel {
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
for recipient in recipients {
if !is_online(recipient) {
target_ids.push(recipient.clone());
}
}
}
_ => {}
}
// Fetch their corresponding sessions.
let mut cursor = get_collection("accounts")
.find(
doc! {
"_id": {
"$in": target_ids
},
"sessions.subscription": {
"$exists": true
}
},
FindOptions::builder()
.projection(doc! { "sessions": 1 })
.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();
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();
let response = client.send(m).await.ok();
}
}
Ok(())
}
@@ -115,6 +200,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,8 +1,10 @@
mod autumn;
mod channel;
mod guild;
mod message;
mod user;
pub use autumn::*;
pub use channel::*;
pub use guild::*;
pub use message::*;

View File

@@ -1,6 +1,11 @@
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use mongodb::options::{Collation, FindOneOptions};
use crate::{database::permissions::user::UserPermissions, notifications::websocket::is_online};
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::notifications::websocket::is_online;
use crate::database::permissions::user::UserPermissions;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus {
@@ -62,4 +67,32 @@ impl User {
self
}
/// Utility function for checking claimed usernames.
pub async fn is_username_taken(username: &str) -> Result<bool> {
if username == "revolt" && username == "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

@@ -68,14 +68,12 @@ impl<'a> PermissionCalculator<'a> {
|| 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,
)

View File

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

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,8 @@ pub async fn req(user: User, target: Ref) -> Result<()> {
Message::create(
"00000000000000000000000000".to_string(),
id.clone(),
format!("<@{}> left the group.", user.id),
// ! FIXME: make a schema for this
format!("{{\"type\":\"user_left\",\"id\":\"{}\"}}", user.id),
)
.publish(&target)
.await

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,8 @@ 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),
// ! FIXME: make a schema for this
format!("{{\"type\":\"user_added\",\"id\":\"{}\",\"by\":\"{}\"}}", member.id, 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,8 @@ 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),
// ! FIXME: make a schema for this
format!("{{\"type\":\"user_remove\",\"id\":\"{}\",\"by\":\"{}\"}}", member.id, user.id),
)
.publish(&channel)
.await

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

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

View File

@@ -1,7 +1,10 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use mongodb::{
bson::{doc, from_document},
options::FindOneOptions,
};
use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
@@ -9,11 +12,13 @@ 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))]
nonce: String,
#[validate(length(min = 1, max = 128))]
attachment: Option<String>,
}
#[post("/<target>/messages", data = "<message>")]
@@ -21,6 +26,10 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
message
.validate()
.map_err(|error| Error::FailedValidation { error })?;
if message.content.len() == 0 && message.attachment.is_none() {
return Err(Error::EmptyMessage);
}
let target = target.fetch_channel().await?;
@@ -29,7 +38,7 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
.for_channel()
.await?;
if !perm.get_send_message() {
Err(Error::LabelMe)?
Err(Error::MissingPermission)?
}
if get_collection("messages")
@@ -37,7 +46,9 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
doc! {
"nonce": &message.nonce
},
None,
FindOneOptions::builder()
.projection(doc! { "_id": 1 })
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
@@ -49,12 +60,63 @@ pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonVal
Err(Error::DuplicateNonce)?
}
let id = Ulid::new().to_string();
let attachments = get_collection("attachments");
let attachment = if let Some(attachment_id) = &message.attachment {
if let Some(doc) = attachments
.find_one(
doc! {
"_id": attachment_id,
"message_id": {
"$exists": false
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "attachment",
})?
{
let attachment = from_document::<File>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "attachment",
})?;
attachments
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
"message_id": &id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
Some(attachment)
} else {
return Err(Error::UnknownAttachment);
}
} else {
None
};
let msg = Message {
id: Ulid::new().to_string(),
id,
channel: target.id().to_string(),
author: user.id,
content: message.content.clone(),
attachment,
nonce: Some(message.nonce.clone()),
edited: None,
};

View File

@@ -9,6 +9,7 @@ mod message_delete;
mod message_edit;
mod message_fetch;
mod message_query;
mod message_query_stale;
mod message_send;
pub fn routes() -> Vec<Route> {
@@ -17,6 +18,7 @@ pub fn routes() -> Vec<Route> {
delete_channel::req,
message_send::req,
message_query::req,
message_query_stale::req,
message_fetch::req,
message_edit::req,
message_delete::req,

View File

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

View File

@@ -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,31 +27,11 @@ 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(
doc! {
"username": &data.username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.is_some()
{
Err(Error::UsernameTaken)?
}
col.insert_one(
get_collection("users").insert_one(
doc! {
"_id": session.user_id,
"username": &data.username

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

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
use crate::util::variables::{
DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, USE_EMAIL, USE_HCAPTCHA,
AUTUMN_URL, DISABLE_REGISTRATION, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, USE_AUTUMN,
USE_EMAIL, USE_HCAPTCHA, VAPID_PUBLIC_KEY,
};
use mongodb::bson::doc;
@@ -8,7 +9,7 @@ use rocket_contrib::json::JsonValue;
#[get("/")]
pub async fn root() -> JsonValue {
json!({
"revolt": "0.3.3-alpha.2",
"revolt": "0.3.3-alpha.8",
"features": {
"registration": !*DISABLE_REGISTRATION,
"captcha": {
@@ -16,8 +17,13 @@ pub async fn root() -> JsonValue {
"key": HCAPTCHA_SITEKEY.to_string()
},
"email": *USE_EMAIL,
"invite_only": *INVITE_ONLY
"invite_only": *INVITE_ONLY,
"autumn": {
"enabled": *USE_AUTUMN,
"url": *AUTUMN_URL
}
},
"ws": *EXTERNAL_WS_URL,
"vapid": *VAPID_PUBLIC_KEY
})
}

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

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

View File

@@ -6,6 +6,7 @@ mod fetch_dms;
mod fetch_relationship;
mod fetch_relationships;
mod fetch_user;
mod find_mutual;
mod get_avatar;
mod get_default_avatar;
mod open_dm;
@@ -22,6 +23,7 @@ pub fn routes() -> Vec<Route> {
fetch_dms::req,
open_dm::req,
// Relationships
find_mutual::req,
fetch_relationships::req,
fetch_relationship::req,
add_friend::req,

View File

@@ -36,8 +36,12 @@ pub enum Error {
// ? Channel related errors.
#[snafu(display("This channel does not exist!"))]
UnknownChannel,
#[snafu(display("Attachment does not exist!"))]
UnknownAttachment,
#[snafu(display("Cannot edit someone else's message."))]
CannotEditMessage,
#[snafu(display("Cannot send empty message."))]
EmptyMessage,
#[snafu(display("Cannot remove yourself from a group, use delete channel instead."))]
CannotRemoveYourself,
#[snafu(display("Group size is too large."))]
@@ -48,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."))]
@@ -57,6 +63,8 @@ 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("Already created an object with this nonce."))]
@@ -84,7 +92,9 @@ impl<'r> Responder<'r, 'static> for Error {
Error::NotFriends => Status::Forbidden,
Error::UnknownChannel => Status::NotFound,
Error::UnknownAttachment => Status::BadRequest,
Error::CannotEditMessage => Status::Forbidden,
Error::EmptyMessage => Status::UnprocessableEntity,
Error::CannotRemoveYourself => Status::BadRequest,
Error::GroupTooLarge { .. } => Status::Forbidden,
Error::AlreadyInGroup => Status::Conflict,
@@ -93,7 +103,9 @@ 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::DuplicateNonce => Status::Conflict,
Error::NoEffect => Status::Ok,
};

View File

@@ -7,18 +7,24 @@ lazy_static! {
// Application Settings
pub static ref MONGO_URI: String =
env::var("REVOLT_MONGO_URI").expect("Missing REVOLT_MONGO_URI environment variable.");
pub static ref WS_HOST: String =
env::var("REVOLT_WS_HOST").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
pub static ref PUBLIC_URL: String =
env::var("REVOLT_PUBLIC_URL").expect("Missing REVOLT_PUBLIC_URL environment variable.");
pub static ref APP_URL: String =
env::var("REVOLT_APP_URL").unwrap_or_else(|_| "https://app.revolt.chat".to_string());
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 HCAPTCHA_KEY: String =
env::var("REVOLT_HCAPTCHA_KEY").unwrap_or_else(|_| "0x0000000000000000000000000000000000000000".to_string());
pub static ref HCAPTCHA_SITEKEY: String =
env::var("REVOLT_HCAPTCHA_SITEKEY").unwrap_or_else(|_| "10000000-ffff-ffff-ffff-000000000001".to_string());
pub static ref WS_HOST: String =
env::var("REVOLT_WS_HOST").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
pub static ref VAPID_PRIVATE_KEY: String =
env::var("REVOLT_VAPID_PRIVATE_KEY").expect("Missing REVOLT_VAPID_PRIVATE_KEY environment variable.");
pub static ref VAPID_PUBLIC_KEY: String =
env::var("REVOLT_VAPID_PUBLIC_KEY").expect("Missing REVOLT_VAPID_PUBLIC_KEY environment variable.");
// Application Flags
pub static ref DISABLE_REGISTRATION: bool = env::var("REVOLT_DISABLE_REGISTRATION").map_or(false, |v| v == "1");
@@ -32,6 +38,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();
// SMTP Settings
pub static ref SMTP_HOST: String =