feat(bot): restore /bind command and improve /setup welcoming experience

This commit is contained in:
2026-03-15 01:17:22 +04:00
parent b2e1e0f213
commit f4e5a49621
6 changed files with 137 additions and 345 deletions

View File

@@ -874,13 +874,14 @@ describe('registerHouseholdSetupCommands', () => {
chat_id: -100123456 chat_id: -100123456
} }
}) })
expect(sendPayload.text).toContain('Kojori House is ready!') expect(sendPayload.text).toContain('Welcome! Kojori House is successfully registered')
expect(sendPayload.text).toContain('Topics: 0/5 configured') expect(sendPayload.text).toContain("Let's configure your household topics to get started")
expect(sendPayload.text).toContain('⚪ purchases') expect(sendPayload.text).toContain('(0/5)')
expect(sendPayload.text).toContain('⚪ payments') expect(sendPayload.text).toContain('⚪ Purchases')
expect(sendPayload.text).toContain('⚪ Payments')
// Check that join household button exists // Check that join household button exists
expect(JSON.stringify(sendPayload.reply_markup)).toContain('Join household') expect(JSON.stringify(sendPayload.reply_markup)).toContain('Join household')
expect(JSON.stringify(sendPayload.reply_markup)).toContain('+ purchases') expect(JSON.stringify(sendPayload.reply_markup)).toContain('Setup Purchases')
expect(JSON.stringify(sendPayload.reply_markup)).toContain('setup_topic:create:purchase') expect(JSON.stringify(sendPayload.reply_markup)).toContain('setup_topic:create:purchase')
}) })
@@ -998,7 +999,7 @@ describe('registerHouseholdSetupCommands', () => {
method: 'answerCallbackQuery', method: 'answerCallbackQuery',
payload: { payload: {
callback_query_id: 'callback-1', callback_query_id: 'callback-1',
text: 'purchases topic created and bound: Shared purchases.' text: 'Purchases topic created and bound: Shared purchases.'
} }
}) })
expect(calls[3]).toMatchObject({ expect(calls[3]).toMatchObject({
@@ -1006,7 +1007,7 @@ describe('registerHouseholdSetupCommands', () => {
payload: { payload: {
chat_id: -100123456, chat_id: -100123456,
message_id: 91, message_id: 91,
text: expect.stringContaining('✅ purchases') text: expect.stringContaining('✅ Purchases')
} }
}) })
@@ -1134,7 +1135,7 @@ describe('registerHouseholdSetupCommands', () => {
method: 'sendMessage', method: 'sendMessage',
payload: { payload: {
chat_id: -100123456, chat_id: -100123456,
text: 'Setup state reset for Kojori House. Run /setup again to bind topics from scratch.' text: 'Setup state reset for Kojori House. Run /setup again to configure topics from scratch.'
} }
}) })
expect(await repository.listHouseholdTopicBindings('household-1')).toEqual([]) expect(await repository.listHouseholdTopicBindings('household-1')).toEqual([])

View File

@@ -20,7 +20,7 @@ import { resolveReplyLocale } from './bot-locale'
const APPROVE_MEMBER_CALLBACK_PREFIX = 'approve_member:' const APPROVE_MEMBER_CALLBACK_PREFIX = 'approve_member:'
const SETUP_CREATE_TOPIC_CALLBACK_PREFIX = 'setup_topic:create:' const SETUP_CREATE_TOPIC_CALLBACK_PREFIX = 'setup_topic:create:'
const SETUP_BIND_TOPIC_ACTION = 'setup_topic_binding' as const
const HOUSEHOLD_TOPIC_ROLE_ORDER: readonly HouseholdTopicRole[] = [ const HOUSEHOLD_TOPIC_ROLE_ORDER: readonly HouseholdTopicRole[] = [
'chat', 'chat',
'purchase', 'purchase',
@@ -39,11 +39,6 @@ function isGroupChat(ctx: Context): ctx is Context & {
return ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup' return ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup'
} }
function isTopicMessage(ctx: Context): boolean {
const message = ctx.msg
return !!message && 'is_topic_message' in message && message.is_topic_message === true
}
async function isGroupAdmin(ctx: Context): Promise<boolean> { async function isGroupAdmin(ctx: Context): Promise<boolean> {
if (!ctx.chat || !ctx.from) { if (!ctx.chat || !ctx.from) {
return false return false
@@ -79,63 +74,6 @@ function unsetupRejectionMessage(
} }
} }
function bindRejectionMessage(
locale: BotLocale,
reason: 'not_admin' | 'household_not_found' | 'not_topic_message'
): string {
const t = getBotTranslations(locale).setup
switch (reason) {
case 'not_admin':
return t.onlyTelegramAdminsBindTopics
case 'household_not_found':
return t.householdNotConfigured
case 'not_topic_message':
return t.useCommandInTopic
}
}
function bindTopicUsageMessage(
locale: BotLocale,
role: 'chat' | 'purchase' | 'feedback' | 'reminders' | 'payments'
): string {
const t = getBotTranslations(locale).setup
switch (role) {
case 'chat':
return t.useBindChatTopicInGroup
case 'purchase':
return t.useBindPurchaseTopicInGroup
case 'feedback':
return t.useBindFeedbackTopicInGroup
case 'reminders':
return t.useBindRemindersTopicInGroup
case 'payments':
return t.useBindPaymentsTopicInGroup
}
}
function bindTopicSuccessMessage(
locale: BotLocale,
role: 'chat' | 'purchase' | 'feedback' | 'reminders' | 'payments',
householdName: string,
threadId: string
): string {
const t = getBotTranslations(locale).setup
switch (role) {
case 'chat':
return t.chatTopicSaved(householdName, threadId)
case 'purchase':
return t.purchaseTopicSaved(householdName, threadId)
case 'feedback':
return t.feedbackTopicSaved(householdName, threadId)
case 'reminders':
return t.remindersTopicSaved(householdName, threadId)
case 'payments':
return t.paymentsTopicSaved(householdName, threadId)
}
}
function adminRejectionMessage( function adminRejectionMessage(
locale: BotLocale, locale: BotLocale,
reason: 'not_admin' | 'household_not_found' | 'pending_not_found' reason: 'not_admin' | 'household_not_found' | 'pending_not_found'
@@ -473,63 +411,6 @@ export function registerHouseholdSetupCommands(options: {
}) })
} }
async function handleBindTopicCommand(
ctx: Context,
role: 'chat' | 'purchase' | 'feedback' | 'reminders' | 'payments'
): Promise<void> {
const locale = await resolveReplyLocale({
ctx,
repository: options.householdConfigurationRepository
})
if (!isGroupChat(ctx)) {
await ctx.reply(bindTopicUsageMessage(locale, role))
return
}
const actorIsAdmin = await isGroupAdmin(ctx)
const telegramThreadId =
isTopicMessage(ctx) && ctx.msg && 'message_thread_id' in ctx.msg
? ctx.msg.message_thread_id?.toString()
: undefined
const result = await options.householdSetupService.bindTopic({
actorIsAdmin,
telegramChatId: ctx.chat.id.toString(),
role,
...(telegramThreadId
? {
telegramThreadId
}
: {})
})
if (result.status === 'rejected') {
await ctx.reply(bindRejectionMessage(locale, result.reason))
return
}
options.logger?.info(
{
event: 'household_setup.topic_bound',
role: result.binding.role,
telegramChatId: result.household.telegramChatId,
telegramThreadId: result.binding.telegramThreadId,
householdId: result.household.householdId,
actorTelegramUserId: ctx.from?.id?.toString()
},
'Household topic bound'
)
await ctx.reply(
bindTopicSuccessMessage(
locale,
role,
result.household.householdName,
result.binding.telegramThreadId
)
)
}
options.bot.command('start', async (ctx) => { options.bot.command('start', async (ctx) => {
const fallbackLocale = await resolveReplyLocale({ const fallbackLocale = await resolveReplyLocale({
ctx, ctx,
@@ -714,7 +595,7 @@ export function registerHouseholdSetupCommands(options: {
if (result.status === 'noop') { if (result.status === 'noop') {
await options.promptRepository?.clearPendingActionsForChat( await options.promptRepository?.clearPendingActionsForChat(
telegramChatId, telegramChatId,
SETUP_BIND_TOPIC_ACTION 'setup_topic_binding'
) )
await ctx.reply(t.setup.unsetupNoop) await ctx.reply(t.setup.unsetupNoop)
return return
@@ -722,7 +603,7 @@ export function registerHouseholdSetupCommands(options: {
await options.promptRepository?.clearPendingActionsForChat( await options.promptRepository?.clearPendingActionsForChat(
telegramChatId, telegramChatId,
SETUP_BIND_TOPIC_ACTION 'setup_topic_binding'
) )
options.logger?.info( options.logger?.info(
@@ -738,103 +619,6 @@ export function registerHouseholdSetupCommands(options: {
await ctx.reply(t.setup.unsetupComplete(result.household.householdName)) await ctx.reply(t.setup.unsetupComplete(result.household.householdName))
}) })
options.bot.command('bind_chat_topic', async (ctx) => {
await handleBindTopicCommand(ctx, 'chat')
})
options.bot.command('bind_purchase_topic', async (ctx) => {
await handleBindTopicCommand(ctx, 'purchase')
})
options.bot.command('bind_feedback_topic', async (ctx) => {
await handleBindTopicCommand(ctx, 'feedback')
})
options.bot.command('bind_reminders_topic', async (ctx) => {
await handleBindTopicCommand(ctx, 'reminders')
})
options.bot.command('bind_payments_topic', async (ctx) => {
await handleBindTopicCommand(ctx, 'payments')
})
options.bot.command('bind', async (ctx) => {
const locale = await resolveReplyLocale({
ctx,
repository: options.householdConfigurationRepository
})
const t = getBotTranslations(locale)
if (!isGroupChat(ctx)) {
await ctx.reply(t.setup.useBindInTopic)
return
}
if (!options.householdConfigurationRepository) {
await ctx.reply(t.setup.householdNotConfigured)
return
}
const household = await options.householdConfigurationRepository.getTelegramHouseholdChat(
ctx.chat.id.toString()
)
if (!household) {
await ctx.reply(t.setup.householdNotConfigured)
return
}
if (!(await isGroupAdmin(ctx))) {
await ctx.reply(t.setup.onlyTelegramAdminsBindTopics)
return
}
const telegramThreadId =
ctx.msg && 'message_thread_id' in ctx.msg ? ctx.msg.message_thread_id?.toString() : null
// If not in a topic, show error
if (!telegramThreadId) {
await ctx.reply(t.setup.useBindInTopic)
return
}
// Check if this topic is already bound
const existingBinding =
await options.householdConfigurationRepository.findHouseholdTopicByTelegramContext({
telegramChatId: ctx.chat.id.toString(),
telegramThreadId
})
if (existingBinding) {
const roleLabel = setupTopicRoleLabel(locale, existingBinding.role)
await ctx.reply(t.setup.topicAlreadyBound(roleLabel))
return
}
// Get all existing bindings to show which roles are available
const bindings = await options.householdConfigurationRepository.listHouseholdTopicBindings(
household.householdId
)
const boundRoles = new Set(bindings.map((b) => b.role))
const availableRoles = HOUSEHOLD_TOPIC_ROLE_ORDER.filter((role) => !boundRoles.has(role))
if (availableRoles.length === 0) {
await ctx.reply(t.setup.allRolesConfigured)
return
}
// Show role selection buttons
await ctx.reply(t.setup.bindSelectRole, {
reply_markup: {
inline_keyboard: availableRoles.map((role) => [
{
text: setupTopicRoleLabel(locale, role),
callback_data: `bind_topic:${role}:${telegramThreadId}`
}
])
}
})
})
options.bot.command('pending_members', async (ctx) => { options.bot.command('pending_members', async (ctx) => {
const locale = await resolveReplyLocale({ const locale = await resolveReplyLocale({
ctx, ctx,
@@ -974,6 +758,67 @@ export function registerHouseholdSetupCommands(options: {
}) })
}) })
options.bot.command('bind', async (ctx) => {
const locale = await resolveReplyLocale({
ctx,
repository: options.householdConfigurationRepository
})
const t = getBotTranslations(locale)
if (!isGroupChat(ctx)) {
await ctx.reply(t.setup.useSetupInGroup)
return
}
if (!ctx.message?.message_thread_id) {
await ctx.reply(t.setup.useCommandInTopic)
return
}
const actorIsAdmin = await isGroupAdmin(ctx)
if (!actorIsAdmin) {
await ctx.reply(t.setup.onlyTelegramAdminsBindTopics)
return
}
const telegramChatId = ctx.chat.id.toString()
const household = options.householdConfigurationRepository
? await options.householdConfigurationRepository.getTelegramHouseholdChat(telegramChatId)
: null
if (!household) {
await ctx.reply(t.setup.householdNotConfigured)
return
}
const bindings = options.householdConfigurationRepository
? await options.householdConfigurationRepository.listHouseholdTopicBindings(
household.householdId
)
: []
const configuredRoles = new Set(bindings.map((b) => b.role))
const availableRoles = HOUSEHOLD_TOPIC_ROLE_ORDER.filter((role) => !configuredRoles.has(role))
if (availableRoles.length === 0) {
await ctx.reply(t.setup.allRolesConfigured)
return
}
const rows = availableRoles.map((role) => [
{
text: setupTopicRoleLabel(locale, role),
callback_data: `bind_topic:${role}:${ctx.message!.message_thread_id}`
}
])
await ctx.reply(t.setup.bindSelectRole, {
reply_markup: {
inline_keyboard: rows
}
})
})
options.bot.callbackQuery( options.bot.callbackQuery(
new RegExp(`^${APPROVE_MEMBER_CALLBACK_PREFIX}(\\d+)$`), new RegExp(`^${APPROVE_MEMBER_CALLBACK_PREFIX}(\\d+)$`),
async (ctx) => { async (ctx) => {
@@ -1095,7 +940,10 @@ export function registerHouseholdSetupCommands(options: {
if (result.status === 'rejected') { if (result.status === 'rejected') {
await ctx.answerCallbackQuery({ await ctx.answerCallbackQuery({
text: bindRejectionMessage(locale, result.reason), text:
result.reason === 'not_admin'
? t.onlyTelegramAdminsBindTopics
: t.householdNotConfigured,
show_alert: true show_alert: true
}) })
return return
@@ -1133,9 +981,8 @@ export function registerHouseholdSetupCommands(options: {
} }
) )
// Bind topic callback from /bind command
options.bot.callbackQuery( options.bot.callbackQuery(
new RegExp(`^bind_topic:(chat|purchase|feedback|reminders|payments):(\\d+)$`), /^bind_topic:(chat|purchase|feedback|reminders|payments):(\d+)$/,
async (ctx) => { async (ctx) => {
const locale = await resolveReplyLocale({ const locale = await resolveReplyLocale({
ctx, ctx,
@@ -1151,7 +998,11 @@ export function registerHouseholdSetupCommands(options: {
return return
} }
const role = ctx.match[1] as HouseholdTopicRole
const telegramThreadId = ctx.match[2]!
const telegramChatId = ctx.chat.id.toString()
const actorIsAdmin = await isGroupAdmin(ctx) const actorIsAdmin = await isGroupAdmin(ctx)
if (!actorIsAdmin) { if (!actorIsAdmin) {
await ctx.answerCallbackQuery({ await ctx.answerCallbackQuery({
text: t.onlyTelegramAdminsBindTopics, text: t.onlyTelegramAdminsBindTopics,
@@ -1160,19 +1011,19 @@ export function registerHouseholdSetupCommands(options: {
return return
} }
const role = ctx.match[1] as HouseholdTopicRole
const telegramThreadId = ctx.match[2]!
const result = await options.householdSetupService.bindTopic({ const result = await options.householdSetupService.bindTopic({
actorIsAdmin, actorIsAdmin,
telegramChatId: ctx.chat.id.toString(), telegramChatId,
telegramThreadId, telegramThreadId,
role role
}) })
if (result.status === 'rejected') { if (result.status === 'rejected') {
await ctx.answerCallbackQuery({ await ctx.answerCallbackQuery({
text: bindRejectionMessage(locale, result.reason), text:
result.reason === 'not_admin'
? t.onlyTelegramAdminsBindTopics
: t.householdNotConfigured,
show_alert: true show_alert: true
}) })
return return
@@ -1185,11 +1036,12 @@ export function registerHouseholdSetupCommands(options: {
) )
}) })
// Edit the role selection message to show success if (ctx.msg) {
await ctx.editMessageText( await ctx.editMessageText(
t.topicBoundSuccess(setupTopicRoleLabel(locale, role), result.household.householdName) t.topicBoundSuccess(setupTopicRoleLabel(locale, role), result.household.householdName)
) )
} }
}
) )
} }
} }

View File

@@ -9,12 +9,7 @@ export const enBotTranslations: BotTranslationCatalog = {
cancel: 'Cancel the current prompt', cancel: 'Cancel the current prompt',
setup: 'Register this group as a household', setup: 'Register this group as a household',
unsetup: 'Reset topic setup for this group', unsetup: 'Reset topic setup for this group',
bind_chat_topic: 'Bind the current topic for casual conversation', bind: 'Bind current topic to a specific role',
bind_purchase_topic: 'Bind the current topic as purchases',
bind_feedback_topic: 'Bind the current topic as feedback',
bind_reminders_topic: 'Bind the current topic as reminders',
bind_payments_topic: 'Bind the current topic as payments',
bind: 'Bind current topic to a household role',
join_link: 'Get a shareable link for new members to join', join_link: 'Get a shareable link for new members to join',
payment_add: 'Record your rent or utilities payment', payment_add: 'Record your rent or utilities payment',
pending_members: 'List pending household join requests', pending_members: 'List pending household join requests',
@@ -56,12 +51,19 @@ export const enBotTranslations: BotTranslationCatalog = {
joinRequestSent: (householdName) => joinRequestSent: (householdName) =>
`Join request sent for ${householdName}. Wait for a household admin to confirm you.`, `Join request sent for ${householdName}. Wait for a household admin to confirm you.`,
setupSummary: ({ householdName, created }) => setupSummary: ({ householdName, created }) =>
`${created ? '' : ''} ${householdName} is ${created ? 'ready' : 'already registered'}!`, `${created ? '' : ''} Welcome! ${householdName} is ${created ? 'successfully registered' : 'already active'} and ready to help.`,
setupTopicsHeading: (configured, total) => `Topics: ${configured}/${total} configured`, setupTopicsHeading: (configured, total) =>
`Let's configure your household topics to get started (${configured}/${total}):`,
setupTopicBound: (role) => `${role}`, setupTopicBound: (role) => `${role}`,
setupTopicMissing: (role) => `${role}`, setupTopicMissing: (role) => `${role}`,
setupTopicCreateButton: (role) => `+ ${role}`, setupTopicCreateButton: (role) => `Setup ${role}`,
setupTopicBindButton: (role) => `Bind ${role}`, setupTopicBindButton: (role) => `Bind ${role}`,
useBindInTopic: 'Run /bind inside a topic to link it to a role.',
topicAlreadyBound: (role) => `This topic is already linked to ${role}.`,
bindSelectRole: 'Link this topic to:',
topicBoundSuccess: (role, householdName) =>
`Successfully linked as ${role} for ${householdName}.`,
allRolesConfigured: 'All topic roles are already configured.',
setupTopicCreateFailed: setupTopicCreateFailed:
'I could not create that topic. Check bot admin permissions and forum settings.', 'I could not create that topic. Check bot admin permissions and forum settings.',
setupTopicCreateForbidden: setupTopicCreateForbidden:
@@ -73,15 +75,15 @@ export const enBotTranslations: BotTranslationCatalog = {
setupTopicBindRoleName: (role) => { setupTopicBindRoleName: (role) => {
switch (role) { switch (role) {
case 'chat': case 'chat':
return 'chat' return 'Discussions'
case 'purchase': case 'purchase':
return 'purchases' return 'Purchases'
case 'feedback': case 'feedback':
return 'feedback' return 'Feedback'
case 'reminders': case 'reminders':
return 'reminders' return 'Reminders'
case 'payments': case 'payments':
return 'payments' return 'Payments'
} }
}, },
setupTopicSuggestedName: (role) => { setupTopicSuggestedName: (role) => {
@@ -101,23 +103,8 @@ export const enBotTranslations: BotTranslationCatalog = {
onlyTelegramAdminsUnsetup: 'Only Telegram group admins can run /unsetup.', onlyTelegramAdminsUnsetup: 'Only Telegram group admins can run /unsetup.',
useUnsetupInGroup: 'Use /unsetup inside the household group.', useUnsetupInGroup: 'Use /unsetup inside the household group.',
unsetupComplete: (householdName) => unsetupComplete: (householdName) =>
`Setup state reset for ${householdName}. Run /setup again to bind topics from scratch.`, `Setup state reset for ${householdName}. Run /setup again to configure topics from scratch.`,
unsetupNoop: 'Nothing to reset for this group yet. Run /setup when you are ready.', unsetupNoop: 'Nothing to reset for this group yet. Run /setup when you are ready.',
useBindChatTopicInGroup: 'Use /bind_chat_topic inside the household group topic.',
chatTopicSaved: (householdName, threadId) =>
`Chat topic saved for ${householdName} (thread ${threadId}).`,
useBindPurchaseTopicInGroup: 'Use /bind_purchase_topic inside the household group topic.',
purchaseTopicSaved: (householdName, threadId) =>
`Purchase topic saved for ${householdName} (thread ${threadId}).`,
useBindFeedbackTopicInGroup: 'Use /bind_feedback_topic inside the household group topic.',
feedbackTopicSaved: (householdName, threadId) =>
`Feedback topic saved for ${householdName} (thread ${threadId}).`,
useBindRemindersTopicInGroup: 'Use /bind_reminders_topic inside the household group topic.',
remindersTopicSaved: (householdName, threadId) =>
`Reminders topic saved for ${householdName} (thread ${threadId}).`,
useBindPaymentsTopicInGroup: 'Use /bind_payments_topic inside the household group topic.',
paymentsTopicSaved: (householdName, threadId) =>
`Payments topic saved for ${householdName} (thread ${threadId}).`,
usePendingMembersInGroup: 'Use /pending_members inside the household group.', usePendingMembersInGroup: 'Use /pending_members inside the household group.',
useApproveMemberInGroup: 'Use /approve_member inside the household group.', useApproveMemberInGroup: 'Use /approve_member inside the household group.',
approveMemberUsage: 'Usage: /approve_member <telegram_user_id>', approveMemberUsage: 'Usage: /approve_member <telegram_user_id>',
@@ -130,12 +117,7 @@ export const enBotTranslations: BotTranslationCatalog = {
useJoinLinkInGroup: 'Use /join_link inside the household group.', useJoinLinkInGroup: 'Use /join_link inside the household group.',
joinLinkUnavailable: 'Could not generate join link.', joinLinkUnavailable: 'Could not generate join link.',
joinLinkReady: (link, householdName) => joinLinkReady: (link, householdName) =>
`Join link for ${householdName}:\n${link}\n\nAnyone with this link can join the household. Share it carefully.`, `Join link for ${householdName}:\n${link}\n\nAnyone with this link can join the household. Share it carefully.`
useBindInTopic: 'Use /bind inside a topic to bind it to a role.',
topicAlreadyBound: (role) => `This topic is already bound as ${role}.`,
bindSelectRole: 'Bind this topic as:',
topicBoundSuccess: (role, householdName) => `Bound as ${role} for ${householdName}.`,
allRolesConfigured: 'All topic roles are already configured.'
}, },
anonymousFeedback: { anonymousFeedback: {
title: 'Anonymous household note', title: 'Anonymous household note',
@@ -147,7 +129,7 @@ export const enBotTranslations: BotTranslationCatalog = {
multipleHouseholds: multipleHouseholds:
'You belong to multiple households. Open the target household from its group until household selection is added.', 'You belong to multiple households. Open the target household from its group until household selection is added.',
feedbackTopicMissing: feedbackTopicMissing:
'Anonymous feedback is not configured for your household yet. Ask an admin to run /bind_feedback_topic.', 'Anonymous feedback is not configured for your household yet. Ask an admin to run /setup and create a feedback topic.',
duplicate: 'This anonymous feedback message was already processed.', duplicate: 'This anonymous feedback message was already processed.',
delivered: 'Anonymous feedback delivered.', delivered: 'Anonymous feedback delivered.',
savedButPostFailed: 'Anonymous feedback was saved, but posting failed. Try again later.', savedButPostFailed: 'Anonymous feedback was saved, but posting failed. Try again later.',
@@ -325,7 +307,7 @@ export const enBotTranslations: BotTranslationCatalog = {
}, },
payments: { payments: {
topicMissing: topicMissing:
'Payments topic is not configured for this household yet. Ask an admin to run /bind_payments_topic.', 'Payments topic is not configured for this household yet. Ask an admin to run /setup and create a payments topic.',
balanceReply: (kind) => balanceReply: (kind) =>
kind === 'rent' ? 'Current rent payment guidance:' : 'Current utilities payment guidance:', kind === 'rent' ? 'Current rent payment guidance:' : 'Current utilities payment guidance:',
proposal: (kind, amount, currency) => proposal: (kind, amount, currency) =>

View File

@@ -9,12 +9,7 @@ export const ruBotTranslations: BotTranslationCatalog = {
cancel: 'Отменить текущий ввод', cancel: 'Отменить текущий ввод',
setup: 'Подключить эту группу как дом', setup: 'Подключить эту группу как дом',
unsetup: 'Сбросить настройку топиков для этой группы', unsetup: 'Сбросить настройку топиков для этой группы',
bind_chat_topic: 'Назначить текущий топик для разговоров', bind: 'Привязать текущий топик к конкретной роли',
bind_purchase_topic: 'Назначить текущий топик для покупок',
bind_feedback_topic: 'Назначить текущий топик для анонимных сообщений',
bind_reminders_topic: 'Назначить текущий топик для напоминаний',
bind_payments_topic: 'Назначить текущий топик для оплат',
bind: 'Привязать текущий топик к роли дома',
join_link: 'Получить ссылку для приглашения новых участников', join_link: 'Получить ссылку для приглашения новых участников',
payment_add: 'Подтвердить оплату аренды или коммуналки', payment_add: 'Подтвердить оплату аренды или коммуналки',
pending_members: 'Показать ожидающие заявки на вступление', pending_members: 'Показать ожидающие заявки на вступление',
@@ -58,12 +53,19 @@ export const ruBotTranslations: BotTranslationCatalog = {
joinRequestSent: (householdName) => joinRequestSent: (householdName) =>
`Заявка на вступление в ${householdName} отправлена. Дождитесь подтверждения от админа дома.`, `Заявка на вступление в ${householdName} отправлена. Дождитесь подтверждения от админа дома.`,
setupSummary: ({ householdName, created }) => setupSummary: ({ householdName, created }) =>
`${created ? '' : ''} ${householdName} ${created ? 'готов' : 'уже подключён'}!`, `${created ? '' : ''} Добро пожаловать! Дом ${householdName} ${created ? 'успешно зарегистрирован' : 'уже активен'} и готов к работе.`,
setupTopicsHeading: (configured, total) => `Топики: ${configured}/${total} настроено`, setupTopicsHeading: (configured, total) =>
`Давайте настроим топики для вашего дома (${configured}/${total}):`,
setupTopicBound: (role) => `${role}`, setupTopicBound: (role) => `${role}`,
setupTopicMissing: (role) => `${role}`, setupTopicMissing: (role) => `${role}`,
setupTopicCreateButton: (role) => `+ ${role}`, setupTopicCreateButton: (role) => `Настроить ${role}`,
setupTopicBindButton: (role) => `Привязать ${role}`, setupTopicBindButton: (role) => `Привязать ${role}`,
useBindInTopic: 'Используйте /bind внутри топика, чтобы привязать его к роли.',
topicAlreadyBound: (role) => `Этот топик уже привязан к роли «${role}».`,
bindSelectRole: 'Привязать этот топик к:',
topicBoundSuccess: (role, householdName) =>
`Топик успешно привязан как «${role}» для ${householdName}.`,
allRolesConfigured: 'Все роли топиков уже настроены.',
setupTopicCreateFailed: setupTopicCreateFailed:
'Не удалось создать этот топик. Проверьте права бота и включённые форум-топики в группе.', 'Не удалось создать этот топик. Проверьте права бота и включённые форум-топики в группе.',
setupTopicCreateForbidden: setupTopicCreateForbidden:
@@ -75,15 +77,15 @@ export const ruBotTranslations: BotTranslationCatalog = {
setupTopicBindRoleName: (role) => { setupTopicBindRoleName: (role) => {
switch (role) { switch (role) {
case 'chat': case 'chat':
return 'разговоров' return 'Общение'
case 'purchase': case 'purchase':
return 'покупки' return 'Покупки'
case 'feedback': case 'feedback':
return 'обратной связи' return 'Фидбек'
case 'reminders': case 'reminders':
return 'напоминаний' return 'Напоминания'
case 'payments': case 'payments':
return 'оплат' return 'Оплаты'
} }
}, },
setupTopicSuggestedName: (role) => { setupTopicSuggestedName: (role) => {
@@ -103,23 +105,8 @@ export const ruBotTranslations: BotTranslationCatalog = {
onlyTelegramAdminsUnsetup: 'Только админы Telegram-группы могут запускать /unsetup.', onlyTelegramAdminsUnsetup: 'Только админы Telegram-группы могут запускать /unsetup.',
useUnsetupInGroup: 'Используйте /unsetup внутри группы дома.', useUnsetupInGroup: 'Используйте /unsetup внутри группы дома.',
unsetupComplete: (householdName) => unsetupComplete: (householdName) =>
`Состояние настройки для ${householdName} сброшено. Запустите /setup ещё раз, чтобы заново привязать топики.`, `Состояние настройки для ${householdName} сброшено. Запустите /setup ещё раз, чтобы заново настроить топики.`,
unsetupNoop: 'Для этой группы пока нечего сбрасывать. Когда будете готовы, запустите /setup.', unsetupNoop: 'Для этой группы пока нечего сбрасывать. Когда будете готовы, запустите /setup.',
useBindChatTopicInGroup: 'Используйте /bind_chat_topic внутри топика группы дома.',
chatTopicSaved: (householdName, threadId) =>
`Топик для разговоров сохранён для ${householdName} (тред ${threadId}).`,
useBindPurchaseTopicInGroup: 'Используйте /bind_purchase_topic внутри топика группы дома.',
purchaseTopicSaved: (householdName, threadId) =>
`Топик покупок сохранён для ${householdName} (тред ${threadId}).`,
useBindFeedbackTopicInGroup: 'Используйте /bind_feedback_topic внутри топика группы дома.',
feedbackTopicSaved: (householdName, threadId) =>
`Топик обратной связи сохранён для ${householdName} (тред ${threadId}).`,
useBindRemindersTopicInGroup: 'Используйте /bind_reminders_topic внутри топика группы дома.',
remindersTopicSaved: (householdName, threadId) =>
`Топик напоминаний сохранён для ${householdName} (тред ${threadId}).`,
useBindPaymentsTopicInGroup: 'Используйте /bind_payments_topic внутри топика группы дома.',
paymentsTopicSaved: (householdName, threadId) =>
`Топик оплат сохранён для ${householdName} (тред ${threadId}).`,
usePendingMembersInGroup: 'Используйте /pending_members внутри группы дома.', usePendingMembersInGroup: 'Используйте /pending_members внутри группы дома.',
useApproveMemberInGroup: 'Используйте /approve_member внутри группы дома.', useApproveMemberInGroup: 'Используйте /approve_member внутри группы дома.',
approveMemberUsage: 'Использование: /approve_member <telegram_user_id>', approveMemberUsage: 'Использование: /approve_member <telegram_user_id>',
@@ -132,12 +119,7 @@ export const ruBotTranslations: BotTranslationCatalog = {
useJoinLinkInGroup: 'Используйте /join_link внутри группы дома.', useJoinLinkInGroup: 'Используйте /join_link внутри группы дома.',
joinLinkUnavailable: 'Не удалось сгенерировать ссылку для вступления.', joinLinkUnavailable: 'Не удалось сгенерировать ссылку для вступления.',
joinLinkReady: (link, householdName) => joinLinkReady: (link, householdName) =>
`Поделитесь этой ссылкой, чтобы пригласить участников в ${householdName}:\n\n${link}\n\nЛюбой, у кого есть эта ссылка, может подать заявку на вступление.`, `Поделитесь этой ссылкой, чтобы пригласить участников в ${householdName}:\n\n${link}\n\nЛюбой, у кого есть эта ссылка, может подать заявку на вступление.`
useBindInTopic: 'Используйте /bind внутри топика, чтобы привязать его к роли.',
topicAlreadyBound: (role) => `Этот топик уже привязан как ${role}.`,
bindSelectRole: 'Привязать этот топик как:',
topicBoundSuccess: (role, householdName) => `Привязан как ${role} для ${householdName}.`,
allRolesConfigured: 'Все роли топиков уже настроены.'
}, },
anonymousFeedback: { anonymousFeedback: {
title: 'Анонимное сообщение по дому', title: 'Анонимное сообщение по дому',
@@ -149,7 +131,7 @@ export const ruBotTranslations: BotTranslationCatalog = {
multipleHouseholds: multipleHouseholds:
'Вы состоите в нескольких домах. Откройте нужный дом из его группы, пока выбор дома ещё не добавлен.', 'Вы состоите в нескольких домах. Откройте нужный дом из его группы, пока выбор дома ещё не добавлен.',
feedbackTopicMissing: feedbackTopicMissing:
'Для вашего дома ещё не настроен анонимный топик. Попросите админа выполнить /bind_feedback_topic.', 'Для вашего дома ещё не настроен анонимный топик. Попросите админа выполнить /setup и создать топик для обратной связи.',
duplicate: 'Это анонимное сообщение уже было обработано.', duplicate: 'Это анонимное сообщение уже было обработано.',
delivered: 'Анонимное сообщение отправлено.', delivered: 'Анонимное сообщение отправлено.',
savedButPostFailed: savedButPostFailed:
@@ -329,7 +311,7 @@ export const ruBotTranslations: BotTranslationCatalog = {
}, },
payments: { payments: {
topicMissing: topicMissing:
'Для этого дома ещё не настроен топик оплат. Попросите админа выполнить /bind_payments_topic.', 'Для этого дома ещё не настроен топик оплат. Попросите админа выполнить /setup и создать топик для оплат.',
balanceReply: (kind) => balanceReply: (kind) =>
kind === 'rent' ? 'Текущая сводка по аренде:' : 'Текущая сводка по коммуналке:', kind === 'rent' ? 'Текущая сводка по аренде:' : 'Текущая сводка по коммуналке:',
proposal: (kind, amount, currency) => proposal: (kind, amount, currency) =>

View File

@@ -8,11 +8,6 @@ export type TelegramCommandName =
| 'setup' | 'setup'
| 'unsetup' | 'unsetup'
| 'bind' | 'bind'
| 'bind_chat_topic'
| 'bind_purchase_topic'
| 'bind_feedback_topic'
| 'bind_reminders_topic'
| 'bind_payments_topic'
| 'join_link' | 'join_link'
| 'payment_add' | 'payment_add'
| 'pending_members' | 'pending_members'
@@ -25,11 +20,6 @@ export interface BotCommandDescriptions {
cancel: string cancel: string
setup: string setup: string
unsetup: string unsetup: string
bind_chat_topic: string
bind_purchase_topic: string
bind_feedback_topic: string
bind_reminders_topic: string
bind_payments_topic: string
bind: string bind: string
join_link: string join_link: string
payment_add: string payment_add: string
@@ -100,16 +90,11 @@ export interface BotTranslationCatalog {
useUnsetupInGroup: string useUnsetupInGroup: string
unsetupComplete: (householdName: string) => string unsetupComplete: (householdName: string) => string
unsetupNoop: string unsetupNoop: string
useBindChatTopicInGroup: string useBindInTopic: string
chatTopicSaved: (householdName: string, threadId: string) => string topicAlreadyBound: (role: string) => string
useBindPurchaseTopicInGroup: string bindSelectRole: string
purchaseTopicSaved: (householdName: string, threadId: string) => string topicBoundSuccess: (role: string, householdName: string) => string
useBindFeedbackTopicInGroup: string allRolesConfigured: string
feedbackTopicSaved: (householdName: string, threadId: string) => string
useBindRemindersTopicInGroup: string
remindersTopicSaved: (householdName: string, threadId: string) => string
useBindPaymentsTopicInGroup: string
paymentsTopicSaved: (householdName: string, threadId: string) => string
usePendingMembersInGroup: string usePendingMembersInGroup: string
useApproveMemberInGroup: string useApproveMemberInGroup: string
approveMemberUsage: string approveMemberUsage: string
@@ -121,11 +106,6 @@ export interface BotTranslationCatalog {
useJoinLinkInGroup: string useJoinLinkInGroup: string
joinLinkUnavailable: string joinLinkUnavailable: string
joinLinkReady: (link: string, householdName: string) => string joinLinkReady: (link: string, householdName: string) => string
useBindInTopic: string
topicAlreadyBound: (role: string) => string
bindSelectRole: string
topicBoundSuccess: (role: string, householdName: string) => string
allRolesConfigured: string
} }
anonymousFeedback: { anonymousFeedback: {
title: string title: string

View File

@@ -36,11 +36,6 @@ const GROUP_ADMIN_COMMAND_NAMES = [
'setup', 'setup',
'unsetup', 'unsetup',
'bind', 'bind',
'bind_chat_topic',
'bind_purchase_topic',
'bind_feedback_topic',
'bind_reminders_topic',
'bind_payments_topic',
'join_link', 'join_link',
'pending_members', 'pending_members',
'approve_member' 'approve_member'