@@ -14,13 +14,16 @@ export default class ModApproveComponent implements ComponentHandler<string> {
1414 public async handle ( interaction : APIMessageComponentInteraction , questionIdStr : string ) {
1515 const questionId = Number . parseInt ( questionIdStr , 10 ) ;
1616
17- // Fetch the question and AMA session
17+ // Ack within Discord's 3s window before doing any DB/REST work below; everything past this point
18+ // finishes via editReply/followUp instead of reply/updateMessage.
19+ await client . api . interactions . deferMessageUpdate ( interaction . id , interaction . token ) ;
20+
1821 const [ question ] = await getContext ( ) . db < AmaQuestions [ ] > `
1922 SELECT * FROM ama_questions WHERE id = ${ questionId }
2023 ` ;
2124
2225 if ( ! question ) {
23- await client . api . interactions . reply ( interaction . id , interaction . token , {
26+ await client . api . interactions . followUp ( interaction . application_id , interaction . token , {
2427 content : 'Question not found. It may have been deleted.' ,
2528 flags : MessageFlags . Ephemeral ,
2629 } ) ;
@@ -36,13 +39,33 @@ export default class ModApproveComponent implements ComponentHandler<string> {
3639 }
3740
3841 if ( session . ended ) {
39- await client . api . interactions . reply ( interaction . id , interaction . token , {
42+ await client . api . interactions . followUp ( interaction . application_id , interaction . token , {
4043 content : 'This AMA session has ended.' ,
4144 flags : MessageFlags . Ephemeral ,
4245 } ) ;
4346 return ;
4447 }
4548
49+ // Determine the next queue up front so the claim below can move the row straight to its target
50+ // state; this also doubles as a lock — only one concurrent click can win the row.
51+ const nextQueue = getNextQueue ( CurrentlyInQueue . mod , session ) ;
52+ const targetState = nextQueue ?. kind === CurrentlyInQueue . guest ? 'PENDING_GUEST_REVIEW' : 'APPROVED' ;
53+
54+ const [ claimed ] = await getContext ( ) . db < AmaQuestions [ ] > `
55+ UPDATE ama_questions
56+ SET state = ${ targetState } , updated_at = now()
57+ WHERE id = ${ question . id } AND state = 'PENDING_MOD_REVIEW'
58+ RETURNING *
59+ ` ;
60+
61+ if ( ! claimed ) {
62+ await client . api . interactions . followUp ( interaction . application_id , interaction . token , {
63+ content : 'This question was already handled by another moderator.' ,
64+ flags : MessageFlags . Ephemeral ,
65+ } ) ;
66+ return ;
67+ }
68+
4669 // Get user details from the interaction
4770 const user = await client . api . users . get ( question . authorId ) ;
4871 const member = interaction . guild_id
@@ -53,9 +76,6 @@ export default class ModApproveComponent implements ComponentHandler<string> {
5376 // question text itself comes straight from the DB (the source message's text has a footer baked in).
5477 const attachments = interaction . message . attachments ?? [ ] ;
5578
56- // Determine the next queue
57- const nextQueue = getNextQueue ( CurrentlyInQueue . mod , session ) ;
58-
5979 try {
6080 if ( nextQueue ?. kind === CurrentlyInQueue . guest ) {
6181 // Post to guest queue
@@ -69,9 +89,7 @@ export default class ModApproveComponent implements ComponentHandler<string> {
6989 } ) ;
7090
7191 await getContext ( ) . db `
72- UPDATE ama_questions
73- SET state = 'PENDING_GUEST_REVIEW', guest_queue_message_id = ${ msg . id } , updated_at = now()
74- WHERE id = ${ question . id }
92+ UPDATE ama_questions SET guest_queue_message_id = ${ msg . id } WHERE id = ${ question . id }
7593 ` ;
7694 } else {
7795 // Post directly to answers channel
@@ -85,14 +103,12 @@ export default class ModApproveComponent implements ComponentHandler<string> {
85103 } ) ;
86104
87105 await getContext ( ) . db `
88- UPDATE ama_questions
89- SET state = 'APPROVED', answers_message_id = ${ msg . id } , updated_at = now()
90- WHERE id = ${ question . id }
106+ UPDATE ama_questions SET answers_message_id = ${ msg . id } WHERE id = ${ question . id }
91107 ` ;
92108 }
93109
94110 // Update the message to show it was approved
95- await client . api . interactions . updateMessage ( interaction . id , interaction . token , {
111+ await client . api . interactions . editReply ( interaction . application_id , interaction . token , {
96112 components : [
97113 {
98114 type : ComponentType . ActionRow ,
@@ -109,8 +125,11 @@ export default class ModApproveComponent implements ComponentHandler<string> {
109125 ] ,
110126 } ) ;
111127 } catch ( error ) {
128+ // The row is already claimed (state flipped) at this point; if the queue post itself failed, the
129+ // question is stuck claimed with no downstream message and needs manual follow-up — logged loudly
130+ // here rather than attempting a rollback/saga for what should be a rare failure mode.
112131 getContext ( ) . logger . error ( { error, questionId } , 'Failed to approve question' ) ;
113- await client . api . interactions . reply ( interaction . id , interaction . token , {
132+ await client . api . interactions . followUp ( interaction . application_id , interaction . token , {
114133 content : 'Failed to approve question. Please try again.' ,
115134 flags : MessageFlags . Ephemeral ,
116135 } ) ;
0 commit comments