Skip to content

Commit 8a0665e

Browse files
committed
feat(api): add logic for external 'create ticket' api endpoint
1 parent 3a2fa94 commit 8a0665e

3 files changed

Lines changed: 194 additions & 34 deletions

File tree

Lines changed: 186 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,192 @@ 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 (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

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

37205
Ok(Json(serde_json::json!({
38-
"status": "ticket created",
39-
"message": "Ticket creation endpoint - implementation pending"
206+
"success": true,
207+
"channel_id": channel.id.to_string(),
208+
"user_id": user_id_u64.to_string(),
209+
"username": username,
210+
"message": "Ticket created successfully"
40211
})))
41212
}

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_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)