Skip to content
This repository has been archived by the owner on Nov 23, 2022. It is now read-only.

Commit

Permalink
Merge pull request #66 from FerrisChat/feature/bot-impl
Browse files Browse the repository at this point in the history
Implement Bots
  • Loading branch information
tazz4843 authored Nov 20, 2021
2 parents 67c85a8 + 03996d6 commit 33146f9
Show file tree
Hide file tree
Showing 13 changed files with 409 additions and 14 deletions.
23 changes: 12 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 83 additions & 0 deletions ferrischat_webserver/src/auth/bot_get_token.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use crate::auth::token_gen::generate_random_bits;
use actix_web::{HttpRequest, HttpResponse, Responder};
use ferrischat_common::types::{AuthResponse, InternalServerErrorJson};
use tokio::sync::oneshot::channel;

pub async fn get_bot_token(auth: crate::Authorization, req: HttpRequest) -> impl Responder {
let user_id = get_item_id!(req, "bot_id");
let bigint_user_id = u128_to_bigdecimal!(user_id);
let db = get_db_or_fail!();
let bigint_owner_id =
match sqlx::query!("SELECT * FROM bots WHERE user_id = $1", bigint_user_id)
.fetch_one(db)
.await
{
Ok(r) => r.owner_id,
Err(e) => {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: format!("DB returned a error: {}", e),
})
}
};

let owner_id = bigdecimal_to_u128!(bigint_owner_id);

if owner_id != auth.0 {
return HttpResponse::Forbidden().finish();
}

let token = match generate_random_bits() {
Some(b) => base64::encode_config(b, base64::URL_SAFE),
None => {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: "failed to generate random bits for token generation".to_string(),
})
}
};

let hashed_token = {
let rx = match ferrischat_auth::GLOBAL_HASHER.get() {
Some(h) => {
let (tx, rx) = channel();
if h.send((token.clone(), tx)).await.is_err() {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: "Password hasher has hung up connection".to_string(),
});
};
rx
}
None => {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: "password hasher not found".to_string(),
})
}
};
match rx.await {
Ok(r) => match r {
Ok(r) => r,
Err(e) => {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: format!("failed to hash token: {}", e),
})
}
},
Err(e) => unreachable!(
"failed to receive value from channel despite value being sent earlier on: {}",
e
),
}
};

if let Err(e) = sqlx::query!("INSERT INTO auth_tokens VALUES ($1, $2) ON CONFLICT (user_id) DO UPDATE SET auth_token = $2", bigint_user_id, hashed_token).execute(db).await {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: format!("DB returned a error: {}", e)
})
};
return HttpResponse::Ok().json(AuthResponse {
token: format!(
"{}.{}",
base64::encode_config(user_id.to_string(), base64::URL_SAFE),
token,
),
});
}
2 changes: 2 additions & 0 deletions ferrischat_webserver/src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
mod auth_struct;
mod bot_get_token;
mod get_token;
mod token_gen;

pub use auth_struct::Authorization;
pub use bot_get_token::*;
pub use get_token::*;
pub use token_gen::*;
22 changes: 21 additions & 1 deletion ferrischat_webserver/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ferrischat_macros::expand_version;
use ferrischat_redis::load_redis;
use ferrischat_ws::{init_ws_server, preload_ws};

use crate::auth::get_token;
use crate::auth::*;
use crate::channels::*;
use crate::guilds::*;
use crate::invites::*;
Expand Down Expand Up @@ -152,6 +152,26 @@ pub async fn entrypoint() {
expand_version!("users/{user_id}"),
web::delete().to(delete_user),
)
// POST /users/{user_id}/bots
.route(
expand_version!("users/{user_id}/bots"),
web::post().to(create_bot),
)
// PATCH /users/{user_id}/bots/{bot_id}
.route(
expand_version!("users/{user_id}/bots/{bot_id}"),
web::patch().to(edit_bot),
)
// DELETE /users/{user_id}/bots/{bot_id}
.route(
expand_version!("users/{user_id}/bots/{bot_id}"),
web::delete().to(delete_bot),
)
// POST /users/{user_id}/bots/{bot_id}/auth
.route(
expand_version!("users/{user_id}/bots/{bot_id}/auth"),
web::post().to(get_bot_token),
)
// POST /auth
.route(expand_version!("auth"), web::post().to(get_token))
// GET /ws/info
Expand Down
137 changes: 137 additions & 0 deletions ferrischat_webserver/src/users/bots/create_bot.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use actix_web::{web::Json, HttpResponse, Responder};
use ferrischat_common::request_json::BotCreateJson;
use ferrischat_common::types::{InternalServerErrorJson, ModelType, User, UserFlags};
use ferrischat_snowflake_generator::generate_snowflake;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use tokio::sync::oneshot::channel;

/// POST /api/v0/users/{user_id}/bots
/// Creates a FerrisChat bot with the given info
pub async fn create_bot(
auth: crate::Authorization,
bot_data: Json<BotCreateJson>,
) -> impl Responder {
let db = get_db_or_fail!();
let node_id = get_node_id!();
let user_id = generate_snowflake::<0>(ModelType::User as u8, node_id);
let BotCreateJson { username } = bot_data.0;
let email = format!("{}@bots.ferris.chat", user_id);
let password: String = (&mut thread_rng())
.sample_iter(&Alphanumeric)
.take(64)
.map(char::from)
.collect();
// Gets a discriminator for the user
let user_discrim = {
// Makes sure the name doesn't already exist
let existing: Vec<i16> =
match sqlx::query!("SELECT discriminator FROM users WHERE name = $1", username)
.fetch_all(db)
.await
{
Ok(r) => r.into_iter().map(|x| x.discriminator).collect(),
Err(e) => {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: format!("DB returned a error: {}", e),
})
}
};
// your discrim can be between 1 and 9999
let available = (1..=9999)
.filter(|x| !existing.contains(x))
.collect::<Vec<_>>();
match available.get(rand::thread_rng().gen_range(0..available.len())) {
Some(d) => *d,
None => {
return HttpResponse::Conflict().json(InternalServerErrorJson {
reason: "This username has all possible discriminators taken.".to_string(),
})
}
}
};
let hashed_password = {
let hasher = match ferrischat_auth::GLOBAL_HASHER.get() {
Some(h) => h,
None => {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: "Password hasher not found".to_string(),
})
}
};
let (tx, rx) = channel();
// if this fn errors it will be caught because tx will be dropped as well
// resulting in a error in the match
let _ = hasher.send((password, tx)).await;
match rx.await {
Ok(d) => match d {
Ok(s) => s,
Err(e) => {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: format!("Failed to hash password: {}", e),
})
}
},
Err(_) => {
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: "Other end hung up connection".to_string(),
})
}
}
};
let bigint_bot_id = u128_to_bigdecimal!(user_id);
let bigint_owner_id = u128_to_bigdecimal!(auth.0);

if let Err(e) = sqlx::query!(
"INSERT INTO bots VALUES ($1, $2)",
bigint_bot_id,
bigint_owner_id
)
.execute(db)
.await
{
return HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: format!("DB returned a error: {}", e),
});
}

// tell the database about our new bot
match sqlx::query!(
"INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6)",
bigint_bot_id,
username,
UserFlags::BOT_ACCOUNT.bits(),
email,
hashed_password,
user_discrim,
)
.execute(db)
.await
{
Ok(_) => HttpResponse::Created().json(User {
id: user_id,
name: username,
avatar: None,
guilds: None,
flags: UserFlags::BOT_ACCOUNT,
discriminator: user_discrim,
}),
Err(e) => match e {
sqlx::Error::Database(e) => {
if e.code() == Some("23505".into()) {
HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: "A bot with this email already exists? (this is DEFINITELY a bug)"
.to_string(),
})
} else {
HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: format!("DB returned a error: {}", e),
})
}
}
_ => HttpResponse::InternalServerError().json(InternalServerErrorJson {
reason: format!("DB returned a error: {}", e),
}),
},
}
}
Loading

0 comments on commit 33146f9

Please sign in to comment.