1- use rand:: {
2- prelude:: { IteratorRandom , SliceRandom } ,
3- seq:: IndexedRandom ,
4- Rng ,
5- } ;
1+ use rand:: { prelude:: { IteratorRandom , SliceRandom } , seq:: IndexedRandom , Rng } ;
62use serde_rusqlite:: from_row;
73use std:: { sync:: Arc , time:: Instant } ;
84use tokio:: sync:: MutexGuard ;
9- use tokio:: time:: Instant as TokioInstant ;
105use twilight_http:: Client as HttpClient ;
116use twilight_model:: { gateway:: payload:: incoming:: MessageCreate , id:: Id } ;
127use vesper:: twilight_exports:: UserMarker ;
138
149use crate :: {
15- ai_message,
16- color_quiz:: ColorQuiz ,
17- database:: User ,
18- math_test:: MathTest ,
19- structs:: { Command , List , PendingColorTest , PendingMathTest , State } ,
20- utils:: levels:: xp_required_for_level,
21- zalgos:: zalgify_text,
22- RESPONDERS ,
10+ ai_message, database:: User , quiz_handler, structs:: { Command , List , State } ,
11+ utils:: levels:: xp_required_for_level, zalgos:: zalgify_text, RESPONDERS ,
2312} ;
24- use twilight_model:: http:: attachment:: Attachment ;
2513
2614pub async fn handle_message (
2715 msg : & MessageCreate ,
@@ -37,276 +25,20 @@ pub async fn handle_message(
3725 }
3826 }
3927
40- // Check if there's a pending math test in this channel (anyone can answer)
41- if let Some ( pending_test) = locked_state. pending_math_tests . get ( & msg. channel_id . get ( ) ) {
42- let elapsed = pending_test. started_at . elapsed ( ) ;
43-
44- // Clone values we need before mutable operations
45- let question = pending_test. question . clone ( ) ;
46- let answer = pending_test. answer ;
47- let original_user_id = pending_test. user_id ;
48-
49- // Check if 30 seconds have passed
50- if elapsed. as_secs ( ) > 30 {
51- locked_state. pending_math_tests . remove ( & msg. channel_id . get ( ) ) ;
52-
53- // Timeout the original user for 1 minute
54- let timeout_until = twilight_model:: util:: Timestamp :: from_secs (
55- std:: time:: SystemTime :: now ( )
56- . duration_since ( std:: time:: UNIX_EPOCH )
57- . unwrap ( )
58- . as_secs ( ) as i64
59- + 60 ,
60- )
61- . unwrap ( ) ;
62-
63- if let Some ( guild_id) = msg. guild_id {
64- match http
65- . update_guild_member ( guild_id, Id :: new ( original_user_id) )
66- . communication_disabled_until ( Some ( timeout_until) )
67- {
68- Ok ( req) => {
69- if let Err ( e) = req. exec ( ) . await {
70- tracing:: error!( "Failed to execute timeout: {:?}" , e) ;
71- }
72- }
73- Err ( e) => {
74- tracing:: error!( "Failed to timeout user: {:?}" , e) ;
75- }
76- }
77- }
78-
79- return Ok ( Command :: text ( format ! (
80- "<@{}> Time's up! The answer was `{:.1}`. You've been timed out for 1 minute." ,
81- original_user_id, answer
82- ) ) ) ;
83- }
84-
85- // Check if answer is correct (anyone in channel can answer)
86- let user_answer = msg. content . trim ( ) ;
87- if ( MathTest { question, answer } ) . validate_answer ( user_answer) {
88- locked_state. pending_math_tests . remove ( & msg. channel_id . get ( ) ) ;
89-
90- // Award 50x normal XP (normal is 5-20, so this is 250-1000)
91- let bonus_xp = locked_state. rng . gen_range ( 250 ..1000 ) ;
92-
93- // Update user XP
94- let db = locked_state. db . get ( ) ?;
95- let mut statement = db. prepare ( "SELECT * FROM user WHERE id = ?" ) . unwrap ( ) ;
96- if let Ok ( mut user) = statement. query_one ( [ msg. author . id . get ( ) . to_string ( ) ] , |row| {
97- from_row :: < User > ( row) . map_err ( |_| rusqlite:: Error :: QueryReturnedNoRows )
98- } ) {
99- let level = user. level ;
100- let xp_required = xp_required_for_level ( level) ;
101- let new_xp = user. xp + bonus_xp;
102- user. name = msg. author . name . clone ( ) ;
103-
104- if new_xp >= xp_required {
105- let new_level = level + 1 ;
106- user. level = new_level;
107- user. xp = new_xp - xp_required;
108- user. update_sync ( & db) ?;
109-
110- return Ok ( Command :: text ( format ! (
111- "<@{}> Correct! Well done. You earned {} XP and leveled up to level {}!" ,
112- msg. author. id. get( ) ,
113- bonus_xp,
114- new_level
115- ) )
116- . reply ( ) ) ;
117- } else {
118- user. xp = new_xp;
119- user. update_sync ( & db) ?;
120- }
121- }
122-
123- return Ok ( Command :: text ( format ! (
124- "<@{}> Correct! Well done. You earned {} XP!" ,
125- msg. author. id. get( ) ,
126- bonus_xp
127- ) )
128- . reply ( ) ) ;
129- }
130- // Wrong answer - silently ignore (don't reply)
28+ if let Some ( cmd) = quiz_handler:: handle_math_quiz ( msg, & mut locked_state, http) . await {
29+ return Ok ( cmd) ;
13130 }
13231
133- // Check if there's a pending color test in this channel (anyone can answer)
134- if let Some ( pending_test) = locked_state. pending_color_tests . get ( & msg. channel_id . get ( ) ) {
135- let elapsed = pending_test. started_at . elapsed ( ) ;
136-
137- let r = pending_test. r ;
138- let g = pending_test. g ;
139- let b = pending_test. b ;
140- let original_user_id = pending_test. user_id ;
141-
142- if elapsed. as_secs ( ) > 60 {
143- locked_state. pending_color_tests . remove ( & msg. channel_id . get ( ) ) ;
144-
145- let timeout_until = twilight_model:: util:: Timestamp :: from_secs (
146- std:: time:: SystemTime :: now ( )
147- . duration_since ( std:: time:: UNIX_EPOCH )
148- . unwrap ( )
149- . as_secs ( ) as i64
150- + 60 ,
151- )
152- . unwrap ( ) ;
153-
154- if let Some ( guild_id) = msg. guild_id {
155- match http
156- . update_guild_member ( guild_id, Id :: new ( original_user_id) )
157- . communication_disabled_until ( Some ( timeout_until) )
158- {
159- Ok ( req) => {
160- if let Err ( e) = req. exec ( ) . await {
161- tracing:: error!( "Failed to execute timeout: {:?}" , e) ;
162- }
163- }
164- Err ( e) => {
165- tracing:: error!( "Failed to timeout user: {:?}" , e) ;
166- }
167- }
168- }
169-
170- return Ok ( Command :: text ( format ! (
171- "<@{}> Time's up! The color was `rgb({}, {}, {})` or `#{:02x}{:02x}{:02x}`. You've been timed out for 1 minute." ,
172- original_user_id,
173- r,
174- g,
175- b,
176- r,
177- g,
178- b
179- ) ) ) ;
180- }
181-
182- let user_answer = msg. content . trim ( ) ;
183- let quiz = ColorQuiz { r, g, b } ;
184-
185- if quiz. validate_answer ( user_answer) {
186- locked_state. pending_color_tests . remove ( & msg. channel_id . get ( ) ) ;
187-
188- // Award same XP as math test (250-1000)
189- let bonus_xp = locked_state. rng . gen_range ( 250 ..1000 ) ;
190-
191- // Update user XP
192- let db = locked_state. db . get ( ) ?;
193- let mut statement = db. prepare ( "SELECT * FROM user WHERE id = ?" ) . unwrap ( ) ;
194- if let Ok ( mut user) = statement. query_one ( [ msg. author . id . get ( ) . to_string ( ) ] , |row| {
195- from_row :: < User > ( row) . map_err ( |_| rusqlite:: Error :: QueryReturnedNoRows )
196- } ) {
197- let level = user. level ;
198- let xp_required = xp_required_for_level ( level) ;
199- let new_xp = user. xp + bonus_xp;
200- user. name = msg. author . name . clone ( ) ;
201-
202- if new_xp >= xp_required {
203- let new_level = level + 1 ;
204- user. level = new_level;
205- user. xp = new_xp - xp_required;
206- user. update_sync ( & db) ?;
207-
208- return Ok ( Command :: text ( format ! (
209- "<@{}> Correct! The color was `rgb({}, {}, {})` or `#{:02x}{:02x}{:02x}`. You earned {} XP and leveled up to level {}!" ,
210- msg. author. id. get( ) ,
211- r,
212- g,
213- b,
214- r,
215- g,
216- b,
217- bonus_xp,
218- new_level
219- ) )
220- . reply ( ) ) ;
221- } else {
222- user. xp = new_xp;
223- user. update_sync ( & db) ?;
224- }
225- }
226-
227- return Ok ( Command :: text ( format ! (
228- "<@{}> Correct! The color was `rgb({}, {}, {})` or `#{:02x}{:02x}{:02x}`. You earned {} XP!" ,
229- msg. author. id. get( ) ,
230- r,
231- g,
232- b,
233- r,
234- g,
235- b,
236- bonus_xp
237- ) )
238- . reply ( ) ) ;
239- }
240- // Wrong answer - silently ignore (don't reply)
32+ if let Some ( cmd) = quiz_handler:: handle_color_quiz ( msg, & mut locked_state, http) . await {
33+ return Ok ( cmd) ;
24134 }
24235
243- // Random 1/100 chance to trigger math test
244- let should_trigger_math = locked_state. config . openai_api_key . is_some ( )
245- && locked_state. rng . gen_range ( 0 ..100 ) == 42
246- && !locked_state. pending_math_tests . contains_key ( & msg. channel_id . get ( ) )
247- && !locked_state. pending_color_tests . contains_key ( & msg. channel_id . get ( ) ) ;
248-
249- if should_trigger_math {
250- let api_key = locked_state. config . openai_api_key . clone ( ) . unwrap ( ) ;
251- let db_clone = locked_state. db . clone ( ) ;
252-
253- // Use a new RNG for the async operation
254- let mut new_rng = rand:: thread_rng ( ) ;
255-
256- match MathTest :: generate ( & api_key, & db_clone, & mut new_rng) . await {
257- Ok ( test) => {
258- let pending = PendingMathTest {
259- user_id : msg. author . id . get ( ) ,
260- channel_id : msg. channel_id . get ( ) ,
261- question : test. question . clone ( ) ,
262- answer : test. answer ,
263- started_at : TokioInstant :: now ( ) ,
264- } ;
265-
266- locked_state. pending_math_tests . insert ( msg. channel_id . get ( ) , pending) ;
267-
268- return Ok ( Command :: text ( format ! (
269- "**MATH TEST TIME!** Solve this in 30 seconds:\n `{}`\n (Answer to 1 decimal place)" ,
270- test. question
271- ) ) ) ;
272- }
273- Err ( e) => {
274- tracing:: error!( "Failed to generate math test: {:?}" , e) ;
275- }
276- }
36+ if let Some ( cmd) = quiz_handler:: trigger_math_quiz ( msg, & mut locked_state) . await {
37+ return Ok ( cmd) ;
27738 }
27839
279- let should_trigger_color = locked_state. rng . gen_range ( 0 ..100 ) == 42
280- && !locked_state. pending_color_tests . contains_key ( & msg. channel_id . get ( ) )
281- && !locked_state. pending_math_tests . contains_key ( & msg. channel_id . get ( ) ) ;
282-
283- if should_trigger_color {
284- let quiz = ColorQuiz :: generate ( & mut locked_state. rng ) ;
285-
286- match quiz. generate_image ( ) {
287- Ok ( image_data) => {
288- let pending = PendingColorTest {
289- user_id : msg. author . id . get ( ) ,
290- channel_id : msg. channel_id . get ( ) ,
291- r : quiz. r ,
292- g : quiz. g ,
293- b : quiz. b ,
294- started_at : TokioInstant :: now ( ) ,
295- } ;
296-
297- locked_state. pending_color_tests . insert ( msg. channel_id . get ( ) , pending) ;
298-
299- return Ok ( Command :: text ( "**COLOR QUIZ TIME!** Guess this color in 60 seconds!\n Format: `#RRGGBB`" )
300- . attachments ( vec ! [ Attachment :: from_bytes(
301- "color.png" . to_string( ) ,
302- image_data,
303- 1
304- ) ] ) ) ;
305- }
306- Err ( e) => {
307- tracing:: error!( "Failed to generate color quiz image: {:?}" , e) ;
308- }
309- }
40+ if let Some ( cmd) = quiz_handler:: trigger_color_quiz ( msg, & mut locked_state) . await {
41+ return Ok ( cmd) ;
31042 }
31143
31244 let user = {
0 commit comments