Fix Bind Topic Error by making callback handling more robust

- Wrap answerCallbackQuery and editMessageText in try-catch to handle expired queries
- Answer callback queries as early as possible
- Add setup_tracking to allowed pending action types for better type safety
- Ignore 'query is too old' and 'message is not modified' errors gracefully
This commit is contained in:
2026-03-15 02:50:32 +04:00
parent 5e39cdf455
commit 07c5ffb82d
3 changed files with 116 additions and 69 deletions

View File

@@ -945,16 +945,22 @@ export function registerHouseholdSetupCommands(options: {
}) })
if (result.status === 'rejected') { if (result.status === 'rejected') {
await ctx.answerCallbackQuery({ await ctx
text: adminRejectionMessage(locale, result.reason), .answerCallbackQuery({
show_alert: true text: adminRejectionMessage(locale, result.reason),
}) show_alert: true
})
.catch(() => {})
return return
} }
await ctx.answerCallbackQuery({ try {
text: t.setup.approvedMemberToast(result.member.displayName) await ctx.answerCallbackQuery({
}) text: t.setup.approvedMemberToast(result.member.displayName)
})
} catch {
// Ignore stale query
}
if (ctx.msg) { if (ctx.msg) {
const refreshed = await options.householdAdminService.listPendingMembers({ const refreshed = await options.householdAdminService.listPendingMembers({
@@ -963,13 +969,17 @@ export function registerHouseholdSetupCommands(options: {
}) })
if (refreshed.status === 'ok') { if (refreshed.status === 'ok') {
if (refreshed.members.length === 0) { try {
await ctx.editMessageText(t.setup.pendingMembersEmpty(refreshed.householdName)) if (refreshed.members.length === 0) {
} else { await ctx.editMessageText(t.setup.pendingMembersEmpty(refreshed.householdName))
const reply = pendingMembersReply(locale, refreshed) } else {
await ctx.editMessageText(reply.text, { const reply = pendingMembersReply(locale, refreshed)
reply_markup: reply.reply_markup await ctx.editMessageText(reply.text, {
}) reply_markup: reply.reply_markup
})
}
} catch {
// Ignore message edit errors
} }
} }
} }
@@ -989,10 +999,12 @@ export function registerHouseholdSetupCommands(options: {
const t = getBotTranslations(locale).setup const t = getBotTranslations(locale).setup
if (!isGroupChat(ctx)) { if (!isGroupChat(ctx)) {
await ctx.answerCallbackQuery({ await ctx
text: t.useButtonInGroup, .answerCallbackQuery({
show_alert: true text: t.useButtonInGroup,
}) show_alert: true
})
.catch(() => {})
return return
} }
@@ -1004,18 +1016,22 @@ export function registerHouseholdSetupCommands(options: {
: null : null
if (!actorIsAdmin) { if (!actorIsAdmin) {
await ctx.answerCallbackQuery({ await ctx
text: t.onlyTelegramAdminsBindTopics, .answerCallbackQuery({
show_alert: true text: t.onlyTelegramAdminsBindTopics,
}) show_alert: true
})
.catch(() => {})
return return
} }
if (!household) { if (!household) {
await ctx.answerCallbackQuery({ await ctx
text: t.householdNotConfigured, .answerCallbackQuery({
show_alert: true text: t.householdNotConfigured,
}) show_alert: true
})
.catch(() => {})
return return
} }
@@ -1031,13 +1047,15 @@ export function registerHouseholdSetupCommands(options: {
}) })
if (result.status === 'rejected') { if (result.status === 'rejected') {
await ctx.answerCallbackQuery({ await ctx
text: .answerCallbackQuery({
result.reason === 'not_admin' text:
? t.onlyTelegramAdminsBindTopics result.reason === 'not_admin'
: t.householdNotConfigured, ? t.onlyTelegramAdminsBindTopics
show_alert: true : t.householdNotConfigured,
}) show_alert: true
})
.catch(() => {})
return return
} }
@@ -1050,15 +1068,23 @@ export function registerHouseholdSetupCommands(options: {
botUsername: ctx.me.username botUsername: ctx.me.username
}) })
await ctx.answerCallbackQuery({ try {
text: t.setupTopicCreated(setupTopicRoleLabel(locale, role), topicName) await ctx.answerCallbackQuery({
}) text: t.setupTopicCreated(setupTopicRoleLabel(locale, role), topicName)
})
} catch {
// Ignore stale query
}
if (ctx.msg) { if (ctx.msg) {
await ctx.editMessageText( try {
reply.text, await ctx.editMessageText(
'reply_markup' in reply ? { reply_markup: reply.reply_markup } : {} reply.text,
) 'reply_markup' in reply ? { reply_markup: reply.reply_markup } : {}
)
} catch {
// Ignore message edit errors
}
} }
} catch (error) { } catch (error) {
const message = const message =
@@ -1067,10 +1093,12 @@ export function registerHouseholdSetupCommands(options: {
? t.setupTopicCreateForbidden ? t.setupTopicCreateForbidden
: t.setupTopicCreateFailed : t.setupTopicCreateFailed
await ctx.answerCallbackQuery({ await ctx
text: message, .answerCallbackQuery({
show_alert: true text: message,
}) show_alert: true
})
.catch(() => {})
} }
} }
) )
@@ -1085,10 +1113,12 @@ export function registerHouseholdSetupCommands(options: {
const t = getBotTranslations(locale).setup const t = getBotTranslations(locale).setup
if (!isGroupChat(ctx)) { if (!isGroupChat(ctx)) {
await ctx.answerCallbackQuery({ await ctx
text: t.useButtonInGroup, .answerCallbackQuery({
show_alert: true text: t.useButtonInGroup,
}) show_alert: true
})
.catch(() => {})
return return
} }
@@ -1098,10 +1128,12 @@ export function registerHouseholdSetupCommands(options: {
const actorIsAdmin = await isGroupAdmin(ctx) const actorIsAdmin = await isGroupAdmin(ctx)
if (!actorIsAdmin) { if (!actorIsAdmin) {
await ctx.answerCallbackQuery({ await ctx
text: t.onlyTelegramAdminsBindTopics, .answerCallbackQuery({
show_alert: true text: t.onlyTelegramAdminsBindTopics,
}) show_alert: true
})
.catch(() => {})
return return
} }
@@ -1113,27 +1145,37 @@ export function registerHouseholdSetupCommands(options: {
}) })
if (result.status === 'rejected') { if (result.status === 'rejected') {
await ctx.answerCallbackQuery({ await ctx
text: .answerCallbackQuery({
result.reason === 'not_admin' text:
? t.onlyTelegramAdminsBindTopics result.reason === 'not_admin'
: t.householdNotConfigured, ? t.onlyTelegramAdminsBindTopics
show_alert: true : t.householdNotConfigured,
}) show_alert: true
})
.catch(() => {})
return return
} }
await ctx.answerCallbackQuery({ try {
text: t.topicBoundSuccess( await ctx.answerCallbackQuery({
setupTopicRoleLabel(locale, role), text: t.topicBoundSuccess(
result.household.householdName setupTopicRoleLabel(locale, role),
) result.household.householdName
}) )
})
} catch {
// Ignore stale query
}
if (ctx.msg) { if (ctx.msg) {
await ctx.editMessageText( try {
t.topicBoundSuccess(setupTopicRoleLabel(locale, role), result.household.householdName) await ctx.editMessageText(
) t.topicBoundSuccess(setupTopicRoleLabel(locale, role), result.household.householdName)
)
} catch {
// Ignore message edit errors
}
} }
// Try to update the main /setup checklist if it exists // Try to update the main /setup checklist if it exists

View File

@@ -37,6 +37,10 @@ function parsePendingActionType(raw: string): TelegramPendingActionType {
return raw return raw
} }
if (raw === 'setup_tracking') {
return raw
}
throw new Error(`Unexpected telegram pending action type: ${raw}`) throw new Error(`Unexpected telegram pending action type: ${raw}`)
} }

View File

@@ -7,7 +7,8 @@ export const TELEGRAM_PENDING_ACTION_TYPES = [
'payment_topic_clarification', 'payment_topic_clarification',
'payment_topic_confirmation', 'payment_topic_confirmation',
'reminder_utility_entry', 'reminder_utility_entry',
'setup_topic_binding' 'setup_topic_binding',
'setup_tracking'
] as const ] as const
export type TelegramPendingActionType = (typeof TELEGRAM_PENDING_ACTION_TYPES)[number] export type TelegramPendingActionType = (typeof TELEGRAM_PENDING_ACTION_TYPES)[number]