chore(thanos): strip down codebase to just API routes

This commit is contained in:
Paul Makles
2022-01-27 14:16:30 +00:00
parent ab4c3f913e
commit 0fdb749199
116 changed files with 893 additions and 10025 deletions
-20
View File
@@ -1,20 +0,0 @@
use serde::{Serialize, Deserialize};
pub fn if_false(t: &bool) -> bool {
!t
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Bot {
#[serde(rename = "_id")]
pub id: String,
pub owner: String,
pub token: String,
pub public: bool,
#[serde(skip_serializing_if = "if_false", default)]
pub analytics: bool,
#[serde(skip_serializing_if = "if_false", default)]
pub discoverable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub interactions_url: Option<String>,
}
-392
View File
@@ -1,392 +0,0 @@
use std::collections::HashMap;
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_GROUP_SIZE;
use futures::StreamExt;
use mongodb::bson::Bson;
use mongodb::{
bson::{doc, to_document, Document},
options::FindOptions,
};
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "channel_type")]
pub enum Channel {
SavedMessages {
#[serde(rename = "_id")]
id: String,
user: String,
},
DirectMessage {
#[serde(rename = "_id")]
id: String,
active: bool,
recipients: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
},
Group {
#[serde(rename = "_id")]
id: String,
name: String,
owner: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
recipients: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
permissions: Option<i32>,
#[serde(skip_serializing_if = "entities::server::if_false", default)]
nsfw: bool
},
TextChannel {
#[serde(rename = "_id")]
id: String,
server: 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")]
last_message_id: 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>,
#[serde(skip_serializing_if = "entities::server::if_false", default)]
nsfw: bool
},
VoiceChannel {
#[serde(rename = "_id")]
id: String,
server: 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>,
#[serde(skip_serializing_if = "entities::server::if_false", default)]
nsfw: bool
},
}
impl Channel {
pub fn id(&self) -> &str {
match self {
Channel::SavedMessages { id, .. }
| Channel::DirectMessage { id, .. }
| Channel::Group { id, .. }
| Channel::TextChannel { id, .. }
| Channel::VoiceChannel { id, .. } => id,
}
}
pub fn has_messaging(&self) -> Result<()> {
match self {
Channel::SavedMessages { .. }
| Channel::DirectMessage { .. }
| Channel::Group { .. }
| Channel::TextChannel { .. } => Ok(()),
Channel::VoiceChannel { .. } => Err(Error::InvalidOperation)
}
}
pub async fn publish(self) -> Result<()> {
get_collection("channels")
.insert_one(
to_document(&self).map_err(|_| Error::DatabaseError {
operation: "to_bson",
with: "channel",
})?,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "channel",
})?;
let channel_id = self.id().to_string();
ClientboundNotification::ChannelCreate(self).publish(channel_id);
Ok(())
}
pub async fn publish_update(&self, data: Value) -> Result<()> {
let id = self.id().to_string();
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data,
clear: None,
}
.publish(id);
Ok(())
}
pub async fn delete_associated_objects(id: Bson) -> Result<()> {
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");
// 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.
let message_ids = messages
.find(
doc! {
"channel": &id,
"attachment": {
"$exists": 1
}
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "fetch_many",
with: "messages",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
// If we found any, mark them as deleted.
if message_ids.len() > 0 {
get_collection("attachments")
.update_many(
doc! {
"message_id": {
"$in": message_ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "attachments",
})?;
}
// And then delete said messages.
messages
.delete_many(
doc! {
"channel": id
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
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")
.delete_one(
doc! {
"_id": id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "channel",
})?;
ClientboundNotification::ChannelDelete { id: id.to_string() }.publish(id.to_string());
if let Channel::Group { icon, .. } = self {
if let Some(attachment) = icon {
attachment.delete().await?;
}
}
Ok(())
}
pub async fn add_to_group(&self, member: String, by_user: String) -> Result<()> {
if let Channel::Group { id, recipients, .. } = &self {
if recipients.len() >= *MAX_GROUP_SIZE {
Err(Error::GroupTooLarge {
max: *MAX_GROUP_SIZE,
})?
}
if recipients.iter().find(|x| *x == &member).is_some() {
Err(Error::AlreadyInGroup)?
}
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$push": {
"recipients": &member
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
ClientboundNotification::ChannelGroupJoin {
id: id.clone(),
user: member.clone(),
}
.publish(id.clone());
Content::SystemMessage(SystemMessage::UserAdded {
id: member,
by: by_user,
})
.send_as_system(&self)
.await
.ok();
Ok(())
} else {
Err(Error::InvalidOperation)
}
}
}
-96
View File
@@ -1,96 +0,0 @@
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(())
}
}
-363
View File
@@ -1,363 +0,0 @@
use crate::util::variables::{USE_JANUARY, PUBLIC_URL, APP_URL};
use crate::{
database::*,
notifications::{events::ClientboundNotification, websocket::is_online},
util::result::{Error, Result},
};
use mongodb::bson::{doc, to_bson, DateTime};
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
use std::collections::HashSet;
use std::time::SystemTime;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PushNotification {
pub author: String,
pub icon: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
pub body: String,
pub tag: String,
pub timestamp: u64,
pub url: String,
}
impl PushNotification {
pub async fn new(msg: Message, channel: &Channel) -> Self {
let author = Ref::from_unchecked(msg.author.clone())
.fetch_user()
.await;
let (author, avatar) = if let Ok(author) = author {
(Some(author.username), author.avatar)
} else {
(None, None)
};
let icon = if let Some(avatar) = avatar {
avatar.get_autumn_url()
} else {
format!("{}/users/{}/default_avatar", PUBLIC_URL.as_str(), msg.author)
};
let image = msg.attachments.map_or(None, |attachments| {
attachments
.first()
.map_or(None, |v| Some(v.get_autumn_url()))
});
let body = match msg.content {
Content::Text(body) => body,
Content::SystemMessage(sys_msg) => sys_msg.into()
};
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time should be valid")
.as_secs();
Self {
author: author.unwrap_or_else(|| "Unknown".into()),
icon,
image,
body,
tag: channel.id().to_string(),
timestamp,
url: format!("{}/channel/{}/{}", *APP_URL, channel.id(), msg.id),
}
}
}
#[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_joined")]
UserJoined { id: String },
#[serde(rename = "user_left")]
UserLeft { id: String },
#[serde(rename = "user_kicked")]
UserKicked { id: String },
#[serde(rename = "user_banned")]
UserBanned { id: String },
#[serde(rename = "channel_renamed")]
ChannelRenamed { name: String, by: String },
#[serde(rename = "channel_description_changed")]
ChannelDescriptionChanged { by: String },
#[serde(rename = "channel_icon_changed")]
ChannelIconChanged { by: String },
}
impl Into<String> for SystemMessage {
fn into(self) -> String {
match self {
SystemMessage::Text { content } => content,
SystemMessage::UserAdded { .. } => "User added to the channel.".to_string(),
SystemMessage::UserRemove { .. } => "User removed from the channel.".to_string(),
SystemMessage::UserJoined { .. } => "User joined the channel.".to_string(),
SystemMessage::UserLeft { .. } => "User left the channel.".to_string(),
SystemMessage::UserKicked { .. } => "User kicked from the channel.".to_string(),
SystemMessage::UserBanned { .. } => "User banned from the channel.".to_string(),
SystemMessage::ChannelRenamed { .. } => "Channel renamed.".to_string(),
SystemMessage::ChannelDescriptionChanged { .. } => "Channel description changed.".to_string(),
SystemMessage::ChannelIconChanged { .. } => "Channel icon changed.".to_string()
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Content {
Text(String),
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,
None
)
.publish(&target, false)
.await
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Validate)]
pub struct Masquerade {
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 32))]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 1, max = 128))]
avatar: Option<String>
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Message {
#[serde(rename = "_id")]
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<String>,
pub channel: String,
pub author: String,
pub content: Content,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<File>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edited: Option<DateTime>,
#[serde(skip_serializing_if = "Option::is_none")]
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>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub masquerade: Option<Masquerade>
}
impl Message {
pub fn create(
author: String,
channel: String,
content: Content,
mentions: Option<Vec<String>>,
replies: Option<Vec<String>>,
masquerade: Option<Masquerade>,
) -> Message {
Message {
id: Ulid::new().to_string(),
nonce: None,
channel,
author,
content,
attachments: None,
edited: None,
embeds: None,
mentions,
replies,
masquerade
}
}
pub async fn publish(self, channel: &Channel, process_embeds: bool) -> Result<()> {
// Publish message event
ClientboundNotification::Message(self.clone())
.publish(channel.id().to_string());
// Commit message to database
get_collection("messages")
.insert_one(to_bson(&self).unwrap().as_document().unwrap().clone(), None)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "message",
})?;
// spawn task_queue ( process embeds )
if process_embeds {
self.process_embed().await;
}
// spawn task_queue ( update last_message_id )
match channel {
Channel::DirectMessage { id, .. } =>
crate::task_queue::task_last_message_id::queue(id.clone(), self.id.clone(), true).await,
Channel::Group { id, .. } | Channel::TextChannel { id, .. } =>
crate::task_queue::task_last_message_id::queue(id.clone(), self.id.clone(), false).await,
_ => {}
}
// if mentions {
// spawn task_queue ( update channel_unreads )
// }
if let Some(mentions) = &self.mentions {
for user in mentions {
crate::task_queue::task_ack::queue(
channel.id().into(),
user.clone(),
crate::task_queue::task_ack::AckEvent::AddMention {
ids: vec![ self.id.clone() ]
}
).await;
}
}
// if (channel => DM | Group) | mentions {
// spawn task_queue ( web push )
// }
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());
}
}
}
Channel::TextChannel { .. } => {
if let Some(mentions) = &self.mentions {
target_ids.append(&mut mentions.clone());
}
}
_ => {}
}
if target_ids.len() > 0 {
if let Ok(payload) = serde_json::to_string(&PushNotification::new(self, &channel).await) {
crate::task_queue::task_web_push::queue(target_ids, payload).await;
}
}
Ok(())
}
pub async fn publish_update(self, data: Value) -> Result<()> {
let channel = self.channel.clone();
ClientboundNotification::MessageUpdate {
id: self.id.clone(),
channel: self.channel.clone(),
data,
}
.publish(channel);
self.process_embed().await;
Ok(())
}
pub async fn process_embed(&self) {
if !*USE_JANUARY {
return;
}
if let Content::Text(text) = &self.content {
crate::task_queue::task_process_embeds::queue(self.channel.clone(), self.id.clone(), text.clone()).await;
}
}
pub async fn delete(&self) -> Result<()> {
if let Some(attachments) = &self.attachments {
for attachment in attachments {
attachment.delete().await?;
}
}
get_collection("messages")
.delete_one(
doc! {
"_id": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "message",
})?;
let channel = self.channel.clone();
ClientboundNotification::MessageDelete {
id: self.id.clone(),
channel: self.channel.clone(),
}
.publish(channel);
if let Some(attachments) = &self.attachments {
let attachment_ids: Vec<String> =
attachments.iter().map(|f| f.id.to_string()).collect();
get_collection("attachments")
.update_many(
doc! {
"_id": {
"$in": attachment_ids
}
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "attachment",
})?;
}
Ok(())
}
pub fn add_associated_user_ids(&self, ids: &mut HashSet<String>) {
ids.insert(self.author.clone());
if let Content::SystemMessage(sys) = &self.content {
match sys {
SystemMessage::Text { .. } => {},
SystemMessage::UserAdded { id, by } => { ids.insert(id.clone()); ids.insert(by.clone()); },
SystemMessage::UserRemove { id, by } => { ids.insert(id.clone()); ids.insert(by.clone()); },
SystemMessage::UserJoined { id } => { ids.insert(id.clone()); },
SystemMessage::UserLeft { id } => { ids.insert(id.clone()); },
SystemMessage::UserKicked { id } => { ids.insert(id.clone()); },
SystemMessage::UserBanned { id } => { ids.insert(id.clone()); },
SystemMessage::ChannelRenamed { by, .. } => { ids.insert(by.clone()); },
SystemMessage::ChannelDescriptionChanged { by } => { ids.insert(by.clone()); },
SystemMessage::ChannelIconChanged { by } => { ids.insert(by.clone()); }
}
}
}
}
@@ -1,122 +0,0 @@
use mongodb::bson::{doc, from_document};
use serde::{Deserialize, Serialize};
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::AUTUMN_URL;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
enum Metadata {
File,
Text,
Image { width: isize, height: isize },
Video { width: isize, height: isize },
Audio,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct File {
#[serde(rename = "_id")]
pub id: String,
tag: String,
filename: String,
metadata: Metadata,
content_type: String,
size: isize,
#[serde(skip_serializing_if = "Option::is_none")]
deleted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
reported: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
server_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
object_id: Option<String>,
}
impl File {
pub async fn find_and_use(
attachment_id: &str,
tag: &str,
parent_type: &str,
parent_id: &str,
) -> Result<File> {
let attachments = get_collection("attachments");
let key = format!("{}_id", parent_type);
if let Some(doc) = attachments
.find_one(
doc! {
"_id": attachment_id,
"tag": &tag,
key.clone(): {
"$exists": false
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "attachment",
})?
{
let attachment = from_document::<File>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "attachment",
})?;
attachments
.update_one(
doc! {
"_id": &attachment.id
},
doc! {
"$set": {
key: &parent_id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})?;
Ok(attachment)
} else {
Err(Error::UnknownAttachment)
}
}
pub async fn delete(&self) -> Result<()> {
get_collection("attachments")
.update_one(
doc! {
"_id": &self.id
},
doc! {
"$set": {
"deleted": true
}
},
None,
)
.await
.map(|_| ())
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "attachment",
})
}
pub fn get_autumn_url(&self) -> String {
format!("{}/{}/{}", AUTUMN_URL.as_str(), self.tag, self.id)
}
}
@@ -1,200 +0,0 @@
use crate::util::{
result::{Error, Result},
variables::JANUARY_URL,
variables::MAX_EMBED_COUNT,
};
use crate::database::entities::microservice::autumn::File;
use linkify::{LinkFinder, LinkKind};
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ImageSize {
Large,
Preview,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Image {
pub url: String,
pub width: isize,
pub height: isize,
pub size: ImageSize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Video {
pub url: String,
pub width: isize,
pub height: isize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum TwitchType {
Channel,
Video,
Clip,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum BandcampType {
Album,
Track,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Special {
None,
YouTube {
id: String,
#[serde(skip_serializing_if = "Option::is_none")]
timestamp: Option<String>,
},
Twitch {
content_type: TwitchType,
id: String,
},
Spotify {
content_type: String,
id: String,
},
Soundcloud,
Bandcamp {
content_type: BandcampType,
id: String,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Metadata {
#[serde(skip_serializing_if = "Option::is_none")]
url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
special: Option<Special>,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<Image>,
#[serde(skip_serializing_if = "Option::is_none")]
video: Option<Video>,
// #[serde(skip_serializing_if = "Option::is_none")]
// opengraph_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
site_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
colour: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Text {
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub colour: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Embed {
Website(Metadata),
Image(Image),
Text(Text),
None,
}
impl Embed {
pub async fn generate(content: String) -> Result<Vec<Embed>> {
lazy_static! {
static ref RE_CODE: Regex = Regex::new("```(?:.|\n)+?```|`(?:.|\n)+?`").unwrap();
static ref RE_IGNORED: Regex = Regex::new("(<http.+>)").unwrap();
}
// Ignore code blocks.
let content = RE_CODE.replace_all(&content, "");
// Ignore all content between angle brackets starting with http.
let content = RE_IGNORED.replace_all(&content, "");
let content = content
// Ignore quoted lines.
.split("\n")
.map(|v| {
if let Some(c) = v.chars().next() {
if c == '>' {
return "";
}
}
v
})
.collect::<Vec<&str>>()
.join("\n");
let mut finder = LinkFinder::new();
finder.kinds(&[LinkKind::Url]);
let links: Vec<_> = finder.links(&content).take(*MAX_EMBED_COUNT).collect();
if links.len() == 0 {
return Err(Error::LabelMe);
}
let mut embeds: Vec<Embed> = Vec::new();
let mut link_index = 0;
// ! FIXME: batch request to january?
while link_index < links.len() {
let link = &links[link_index];
// Check if we already processed this link.
if link_index != 0 && links.iter().take(link_index).any(|x| x.as_str() == link.as_str()) {
link_index = link_index + 1;
continue;
}
let client = reqwest::Client::new();
let result = client
.get(&format!("{}/embed", *JANUARY_URL))
.query(&[("url", link.as_str())])
.send()
.await;
if result.is_err() {
link_index = link_index + 1;
continue;
}
let response = result.unwrap();
if response.status().is_success() {
let res: Embed = response.json().await.map_err(|_| Error::InvalidOperation)?;
embeds.push(res);
}
link_index = link_index + 1;
}
// Prevent database update when no embeds are found.
if embeds.len() > 0 {
Ok(embeds)
} else {
Err(Error::LabelMe)
}
}
}
@@ -1,2 +0,0 @@
pub mod autumn;
pub mod january;
-20
View File
@@ -1,20 +0,0 @@
mod channel;
mod invites;
mod message;
mod microservice;
mod server;
mod sync;
mod user;
mod bots;
use microservice::*;
pub use autumn::*;
pub use channel::*;
pub use invites::*;
pub use january::*;
pub use message::*;
pub use server::*;
pub use sync::*;
pub use user::*;
pub use bots::*;
-470
View File
@@ -1,470 +0,0 @@
use std::collections::HashMap;
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{Bson, doc};
use mongodb::bson::from_document;
use mongodb::bson::to_document;
use mongodb::bson::Document;
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MemberCompositeKey {
pub server: String,
pub user: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Member {
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
#[serde(skip_serializing_if = "Option::is_none")]
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
);
pub fn if_false(t: &bool) -> bool {
!t
}
#[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>,
#[serde(skip_serializing_if = "if_false", default)]
pub hoist: bool,
#[serde(default)]
pub rank: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Category {
pub id: String,
pub title: String,
pub channels: Vec<String>
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Ban {
#[serde(rename = "_id")]
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)]
pub struct Server {
#[serde(rename = "_id")]
pub id: String,
pub owner: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<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")]
pub icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub banner: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
#[serde(skip_serializing_if = "if_false", default)]
pub nsfw: bool,
#[serde(skip_serializing_if = "if_false", default)]
pub analytics: bool,
#[serde(skip_serializing_if = "if_false", default)]
pub discoverable: bool
}
impl Server {
pub async fn create(self) -> Result<()> {
get_collection("servers")
.insert_one(
to_document(&self).map_err(|_| Error::DatabaseError {
operation: "to_bson",
with: "channel",
})?,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "server",
})?;
Ok(())
}
pub async fn publish_update(&self, data: Value) -> Result<()> {
ClientboundNotification::ServerUpdate {
id: self.id.clone(),
data,
clear: None,
}
.publish(self.id.clone());
Ok(())
}
pub async fn delete(&self) -> Result<()> {
// 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_members_with_ids(id: &str, ids: &Vec<String>) -> Result<Vec<Member>> {
Ok(get_collection("server_members")
.find(
doc! {
"_id.server": id,
"_id.user": {
"$in": ids
}
},
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 mark_as_read(&self, id: &str) -> Result<()> {
let current_time = Ulid::new().to_string();
let unreads = get_collection("channel_unreads");
unreads.delete_many(
doc! {
"_id.channel": {
"$in": &self.channels
},
"_id.user": &id
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_many",
with: "channel_unreads",
})?;
unreads.insert_many(
self.channels
.iter()
.map(|channel| doc! {
"_id": {
"channel": channel,
"user": &id
},
"last_id": &current_time
})
.collect::<Vec<Document>>(),
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "channel_unreads",
})
.map(|_| ())
}
pub async fn join_member(&self, id: &str) -> Result<()> {
// Check if user is banned.
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);
}
// Add user to server.
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",
})?;
// Announce that user joined server.
ClientboundNotification::ServerMemberJoin {
id: self.id.clone(),
user: id.to_string(),
}
.publish(self.id.clone());
// Broadcast join message.
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?;
}
}
// Mark entire server as read.
self.mark_as_read(&id).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",
})?)
}
}
-19
View File
@@ -1,19 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
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>>,
}
-337
View File
@@ -1,337 +0,0 @@
use futures::StreamExt;
use mongodb::bson::Document;
use mongodb::options::{Collation, FindOneOptions};
use mongodb::{
bson::{doc, from_document},
options::FindOptions,
};
use num_enum::TryFromPrimitive;
use serde::{Deserialize, Serialize};
use std::ops;
use ulid::Ulid;
use validator::Validate;
use crate::database::permissions::user::UserPermissions;
use crate::database::*;
use crate::notifications::websocket::is_online;
use crate::util::result::{Error, Result};
use crate::util::variables::EARLY_ADOPTER_BADGE;
use crate::util::variables::MAX_SERVER_COUNT;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum RelationshipStatus {
None,
User,
Friend,
Outgoing,
Incoming,
Blocked,
BlockedOther,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Relationship {
#[serde(rename = "_id")]
pub id: String,
pub status: RelationshipStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum Presence {
Online,
Idle,
Busy,
Invisible,
}
#[derive(Validate, Serialize, Deserialize, Debug, Clone)]
pub struct UserStatus {
#[validate(length(min = 1, max = 128))]
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub presence: Option<Presence>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserProfile {
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<File>,
}
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(i32)]
pub enum Badges {
Developer = 1,
Translator = 2,
Supporter = 4,
ResponsibleDisclosure = 8,
Founder = 16,
PlatformModeration = 32,
ActiveSupporter = 64,
Paw = 128,
EarlyAdopter = 256,
ReservedRelevantJokeBadge1 = 512
}
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct BotInformation {
owner: String
}
// When changing this struct, update notifications/payload.rs#113
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct User {
#[serde(rename = "_id")]
pub id: String,
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bot: Option<BotInformation>,
// ? This should never be pushed to the collection.
#[serde(skip_serializing_if = "Option::is_none")]
pub relationship: Option<RelationshipStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub online: Option<bool>,
}
impl User {
/// Mutate the user object to include relationship as seen by user.
pub fn from(mut self, user: &User) -> User {
self.relationship = Some(RelationshipStatus::None);
if self.id == user.id {
self.relationship = Some(RelationshipStatus::User);
return self;
}
self.relations = None;
if let Some(relations) = &user.relations {
if let Some(relationship) = relations.iter().find(|x| self.id == x.id) {
self.relationship = Some(relationship.status.clone());
return self;
}
}
self
}
/// Apply any relevant badges.
pub fn apply_badges(mut self) -> User {
let mut badges = self.badges.unwrap_or_else(|| 0);
if let Ok(id) = Ulid::from_string(&self.id) {
if id.datetime().timestamp_millis() < *EARLY_ADOPTER_BADGE {
badges = badges + Badges::EarlyAdopter;
}
}
self.badges = Some(badges);
self
}
/// Mutate the user object to appear as seen by user.
pub fn with(self, permissions: UserPermissions<[u32; 1]>) -> User {
let mut user = self.apply_badges();
if permissions.get_view_profile() {
user.online = Some(is_online(&user.id));
} else {
user.status = None;
}
// If the user's status is `Presence::Invisible`, return it as `Presence::Offline`
if let Some(status) = &user.status {
if let Some(presence) = &status.presence {
if presence == &Presence::Invisible {
user.status = None;
user.online = Some(false);
}
}
}
user.profile = None;
user
}
/// Mutate the user object to appear as seen by user.
/// Also overrides the relationship status.
pub async fn from_override(
mut self,
user: &User,
relationship: RelationshipStatus,
) -> Result<User> {
let permissions = PermissionCalculator::new(&user)
.with_relationship(&relationship)
.for_user(&self.id)
.await?;
self.relations = None;
self.relationship = Some(relationship);
Ok(self.with(permissions))
}
/// Utility function for checking claimed usernames.
pub async fn is_username_taken(username: &str) -> Result<bool> {
if username.to_lowercase() == "revolt" || username.to_lowercase() == "admin" || username.to_lowercase() == "system" {
return Ok(true);
}
if get_collection("users")
.find_one(
doc! {
"username": username
},
FindOneOptions::builder()
.collation(Collation::builder().locale("en").strength(2).build())
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user",
})?
.is_some()
{
Ok(true)
} else {
Ok(false)
}
}
/// Utility function for fetching multiple users from the perspective of one.
/// Assumes user has a mutual connection with others.
pub async fn fetch_multiple_users(&self, user_ids: &Vec<String>) -> Result<Vec<User>> {
let mut users = vec![];
let mut cursor = get_collection("users")
.find(
doc! {
"_id": {
"$in": user_ids
}
},
FindOptions::builder()
.projection(
doc! { "_id": 1, "username": 1, "avatar": 1, "badges": 1, "status": 1, "flags": 1, "bot": 1 },
)
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let other: User = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "user",
})?;
let permissions = PermissionCalculator::new(&self)
.with_mutual_connection()
.with_user(&other)
.for_user_given()
.await?;
users.push(other.from(&self).with(permissions));
}
}
Ok(users)
}
/// 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.
pub async fn fetch_server_ids(id: &str) -> Result<Vec<String>> {
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| {
x.get_document("_id")
.ok()
.map(|i| i.get_str("server").ok().map(|x| x.to_string()))
})
.flatten()
.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)
}
/// Check if this user can acquire another server.
pub async fn can_acquire_server(id: &str) -> Result<bool> {
let server_ids = User::fetch_server_ids(&id).await?;
Ok(server_ids.len() < *MAX_SERVER_COUNT)
}
}
-4
View File
@@ -1,4 +0,0 @@
pub mod reference;
pub mod user;
pub use reference::Ref;
-162
View File
@@ -1,162 +0,0 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, from_document};
use rocket::request::FromParam;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Ref {
#[validate(length(min = 1, max = 26))]
pub id: String,
}
impl Ref {
pub fn from_unchecked(id: String) -> Ref {
Ref { id }
}
pub fn from(id: String) -> Result<Ref> {
let r = Ref { id };
r.validate()
.map_err(|error| Error::FailedValidation { error })?;
Ok(r)
}
async fn fetch<T: DeserializeOwned>(&self, collection: &'static str) -> Result<T> {
let doc = get_collection(&collection)
.find_one(
doc! {
"_id": &self.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: &collection,
})?
.ok_or_else(|| Error::NotFound)?;
Ok(from_document::<T>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: &collection,
})?)
}
pub async fn fetch_user(&self) -> Result<User> {
self.fetch("users").await
}
pub async fn fetch_channel(&self) -> Result<Channel> {
self.fetch("channels").await
}
pub async fn fetch_server(&self) -> Result<Server> {
self.fetch("servers").await
}
pub async fn fetch_invite(&self) -> Result<Invite> {
match self.fetch("channel_invites").await {
Ok(invite) => Ok(invite),
Err(err) => {
if let Ok(server) = self.fetch::<Server>("servers").await {
if server.discoverable {
return Ok(
Invite::Server {
code: server.id.clone(),
server: server.id,
creator: server.owner,
channel: server.channels[0].clone(),
}
)
}
}
Err(err)
}
}
}
pub async fn fetch_bot(&self) -> Result<Bot> {
self.fetch("bots").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> {
let message: Message = self.fetch("messages").await?;
if &message.channel != channel.id() {
Err(Error::InvalidOperation)
} else {
Ok(message)
}
}
}
impl User {
pub fn as_ref(&self) -> Ref {
Ref {
id: self.id.to_string(),
}
}
}
impl<'r> FromParam<'r> for Ref {
type Error = &'r str;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
if let Ok(result) = Ref::from(param.to_string()) {
if result.validate().is_ok() {
return Ok(result);
}
}
Err(param)
}
}
-81
View File
@@ -1,81 +0,0 @@
use crate::database::*;
use mongodb::bson::{doc, from_document};
use rauth::entities::Session;
use rocket::http::Status;
use rocket::request::{self, FromRequest, Outcome, Request};
#[rocket::async_trait]
impl<'r> FromRequest<'r> for User {
type Error = rauth::util::Error;
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let user: &Option<User> = request.local_cache_async(async {
let header_bot_token = request
.headers()
.get("x-bot-token")
.next()
.map(|x| x.to_string());
if let Some(bot_token) = header_bot_token {
if let Ok(result) = get_collection("bots")
.find_one(
doc! {
"token": bot_token
},
None,
)
.await
{
if let Some(doc) = result {
let id = doc.get_str("_id").unwrap();
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &id
},
None,
)
.await
{
if let Some(doc) = result {
if let Ok(user) = from_document(doc) {
return Some(user)
}
}
}
}
}
} else {
if let Outcome::Success(session) = request.guard::<Session>().await {
if let Ok(result) = get_collection("users")
.find_one(
doc! {
"_id": &session.user_id
},
None,
)
.await
{
if let Some(doc) = result {
if let Ok(user) = from_document(doc) {
return Some(user)
}
}
}
}
}
None
}).await;
if let Some(user) = user {
Outcome::Success(user.clone())
} else {
Outcome::Failure((
Status::Forbidden,
rauth::util::Error::InvalidSession,
))
}
}
}
-188
View File
@@ -1,188 +0,0 @@
use super::super::get_db;
use super::scripts::LATEST_REVISION;
use log::info;
use mongodb::bson::doc;
use mongodb::options::CreateCollectionOptions;
pub async fn create_database() {
info!("Creating database.");
let db = get_db();
db.create_collection("accounts", None)
.await
.expect("Failed to create accounts collection.");
db.create_collection("users", None)
.await
.expect("Failed to create users collection.");
db.create_collection("channels", None)
.await
.expect("Failed to create channels collection.");
db.create_collection("messages", None)
.await
.expect("Failed to create messages collection.");
db.create_collection("servers", None)
.await
.expect("Failed to create servers collection.");
db.create_collection("server_members", None)
.await
.expect("Failed to create server_members collection.");
db.create_collection("server_bans", None)
.await
.expect("Failed to create server_bans collection.");
db.create_collection("channel_invites", None)
.await
.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)
.await
.expect("Failed to create migrations collection.");
db.create_collection("attachments", None)
.await
.expect("Failed to create attachments collection.");
db.create_collection("user_settings", None)
.await
.expect("Failed to create user_settings collection.");
db.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
db.create_collection(
"pubsub",
CreateCollectionOptions::builder()
.capped(true)
.size(1_000_000)
.build(),
)
.await
.expect("Failed to create pubsub collection.");
db.run_command(
doc! {
"createIndexes": "users",
"indexes": [
{
"key": {
"username": 1
},
"name": "username",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
}
]
},
None,
)
.await
.expect("Failed to create username index.");
db.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"content": "text"
},
"name": "content"
},
{
"key": {
"channel": 1
},
"name": "channel"
},
{
"key": {
"channel": 1,
"_id": 1
},
"name": "channel_id_compound"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
get_db()
.run_command(
doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1,
"_id.user": 1,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create channel_unreads index.");
get_db()
.run_command(
doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1,
"_id.user": 1,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create server_members index.");
db.collection("migrations")
.insert_one(
doc! {
"_id": 0,
"revision": LATEST_REVISION
},
None,
)
.await
.expect("Failed to save migration info.");
info!("Created database.");
}
-22
View File
@@ -1,22 +0,0 @@
use super::get_connection;
pub mod init;
pub mod scripts;
pub async fn run_migrations() {
let client = get_connection();
let list = client
.list_database_names(None, None)
.await
.expect("Failed to fetch database names.");
if list.iter().position(|x| x == "revolt").is_none() {
init::create_database().await;
} else {
scripts::migrate_database().await;
}
// panic!("https://pbs.twimg.com/media/EDTpB5JWwAUvyxd.jpg");
rauth::entities::sync_models(&super::get_db()).await;
}
-474
View File
@@ -1,474 +0,0 @@
use crate::database::{permissions, get_collection, get_db, PermissionTuple};
use futures::StreamExt;
use log::info;
use mongodb::{bson::{Document, doc, from_bson, from_document, to_document}, options::FindOptions};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct MigrationInfo {
_id: i32,
revision: i32,
}
pub const LATEST_REVISION: i32 = 13;
pub async fn migrate_database() {
let migrations = get_collection("migrations");
let data = migrations
.find_one(None, None)
.await
.expect("Failed to fetch migration data.");
if let Some(doc) = data {
let info: MigrationInfo =
from_document(doc).expect("Failed to read migration information.");
let revision = run_migrations(info.revision).await;
migrations
.update_one(
doc! {
"_id": info._id
},
doc! {
"$set": {
"revision": revision
}
},
None,
)
.await
.expect("Failed to commit migration information.");
info!("Migration complete. Currently at revision {}.", revision);
} else {
panic!("Database was configured incorrectly, possibly because initalization failed.")
}
}
pub async fn run_migrations(revision: i32) -> i32 {
info!("Starting database migration.");
if revision <= 0 {
info!("Running migration [revision 0]: Test migration system.");
}
if revision <= 1 {
info!("Running migration [revision 1 / 2021-04-24]: Migrate to Autumn v1.0.0.");
let messages = get_collection("messages");
let attachments = get_collection("attachments");
messages
.update_many(
doc! { "attachment": { "$exists": 1 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0 } },
None,
)
.await
.expect("Failed to update messages.");
attachments
.update_many(
doc! {},
doc! { "$set": { "tag": "attachments", "size": 0 } },
None,
)
.await
.expect("Failed to update attachments.");
}
if revision <= 2 {
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
get_db()
.create_collection("servers", None)
.await
.expect("Failed to create servers collection.");
}
if revision <= 3 {
info!("Running migration [revision 3 / 2021-05-25]: Support multiple file uploads, add channel_unreads and user_settings.");
let messages = get_collection("messages");
let mut cursor = messages
.find(
doc! {
"attachment": {
"$exists": 1
}
},
FindOptions::builder()
.projection(doc! {
"_id": 1,
"attachments": [ "$attachment" ]
})
.build(),
)
.await
.expect("Failed to fetch messages.");
while let Some(result) = cursor.next().await {
let doc = result.unwrap();
let id = doc.get_str("_id").unwrap();
let attachments = doc.get_array("attachments").unwrap();
messages
.update_one(
doc! { "_id": id },
doc! { "$unset": { "attachment": 1 }, "$set": { "attachments": attachments } },
None,
)
.await
.unwrap();
}
get_db()
.create_collection("channel_unreads", None)
.await
.expect("Failed to create channel_unreads collection.");
get_db()
.create_collection("user_settings", None)
.await
.expect("Failed to create user_settings collection.");
}
if revision <= 4 {
info!("Running migration [revision 4 / 2021-06-01]: Add more server collections.");
get_db()
.create_collection("server_members", None)
.await
.expect("Failed to create server_members collection.");
get_db()
.create_collection("server_bans", None)
.await
.expect("Failed to create server_bans collection.");
get_db()
.create_collection("channel_invites", None)
.await
.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.");
}
if revision <= 7 {
info!("Running migration [revision 7 / 2021-08-11]: Add message text index.");
get_db()
.create_collection("bots", None)
.await
.expect("Failed to create bots collection.");
}
if revision <= 8 {
info!("Running migration [revision 8 / 2021-09-10]: Update to rAuth version 1.");
get_db()
.run_command(
doc! {
"dropIndexes": "accounts",
"index": ["email", "email_normalised"]
},
None,
)
.await
.expect("Failed to delete legacy account indexes.");
let col = get_collection("sessions");
let mut cursor = get_collection("accounts")
.find(doc! { }, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(account) = doc {
let id = account.get_str("_id").unwrap();
if let Some(sessions) = account.get("sessions") {
#[derive(Deserialize)]
struct Session {
id: String,
token: String,
friendly_name: String,
subscription: Option<Document>,
}
let sessions = from_bson::<Vec<Session>>(sessions.clone()).unwrap();
for session in sessions {
info!("Converting session {} to new format.", &session.id);
let mut doc = doc! {
"_id": session.id,
"token": session.token,
"user_id": id.clone(),
"name": session.friendly_name,
};
if let Some(sub) = session.subscription {
doc.insert("subscription", sub);
}
col.insert_one(doc, None).await.ok();
}
} else {
info!("Account doesn't have any sessions!");
}
}
}
get_collection("accounts")
.update_many(
doc! { },
doc! {
"$unset": {
"sessions": 1,
},
"$set": {
"mfa": {
"recovery_codes": []
}
}
},
None
)
.await
.unwrap();
}
if revision <= 9 {
info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id.");
let mut cursor = get_collection("channels")
.find(doc! { }, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await {
if let Ok(channel) = doc {
let channel_id = channel.get_str("_id").unwrap();
if let Some(last_message) = channel.get("last_message") {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Obj {
#[serde(rename = "_id")]
id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum LastMessage {
Obj(Obj),
Id(String)
}
let lm = from_bson::<LastMessage>(last_message.clone()).unwrap();
let id = match lm {
LastMessage::Obj(Obj { id }) => id,
LastMessage::Id(id) => id
};
info!("Converting session {} to new format.", &channel_id);
get_collection("channels")
.update_one(
doc! {
"_id": channel_id
},
doc! {
"$set": {
"last_message_id": id
},
"$unset": {
"last_message": 1,
}
},
None
)
.await
.unwrap();
} else {
info!("{} has no last_message.", &channel_id);
}
}
}
}
if revision <= 10 {
info!("Running migration [revision 10 / 2021-11-01]: Remove nonce values on channels and servers.");
get_collection("servers")
.update_many(
doc! {},
doc! {
"$unset": {
"nonce": 1,
}
},
None
)
.await
.unwrap();
get_collection("channels")
.update_many(
doc! {},
doc! {
"$unset": {
"nonce": 1,
}
},
None
)
.await
.unwrap();
}
if revision <= 11 {
info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1
},
"name": "channel"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
get_db()
.run_command(
doc! {
"createIndexes": "channel_unreads",
"indexes": [
{
"key": {
"_id.channel": 1,
"_id.user": 1,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create channel_unreads index.");
get_db()
.run_command(
doc! {
"createIndexes": "server_members",
"indexes": [
{
"key": {
"_id.server": 1,
"_id.user": 1,
},
"name": "compound_id"
},
{
"key": {
"_id.user": 1,
},
"name": "user_id"
}
]
},
None,
)
.await
.expect("Failed to create server_members index.");
}
if revision <= 12 {
info!("Running migration [revision 12 / 2021-11-21]: Add indexes to database.");
get_db()
.run_command(
doc! {
"createIndexes": "messages",
"indexes": [
{
"key": {
"channel": 1,
"_id": 1
},
"name": "channel_id_compound"
}
]
},
None,
)
.await
.expect("Failed to create message index.");
}
// Need to migrate fields on attachments, change `user_id`, `object_id`, etc to `parent`.
// Reminder to update LATEST_REVISION when adding new migrations.
LATEST_REVISION
}
-36
View File
@@ -1,36 +0,0 @@
use crate::util::variables::MONGO_URI;
use mongodb::{Client, Collection, Database};
use once_cell::sync::OnceCell;
static DBCONN: OnceCell<Client> = OnceCell::new();
pub async fn connect() {
let client = Client::with_uri_str(&MONGO_URI)
.await
.expect("Failed to init db connection.");
DBCONN.set(client).unwrap();
migrations::run_migrations().await;
}
pub fn get_connection() -> &'static Client {
DBCONN.get().unwrap()
}
pub fn get_db() -> Database {
get_connection().database("revolt")
}
pub fn get_collection(collection: &str) -> Collection {
get_db().collection(collection)
}
pub mod entities;
pub mod guards;
pub mod migrations;
pub mod permissions;
pub use entities::*;
pub use guards::*;
pub use permissions::*;
-159
View File
@@ -1,159 +0,0 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use super::PermissionCalculator;
use num_enum::TryFromPrimitive;
use std::ops;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum ChannelPermission {
View = 0b00000000000000000000000000000001, // 1
SendMessage = 0b00000000000000000000000000000010, // 2
ManageMessages = 0b00000000000000000000000000000100, // 4
ManageChannel = 0b00000000000000000000000000001000, // 8
VoiceCall = 0b00000000000000000000000000010000, // 16
InviteOthers = 0b00000000000000000000000000100000, // 32
EmbedLinks = 0b00000000000000000000000001000000, // 64
UploadFiles = 0b00000000000000000000000010000000, // 128
Masquerade = 0b00000000000000000000000100000000, // 256
}
lazy_static! {
pub static ref DEFAULT_PERMISSION_DM: u32 =
ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::ManageChannel
+ ChannelPermission::VoiceCall
+ ChannelPermission::InviteOthers
+ ChannelPermission::EmbedLinks
+ ChannelPermission::UploadFiles
+ ChannelPermission::Masquerade;
pub static ref DEFAULT_PERMISSION_SERVER: u32 =
ChannelPermission::View
+ ChannelPermission::SendMessage
+ ChannelPermission::VoiceCall
+ ChannelPermission::InviteOthers
+ ChannelPermission::EmbedLinks
+ ChannelPermission::UploadFiles;
}
impl_op_ex!(+ |a: &ChannelPermission, b: &ChannelPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &ChannelPermission| -> u32 { *a | *b as u32 });
bitfield! {
pub struct ChannelPermissions(MSB0 [u32]);
u32;
pub get_view, _: 31;
pub get_send_message, _: 30;
pub get_manage_messages, _: 29;
pub get_manage_channel, _: 28;
pub get_voice_call, _: 27;
pub get_invite_others, _: 26;
pub get_embed_links, _: 25;
pub get_upload_files, _: 24;
pub get_masquerade, _: 23;
}
impl<'a> PermissionCalculator<'a> {
pub async fn calculate_channel(self) -> Result<u32> {
let channel = if let Some(channel) = self.channel {
channel
} else {
unreachable!()
};
match channel {
Channel::SavedMessages { user: owner, .. } => {
if &self.perspective.id == owner {
Ok(u32::MAX)
} else {
Ok(0)
}
}
Channel::DirectMessage { recipients, .. } => {
if recipients
.iter()
.find(|x| *x == &self.perspective.id)
.is_some()
{
if let Some(recipient) = recipients.iter().find(|x| *x != &self.perspective.id)
{
let perms = self.for_user(recipient).await?;
if perms.get_send_message() {
return Ok(*DEFAULT_PERMISSION_DM);
}
return Ok(ChannelPermission::View as u32);
}
}
Ok(0)
}
Channel::Group { recipients, permissions, owner, .. } => {
if &self.perspective.id == owner {
return Ok(*DEFAULT_PERMISSION_DM)
}
if recipients
.iter()
.find(|x| *x == &self.perspective.id)
.is_some()
{
if let Some(permissions) = permissions {
Ok(permissions.clone() as u32)
} else {
Ok(*DEFAULT_PERMISSION_DM)
}
} else {
Ok(0)
}
}
Channel::TextChannel { server, default_permissions, role_permissions, .. }
| Channel::VoiceChannel { server, default_permissions, role_permissions, .. } => {
let server = Ref::from_unchecked(server.clone()).fetch_server().await?;
if self.perspective.id == server.owner {
Ok(u32::MAX)
} else {
match Ref::from_unchecked(self.perspective.id.clone()).fetch_member(&server.id).await {
Ok(member) => {
let mut perm = if let Some(permission) = default_permissions {
*permission as u32
} else {
server.default_permissions.1 as u32
};
if let Some(roles) = member.roles {
for role in roles {
if let Some(permission) = role_permissions.get(&role) {
perm |= *permission as u32;
}
if let Some(server_role) = server.roles.get(&role) {
perm |= server_role.permissions.1 as u32;
}
}
}
Ok(perm)
}
Err(error) => {
match &error {
Error::NotFound => Ok(0),
_ => Err(error)
}
}
}
}
}
}
}
pub async fn for_channel(self) -> Result<ChannelPermissions<[u32; 1]>> {
Ok(ChannelPermissions([self.calculate_channel().await?]))
}
}
-77
View File
@@ -1,77 +0,0 @@
pub use crate::database::*;
pub mod channel;
pub mod server;
pub mod user;
pub use user::get_relationship;
pub struct PermissionCalculator<'a> {
perspective: &'a User,
user: Option<&'a User>,
relationship: Option<&'a RelationshipStatus>,
channel: Option<&'a Channel>,
server: Option<&'a Server>,
// member: Option<&'a Member>,
has_mutual_connection: bool,
}
impl<'a> PermissionCalculator<'a> {
pub fn new(perspective: &'a User) -> PermissionCalculator {
PermissionCalculator {
perspective,
user: None,
relationship: None,
channel: None,
server: None,
// member: None,
has_mutual_connection: false,
}
}
pub fn with_user(self, user: &'a User) -> PermissionCalculator {
PermissionCalculator {
user: Some(&user),
..self
}
}
pub fn with_relationship(self, relationship: &'a RelationshipStatus) -> PermissionCalculator {
PermissionCalculator {
relationship: Some(&relationship),
..self
}
}
pub fn with_channel(self, channel: &'a Channel) -> PermissionCalculator {
PermissionCalculator {
channel: Some(&channel),
..self
}
}
pub fn with_server(self, server: &'a Server) -> PermissionCalculator {
PermissionCalculator {
server: Some(&server),
..self
}
}
/* pub fn with_member(self, member: &'a Member) -> PermissionCalculator {
PermissionCalculator {
member: Some(&member),
..self
}
} */
pub fn with_mutual_connection(self) -> PermissionCalculator<'a> {
PermissionCalculator {
has_mutual_connection: true,
..self
}
}
}
-89
View File
@@ -1,89 +0,0 @@
use crate::util::result::{Error, Result};
use super::PermissionCalculator;
use super::Ref;
use num_enum::TryFromPrimitive;
use std::ops;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum ServerPermission {
View = 0b00000000000000000000000000000001, // 1
ManageRoles = 0b00000000000000000000000000000010, // 2
ManageChannels = 0b00000000000000000000000000000100, // 4
ManageServer = 0b00000000000000000000000000001000, // 8
KickMembers = 0b00000000000000000000000000010000, // 16
BanMembers = 0b00000000000000000000000000100000, // 32
// 6 bits of space
ChangeNickname = 0b00000000000000000001000000000000, // 4096
ManageNicknames = 0b00000000000000000010000000000000, // 8192
ChangeAvatar = 0b00000000000000000100000000000000, // 16382
RemoveAvatars = 0b00000000000000001000000000000000, // 32768
// 16 bits of space
}
lazy_static! {
pub static ref DEFAULT_PERMISSION: u32 =
ServerPermission::View
+ ServerPermission::ChangeNickname
+ ServerPermission::ChangeAvatar;
}
impl_op_ex!(+ |a: &ServerPermission, b: &ServerPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &ServerPermission| -> u32 { *a | *b as u32 });
bitfield! {
pub struct ServerPermissions(MSB0 [u32]);
u32;
pub get_view, _: 31;
pub get_manage_roles, _: 30;
pub get_manage_channels, _: 29;
pub get_manage_server, _: 28;
pub get_kick_members, _: 27;
pub get_ban_members, _: 26;
pub get_change_nickname, _: 19;
pub get_manage_nicknames, _: 18;
pub get_change_avatar, _: 17;
pub get_remove_avatars, _: 16;
}
impl<'a> PermissionCalculator<'a> {
pub async fn calculate_server(self) -> Result<u32> {
let server = if let Some(server) = self.server {
server
} else {
unreachable!()
};
if self.perspective.id == server.owner {
Ok(u32::MAX)
} else {
match Ref::from_unchecked(self.perspective.id.clone()).fetch_member(&server.id).await {
Ok(member) => {
let mut perm = server.default_permissions.0 as u32;
if let Some(roles) = member.roles {
for role in roles {
if let Some(server_role) = server.roles.get(&role) {
perm |= server_role.permissions.0 as u32;
}
}
}
Ok(perm)
}
Err(error) => {
match &error {
Error::NotFound => Ok(0),
_ => Err(error)
}
}
}
}
}
pub async fn for_server(self) -> Result<ServerPermissions<[u32; 1]>> {
Ok(ServerPermissions([self.calculate_server().await?]))
}
}
-131
View File
@@ -1,131 +0,0 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use super::PermissionCalculator;
use mongodb::bson::doc;
use num_enum::TryFromPrimitive;
use std::ops;
#[derive(Debug, PartialEq, Eq, TryFromPrimitive, Copy, Clone)]
#[repr(u32)]
pub enum UserPermission {
Access = 0b00000000000000000000000000000001, // 1
ViewProfile = 0b00000000000000000000000000000010, // 2
SendMessage = 0b00000000000000000000000000000100, // 4
Invite = 0b00000000000000000000000000001000, // 8
}
bitfield! {
pub struct UserPermissions(MSB0 [u32]);
u32;
pub get_access, _: 31;
pub get_view_profile, _: 30;
pub get_send_message, _: 29;
pub get_invite, _: 28;
}
impl_op_ex!(+ |a: &UserPermission, b: &UserPermission| -> u32 { *a as u32 | *b as u32 });
impl_op_ex_commutative!(+ |a: &u32, b: &UserPermission| -> u32 { *a | *b as u32 });
pub fn get_relationship(a: &User, b: &str) -> RelationshipStatus {
if a.id == b {
return RelationshipStatus::User;
}
if let Some(relations) = &a.relations {
if let Some(relationship) = relations.iter().find(|x| x.id == b) {
return relationship.status.clone();
}
}
RelationshipStatus::None
}
impl<'a> PermissionCalculator<'a> {
pub async fn calculate_user(self, target: &str) -> Result<u32> {
if &self.perspective.id == target {
return Ok(u32::MAX);
}
let mut permissions: u32 = 0;
match self
.relationship
.clone()
.map(|v| v.to_owned())
.unwrap_or_else(|| get_relationship(&self.perspective, &target))
{
RelationshipStatus::Friend | RelationshipStatus::User => return Ok(u32::MAX),
RelationshipStatus::Blocked | RelationshipStatus::BlockedOther => {
return Ok(UserPermission::Access as u32)
}
RelationshipStatus::Incoming | RelationshipStatus::Outgoing => {
permissions = UserPermission::Access as u32;
// ! INFO: if we add boolean switch for permission to
// ! message people who have mutual, we need to get
// ! rid of this return statement.
// return Ok(permissions);
}
_ => {}
}
let check_server_overlap = async || {
let server_ids = User::fetch_server_ids(&self.perspective.id).await?;
Ok(
get_collection("server_members")
.find_one(
doc! {
"_id.user": &target,
"_id.server": {
"$in": server_ids
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "server_members",
})?
.is_some()
)
};
if self.has_mutual_connection
|| check_server_overlap().await?
|| get_collection("channels")
.find_one(
doc! {
"channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ &self.perspective.id, target ]
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "channels",
})?
.is_some()
{
// ! FIXME: add privacy settings
return Ok(UserPermission::Access + UserPermission::ViewProfile);
}
Ok(permissions)
}
pub async fn for_user(self, target: &str) -> Result<UserPermissions<[u32; 1]>> {
Ok(UserPermissions([self.calculate_user(&target).await?]))
}
pub async fn for_user_given(self) -> Result<UserPermissions<[u32; 1]>> {
let id = &self.user.unwrap().id;
Ok(UserPermissions([self.calculate_user(&id).await?]))
}
}
+20 -20
View File
@@ -14,13 +14,13 @@ extern crate impl_ops;
extern crate bitfield;
extern crate ctrlc;
pub mod database;
pub mod notifications;
//pub mod database;
//pub mod notifications;
pub mod routes;
pub mod redis;
//pub mod redis;
pub mod util;
pub mod version;
pub mod task_queue;
//pub mod task_queue;
use async_std::task;
use futures::join;
@@ -31,11 +31,11 @@ use rauth::{
};
use std::str::FromStr;
use rocket_cors::AllowedOrigins;
use util::variables::{
/*use util::variables::{
APP_URL, HCAPTCHA_KEY, INVITE_ONLY, SMTP_FROM, SMTP_HOST, SMTP_PASSWORD, SMTP_USERNAME,
USE_EMAIL, USE_HCAPTCHA,
};
use crate::util::ratelimit::RatelimitState;
use crate::util::ratelimit::RatelimitState;*/
#[async_std::main]
async fn main() {
@@ -47,11 +47,11 @@ async fn main() {
crate::version::VERSION
);
util::variables::preflight_checks();
/*util::variables::preflight_checks();
database::connect().await;
redis::connect().await;
notifications::hive::init_hive().await;
task_queue::start_queues().await;
task_queue::start_queues().await;*/
ctrlc::set_handler(move || {
// Force ungraceful exit to avoid hang.
@@ -60,12 +60,12 @@ async fn main() {
.expect("Error setting Ctrl-C handler");
let web_task = task::spawn(launch_web());
let hive_task = task::spawn_local(notifications::hive::listen());
//let hive_task = task::spawn_local(notifications::hive::listen());
join!(
web_task,
hive_task,
notifications::websocket::launch_server()
//hive_task,
//notifications::websocket::launch_server()
);
}
@@ -84,7 +84,7 @@ async fn launch_web() {
.expect("Failed to create CORS.");
let mut config = Config {
email_verification: if *USE_EMAIL {
email_verification: /*if *USE_EMAIL {
EmailVerification::Enabled {
smtp: SMTPSettings {
from: (*SMTP_FROM).to_string(),
@@ -114,11 +114,11 @@ async fn launch_web() {
}
} else {
EmailVerification::Disabled
},
},*/ EmailVerification::Disabled,
..Default::default()
};
if *INVITE_ONLY {
/*if *INVITE_ONLY {
config.invite_only = true;
}
@@ -126,17 +126,17 @@ async fn launch_web() {
config.captcha = Captcha::HCaptcha {
secret: HCAPTCHA_KEY.clone(),
};
}
}*/
let auth = Auth::new(database::get_db(), config);
//let auth = Auth::new(database::get_db(), config);
let rocket = rocket::build();
routes::mount(rocket)
.mount("/", rocket_cors::catch_all_options_routes())
.mount("/auth/account", rauth::web::account::routes())
.mount("/auth/session", rauth::web::session::routes())
.manage(auth)
//.mount("/auth/account", rauth::web::account::routes())
//.mount("/auth/session", rauth::web::session::routes())
//.manage(auth)
.manage(cors.clone())
.manage(RatelimitState::new())
//.manage(RatelimitState::new())
.attach(cors)
.launch()
.await
-281
View File
@@ -1,281 +0,0 @@
use hive_pubsub::PubSub;
use mongodb::bson::doc;
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
use super::hive::{get_hive, subscribe_if_exists};
use crate::{database::*, util::result::Result};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "error")]
pub enum WebSocketError {
LabelMe,
InternalError { at: String },
InvalidSession,
OnboardingNotFinished,
AlreadyAuthenticated,
MalformedData { msg: String },
}
#[derive(Deserialize, Debug)]
pub struct Auth {
pub token: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum Ping {
Binary(Vec<u8>),
Number(usize)
}
#[derive(Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ServerboundNotification {
Authenticate(Auth),
BeginTyping { channel: String },
EndTyping { channel: String },
Ping { data: Ping, responded: Option<()> },
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveUserField {
ProfileContent,
ProfileBackground,
StatusText,
Avatar,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveChannelField {
Icon,
Description,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveServerField {
Icon,
Banner,
Description,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveRoleField {
Colour,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveMemberField {
Nickname,
Avatar,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum RemoveBotField {
InteractionsURL,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum ClientboundNotification {
Error(WebSocketError),
Authenticated,
Ready {
users: Vec<User>,
servers: Vec<Server>,
channels: Vec<Channel>,
members: Vec<Member>,
},
Pong { data: Ping },
Message(Message),
MessageUpdate {
id: String,
channel: String,
data: Value,
},
MessageDelete {
id: String,
channel: String,
},
ChannelCreate(Channel),
ChannelUpdate {
id: String,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveChannelField>,
},
ChannelDelete {
id: String,
},
ChannelGroupJoin {
id: String,
user: String,
},
ChannelGroupLeave {
id: String,
user: String,
},
ChannelStartTyping {
id: String,
user: String,
},
ChannelStopTyping {
id: String,
user: String,
},
ChannelAck {
id: String,
user: String,
message_id: String,
},
ServerUpdate {
id: String,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveServerField>,
},
ServerDelete {
id: String,
},
ServerMemberUpdate {
id: MemberCompositeKey,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveMemberField>,
},
ServerMemberJoin {
id: String,
user: String,
},
ServerMemberLeave {
id: String,
user: String,
},
ServerRoleUpdate {
id: String,
role_id: String,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveRoleField>,
},
ServerRoleDelete {
id: String,
role_id: String,
},
UserUpdate {
id: String,
data: Value,
#[serde(skip_serializing_if = "Option::is_none")]
clear: Option<RemoveUserField>,
},
UserRelationship {
id: String,
user: User,
status: RelationshipStatus,
},
UserSettingsUpdate {
id: String,
update: Value,
},
}
impl ClientboundNotification {
pub fn publish(self, topic: String) {
async_std::task::spawn(async move {
prehandle_hook(&self).await.ok(); // ! FIXME: this should be moved to pubsub
hive_pubsub::backend::redis::publish(get_hive(), topic, self)
.await
.ok();
});
}
pub fn publish_as_user(self, user: String) {
self.clone().publish(user.clone());
async_std::task::spawn(async move {
if let Ok(server_ids) = User::fetch_server_ids(&user).await {
for server in server_ids {
self.clone().publish(server.clone());
}
}
});
}
}
pub async fn prehandle_hook(notification: &ClientboundNotification) -> Result<()> {
match &notification {
ClientboundNotification::ChannelGroupJoin { id, user } => {
subscribe_if_exists(user.clone(), id.clone()).ok();
}
ClientboundNotification::ChannelCreate(channel) => {
let channel_id = channel.id();
match &channel {
Channel::SavedMessages { user, .. } => {
subscribe_if_exists(user.clone(), channel_id.to_string()).ok();
}
Channel::DirectMessage { recipients, .. } | Channel::Group { recipients, .. } => {
for recipient in recipients {
subscribe_if_exists(recipient.clone(), channel_id.to_string()).ok();
}
}
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
// ! FIXME: write a better algorithm?
let members = Server::fetch_member_ids(server).await?;
for member in members {
subscribe_if_exists(member.clone(), channel_id.to_string()).ok();
}
}
}
}
ClientboundNotification::ServerMemberJoin { id, user } => {
let server = Ref::from_unchecked(id.clone()).fetch_server().await?;
subscribe_if_exists(user.clone(), id.clone()).ok();
for channel in server.channels {
subscribe_if_exists(user.clone(), channel).ok();
}
}
ClientboundNotification::UserRelationship { id, user, status } => {
if status != &RelationshipStatus::None {
subscribe_if_exists(id.clone(), user.id.clone()).ok();
}
}
_ => {}
}
Ok(())
}
pub async fn posthandle_hook(notification: &ClientboundNotification) {
match &notification {
ClientboundNotification::ChannelDelete { id } => {
get_hive().hive.drop_topic(&id).ok();
}
ClientboundNotification::ChannelGroupLeave { id, user } => {
get_hive().hive.unsubscribe(user, id).ok();
}
ClientboundNotification::ServerDelete { id } => {
get_hive().hive.drop_topic(&id).ok();
}
ClientboundNotification::ServerMemberLeave { id, user } => {
get_hive().hive.unsubscribe(user, id).ok();
if let Ok(server) = Ref::from_unchecked(id.clone()).fetch_server().await {
for channel in server.channels {
get_hive().hive.unsubscribe(user, &channel).ok();
}
}
}
ClientboundNotification::UserRelationship { id, user, status } => {
if status == &RelationshipStatus::None {
get_hive().hive.unsubscribe(id, &user.id).ok();
}
}
_ => {}
}
}
-73
View File
@@ -1,73 +0,0 @@
use std::sync::{Arc, Mutex};
use super::{events::ClientboundNotification, websocket};
use crate::redis::get_pool;
use crate::util::variables::REDIS_URI;
use futures::FutureExt;
use hive_pubsub::backend::redis::RedisPubSub;
use hive_pubsub::PubSub;
use log::{debug, error};
use once_cell::sync::OnceCell;
use serde_json::to_string;
type Hive<'a> = RedisPubSub<'a, String, String, ClientboundNotification>;
static HIVE: OnceCell<Hive<'static>> = OnceCell::new();
pub async fn init_hive() {
let pubsub_con = redis::Client::open(REDIS_URI.to_string()).unwrap().get_async_connection().await.unwrap().into_pubsub();
let hive = RedisPubSub::new(
|ids, notification: ClientboundNotification| {
let notif = notification.clone();
async_std::task::spawn(async move {
super::events::posthandle_hook(&notif).await;
});
if let Ok(data) = to_string(&notification) {
debug!("Pushing out notification. {}", data);
websocket::publish(ids, notification);
} else {
error!("Failed to serialise notification.");
}
},
get_pool(),
Arc::new(Mutex::new(pubsub_con))
);
if HIVE.set(hive).is_err() {
panic!("Failed to set global pubsub instance.");
}
}
pub async fn listen() {
HIVE.get()
.unwrap()
.clone()
.listen()
.fuse()
.await
.expect("Hive hit an error");
}
pub fn subscribe_multiple(user: String, topics: Vec<String>) -> Result<(), String> {
let hive = HIVE.get().unwrap();
for topic in topics {
hive.subscribe(user.clone(), topic)?;
}
Ok(())
}
pub fn subscribe_if_exists(user: String, topic: String) -> Result<(), String> {
let hive = HIVE.get().unwrap();
if hive.hive.map.lock().unwrap().get_left(&user).is_some() {
hive.subscribe(user, topic)?;
}
Ok(())
}
pub fn get_hive() -> &'static Hive<'static> {
HIVE.get().unwrap()
}
-5
View File
@@ -1,5 +0,0 @@
pub mod events;
pub mod hive;
pub mod payload;
pub mod subscriptions;
pub mod websocket;
-124
View File
@@ -1,124 +0,0 @@
use std::collections::HashSet;
use crate::{database::*, notifications::events::ClientboundNotification};
use crate::{
database::{entities::User, get_collection},
util::result::{Error, Result},
};
use futures::StreamExt;
use mongodb::bson::{doc, from_document};
pub async fn generate_ready(mut user: User) -> Result<ClientboundNotification> {
let mut user_ids: HashSet<String> = HashSet::new();
if let Some(relationships) = &user.relations {
user_ids.extend(
relationships
.iter()
.map(|relationship| relationship.id.clone()),
);
}
let members = User::fetch_memberships(&user.id).await?;
let server_ids: Vec<String> = members.iter()
.map(|x| x.id.server.clone())
.collect();
let mut cursor = get_collection("servers")
.find(
doc! {
"_id": {
"$in": server_ids
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "servers",
})?;
let mut servers = vec![];
let mut channel_ids = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let server: Server = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "server",
})?;
channel_ids.extend(server.channels.iter().cloned());
servers.push(server);
}
}
let mut cursor = get_collection("channels")
.find(
doc! {
"$or": [
{
"_id": {
"$in": channel_ids
}
},
{
"channel_type": "SavedMessages",
"user": &user.id
},
{
"channel_type": "DirectMessage",
"recipients": &user.id
},
{
"channel_type": "Group",
"recipients": &user.id
}
]
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "channels",
})?;
let mut channels = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
let channel = from_document(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "channel",
})?;
if let Channel::Group { recipients, .. } = &channel {
user_ids.extend(recipients.iter().cloned());
} else if let Channel::DirectMessage { recipients, .. } = &channel {
user_ids.extend(recipients.iter().cloned());
}
channels.push(channel);
}
}
user_ids.remove(&user.id);
let mut users = if user_ids.len() > 0 {
user.fetch_multiple_users(&user_ids.into_iter().collect::<Vec<String>>())
.await?
} else {
vec![]
};
user.relationship = Some(RelationshipStatus::User);
user.online = Some(true);
users.push(user.apply_badges());
Ok(ClientboundNotification::Ready {
users,
servers,
channels,
members
})
}
-87
View File
@@ -1,87 +0,0 @@
use crate::database::*;
use super::hive::get_hive;
use futures::StreamExt;
use hive_pubsub::PubSub;
use mongodb::bson::doc;
use mongodb::bson::Document;
use mongodb::options::FindOptions;
pub async fn generate_subscriptions(user: &User) -> Result<(), String> {
let hive = get_hive();
hive.subscribe(user.id.clone(), user.id.clone())?;
if let Some(relations) = &user.relations {
for relation in relations {
hive.subscribe(user.id.clone(), relation.id.clone())?;
}
}
let server_ids = User::fetch_server_ids(&user.id)
.await
.map_err(|_| "Failed to fetch memberships.".to_string())?;
let channel_ids = get_collection("servers")
.find(
doc! {
"_id": {
"$in": &server_ids
}
},
None,
)
.await
.map_err(|_| "Failed to fetch servers.".to_string())?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| {
x.get_array("channels").ok().map(|v| {
v.into_iter()
.filter_map(|x| x.as_str().map(|x| x.to_string()))
.collect::<Vec<String>>()
})
})
.flatten()
.collect::<Vec<String>>();
for id in server_ids {
hive.subscribe(user.id.clone(), id)?;
}
for id in channel_ids {
hive.subscribe(user.id.clone(), id)?;
}
let mut cursor = get_collection("channels")
.find(
doc! {
"$or": [
{
"channel_type": "SavedMessages",
"user": &user.id
},
{
"channel_type": "DirectMessage",
"recipients": &user.id
},
{
"channel_type": "Group",
"recipients": &user.id
}
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| "Failed to fetch channels.".to_string())?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
hive.subscribe(user.id.clone(), doc.get_str("_id").unwrap().to_string())?;
}
}
Ok(())
}
-393
View File
@@ -1,393 +0,0 @@
use crate::database::*;
use crate::notifications::events::Ping;
use crate::util::variables::WS_HOST;
use super::subscriptions;
use async_std::net::{TcpListener, TcpStream};
use async_std::task;
use async_tungstenite::tungstenite::{handshake::server, Message};
use futures::channel::{
mpsc::{unbounded, UnboundedSender},
oneshot,
};
use futures::stream::TryStreamExt;
use futures::{pin_mut, prelude::*};
use hive_pubsub::PubSub;
use log::{debug, info};
use many_to_many::ManyToMany;
use mongodb::bson::doc;
use rauth::entities::{Model, Session};
use rmp_serde;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex, RwLock};
use url::Url;
use super::{
events::{ClientboundNotification, ServerboundNotification, WebSocketError},
hive::get_hive,
};
type Tx = UnboundedSender<Message>;
type PeerMap = Arc<Mutex<HashMap<SocketAddr, (Tx, MSGFormat)>>>;
lazy_static! {
static ref CONNECTIONS: PeerMap = Arc::new(Mutex::new(HashMap::new()));
static ref USERS: Arc<RwLock<ManyToMany<String, SocketAddr>>> =
Arc::new(RwLock::new(ManyToMany::new()));
}
pub async fn launch_server() {
let try_socket = TcpListener::bind(WS_HOST.to_string()).await;
let listener = try_socket.expect("Failed to bind");
info!("Listening on: {}", *WS_HOST);
while let Ok((stream, _)) = listener.accept().await {
task::spawn(accept(stream));
}
}
#[derive(Debug, Clone)]
enum MSGFormat {
JSON,
MSGPACK,
}
struct HeaderCallback {
sender: oneshot::Sender<MSGFormat>,
}
impl server::Callback for HeaderCallback {
fn on_request(
self,
request: &server::Request,
response: server::Response,
) -> Result<server::Response, server::ErrorResponse> {
// we dont get some of the data sometimes so im generating a fake url with the only data we actually need
let url = format!(
"ws://example.com?{}",
request.uri().query().unwrap_or("?format=json")
);
let mut query: HashMap<_, _> = url
.parse::<Url>()
.unwrap()
.query_pairs()
.into_owned()
.collect(); // should be safe to use unwrap here as we just made the url ourself
let format_query: Option<String> = query.remove("format");
let format = match format_query.as_deref().unwrap_or("json") {
"msgpack" => MSGFormat::MSGPACK,
"json" => MSGFormat::JSON,
_ => MSGFormat::JSON, // Fallback to JSON.
};
if self.sender.send(format).is_ok() {
Ok(response)
} else {
Err(server::ErrorResponse::new(None))
}
}
}
async fn accept(stream: TcpStream) {
let addr = stream
.peer_addr()
.expect("Connected streams should have a peer address.");
let (sender, receiver) = oneshot::channel::<MSGFormat>();
let ws_stream =
async_tungstenite::accept_hdr_async_with_config(stream, HeaderCallback { sender }, None)
.await
.expect("Error during websocket handshake.");
let msg_format = receiver.await.unwrap(); // TODO: not use unwrap
info!("User established WebSocket connection from {}.", &addr);
let (write, read) = ws_stream.split();
let (tx, rx) = unbounded();
CONNECTIONS.lock().unwrap().insert(addr, (tx.clone(), msg_format.clone()));
let send = |notification: ClientboundNotification| {
let res = match msg_format {
MSGFormat::JSON => match serde_json::to_string(&notification) {
Ok(s) => Message::Text(s),
Err(_) => return
}
MSGFormat::MSGPACK => match rmp_serde::to_vec_named(&notification) {
Ok(v) => Message::Binary(v),
Err(_) => return,
},
};
if let Err(_) = tx.unbounded_send(res) {
debug!("Failed unbounded_send to websocket stream.");
}
};
let user_id: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let mutex_generator = || user_id.clone();
let fwd = rx.map(Ok).forward(write);
let incoming = read.try_for_each(async move |msg| {
let mutex = mutex_generator();
let maybe_decoded = match msg {
Message::Text(text) => {
serde_json::from_str::<ServerboundNotification>(&text).map_err(|e| e.to_string())
}
Message::Binary(vec) => {
rmp_serde::decode::from_read::<&[u8], ServerboundNotification>(vec.as_slice())
.map_err(|e| e.to_string())
}
Message::Ping(vec) => Ok(ServerboundNotification::Ping { data: Ping::Binary(vec), responded: Some(()) }),
_ => return Ok(()),
};
let notification = match maybe_decoded {
Err(why) => {
send(ClientboundNotification::Error(
WebSocketError::MalformedData {
msg: why.to_string(),
},
));
return Ok(());
}
Ok(n) => n,
};
match notification {
ServerboundNotification::Authenticate(auth) => {
{
if mutex.lock().unwrap().is_some() {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
let id = if let Ok(Some(session)) =
Session::find_one(&get_db(), doc! { "token": &auth.token }, None).await
{
Some(session.user_id)
} else if let Ok(Some(bot)) = get_collection("bots")
.find_one(doc! { "token": auth.token }, None)
.await
{
Some(bot.get_str("_id").unwrap().to_string())
} else {
None
};
if let Some(id) = id {
if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await {
let is_invisible = if let Some(status) = &user.status {
if let Some(presence) = &status.presence {
presence == &Presence::Invisible
} else {
false
}
} else {
false
};
let was_online = is_online(&id);
{
match USERS.write() {
Ok(mut map) => {
map.insert(id.clone(), addr);
}
Err(_) => {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Writing users map.".to_string(),
},
));
return Ok(());
}
}
}
*mutex.lock().unwrap() = Some(id.clone());
if let Err(_) = subscriptions::generate_subscriptions(&user).await {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Generating subscriptions.".to_string(),
},
));
return Ok(());
}
send(ClientboundNotification::Authenticated);
match super::payload::generate_ready(user).await {
Ok(payload) => {
send(payload);
if !was_online && !is_invisible {
ClientboundNotification::UserUpdate {
id: id.clone(),
data: json!({
"online": true
}),
clear: None,
}
.publish_as_user(id);
}
}
Err(_) => {
send(ClientboundNotification::Error(
WebSocketError::InternalError {
at: "Generating payload.".to_string(),
},
));
return Ok(());
}
}
} else {
send(ClientboundNotification::Error(
WebSocketError::OnboardingNotFinished,
));
}
} else {
send(ClientboundNotification::Error(
WebSocketError::InvalidSession,
));
}
}
// ! 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();
mutex.as_ref().unwrap().clone()
};
ClientboundNotification::ChannelStartTyping {
id: channel.clone(),
user,
}
.publish(channel);
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
ServerboundNotification::EndTyping { channel } => {
if mutex.lock().unwrap().is_some() {
let user = {
let mutex = mutex.lock().unwrap();
mutex.as_ref().unwrap().clone()
};
ClientboundNotification::ChannelStopTyping {
id: channel.clone(),
user,
}
.publish(channel);
} else {
send(ClientboundNotification::Error(
WebSocketError::AlreadyAuthenticated,
));
return Ok(());
}
}
ServerboundNotification::Ping { data, responded } => {
debug!("Ping received from connection {}. Payload: {:?}", &addr, data);
if responded.is_none() {
send(ClientboundNotification::Pong { data });
}
}
}
Ok(())
});
pin_mut!(fwd, incoming);
future::select(fwd, incoming).await;
info!("User {} disconnected.", &addr);
CONNECTIONS.lock().unwrap().remove(&addr);
let mut offline = None;
{
let user_id = user_id.lock().unwrap();
if let Some(user_id) = user_id.as_ref() {
let mut users = USERS.write().unwrap();
users.remove(&user_id, &addr);
if users.get_left(&user_id).is_none() {
get_hive().drop_client(&user_id).unwrap();
offline = Some(user_id.clone());
}
}
}
if let Some(id) = offline {
ClientboundNotification::UserUpdate {
id: id.clone(),
data: json!({
"online": false
}),
clear: None,
}
.publish_as_user(id);
}
}
pub fn publish(ids: Vec<String>, notification: ClientboundNotification) {
let mut targets = vec![];
{
let users = USERS.read().unwrap();
for id in ids {
// Block certain notifications from reaching users that aren't meant to see them.
match &notification {
ClientboundNotification::UserRelationship { id: user_id, .. }
| ClientboundNotification::UserSettingsUpdate { id: user_id, .. }
| ClientboundNotification::ChannelAck { user: user_id, .. } => {
if &id != user_id {
continue;
}
}
_ => {}
}
if let Some(mut arr) = users.get_left(&id) {
targets.append(&mut arr);
}
}
}
let json_msg = Message::Text(serde_json::to_string(&notification).unwrap());
let msgpack_msg = Message::Binary(rmp_serde::to_vec_named(&notification).unwrap());
let connections = CONNECTIONS.lock().unwrap();
for target in targets {
if let Some((conn, msg_format)) = connections.get(&target) {
let msg = match msg_format {
MSGFormat::JSON => json_msg.clone(),
MSGFormat::MSGPACK => msgpack_msg.clone()
};
if let Err(_) = conn.unbounded_send(msg) {
debug!("Failed unbounded_send.");
}
}
}
}
pub fn is_online(user: &String) -> bool {
USERS.read().unwrap().get_left(&user).is_some()
}
-18
View File
@@ -1,18 +0,0 @@
use crate::util::variables::REDIS_URI;
use mobc::Pool;
use mobc_redis::RedisConnectionManager;
use once_cell::sync::OnceCell;
static REDISPOOL: OnceCell<Pool<RedisConnectionManager>> = OnceCell::new();
pub async fn connect() {
let client = mobc_redis::redis::Client::open(REDIS_URI.to_string()).unwrap();
let manager = mobc_redis::RedisConnectionManager::new(client);
let pool = mobc::Pool::builder().max_open(100).build(manager);
REDISPOOL.set(pool).ok().unwrap();
}
pub fn get_pool() -> &'static Pool<RedisConnectionManager> {
REDISPOOL.get().unwrap()
}
+4 -75
View File
@@ -1,13 +1,9 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_BOT_COUNT;
use crate::util::regex::RE_USERNAME;
use mongodb::bson::{doc, to_document};
use revolt_quark::Result;
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use nanoid::nanoid;
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
@@ -17,73 +13,6 @@ pub struct Data {
}
#[post("/create", data = "<info>")]
pub async fn create_bot(user: User, info: Json<Data>) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
if get_collection("bots")
.count_documents(
doc! {
"owner": &user.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "count_documents",
with: "bots",
})? as usize >= *MAX_BOT_COUNT {
return Err(Error::ReachedMaximumBots)
}
let id = Ulid::new().to_string();
let token = nanoid!(64);
let bot = Bot {
id: id.clone(),
owner: user.id.clone(),
token,
public: false,
analytics: false,
discoverable: false,
interactions_url: None
};
if User::is_username_taken(&info.name).await? {
return Err(Error::UsernameTaken);
}
get_collection("users")
.insert_one(
doc! {
"_id": &id,
"username": &info.name,
"bot": {
"owner": &user.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "user",
})?;
get_collection("bots")
.insert_one(
to_document(&bot).map_err(|_| Error::DatabaseError { with: "bot", operation: "to_document" })?,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "user",
})?;
Ok(json!(bot))
pub async fn create_bot(/* user: User ,*/ info: Json<Data>) -> Result<Value> {
todo!()
}
+4 -64
View File
@@ -1,67 +1,7 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, EmptyResponse, Result};
use mongodb::bson::doc;
use revolt_quark::Result;
use rauth::util::EmptyResponse;
#[delete("/<target>")]
pub async fn delete_bot(user: User, target: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bot = target.fetch_bot().await?;
if bot.owner != user.id {
return Err(Error::MissingPermission);
}
let username = format!("Deleted User {}", &bot.id);
get_collection("users")
.update_one(
doc! {
"_id": &bot.id
},
doc! {
"$set": {
"username": &username,
"flags": 2
},
"$unset": {
"avatar": 1,
"status": 1,
"profile": 1
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
with: "user",
operation: "update_one"
})?;
ClientboundNotification::UserUpdate {
id: target.id.clone(),
data: json!({
"username": username,
"flags": 2
}),
clear: None,
}
.publish_as_user(target.id.clone());
get_collection("bots")
.delete_one(
doc! {
"_id": &bot.id
},
None
)
.await
.map_err(|_| Error::DatabaseError {
with: "bot",
operation: "delete_one"
})?;
Ok(EmptyResponse {})
pub async fn delete_bot(/*user: UserRef, target: Ref*/ target: String) -> Result<EmptyResponse> {
todo!()
}
+5 -101
View File
@@ -1,8 +1,7 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveBotField};
use crate::util::regex::RE_USERNAME;
use revolt_quark::{Result, models::bot::FieldsBot, EmptyResponse};
use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
@@ -16,105 +15,10 @@ pub struct Data {
public: Option<bool>,
analytics: Option<bool>,
interactions_url: Option<String>,
remove: Option<RemoveBotField>,
remove: Option<FieldsBot>,
}
#[patch("/<target>", data = "<data>")]
pub async fn edit_bot(user: User, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.name.is_none()
&& data.public.is_none()
&& data.analytics.is_none()
&& data.interactions_url.is_none()
&& data.remove.is_none()
{
return Ok(EmptyResponse {});
}
let bot = target.fetch_bot().await?;
if bot.owner != user.id {
return Err(Error::MissingPermission);
}
if let Some(name) = &data.name {
if User::is_username_taken(&name).await? {
return Err(Error::UsernameTaken);
}
get_collection("users")
.update_one(
doc! { "_id": &target.id },
doc! {
"$set": {
"username": name
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
ClientboundNotification::UserUpdate {
id: target.id.clone(),
data: json!({
"username": name
}),
clear: None,
}
.publish_as_user(target.id.clone());
}
let mut set = doc! {};
let mut unset = doc! {};
if let Some(remove) = &data.remove {
match remove {
RemoveBotField::InteractionsURL => {
unset.insert("interactions_url", 1);
}
}
}
if let Some(public) = &data.public {
set.insert("public", public);
}
if let Some(analytics) = &data.analytics {
set.insert("analytics", analytics);
}
if let Some(interactions_url) = &data.interactions_url {
set.insert("interactions_url", interactions_url);
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("bots")
.update_one(doc! { "_id": &target.id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "bot",
})?;
}
Ok(EmptyResponse {})
pub async fn edit_bot(/*user: UserRef, target: Ref,*/ target: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -21
View File
@@ -1,26 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::Result;
use serde_json::Value;
#[get("/<target>")]
pub async fn fetch_bot(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bot = target.fetch_bot().await?;
if !bot.public {
if bot.owner != user.id {
return Err(Error::BotIsPrivate);
}
}
let user = Ref::from_unchecked(bot.id.clone()).fetch_user().await?;
Ok(json!({
"bot": bot,
"user": user
}))
pub async fn fetch_bot(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -51
View File
@@ -1,56 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::Result;
use futures::StreamExt;
use mongodb::bson::{Document, doc, from_document};
use serde_json::Value;
#[get("/@me")]
pub async fn fetch_owned_bots(user: User) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bots = get_collection("bots")
.find(
doc! {
"owner": &user.id
},
None
)
.await
.map_err(|_| Error::DatabaseError {
with: "bots",
operation: "find"
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| from_document(x).ok())
.collect::<Vec<Bot>>();
let users = get_collection("users")
.find(
doc! {
"bot.owner": &user.id
},
None
)
.await
.map_err(|_| Error::DatabaseError {
with: "users",
operation: "find"
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| from_document(x).ok())
.collect::<Vec<User>>();
Ok(json!({
"bots": bots,
"users": users
}))
pub async fn fetch_owned_bots(/*user: UserRef*/) -> Result<Value> {
todo!()
}
+3 -23
View File
@@ -1,28 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::Result;
use serde_json::Value;
#[get("/<target>/invite")]
pub async fn fetch_public_bot(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bot = target.fetch_bot().await?;
if !bot.public {
if bot.owner != user.id {
return Err(Error::BotIsPrivate);
}
}
let user = Ref::from_unchecked(bot.id.clone()).fetch_user().await?;
Ok(json!({
"_id": bot.id,
"username": user.username,
"avatar": user.avatar,
"description": user.profile.map(|p| p.content)
}))
pub async fn fetch_public_bot(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -47
View File
@@ -1,5 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
use rocket::serde::json::Json;
use serde::Deserialize;
@@ -22,49 +21,6 @@ pub enum Destination {
}
#[post("/<target>/invite", data = "<dest>")]
pub async fn invite_bot(user: User, target: Ref, dest: Json<Destination>) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let bot = target.fetch_bot().await?;
if !bot.public {
if bot.owner != user.id {
return Err(Error::BotIsPrivate);
}
}
match dest.into_inner() {
Destination::Server(ServerId { server }) => {
let server = Ref::from(server)?.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if !perm.get_manage_server() {
Err(Error::MissingPermission)?
}
server.join_member(&bot.id).await?;
Ok(EmptyResponse {})
}
Destination::Group(GroupId { group }) => {
let channel = Ref::from(group)?.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_invite_others() {
Err(Error::MissingPermission)?
}
channel.add_to_group(bot.id, user.id).await?;
Ok(EmptyResponse {})
}
}
pub async fn invite_bot(/*user: UserRef, target: Ref,*/ target: String, dest: Json<Destination>) -> Result<EmptyResponse> {
todo!()
}
+3 -35
View File
@@ -1,38 +1,6 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
#[put("/<target>/ack/<message>")]
pub async fn req(user: User, target: Ref, message: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
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)?
}
crate::task_queue::task_ack::queue(
target.id().into(),
user.id.clone(),
crate::task_queue::task_ack::AckEvent::AckMessage {
id: message.id.clone()
}
).await;
ClientboundNotification::ChannelAck {
id: target.id().into(),
user: user.id.clone(),
message_id: message.id,
}
.publish(user.id);
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref, message: Ref*/ target: String, message: String) -> Result<EmptyResponse> {
todo!()
}
+3 -114
View File
@@ -1,119 +1,8 @@
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::ClientboundNotification};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
#[delete("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<EmptyResponse> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
match &target {
Channel::SavedMessages { .. } => Err(Error::NoEffect),
Channel::DirectMessage { .. } => {
get_collection("channels")
.update_one(
doc! {
"_id": target.id()
},
doc! {
"$set": {
"active": false
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
Ok(EmptyResponse {})
}
Channel::Group {
id,
owner,
recipients,
..
} => {
if &user.id == owner {
if let Some(new_owner) = recipients.iter().find(|x| *x != &user.id) {
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$set": {
"owner": new_owner
},
"$pull": {
"recipients": &user.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
target.publish_update(json!({ "owner": new_owner })).await?;
} else {
target.delete().await?;
return Ok(EmptyResponse {});
}
} else {
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$pull": {
"recipients": &user.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
ClientboundNotification::ChannelGroupLeave {
id: id.clone(),
user: user.id.clone(),
}
.publish(id.clone());
Content::SystemMessage(SystemMessage::UserLeft { id: user.id })
.send_as_system(&target)
.await
.ok();
Ok(EmptyResponse {})
}
Channel::TextChannel { .. } |
Channel::VoiceChannel { .. } => {
if perm.get_manage_channel() {
target.delete().await?;
Ok(EmptyResponse {})
} else {
Err(Error::MissingPermission)
}
}
}
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<EmptyResponse> {
todo!()
}
+5 -139
View File
@@ -1,8 +1,6 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveChannelField};
use revolt_quark::{EmptyResponse, Result, models::channel::FieldsChannel};
use mongodb::bson::{doc, to_document};
use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -17,144 +15,12 @@ pub struct Data {
description: Option<String>,
#[validate(length(min = 1, max = 128))]
icon: Option<String>,
remove: Option<RemoveChannelField>,
remove: Option<FieldsChannel>,
#[serde(skip_serializing_if = "Option::is_none")]
nsfw: Option<bool>
}
#[patch("/<target>", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.name.is_none()
&& data.description.is_none()
&& data.icon.is_none()
&& data.remove.is_none()
&& data.nsfw.is_none()
{
return Ok(EmptyResponse {});
}
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_manage_channel() {
Err(Error::MissingPermission)?
}
match &target {
Channel::Group { id, icon, .. }
| Channel::TextChannel { id, icon, .. }
| Channel::VoiceChannel { id, icon, .. } => {
let mut set = doc! {};
let mut unset = doc! {};
let mut remove_icon = false;
if let Some(remove) = &data.remove {
match remove {
RemoveChannelField::Icon => {
unset.insert("icon", 1);
remove_icon = true;
}
RemoveChannelField::Description => {
unset.insert("description", 1);
}
}
}
if let Some(name) = &data.name {
set.insert("name", name);
}
if let Some(description) = &data.description {
set.insert("description", description);
}
if let Some(attachment_id) = &data.icon {
let attachment =
File::find_and_use(&attachment_id, "icons", "object", target.id()).await?;
set.insert(
"icon",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_icon = true;
}
if let Some(nsfw) = &data.nsfw {
set.insert("nsfw", nsfw);
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("channels")
.update_one(doc! { "_id": &id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
}
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!(set),
clear: data.remove,
}
.publish(id.clone());
if let Channel::Group { .. } = &target {
if let Some(name) = data.name {
Content::SystemMessage(SystemMessage::ChannelRenamed {
name,
by: user.id.clone(),
})
.send_as_system(&target)
.await
.ok();
}
if let Some(_) = data.description {
Content::SystemMessage(SystemMessage::ChannelDescriptionChanged {
by: user.id.clone(),
})
.send_as_system(&target)
.await
.ok();
}
if let Some(_) = data.icon {
Content::SystemMessage(SystemMessage::ChannelIconChanged { by: user.id })
.send_as_system(&target)
.await
.ok();
}
}
if remove_icon {
if let Some(old_icon) = icon {
old_icon.delete().await?;
}
}
Ok(EmptyResponse {})
}
_ => Err(Error::InvalidOperation),
}
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -15
View File
@@ -1,20 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::Result;
use rocket::serde::json::Value;
#[get("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
Ok(json!(target))
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -19
View File
@@ -1,24 +1,8 @@
use crate::util::result::{Error, Result, EmptyResponse};
use crate::database::*;
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
#[put("/<target>/recipients/<member>")]
pub async fn req(user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
if get_relationship(&user, &member.id) != RelationshipStatus::Friend {
Err(Error::NotFriends)?
}
let channel = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_invite_others() {
Err(Error::MissingPermission)?
}
channel.add_to_group(member.id, user.id).await?;
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref, member: Ref*/ target: String, member: String) -> Result<EmptyResponse> {
todo!()
}
+3 -50
View File
@@ -1,14 +1,8 @@
use crate::database::*;
use crate::util::idempotency::IdempotencyKey;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_GROUP_SIZE;
use revolt_quark::Result;
use mongodb::bson::doc;
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::iter::FromIterator;
use ulid::Ulid;
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
@@ -23,47 +17,6 @@ pub struct Data {
}
#[post("/create", data = "<info>")]
pub async fn req(_idempotency: IdempotencyKey, user: User, info: Json<Data>) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let mut set: HashSet<String> = HashSet::from_iter(info.users.iter().cloned());
set.insert(user.id.clone());
if set.len() > *MAX_GROUP_SIZE {
Err(Error::GroupTooLarge {
max: *MAX_GROUP_SIZE,
})?
}
for target in &set {
match get_relationship(&user, target) {
RelationshipStatus::Friend | RelationshipStatus::User => {}
_ => {
return Err(Error::NotFriends);
}
}
}
let id = Ulid::new().to_string();
let channel = Channel::Group {
id,
name: info.name,
description: info.description,
owner: user.id,
recipients: set.into_iter().collect::<Vec<String>>(),
icon: None,
last_message_id: None,
permissions: None,
nsfw: info.nsfw.unwrap_or_default()
};
channel.clone().publish().await?;
Ok(json!(channel))
pub async fn req(/*_idempotency: IdempotencyKey, user: User,*/ info: Json<Data>) -> Result<Value> {
todo!();
}
+3 -61
View File
@@ -1,66 +1,8 @@
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::ClientboundNotification};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
#[delete("/<target>/recipients/<member>")]
pub async fn req(user: User, target: Ref, member: Ref) -> Result<EmptyResponse> {
if &user.id == &member.id {
Err(Error::CannotRemoveYourself)?
}
let channel = target.fetch_channel().await?;
if let Channel::Group {
id,
owner,
recipients,
..
} = &channel
{
if &user.id != owner {
// figure out if we want to use perm system here
Err(Error::MissingPermission)?
}
if recipients.iter().find(|x| *x == &member.id).is_none() {
Err(Error::NotInGroup)?
}
get_collection("channels")
.update_one(
doc! {
"_id": &id
},
doc! {
"$pull": {
"recipients": &member.id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel",
})?;
ClientboundNotification::ChannelGroupLeave {
id: id.clone(),
user: member.id.clone(),
}
.publish(id.clone());
Content::SystemMessage(SystemMessage::UserRemove {
id: member.id,
by: user.id,
})
.send_as_system(&channel)
.await
.ok();
Ok(EmptyResponse {})
} else {
Err(Error::InvalidOperation)
}
pub async fn req(/*user: UserRef, target: Ref, member: Ref*/ target: String, member: String) -> Result<EmptyResponse> {
todo!()
}
+3 -34
View File
@@ -1,8 +1,6 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::Result;
use mongodb::bson::doc;
use nanoid::nanoid;
use rocket::serde::json::Value;
lazy_static! {
@@ -14,35 +12,6 @@ lazy_static! {
}
#[post("/<target>/invites")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_invite_others() {
return Err(Error::MissingPermission);
}
let code = nanoid!(8, &*ALPHABET);
match &target {
Channel::Group { .. } => {
unimplemented!()
}
Channel::TextChannel { id, server, .. }
| Channel::VoiceChannel { id, server, .. } => {
Invite::Server {
code: code.clone(),
creator: user.id,
server: server.clone(),
channel: id.clone(),
}
.save()
.await?;
Ok(json!({ "code": code }))
}
_ => Err(Error::InvalidOperation),
}
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -18
View File
@@ -1,23 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::Result;
use rocket::serde::json::Value;
#[get("/<target>/members")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_channel().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
if let Channel::Group { recipients, .. } = target {
Ok(json!(user.fetch_multiple_users(&recipients).await?))
} else {
Err(Error::InvalidOperation)
}
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -24
View File
@@ -1,29 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
#[delete("/<target>/messages/<msg>")]
pub async fn req(user: User, target: Ref, msg: Ref) -> Result<EmptyResponse> {
let channel = target.fetch_channel().await?;
channel.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let message = msg.fetch_message(&channel).await?;
if message.author != user.id && !perm.get_manage_messages() {
match channel {
Channel::SavedMessages { .. } => unreachable!(),
_ => Err(Error::CannotEditMessage)?,
}
}
message.delete().await?;
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref, msg: Ref*/ target: String, msg: String) -> Result<EmptyResponse> {
todo!()
}
+5 -91
View File
@@ -1,6 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::routes::channels::message_send::SendableEmbed;
use revolt_quark::{EmptyResponse, Result};
use chrono::Utc;
use mongodb::bson::{Bson, Document, doc, to_document};
@@ -12,95 +10,11 @@ use validator::Validate;
pub struct Data {
#[validate(length(min = 1, max = 2000))]
content: Option<String>,
#[validate(length(min = 0, max = 10))]
embeds: Option<Vec<SendableEmbed>>
// #[validate(length(min = 0, max = 10))]
// embeds: Option<Vec<SendableEmbed>>
}
#[patch("/<target>/messages/<msg>", data = "<edit>")]
pub async fn req(user: User, target: Ref, msg: Ref, edit: Json<Data>) -> Result<EmptyResponse> {
edit.validate()
.map_err(|error| Error::FailedValidation { error })?;
let channel = target.fetch_channel().await?;
channel.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let mut message = msg.fetch_message(&channel).await?;
if message.author != user.id {
Err(Error::CannotEditMessage)?
}
let edited = Utc::now();
let mut set = doc! { "edited": Bson::DateTime(edited) };
let mut unset = doc! {};
let mut update = json!({ "edited": Bson::DateTime(edited) });
if let Some(new_content) = &edit.content {
set.insert("content", new_content.clone());
update.as_object_mut().unwrap().insert("content".to_string(), json!(new_content.clone()));
message.content = Content::Text(new_content.clone());
}
let mut new_embeds: Vec<Embed> = vec![];
if let Some(embeds) = &message.embeds {
for embed in embeds {
match embed {
Embed::Text(embed) => new_embeds.push(Embed::Text(embed.clone())),
_ => {}
}
}
}
if let Some(edited_embeds) = &edit.embeds {
new_embeds.clear();
for embed in edited_embeds {
new_embeds.push(embed.clone().into_embed(message.id.clone()).await?);
}
}
if new_embeds.len() > 0 {
let embed_docs: Vec<Document> = new_embeds
.clone()
.into_iter()
.map(|embed| to_document(&embed).unwrap())
.collect();
let obj = update.as_object_mut().unwrap();
obj.insert("embeds".to_string(), json!(embed_docs));
set.insert("embeds", embed_docs);
message.embeds = Some(new_embeds)
} else if edit.embeds.is_some() {
let obj = update.as_object_mut().unwrap();
obj.insert("embeds".to_string(), json!([]));
unset.insert("embeds", 1 as u32);
}
get_collection("messages")
.update_one(
doc! {
"_id": &message.id
},
doc! {
"$set": set,
"$unset": unset
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "message",
})?;
message.publish_update(update).await?;
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref, msg: Ref,*/ target: String, msg: String, edit: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -16
View File
@@ -1,21 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::Result;
use rocket::serde::json::Value;
#[get("/<target>/messages/<msg>")]
pub async fn req(user: User, target: Ref, msg: Ref) -> Result<Value> {
let channel = target.fetch_channel().await?;
channel.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&channel)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let message = msg.fetch_message(&channel).await?;
Ok(json!(message))
pub async fn req(/*user: UserRef, target: Ref, msg: Ref*/ target: String, msg: String) -> Result<Value> {
todo!()
}
+3 -149
View File
@@ -1,7 +1,6 @@
use std::collections::HashSet;
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::{StreamExt, try_join};
use mongodb::{
@@ -36,151 +35,6 @@ pub struct Options {
}
#[get("/<target>/messages?<options..>")]
pub async fn req(user: User, target: Ref, options: Options) -> Result<Value> {
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_channel().await?;
target.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let mut messages = vec![];
let collection = get_collection("messages");
let limit = options.limit.unwrap_or(50);
let channel = target.id();
if let Some(nearby) = &options.nearby {
let mut cursors = try_join!(
collection.find(
doc! {
"channel": channel,
"_id": {
"$gte": &nearby
}
},
FindOptions::builder()
.limit(limit / 2 + 1)
.sort(doc! {
"_id": 1
})
.build(),
),
collection.find(
doc! {
"channel": channel,
"_id": {
"$lt": &nearby
}
},
FindOptions::builder()
.limit(limit / 2)
.sort(doc! {
"_id": -1
})
.build(),
)
)
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
while let Some(result) = cursors.0.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
while let Some(result) = cursors.1.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
} else {
let mut query = doc! { "channel": target.id() };
if let Some(before) = &options.before {
query.insert("_id", doc! { "$lt": before });
}
if let Some(after) = &options.after {
query.insert("_id", doc! { "$gt": after });
}
let sort: i32 = if let Sort::Latest = options.sort.as_ref().unwrap_or_else(|| &Sort::Latest) {
-1
} else {
1
};
let mut cursor = collection
.find(
query,
FindOptions::builder()
.limit(limit)
.sort(doc! {
"_id": sort
})
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
}
if options.include_users.unwrap_or_else(|| false) {
let mut ids = HashSet::new();
for message in &messages {
message.add_associated_user_ids(&mut ids);
}
ids.remove(&user.id);
let user_ids = ids.into_iter().collect();
let users = user.fetch_multiple_users(&user_ids).await?;
if let Channel::TextChannel { server, .. } = target {
Ok(json!({
"messages": messages,
"users": users,
"members": Server::fetch_members_with_ids(&server, &user_ids).await?
}))
} else {
Ok(json!({
"messages": messages,
"users": users,
}))
}
} else {
Ok(json!(messages))
}
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, options: Options) -> Result<Value> {
todo!()
}
+3 -61
View File
@@ -1,5 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, from_document};
@@ -12,63 +11,6 @@ pub struct Options {
}
#[post("/<target>/messages/stale", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Options>) -> Result<Value> {
if data.ids.len() > 150 {
return Err(Error::TooManyIds);
}
let target = target.fetch_channel().await?;
target.has_messaging()?;
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
}))
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, data: Json<Options>) -> Result<Value> {
todo!()
}
+3 -121
View File
@@ -1,7 +1,4 @@
use std::collections::HashSet;
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::Result;
use futures::StreamExt;
use mongodb::{
@@ -42,121 +39,6 @@ pub struct Options {
}
#[post("/<target>/search", data = "<options>")]
pub async fn req(user: User, target: Ref, options: Json<Options>) -> Result<Value> {
options
.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_channel().await?;
target.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let mut messages = vec![];
let limit = options.limit.unwrap_or(50);
let mut filter = doc! {
"channel": target.id(),
"$text": {
"$search": &options.query
}
};
if let Some(doc) = match (&options.before, &options.after) {
(Some(before), Some(after)) => Some(doc! {
"lt": before,
"gt": after
}),
(Some(before), _) => Some(doc! {
"lt": before
}),
(_, Some(after)) => Some(doc! {
"gt": after
}),
_ => None
} {
filter.insert("_id", doc);
}
let mut cursor = get_collection("messages")
.find(
filter,
FindOptions::builder()
.projection(
if let Sort::Relevance = &options.sort {
doc! {
"score": {
"$meta": "textScore"
}
}
} else {
doc! {}
}
)
.limit(limit)
.sort(
match &options.sort {
Sort::Relevance => doc! {
"score": {
"$meta": "textScore"
}
},
Sort::Latest => doc! {
"_id": -1 as i32
},
Sort::Oldest => doc! {
"_id": 1 as i32
}
}
)
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "messages",
})?;
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
messages.push(
from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
operation: "from_document",
with: "message",
})?,
);
}
}
if options.include_users.unwrap_or_else(|| false) {
let mut ids = HashSet::new();
for message in &messages {
message.add_associated_user_ids(&mut ids);
}
ids.remove(&user.id);
let user_ids = ids.into_iter().collect();
let users = user.fetch_multiple_users(&user_ids).await?;
if let Channel::TextChannel { server, .. } = target {
Ok(json!({
"messages": messages,
"users": users,
"members": Server::fetch_members_with_ids(&server, &user_ids).await?
}))
} else {
Ok(json!({
"messages": messages,
"users": users,
}))
}
} else {
Ok(json!(messages))
}
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, options: Json<Options>) -> Result<Value> {
todo!()
}
+4 -137
View File
@@ -1,15 +1,9 @@
use std::collections::HashSet;
use crate::database::*;
use crate::util::idempotency::IdempotencyKey;
use crate::util::ratelimit::{Ratelimiter, RatelimitResponse};
use crate::util::result::{Error, Result};
use revolt_quark::{Result, models::message::Masquerade};
use mongodb::bson::doc;
use regex::Regex;
use rocket::serde::json::{Json, Value};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
#[derive(Serialize, Deserialize)]
@@ -29,24 +23,6 @@ pub struct SendableEmbed {
media: Option<String>,
colour: Option<String>,
}
impl SendableEmbed {
pub async fn into_embed(self, message_id: String) -> Result<Embed> {
let media = if let Some(id) = self.media {
Some(File::find_and_use(&id, "attachments", "message", &message_id).await?)
} else { None };
Ok(Embed::Text(Text {
icon_url: self.icon_url,
url: self.url,
title: self.title,
description: self.description,
media,
colour: self.colour
}))
}
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 0, max = 2000))]
@@ -55,7 +31,7 @@ pub struct Data {
attachments: Option<Vec<String>>,
nonce: Option<String>,
replies: Option<Vec<Reply>>,
#[validate]
//#[validate]
masquerade: Option<Masquerade>,
#[validate(length(min = 1, max = 10))]
embeds: Option<Vec<SendableEmbed>>
@@ -67,115 +43,6 @@ lazy_static! {
}
#[post("/<target>/messages", data = "<message>")]
pub async fn message_send(user: User, _r: Ratelimiter, mut idempotency: IdempotencyKey, target: Ref, message: Json<Data>) -> Result<RatelimitResponse<Value>> {
let message = message.into_inner();
idempotency.consume_nonce(message.nonce.clone());
message
.validate()
.map_err(|error| Error::FailedValidation { error })?;
if message.content.len() == 0
&& (message.attachments.is_none() || message.attachments.as_ref().unwrap().len() == 0)
{
return Err(Error::EmptyMessage);
}
let target = target.fetch_channel().await?;
target.has_messaging()?;
let perm = permissions::PermissionCalculator::new(&user)
.with_channel(&target)
.for_channel()
.await?;
if !perm.get_send_message() {
return Err(Error::MissingPermission)
}
let mut mentions = HashSet::new();
for capture in RE_MENTION.captures_iter(&message.content) {
if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
}
if let Some(_) = &message.masquerade {
if !perm.get_masquerade() {
return Err(Error::MissingPermission)
}
}
let mut replies = HashSet::new();
if let Some(entries) = message.replies {
// ! FIXME: move this to app config
if entries.len() > 5 {
return Err(Error::TooManyReplies)
}
for Reply { id, mention } in entries {
let message = Ref::from_unchecked(id)
.fetch_message(&target)
.await?;
replies.insert(message.id);
if mention {
mentions.insert(message.author);
}
}
}
let id = Ulid::new().to_string();
let mut attachments = vec![];
if let Some(ids) = &message.attachments {
if ids.len() > 0 && !perm.get_upload_files() {
return Err(Error::MissingPermission)
}
// ! FIXME: move this to app config
if ids.len() > 5 {
return Err(Error::TooManyAttachments)
}
for attachment_id in ids {
attachments
.push(File::find_and_use(attachment_id, "attachments", "message", &id).await?);
}
}
let mut embeds = vec![];
if let Some(sendable_embeds) = message.embeds {
for sendable_embed in sendable_embeds {
embeds.push(sendable_embed.into_embed(id.clone()).await?)
}
}
let msg = Message {
id,
channel: target.id().to_string(),
author: user.id,
content: Content::Text(message.content.clone()),
nonce: Some(idempotency.key),
edited: None,
embeds: if embeds.len() > 0 { Some(embeds) } else { None },
attachments: if attachments.len() > 0 { Some(attachments) } else { None },
mentions: if mentions.len() > 0 {
Some(mentions.into_iter().collect::<Vec<String>>())
} else {
None
},
replies: if replies.len() > 0 {
Some(replies.into_iter().collect::<Vec<String>>())
} else {
None
},
masquerade: message.masquerade
};
msg.clone().publish(&target, perm.get_embed_links()).await?;
Ok(RatelimitResponse(json!(msg)))
pub async fn message_send(/*user: UserRef, _r: Ratelimiter, mut idempotency: IdempotencyKey, target: Ref,*/ target: String, message: Json<Data>) -> /*Result<RatelimitResponse<Value>>*/ Result<Value> {
todo!()
}
+3 -55
View File
@@ -3,9 +3,7 @@ use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use validator::Contains;
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::notifications::events::ClientboundNotification;
use revolt_quark::{EmptyResponse, Result};
#[derive(Serialize, Deserialize)]
pub struct Data {
@@ -13,56 +11,6 @@ pub struct Data {
}
#[put("/<target>/permissions/<role>", data = "<data>", rank = 2)]
pub async fn req(user: User, target: Ref, role: String, data: Json<Data>) -> Result<EmptyResponse> {
let target = target.fetch_channel().await?;
match target {
Channel::TextChannel { id, server, mut role_permissions, .. }
| Channel::VoiceChannel { id, server, mut role_permissions, .. } => {
let target = Ref::from_unchecked(server).fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
return Err(Error::MissingPermission);
}
if !target.roles.has_element(&role) {
return Err(Error::NotFound);
}
let permissions: u32 = data.permissions;
get_collection("channels")
.update_one(
doc! { "_id": &id },
doc! {
"$set": {
"role_permissions.".to_owned() + &role: permissions as i32
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel"
})?;
role_permissions.insert(role, permissions as i32);
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!({
"role_permissions": role_permissions
}),
clear: None
}
.publish(id);
Ok(EmptyResponse {})
}
_ => Err(Error::InvalidOperation)
}
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, role: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -85
View File
@@ -2,10 +2,7 @@ use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use crate::database::*;
use crate::database::permissions::channel::{ ChannelPermission, DEFAULT_PERMISSION_DM };
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
#[derive(Serialize, Deserialize)]
pub struct Data {
@@ -13,85 +10,6 @@ pub struct Data {
}
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
let target = target.fetch_channel().await?;
match target {
Channel::Group { id, owner, .. } => {
if user.id == owner {
let permissions: u32 = ChannelPermission::View as u32 | (data.permissions & *DEFAULT_PERMISSION_DM);
get_collection("channels")
.update_one(
doc! { "_id": &id },
doc! {
"$set": {
"permissions": permissions as i32
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel"
})?;
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!({
"permissions": permissions as i32
}),
clear: None
}
.publish(id);
Ok(EmptyResponse {})
} else {
Err(Error::MissingPermission)
}
}
Channel::TextChannel { id, server, .. }
| Channel::VoiceChannel { id, server, .. } => {
let target = Ref::from_unchecked(server).fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
return Err(Error::MissingPermission);
}
let permissions: u32 = data.permissions;
get_collection("channels")
.update_one(
doc! { "_id": &id },
doc! {
"$set": {
"default_permissions": permissions as i32
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channel"
})?;
ClientboundNotification::ChannelUpdate {
id: id.clone(),
data: json!({
"default_permissions": permissions as i32
}),
clear: None
}
.publish(id);
Ok(EmptyResponse {})
}
_ => Err(Error::InvalidOperation)
}
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -80
View File
@@ -1,6 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL};
use revolt_quark::{Error, Result};
use rocket::serde::json::Value;
use serde::{Deserialize, Serialize};
@@ -11,81 +9,6 @@ struct CreateUserResponse {
}
#[post("/<target>/join_call")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
if !*USE_VOSO {
return Err(Error::VosoUnavailable);
}
let target = target.fetch_channel().await?;
match target {
Channel::SavedMessages { .. } | Channel::TextChannel { .. } => {
return Err(Error::CannotJoinCall)
}
_ => {}
}
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)
}
pub async fn req(/*user: UserRef, target: Ref,*/ target: String) -> Result<Value> {
todo!()
}
+3 -27
View File
@@ -1,30 +1,6 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
#[delete("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<EmptyResponse> {
let target = target.fetch_invite().await?;
if target.creator() == &user.id {
target.delete().await?;
} else {
match &target {
Invite::Server { server, .. } => {
let server = Ref::from_unchecked(server.clone()).fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if !perm.get_manage_server() {
return Err(Error::MissingPermission);
}
target.delete().await?;
}
_ => unreachable!(),
}
}
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<EmptyResponse> {
todo!()
}
+4 -35
View File
@@ -1,5 +1,5 @@
use crate::database::*;
use crate::util::result::Result;
use revolt_quark::Result;
use revolt_quark::models::File;
use rocket::serde::json::Value;
use serde::Serialize;
@@ -26,37 +26,6 @@ pub enum InviteResponse {
}
#[get("/<target>")]
pub async fn req(target: Ref) -> Result<Value> {
let target = target.fetch_invite().await?;
match target {
Invite::Server {
channel, creator, ..
} => {
let channel = Ref::from_unchecked(channel).fetch_channel().await?;
let creator = Ref::from_unchecked(creator).fetch_user().await?;
match channel {
Channel::TextChannel { id, server, name, description, .. }
| Channel::VoiceChannel { id, server, name, description, .. } => {
let server = Ref::from_unchecked(server).fetch_server().await?;
Ok(json!(InviteResponse::Server {
member_count: Server::get_member_count(&server.id).await?,
server_id: server.id,
server_name: server.name,
server_icon: server.icon,
server_banner: server.banner,
channel_id: id,
channel_name: name,
channel_description: description,
user_name: creator.username,
user_avatar: creator.avatar
}))
}
_ => unreachable!()
}
}
_ => unreachable!(),
}
pub async fn req(/*target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -37
View File
@@ -1,42 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_SERVER_COUNT;
use revolt_quark::{Error, Result};
use rocket::serde::json::Value;
#[post("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
if !User::can_acquire_server(&user.id).await? {
Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
})?
}
let target = target.fetch_invite().await?;
match target {
Invite::Server { channel, .. } => {
let channel = Ref::from_unchecked(channel).fetch_channel().await?;
let server = match &channel {
Channel::TextChannel { server, .. }
| Channel::VoiceChannel { server, .. } => {
Ref::from_unchecked(server.clone()).fetch_server().await?
}
_ => unreachable!()
};
server.join_member(&user.id).await?;
Ok(json!({
"type": "Server",
"channel": channel,
"server": server
}))
}
_ => unreachable!(),
}
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -29
View File
@@ -1,5 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
use crate::util::regex::RE_USERNAME;
use mongodb::bson::doc;
@@ -15,31 +14,6 @@ pub struct Data {
}
#[post("/complete", data = "<data>")]
pub async fn req(session: Session, user: Option<User>, data: Json<Data>) -> Result<EmptyResponse> {
if user.is_some() {
Err(Error::AlreadyOnboarded)?
}
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if User::is_username_taken(&data.username).await? {
return Err(Error::UsernameTaken);
}
get_collection("users")
.insert_one(
doc! {
"_id": session.user_id,
"username": &data.username
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "user",
})?;
Ok(EmptyResponse {})
pub async fn req(/*session: Session, user: Option<User>,*/ data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+2 -6
View File
@@ -1,11 +1,7 @@
use crate::database::*;
use rauth::entities::Session;
use rocket::serde::json::Value;
#[get("/hello")]
pub async fn req(_session: Session, user: Option<User>) -> Value {
json!({
"onboarding": user.is_none()
})
pub async fn req(/*_session: Session, user: Option<User>*/) -> Value {
todo!()
}
+4 -13
View File
@@ -1,19 +1,10 @@
use crate::database::*;
use crate::util::result::{EmptyResponse, Error, Result};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
use rauth::entities::{Model, Session, WebPushSubscription};
use rocket::serde::json::Json;
#[post("/subscribe", data = "<data>")]
pub async fn req(mut session: Session, data: Json<WebPushSubscription>) -> Result<EmptyResponse> {
session.subscription = Some(data.into_inner());
session
.save(&get_db(), None)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
operation: "save",
with: "session",
})
#[post("/subscribe"/*, data = "<data>"*/)]
pub async fn req(/*mut session: Session, data: Json<WebPushSubscription>*/) -> Result<EmptyResponse> {
todo!()
}
+3 -12
View File
@@ -1,18 +1,9 @@
use crate::database::*;
use crate::util::result::{EmptyResponse, Error, Result};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
use rauth::entities::{Model, Session};
#[post("/unsubscribe")]
pub async fn req(mut session: Session) -> Result<EmptyResponse> {
session.subscription = None;
session
.save(&get_db(), None)
.await
.map(|_| EmptyResponse)
.map_err(|_| Error::DatabaseError {
operation: "save",
with: "session",
})
pub async fn req(/*mut session: Session*/) -> Result<EmptyResponse> {
todo!()
}
+6 -5
View File
@@ -1,14 +1,15 @@
use crate::util::{ratelimit::Ratelimiter, variables::{
/*use crate::util::{ratelimit::Ratelimiter, variables::{
APP_URL, AUTUMN_URL, EXTERNAL_WS_URL, HCAPTCHA_SITEKEY, INVITE_ONLY, JANUARY_URL, USE_AUTUMN,
USE_EMAIL, USE_HCAPTCHA, USE_JANUARY, USE_VOSO, VAPID_PUBLIC_KEY, VOSO_URL, VOSO_WS_HOST,
}};
}};*/
use mongodb::bson::doc;
use rocket::{http::Status, serde::json::Value};
#[get("/")]
pub async fn root() -> Value {
json!({
todo!();
/*json!({
"revolt": crate::version::VERSION,
"features": {
"captcha": {
@@ -34,10 +35,10 @@ pub async fn root() -> Value {
"ws": *EXTERNAL_WS_URL,
"app": *APP_URL,
"vapid": *VAPID_PUBLIC_KEY
})
})*/
}
#[get("/ping")]
pub async fn ping(_limitguard: Ratelimiter) -> Status {
pub async fn ping(/*_limitguard: Ratelimiter*/) -> Status {
Status::Ok
}
+3 -48
View File
@@ -1,5 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
use rocket::serde::json::Json;
@@ -13,50 +12,6 @@ pub struct Data {
}
#[put("/<server>/bans/<target>", data = "<data>")]
pub async fn req(user: User, server: Ref, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let server = server.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if !perm.get_ban_members() {
Err(Error::MissingPermission)?
}
let target = target.fetch_user().await?;
if target.id == user.id {
return Err(Error::InvalidOperation);
}
if target.id == server.owner {
return Err(Error::MissingPermission);
}
let mut document = doc! {
"_id": {
"server": &server.id,
"user": &target.id
}
};
if let Some(reason) = data.reason {
document.insert("reason", reason);
}
get_collection("server_bans")
.insert_one(document, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "insert_one",
with: "server_ban",
})?;
server.remove_member(&target.id, RemoveMember::Ban).await?;
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, server: Ref, target: Ref,*/ server: String, target: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+4 -71
View File
@@ -1,5 +1,5 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use revolt_quark::models::File;
use futures::StreamExt;
use mongodb::options::FindOptions;
@@ -15,73 +15,6 @@ struct BannedUser {
}
#[get("/<target>/bans")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_ban_members() {
return Err(Error::MissingPermission);
}
let mut cursor = get_collection("server_bans")
.find(
doc! {
"_id.server": target.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "server_bans",
})?;
let mut bans = vec![];
let mut user_ids = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(ban) = from_document::<Ban>(doc) {
user_ids.push(ban.id.user.clone());
bans.push(ban);
}
}
}
let mut cursor = get_collection("users")
.find(
doc! {
"_id": {
"$in": user_ids
}
},
FindOptions::builder()
.projection(doc! {
"username": 1,
"avatar": 1
})
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?;
let mut users = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(user) = from_document::<BannedUser>(doc) {
users.push(user);
}
}
}
Ok(json!({
"users": users,
"bans": bans
}))
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -38
View File
@@ -1,43 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
#[delete("/<server>/bans/<target>")]
pub async fn req(user: User, server: Ref, target: Ref) -> Result<EmptyResponse> {
let server = server.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if !perm.get_ban_members() {
Err(Error::MissingPermission)?
}
if target.id == user.id {
return Err(Error::InvalidOperation);
}
if target.id == server.owner {
return Err(Error::MissingPermission);
}
let target = target.fetch_ban(&server.id).await?;
get_collection("server_bans")
.delete_one(
doc! {
"_id.server": &server.id,
"_id.user": &target.id.user
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "delete_one",
with: "server_ban",
})?;
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, server: Ref, target: Ref*/ server: String, target: String) -> Result<EmptyResponse> {
todo!()
}
+3 -69
View File
@@ -1,8 +1,6 @@
use std::collections::HashMap;
use crate::database::*;
use crate::util::idempotency::IdempotencyKey;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use mongodb::bson::doc;
use rocket::serde::json::{Json, Value};
@@ -35,70 +33,6 @@ pub struct Data {
}
#[post("/<target>/channels", data = "<info>")]
pub async fn req(_idempotency: IdempotencyKey, user: User, target: Ref, info: Json<Data>) -> Result<Value> {
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_channels() {
Err(Error::MissingPermission)?
}
let id = Ulid::new().to_string();
let channel = match info.channel_type {
ChannelType::Text => Channel::TextChannel {
id: id.clone(),
server: target.id.clone(),
name: info.name,
description: info.description,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: info.nsfw.unwrap_or_default(),
},
ChannelType::Voice => Channel::VoiceChannel {
id: id.clone(),
server: target.id.clone(),
name: info.name,
description: info.description,
icon: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: info.nsfw.unwrap_or_default()
}
};
channel.clone().publish().await?;
get_collection("servers")
.update_one(
doc! {
"_id": target.id
},
doc! {
"$addToSet": {
"channels": id
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "server",
})?;
Ok(json!(channel))
pub async fn req(/*_idempotency: IdempotencyKey, user: User, target: Ref,*/ target: String, info: Json<Data>) -> Result<Value> {
todo!()
}
+3 -37
View File
@@ -1,5 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, from_document};
@@ -15,39 +14,6 @@ pub struct ServerInvite {
}
#[get("/<target>/invites")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_server() {
Err(Error::MissingPermission)?
}
let mut cursor = get_collection("channel_invites")
.find(
doc! {
"server": target.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "channel_invites",
})?;
let mut invites = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
if let Ok(invite) = from_document::<Invite>(doc) {
invites.push(invite);
}
}
}
Ok(json!(invites))
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+4 -143
View File
@@ -1,10 +1,5 @@
use std::collections::HashSet;
use revolt_quark::{EmptyResponse, Result, models::server_member::FieldsMember};
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveMemberField};
use mongodb::bson::{doc, to_document};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -15,144 +10,10 @@ pub struct Data {
nickname: Option<String>,
avatar: Option<String>,
roles: Option<Vec<String>>,
remove: Option<RemoveMemberField>,
remove: Option<FieldsMember>,
}
#[patch("/<server>/members/<target>", data = "<data>")]
pub async fn req(user: User, server: Ref, target: String, data: Json<Data>) -> Result<EmptyResponse> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.nickname.is_none() && data.avatar.is_none() && data.roles.is_none() && data.remove.is_none() {
return Ok(EmptyResponse {});
}
let server = server.fetch_server().await?;
let target = Ref::from(target)?.fetch_member(&server.id).await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&server)
.for_server()
.await?;
if data.roles.is_some() && !perm.get_manage_roles() {
return Err(Error::MissingPermission);
}
if target.id.user == user.id {
if (data.nickname.is_some() && !perm.get_change_nickname())
|| (data.avatar.is_some() && !perm.get_change_avatar())
{
return Err(Error::MissingPermission);
}
if let Some(remove) = &data.remove {
if match remove {
RemoveMemberField::Avatar => !perm.get_change_avatar(),
RemoveMemberField::Nickname => !perm.get_change_nickname(),
} {
return Err(Error::MissingPermission);
}
}
} else {
if data.avatar.is_some() || (data.nickname.is_some() && !perm.get_manage_nicknames()) {
return Err(Error::MissingPermission);
}
if let Some(remove) = &data.remove {
if match remove {
RemoveMemberField::Avatar => !perm.get_remove_avatars(),
RemoveMemberField::Nickname => !perm.get_manage_nicknames(),
} {
return Err(Error::MissingPermission);
}
}
}
let mut set = doc! {};
let mut unset = doc! {};
let mut remove_avatar = false;
if let Some(remove) = &data.remove {
match remove {
RemoveMemberField::Avatar => {
unset.insert("avatar", 1);
remove_avatar = true;
}
RemoveMemberField::Nickname => {
unset.insert("nickname", 1);
}
}
}
if let Some(name) = &data.nickname {
set.insert("nickname", name);
}
if let Some(attachment_id) = &data.avatar {
let attachment =
File::find_and_use(&attachment_id, "avatars", "user", &target.id.user).await?;
set.insert(
"avatar",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_avatar = true;
}
if let Some(role_ids) = &data.roles {
let mut ids = HashSet::new();
for role in role_ids {
if server.roles.contains_key(role) {
ids.insert(role.clone());
}
}
set.insert("roles", ids.into_iter().collect::<Vec<String>>());
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("server_members")
.update_one(
doc! { "_id.server": &server.id, "_id.user": &target.id.user },
operations,
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "server_member",
})?;
}
ClientboundNotification::ServerMemberUpdate {
id: target.id.clone(),
data: json!(set),
clear: data.remove,
}
.publish(server.id.clone());
let Member { avatar, .. } = target;
if remove_avatar {
if let Some(old_avatar) = avatar {
old_avatar.delete().await?;
}
}
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, server: Ref,*/ server: String, target: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -15
View File
@@ -1,21 +1,9 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use mongodb::bson::doc;
use rocket::serde::json::Value;
#[get("/<target>/members/<member>")]
pub async fn req(user: User, target: Ref, member: String) -> Result<Value> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
Ok(json!(Ref::from(member)?.fetch_member(&target.id).await?))
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, member: String) -> Result<Value> {
todo!()
}
+3 -45
View File
@@ -1,51 +1,9 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, from_document, Document};
use rocket::serde::json::Value;
// ! FIXME: this is a temporary route while permissions are being worked on.
#[get("/<target>/members")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
let members = get_collection("server_members")
.find(
doc! {
"_id.server": target.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>>();
let member_ids = members
.iter()
.map(|m| m.id.user.clone())
.collect::<Vec<String>>();
Ok(json!({
"members": members,
"users": user.fetch_multiple_users(&member_ids).await?
}))
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -28
View File
@@ -1,33 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
#[delete("/<target>/members/<member>")]
pub async fn req(user: User, target: Ref, member: String) -> Result<EmptyResponse> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_kick_members() {
return Err(Error::MissingPermission);
}
let member = Ref::from(member)?.fetch_member(&target.id).await?;
if member.id.user == user.id {
return Err(Error::InvalidOperation);
}
if member.id.user == target.owner {
return Err(Error::MissingPermission);
}
target
.remove_member(&member.id.user, RemoveMember::Kick)
.await?;
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, member: String) -> Result<EmptyResponse> {
todo!()
}
+3 -55
View File
@@ -2,9 +2,7 @@ use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
#[derive(Serialize, Deserialize)]
pub struct Values {
@@ -18,56 +16,6 @@ pub struct Data {
}
#[put("/<target>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn req(user: User, target: Ref, role_id: String, data: Json<Data>) -> Result<EmptyResponse> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
return Err(Error::MissingPermission);
}
if !target.roles.contains_key(&role_id) {
return Err(Error::NotFound);
}
let server_permissions: u32 = data.permissions.server;
let channel_permissions: u32 = data.permissions.channel;
get_collection("servers")
.update_one(
doc! { "_id": &target.id },
doc! {
"$set": {
"roles.".to_owned() + &role_id + &".permissions": [
server_permissions as i32,
channel_permissions as i32
]
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "server"
})?;
ClientboundNotification::ServerRoleUpdate {
id: target.id.clone(),
role_id,
data: json!({
"permissions": [
server_permissions as i32,
channel_permissions as i32
]
}),
clear: None
}
.publish(target.id);
Ok(EmptyResponse)
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, role_id: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -50
View File
@@ -2,9 +2,7 @@ use mongodb::bson::doc;
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
#[derive(Serialize, Deserialize)]
pub struct Values {
@@ -18,51 +16,6 @@ pub struct Data {
}
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
return Err(Error::MissingPermission);
}
let server_permissions: u32 = data.permissions.server;
let channel_permissions: u32 = data.permissions.channel;
get_collection("servers")
.update_one(
doc! { "_id": &target.id },
doc! {
"$set": {
"default_permissions": [
server_permissions as i32,
channel_permissions as i32
]
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "server"
})?;
ClientboundNotification::ServerUpdate {
id: target.id.clone(),
data: json!({
"default_permissions": [
server_permissions as i32,
channel_permissions as i32
]
}),
clear: None
}
.publish(target.id);
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -61
View File
@@ -1,6 +1,4 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use ulid::Ulid;
use mongodb::bson::doc;
@@ -15,62 +13,6 @@ pub struct Data {
}
#[post("/<target>/roles", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<Value> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
Err(Error::MissingPermission)?
}
let id = Ulid::new().to_string();
let perm_tuple = (
*permissions::server::DEFAULT_PERMISSION as i32,
*permissions::channel::DEFAULT_PERMISSION_SERVER as i32
);
get_collection("servers")
.update_one(
doc! {
"_id": &target.id
},
doc! {
"$set": {
"roles.".to_owned() + &id: {
"name": &data.name,
"permissions": [
&perm_tuple.0,
&perm_tuple.1
]
}
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "servers"
})?;
ClientboundNotification::ServerRoleUpdate {
id: target.id.clone(),
role_id: id.clone(),
data: json!({
"name": data.name,
"permissions": &perm_tuple
}),
clear: None
}
.publish(target.id);
Ok(json!({ "id": id, "permissions": perm_tuple }))
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, data: Json<Data>) -> Result<Value> {
todo!()
}
+3 -78
View File
@@ -1,81 +1,6 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use mongodb::bson::doc;
use revolt_quark::{EmptyResponse, Result};
#[delete("/<target>/roles/<role_id>")]
pub async fn req(user: User, target: Ref, role_id: String) -> Result<EmptyResponse> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
Err(Error::MissingPermission)?
}
get_collection("servers")
.update_one(
doc! {
"_id": &target.id
},
doc! {
"$unset": {
"roles.".to_owned() + &role_id: 1
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "servers"
})?;
get_collection("channels")
.update_one(
doc! {
"server": &target.id
},
doc! {
"$unset": {
"role_permissions.".to_owned() + &role_id: 1
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "channels"
})?;
get_collection("server_members")
.update_many(
doc! {
"_id.server": &target.id
},
doc! {
"$pull": {
"roles": &role_id
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_many",
with: "server_members"
})?;
ClientboundNotification::ServerRoleDelete {
id: target.id.clone(),
role_id
}
.publish(target.id);
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, role_id: String) -> Result<EmptyResponse> {
todo!()
}
+4 -92
View File
@@ -1,6 +1,4 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveRoleField};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
use rocket::serde::json::Json;
@@ -15,96 +13,10 @@ pub struct Data {
colour: Option<String>,
hoist: Option<bool>,
rank: Option<i64>,
remove: Option<RemoveRoleField>,
// remove: Option<FieldsRole>,
}
#[patch("/<target>/roles/<role_id>", data = "<data>")]
pub async fn req(user: User, target: Ref, role_id: String, data: Json<Data>) -> Result<EmptyResponse> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.name.is_none() && data.colour.is_none() && data.hoist.is_none() && data.rank.is_none() && data.remove.is_none()
{
return Ok(EmptyResponse {});
}
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_roles() {
return Err(Error::MissingPermission)
}
if !target.roles.contains_key(&role_id) {
return Err(Error::InvalidRole)
}
let mut set = doc! {};
let mut unset = doc! {};
// ! FIXME: we should probably just require clients to support basic MQL incl. $set / $unset
let mut set_update = doc! {};
let role_key = "roles.".to_owned() + &role_id;
if let Some(remove) = &data.remove {
match remove {
RemoveRoleField::Colour => {
unset.insert(role_key.clone() + ".colour", 1);
}
}
}
if let Some(name) = &data.name {
set.insert(role_key.clone() + ".name", name);
set_update.insert("name", name);
}
if let Some(colour) = &data.colour {
set.insert(role_key.clone() + ".colour", colour);
set_update.insert("colour", colour);
}
if let Some(hoist) = &data.hoist {
set.insert(role_key.clone() + ".hoist", hoist);
set_update.insert("hoist", hoist);
}
if let Some(rank) = &data.rank {
set.insert(role_key.clone() + ".rank", rank);
set_update.insert("rank", rank);
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("servers")
.update_one(doc! { "_id": &target.id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "server",
})?;
}
ClientboundNotification::ServerRoleUpdate {
id: target.id.clone(),
role_id,
data: json!(set_update),
clear: data.remove,
}
.publish(target.id.clone());
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, role_id: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -20
View File
@@ -1,23 +1,6 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
#[put("/<target>/ack")]
pub async fn req(user: User, target: Ref) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
target.mark_as_read(&user.id).await?;
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<EmptyResponse> {
todo!()
}
+3 -73
View File
@@ -1,9 +1,6 @@
use std::collections::HashMap;
use crate::database::*;
use crate::util::idempotency::IdempotencyKey;
use crate::util::result::{Error, Result};
use crate::util::variables::MAX_SERVER_COUNT;
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
use rocket::serde::json::{Json, Value};
@@ -22,73 +19,6 @@ pub struct Data {
}
#[post("/create", data = "<info>")]
pub async fn req(_idempotency: IdempotencyKey, user: User, info: Json<Data>) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
if !User::can_acquire_server(&user.id).await? {
Err(Error::TooManyServers {
max: *MAX_SERVER_COUNT,
})?
}
let info = info.into_inner();
info.validate()
.map_err(|error| Error::FailedValidation { error })?;
let id = Ulid::new().to_string();
let cid = Ulid::new().to_string();
let server = Server {
id: id.clone(),
owner: user.id.clone(),
name: info.name,
description: info.description,
channels: vec![cid.clone()],
categories: None,
system_messages: Some(SystemMessageChannels {
user_joined: Some(cid.clone()),
user_left: Some(cid.clone()),
user_kicked: Some(cid.clone()),
user_banned: Some(cid.clone()),
}),
roles: HashMap::new(),
default_permissions: (
*permissions::server::DEFAULT_PERMISSION as i32,
*permissions::channel::DEFAULT_PERMISSION_SERVER as i32
),
icon: None,
banner: None,
flags: None,
nsfw: info.nsfw.unwrap_or_default(),
analytics: false,
discoverable: false,
};
Channel::TextChannel {
id: cid,
server: id,
name: "general".to_string(),
description: None,
icon: None,
last_message_id: None,
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: false
}
.publish()
.await?;
server.clone().create().await?;
server.join_member(&user.id).await?;
Ok(json!(server))
pub async fn req(/*_idempotency: IdempotencyKey, user: User,*/ info: Json<Data>) -> Result<Value> {
todo!()
}
+3 -20
View File
@@ -1,25 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
use mongodb::bson::doc;
#[delete("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<EmptyResponse> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_view() {
return Err(Error::MissingPermission);
}
if user.id == target.owner {
target.delete().await?;
} else {
target.remove_member(&user.id, RemoveMember::Leave).await?;
}
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<EmptyResponse> {
todo!()
}
+4 -138
View File
@@ -1,8 +1,5 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveServerField};
use revolt_quark::{EmptyResponse, Result, models::server::{FieldsServer, SystemMessageChannels, Category}};
use mongodb::bson::{doc, to_bson, to_document};
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -17,143 +14,12 @@ pub struct Data {
banner: Option<String>,
categories: Option<Vec<Category>>,
system_messages: Option<SystemMessageChannels>,
remove: Option<RemoveServerField>,
remove: Option<FieldsServer>,
nsfw: Option<bool>,
analytics: Option<bool>,
}
#[patch("/<target>", data = "<data>")]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<EmptyResponse> {
let data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.name.is_none() && data.description.is_none() && data.icon.is_none() && data.banner.is_none() && data.remove.is_none() && data.categories.is_none() && data.system_messages.is_none() && data.nsfw.is_none() && data.analytics.is_none()
{
return Ok(EmptyResponse {});
}
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_manage_server() {
Err(Error::MissingPermission)?
}
let mut set = doc! {};
let mut unset = doc! {};
let mut remove_icon = false;
let mut remove_banner = false;
if let Some(remove) = &data.remove {
match remove {
RemoveServerField::Icon => {
unset.insert("icon", 1);
remove_icon = true;
}
RemoveServerField::Banner => {
unset.insert("banner", 1);
remove_banner = true;
}
RemoveServerField::Description => {
unset.insert("description", 1);
}
}
}
if let Some(name) = &data.name {
set.insert("name", name);
}
if let Some(description) = &data.description {
set.insert("description", description);
}
if let Some(attachment_id) = &data.icon {
let attachment = File::find_and_use(&attachment_id, "icons", "object", &target.id).await?;
set.insert(
"icon",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_icon = true;
}
if let Some(attachment_id) = &data.banner {
let attachment =
File::find_and_use(&attachment_id, "banners", "server", &target.id).await?;
set.insert(
"banner",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_banner = true;
}
if let Some(categories) = &data.categories {
set.insert("categories", to_bson(&categories).map_err(|_| Error::DatabaseError { operation: "to_document", with: "categories" })?);
}
if let Some(system_messages) = &data.system_messages {
set.insert("system_messages", to_bson(&system_messages).map_err(|_| Error::DatabaseError { operation: "to_document", with: "system_messages" })?);
}
if let Some(nsfw) = &data.nsfw {
set.insert("nsfw", nsfw);
}
if let Some(analytics) = &data.analytics {
set.insert("analytics", analytics);
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("servers")
.update_one(doc! { "_id": &target.id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "server",
})?;
}
ClientboundNotification::ServerUpdate {
id: target.id.clone(),
data: json!(set),
clear: data.remove,
}
.publish(target.id.clone());
let Server { icon, banner, .. } = target;
if remove_icon {
if let Some(old_icon) = icon {
old_icon.delete().await?;
}
}
if remove_banner {
if let Some(old_banner) = banner {
old_banner.delete().await?;
}
}
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef, target: Ref,*/ target: String, data: Json<Data>) -> Result<EmptyResponse> {
todo!()
}
+3 -15
View File
@@ -1,20 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use rocket::serde::json::Value;
#[get("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_server().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_server(&target)
.for_server()
.await?;
if !perm.get_view() {
Err(Error::MissingPermission)?
}
Ok(json!(target))
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -33
View File
@@ -1,5 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use mongodb::bson::doc;
use mongodb::options::FindOneOptions;
@@ -12,35 +11,6 @@ pub struct Options {
}
#[post("/settings/fetch", data = "<options>")]
pub async fn req(user: User, options: Json<Options>) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let options = options.into_inner();
let mut projection = doc! {
"_id": 0,
};
for key in options.keys {
projection.insert(key, 1);
}
if let Some(doc) = get_collection("user_settings")
.find_one(
doc! {
"_id": user.id
},
FindOneOptions::builder().projection(projection).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "user_settings",
})?
{
Ok(json!(doc))
} else {
Ok(json!({}))
}
pub async fn req(/*user: UserRef,*/ options: Json<Options>) -> Result<Value> {
todo!()
}
+3 -8
View File
@@ -1,14 +1,9 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use mongodb::bson::doc;
use rocket::serde::json::Value;
#[get("/unreads")]
pub async fn req(user: User) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
Ok(json!(User::fetch_unreads(&user.id).await?))
pub async fn req(/*user: UserRef*/) -> Result<Value> {
todo!()
}
+3 -56
View File
@@ -1,6 +1,4 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{EmptyResponse, Error, Result};
use revolt_quark::{EmptyResponse, Result};
use chrono::prelude::*;
use mongodb::bson::{doc, to_bson};
@@ -18,57 +16,6 @@ pub struct Options {
}
#[post("/settings/set?<options..>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, options: Options) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot);
}
let data = data.into_inner();
let current_time = Utc::now().timestamp_millis();
let timestamp = if let Some(timestamp) = options.timestamp {
if timestamp > current_time {
current_time
} else {
timestamp
}
} else {
current_time
};
let mut set = doc! {};
for (key, data) in &data {
set.insert(
key.clone(),
vec![
to_bson(&timestamp).unwrap(),
to_bson(&data.clone()).unwrap(),
],
);
}
if set.len() > 0 {
get_collection("user_settings")
.update_one(
doc! {
"_id": &user.id
},
doc! {
"$set": &set
},
UpdateOptions::builder().upsert(true).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user_settings",
})?;
}
ClientboundNotification::UserSettingsUpdate {
id: user.id.clone(),
update: json!(set),
}
.publish(user.id);
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef,*/ data: Json<Data>, options: Options) -> Result<EmptyResponse> {
todo!()
}
+3 -160
View File
@@ -1,6 +1,4 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
@@ -8,161 +6,6 @@ use mongodb::options::{Collation, FindOneOptions};
use rocket::serde::json::Value;
#[put("/<username>/friend")]
pub async fn req(user: User, username: String) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let col = get_collection("users");
let doc = col
.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",
})?
.ok_or_else(|| Error::UnknownUser)?;
let target_id = doc.get_str("_id").map_err(|_| Error::DatabaseError {
operation: "get_str(_id)",
with: "user",
})?;
let target_user = Ref::from(target_id.to_string())?.fetch_user().await?;
if target_user.bot.is_some() {
return Err(Error::IsBot)
}
match get_relationship(&user, &target_id) {
RelationshipStatus::User => return Err(Error::NoEffect),
RelationshipStatus::Friend => return Err(Error::AlreadyFriends),
RelationshipStatus::Outgoing => return Err(Error::AlreadySentRequest),
RelationshipStatus::Blocked => return Err(Error::Blocked),
RelationshipStatus::BlockedOther => return Err(Error::BlockedByOther),
RelationshipStatus::Incoming => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id,
"relations._id": target_id
},
doc! {
"$set": {
"relations.$.status": "Friend"
}
},
None
),
col.update_one(
doc! {
"_id": target_id,
"relations._id": &user.id
},
doc! {
"$set": {
"relations.$.status": "Friend"
}
},
None
)
) {
Ok(_) => {
let target_user = target_user
.from_override(&user, RelationshipStatus::Friend)
.await?;
let user = user
.from_override(&target_user, RelationshipStatus::Friend)
.await?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target_user,
status: RelationshipStatus::Friend,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.to_string(),
user,
status: RelationshipStatus::Friend,
}
.publish(target_id.to_string());
Ok(json!({ "status": "Friend" }))
}
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
}
}
RelationshipStatus::None => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id
},
doc! {
"$push": {
"relations": {
"_id": target_id,
"status": "Outgoing"
}
}
},
None
),
col.update_one(
doc! {
"_id": target_id
},
doc! {
"$push": {
"relations": {
"_id": &user.id,
"status": "Incoming"
}
}
},
None
)
) {
Ok(_) => {
let target_user = target_user
.from_override(&user, RelationshipStatus::Outgoing)
.await?;
let user = user
.from_override(&target_user, RelationshipStatus::Incoming)
.await?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target_user,
status: RelationshipStatus::Outgoing,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.to_string(),
user,
status: RelationshipStatus::Incoming,
}
.publish(target_id.to_string());
Ok(json!({ "status": "Outgoing" }))
}
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
}
}
}
pub async fn req(/*user: UserRef,*/ username: String) -> Result<Value> {
todo!()
}
+3 -164
View File
@@ -1,171 +1,10 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
use rocket::serde::json::Value;
#[put("/<target>/block")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let col = get_collection("users");
let target = target.fetch_user().await?;
match get_relationship(&user, &target.id) {
RelationshipStatus::User | RelationshipStatus::Blocked => Err(Error::NoEffect),
RelationshipStatus::BlockedOther => {
col.update_one(
doc! {
"_id": &user.id,
"relations._id": &target.id
},
doc! {
"$set": {
"relations.$.status": "Blocked"
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
Ok(json!({ "status": "Blocked" }))
}
RelationshipStatus::None => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id
},
doc! {
"$push": {
"relations": {
"_id": &target.id,
"status": "Blocked"
}
}
},
None
),
col.update_one(
doc! {
"_id": &target.id
},
doc! {
"$push": {
"relations": {
"_id": &user.id,
"status": "BlockedOther"
}
}
},
None
)
) {
Ok(_) => {
let target = target
.from_override(&user, RelationshipStatus::Blocked)
.await?;
let user = user
.from_override(&target, RelationshipStatus::BlockedOther)
.await?;
let target_id = target.id.clone();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::BlockedOther,
}
.publish(target_id);
Ok(json!({ "status": "Blocked" }))
}
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
}
}
RelationshipStatus::Friend
| RelationshipStatus::Incoming
| RelationshipStatus::Outgoing => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id,
"relations._id": &target.id
},
doc! {
"$set": {
"relations.$.status": "Blocked"
}
},
None
),
col.update_one(
doc! {
"_id": &target.id,
"relations._id": &user.id
},
doc! {
"$set": {
"relations.$.status": "BlockedOther"
}
},
None
)
) {
Ok(_) => {
let target = target
.from_override(&user, RelationshipStatus::Blocked)
.await?;
let user = user
.from_override(&target, RelationshipStatus::BlockedOther)
.await?;
let target_id = target.id.clone();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::Blocked,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::BlockedOther,
}
.publish(target_id);
Ok(json!({ "status": "Blocked" }))
}
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
}
}
}
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -41
View File
@@ -1,6 +1,4 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use revolt_quark::{EmptyResponse, Result};
use crate::util::regex::RE_USERNAME;
use mongodb::bson::doc;
use rauth::entities::Account;
@@ -19,45 +17,9 @@ pub struct Data {
#[patch("/<_ignore_id>/username", data = "<data>")]
pub async fn req(
account: Account,
user: User,
//user: UserRef,
data: Json<Data>,
_ignore_id: String,
) -> Result<EmptyResponse> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
account.verify_password(&data.password)
.map_err(|_| Error::InvalidCredentials)?;
let mut set = doc! {};
if let Some(username) = &data.username {
if (username.to_lowercase() != user.username.to_lowercase()) && 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!({
"username": data.username
}),
clear: None,
}
.publish_as_user(user.id.clone());
Ok(EmptyResponse {})
todo!()
}
+6 -129
View File
@@ -1,8 +1,7 @@
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result, EmptyResponse};
use crate::{database::*, notifications::events::RemoveUserField};
use revolt_quark::{EmptyResponse, Result, models::user::FieldsUser};
use mongodb::bson::{doc, to_document};
use revolt_quark::models::user::UserStatus;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
@@ -19,138 +18,16 @@ pub struct UserProfileData {
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate]
// #[validate]
status: Option<UserStatus>,
#[validate]
profile: Option<UserProfileData>,
#[validate(length(min = 1, max = 128))]
avatar: Option<String>,
remove: Option<RemoveUserField>,
remove: Option<FieldsUser>,
}
#[patch("/<_ignore_id>", data = "<data>")]
pub async fn req(user: User, data: Json<Data>, _ignore_id: String) -> Result<EmptyResponse> {
let mut data = data.into_inner();
data.validate()
.map_err(|error| Error::FailedValidation { error })?;
if data.status.is_none()
&& data.profile.is_none()
&& data.avatar.is_none()
&& data.remove.is_none()
{
return Ok(EmptyResponse {});
}
let mut unset = doc! {};
let mut set = doc! {};
let mut remove_background = false;
let mut remove_avatar = false;
if let Some(remove) = &data.remove {
match remove {
RemoveUserField::ProfileContent => {
unset.insert("profile.content", 1);
}
RemoveUserField::ProfileBackground => {
unset.insert("profile.background", 1);
remove_background = true;
}
RemoveUserField::StatusText => {
unset.insert("status.text", 1);
}
RemoveUserField::Avatar => {
unset.insert("avatar", 1);
remove_avatar = true;
}
}
}
if let Some(status) = &data.status {
set.insert(
"status",
to_document(&status).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "status",
})?,
);
}
if let Some(profile) = data.profile {
if let Some(content) = profile.content {
set.insert("profile.content", content);
}
if let Some(attachment_id) = profile.background {
let attachment =
File::find_and_use(&attachment_id, "backgrounds", "user", &user.id).await?;
set.insert(
"profile.background",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_background = true;
}
}
let avatar = std::mem::replace(&mut data.avatar, None);
if let Some(attachment_id) = avatar {
let attachment = File::find_and_use(&attachment_id, "avatars", "user", &user.id).await?;
set.insert(
"avatar",
to_document(&attachment).map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "attachment",
})?,
);
remove_avatar = true;
}
let mut operations = doc! {};
if set.len() > 0 {
operations.insert("$set", &set);
}
if unset.len() > 0 {
operations.insert("$unset", unset);
}
if operations.len() > 0 {
get_collection("users")
.update_one(doc! { "_id": &user.id }, operations, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "update_one",
with: "user",
})?;
}
ClientboundNotification::UserUpdate {
id: user.id.clone(),
data: json!(set),
clear: data.remove,
}
.publish_as_user(user.id.clone());
if remove_avatar {
if let Some(old_avatar) = user.avatar {
old_avatar.delete().await?;
}
}
if remove_background {
if let Some(profile) = user.profile {
if let Some(old_background) = profile.background {
old_background.delete().await?;
}
}
}
Ok(EmptyResponse {})
pub async fn req(/*user: UserRef,*/ data: Json<Data>, _ignore_id: String) -> Result<EmptyResponse> {
todo!()
}
+3 -34
View File
@@ -1,40 +1,9 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::StreamExt;
use mongodb::bson::doc;
use rocket::serde::json::Value;
#[get("/dms")]
pub async fn req(user: User) -> Result<Value> {
let mut cursor = get_collection("channels")
.find(
doc! {
"$or": [
{
"channel_type": "DirectMessage",
"active": true
},
{
"channel_type": "Group"
}
],
"recipients": user.id
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "channels",
})?;
let mut channels = vec![];
while let Some(result) = cursor.next().await {
if let Ok(doc) = result {
channels.push(doc);
}
}
Ok(json!(channels))
pub async fn req(/*user: UserRef*/) -> Result<Value> {
todo!()
}
+3 -18
View File
@@ -1,24 +1,9 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use mongodb::bson::doc;
use rocket::serde::json::Value;
#[get("/<target>/profile")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
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!({}))
}
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -8
View File
@@ -1,13 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use rocket::serde::json::Value;
#[get("/<target>/relationship")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
Ok(json!({ "status": get_relationship(&user, &target.id) }))
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -12
View File
@@ -1,17 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use rocket::serde::json::Value;
#[get("/relationships")]
pub async fn req(user: User) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
Ok(if let Some(vec) = user.relations {
json!(vec)
} else {
json!([])
})
pub async fn req(/*user: UserRef*/) -> Result<Value> {
todo!()
}
+3 -4
View File
@@ -1,9 +1,8 @@
use crate::database::*;
use crate::util::result::{Result};
use revolt_quark::Result;
use rocket::serde::json::Value;
#[get("/@me")]
pub async fn req(user: User) -> Result<Value> {
Ok(json!(user))
pub async fn req(/*user: UserRef*/) -> Result<Value> {
todo!()
}
+3 -15
View File
@@ -1,20 +1,8 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use rocket::serde::json::Value;
#[get("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let target = target.fetch_user().await?;
let perm = permissions::PermissionCalculator::new(&user)
.with_user(&target)
.for_user_given()
.await?;
if !perm.get_access() {
Err(Error::MissingPermission)?
}
Ok(json!(target.from(&user).with(perm)))
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -54
View File
@@ -1,5 +1,4 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::StreamExt;
use mongodb::bson::{doc, Document};
@@ -7,56 +6,6 @@ use mongodb::options::FindOptions;
use rocket::serde::json::Value;
#[get("/<target>/mutual")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let users = get_collection("users")
.find(
doc! {
"$and": [
{ "relations": { "$elemMatch": { "_id": &user.id, "status": "Friend" } } },
{ "relations": { "$elemMatch": { "_id": &target.id, "status": "Friend" } } }
]
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find",
with: "users",
})?
.filter_map(async move |s| s.ok())
.collect::<Vec<Document>>()
.await
.into_iter()
.filter_map(|x| x.get_str("_id").ok().map(|x| x.to_string()))
.collect::<Vec<String>>();
let server_ids = User::fetch_server_ids(&user.id).await?;
let servers = get_collection("server_members")
.find(
doc! {
"_id.user": &target.id,
"_id.server": {
"$in": server_ids
}
},
None
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
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("server").ok().map(|x| x.to_string()))
})
.flatten()
.collect::<Vec<String>>();
Ok(json!({ "users": users, "servers": servers }))
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+2 -4
View File
@@ -3,8 +3,6 @@ use rocket::response::{self, Responder};
use rocket::fs::NamedFile;
use std::path::Path;
use crate::database::Ref;
pub struct CachedFile(NamedFile);
pub static CACHE_CONTROL: &'static str = "public, max-age=31536000, immutable";
@@ -18,8 +16,8 @@ impl<'r> Responder<'r, 'static> for CachedFile {
}
#[get("/<target>/default_avatar")]
pub async fn req(target: Ref) -> Option<CachedFile> {
match target.id.chars().nth(25).unwrap() {
pub async fn req(target: String) -> Option<CachedFile> {
match target.chars().nth(25).unwrap() {
'0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' => {
NamedFile::open(Path::new("assets/user_red.png")).await.ok().map(|n| CachedFile(n))
}
+3 -43
View File
@@ -1,50 +1,10 @@
use crate::database::*;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use mongodb::bson::doc;
use rocket::serde::json::Value;
use ulid::Ulid;
#[get("/<target>/dm")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
let query = if user.id == target.id {
doc! {
"channel_type": "SavedMessages",
"user": &user.id
}
} else {
doc! {
"channel_type": "DirectMessage",
"recipients": {
"$all": [ &user.id, &target.id ]
}
}
};
let existing_channel = get_collection("channels")
.find_one(query, None)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "channel",
})?;
if let Some(doc) = existing_channel {
Ok(json!(doc))
} else {
let id = Ulid::new().to_string();
let channel = if user.id == target.id {
Channel::SavedMessages { id, user: user.id }
} else {
Channel::DirectMessage {
id,
active: false,
recipients: vec![user.id, target.id],
last_message_id: None,
}
};
channel.clone().publish().await?;
Ok(json!(channel))
}
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}
+3 -76
View File
@@ -1,83 +1,10 @@
use crate::database::*;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
use revolt_quark::{Error, Result};
use futures::try_join;
use mongodb::bson::doc;
use rocket::serde::json::Value;
#[delete("/<target>/friend")]
pub async fn req(user: User, target: Ref) -> Result<Value> {
if user.bot.is_some() {
return Err(Error::IsBot)
}
let col = get_collection("users");
let target = target.fetch_user().await?;
match get_relationship(&user, &target.id) {
RelationshipStatus::Friend
| RelationshipStatus::Outgoing
| RelationshipStatus::Incoming => {
match try_join!(
col.update_one(
doc! {
"_id": &user.id
},
doc! {
"$pull": {
"relations": {
"_id": &target.id
}
}
},
None
),
col.update_one(
doc! {
"_id": &target.id
},
doc! {
"$pull": {
"relations": {
"_id": &user.id
}
}
},
None
)
) {
Ok(_) => {
let target = target
.from_override(&user, RelationshipStatus::None)
.await?;
let user = user
.from_override(&target, RelationshipStatus::None)
.await?;
let target_id = target.id.clone();
ClientboundNotification::UserRelationship {
id: user.id.clone(),
user: target,
status: RelationshipStatus::None,
}
.publish(user.id.clone());
ClientboundNotification::UserRelationship {
id: target_id.clone(),
user,
status: RelationshipStatus::None,
}
.publish(target_id);
Ok(json!({ "status": "None" }))
}
Err(_) => Err(Error::DatabaseError {
operation: "update_one",
with: "user",
}),
}
}
_ => Err(Error::NoEffect),
}
pub async fn req(/*user: UserRef, target: Ref*/ target: String) -> Result<Value> {
todo!()
}

Some files were not shown because too many files have changed in this diff Show More