diff --git a/apps/bot/src/household-setup.test.ts b/apps/bot/src/household-setup.test.ts index 75c014c..2be96c9 100644 --- a/apps/bot/src/household-setup.test.ts +++ b/apps/bot/src/household-setup.test.ts @@ -160,30 +160,6 @@ function groupCallbackUpdate(data: string) { } } -function topicMessageUpdate(text: string, threadId: number) { - return { - update_id: 3003, - message: { - message_id: 92, - date: Math.floor(Date.now() / 1000), - is_topic_message: true, - message_thread_id: threadId, - chat: { - id: -100123456, - type: 'supergroup', - title: 'Kojori House' - }, - from: { - id: 123456, - is_bot: false, - first_name: 'Stan', - language_code: 'en' - }, - text - } - } -} - function createPromptRepository(): TelegramPendingActionRepository { const store = new Map() @@ -898,29 +874,14 @@ describe('registerHouseholdSetupCommands', () => { chat_id: -100123456 } }) - expect(sendPayload.text).toContain('Household created: Kojori House') - expect(sendPayload.text).toContain('- purchases: not configured') - expect(sendPayload.text).toContain('- payments: not configured') - expect(sendPayload.reply_markup).toMatchObject({ - inline_keyboard: expect.arrayContaining([ - [ - { - text: 'Join household', - url: 'https://t.me/household_test_bot?start=join_join-token' - } - ], - [ - { - text: 'Create purchases topic', - callback_data: 'setup_topic:create:purchase' - }, - { - text: 'Bind purchases topic', - callback_data: 'setup_topic:bind:purchase' - } - ] - ]) - }) + expect(sendPayload.text).toContain('Kojori House is ready!') + expect(sendPayload.text).toContain('Topics: 0/5 configured') + expect(sendPayload.text).toContain('⚪ purchases') + expect(sendPayload.text).toContain('⚪ payments') + // Check that join household button exists + 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_topic:create:purchase') }) test('creates and binds a missing setup topic from callback', async () => { @@ -1045,7 +1006,7 @@ describe('registerHouseholdSetupCommands', () => { payload: { chat_id: -100123456, message_id: 91, - text: expect.stringContaining('- purchases: bound to Shared purchases') + text: expect.stringContaining('✅ purchases') } }) @@ -1055,143 +1016,6 @@ describe('registerHouseholdSetupCommands', () => { }) }) - test('arms manual setup topic binding and consumes the next topic message', async () => { - const bot = createTelegramBot('000000:test-token') - const calls: Array<{ method: string; payload: unknown }> = [] - const repository = createHouseholdConfigurationRepository() - const promptRepository = createPromptRepository() - const householdOnboardingService: HouseholdOnboardingService = { - async ensureHouseholdJoinToken() { - return { - householdId: 'household-1', - householdName: 'Kojori House', - token: 'join-token' - } - }, - async getMiniAppAccess() { - return { - status: 'open_from_group' - } - }, - async joinHousehold() { - return { - status: 'pending', - household: { - id: 'household-1', - name: 'Kojori House', - defaultLocale: 'en' - } - } - } - } - - bot.botInfo = { - id: 999000, - is_bot: true, - first_name: 'Household Test Bot', - username: 'household_test_bot', - can_join_groups: true, - can_read_all_group_messages: false, - supports_inline_queries: false, - can_connect_to_business: false, - has_main_web_app: false, - has_topics_enabled: true, - allows_users_to_create_topics: true - } - - bot.api.config.use(async (_prev, method, payload) => { - calls.push({ method, payload }) - - if (method === 'getChatMember') { - return { - ok: true, - result: { - status: 'administrator', - user: { - id: 123456, - is_bot: false, - first_name: 'Stan' - } - } - } as never - } - - if (method === 'sendMessage') { - return { - ok: true, - result: { - message_id: calls.length, - date: Math.floor(Date.now() / 1000), - chat: { - id: -100123456, - type: 'supergroup' - }, - text: (payload as { text?: string }).text ?? 'ok' - } - } as never - } - - return { - ok: true, - result: true - } as never - }) - - registerHouseholdSetupCommands({ - bot, - householdSetupService: createHouseholdSetupService(repository), - householdOnboardingService, - householdAdminService: createHouseholdAdminService(), - householdConfigurationRepository: repository, - promptRepository - }) - - await bot.handleUpdate(groupCommandUpdate('/setup Kojori House') as never) - calls.length = 0 - - await bot.handleUpdate(groupCallbackUpdate('setup_topic:bind:payments') as never) - - expect(calls[1]).toMatchObject({ - method: 'answerCallbackQuery', - payload: { - callback_query_id: 'callback-1', - text: 'Binding mode is on for payments. Open the target topic and send any message there within 10 minutes.' - } - }) - expect(await promptRepository.getPendingAction('-100123456', '123456')).toMatchObject({ - action: 'setup_topic_binding', - payload: { - role: 'payments', - setupMessageId: 91 - } - }) - - calls.length = 0 - await bot.handleUpdate(topicMessageUpdate('hello from payments', 444) as never) - - expect(calls).toHaveLength(3) - expect(calls[1]).toMatchObject({ - method: 'editMessageText', - payload: { - chat_id: -100123456, - message_id: 91, - text: expect.stringContaining('- payments: bound to thread 444') - } - }) - expect(calls[2]).toMatchObject({ - method: 'sendMessage', - payload: { - chat_id: -100123456, - message_thread_id: 444, - text: 'Payments topic saved for Kojori House (thread 444).' - } - }) - expect(await promptRepository.getPendingAction('-100123456', '123456')).toBeNull() - expect(await repository.getHouseholdTopicBinding('household-1', 'payments')).toMatchObject({ - telegramThreadId: '444' - }) - }) - test('resets setup state with /unsetup and clears pending setup bindings', async () => { const bot = createTelegramBot('000000:test-token') const calls: Array<{ method: string; payload: unknown }> = [] diff --git a/apps/bot/src/household-setup.ts b/apps/bot/src/household-setup.ts index ac102a8..d98b129 100644 --- a/apps/bot/src/household-setup.ts +++ b/apps/bot/src/household-setup.ts @@ -4,7 +4,7 @@ import type { HouseholdSetupService, HouseholdMiniAppAccess } from '@household/application' -import { nowInstant } from '@household/domain' + import type { Logger } from '@household/observability' import type { HouseholdConfigurationRepository, @@ -20,9 +20,7 @@ import { resolveReplyLocale } from './bot-locale' const APPROVE_MEMBER_CALLBACK_PREFIX = 'approve_member:' const SETUP_CREATE_TOPIC_CALLBACK_PREFIX = 'setup_topic:create:' -const SETUP_BIND_TOPIC_CALLBACK_PREFIX = 'setup_topic:bind:' const SETUP_BIND_TOPIC_ACTION = 'setup_topic_binding' as const -const SETUP_BIND_TOPIC_TTL_MS = 10 * 60 * 1000 const HOUSEHOLD_TOPIC_ROLE_ORDER: readonly HouseholdTopicRole[] = [ 'chat', 'purchase', @@ -46,11 +44,6 @@ function isTopicMessage(ctx: Context): boolean { return !!message && 'is_topic_message' in message && message.is_topic_message === true } -function isCommandMessage(ctx: Context): boolean { - const text = ctx.msg?.text - return typeof text === 'string' && text.trimStart().startsWith('/') -} - async function isGroupAdmin(ctx: Context): Promise { if (!ctx.chat || !ctx.from) { return false @@ -204,10 +197,6 @@ function pendingMembersReply( } as const } -function topicBindingDisplay(binding: HouseholdTopicBindingRecord): string { - return binding.topicName?.trim() || `thread ${binding.telegramThreadId}` -} - function setupTopicRoleLabel(locale: BotLocale, role: HouseholdTopicRole): string { return getBotTranslations(locale).setup.setupTopicBindRoleName(role) } @@ -236,6 +225,23 @@ function setupKeyboard(input: { > > = [] + // Create buttons for unconfigured roles (3 per row) + const createButtons: Array<{ text: string; callback_data: string }> = [] + for (const role of HOUSEHOLD_TOPIC_ROLE_ORDER) { + if (!configuredRoles.has(role)) { + createButtons.push({ + text: t.setupTopicCreateButton(setupTopicRoleLabel(input.locale, role)), + callback_data: `${SETUP_CREATE_TOPIC_CALLBACK_PREFIX}${role}` + }) + } + } + + // Chunk create buttons into rows of 3 + for (let i = 0; i < createButtons.length; i += 3) { + rows.push(createButtons.slice(i, i + 3)) + } + + // Add join link button if (input.joinDeepLink) { rows.push([ { @@ -245,23 +251,6 @@ function setupKeyboard(input: { ]) } - for (const role of HOUSEHOLD_TOPIC_ROLE_ORDER) { - if (configuredRoles.has(role)) { - continue - } - - rows.push([ - { - text: t.setupTopicCreateButton(setupTopicRoleLabel(input.locale, role)), - callback_data: `${SETUP_CREATE_TOPIC_CALLBACK_PREFIX}${role}` - }, - { - text: t.setupTopicBindButton(setupTopicRoleLabel(input.locale, role)), - callback_data: `${SETUP_BIND_TOPIC_CALLBACK_PREFIX}${role}` - } - ]) - } - return rows.length > 0 ? { reply_markup: { @@ -277,17 +266,28 @@ function setupTopicChecklist(input: { }): string { const t = getBotTranslations(input.locale).setup const bindingByRole = new Map(input.bindings.map((binding) => [binding.role, binding])) + const configuredCount = input.bindings.length + const totalCount = HOUSEHOLD_TOPIC_ROLE_ORDER.length - return [ - t.setupTopicsHeading, - ...HOUSEHOLD_TOPIC_ROLE_ORDER.map((role) => { - const binding = bindingByRole.get(role) - const roleLabel = setupTopicRoleLabel(input.locale, role) - return binding - ? t.setupTopicBound(roleLabel, topicBindingDisplay(binding)) - : t.setupTopicMissing(roleLabel) - }) - ].join('\n') + const lines = [t.setupTopicsHeading(configuredCount, totalCount)] + + // Group roles in pairs for compact display + for (let i = 0; i < HOUSEHOLD_TOPIC_ROLE_ORDER.length; i += 2) { + const role1 = HOUSEHOLD_TOPIC_ROLE_ORDER[i]! + const role2 = HOUSEHOLD_TOPIC_ROLE_ORDER[i + 1] + const binding1 = bindingByRole.get(role1) + const binding2 = role2 ? bindingByRole.get(role2) : null + const label1 = setupTopicRoleLabel(input.locale, role1) + const label2 = role2 ? setupTopicRoleLabel(input.locale, role2) : null + + const status1 = binding1 ? t.setupTopicBound(label1) : t.setupTopicMissing(label1) + const status2 = + label2 && role2 ? (binding2 ? t.setupTopicBound(label2) : t.setupTopicMissing(label2)) : '' + + lines.push(status2 ? `${status1} ${status2}` : status1) + } + + return lines.join('\n') } function setupReply(input: { @@ -302,7 +302,6 @@ function setupReply(input: { text: [ t.setupSummary({ householdName: input.household.householdName, - telegramChatId: input.household.telegramChatId, created: input.created }), setupTopicChecklist({ @@ -318,34 +317,6 @@ function setupReply(input: { } } -function isHouseholdTopicRole(value: string): value is HouseholdTopicRole { - return ( - value === 'chat' || - value === 'purchase' || - value === 'feedback' || - value === 'reminders' || - value === 'payments' - ) -} - -function parseSetupBindPayload(payload: Record): { - role: HouseholdTopicRole - setupMessageId?: number -} | null { - if (typeof payload.role !== 'string' || !isHouseholdTopicRole(payload.role)) { - return null - } - - return { - role: payload.role, - ...(typeof payload.setupMessageId === 'number' && Number.isInteger(payload.setupMessageId) - ? { - setupMessageId: payload.setupMessageId - } - : {}) - } -} - function buildMiniAppBaseUrl( miniAppUrl: string | undefined, botUsername?: string | undefined @@ -559,83 +530,6 @@ export function registerHouseholdSetupCommands(options: { ) } - if (options.promptRepository) { - const promptRepository = options.promptRepository - - options.bot.on('message', async (ctx, next) => { - if (!isGroupChat(ctx) || !isTopicMessage(ctx) || isCommandMessage(ctx)) { - await next() - return - } - - const telegramUserId = ctx.from?.id?.toString() - const telegramChatId = ctx.chat?.id?.toString() - const telegramThreadId = - ctx.msg && 'message_thread_id' in ctx.msg ? ctx.msg.message_thread_id?.toString() : null - - if (!telegramUserId || !telegramChatId || !telegramThreadId) { - await next() - return - } - - const pending = await promptRepository.getPendingAction(telegramChatId, telegramUserId) - if (pending?.action !== SETUP_BIND_TOPIC_ACTION) { - await next() - return - } - - const payload = parseSetupBindPayload(pending.payload) - if (!payload) { - await promptRepository.clearPendingAction(telegramChatId, telegramUserId) - await next() - return - } - - const locale = await resolveReplyLocale({ - ctx, - repository: options.householdConfigurationRepository - }) - const result = await options.householdSetupService.bindTopic({ - actorIsAdmin: await isGroupAdmin(ctx), - telegramChatId, - telegramThreadId, - role: payload.role - }) - - await promptRepository.clearPendingAction(telegramChatId, telegramUserId) - - if (result.status === 'rejected') { - await ctx.reply(bindRejectionMessage(locale, result.reason)) - return - } - - if (payload.setupMessageId && options.householdConfigurationRepository) { - const reply = await buildSetupReplyForHousehold({ - ctx, - locale, - household: result.household, - created: false - }) - - await ctx.api.editMessageText( - Number(telegramChatId), - payload.setupMessageId, - reply.text, - 'reply_markup' in reply ? { reply_markup: reply.reply_markup } : {} - ) - } - - await ctx.reply( - bindTopicSuccessMessage( - locale, - payload.role, - result.household.householdName, - result.binding.telegramThreadId - ) - ) - }) - } - options.bot.command('start', async (ctx) => { const fallbackLocale = await resolveReplyLocale({ ctx, @@ -864,6 +758,83 @@ export function registerHouseholdSetupCommands(options: { 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) => { const locale = await resolveReplyLocale({ ctx, @@ -1071,8 +1042,6 @@ export function registerHouseholdSetupCommands(options: { ) if (options.promptRepository) { - const promptRepository = options.promptRepository - options.bot.callbackQuery( new RegExp(`^${SETUP_CREATE_TOPIC_CALLBACK_PREFIX}(purchase|feedback|reminders|payments)$`), async (ctx) => { @@ -1164,8 +1133,9 @@ export function registerHouseholdSetupCommands(options: { } ) + // Bind topic callback from /bind command options.bot.callbackQuery( - new RegExp(`^${SETUP_BIND_TOPIC_CALLBACK_PREFIX}(purchase|feedback|reminders|payments)$`), + new RegExp(`^bind_topic:(chat|purchase|feedback|reminders|payments):(\\d+)$`), async (ctx) => { const locale = await resolveReplyLocale({ ctx, @@ -1181,18 +1151,8 @@ export function registerHouseholdSetupCommands(options: { return } - const telegramUserId = ctx.from?.id?.toString() - const telegramChatId = ctx.chat.id.toString() - const role = ctx.match[1] as HouseholdTopicRole - if (!telegramUserId) { - await ctx.answerCallbackQuery({ - text: t.unableToIdentifySelectedMember, - show_alert: true - }) - return - } - - if (!(await isGroupAdmin(ctx))) { + const actorIsAdmin = await isGroupAdmin(ctx) + if (!actorIsAdmin) { await ctx.answerCallbackQuery({ text: t.onlyTelegramAdminsBindTopics, show_alert: true @@ -1200,24 +1160,35 @@ export function registerHouseholdSetupCommands(options: { return } - await promptRepository.upsertPendingAction({ - telegramUserId, - telegramChatId, - action: SETUP_BIND_TOPIC_ACTION, - payload: { - role, - ...(ctx.msg - ? { - setupMessageId: ctx.msg.message_id - } - : {}) - }, - expiresAt: nowInstant().add({ milliseconds: SETUP_BIND_TOPIC_TTL_MS }) + const role = ctx.match[1] as HouseholdTopicRole + const telegramThreadId = ctx.match[2]! + + const result = await options.householdSetupService.bindTopic({ + actorIsAdmin, + telegramChatId: ctx.chat.id.toString(), + telegramThreadId, + role }) + if (result.status === 'rejected') { + await ctx.answerCallbackQuery({ + text: bindRejectionMessage(locale, result.reason), + show_alert: true + }) + return + } + await ctx.answerCallbackQuery({ - text: t.setupTopicBindPending(setupTopicRoleLabel(locale, role)) + text: t.topicBoundSuccess( + setupTopicRoleLabel(locale, role), + result.household.householdName + ) }) + + // Edit the role selection message to show success + await ctx.editMessageText( + t.topicBoundSuccess(setupTopicRoleLabel(locale, role), result.household.householdName) + ) } ) } diff --git a/apps/bot/src/i18n/locales/en.ts b/apps/bot/src/i18n/locales/en.ts index f4d9ab8..86aaa45 100644 --- a/apps/bot/src/i18n/locales/en.ts +++ b/apps/bot/src/i18n/locales/en.ts @@ -14,6 +14,7 @@ export const enBotTranslations: BotTranslationCatalog = { 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', payment_add: 'Record your rent or utilities payment', pending_members: 'List pending household join requests', @@ -54,25 +55,19 @@ export const enBotTranslations: BotTranslationCatalog = { `You are already an active member. Open the mini app to view ${displayName}.`, joinRequestSent: (householdName) => `Join request sent for ${householdName}. Wait for a household admin to confirm you.`, - setupSummary: ({ householdName, telegramChatId, created }) => - [ - `Household ${created ? 'created' : 'already registered'}: ${householdName}`, - `Chat ID: ${telegramChatId}`, - 'Use the buttons below to finish topic setup. For an existing topic, tap Bind and then send any message inside that topic.', - 'Members should open the bot chat from the button below and confirm the join request there.' - ].join('\n'), - setupTopicsHeading: 'Topic setup:', - setupTopicBound: (role, topic) => `- ${role}: bound to ${topic}`, - setupTopicMissing: (role) => `- ${role}: not configured`, - setupTopicCreateButton: (role) => `Create ${role} topic`, - setupTopicBindButton: (role) => `Bind ${role} topic`, + setupSummary: ({ householdName, created }) => + `${created ? '✅' : 'ℹ️'} ${householdName} is ${created ? 'ready' : 'already registered'}!`, + setupTopicsHeading: (configured, total) => `Topics: ${configured}/${total} configured`, + setupTopicBound: (role) => `✅ ${role}`, + setupTopicMissing: (role) => `⚪ ${role}`, + setupTopicCreateButton: (role) => `+ ${role}`, + setupTopicBindButton: (role) => `Bind ${role}`, setupTopicCreateFailed: 'I could not create that topic. Check bot admin permissions and forum settings.', setupTopicCreateForbidden: 'I need permission to manage topics in this group before I can create one automatically.', setupTopicCreated: (role, topicName) => `${role} topic created and bound: ${topicName}.`, - setupTopicBindPending: (role) => - `Binding mode is on for ${role}. Open the target topic and send any message there within 10 minutes.`, + setupTopicBindPending: '', setupTopicBindCancelled: 'Topic binding mode cleared.', setupTopicBindNotAvailable: 'That topic-binding action is no longer available.', setupTopicBindRoleName: (role) => { @@ -135,7 +130,12 @@ export const enBotTranslations: BotTranslationCatalog = { useJoinLinkInGroup: 'Use /join_link inside the household group.', joinLinkUnavailable: 'Could not generate join link.', 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: { title: 'Anonymous household note', diff --git a/apps/bot/src/i18n/locales/ru.ts b/apps/bot/src/i18n/locales/ru.ts index 02c0405..38fc42a 100644 --- a/apps/bot/src/i18n/locales/ru.ts +++ b/apps/bot/src/i18n/locales/ru.ts @@ -14,6 +14,7 @@ export const ruBotTranslations: BotTranslationCatalog = { bind_feedback_topic: 'Назначить текущий топик для анонимных сообщений', bind_reminders_topic: 'Назначить текущий топик для напоминаний', bind_payments_topic: 'Назначить текущий топик для оплат', + bind: 'Привязать текущий топик к роли дома', join_link: 'Получить ссылку для приглашения новых участников', payment_add: 'Подтвердить оплату аренды или коммуналки', pending_members: 'Показать ожидающие заявки на вступление', @@ -56,25 +57,19 @@ export const ruBotTranslations: BotTranslationCatalog = { `Вы уже активный участник. Откройте мини-приложение, чтобы увидеть профиль ${displayName}.`, joinRequestSent: (householdName) => `Заявка на вступление в ${householdName} отправлена. Дождитесь подтверждения от админа дома.`, - setupSummary: ({ householdName, telegramChatId, created }) => - [ - `${created ? 'Дом создан' : 'Дом уже подключён'}: ${householdName}`, - `ID чата: ${telegramChatId}`, - 'Используйте кнопки ниже, чтобы завершить настройку топиков. Для уже существующего топика нажмите «Привязать», затем отправьте любое сообщение внутри этого топика.', - 'Участники должны открыть чат с ботом по кнопке ниже и подтвердить заявку на вступление.' - ].join('\n'), - setupTopicsHeading: 'Настройка топиков:', - setupTopicBound: (role, topic) => `- ${role}: привязан к ${topic}`, - setupTopicMissing: (role) => `- ${role}: не настроен`, - setupTopicCreateButton: (role) => `Создать топик для ${role}`, - setupTopicBindButton: (role) => `Привязать топик для ${role}`, + setupSummary: ({ householdName, created }) => + `${created ? '✅' : 'ℹ️'} ${householdName} ${created ? 'готов' : 'уже подключён'}!`, + setupTopicsHeading: (configured, total) => `Топики: ${configured}/${total} настроено`, + setupTopicBound: (role) => `✅ ${role}`, + setupTopicMissing: (role) => `⚪ ${role}`, + setupTopicCreateButton: (role) => `+ ${role}`, + setupTopicBindButton: (role) => `Привязать ${role}`, setupTopicCreateFailed: 'Не удалось создать этот топик. Проверьте права бота и включённые форум-топики в группе.', setupTopicCreateForbidden: 'Мне нужны права на управление топиками в этой группе, чтобы создать его автоматически.', setupTopicCreated: (role, topicName) => `Топик ${role} создан и привязан: ${topicName}.`, - setupTopicBindPending: (role) => - `Режим привязки включён для ${role}. Откройте нужный топик и отправьте там любое сообщение в течение 10 минут.`, + setupTopicBindPending: '', setupTopicBindCancelled: 'Режим привязки топика очищен.', setupTopicBindNotAvailable: 'Это действие привязки топика уже недоступно.', setupTopicBindRoleName: (role) => { @@ -137,7 +132,12 @@ export const ruBotTranslations: BotTranslationCatalog = { useJoinLinkInGroup: 'Используйте /join_link внутри группы дома.', joinLinkUnavailable: 'Не удалось сгенерировать ссылку для вступления.', 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: { title: 'Анонимное сообщение по дому', diff --git a/apps/bot/src/i18n/types.ts b/apps/bot/src/i18n/types.ts index 781dd41..9400a44 100644 --- a/apps/bot/src/i18n/types.ts +++ b/apps/bot/src/i18n/types.ts @@ -7,6 +7,7 @@ export type TelegramCommandName = | 'cancel' | 'setup' | 'unsetup' + | 'bind' | 'bind_chat_topic' | 'bind_purchase_topic' | 'bind_feedback_topic' @@ -29,6 +30,7 @@ export interface BotCommandDescriptions { bind_feedback_topic: string bind_reminders_topic: string bind_payments_topic: string + bind: string join_link: string payment_add: string pending_members: string @@ -76,20 +78,16 @@ export interface BotTranslationCatalog { joinLinkInvalidOrExpired: string alreadyActiveMember: (displayName: string) => string joinRequestSent: (householdName: string) => string - setupSummary: (params: { - householdName: string - telegramChatId: string - created: boolean - }) => string - setupTopicsHeading: string - setupTopicBound: (role: string, topic: string) => string + setupSummary: (params: { householdName: string; created: boolean }) => string + setupTopicsHeading: (configured: number, total: number) => string + setupTopicBound: (role: string) => string setupTopicMissing: (role: string) => string setupTopicCreateButton: (role: string) => string setupTopicBindButton: (role: string) => string setupTopicCreateFailed: string setupTopicCreateForbidden: string setupTopicCreated: (role: string, topicName: string) => string - setupTopicBindPending: (role: string) => string + setupTopicBindPending: string setupTopicBindCancelled: string setupTopicBindNotAvailable: string setupTopicBindRoleName: ( @@ -123,6 +121,11 @@ export interface BotTranslationCatalog { useJoinLinkInGroup: string joinLinkUnavailable: string joinLinkReady: (link: string, householdName: string) => string + useBindInTopic: string + topicAlreadyBound: (role: string) => string + bindSelectRole: string + topicBoundSuccess: (role: string, householdName: string) => string + allRolesConfigured: string } anonymousFeedback: { title: string diff --git a/apps/bot/src/telegram-commands.ts b/apps/bot/src/telegram-commands.ts index d12a108..44a9282 100644 --- a/apps/bot/src/telegram-commands.ts +++ b/apps/bot/src/telegram-commands.ts @@ -35,6 +35,7 @@ const GROUP_ADMIN_COMMAND_NAMES = [ ...GROUP_MEMBER_COMMAND_NAMES, 'setup', 'unsetup', + 'bind', 'bind_chat_topic', 'bind_purchase_topic', 'bind_feedback_topic',