This repository has been archived by the owner on Nov 23, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #66 from FerrisChat/feature/bot-impl
Implement Bots
- Loading branch information
Showing
13 changed files
with
409 additions
and
14 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
), | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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::*; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}), | ||
}, | ||
} | ||
} |
Oops, something went wrong.