Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Commits on Source (68)
Showing
with 1732 additions and 225 deletions
...@@ -1599,6 +1599,15 @@ dependencies = [ ...@@ -1599,6 +1599,15 @@ dependencies = [
"rand 0.6.5", "rand 0.6.5",
] ]
[[package]]
name = "nanoid"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8"
dependencies = [
"rand 0.8.3",
]
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.7" version = "0.2.7"
...@@ -2332,8 +2341,8 @@ dependencies = [ ...@@ -2332,8 +2341,8 @@ dependencies = [
[[package]] [[package]]
name = "rauth" name = "rauth"
version = "0.2.7" version = "0.2.7-patch.0"
source = "git+https://gitlab.insrt.uk/insert/rauth?rev=1db825a0059639144ea8802f6d51b5d27c5b986e#1db825a0059639144ea8802f6d51b5d27c5b986e" source = "git+https://gitlab.insrt.uk/insert/rauth?rev=00d3c3dff51cf3242a7d4adda4c5184c97fa2a03#00d3c3dff51cf3242a7d4adda4c5184c97fa2a03"
dependencies = [ dependencies = [
"chrono", "chrono",
"handlebars", "handlebars",
...@@ -2341,7 +2350,7 @@ dependencies = [ ...@@ -2341,7 +2350,7 @@ dependencies = [
"lazy_static", "lazy_static",
"lettre", "lettre",
"mongodb", "mongodb",
"nanoid", "nanoid 0.3.0",
"regex", "regex",
"reqwest", "reqwest",
"rocket", "rocket",
...@@ -2497,6 +2506,7 @@ dependencies = [ ...@@ -2497,6 +2506,7 @@ dependencies = [
"many-to-many", "many-to-many",
"md5", "md5",
"mongodb", "mongodb",
"nanoid 0.4.0",
"num_enum", "num_enum",
"once_cell", "once_cell",
"rand 0.7.3", "rand 0.7.3",
......
...@@ -16,6 +16,7 @@ log = "0.4.11" ...@@ -16,6 +16,7 @@ log = "0.4.11"
ulid = "0.4.1" ulid = "0.4.1"
rand = "0.7.3" rand = "0.7.3"
time = "0.2.16" time = "0.2.16"
nanoid = "0.4.0"
base64 = "0.13.0" base64 = "0.13.0"
linkify = "0.6.0" linkify = "0.6.0"
dotenv = "0.15.0" dotenv = "0.15.0"
...@@ -41,7 +42,7 @@ async-std = { version = "1.8.0", features = ["tokio02", "attributes"] } ...@@ -41,7 +42,7 @@ async-std = { version = "1.8.0", features = ["tokio02", "attributes"] }
async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] } async-tungstenite = { version = "0.10.0", features = ["async-std-runtime"] }
rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" } rocket_cors = { git = "https://github.com/insertish/rocket_cors", branch = "master" }
mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false } mongodb = { version = "1.1.1", features = ["tokio-runtime"], default-features = false }
rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "1db825a0059639144ea8802f6d51b5d27c5b986e" } rauth = { git = "https://gitlab.insrt.uk/insert/rauth", rev = "00d3c3dff51cf3242a7d4adda4c5184c97fa2a03" }
rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" } rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00" }
rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" } rocket_prometheus = { git = "https://github.com/insertish/rocket_prometheus", rev = "3d825aedb42793246c306a81fe67c5b187948983" }
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false } rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "031948c1daaa146128d8a435be116476f2adde00", default-features = false }
This diff is collapsed.
...@@ -4,57 +4,58 @@ ...@@ -4,57 +4,58 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Reset your password.</title> <title>Reset your password.</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style type="text/css"> <style type="text/css">
a[x-apple-data-detectors] {color: inherit !important;} a[x-apple-data-detectors] {color: inherit !important;}
</style> </style>
</head> </head>
<body style="margin: 0; padding: 0;"> <body style="margin: 0; padding: 0;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"> <table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr> <tr>
<td style="padding: 20px 0 30px 0;"> <td style="padding: 20px 0 30px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;"> <table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
<tr> <tr>
<td align="center" bgcolor="#ff4654"> <td align="center" bgcolor="#ff4654">
<img src="https://revolt.chat/header.png" alt="REVOLT logo" width="600" height="168" style="display: block;" /> <img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
</td> </td>
</tr> </tr>
<tr> <tr>
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;"> <td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr> <tr>
<td style="color: #153643; font-family: Arial, sans-serif;"> <td style="color: #153643; font-family: Arial, sans-serif;">
<h1 style="font-size: 24px; margin: 0;">Reset your password!</h1> <h1 style="font-size: 24px; margin: 0;">Reset your password!</h1>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;"> <td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
<p style="margin: 0;"> <p>
Reset your password by <a href="{{url}}">clicking here</a>. You requested a password reset, if you didn't perform this action you can safely ignore this email.
</p> </p>
<p> <p style="margin: 0;">
Or by manually navigating to the URL: {{url}} Reset your password by navigating to <a href="{{url}}">{{url}}</a>.
</p> </p>
</td> </td>
</tr> </tr>
</table> </table>
</td> </td>
</tr> </tr>
<tr> <tr>
<td bgcolor="#ff4654" style="padding: 30px 30px;"> <td bgcolor="#ff4654" style="padding: 30px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr> <tr>
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;"> <td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
<p style="margin: 0;">Sent by REVOLT. &middot; <a style="color: white;" href="https://revolt.chat">Website</a> &middot; <a style="color: white;" href="https://gitlab.insrt.uk/revolt">Source Code</a></p> <p style="margin: 0;">Sent by Revolt. &middot; Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
</td> <p>Revolt is a user-first chat platform built with modern web technologies.</p>
</tr> </td>
</table> </tr>
</td> </table>
</tr> </td>
</table> </tr>
</table>
</td> </td>
</tr> </tr>
</table> </table>
......
<h2>Reset your password.</h2>
<p>
You requested a password reset, if you did not perform this action you can safely ignore this email.
</p>
<p>
Reset your password here: <a href="{{url}}">{{url}}</a>
</p>
<br/>
<p>
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
</p>
<p>
Revolt is a user-first chat platform built with modern web technologies.
</p>
\ No newline at end of file
...@@ -4,57 +4,58 @@ ...@@ -4,57 +4,58 @@
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Verify your account.</title> <title>Verify your account.</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style type="text/css"> <style type="text/css">
a[x-apple-data-detectors] {color: inherit !important;} a[x-apple-data-detectors] {color: inherit !important;}
</style> </style>
</head> </head>
<body style="margin: 0; padding: 0;"> <body style="margin: 0; padding: 0;">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%"> <table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr> <tr>
<td style="padding: 20px 0 30px 0;"> <td style="padding: 20px 0 30px 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;"> <table align="center" border="0" cellpadding="0" cellspacing="0" width="600" style="border-collapse: collapse; border: 1px solid #cccccc;">
<tr> <tr>
<td align="center" bgcolor="#ff4654"> <td align="center" bgcolor="#ff4654">
<img src="https://revolt.chat/header.png" alt="REVOLT logo" width="600" height="168" style="display: block;" /> <img src="https://revolt.chat/header.png" alt="Revolt logo" width="600" height="168" style="display: block;" />
</td> </td>
</tr> </tr>
<tr> <tr>
<td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;"> <td bgcolor="#ffffff" style="padding: 40px 30px 40px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr> <tr>
<td style="color: #153643; font-family: Arial, sans-serif;"> <td style="color: #153643; font-family: Arial, sans-serif;">
<h1 style="font-size: 24px; margin: 0;">Verify your account!</h1> <h1 style="font-size: 24px; margin: 0;">You're almost there!</h1>
</td> </td>
</tr> </tr>
<tr> <tr>
<td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;"> <td style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 24px; padding: 20px 0 0 0;">
<p style="margin: 0;"> <p>
Please verify your account by <a href="{{url}}">clicking here</a>. Verify your account to be able to log into the platform. If you didn't perform this action you can safely ignore this email.
</p> </p>
<p> <p style="margin: 0;">
Or by manually navigating to the URL: {{url}} Please verify your account by navigating to <a href="{{url}}">{{url}}</a>.
</p> </p>
</td> </td>
</tr> </tr>
</table> </table>
</td> </td>
</tr> </tr>
<tr> <tr>
<td bgcolor="#ff4654" style="padding: 30px 30px;"> <td bgcolor="#ff4654" style="padding: 30px 30px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;"> <table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse;">
<tr> <tr>
<td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;"> <td style="color: #ffffff; font-family: Arial, sans-serif; font-size: 14px;">
<p style="margin: 0;">Sent by REVOLT. &middot; <a style="color: white;" href="https://revolt.chat">Website</a> &middot; <a style="color: white;" href="https://gitlab.insrt.uk/revolt">Source Code</a></p> <p style="margin: 0;">Sent by Revolt. &middot; Website: <a style="color: white;" href="https://revolt.chat">https://revolt.chat</a></p>
</td> <p>Revolt is a user-first chat platform built with modern web technologies.</p>
</tr> </td>
</table> </tr>
</td> </table>
</tr> </td>
</table> </tr>
</table>
</td> </td>
</tr> </tr>
</table> </table>
......
<h2>You're almost there!</h2>
<p>
Verify your account to be able to log into the platform.<br/>
If you did not perform this action you can safely ignore this email.
</p>
<p>
Please verify your account here: <a href="{{url}}">{{url}}</a>
</p>
<br/>
<p>
Sent by Revolt. · Website: <a href="https://revolt.chat">https://revolt.chat</a>
</p>
<p>
Revolt is a user-first chat platform built with modern web technologies.
</p>
\ No newline at end of file
#!/bin/bash #!/bin/bash
source set_version.sh source set_version.sh
docker build -t revoltchat/server:${version} . && docker build -t revoltchat/server:${version} . &&
docker push revoltchat/server:${version} docker tag revoltchat/server:${version} revoltchat/server:latest &&
docker push revoltchat/server:${version} &&
docker push revoltchat/server:latest
#!/bin/bash #!/bin/bash
export version=0.5.0-alpha.0 export version=0.5.1-alpha.21
echo "pub const VERSION: &str = \"${version}\";" > src/version.rs echo "pub const VERSION: &str = \"${version}\";" > src/version.rs
use std::collections::HashMap;
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 futures::StreamExt; use futures::StreamExt;
use mongodb::bson::Bson;
use mongodb::{ use mongodb::{
bson::{doc, from_document, to_document, Document}, bson::{doc, to_document, Document},
options::FindOptions, options::FindOptions,
}; };
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
...@@ -42,13 +45,17 @@ pub enum Channel { ...@@ -42,13 +45,17 @@ pub enum Channel {
name: String, name: String,
owner: String, owner: String,
description: String, #[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
recipients: Vec<String>, recipients: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>, 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>,
#[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<i32>,
}, },
TextChannel { TextChannel {
#[serde(rename = "_id")] #[serde(rename = "_id")]
...@@ -58,10 +65,37 @@ pub enum Channel { ...@@ -58,10 +65,37 @@ pub enum Channel {
nonce: Option<String>, nonce: Option<String>,
name: String, name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>, description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>, icon: Option<File>,
} #[serde(skip_serializing_if = "Option::is_none")]
last_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<i32>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
role_permissions: HashMap<String, i32>
},
VoiceChannel {
#[serde(rename = "_id")]
id: String,
server: String,
#[serde(skip_serializing_if = "Option::is_none")]
nonce: Option<String>,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
default_permissions: Option<i32>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
role_permissions: HashMap<String, i32>
},
} }
impl Channel { impl Channel {
...@@ -70,24 +104,18 @@ impl Channel { ...@@ -70,24 +104,18 @@ impl Channel {
Channel::SavedMessages { id, .. } Channel::SavedMessages { id, .. }
| Channel::DirectMessage { id, .. } | Channel::DirectMessage { id, .. }
| Channel::Group { id, .. } | Channel::Group { id, .. }
| Channel::TextChannel { id, .. } => id, | Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
} }
} }
pub fn has_messaging(&self) -> Result<()> {
pub async fn get(id: &str) -> Result<Channel> { match self {
let doc = get_collection("channels") Channel::SavedMessages { .. }
.find_one(doc! { "_id": id }, None) | Channel::DirectMessage { .. }
.await | Channel::Group { .. }
.map_err(|_| Error::DatabaseError { | Channel::TextChannel { .. } => Ok(()),
operation: "find_one", Channel::VoiceChannel { .. } => Err(Error::InvalidOperation)
with: "channel", }
})?
.ok_or_else(|| Error::UnknownChannel)?;
from_document::<Channel>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "channel",
})
} }
pub async fn publish(self) -> Result<()> { pub async fn publish(self) -> Result<()> {
...@@ -123,15 +151,44 @@ impl Channel { ...@@ -123,15 +151,44 @@ impl Channel {
Ok(()) Ok(())
} }
pub async fn delete(&self) -> Result<()> { pub async fn delete_associated_objects(id: Bson) -> Result<()> {
let id = self.id(); get_collection("channel_invites")
.delete_many(
doc! {
"channel": id
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_invites",
})
}
pub async fn delete_messages(id: Bson) -> Result<()> {
let messages = get_collection("messages"); let messages = get_collection("messages");
// Delete any unreads.
get_collection("channel_unreads")
.delete_many(
doc! {
"_id.channel": &id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_unreads",
})?;
// Check if there are any attachments we need to delete. // Check if there are any attachments we need to delete.
let message_ids = messages let message_ids = messages
.find( .find(
doc! { doc! {
"channel": id, "channel": &id,
"attachment": { "attachment": {
"$exists": 1 "$exists": 1
} }
...@@ -182,11 +239,88 @@ impl Channel { ...@@ -182,11 +239,88 @@ impl Channel {
None, None,
) )
.await .await
.map(|_| ())
.map_err(|_| Error::DatabaseError { .map_err(|_| Error::DatabaseError {
operation: "delete_many", operation: "delete_many",
with: "messages", with: "messages",
})?; })
}
pub async fn delete(&self) -> Result<()> {
let id = self.id();
// Delete any invites.
Channel::delete_associated_objects(Bson::String(id.to_string())).await?;
// Delete messages.
match &self {
Channel::VoiceChannel { .. } => {},
_ => {
Channel::delete_messages(Bson::String(id.to_string())).await?;
}
}
// Remove from server object.
match &self {
Channel::TextChannel { server, .. }
| Channel::VoiceChannel { server, .. } => {
let server = Ref::from_unchecked(server.clone()).fetch_server().await?;
let mut update = doc! {
"$pull": {
"channels": id
}
};
if let Some(sys) = &server.system_messages {
let mut unset = doc! {};
if let Some(cid) = &sys.user_joined {
if id == cid {
unset.insert("system_messages.user_joined", 1);
}
}
if let Some(cid) = &sys.user_left {
if id == cid {
unset.insert("system_messages.user_left", 1);
}
}
if let Some(cid) = &sys.user_kicked {
if id == cid {
unset.insert("system_messages.user_kicked", 1);
}
}
if let Some(cid) = &sys.user_banned {
if id == cid {
unset.insert("system_messages.user_banned", 1);
}
}
if unset.len() > 0 {
update.insert("$unset", unset);
}
}
get_collection("servers")
.update_one(
doc! {
"_id": server.id
},
update,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "servers",
})?;
},
_ => {}
}
// Finally, delete the channel object.
get_collection("channels") get_collection("channels")
.delete_one( .delete_one(
doc! { doc! {
......
use mongodb::bson::doc;
use mongodb::bson::from_document;
use mongodb::bson::to_document;
use serde::{Deserialize, Serialize};
use crate::database::get_collection;
use crate::util::result::Error;
use crate::util::result::Result;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Invite {
Server {
#[serde(rename = "_id")]
code: String,
server: String,
creator: String,
channel: String,
},
Group {
#[serde(rename = "_id")]
code: String,
creator: String,
channel: String,
}, /* User {
code: String,
user: String
} */
}
impl Invite {
pub fn code(&self) -> &String {
match &self {
Invite::Server { code, .. } => code,
Invite::Group { code, .. } => code,
}
}
pub fn creator(&self) -> &String {
match &self {
Invite::Server { creator, .. } => creator,
Invite::Group { creator, .. } => creator,
}
}
pub async fn get(code: &str) -> Result<Invite> {
let doc = get_collection("channel_invites")
.find_one(doc! { "_id": code }, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "invite",
})?
.ok_or_else(|| Error::UnknownServer)?;
from_document::<Invite>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "invite",
})
}
pub async fn save(self) -> Result<()> {
get_collection("channel_invites")
.insert_one(
to_document(&self).map_err(|_| Error::DatabaseError {
operation: "to_bson",
with: "invite",
})?,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "invite",
})?;
Ok(())
}
pub async fn delete(&self) -> Result<()> {
get_collection("channel_invites")
.delete_one(
doc! {
"_id": self.code()
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "invite",
})?;
Ok(())
}
}
...@@ -6,6 +6,7 @@ use crate::{ ...@@ -6,6 +6,7 @@ use crate::{
}; };
use futures::StreamExt; use futures::StreamExt;
use mongodb::options::UpdateOptions;
use mongodb::{ use mongodb::{
bson::{doc, to_bson, DateTime}, bson::{doc, to_bson, DateTime},
options::FindOptions, options::FindOptions,
...@@ -26,8 +27,14 @@ pub enum SystemMessage { ...@@ -26,8 +27,14 @@ pub enum SystemMessage {
UserAdded { id: String, by: String }, UserAdded { id: String, by: String },
#[serde(rename = "user_remove")] #[serde(rename = "user_remove")]
UserRemove { id: String, by: String }, UserRemove { id: String, by: String },
#[serde(rename = "user_joined")]
UserJoined { id: String },
#[serde(rename = "user_left")] #[serde(rename = "user_left")]
UserLeft { id: String }, UserLeft { id: String },
#[serde(rename = "user_kicked")]
UserKicked { id: String },
#[serde(rename = "user_banned")]
UserBanned { id: String },
#[serde(rename = "channel_renamed")] #[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String }, ChannelRenamed { name: String, by: String },
#[serde(rename = "channel_description_changed")] #[serde(rename = "channel_description_changed")]
...@@ -43,6 +50,20 @@ pub enum Content { ...@@ -43,6 +50,20 @@ pub enum Content {
SystemMessage(SystemMessage), SystemMessage(SystemMessage),
} }
impl Content {
pub async fn send_as_system(self, target: &Channel) -> Result<()> {
Message::create(
"00000000000000000000000000".to_string(),
target.id().to_string(),
self,
None,
None
)
.publish(&target, false)
.await
}
}
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message { pub struct Message {
#[serde(rename = "_id")] #[serde(rename = "_id")]
...@@ -59,10 +80,20 @@ pub struct Message { ...@@ -59,10 +80,20 @@ pub struct Message {
pub edited: Option<DateTime>, pub edited: Option<DateTime>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<Embed>>, pub embeds: Option<Vec<Embed>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>
} }
impl Message { impl Message {
pub fn create(author: String, channel: String, content: Content) -> Message { pub fn create(
author: String,
channel: String,
content: Content,
mentions: Option<Vec<String>>,
replies: Option<Vec<String>>,
) -> Message {
Message { Message {
id: Ulid::new().to_string(), id: Ulid::new().to_string(),
nonce: None, nonce: None,
...@@ -73,10 +104,12 @@ impl Message { ...@@ -73,10 +104,12 @@ impl Message {
attachments: None, attachments: None,
edited: None, edited: None,
embeds: None, embeds: None,
mentions,
replies
} }
} }
pub async fn publish(self, channel: &Channel) -> Result<()> { pub async fn publish(self, channel: &Channel, process_embeds: bool) -> Result<()> {
get_collection("messages") get_collection("messages")
.insert_one(to_bson(&self).unwrap().as_document().unwrap().clone(), None) .insert_one(to_bson(&self).unwrap().as_document().unwrap().clone(), None)
.await .await
...@@ -85,39 +118,30 @@ impl Message { ...@@ -85,39 +118,30 @@ impl Message {
with: "message", with: "message",
})?; })?;
let mut set = if let Content::Text(text) = &self.content { // ! FIXME: all this code is legitimately crap
doc! { // ! rewrite when can be asked
"last_message": {
"_id": self.id.clone(), let ss = self.clone();
"author": self.author.clone(), let c_clone = channel.clone();
"short": text.chars().take(24).collect::<String>() async_std::task::spawn(async move {
let mut set = if let Content::Text(text) = &ss.content {
doc! {
"last_message": {
"_id": ss.id.clone(),
"author": ss.author.clone(),
"short": text.chars().take(128).collect::<String>()
}
} }
} } else {
} else { doc! {}
doc! {} };
};
// ! FIXME: temp code // ! MARK AS ACTIVE
let channels = get_collection("channels"); // ! FIXME: temp code
match &channel { let channels = get_collection("channels");
Channel::DirectMessage { id, .. } => { match &c_clone {
set.insert("active", true); Channel::DirectMessage { id, .. } => {
channels set.insert("active", true);
.update_one(
doc! { "_id": id },
doc! {
"$set": set
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
Channel::Group { id, .. } => {
if let Content::Text(_) = &self.content {
channels channels
.update_one( .update_one(
doc! { "_id": id }, doc! { "_id": id },
...@@ -127,39 +151,116 @@ impl Message { ...@@ -127,39 +151,116 @@ impl Message {
None, None,
) )
.await .await
.map_err(|_| Error::DatabaseError { /*.map_err(|_| Error::DatabaseError {
operation: "update_one", operation: "update_one",
with: "channel", with: "channel",
})?; })?;*/
.unwrap();
}
Channel::Group { id, .. } => {
if let Content::Text(_) = &ss.content {
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": set
},
None,
)
.await
/*.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;*/
.unwrap();
}
}
Channel::TextChannel { id, .. } => {
if let Content::Text(_) = &ss.content {
channels
.update_one(
doc! { "_id": id },
doc! {
"$set": {
"last_message": &ss.id
}
},
None,
)
.await
/*.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;*/
.unwrap();
}
} }
_ => {}
} }
_ => {} });
// ! FIXME: also temp code
// ! THIS ADDS ANY MENTIONS
if let Some(mentions) = &self.mentions {
let message = self.id.clone();
let channel = self.channel.clone();
let mentions = mentions.clone();
async_std::task::spawn(async move {
get_collection("channel_unreads")
.update_many(
doc! {
"_id.channel": channel,
"_id.user": {
"$in": mentions
}
},
doc! {
"$push": {
"mentions": message
}
},
UpdateOptions::builder().upsert(true).build(),
)
.await
/*.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "channel_unreads",
})?;*/
.unwrap();
});
} }
self.process_embed(); if process_embeds {
self.process_embed();
}
let mentions = self.mentions.clone();
let enc = serde_json::to_string(&self).unwrap(); let enc = serde_json::to_string(&self).unwrap();
ClientboundNotification::Message(self).publish(channel.id().to_string()); ClientboundNotification::Message(self).publish(channel.id().to_string());
/* /*
Web Push Test Code Web Push Test Code
! FIXME: temp code
*/ */
let c_clone = channel.clone();
// Find all offline users. async_std::task::spawn(async move {
let mut target_ids = vec![]; // Find all offline users.
match &channel { let mut target_ids = vec![];
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => { match &c_clone {
for recipient in recipients { Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
if !is_online(recipient) { for recipient in recipients {
target_ids.push(recipient.clone()); if !is_online(recipient) {
target_ids.push(recipient.clone());
}
} }
} }
Channel::TextChannel { .. } => {
if let Some(mut mentions) = mentions {
target_ids.append(&mut mentions);
}
}
_ => {}
} }
_ => {}
}
async_std::task::spawn(async move {
// Fetch their corresponding sessions. // Fetch their corresponding sessions.
if let Ok(mut cursor) = get_collection("accounts") if let Ok(mut cursor) = get_collection("accounts")
.find( .find(
...@@ -241,6 +342,8 @@ impl Message { ...@@ -241,6 +342,8 @@ impl Message {
} }
if let Content::Text(text) = &self.content { if let Content::Text(text) = &self.content {
// ! FIXME: re-write this at some point,
// ! or just before we allow user generated embeds
let id = self.id.clone(); let id = self.id.clone();
let content = text.clone(); let content = text.clone();
let channel = self.channel.clone(); let channel = self.channel.clone();
......
...@@ -3,6 +3,7 @@ use crate::util::{ ...@@ -3,6 +3,7 @@ use crate::util::{
variables::JANUARY_URL, variables::JANUARY_URL,
}; };
use linkify::{LinkFinder, LinkKind}; use linkify::{LinkFinder, LinkKind};
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
...@@ -63,7 +64,9 @@ pub enum Special { ...@@ -63,7 +64,9 @@ pub enum Special {
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Metadata { pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>, url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
special: Option<Special>, special: Option<Special>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
...@@ -95,10 +98,17 @@ pub enum Embed { ...@@ -95,10 +98,17 @@ pub enum Embed {
impl Embed { impl Embed {
pub async fn generate(content: String) -> Result<Vec<Embed>> { pub async fn generate(content: String) -> Result<Vec<Embed>> {
lazy_static! {
static ref RE_CODE: Regex = Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap();
}
// Ignore code blocks.
let content = RE_CODE.replace_all(&content, "");
let content = content let content = content
// Ignore quoted lines.
.split("\n") .split("\n")
.map(|v| { .map(|v| {
// Ignore quoted lines.
if let Some(c) = v.chars().next() { if let Some(c) = v.chars().next() {
if c == '>' { if c == '>' {
return ""; return "";
......
mod channel; mod channel;
mod invites;
mod message; mod message;
mod microservice; mod microservice;
mod server; mod server;
...@@ -9,6 +10,7 @@ use microservice::*; ...@@ -9,6 +10,7 @@ use microservice::*;
pub use autumn::*; pub use autumn::*;
pub use channel::*; pub use channel::*;
pub use invites::*;
pub use january::*; pub use january::*;
pub use message::*; pub use message::*;
pub use server::*; pub use server::*;
......
use std::collections::HashMap;
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; use futures::StreamExt;
use mongodb::bson::{Bson, doc};
use mongodb::bson::from_document; use mongodb::bson::from_document;
use mongodb::bson::to_document; use mongodb::bson::to_document;
use mongodb::bson::Document;
use rocket_contrib::json::JsonValue; use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
...@@ -17,20 +21,56 @@ pub struct MemberCompositeKey { ...@@ -17,20 +21,56 @@ pub struct MemberCompositeKey {
pub struct Member { pub struct Member {
#[serde(rename = "_id")] #[serde(rename = "_id")]
pub id: MemberCompositeKey, pub id: MemberCompositeKey,
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>, pub nickname: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>
}
pub type PermissionTuple = (
i32, // server permission
i32 // channel permission
);
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Role {
pub name: String,
pub permissions: PermissionTuple,
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>
// Bri'ish API conventions
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Invite { pub struct Category {
pub code: String, pub id: String,
pub creator: String, pub title: String,
pub channel: String, pub channels: Vec<String>
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Ban { pub struct Ban {
pub id: String, #[serde(rename = "_id")]
pub reason: String, pub id: MemberCompositeKey,
pub reason: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SystemMessageChannels {
pub user_joined: Option<String>,
pub user_left: Option<String>,
pub user_kicked: Option<String>,
pub user_banned: Option<String>,
}
pub enum RemoveMember {
Leave,
Kick,
Ban,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
...@@ -42,10 +82,18 @@ pub struct Server { ...@@ -42,10 +82,18 @@ pub struct Server {
pub owner: String, pub owner: String,
pub name: String, pub name: String,
// pub default_permissions: u32, #[serde(skip_serializing_if = "Option::is_none")]
// pub invites: Vec<Invite>, pub description: Option<String>,
// pub bans: Vec<Ban>,
pub channels: Vec<String>, pub channels: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<Category>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_messages: Option<SystemMessageChannels>,
#[serde(default = "HashMap::new", skip_serializing_if = "HashMap::is_empty")]
pub roles: HashMap<String, Role>,
pub default_permissions: PermissionTuple,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<File>, pub icon: Option<File>,
...@@ -54,23 +102,7 @@ pub struct Server { ...@@ -54,23 +102,7 @@ pub struct Server {
} }
impl Server { impl Server {
pub async fn get(id: &str) -> Result<Server> { pub async fn create(self) -> Result<()> {
let doc = get_collection("servers")
.find_one(doc! { "_id": id }, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server",
})?
.ok_or_else(|| Error::UnknownServer)?;
from_document::<Server>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "server",
})
}
pub async fn publish(self) -> Result<()> {
get_collection("servers") get_collection("servers")
.insert_one( .insert_one(
to_document(&self).map_err(|_| Error::DatabaseError { to_document(&self).map_err(|_| Error::DatabaseError {
...@@ -85,9 +117,6 @@ impl Server { ...@@ -85,9 +117,6 @@ impl Server {
with: "server", with: "server",
})?; })?;
let server_id = self.id.clone();
ClientboundNotification::ServerCreate(self).publish(server_id);
Ok(()) Ok(())
} }
...@@ -103,6 +132,252 @@ impl Server { ...@@ -103,6 +132,252 @@ impl Server {
} }
pub async fn delete(&self) -> Result<()> { pub async fn delete(&self) -> Result<()> {
unimplemented!() // Check if there are any attachments we need to delete.
Channel::delete_messages(Bson::Document(doc! { "$in": &self.channels })).await?;
// Delete all channels.
get_collection("channels")
.delete_many(
doc! {
"server": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channels",
})?;
// Delete any associated objects, e.g. unreads and invites.
Channel::delete_associated_objects(Bson::Document(doc! { "$in": &self.channels })).await?;
// Delete members and bans.
for with in &["server_members", "server_bans"] {
get_collection(with)
.delete_many(
doc! {
"_id.server": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with,
})?;
}
// Delete server icon / banner.
if let Some(attachment) = &self.icon {
attachment.delete().await?;
}
if let Some(attachment) = &self.banner {
attachment.delete().await?;
}
// Delete the server
get_collection("servers")
.delete_one(
doc! {
"_id": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "server",
})?;
ClientboundNotification::ServerDelete {
id: self.id.clone(),
}
.publish(self.id.clone());
Ok(())
}
pub async fn fetch_members(id: &str) -> Result<Vec<Member>> {
Ok(get_collection("server_members")
.find(
doc! {
"_id.server": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_members",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| from_document(x).ok())
.collect::<Vec<Member>>())
}
pub async fn fetch_member_ids(id: &str) -> Result<Vec<String>> {
Ok(get_collection("server_members")
.find(
doc! {
"_id.server": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_members",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| {
x.get_document("_id")
.ok()
.map(|i| i.get_str("user").ok().map(|x| x.to_string()))
})
.flatten()
.collect::<Vec<String>>())
}
pub async fn join_member(&self, id: &str) -> Result<()> {
if get_collection("server_bans")
.find_one(
doc! {
"_id.server": &self.id,
"_id.user": &id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server_bans",
})?
.is_some()
{
return Err(Error::Banned);
}
get_collection("server_members")
.insert_one(
doc! {
"_id": {
"server": &self.id,
"user": &id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "server_members",
})?;
ClientboundNotification::ServerMemberJoin {
id: self.id.clone(),
user: id.to_string(),
}
.publish(self.id.clone());
if let Some(channels) = &self.system_messages {
if let Some(cid) = &channels.user_joined {
let channel = Ref::from_unchecked(cid.clone()).fetch_channel().await?;
Content::SystemMessage(SystemMessage::UserJoined { id: id.to_string() })
.send_as_system(&channel)
.await?;
}
}
Ok(())
}
pub async fn remove_member(&self, id: &str, removal: RemoveMember) -> Result<()> {
let result = get_collection("server_members")
.delete_one(
doc! {
"_id": {
"server": &self.id,
"user": &id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "server_members",
})?;
if result.deleted_count > 0 {
ClientboundNotification::ServerMemberLeave {
id: self.id.clone(),
user: id.to_string(),
}
.publish(self.id.clone());
if let Some(channels) = &self.system_messages {
let message = match removal {
RemoveMember::Leave => {
if let Some(cid) = &channels.user_left {
Some((cid.clone(), SystemMessage::UserLeft { id: id.to_string() }))
} else {
None
}
}
RemoveMember::Kick => {
if let Some(cid) = &channels.user_kicked {
Some((
cid.clone(),
SystemMessage::UserKicked { id: id.to_string() },
))
} else {
None
}
}
RemoveMember::Ban => {
if let Some(cid) = &channels.user_banned {
Some((
cid.clone(),
SystemMessage::UserBanned { id: id.to_string() },
))
} else {
None
}
}
};
if let Some((cid, message)) = message {
let channel = Ref::from_unchecked(cid).fetch_channel().await?;
Content::SystemMessage(message)
.send_as_system(&channel)
.await?;
}
}
}
Ok(())
}
pub async fn get_member_count(id: &str) -> Result<i64> {
Ok(get_collection("server_members")
.count_documents(
doc! {
"_id.server": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "count_documents",
with: "server_members",
})?)
} }
} }
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
pub type UserSettings = HashMap<String, (i64, String)>; pub type UserSettings = HashMap<String, (i64, String)>;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ChannelCompositeKey {
pub channel: String,
pub user: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ChannelUnread {
#[serde(rename = "_id")]
pub id: ChannelCompositeKey,
pub last_id: Option<String>,
pub mentions: Option<Vec<String>>,
}
...@@ -28,14 +28,14 @@ pub enum RelationshipStatus { ...@@ -28,14 +28,14 @@ pub enum RelationshipStatus {
BlockedOther, BlockedOther,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Relationship { pub struct Relationship {
#[serde(rename = "_id")] #[serde(rename = "_id")]
pub id: String, pub id: String,
pub status: RelationshipStatus, pub status: RelationshipStatus,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum Presence { pub enum Presence {
Online, Online,
Idle, Idle,
...@@ -43,7 +43,7 @@ pub enum Presence { ...@@ -43,7 +43,7 @@ pub enum Presence {
Invisible, Invisible,
} }
#[derive(Validate, Serialize, Deserialize, Debug)] #[derive(Validate, Serialize, Deserialize, Debug, Clone)]
pub struct UserStatus { pub struct UserStatus {
#[validate(length(min = 1, max = 128))] #[validate(length(min = 1, max = 128))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
...@@ -52,7 +52,7 @@ pub struct UserStatus { ...@@ -52,7 +52,7 @@ pub struct UserStatus {
pub presence: Option<Presence>, pub presence: Option<Presence>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserProfile { pub struct UserProfile {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>, pub content: Option<String>,
...@@ -66,13 +66,15 @@ pub enum Badges { ...@@ -66,13 +66,15 @@ pub enum Badges {
Developer = 1, Developer = 1,
Translator = 2, Translator = 2,
Supporter = 4, Supporter = 4,
ResponsibleDisclosure = 8,
RevoltTeam = 16,
EarlyAdopter = 256, EarlyAdopter = 256,
} }
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 }); impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
// When changing this struct, update notifications/payload.rs#80 // When changing this struct, update notifications/payload.rs#80
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct User { pub struct User {
#[serde(rename = "_id")] #[serde(rename = "_id")]
pub id: String, pub id: String,
...@@ -157,7 +159,7 @@ impl User { ...@@ -157,7 +159,7 @@ impl User {
/// Utility function for checking claimed usernames. /// Utility function for checking claimed usernames.
pub async fn is_username_taken(username: &str) -> Result<bool> { pub async fn is_username_taken(username: &str) -> Result<bool> {
if username.to_lowercase() == "revolt" && username.to_lowercase() == "admin" { if username.to_lowercase() == "revolt" || username.to_lowercase() == "admin" || username.to_lowercase() == "system" {
return Ok(true); return Ok(true);
} }
...@@ -184,6 +186,7 @@ impl User { ...@@ -184,6 +186,7 @@ impl User {
} }
/// Utility function for fetching multiple users from the perspective of one. /// Utility function for fetching multiple users from the perspective of one.
/// Assumes user has a mutual connection with others.
pub async fn fetch_multiple_users(&self, user_ids: Vec<String>) -> Result<Vec<User>> { pub async fn fetch_multiple_users(&self, user_ids: Vec<String>) -> Result<Vec<User>> {
let mut users = vec![]; let mut users = vec![];
let mut cursor = get_collection("users") let mut cursor = get_collection("users")
...@@ -225,12 +228,36 @@ impl User { ...@@ -225,12 +228,36 @@ impl User {
Ok(users) Ok(users)
} }
/// Utility function to get all of a user's memberships.
pub async fn fetch_memberships(id: &str) -> Result<Vec<Member>> {
Ok(get_collection("server_members")
.find(
doc! {
"_id.user": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_members",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| {
from_document(x).ok()
})
.collect::<Vec<Member>>())
}
/// Utility function to get all the server IDs the user is in. /// Utility function to get all the server IDs the user is in.
pub async fn fetch_server_ids(&self) -> Result<Vec<String>> { pub async fn fetch_server_ids(id: &str) -> Result<Vec<String>> {
Ok(get_collection("server_members") Ok(get_collection("server_members")
.find( .find(
doc! { doc! {
"_id.user": &self.id "_id.user": id
}, },
None, None,
) )
...@@ -251,4 +278,23 @@ impl User { ...@@ -251,4 +278,23 @@ impl User {
.flatten() .flatten()
.collect::<Vec<String>>()) .collect::<Vec<String>>())
} }
/// Utility function to fetch unread objects for user.
pub async fn fetch_unreads(id: &str) -> Result<Vec<Document>> {
Ok(get_collection("channel_unreads")
.find(
doc! {
"_id.user": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user_settings",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await)
}
} }
...@@ -9,16 +9,23 @@ use validator::Validate; ...@@ -9,16 +9,23 @@ use validator::Validate;
#[derive(Validate, Serialize, Deserialize)] #[derive(Validate, Serialize, Deserialize)]
pub struct Ref { pub struct Ref {
#[validate(length(min = 26, max = 26))] #[validate(length(min = 1, max = 26))]
pub id: String, pub id: String,
} }
impl Ref { impl Ref {
pub fn from_unchecked(id: String) -> Ref {
Ref { id }
}
pub fn from(id: String) -> Result<Ref> { pub fn from(id: String) -> Result<Ref> {
Ok(Ref { id }) let r = Ref { id };
r.validate()
.map_err(|error| Error::FailedValidation { error })?;
Ok(r)
} }
pub async fn fetch<T: DeserializeOwned>(&self, collection: &'static str) -> Result<T> { async fn fetch<T: DeserializeOwned>(&self, collection: &'static str) -> Result<T> {
let doc = get_collection(&collection) let doc = get_collection(&collection)
.find_one( .find_one(
doc! { doc! {
...@@ -31,7 +38,7 @@ impl Ref { ...@@ -31,7 +38,7 @@ impl Ref {
operation: "find_one", operation: "find_one",
with: &collection, with: &collection,
})? })?
.ok_or_else(|| Error::UnknownUser)?; .ok_or_else(|| Error::NotFound)?;
Ok(from_document::<T>(doc).map_err(|_| Error::DatabaseError { Ok(from_document::<T>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document", operation: "from_document",
...@@ -51,6 +58,56 @@ impl Ref { ...@@ -51,6 +58,56 @@ impl Ref {
self.fetch("servers").await self.fetch("servers").await
} }
pub async fn fetch_invite(&self) -> Result<Invite> {
self.fetch("channel_invites").await
}
pub async fn fetch_member(&self, server: &str) -> Result<Member> {
let doc = get_collection("server_members")
.find_one(
doc! {
"_id.user": &self.id,
"_id.server": server
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server_member",
})?
.ok_or_else(|| Error::NotFound)?;
Ok(
from_document::<Member>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "server_member",
})?,
)
}
pub async fn fetch_ban(&self, server: &str) -> Result<Ban> {
let doc = get_collection("server_bans")
.find_one(
doc! {
"_id.user": &self.id,
"_id.server": server
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server_ban",
})?
.ok_or_else(|| Error::NotFound)?;
Ok(from_document::<Ban>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "server_ban",
})?)
}
pub async fn fetch_message(&self, channel: &Channel) -> Result<Message> { pub async fn fetch_message(&self, channel: &Channel) -> Result<Message> {
let message: Message = self.fetch("messages").await?; let message: Message = self.fetch("messages").await?;
if &message.channel != channel.id() { if &message.channel != channel.id() {
......
...@@ -37,9 +37,13 @@ pub async fn create_database() { ...@@ -37,9 +37,13 @@ pub async fn create_database() {
.await .await
.expect("Failed to create server_bans collection."); .expect("Failed to create server_bans collection.");
db.create_collection("invites", None) db.create_collection("channel_invites", None)
.await .await
.expect("Failed to create invites collection."); .expect("Failed to create channel_invites collection.");
db.create_collection("channel_unreads", None)
.await
.expect("Failed to create channel_unreads collection.");
db.create_collection("migrations", None) db.create_collection("migrations", None)
.await .await
...@@ -49,10 +53,6 @@ pub async fn create_database() { ...@@ -49,10 +53,6 @@ pub async fn create_database() {
.await .await
.expect("Failed to create attachments collection."); .expect("Failed to create attachments collection.");
db.create_collection("channel_unreads", None)
.await
.expect("Failed to create channel_unreads collection.");
db.create_collection("user_settings", None) db.create_collection("user_settings", None)
.await .await
.expect("Failed to create user_settings collection."); .expect("Failed to create user_settings collection.");
...@@ -122,6 +122,23 @@ pub async fn create_database() { ...@@ -122,6 +122,23 @@ pub async fn create_database() {
.await .await
.expect("Failed to create username index."); .expect("Failed to create username index.");
db.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
db.collection("migrations") db.collection("migrations")
.insert_one( .insert_one(
doc! { doc! {
......
use crate::database::{get_collection, get_db}; use crate::database::{permissions, get_collection, get_db, PermissionTuple};
use futures::StreamExt; use futures::StreamExt;
use log::info; use log::info;
use mongodb::{ use mongodb::{bson::{doc, from_document, to_document}, options::FindOptions};
bson::{doc, from_document},
options::FindOptions,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
...@@ -14,7 +11,7 @@ struct MigrationInfo { ...@@ -14,7 +11,7 @@ struct MigrationInfo {
revision: i32, revision: i32,
} }
pub const LATEST_REVISION: i32 = 5; pub const LATEST_REVISION: i32 = 7;
pub async fn migrate_database() { pub async fn migrate_database() {
let migrations = get_collection("migrations"); let migrations = get_collection("migrations");
...@@ -152,9 +149,58 @@ pub async fn run_migrations(revision: i32) -> i32 { ...@@ -152,9 +149,58 @@ pub async fn run_migrations(revision: i32) -> i32 {
.expect("Failed to create server_bans collection."); .expect("Failed to create server_bans collection.");
get_db() get_db()
.create_collection("invites", None) .create_collection("channel_invites", None)
.await .await
.expect("Failed to create invites collection."); .expect("Failed to create channel_invites collection.");
}
if revision <= 5 {
info!("Running migration [revision 5 / 2021-06-26]: Add permissions.");
#[derive(Serialize)]
struct Server {
pub default_permissions: PermissionTuple,
}
let server = Server {
default_permissions: (
*permissions::server::DEFAULT_PERMISSION as i32,
*permissions::channel::DEFAULT_PERMISSION_SERVER as i32
)
};
get_collection("servers")
.update_many(
doc! { },
doc! {
"$set": to_document(&server).unwrap()
},
None
)
.await
.expect("Failed to migrate servers.");
}
if revision <= 6 {
info!("Running migration [revision 6 / 2021-07-09]: Add message text index.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
} }
// Reminder to update LATEST_REVISION when adding new migrations. // Reminder to update LATEST_REVISION when adding new migrations.
......