Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 995 additions and 1014 deletions
use std::collections::HashSet;
use crate::database::*;
use crate::util::result::{Error, Result};
use futures::StreamExt;
use mongodb::{
bson::{doc, from_document},
options::FindOptions,
};
use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Serialize, Deserialize, FromFormValue)]
pub enum Sort {
Relevance,
Latest,
Oldest,
}
impl Default for Sort {
fn default() -> Sort {
Sort::Relevance
}
}
#[derive(Validate, Serialize, Deserialize, FromForm)]
pub struct Options {
#[validate(length(min = 1, max = 64))]
query: String,
#[validate(range(min = 1, max = 100))]
limit: Option<i64>,
#[validate(length(min = 26, max = 26))]
before: Option<String>,
#[validate(length(min = 26, max = 26))]
after: Option<String>,
#[serde(default = "Sort::default")]
sort: Sort,
include_users: Option<bool>,
}
#[post("/<target>/search", data = "<options>")]
pub async fn req(user: User, target: Ref, options: Json<Options>) -> Result<JsonValue> {
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
},
Sort::Oldest => doc! {
"_id": 1
}
}
)
.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 {
ids.insert(message.author.clone());
}
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(&server).await?
}))
} else {
Ok(json!({
"messages": messages,
"users": users,
}))
}
} else {
Ok(json!(messages))
}
}
use std::collections::HashSet;
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::{bson::doc, options::FindOneOptions};
use regex::Regex;
use rocket_contrib::json::{Json, JsonValue};
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use validator::Validate;
#[derive(Serialize, Deserialize)]
pub struct Reply {
id: String,
mention: bool
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 0, max = 2000))]
content: String,
// Maximum length of 36 allows both ULIDs and UUIDs.
#[validate(length(min = 1, max = 36))]
nonce: String,
#[validate(length(min = 1, max = 128))]
attachments: Option<Vec<String>>,
replies: Option<Vec<Reply>>,
}
lazy_static! {
static ref RE_ULID: Regex = Regex::new(r"<@([0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26})>").unwrap();
}
#[post("/<target>/messages", data = "<message>")]
pub async fn req(user: User, target: Ref, message: Json<Data>) -> Result<JsonValue> {
let message = message.into_inner();
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)
}
if get_collection("messages")
.find_one(
doc! {
"nonce": &message.nonce
},
FindOneOptions::builder()
.projection(doc! { "_id": 1 })
.build(),
)
.await
.map_err(|_| Error::DatabaseError {
operation: "find_one",
with: "message",
})?
.is_some()
{
Err(Error::DuplicateNonce)?
}
let id = Ulid::new().to_string();
let mut mentions = HashSet::new();
if let Some(captures) = RE_ULID.captures_iter(&message.content).next() {
// ! FIXME: in the future, verify in group so we can send out push
mentions.insert(captures[1].to_string());
}
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 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 msg = Message {
id,
channel: target.id().to_string(),
author: user.id,
content: Content::Text(message.content.clone()),
nonce: Some(message.nonce.clone()),
edited: None,
embeds: 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
},
};
msg.clone().publish(&target, perm.get_embed_links()).await?;
Ok(json!(msg))
}
use rocket::Route;
mod channel_ack;
mod channel_delete;
mod channel_edit;
mod channel_fetch;
mod members_fetch;
mod group_add_member;
mod group_create;
mod group_remove_member;
mod invite_create;
mod voice_join;
mod message_delete;
mod message_edit;
mod message_fetch;
mod message_query;
mod message_search;
mod message_query_stale;
mod message_send;
mod permissions_set;
mod permissions_set_default;
pub fn routes() -> Vec<Route> {
routes![
channel_ack::req,
channel_fetch::req,
members_fetch::req,
channel_delete::req,
channel_edit::req,
invite_create::req,
message_send::req,
message_query::req,
message_search::req,
message_query_stale::req,
message_fetch::req,
message_edit::req,
message_delete::req,
group_create::req,
group_add_member::req,
group_remove_member::req,
voice_join::req,
permissions_set::req,
permissions_set_default::req,
]
}
use mongodb::bson::doc;
use rocket_contrib::json::Json;
use serde::{Serialize, Deserialize};
use validator::Contains;
use crate::database::*;
use crate::database::permissions::channel::ChannelPermission;
use crate::notifications::events::ClientboundNotification;
use crate::util::result::{Error, Result};
#[derive(Serialize, Deserialize)]
pub struct Data {
permissions: u32
}
#[put("/<target>/permissions/<role>", data = "<data>", rank = 2)]
pub async fn req(user: User, target: Ref, role: String, data: Json<Data>) -> Result<()> {
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 = ChannelPermission::View as 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(())
}
_ => Err(Error::InvalidOperation)
}
}
use mongodb::bson::doc;
use rocket_contrib::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};
#[derive(Serialize, Deserialize)]
pub struct Data {
permissions: u32
}
#[put("/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn req(user: User, target: Ref, data: Json<Data>) -> Result<()> {
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(())
} 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 = ChannelPermission::View as 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(())
}
_ => Err(Error::InvalidOperation)
}
}
use crate::database::*;
use crate::util::result::{Error, Result};
use crate::util::variables::{USE_VOSO, VOSO_MANAGE_TOKEN, VOSO_URL};
use rocket_contrib::json::JsonValue;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct CreateUserResponse {
token: String,
}
#[post("/<target>/join_call")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
if !*USE_VOSO {
return Err(Error::VosoUnavailable);
}
let target = target.fetch_channel().await?;
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)
}
}
use super::channel::ChannelType;
use super::Response;
use crate::database::guild::{fetch_member as get_member, get_invite, Guild, MemberKey};
use crate::database::{
self, channel::fetch_channel, channel::Channel, guild::serialise_guilds_with_channels,
user::User, Permission, PermissionCalculator,
};
use crate::notifications::{
self,
events::{guilds::*, Notification},
};
use crate::util::gen_token;
use mongodb::bson::{doc, Bson};
use mongodb::options::{FindOneOptions, FindOptions};
use rocket::request::Form;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
// ! FIXME: GET RID OF THIS
macro_rules! with_permissions {
($user: expr, $target: expr) => {{
let permissions = PermissionCalculator::new($user.clone())
.guild($target.clone())
.fetch_data();
let value = permissions.as_permission();
if !value.get_access() {
return None;
}
(value, permissions.member.unwrap())
}};
}
/// fetch your guilds
#[get("/@me")]
pub fn my_guilds(user: User) -> Response {
if let Ok(gids) = user.find_guilds() {
if let Ok(data) = serialise_guilds_with_channels(&gids) {
Response::Success(json!(data))
} else {
Response::InternalServerError(json!({ "error": "Failed to fetch guilds." }))
}
} else {
Response::InternalServerError(json!({ "error": "Failed to fetch memberships." }))
}
}
/// fetch a guild
#[get("/<target>")]
pub fn guild(user: User, target: Guild) -> Option<Response> {
with_permissions!(user, target);
if let Ok(result) = target.seralise_with_channels() {
Some(Response::Success(result))
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to fetch channels!" }),
))
}
}
/// delete or leave a guild
#[delete("/<target>")]
pub fn remove_guild(user: User, target: Guild) -> Option<Response> {
with_permissions!(user, target);
if user.id == target.owner {
let channels = database::get_collection("channels");
if let Ok(result) = channels.find(
doc! {
"type": 2,
"guild": &target.id
},
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
) {
let mut values = vec![];
for item in result {
if let Ok(doc) = item {
values.push(Bson::String(doc.get_str("_id").unwrap().to_string()));
}
}
if database::get_collection("messages")
.delete_many(
doc! {
"channel": {
"$in": values
}
},
None,
)
.is_ok()
{
if channels
.delete_many(
doc! {
"type": 2,
"guild": &target.id,
},
None,
)
.is_ok()
{
if database::get_collection("members")
.delete_many(
doc! {
"_id.guild": &target.id,
},
None,
)
.is_ok()
{
if database::get_collection("guilds")
.delete_one(
doc! {
"_id": &target.id
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
None,
target.id.clone(),
Notification::guild_delete(Delete {
id: target.id.clone(),
}),
);
Some(Response::Result(super::Status::Ok))
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to delete guild." }),
))
}
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to delete guild members." }),
))
}
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to delete guild channels." }),
))
}
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to delete guild messages." }),
))
}
} else {
Some(Response::InternalServerError(
json!({ "error": "Could not fetch channels." }),
))
}
} else if database::get_collection("members")
.delete_one(
doc! {
"_id.guild": &target.id,
"_id.user": &user.id,
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
None,
target.id.clone(),
Notification::guild_user_leave(UserLeave {
id: target.id.clone(),
user: user.id.clone(),
banned: false,
}),
);
Some(Response::Result(super::Status::Ok))
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to remove you from the guild." }),
))
}
}
#[derive(Serialize, Deserialize)]
pub struct CreateChannel {
nonce: String,
name: String,
description: Option<String>,
}
/// create a new channel
#[post("/<target>/channels", data = "<info>")]
pub fn create_channel(user: User, target: Guild, info: Json<CreateChannel>) -> Option<Response> {
let (permissions, _) = with_permissions!(user, target);
if !permissions.get_manage_channels() {
return Some(Response::LackingPermission(Permission::ManageChannels));
}
let nonce: String = info.nonce.chars().take(32).collect();
let name: String = info.name.chars().take(32).collect();
let description: String = info
.description
.clone()
.unwrap_or(String::new())
.chars()
.take(255)
.collect();
if let Ok(result) =
database::get_collection("channels").find_one(doc! { "nonce": &nonce }, None)
{
if result.is_some() {
return Some(Response::BadRequest(
json!({ "error": "Channel already created." }),
));
}
let id = Ulid::new().to_string();
if database::get_collection("channels")
.insert_one(
doc! {
"_id": &id,
"nonce": &nonce,
"type": 2,
"guild": &target.id,
"name": &name,
"description": &description,
},
None,
)
.is_ok()
{
if database::get_collection("guilds")
.update_one(
doc! {
"_id": &target.id
},
doc! {
"$addToSet": {
"channels": &id
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
None,
target.id.clone(),
Notification::guild_channel_create(ChannelCreate {
id: target.id.clone(),
channel: id.clone(),
name: name.clone(),
description: description.clone(),
}),
);
Some(Response::Success(json!({ "id": &id })))
} else {
Some(Response::InternalServerError(
json!({ "error": "Couldn't save channel list." }),
))
}
} else {
Some(Response::InternalServerError(
json!({ "error": "Couldn't create channel." }),
))
}
} else {
Some(Response::BadRequest(
json!({ "error": "Failed to check if channel was made." }),
))
}
}
#[derive(Serialize, Deserialize)]
pub struct InviteOptions {
// ? TODO: add options
}
/// create a new invite
#[post("/<target>/channels/<channel>/invite", data = "<_options>")]
pub fn create_invite(
user: User,
target: Guild,
channel: Channel,
_options: Json<InviteOptions>,
) -> Option<Response> {
let (permissions, _) = with_permissions!(user, target);
if !permissions.get_create_invite() {
return Some(Response::LackingPermission(Permission::CreateInvite));
}
let code = gen_token(7);
if database::get_collection("guilds")
.update_one(
doc! { "_id": target.id },
doc! {
"$push": {
"invites": {
"code": &code,
"creator": user.id,
"channel": channel.id,
}
}
},
None,
)
.is_ok()
{
Some(Response::Success(json!({ "code": code })))
} else {
Some(Response::BadRequest(
json!({ "error": "Failed to create invite." }),
))
}
}
/// remove an invite
#[delete("/<target>/invites/<code>")]
pub fn remove_invite(user: User, target: Guild, code: String) -> Option<Response> {
let (permissions, _) = with_permissions!(user, target);
if let Some((guild_id, _, invite)) = get_invite(&code, None) {
if invite.creator != user.id && !permissions.get_manage_server() {
return Some(Response::LackingPermission(Permission::ManageServer));
}
if database::get_collection("guilds")
.update_one(
doc! {
"_id": &guild_id,
},
doc! {
"$pull": {
"invites": {
"code": &code
}
}
},
None,
)
.is_ok()
{
Some(Response::Result(super::Status::Ok))
} else {
Some(Response::BadRequest(
json!({ "error": "Failed to delete invite." }),
))
}
} else {
Some(Response::NotFound(
json!({ "error": "Failed to fetch invite or code is invalid." }),
))
}
}
/// fetch all guild invites
#[get("/<target>/invites")]
pub fn fetch_invites(user: User, target: Guild) -> Option<Response> {
let (permissions, _) = with_permissions!(user, target);
if !permissions.get_manage_server() {
return Some(Response::LackingPermission(Permission::ManageServer));
}
Some(Response::Success(json!(target.invites)))
}
/// view an invite before joining
#[get("/join/<code>", rank = 1)]
pub fn fetch_invite(user: User, code: String) -> Response {
if let Some((guild_id, name, invite)) = get_invite(&code, user.id) {
match fetch_channel(&invite.channel) {
Ok(result) => {
if let Some(channel) = result {
Response::Success(json!({
"guild": {
"id": guild_id,
"name": name,
},
"channel": {
"id": channel.id,
"name": channel.name,
}
}))
} else {
Response::NotFound(json!({ "error": "Channel does not exist." }))
}
}
Err(err) => Response::InternalServerError(json!({ "error": err })),
}
} else {
Response::NotFound(json!({ "error": "Failed to fetch invite or code is invalid." }))
}
}
/// join a guild using an invite
#[post("/join/<code>", rank = 1)]
pub fn use_invite(user: User, code: String) -> Response {
if let Some((guild_id, _, invite)) = get_invite(&code, Some(user.id.clone())) {
if let Ok(result) = database::get_collection("members").find_one(
doc! {
"_id.guild": &guild_id,
"_id.user": &user.id
},
FindOneOptions::builder()
.projection(doc! { "_id": 1 })
.build(),
) {
if result.is_none() {
if database::get_collection("members")
.insert_one(
doc! {
"_id": {
"guild": &guild_id,
"user": &user.id
}
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
None,
guild_id.clone(),
Notification::guild_user_join(UserJoin {
id: guild_id.clone(),
user: user.id.clone(),
}),
);
Response::Success(json!({
"guild": &guild_id,
"channel": &invite.channel,
}))
} else {
Response::InternalServerError(
json!({ "error": "Failed to add you to the guild." }),
)
}
} else {
Response::BadRequest(json!({ "error": "Already in the guild." }))
}
} else {
Response::InternalServerError(
json!({ "error": "Failed to check if you're in the guild." }),
)
}
} else {
Response::NotFound(json!({ "error": "Failed to fetch invite or code is invalid." }))
}
}
#[derive(Serialize, Deserialize)]
pub struct CreateGuild {
name: String,
description: Option<String>,
nonce: String,
}
/// create a new guild
#[post("/create", data = "<info>")]
pub fn create_guild(user: User, info: Json<CreateGuild>) -> Response {
if !user.email_verification.verified {
return Response::Unauthorized(json!({ "error": "Email not verified!" }));
}
let name: String = info.name.chars().take(32).collect();
let description: String = info
.description
.clone()
.unwrap_or("No description.".to_string())
.chars()
.take(255)
.collect();
let nonce: String = info.nonce.chars().take(32).collect();
let channels = database::get_collection("channels");
let col = database::get_collection("guilds");
if col
.find_one(doc! { "nonce": nonce.clone() }, None)
.unwrap()
.is_some()
{
return Response::BadRequest(json!({ "error": "Guild already created!" }));
}
let id = Ulid::new().to_string();
let channel_id = Ulid::new().to_string();
if channels
.insert_one(
doc! {
"_id": channel_id.clone(),
"type": ChannelType::GUILDCHANNEL as u32,
"name": "general",
"description": "",
"guild": id.clone(),
},
None,
)
.is_err()
{
return Response::InternalServerError(
json!({ "error": "Failed to create guild channel." }),
);
}
if database::get_collection("members")
.insert_one(
doc! {
"_id": {
"guild": &id,
"user": &user.id
}
},
None,
)
.is_err()
{
return Response::InternalServerError(
json!({ "error": "Failed to add you to members list." }),
);
}
if col
.insert_one(
doc! {
"_id": &id,
"nonce": nonce,
"name": name,
"description": description,
"owner": &user.id,
"channels": [ channel_id.clone() ],
"invites": [],
"bans": [],
"default_permissions": 51,
},
None,
)
.is_ok()
{
Response::Success(json!({ "id": id }))
} else {
channels
.delete_one(doc! { "_id": channel_id }, None)
.expect("Failed to delete the channel we just made.");
Response::InternalServerError(json!({ "error": "Failed to create guild." }))
}
}
/// fetch a guild's member
#[get("/<target>/members")]
pub fn fetch_members(user: User, target: Guild) -> Option<Response> {
with_permissions!(user, target);
if let Ok(result) =
database::get_collection("members").find(doc! { "_id.guild": target.id }, None)
{
let mut users = vec![];
for item in result {
if let Ok(doc) = item {
users.push(json!({
"id": doc.get_document("_id").unwrap().get_str("user").unwrap(),
"nickname": doc.get_str("nickname").ok(),
}));
}
}
Some(Response::Success(json!(users)))
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to fetch members." }),
))
}
}
/// fetch a guild member
#[get("/<target>/members/<other>")]
pub fn fetch_member(user: User, target: Guild, other: String) -> Option<Response> {
with_permissions!(user, target);
if let Ok(result) = get_member(MemberKey(target.id, other)) {
if let Some(member) = result {
Some(Response::Success(json!({
"id": member.id.user,
"nickname": member.nickname,
})))
} else {
Some(Response::NotFound(
json!({ "error": "Member does not exist!" }),
))
}
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to fetch member." }),
))
}
}
/// kick a guild member
#[delete("/<target>/members/<other>")]
pub fn kick_member(user: User, target: Guild, other: String) -> Option<Response> {
let (permissions, _) = with_permissions!(user, target);
if user.id == other {
return Some(Response::BadRequest(
json!({ "error": "Cannot kick yourself." }),
));
}
if !permissions.get_kick_members() {
return Some(Response::LackingPermission(Permission::KickMembers));
}
if let Ok(result) = get_member(MemberKey(target.id.clone(), other.clone())) {
if result.is_none() {
return Some(Response::BadRequest(
json!({ "error": "User not part of guild." }),
));
}
} else {
return Some(Response::InternalServerError(
json!({ "error": "Failed to fetch member." }),
));
}
if database::get_collection("members")
.delete_one(
doc! {
"_id.guild": &target.id,
"_id.user": &other,
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
None,
target.id.clone(),
Notification::guild_user_leave(UserLeave {
id: target.id.clone(),
user: other.clone(),
banned: false,
}),
);
Some(Response::Result(super::Status::Ok))
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to kick member." }),
))
}
}
#[derive(Serialize, Deserialize, FromForm)]
pub struct BanOptions {
reason: Option<String>,
}
/// ban a guild member
#[put("/<target>/members/<other>/ban?<options..>")]
pub fn ban_member(
user: User,
target: Guild,
other: String,
options: Form<BanOptions>,
) -> Option<Response> {
let (permissions, _) = with_permissions!(user, target);
let reason: String = options
.reason
.clone()
.unwrap_or("No reason specified.".to_string())
.chars()
.take(64)
.collect();
if user.id == other {
return Some(Response::BadRequest(
json!({ "error": "Cannot ban yourself." }),
));
}
if !permissions.get_ban_members() {
return Some(Response::LackingPermission(Permission::BanMembers));
}
if let Ok(result) = get_member(MemberKey(target.id.clone(), other.clone())) {
if result.is_none() {
return Some(Response::BadRequest(
json!({ "error": "User not part of guild." }),
));
}
} else {
return Some(Response::InternalServerError(
json!({ "error": "Failed to fetch member." }),
));
}
if database::get_collection("guilds")
.update_one(
doc! { "_id": &target.id },
doc! {
"$push": {
"bans": {
"id": &other,
"reason": reason,
}
}
},
None,
)
.is_err()
{
return Some(Response::BadRequest(
json!({ "error": "Failed to add ban to guild." }),
));
}
if database::get_collection("members")
.delete_one(
doc! {
"_id.guild": &target.id,
"_id.user": &other,
},
None,
)
.is_ok()
{
notifications::send_message_threaded(
None,
target.id.clone(),
Notification::guild_user_leave(UserLeave {
id: target.id.clone(),
user: other.clone(),
banned: true,
}),
);
Some(Response::Result(super::Status::Ok))
} else {
Some(Response::InternalServerError(
json!({ "error": "Failed to kick member after adding to ban list." }),
))
}
}
/// unban a guild member
#[delete("/<target>/members/<other>/ban")]
pub fn unban_member(user: User, target: Guild, other: String) -> Option<Response> {
let (permissions, _) = with_permissions!(user, target);
if user.id == other {
return Some(Response::BadRequest(
json!({ "error": "Cannot unban yourself (not checking if you're banned)." }),
));
}
if !permissions.get_ban_members() {
return Some(Response::LackingPermission(Permission::BanMembers));
}
if target.bans.iter().any(|v| v.id == other) {
return Some(Response::BadRequest(json!({ "error": "User not banned." })));
}
if database::get_collection("guilds")
.update_one(
doc! {
"_id": &target.id
},
doc! {
"$pull": {
"bans": {
"$elemMatch": {
"id": &other
}
}
}
},
None,
)
.is_ok()
{
Some(Response::Result(super::Status::Ok))
} else {
Some(Response::BadRequest(
json!({ "error": "Failed to remove ban." }),
))
}
}
#[options("/<_target>")]
pub fn guild_preflight(_target: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/channels")]
pub fn create_channel_preflight(_target: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/channels/<_channel>/invite")]
pub fn create_invite_preflight(_target: String, _channel: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/invites/<_code>")]
pub fn remove_invite_preflight(_target: String, _code: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/invites")]
pub fn fetch_invites_preflight(_target: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/join/<_code>", rank = 1)]
pub fn invite_preflight(_code: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/create")]
pub fn create_guild_preflight() -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/members")]
pub fn fetch_members_preflight(_target: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/members/<_other>")]
pub fn fetch_member_preflight(_target: String, _other: String) -> Response {
Response::Result(super::Status::Ok)
}
#[options("/<_target>/members/<_other>/ban")]
pub fn ban_member_preflight(_target: String, _other: String) -> Response {
Response::Result(super::Status::Ok)
}
use crate::database::*;
use crate::util::result::{Error, Result};
#[delete("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<()> {
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!(),
}
}
}
use crate::database::*;
use crate::util::result::Result;
use rocket_contrib::json::JsonValue;
use serde::Serialize;
#[derive(Serialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum InviteResponse {
Server {
server_id: String,
server_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
server_icon: Option<File>,
#[serde(skip_serializing_if = "Option::is_none")]
server_banner: Option<File>,
channel_id: String,
channel_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
channel_description: Option<String>,
user_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
user_avatar: Option<File>,
member_count: i64,
},
}
#[get("/<target>")]
pub async fn req(target: Ref) -> Result<JsonValue> {
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!(),
}
}
use crate::database::*;
use crate::util::result::Result;
use rocket_contrib::json::JsonValue;
#[post("/<target>")]
pub async fn req(user: User, target: Ref) -> Result<JsonValue> {
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!(),
}
}
use rocket::Route;
mod invite_delete;
mod invite_fetch;
mod invite_join;
pub fn routes() -> Vec<Route> {
routes![invite_fetch::req, invite_join::req, invite_delete::req]
}
pub use rocket::http::Status;
pub use rocket::response::Redirect;
use rocket::Rocket;
use rocket_contrib::json::JsonValue;
use crate::database::Permission;
pub mod account;
pub mod channel;
pub mod guild;
pub mod root;
pub mod user;
#[derive(Responder)]
pub enum Response {
#[response()]
Result(Status),
#[response()]
Success(JsonValue),
#[response()]
Redirect(Redirect),
#[response(status = 207)]
PartialStatus(JsonValue),
#[response(status = 400)]
BadRequest(JsonValue),
#[response(status = 401)]
Unauthorized(JsonValue),
#[response(status = 401)]
LackingPermission(Permission),
#[response(status = 404)]
NotFound(JsonValue),
#[response(status = 406)]
NotAcceptable(JsonValue),
#[response(status = 409)]
Conflict(JsonValue),
#[response(status = 410)]
Gone(JsonValue),
#[response(status = 418)]
Teapot(JsonValue),
#[response(status = 422)]
UnprocessableEntity(JsonValue),
#[response(status = 429)]
TooManyRequests(JsonValue),
#[response(status = 500)]
InternalServerError(JsonValue),
}
use rocket::http::ContentType;
use rocket::request::Request;
use std::io::Cursor;
impl<'a> rocket::response::Responder<'a> for Permission {
fn respond_to(self, _: &Request) -> rocket::response::Result<'a> {
rocket::response::Response::build()
.header(ContentType::JSON)
.sized_body(Cursor::new(format!(
"{{\"error\":\"Lacking permission: {:?}.\",\"permission\":{}}}",
self, self as u32,
)))
.ok()
}
}
mod channels;
mod invites;
mod onboard;
mod push;
mod root;
mod servers;
mod sync;
mod users;
pub fn mount(rocket: Rocket) -> Rocket {
rocket
.mount("/", routes![root::root, root::root_preflight, root::teapot])
.mount(
"/account",
routes![
account::create,
account::verify_email,
account::resend_email,
account::login,
account::token,
account::create_preflight,
account::verify_email_preflight,
account::resend_email_preflight,
account::login_preflight,
account::token_preflight,
],
)
.mount(
"/users",
routes![
user::me,
user::user,
user::query,
user::dms,
user::dm,
user::get_friends,
user::get_friend,
user::add_friend,
user::remove_friend,
user::block_user,
user::unblock_user,
user::user_preflight,
user::query_preflight,
user::dms_preflight,
user::dm_preflight,
user::friend_preflight,
user::block_user_preflight,
],
)
.mount(
"/channels",
routes![
channel::create_group,
channel::channel,
channel::add_member,
channel::remove_member,
channel::delete,
channel::messages,
channel::get_message,
channel::send_message,
channel::edit_message,
channel::delete_message,
channel::create_group_preflight,
channel::channel_preflight,
channel::member_preflight,
channel::messages_preflight,
channel::message_preflight,
],
)
.mount(
"/guild",
routes![
guild::my_guilds,
guild::guild,
guild::remove_guild,
guild::create_channel,
guild::create_invite,
guild::remove_invite,
guild::fetch_invites,
guild::fetch_invite,
guild::use_invite,
guild::create_guild,
guild::fetch_members,
guild::fetch_member,
guild::kick_member,
guild::ban_member,
guild::unban_member,
guild::guild_preflight,
guild::create_channel_preflight,
guild::create_invite_preflight,
guild::remove_invite_preflight,
guild::fetch_invites_preflight,
guild::invite_preflight,
guild::create_guild_preflight,
guild::fetch_members_preflight,
guild::fetch_member_preflight,
guild::ban_member_preflight,
],
)
.mount("/", routes![root::root])
.mount("/onboard", onboard::routes())
.mount("/users", users::routes())
.mount("/channels", channels::routes())
.mount("/servers", servers::routes())
.mount("/invites", invites::routes())
.mount("/push", push::routes())
.mount("/sync", sync::routes())
}
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rauth::auth::Session;
use regex::Regex;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap();
}
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 2, max = 32), regex = "RE_USERNAME")]
username: String,
}
#[post("/complete", data = "<data>")]
pub async fn req(session: Session, user: Option<User>, data: Json<Data>) -> Result<()> {
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(())
}
use crate::database::*;
use rauth::auth::Session;
use rocket_contrib::json::JsonValue;
#[get("/hello")]
pub async fn req(_session: Session, user: Option<User>) -> JsonValue {
json!({
"onboarding": user.is_none()
})
}
use rocket::Route;
mod complete;
mod hello;
pub fn routes() -> Vec<Route> {
routes![hello::req, complete::req]
}
use rocket::Route;
mod subscribe;
mod unsubscribe;
pub fn routes() -> Vec<Route> {
routes![subscribe::req, unsubscribe::req]
}
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::{doc, to_document};
use rauth::auth::Session;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Subscription {
endpoint: String,
p256dh: String,
auth: String,
}
#[post("/subscribe", data = "<data>")]
pub async fn req(session: Session, data: Json<Subscription>) -> Result<()> {
let data = data.into_inner();
get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$set": {
"sessions.$.subscription": to_document(&data)
.map_err(|_| Error::DatabaseError { operation: "to_document", with: "subscription" })?
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError { operation: "update_one", with: "account" })?;
Ok(())
}
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rauth::auth::Session;
#[post("/unsubscribe")]
pub async fn req(session: Session) -> Result<()> {
get_collection("accounts")
.update_one(
doc! {
"_id": session.user_id,
"sessions.id": session.id.unwrap()
},
doc! {
"$unset": {
"sessions.$.subscription": 1
}
},
None,
)
.await
.map_err(|_| Error::DatabaseError {
operation: "to_document",
with: "subscription",
})?;
Ok(())
}
use super::Response;
use crate::util::variables::{USE_EMAIL_VERIFICATION, USE_HCAPTCHA};
use crate::util::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_contrib::json::JsonValue;
/// root
#[get("/")]
pub fn root() -> Response {
Response::Success(json!({
"revolt": "0.2.9",
"version": {
"major": 0,
"minor": 2,
"patch": 9
},
pub async fn root() -> JsonValue {
json!({
"revolt": crate::version::VERSION,
"features": {
"email_verification": USE_EMAIL_VERIFICATION.clone(),
"captcha": USE_HCAPTCHA.clone(),
}
}))
}
#[options("/")]
pub fn root_preflight() -> Response {
Response::Result(super::Status::Ok)
}
/// I'm a teapot.
#[delete("/")]
pub fn teapot() -> Response {
Response::Teapot(json!({
"teapot": true,
"can_delete": false
}))
"captcha": {
"enabled": *USE_HCAPTCHA,
"key": HCAPTCHA_SITEKEY.to_string()
},
"email": *USE_EMAIL,
"invite_only": *INVITE_ONLY,
"autumn": {
"enabled": *USE_AUTUMN,
"url": *AUTUMN_URL
},
"january": {
"enabled": *USE_JANUARY,
"url": *JANUARY_URL
},
"voso": {
"enabled": *USE_VOSO,
"url": *VOSO_URL,
"ws": *VOSO_WS_HOST
}
},
"ws": *EXTERNAL_WS_URL,
"app": *APP_URL,
"vapid": *VAPID_PUBLIC_KEY
})
}
use crate::database::*;
use crate::util::result::{Error, Result};
use mongodb::bson::doc;
use rocket_contrib::json::Json;
use serde::{Deserialize, Serialize};
use validator::Validate;
#[derive(Validate, Serialize, Deserialize)]
pub struct Data {
#[validate(length(min = 1, max = 1024))]
reason: Option<String>,
}
#[put("/<server>/bans/<target>", data = "<data>")]
pub async fn req(user: User, server: Ref, target: Ref, data: Json<Data>) -> Result<()> {
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
}