Skip to content

Commit 164c8ee

Browse files
authored
Merge pull request #270 from Rustmail/269-create-ticket-api-endpoint
feat(api): create ticket api endpoint
2 parents 3a2fa94 + 43f7485 commit 164c8ee

6 files changed

Lines changed: 238 additions & 38 deletions

File tree

Lines changed: 208 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
use crate::db::repr::{ApiKey, Permission};
22
use crate::prelude::api::*;
3-
use crate::types::BotState;
3+
use crate::prelude::db::*;
4+
use crate::prelude::i18n::*;
5+
use crate::prelude::utils::*;
6+
use crate::types::{BotCommand, BotState};
47
use axum::Json;
58
use axum::extract::{Extension, State};
69
use axum::http::StatusCode;
710
use rustmail_types::CreateTicket;
11+
use serenity::all::{ChannelId, CreateChannel, GuildId, UserId};
12+
use std::collections::HashMap;
813
use std::sync::Arc;
914
use tokio::sync::Mutex;
1015

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

19-
let _current_config = {
24+
let user_id_u64 = update.discord_id.parse::<u64>().map_err(|_| {
25+
(
26+
StatusCode::BAD_REQUEST,
27+
"Invalid Discord ID format".to_string(),
28+
)
29+
})?;
30+
31+
let user_id = UserId::new(user_id_u64);
32+
33+
let (mut config, db_pool, bot_http, command_tx) = {
2034
let state = bot_state.lock().await;
21-
match &state.config {
22-
Some(c) => c.clone(),
23-
None => {
24-
return Err((
25-
StatusCode::INTERNAL_SERVER_ERROR,
26-
"Configuration not loaded".to_string(),
27-
));
28-
}
29-
}
35+
let config = state
36+
.config
37+
.as_ref()
38+
.ok_or((
39+
StatusCode::INTERNAL_SERVER_ERROR,
40+
"Configuration not loaded".to_string(),
41+
))?
42+
.clone();
43+
let db_pool = state
44+
.db_pool
45+
.as_ref()
46+
.ok_or((
47+
StatusCode::INTERNAL_SERVER_ERROR,
48+
"Database not available".to_string(),
49+
))?
50+
.clone();
51+
let bot_http = state
52+
.bot_http
53+
.as_ref()
54+
.ok_or((
55+
StatusCode::INTERNAL_SERVER_ERROR,
56+
"Bot HTTP client not available".to_string(),
57+
))?
58+
.clone();
59+
let command_tx = state.command_tx.clone();
60+
61+
(config, db_pool, bot_http, command_tx)
3062
};
3163

64+
config.db_pool = Some(db_pool.clone());
65+
66+
println!(
67+
"API Key #{} creating ticket for Discord ID: {}",
68+
api_key.id, user_id_u64
69+
);
70+
71+
let user = bot_http.get_user(user_id).await.map_err(|e| {
72+
(
73+
StatusCode::NOT_FOUND,
74+
format!("Discord user not found: {}", e),
75+
)
76+
})?;
77+
78+
if user.bot {
79+
return Err((
80+
StatusCode::BAD_REQUEST,
81+
"Cannot create ticket for bot users".to_string(),
82+
));
83+
}
84+
85+
let (tx, rx) = tokio::sync::oneshot::channel();
86+
command_tx
87+
.send(BotCommand::CheckUserIsMember {
88+
user_id: user_id_u64,
89+
resp: tx,
90+
})
91+
.await
92+
.map_err(|_| {
93+
(
94+
StatusCode::INTERNAL_SERVER_ERROR,
95+
"Failed to communicate with bot".to_string(),
96+
)
97+
})?;
98+
99+
let is_member = rx.await.map_err(|_| {
100+
(
101+
StatusCode::INTERNAL_SERVER_ERROR,
102+
"Failed to check guild membership".to_string(),
103+
)
104+
})?;
105+
106+
if !is_member {
107+
return Err((
108+
StatusCode::FORBIDDEN,
109+
"User is not a member of the community guild".to_string(),
110+
));
111+
}
112+
113+
if thread_exists(user_id, &db_pool).await {
114+
return if let Some(channel_id_str) = get_thread_channel_by_user_id(user_id, &db_pool).await
115+
{
116+
Err((
117+
StatusCode::CONFLICT,
118+
format!("User already has an active ticket: <#{}>", channel_id_str),
119+
))
120+
} else {
121+
Err((
122+
StatusCode::CONFLICT,
123+
"User already has an active ticket".to_string(),
124+
))
125+
};
126+
}
127+
128+
let username = user.name.clone();
129+
let thread_name = format!("🔴・{}・0m", username);
130+
let staff_guild_id = GuildId::new(config.bot.get_staff_guild_id());
131+
let inbox_category_id = ChannelId::new(config.thread.inbox_category_id);
132+
133+
let channel_builder = CreateChannel::new(&thread_name).category(inbox_category_id);
134+
135+
let channel = staff_guild_id
136+
.create_channel(&bot_http, channel_builder)
137+
.await
138+
.map_err(|e| {
139+
(
140+
StatusCode::INTERNAL_SERVER_ERROR,
141+
format!("Failed to create Discord channel: {}", e),
142+
)
143+
})?;
144+
145+
create_thread_for_user(&channel, user_id_u64 as i64, &username, &db_pool)
146+
.await
147+
.map_err(|e| {
148+
let http_clone = bot_http.clone();
149+
let channel_id = channel.id;
150+
tokio::spawn(async move {
151+
let _ = http_clone.delete_channel(channel_id, None).await;
152+
});
153+
(
154+
StatusCode::INTERNAL_SERVER_ERROR,
155+
format!("Failed to create thread record: {}", e),
156+
)
157+
})?;
158+
159+
let community_guild_id = GuildId::new(config.bot.get_community_guild_id());
160+
let member_join_date = community_guild_id
161+
.member(&bot_http, user_id)
162+
.await
163+
.ok()
164+
.and_then(|m| m.joined_at)
165+
.map(|dt| dt.format("%Y-%m-%d").to_string())
166+
.unwrap_or_else(|| "Unknown".to_string());
167+
168+
let logs_count = match get_logs_from_user_id(&user_id.to_string(), &db_pool).await {
169+
Ok(logs) => logs.len(),
170+
Err(_) => 0,
171+
};
172+
173+
let params = {
174+
let mut p = HashMap::new();
175+
p.insert("logs_count".to_string(), logs_count.to_string());
176+
p.insert("prefix".to_string(), config.command.prefix.clone());
177+
p
178+
};
179+
180+
let logs_info = get_translated_message(
181+
&config,
182+
"new_thread.show_logs",
183+
Some(&params),
184+
None,
185+
None,
186+
None,
187+
)
188+
.await;
189+
190+
let open_thread_message = get_user_recap(user_id, &username, &member_join_date, &logs_info);
191+
192+
let ctx = {
193+
let state = bot_state.lock().await;
194+
let ctx_lock = state.bot_context.read().await;
195+
ctx_lock
196+
.as_ref()
197+
.ok_or((
198+
StatusCode::INTERNAL_SERVER_ERROR,
199+
"Bot context not available".to_string(),
200+
))?
201+
.clone()
202+
};
203+
204+
if let Err(e) = MessageBuilder::system_message(&ctx, &config)
205+
.to_channel(channel.id)
206+
.content(open_thread_message)
207+
.send(true)
208+
.await
209+
{
210+
eprintln!("Failed to send message to channel via MessageBuilder: {:?}", e);
211+
}
212+
213+
if let Err(e) = MessageBuilder::system_message(&ctx, &config)
214+
.content(&config.bot.welcome_message)
215+
.to_user(user_id)
216+
.send(true)
217+
.await
218+
{
219+
eprintln!("Failed to send DM via MessageBuilder: {:?}", e);
220+
}
221+
32222
println!(
33-
"API Key #{} creating ticket for Discord ID: {:?}",
34-
api_key.id, update.discord_id
223+
"API Key #{} successfully created ticket for user {} (channel: {})",
224+
api_key.id, username, channel.id
35225
);
36226

37227
Ok(Json(serde_json::json!({
38-
"status": "ticket created",
39-
"message": "Ticket creation endpoint - implementation pending"
228+
"success": true,
229+
"channel_id": channel.id.to_string(),
230+
"user_id": user_id_u64.to_string(),
231+
"username": username,
232+
"message": "Ticket created successfully"
40233
})))
41234
}

rustmail/src/api/middleware/auth.rs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::prelude::api::*;
2-
use crate::prelude::types::*;
32
use crate::prelude::db::*;
4-
use axum::extract::State;
3+
use crate::prelude::types::*;
54
use axum::extract::Request;
5+
use axum::extract::State;
66
use axum::middleware::Next;
77
use axum::response::{IntoResponse, Response};
88
use axum_extra::extract::CookieJar;
@@ -87,12 +87,6 @@ pub async fn auth_middleware(
8787
mut req: Request,
8888
next: Next,
8989
) -> Response {
90-
let session_cookie = jar.get("session_id");
91-
92-
if session_cookie.is_none() {
93-
return (StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
94-
}
95-
9690
let db_pool = {
9791
let state_lock = bot_state.lock().await;
9892
match &state_lock.db_pool {
@@ -111,7 +105,7 @@ pub async fn auth_middleware(
111105
if let Ok(api_key_str) = api_key_header.to_str() {
112106
let key_hash = hash_api_key(api_key_str);
113107

114-
match get_api_key_by_hash(&db_pool, &key_hash).await {
108+
return match get_api_key_by_hash(&db_pool, &key_hash).await {
115109
Ok(Some(api_key)) => {
116110
if api_key.is_valid() {
117111
let pool_clone = db_pool.clone();
@@ -121,21 +115,17 @@ pub async fn auth_middleware(
121115
});
122116

123117
req.extensions_mut().insert(api_key);
124-
return next.run(req).await;
118+
next.run(req).await
125119
} else {
126-
return (StatusCode::UNAUTHORIZED, "API key expired or inactive")
127-
.into_response();
120+
(StatusCode::UNAUTHORIZED, "API key expired or inactive").into_response()
128121
}
129122
}
130-
Ok(None) => {
131-
return (StatusCode::UNAUTHORIZED, "Invalid API key").into_response();
132-
}
123+
Ok(None) => (StatusCode::UNAUTHORIZED, "Invalid API key").into_response(),
133124
Err(e) => {
134125
eprintln!("Error fetching API key: {}", e);
135-
return (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error")
136-
.into_response();
126+
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error").into_response()
137127
}
138-
}
128+
};
139129
}
140130
}
141131

rustmail/src/bot.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub async fn init_bot_state() -> Arc<Mutex<BotState>> {
3535
db_pool: Some(pool),
3636
command_tx: command_tx.clone(),
3737
bot_http: None,
38+
bot_context: Arc::new(tokio::sync::RwLock::new(None)),
3839
};
3940

4041
Arc::new(Mutex::new(bot_state))
@@ -147,6 +148,7 @@ pub async fn run_bot(
147148
&config,
148149
registry.clone(),
149150
shutdown_rx.clone(),
151+
bot_state.clone(),
150152
))
151153
.event_handler(
152154
GuildMessagesHandler::new(

rustmail/src/handlers/ready_handler.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::prelude::commands::*;
33
use crate::prelude::config::*;
44
use crate::prelude::features::*;
55
use crate::prelude::modules::*;
6+
use crate::prelude::types::*;
67
use serenity::all::{ActivityData, CreateCommand, GuildId};
78
use serenity::futures::future::join_all;
89
use serenity::{
@@ -12,22 +13,29 @@ use serenity::{
1213
use sqlx::SqlitePool;
1314
use std::sync::Arc;
1415
use std::time::Duration;
15-
use tokio::sync::watch::Receiver;
16+
use tokio::sync::{watch::Receiver, Mutex};
1617
use tokio::time::interval;
1718

1819
#[derive(Clone)]
1920
pub struct ReadyHandler {
2021
pub config: Config,
2122
pub registry: Arc<CommandRegistry>,
2223
pub shutdown: Arc<Receiver<bool>>,
24+
pub bot_state: Arc<Mutex<BotState>>,
2325
}
2426

2527
impl ReadyHandler {
26-
pub fn new(config: &Config, registry: Arc<CommandRegistry>, shutdown: Receiver<bool>) -> Self {
28+
pub fn new(
29+
config: &Config,
30+
registry: Arc<CommandRegistry>,
31+
shutdown: Receiver<bool>,
32+
bot_state: Arc<Mutex<BotState>>,
33+
) -> Self {
2734
Self {
2835
config: config.clone(),
2936
registry,
3037
shutdown: Arc::new(shutdown),
38+
bot_state,
3139
}
3240
}
3341
}
@@ -36,6 +44,13 @@ impl ReadyHandler {
3644
impl EventHandler for ReadyHandler {
3745
async fn ready(&self, ctx: Context, ready: Ready) {
3846
println!("{} is online !", ready.user.name);
47+
48+
{
49+
let state = self.bot_state.lock().await;
50+
let mut ctx_lock = state.bot_context.write().await;
51+
*ctx_lock = Some(ctx.clone());
52+
}
53+
3954
let pool = match &self.config.db_pool {
4055
Some(pool) => pool,
4156
None => {

rustmail/src/types/bot.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::prelude::config::*;
2-
use serenity::all::Http;
2+
use serenity::all::{Context, Http};
33
use std::sync::Arc;
4-
use tokio::sync::watch::Sender;
4+
use tokio::sync::{watch::Sender, RwLock};
55
use tokio::task::JoinHandle;
66

77
pub enum BotStatus {
@@ -25,4 +25,5 @@ pub struct BotState {
2525
pub db_pool: Option<sqlx::SqlitePool>,
2626
pub command_tx: tokio::sync::mpsc::Sender<BotCommand>,
2727
pub bot_http: Option<Arc<Http>>,
28+
pub bot_context: Arc<RwLock<Option<Context>>>,
2829
}

rustmail_types/src/api/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ pub struct ConfigResponse {
2020
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
2121
pub struct CreateTicket {
2222
pub discord_id: String,
23-
pub api_key: String,
2423
}
2524

2625
#[derive(Debug, Clone, Serialize, Deserialize)]

0 commit comments

Comments
 (0)