mirror of
https://github.com/dsec-hub/dsec-discord-bot.git
synced 2026-09-22 07:44:26 +00:00
COR-03: fix verification failing on a member's first attempt
The insert into dsec_discord_members deserialised the response into
Vec<DiscordMemberRow>, but without `.returning(...)` supabase-lib-rs never sends
`Prefer: return=representation`, so PostgREST returns a 201 with an empty body.
Parsing that empty body failed and the `?` aborted before add_role and before
any reply — so every member's first click died with "This interaction failed",
while the row was still written (which is why the second attempt worked). Add
`.returning("student_id,discord_id")` so the row comes back and parses.
Also wrap the post-modal verification work so any failure sends the user an
ephemeral "something went wrong" instead of a dead interaction (poise's on_error
has no handle to the modal submission), and set an explicit on_error on
FrameworkOptions that replies ephemerally on command errors and keeps the
default logging. The database-write-first ordering is left unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XrE7F9ZuBWdQnS8CZvYDE
This commit is contained in:
parent
14a2fa8c2f
commit
2b668388d5
2 changed files with 106 additions and 50 deletions
|
|
@ -86,12 +86,18 @@ async fn add_dsec_discord_table(
|
||||||
"discord_id": member_id,
|
"discord_id": member_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// `.returning(...)` makes supabase-lib-rs send `Prefer: return=representation`.
|
||||||
|
// Without it PostgREST defaults a POST to `return=minimal` — a 201 with an
|
||||||
|
// empty body — and deserialising that empty body into Vec<DiscordMemberRow>
|
||||||
|
// failed, aborting before the role grant even though the row was written.
|
||||||
|
// That is why verification failed on every member's first attempt (COR-03).
|
||||||
let _: Vec<DiscordMemberRow> = data
|
let _: Vec<DiscordMemberRow> = data
|
||||||
.state
|
.state
|
||||||
.supabase
|
.supabase
|
||||||
.database()
|
.database()
|
||||||
.insert("dsec_discord_members")
|
.insert("dsec_discord_members")
|
||||||
.values(new_member)?
|
.values(new_member)?
|
||||||
|
.returning("student_id,discord_id")
|
||||||
.execute()
|
.execute()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -220,9 +226,14 @@ async fn handle_verify(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
// From here on we hold the modal-submit token, so the slower member fetch
|
// From here on we hold the modal-submit token, so the slower member fetch and
|
||||||
// and database work below is no longer racing the button's ack window.
|
// database work below is no longer racing the button's ack window. Wrap that
|
||||||
|
// work so any failure (e.g. a database error) still sends the user an
|
||||||
|
// ephemeral message rather than leaving a dead "This interaction failed"
|
||||||
|
// interaction — poise's on_error cannot reach this modal submission (COR-03).
|
||||||
let user_id = component_interaction.user.id;
|
let user_id = component_interaction.user.id;
|
||||||
|
|
||||||
|
let verify_result: Result<(), Error> = async {
|
||||||
let discord_member = GuildId::member(guild_id, ctx, user_id).await?;
|
let discord_member = GuildId::member(guild_id, ctx, user_id).await?;
|
||||||
|
|
||||||
let input_student_id = modal_data.student_id.to_lowercase();
|
let input_student_id = modal_data.student_id.to_lowercase();
|
||||||
|
|
@ -282,6 +293,28 @@ async fn handle_verify(
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(err) = verify_result {
|
||||||
|
eprintln!("[verify] verification failed after modal submit: {err}");
|
||||||
|
// Best-effort ephemeral error so the user does not see the generic
|
||||||
|
// "This interaction failed" with no way forward.
|
||||||
|
let _ = modal_submit
|
||||||
|
.create_response(
|
||||||
|
ctx,
|
||||||
|
ephemeral_embed(
|
||||||
|
CreateEmbed::new()
|
||||||
|
.title("Something went wrong")
|
||||||
|
.description(
|
||||||
|
"A maintainer has been notified. Please try again in a minute.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
23
src/main.rs
23
src/main.rs
|
|
@ -126,6 +126,28 @@ async fn event_handler(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Framework-level error handler. On a slash-command error it replies ephemerally
|
||||||
|
/// so the user is not left with a dead interaction, and it always runs poise's
|
||||||
|
/// default logging. `FrameworkOptions::default()` installs this default logger
|
||||||
|
/// too, but nothing surfaces it (no tracing subscriber, no log shipping — OPS-04),
|
||||||
|
/// so an explicit handler is set (COR-03).
|
||||||
|
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
|
||||||
|
if let poise::FrameworkError::Command { ctx, .. } = &error {
|
||||||
|
let _ = ctx
|
||||||
|
.send(
|
||||||
|
poise::CreateReply::default()
|
||||||
|
.content(
|
||||||
|
"Something went wrong — a maintainer has been notified. Please try again in a minute.",
|
||||||
|
)
|
||||||
|
.ephemeral(true),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
if let Err(e) = poise::builtins::on_error(error).await {
|
||||||
|
eprintln!("[on_error] failed while handling a framework error: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenv().ok(); // load env
|
dotenv().ok(); // load env
|
||||||
|
|
@ -155,6 +177,7 @@ async fn main() {
|
||||||
event_handler: |ctx, event, framework, data| {
|
event_handler: |ctx, event, framework, data| {
|
||||||
Box::pin(event_handler(ctx, event, framework, data))
|
Box::pin(event_handler(ctx, event, framework, data))
|
||||||
},
|
},
|
||||||
|
on_error: |error| Box::pin(on_error(error)),
|
||||||
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue