Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 208 additions & 15 deletions rustmail/src/api/handler/externals/tickets/create.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use crate::db::repr::{ApiKey, Permission};
use crate::prelude::api::*;
use crate::types::BotState;
use crate::prelude::db::*;
use crate::prelude::i18n::*;
use crate::prelude::utils::*;
use crate::types::{BotCommand, BotState};
use axum::Json;
use axum::extract::{Extension, State};
use axum::http::StatusCode;
use rustmail_types::CreateTicket;
use serenity::all::{ChannelId, CreateChannel, GuildId, UserId};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;

Expand All @@ -16,26 +21,214 @@ pub async fn handle_external_ticket_create(
check_permission(&api_key, Permission::CreateTicket)
.map_err(|e| (StatusCode::FORBIDDEN, format!("{:?}", e)))?;

let _current_config = {
let user_id_u64 = update.discord_id.parse::<u64>().map_err(|_| {
(
StatusCode::BAD_REQUEST,
"Invalid Discord ID format".to_string(),
)
})?;

let user_id = UserId::new(user_id_u64);

let (mut config, db_pool, bot_http, command_tx) = {
let state = bot_state.lock().await;
match &state.config {
Some(c) => c.clone(),
None => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Configuration not loaded".to_string(),
));
}
}
let config = state
.config
.as_ref()
.ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
"Configuration not loaded".to_string(),
))?
.clone();
let db_pool = state
.db_pool
.as_ref()
.ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
"Database not available".to_string(),
))?
.clone();
let bot_http = state
.bot_http
.as_ref()
.ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
"Bot HTTP client not available".to_string(),
))?
.clone();
let command_tx = state.command_tx.clone();

(config, db_pool, bot_http, command_tx)
};

config.db_pool = Some(db_pool.clone());

println!(
"API Key #{} creating ticket for Discord ID: {}",
api_key.id, user_id_u64
);

let user = bot_http.get_user(user_id).await.map_err(|e| {
(
StatusCode::NOT_FOUND,
format!("Discord user not found: {}", e),
)
})?;

if user.bot {
return Err((
StatusCode::BAD_REQUEST,
"Cannot create ticket for bot users".to_string(),
));
}

let (tx, rx) = tokio::sync::oneshot::channel();
command_tx
.send(BotCommand::CheckUserIsMember {
user_id: user_id_u64,
resp: tx,
})
.await
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to communicate with bot".to_string(),
)
})?;

let is_member = rx.await.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to check guild membership".to_string(),
)
})?;

if !is_member {
return Err((
StatusCode::FORBIDDEN,
"User is not a member of the community guild".to_string(),
));
}

if thread_exists(user_id, &db_pool).await {
return if let Some(channel_id_str) = get_thread_channel_by_user_id(user_id, &db_pool).await
{
Err((
StatusCode::CONFLICT,
format!("User already has an active ticket: <#{}>", channel_id_str),
))
} else {
Err((
StatusCode::CONFLICT,
"User already has an active ticket".to_string(),
))
};
}

let username = user.name.clone();
let thread_name = format!("🔴・{}・0m", username);
let staff_guild_id = GuildId::new(config.bot.get_staff_guild_id());
let inbox_category_id = ChannelId::new(config.thread.inbox_category_id);

let channel_builder = CreateChannel::new(&thread_name).category(inbox_category_id);

let channel = staff_guild_id
.create_channel(&bot_http, channel_builder)
.await
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to create Discord channel: {}", e),
)
})?;

create_thread_for_user(&channel, user_id_u64 as i64, &username, &db_pool)
.await
.map_err(|e| {
let http_clone = bot_http.clone();
let channel_id = channel.id;
tokio::spawn(async move {
let _ = http_clone.delete_channel(channel_id, None).await;
});
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to create thread record: {}", e),
)
})?;

let community_guild_id = GuildId::new(config.bot.get_community_guild_id());
let member_join_date = community_guild_id
.member(&bot_http, user_id)
.await
.ok()
.and_then(|m| m.joined_at)
.map(|dt| dt.format("%Y-%m-%d").to_string())
.unwrap_or_else(|| "Unknown".to_string());

let logs_count = match get_logs_from_user_id(&user_id.to_string(), &db_pool).await {
Ok(logs) => logs.len(),
Err(_) => 0,
};

let params = {
let mut p = HashMap::new();
p.insert("logs_count".to_string(), logs_count.to_string());
p.insert("prefix".to_string(), config.command.prefix.clone());
p
};

let logs_info = get_translated_message(
&config,
"new_thread.show_logs",
Some(&params),
None,
None,
None,
)
.await;

let open_thread_message = get_user_recap(user_id, &username, &member_join_date, &logs_info);

let ctx = {
let state = bot_state.lock().await;
let ctx_lock = state.bot_context.read().await;
ctx_lock
.as_ref()
.ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
"Bot context not available".to_string(),
))?
.clone()
};

if let Err(e) = MessageBuilder::system_message(&ctx, &config)
.to_channel(channel.id)
.content(open_thread_message)
.send(true)
.await
{
eprintln!("Failed to send message to channel via MessageBuilder: {:?}", e);
}

if let Err(e) = MessageBuilder::system_message(&ctx, &config)
.content(&config.bot.welcome_message)
.to_user(user_id)
.send(true)
.await
{
eprintln!("Failed to send DM via MessageBuilder: {:?}", e);
}

println!(
"API Key #{} creating ticket for Discord ID: {:?}",
api_key.id, update.discord_id
"API Key #{} successfully created ticket for user {} (channel: {})",
api_key.id, username, channel.id
);

Ok(Json(serde_json::json!({
"status": "ticket created",
"message": "Ticket creation endpoint - implementation pending"
"success": true,
"channel_id": channel.id.to_string(),
"user_id": user_id_u64.to_string(),
"username": username,
"message": "Ticket created successfully"
})))
}
26 changes: 8 additions & 18 deletions rustmail/src/api/middleware/auth.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::prelude::api::*;
use crate::prelude::types::*;
use crate::prelude::db::*;
use axum::extract::State;
use crate::prelude::types::*;
use axum::extract::Request;
use axum::extract::State;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use axum_extra::extract::CookieJar;
Expand Down Expand Up @@ -87,12 +87,6 @@ pub async fn auth_middleware(
mut req: Request,
next: Next,
) -> Response {
let session_cookie = jar.get("session_id");

if session_cookie.is_none() {
return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
}

let db_pool = {
let state_lock = bot_state.lock().await;
match &state_lock.db_pool {
Expand All @@ -111,7 +105,7 @@ pub async fn auth_middleware(
if let Ok(api_key_str) = api_key_header.to_str() {
let key_hash = hash_api_key(api_key_str);

match get_api_key_by_hash(&db_pool, &key_hash).await {
return match get_api_key_by_hash(&db_pool, &key_hash).await {
Ok(Some(api_key)) => {
if api_key.is_valid() {
let pool_clone = db_pool.clone();
Expand All @@ -121,21 +115,17 @@ pub async fn auth_middleware(
});

req.extensions_mut().insert(api_key);
return next.run(req).await;
next.run(req).await
} else {
return (StatusCode::UNAUTHORIZED, "API key expired or inactive")
.into_response();
(StatusCode::UNAUTHORIZED, "API key expired or inactive").into_response()
}
}
Ok(None) => {
return (StatusCode::UNAUTHORIZED, "Invalid API key").into_response();
}
Ok(None) => (StatusCode::UNAUTHORIZED, "Invalid API key").into_response(),
Err(e) => {
eprintln!("Error fetching API key: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
.into_response();
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response()
}
}
};
}
}

Expand Down
2 changes: 2 additions & 0 deletions rustmail/src/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub async fn init_bot_state() -> Arc<Mutex<BotState>> {
db_pool: Some(pool),
command_tx: command_tx.clone(),
bot_http: None,
bot_context: Arc::new(tokio::sync::RwLock::new(None)),
};

Arc::new(Mutex::new(bot_state))
Expand Down Expand Up @@ -147,6 +148,7 @@ pub async fn run_bot(
&config,
registry.clone(),
shutdown_rx.clone(),
bot_state.clone(),
))
.event_handler(
GuildMessagesHandler::new(
Expand Down
19 changes: 17 additions & 2 deletions rustmail/src/handlers/ready_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::prelude::commands::*;
use crate::prelude::config::*;
use crate::prelude::features::*;
use crate::prelude::modules::*;
use crate::prelude::types::*;
use serenity::all::{ActivityData, CreateCommand, GuildId};
use serenity::futures::future::join_all;
use serenity::{
Expand All @@ -12,22 +13,29 @@ use serenity::{
use sqlx::SqlitePool;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch::Receiver;
use tokio::sync::{watch::Receiver, Mutex};
use tokio::time::interval;

#[derive(Clone)]
pub struct ReadyHandler {
pub config: Config,
pub registry: Arc<CommandRegistry>,
pub shutdown: Arc<Receiver<bool>>,
pub bot_state: Arc<Mutex<BotState>>,
}

impl ReadyHandler {
pub fn new(config: &Config, registry: Arc<CommandRegistry>, shutdown: Receiver<bool>) -> Self {
pub fn new(
config: &Config,
registry: Arc<CommandRegistry>,
shutdown: Receiver<bool>,
bot_state: Arc<Mutex<BotState>>,
) -> Self {
Self {
config: config.clone(),
registry,
shutdown: Arc::new(shutdown),
bot_state,
}
}
}
Expand All @@ -36,6 +44,13 @@ impl ReadyHandler {
impl EventHandler for ReadyHandler {
async fn ready(&self, ctx: Context, ready: Ready) {
println!("{} is online !", ready.user.name);

{
let state = self.bot_state.lock().await;
let mut ctx_lock = state.bot_context.write().await;
*ctx_lock = Some(ctx.clone());
}

let pool = match &self.config.db_pool {
Some(pool) => pool,
None => {
Expand Down
5 changes: 3 additions & 2 deletions rustmail/src/types/bot.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::prelude::config::*;
use serenity::all::Http;
use serenity::all::{Context, Http};
use std::sync::Arc;
use tokio::sync::watch::Sender;
use tokio::sync::{watch::Sender, RwLock};
use tokio::task::JoinHandle;

pub enum BotStatus {
Expand All @@ -25,4 +25,5 @@ pub struct BotState {
pub db_pool: Option<sqlx::SqlitePool>,
pub command_tx: tokio::sync::mpsc::Sender<BotCommand>,
pub bot_http: Option<Arc<Http>>,
pub bot_context: Arc<RwLock<Option<Context>>>,
}
1 change: 0 additions & 1 deletion rustmail_types/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ pub struct ConfigResponse {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct CreateTicket {
pub discord_id: String,
pub api_key: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down