feat(bot): refactor /setup and add /bind command

- Simplify /setup message: remove chat ID, use emojis, compact layout
- Remove 'Bind' buttons from /setup
- Add /bind command for binding existing topics
- Remove old binding mode with 10-min timeout
- Update i18n translations for en and ru
This commit is contained in:
2026-03-15 00:14:40 +04:00
parent e24c53dce2
commit b2e1e0f213
6 changed files with 196 additions and 397 deletions

View File

@@ -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<string, TelegramPendingActionRecord>()
@@ -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 }> = []

View File

@@ -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<boolean> {
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<string, unknown>): {
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: t.setupTopicBindPending(setupTopicRoleLabel(locale, role))
text: bindRejectionMessage(locale, result.reason),
show_alert: true
})
return
}
await ctx.answerCallbackQuery({
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)
)
}
)
}

View File

@@ -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',

View File

@@ -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: 'Анонимное сообщение по дому',

View File

@@ -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

View File

@@ -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',