fix: don't burn a Telegram pairing code on the way out
Nightly Build / build (push) Successful in 7m47s

apply_pairing_code consumes the pending entry and save_config writes that
consumption, so from that line on the code is spent — but the handler then
returned `?` on the per-user status blob. A failure there sent the user
back to the form holding a code that now reads "invalid or expired": the
one message guaranteed to make a pairing that actually succeeded look like
one that never happened. The blob is what the page renders as "linked";
the binding is real without it, so it warns instead.

The same write also refreshes shared.bindings directly. The dispatcher
learns the new binding through the ConfigKeyUpdated broadcast, which is
lossy, and a dropped event would leave the bot treating the chat as
unbound — asking the user to pair again, immediately after pairing. The
event is now a confirmation, not the delivery, on both sides of the flow.
This commit is contained in:
2026-08-06 23:36:34 +01:00
parent c1b90ba5f8
commit 94bffe6760
+22 -2
View File
@@ -251,9 +251,29 @@ impl Plugin for TelegramPlugin {
let mut cfg = auth::load_config(&*shared.config).await?; let mut cfg = auth::load_config(&*shared.config).await?;
let chat_id = auth::apply_pairing_code(&mut cfg, code, user_id)?; let chat_id = auth::apply_pairing_code(&mut cfg, code, user_id)?;
auth::save_config(&*shared.config, &cfg).await?; auth::save_config(&*shared.config, &cfg).await?;
ctx.user_config
// The code is spent the moment that write lands, so everything after it
// must be best-effort: an error from here on sends the user back to a
// form where their code now reads as "invalid or expired", which is the
// one message guaranteed to make them think the pairing never happened.
//
// Refreshing the cache is the same lossy-bus hole as on the issuing side
// (`auth::handle_pairing`): the binding reaches the dispatcher through a
// `ConfigKeyUpdated` broadcast, and a dropped event would leave the bot
// treating this chat as unbound — asking to pair again, right after a
// pairing that in fact succeeded. Writing it here makes the event a
// confirmation rather than the delivery.
*shared.bindings.write().await = cfg;
// The status blob is what the page renders as "linked"; the binding is
// already real without it.
if let Err(e) = ctx.user_config
.set(self.id(), user_id, json!({ "linked": true, "chat_id": chat_id })) .set(self.id(), user_id, json!({ "linked": true, "chat_id": chat_id }))
.await?; .await
{
warn!(user_id, chat_id, error = %e,
"telegram: paired, but the per-user status blob could not be stored");
}
info!(user_id, chat_id, "telegram: user self-paired via the web UI"); info!(user_id, chat_id, "telegram: user self-paired via the web UI");
Ok(()) Ok(())
} }