mirror of
https://github.com/stoatchat/stoatchat.git
synced 2026-07-14 13:36:59 +00:00
12
src/database/entities/bots.rs
Normal file
12
src/database/entities/bots.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[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 = "Option::is_none")]
|
||||
pub interactions_url: Option<String>,
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod microservice;
|
||||
mod server;
|
||||
mod sync;
|
||||
mod user;
|
||||
mod bots;
|
||||
|
||||
use microservice::*;
|
||||
|
||||
@@ -16,3 +17,4 @@ pub use message::*;
|
||||
pub use server::*;
|
||||
pub use sync::*;
|
||||
pub use user::*;
|
||||
pub use bots::*;
|
||||
|
||||
@@ -37,13 +37,20 @@ pub type PermissionTuple = (
|
||||
i32 // channel permission
|
||||
);
|
||||
|
||||
fn if_false(t: &bool) -> bool {
|
||||
*t == false
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Role {
|
||||
pub name: String,
|
||||
pub permissions: PermissionTuple,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub colour: Option<String>
|
||||
// Bri'ish API conventions
|
||||
pub colour: Option<String>,
|
||||
#[serde(skip_serializing_if = "if_false", default)]
|
||||
pub hoist: bool,
|
||||
#[serde(default)]
|
||||
pub rank: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
|
||||
@@ -73,7 +73,12 @@ pub enum Badges {
|
||||
|
||||
impl_op_ex_commutative!(+ |a: &i32, b: &Badges| -> i32 { *a | *b as i32 });
|
||||
|
||||
// When changing this struct, update notifications/payload.rs#80
|
||||
#[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")]
|
||||
@@ -91,6 +96,11 @@ pub struct User {
|
||||
#[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>,
|
||||
|
||||
@@ -10,6 +10,61 @@ impl<'r> FromRequest<'r> for User {
|
||||
type Error = rauth::util::Error;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let header_bot_token = request
|
||||
.headers()
|
||||
.get("x-bot-token")
|
||||
.next()
|
||||
.map(|x| x.to_string());
|
||||
|
||||
if let Some(bot_token) = header_bot_token {
|
||||
return 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 {
|
||||
Outcome::Success(from_document(doc).unwrap())
|
||||
} else {
|
||||
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((
|
||||
Status::InternalServerError,
|
||||
rauth::util::Error::DatabaseError {
|
||||
operation: "find_one",
|
||||
with: "user",
|
||||
},
|
||||
))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((Status::Forbidden, rauth::util::Error::InvalidSession))
|
||||
}
|
||||
} else {
|
||||
Outcome::Failure((
|
||||
Status::InternalServerError,
|
||||
rauth::util::Error::DatabaseError {
|
||||
operation: "find_one",
|
||||
with: "bot",
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let session: Session = request.guard::<Session>().await.unwrap();
|
||||
|
||||
if let Ok(result) = get_collection("users")
|
||||
|
||||
@@ -57,6 +57,10 @@ pub async fn create_database() {
|
||||
.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()
|
||||
|
||||
@@ -11,7 +11,7 @@ struct MigrationInfo {
|
||||
revision: i32,
|
||||
}
|
||||
|
||||
pub const LATEST_REVISION: i32 = 7;
|
||||
pub const LATEST_REVISION: i32 = 8;
|
||||
|
||||
pub async fn migrate_database() {
|
||||
let migrations = get_collection("migrations");
|
||||
@@ -203,6 +203,15 @@ pub async fn run_migrations(revision: i32) -> i32 {
|
||||
.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.");
|
||||
}
|
||||
|
||||
// Reminder to update LATEST_REVISION when adding new migrations.
|
||||
LATEST_REVISION
|
||||
}
|
||||
|
||||
@@ -17,10 +17,22 @@ pub enum WebSocketError {
|
||||
AlreadyAuthenticated,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct BotAuth {
|
||||
pub token: String
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum AuthType {
|
||||
User(Session),
|
||||
Bot(BotAuth)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ServerboundNotification {
|
||||
Authenticate(Session),
|
||||
Authenticate(AuthType),
|
||||
BeginTyping { channel: String },
|
||||
EndTyping { channel: String },
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::database::*;
|
||||
use crate::notifications::events::{AuthType, BotAuth};
|
||||
use crate::util::variables::WS_HOST;
|
||||
|
||||
use super::subscriptions;
|
||||
@@ -12,8 +13,9 @@ use futures::{pin_mut, prelude::*};
|
||||
use hive_pubsub::PubSub;
|
||||
use log::{debug, info};
|
||||
use many_to_many::ManyToMany;
|
||||
use mongodb::bson::doc;
|
||||
use rauth::{
|
||||
auth::{Auth, Session},
|
||||
auth::{Auth},
|
||||
options::Options,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
@@ -66,15 +68,15 @@ async fn accept(stream: TcpStream) {
|
||||
}
|
||||
};
|
||||
|
||||
let session: Arc<Mutex<Option<Session>>> = Arc::new(Mutex::new(None));
|
||||
let mutex_generator = || session.clone();
|
||||
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();
|
||||
if let Message::Text(text) = msg {
|
||||
if let Ok(notification) = serde_json::from_str::<ServerboundNotification>(&text) {
|
||||
match notification {
|
||||
ServerboundNotification::Authenticate(new_session) => {
|
||||
ServerboundNotification::Authenticate(auth) => {
|
||||
{
|
||||
if mutex.lock().unwrap().is_some() {
|
||||
send(ClientboundNotification::Error(
|
||||
@@ -85,12 +87,34 @@ async fn accept(stream: TcpStream) {
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(validated_session) =
|
||||
Auth::new(get_collection("accounts"), Options::new())
|
||||
.verify_session(new_session)
|
||||
.await
|
||||
{
|
||||
let id = validated_session.user_id.clone();
|
||||
if let Some(id) = match auth {
|
||||
AuthType::User(new_session) => {
|
||||
if let Ok(validated_session) =
|
||||
Auth::new(get_collection("accounts"), Options::new())
|
||||
.verify_session(new_session)
|
||||
.await
|
||||
{
|
||||
Some(validated_session.user_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
AuthType::Bot(BotAuth { token }) => {
|
||||
if let Ok(doc) = get_collection("bots")
|
||||
.find_one(
|
||||
doc! { "token": token },
|
||||
None
|
||||
).await {
|
||||
if let Some(doc) = doc {
|
||||
Some(doc.get_str("_id").unwrap().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} {
|
||||
if let Ok(user) = (Ref { id: id.clone() }).fetch_user().await {
|
||||
let was_online = is_online(&id);
|
||||
{
|
||||
@@ -110,7 +134,7 @@ async fn accept(stream: TcpStream) {
|
||||
}
|
||||
}
|
||||
|
||||
*mutex.lock().unwrap() = Some(validated_session);
|
||||
*mutex.lock().unwrap() = Some(id.clone());
|
||||
|
||||
if let Err(_) = subscriptions::generate_subscriptions(&user).await {
|
||||
send(ClientboundNotification::Error(
|
||||
@@ -166,8 +190,7 @@ async fn accept(stream: TcpStream) {
|
||||
if mutex.lock().unwrap().is_some() {
|
||||
let user = {
|
||||
let mutex = mutex.lock().unwrap();
|
||||
let session = mutex.as_ref().unwrap();
|
||||
session.user_id.clone()
|
||||
mutex.as_ref().unwrap().clone()
|
||||
};
|
||||
|
||||
ClientboundNotification::ChannelStartTyping {
|
||||
@@ -187,8 +210,7 @@ async fn accept(stream: TcpStream) {
|
||||
if mutex.lock().unwrap().is_some() {
|
||||
let user = {
|
||||
let mutex = mutex.lock().unwrap();
|
||||
let session = mutex.as_ref().unwrap();
|
||||
session.user_id.clone()
|
||||
mutex.as_ref().unwrap().clone()
|
||||
};
|
||||
|
||||
ClientboundNotification::ChannelStopTyping {
|
||||
@@ -219,13 +241,13 @@ async fn accept(stream: TcpStream) {
|
||||
|
||||
let mut offline = None;
|
||||
{
|
||||
let session = session.lock().unwrap();
|
||||
if let Some(session) = session.as_ref() {
|
||||
let user_id = user_id.lock().unwrap();
|
||||
if let Some(user_id) = user_id.as_ref() {
|
||||
let mut users = USERS.write().unwrap();
|
||||
users.remove(&session.user_id, &addr);
|
||||
if users.get_left(&session.user_id).is_none() {
|
||||
get_hive().drop_client(&session.user_id).unwrap();
|
||||
offline = Some(session.user_id.clone());
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
89
src/routes/bots/create.rs
Normal file
89
src/routes/bots/create.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use crate::database::*;
|
||||
use crate::util::result::{Error, Result};
|
||||
use crate::util::variables::MAX_BOT_COUNT;
|
||||
|
||||
use mongodb::bson::{doc, to_document};
|
||||
use regex::Regex;
|
||||
use rocket::serde::json::{Json, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use nanoid::nanoid;
|
||||
use validator::Validate;
|
||||
|
||||
// ! FIXME: should be global somewhere; maybe use config(?)
|
||||
// ! tip: CTRL + F, RE_USERNAME
|
||||
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")]
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[post("/create", data = "<info>")]
|
||||
pub async fn create_bot(user: User, info: Json<Data>) -> Result<Value> {
|
||||
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,
|
||||
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))
|
||||
}
|
||||
9
src/routes/bots/mod.rs
Normal file
9
src/routes/bots/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use rocket::Route;
|
||||
|
||||
mod create;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
create::create_bot
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod root;
|
||||
mod servers;
|
||||
mod sync;
|
||||
mod users;
|
||||
mod bots;
|
||||
|
||||
pub fn mount(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
rocket
|
||||
@@ -18,6 +19,7 @@ pub fn mount(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
.mount("/users", users::routes())
|
||||
.mount("/channels", channels::routes())
|
||||
.mount("/servers", servers::routes())
|
||||
.mount("/bots", bots::routes())
|
||||
.mount("/invites", invites::routes())
|
||||
.mount("/push", push::routes())
|
||||
.mount("/sync", sync::routes())
|
||||
|
||||
@@ -13,6 +13,8 @@ pub struct Data {
|
||||
name: Option<String>,
|
||||
#[validate(length(min = 1, max = 32))]
|
||||
colour: Option<String>,
|
||||
hoist: Option<bool>,
|
||||
rank: Option<i64>,
|
||||
remove: Option<RemoveRoleField>,
|
||||
}
|
||||
|
||||
@@ -22,7 +24,7 @@ pub async fn req(user: User, target: Ref, role_id: String, data: Json<Data>) ->
|
||||
data.validate()
|
||||
.map_err(|error| Error::FailedValidation { error })?;
|
||||
|
||||
if data.name.is_none() && data.colour.is_none() && data.remove.is_none()
|
||||
if data.name.is_none() && data.colour.is_none() && data.hoist.is_none() && data.rank.is_none() && data.remove.is_none()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
@@ -67,6 +69,16 @@ pub async fn req(user: User, target: Ref, role_id: String, data: Json<Data>) ->
|
||||
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);
|
||||
|
||||
@@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
// ! FIXME: should be global somewhere; maybe use config(?)
|
||||
// ! tip: CTRL + F, RE_USERNAME
|
||||
lazy_static! {
|
||||
static ref RE_USERNAME: Regex = Regex::new(r"^[a-zA-Z0-9_.]+$").unwrap();
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ pub enum Error {
|
||||
InvalidRole,
|
||||
Banned,
|
||||
|
||||
// ? Bot related errors.
|
||||
ReachedMaximumBots,
|
||||
|
||||
// ? General errors.
|
||||
TooManyIds,
|
||||
FailedValidation {
|
||||
@@ -98,6 +101,8 @@ impl<'r> Responder<'r, 'static> for Error {
|
||||
Error::InvalidRole => Status::NotFound,
|
||||
Error::Banned => Status::Forbidden,
|
||||
|
||||
Error::ReachedMaximumBots => Status::BadRequest,
|
||||
|
||||
Error::FailedValidation { .. } => Status::UnprocessableEntity,
|
||||
Error::DatabaseError { .. } => Status::InternalServerError,
|
||||
Error::InternalError => Status::InternalServerError,
|
||||
|
||||
@@ -62,6 +62,8 @@ lazy_static! {
|
||||
// Application Logic Settings
|
||||
pub static ref MAX_GROUP_SIZE: usize =
|
||||
env::var("REVOLT_MAX_GROUP_SIZE").unwrap_or_else(|_| "50".to_string()).parse().unwrap();
|
||||
pub static ref MAX_BOT_COUNT: usize =
|
||||
env::var("REVOLT_MAX_BOT_COUNT").unwrap_or_else(|_| "5".to_string()).parse().unwrap();
|
||||
pub static ref EARLY_ADOPTER_BADGE: i64 =
|
||||
env::var("REVOLT_EARLY_ADOPTER_BADGE").unwrap_or_else(|_| "0".to_string()).parse().unwrap();
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub const VERSION: &str = "0.5.1-alpha.24";
|
||||
pub const VERSION: &str = "0.5.2-alpha.0";
|
||||
|
||||
Reference in New Issue
Block a user