mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 14:04:04 +00:00
feat(bot): replace /invite with /join_link command
- Remove /invite command and targeted invite flow - Add /join_link command to generate shareable household join link - Update i18n translations for en and ru - Update command registration to include join_link in admin commands
This commit is contained in:
@@ -133,52 +133,6 @@ function groupCommandUpdate(text: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupReplyCommandUpdate(text: string, repliedUser: { id: number; firstName: string }) {
|
|
||||||
const commandToken = text.split(' ')[0] ?? text
|
|
||||||
|
|
||||||
return {
|
|
||||||
update_id: 3004,
|
|
||||||
message: {
|
|
||||||
message_id: 82,
|
|
||||||
date: Math.floor(Date.now() / 1000),
|
|
||||||
chat: {
|
|
||||||
id: -100123456,
|
|
||||||
type: 'supergroup',
|
|
||||||
title: 'Kojori House'
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
id: 123456,
|
|
||||||
is_bot: false,
|
|
||||||
first_name: 'Stan',
|
|
||||||
language_code: 'en'
|
|
||||||
},
|
|
||||||
reply_to_message: {
|
|
||||||
message_id: 80,
|
|
||||||
date: Math.floor(Date.now() / 1000),
|
|
||||||
chat: {
|
|
||||||
id: -100123456,
|
|
||||||
type: 'supergroup',
|
|
||||||
title: 'Kojori House'
|
|
||||||
},
|
|
||||||
from: {
|
|
||||||
id: repliedUser.id,
|
|
||||||
is_bot: false,
|
|
||||||
first_name: repliedUser.firstName
|
|
||||||
},
|
|
||||||
text: 'hello'
|
|
||||||
},
|
|
||||||
text,
|
|
||||||
entities: [
|
|
||||||
{
|
|
||||||
offset: 0,
|
|
||||||
length: commandToken.length,
|
|
||||||
type: 'bot_command'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupCallbackUpdate(data: string) {
|
function groupCallbackUpdate(data: string) {
|
||||||
return {
|
return {
|
||||||
update_id: 3002,
|
update_id: 3002,
|
||||||
@@ -969,394 +923,6 @@ describe('registerHouseholdSetupCommands', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('creates a targeted in-group invite from a replied user message', async () => {
|
|
||||||
const bot = createTelegramBot('000000:test-token')
|
|
||||||
const calls: Array<{ method: string; payload: unknown }> = []
|
|
||||||
const repository = createHouseholdConfigurationRepository()
|
|
||||||
const promptRepository = createPromptRepository()
|
|
||||||
|
|
||||||
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: 410,
|
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
groupReplyCommandUpdate('/invite', { id: 654321, firstName: 'Chorbanaut' }) as never
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(calls[1]).toMatchObject({
|
|
||||||
method: 'sendMessage',
|
|
||||||
payload: {
|
|
||||||
chat_id: -100123456,
|
|
||||||
text: 'Invitation prepared for Chorbanaut. Tap below to join Kojori House.',
|
|
||||||
reply_markup: {
|
|
||||||
inline_keyboard: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: 'Join household',
|
|
||||||
url: 'https://t.me/household_test_bot?start=invite_-100123456_654321'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(await promptRepository.getPendingAction('invite:-100123456', '654321')).toMatchObject({
|
|
||||||
action: 'household_group_invite',
|
|
||||||
payload: {
|
|
||||||
joinToken: 'join-token',
|
|
||||||
householdId: 'household-1',
|
|
||||||
householdName: 'Kojori House',
|
|
||||||
targetDisplayName: 'Chorbanaut',
|
|
||||||
inviteMessageId: 410
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('rejects household invite links for the wrong Telegram user', async () => {
|
|
||||||
const bot = createTelegramBot('000000:test-token')
|
|
||||||
const calls: Array<{ method: string; payload: unknown }> = []
|
|
||||||
|
|
||||||
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 })
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
result: {
|
|
||||||
message_id: 1,
|
|
||||||
date: Math.floor(Date.now() / 1000),
|
|
||||||
chat: {
|
|
||||||
id: 111111,
|
|
||||||
type: 'private'
|
|
||||||
},
|
|
||||||
text: (payload as { text?: string }).text ?? 'ok'
|
|
||||||
}
|
|
||||||
} as never
|
|
||||||
})
|
|
||||||
|
|
||||||
registerHouseholdSetupCommands({
|
|
||||||
bot,
|
|
||||||
householdSetupService: createRejectedHouseholdSetupService(),
|
|
||||||
householdOnboardingService: {
|
|
||||||
async ensureHouseholdJoinToken() {
|
|
||||||
return {
|
|
||||||
householdId: 'household-1',
|
|
||||||
householdName: 'Kojori House',
|
|
||||||
token: 'join-token'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getMiniAppAccess() {
|
|
||||||
return {
|
|
||||||
status: 'open_from_group'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async joinHousehold() {
|
|
||||||
return {
|
|
||||||
status: 'invalid_token'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
householdAdminService: createHouseholdAdminService(),
|
|
||||||
promptRepository: createPromptRepository()
|
|
||||||
})
|
|
||||||
|
|
||||||
await bot.handleUpdate(
|
|
||||||
startUpdate('/start invite_-100123456_654321', {
|
|
||||||
userId: 111111,
|
|
||||||
firstName: 'Wrong user'
|
|
||||||
}) as never
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(calls[0]).toMatchObject({
|
|
||||||
method: 'sendMessage',
|
|
||||||
payload: {
|
|
||||||
chat_id: 111111,
|
|
||||||
text: 'This invite is for a different Telegram user.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('consumes a targeted invite for the invited user and updates the group message', async () => {
|
|
||||||
const bot = createTelegramBot('000000:test-token')
|
|
||||||
const calls: Array<{ method: string; payload: unknown }> = []
|
|
||||||
const repository = createHouseholdConfigurationRepository()
|
|
||||||
const promptRepository = createPromptRepository()
|
|
||||||
const joinCalls: string[] = []
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const householdOnboardingService: HouseholdOnboardingService = {
|
|
||||||
async ensureHouseholdJoinToken() {
|
|
||||||
return {
|
|
||||||
householdId: 'household-1',
|
|
||||||
householdName: 'Kojori House',
|
|
||||||
token: 'join-token'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async getMiniAppAccess(input) {
|
|
||||||
if (joinCalls.includes(input.identity.telegramUserId)) {
|
|
||||||
return {
|
|
||||||
status: 'pending',
|
|
||||||
household: {
|
|
||||||
id: 'household-1',
|
|
||||||
name: 'Kojori House',
|
|
||||||
defaultLocale: 'en'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'open_from_group'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async joinHousehold(input) {
|
|
||||||
joinCalls.push(input.identity.telegramUserId)
|
|
||||||
return {
|
|
||||||
status: 'pending',
|
|
||||||
household: {
|
|
||||||
id: 'household-1',
|
|
||||||
name: 'Kojori House',
|
|
||||||
defaultLocale: 'en'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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') {
|
|
||||||
const chatId = (payload as { chat_id?: number }).chat_id ?? 0
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
result: {
|
|
||||||
message_id: chatId === -100123456 ? 411 : 1,
|
|
||||||
date: Math.floor(Date.now() / 1000),
|
|
||||||
chat: {
|
|
||||||
id: chatId,
|
|
||||||
type: chatId > 0 ? 'private' : 'supergroup'
|
|
||||||
},
|
|
||||||
text: (payload as { text?: string }).text ?? 'ok'
|
|
||||||
}
|
|
||||||
} as never
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method === 'editMessageText') {
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
result: {
|
|
||||||
message_id: (payload as { message_id?: number }).message_id ?? 411,
|
|
||||||
date: Math.floor(Date.now() / 1000),
|
|
||||||
chat: {
|
|
||||||
id: (payload as { chat_id?: number }).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(
|
|
||||||
groupReplyCommandUpdate('/invite', { id: 654321, firstName: 'Chorbanaut' }) as never
|
|
||||||
)
|
|
||||||
|
|
||||||
calls.length = 0
|
|
||||||
await bot.handleUpdate(
|
|
||||||
startUpdate('/start invite_-100123456_654321', {
|
|
||||||
userId: 654321,
|
|
||||||
firstName: 'Chorbanaut'
|
|
||||||
}) as never
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(calls[0]).toMatchObject({
|
|
||||||
method: 'editMessageText',
|
|
||||||
payload: {
|
|
||||||
chat_id: -100123456,
|
|
||||||
message_id: 411,
|
|
||||||
text: 'Chorbanaut sent a join request for Kojori House.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(calls[1]).toMatchObject({
|
|
||||||
method: 'sendMessage',
|
|
||||||
payload: {
|
|
||||||
chat_id: 654321,
|
|
||||||
text: 'Join request sent for Kojori House. Wait for a household admin to confirm you.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(await promptRepository.getPendingAction('invite:-100123456', '654321')).toMatchObject({
|
|
||||||
action: 'household_group_invite',
|
|
||||||
payload: {
|
|
||||||
completed: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
calls.length = 0
|
|
||||||
await bot.handleUpdate(
|
|
||||||
startUpdate('/start invite_-100123456_654321', {
|
|
||||||
userId: 654321,
|
|
||||||
firstName: 'Chorbanaut'
|
|
||||||
}) as never
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(calls[0]).toMatchObject({
|
|
||||||
method: 'editMessageText',
|
|
||||||
payload: {
|
|
||||||
chat_id: -100123456,
|
|
||||||
message_id: 411,
|
|
||||||
text: 'Chorbanaut sent a join request for Kojori House.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
expect(calls[1]).toMatchObject({
|
|
||||||
method: 'sendMessage',
|
|
||||||
payload: {
|
|
||||||
chat_id: 654321,
|
|
||||||
text: 'Join request sent for Kojori House. Wait for a household admin to confirm you.'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('creates and binds a missing setup topic from callback', async () => {
|
test('creates and binds a missing setup topic from callback', async () => {
|
||||||
const bot = createTelegramBot('000000:test-token')
|
const bot = createTelegramBot('000000:test-token')
|
||||||
const calls: Array<{ method: string; payload: unknown }> = []
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
@@ -1853,4 +1419,119 @@ describe('registerHouseholdSetupCommands', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('generates a join link with /join_link command', async () => {
|
||||||
|
const bot = createTelegramBot('000000:test-token')
|
||||||
|
const calls: Array<{ method: string; payload: unknown }> = []
|
||||||
|
const repository = createHouseholdConfigurationRepository()
|
||||||
|
|
||||||
|
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: 500,
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
const householdOnboardingService: HouseholdOnboardingService = {
|
||||||
|
async ensureHouseholdJoinToken() {
|
||||||
|
return {
|
||||||
|
householdId: 'household-1',
|
||||||
|
householdName: 'Kojori House',
|
||||||
|
token: 'test-join-token'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async getMiniAppAccess() {
|
||||||
|
return {
|
||||||
|
status: 'open_from_group'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async joinHousehold() {
|
||||||
|
return {
|
||||||
|
status: 'pending',
|
||||||
|
household: {
|
||||||
|
id: 'household-1',
|
||||||
|
name: 'Kojori House',
|
||||||
|
defaultLocale: 'en'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerHouseholdSetupCommands({
|
||||||
|
bot,
|
||||||
|
householdSetupService: createHouseholdSetupService(repository),
|
||||||
|
householdOnboardingService,
|
||||||
|
householdAdminService: createHouseholdAdminService(),
|
||||||
|
householdConfigurationRepository: repository,
|
||||||
|
promptRepository: createPromptRepository()
|
||||||
|
})
|
||||||
|
|
||||||
|
await bot.handleUpdate(groupCommandUpdate('/setup Kojori House') as never)
|
||||||
|
calls.length = 0
|
||||||
|
|
||||||
|
await bot.handleUpdate(groupCommandUpdate('/join_link') as never)
|
||||||
|
|
||||||
|
expect(calls[1]).toMatchObject({
|
||||||
|
method: 'sendMessage',
|
||||||
|
payload: {
|
||||||
|
chat_id: -100123456,
|
||||||
|
text: 'Join link for Kojori House:\nhttps://t.me/household_test_bot?start=join_test-join-token\n\nAnyone with this link can join the household. Share it carefully.',
|
||||||
|
reply_markup: {
|
||||||
|
inline_keyboard: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Join household',
|
||||||
|
url: 'https://t.me/household_test_bot?start=join_test-join-token'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,14 +17,10 @@ import type { Bot, Context } from 'grammy'
|
|||||||
|
|
||||||
import { getBotTranslations, type BotLocale } from './i18n'
|
import { getBotTranslations, type BotLocale } from './i18n'
|
||||||
import { resolveReplyLocale } from './bot-locale'
|
import { resolveReplyLocale } from './bot-locale'
|
||||||
import { buildBotStartDeepLink } from './telegram-deep-links'
|
|
||||||
|
|
||||||
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_CALLBACK_PREFIX = 'setup_topic:bind:'
|
const SETUP_BIND_TOPIC_CALLBACK_PREFIX = 'setup_topic:bind:'
|
||||||
const GROUP_INVITE_START_PREFIX = 'invite_'
|
|
||||||
const GROUP_INVITE_ACTION = 'household_group_invite' as const
|
|
||||||
const GROUP_INVITE_TTL_MS = 3 * 24 * 60 * 60 * 1000
|
|
||||||
const SETUP_BIND_TOPIC_ACTION = 'setup_topic_binding' as const
|
const SETUP_BIND_TOPIC_ACTION = 'setup_topic_binding' as const
|
||||||
const SETUP_BIND_TOPIC_TTL_MS = 10 * 60 * 1000
|
const SETUP_BIND_TOPIC_TTL_MS = 10 * 60 * 1000
|
||||||
const HOUSEHOLD_TOPIC_ROLE_ORDER: readonly HouseholdTopicRole[] = [
|
const HOUSEHOLD_TOPIC_ROLE_ORDER: readonly HouseholdTopicRole[] = [
|
||||||
@@ -170,45 +166,6 @@ function actorDisplayName(ctx: Context): string | undefined {
|
|||||||
return fullName || ctx.from?.username?.trim() || undefined
|
return fullName || ctx.from?.username?.trim() || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function telegramUserDisplayName(input: {
|
|
||||||
firstName: string | undefined
|
|
||||||
lastName: string | undefined
|
|
||||||
username: string | undefined
|
|
||||||
fallback: string
|
|
||||||
}): string {
|
|
||||||
const fullName = [input.firstName?.trim(), input.lastName?.trim()]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' ')
|
|
||||||
.trim()
|
|
||||||
return fullName || input.username?.trim() || input.fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function repliedTelegramUser(ctx: Context): {
|
|
||||||
telegramUserId: string
|
|
||||||
displayName: string
|
|
||||||
username?: string
|
|
||||||
} | null {
|
|
||||||
const replied = ctx.msg?.reply_to_message
|
|
||||||
if (!replied?.from || replied.from.is_bot) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
telegramUserId: replied.from.id.toString(),
|
|
||||||
displayName: telegramUserDisplayName({
|
|
||||||
firstName: replied.from.first_name,
|
|
||||||
lastName: replied.from.last_name,
|
|
||||||
username: replied.from.username,
|
|
||||||
fallback: `Telegram ${replied.from.id}`
|
|
||||||
}),
|
|
||||||
...(replied.from.username
|
|
||||||
? {
|
|
||||||
username: replied.from.username
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPendingMemberLabel(displayName: string): string {
|
function buildPendingMemberLabel(displayName: string): string {
|
||||||
const normalized = displayName.trim().replaceAll(/\s+/g, ' ')
|
const normalized = displayName.trim().replaceAll(/\s+/g, ' ')
|
||||||
if (normalized.length <= 32) {
|
if (normalized.length <= 32) {
|
||||||
@@ -389,75 +346,6 @@ function parseSetupBindPayload(payload: Record<string, unknown>): {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function invitePendingChatId(telegramChatId: string): string {
|
|
||||||
return `invite:${telegramChatId}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseGroupInvitePayload(payload: Record<string, unknown>): {
|
|
||||||
joinToken: string
|
|
||||||
householdId: string
|
|
||||||
householdName: string
|
|
||||||
targetDisplayName: string
|
|
||||||
inviteMessageId?: number
|
|
||||||
completed?: boolean
|
|
||||||
} | null {
|
|
||||||
if (
|
|
||||||
typeof payload.joinToken !== 'string' ||
|
|
||||||
payload.joinToken.trim().length === 0 ||
|
|
||||||
typeof payload.householdId !== 'string' ||
|
|
||||||
payload.householdId.trim().length === 0 ||
|
|
||||||
typeof payload.householdName !== 'string' ||
|
|
||||||
payload.householdName.trim().length === 0 ||
|
|
||||||
typeof payload.targetDisplayName !== 'string' ||
|
|
||||||
payload.targetDisplayName.trim().length === 0
|
|
||||||
) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
joinToken: payload.joinToken,
|
|
||||||
householdId: payload.householdId,
|
|
||||||
householdName: payload.householdName,
|
|
||||||
targetDisplayName: payload.targetDisplayName,
|
|
||||||
...(typeof payload.inviteMessageId === 'number' && Number.isInteger(payload.inviteMessageId)
|
|
||||||
? {
|
|
||||||
inviteMessageId: payload.inviteMessageId
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(payload.completed === true
|
|
||||||
? {
|
|
||||||
completed: true
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseInviteStartPayload(payload: string): {
|
|
||||||
telegramChatId: string
|
|
||||||
targetTelegramUserId: string
|
|
||||||
} | null {
|
|
||||||
const match = /^invite_(-?\d+)_(\d+)$/.exec(payload)
|
|
||||||
if (!match) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
telegramChatId: match[1]!,
|
|
||||||
targetTelegramUserId: match[2]!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildGroupInviteDeepLink(
|
|
||||||
botUsername: string | undefined,
|
|
||||||
telegramChatId: string,
|
|
||||||
targetTelegramUserId: string
|
|
||||||
): string | null {
|
|
||||||
return buildBotStartDeepLink(
|
|
||||||
botUsername,
|
|
||||||
`${GROUP_INVITE_START_PREFIX}${telegramChatId}_${targetTelegramUserId}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildMiniAppBaseUrl(
|
function buildMiniAppBaseUrl(
|
||||||
miniAppUrl: string | undefined,
|
miniAppUrl: string | undefined,
|
||||||
botUsername?: string | undefined
|
botUsername?: string | undefined
|
||||||
@@ -562,46 +450,6 @@ export function registerHouseholdSetupCommands(options: {
|
|||||||
miniAppUrl?: string
|
miniAppUrl?: string
|
||||||
logger?: Logger
|
logger?: Logger
|
||||||
}): void {
|
}): void {
|
||||||
async function editGroupInviteCompletion(input: {
|
|
||||||
locale: BotLocale
|
|
||||||
telegramChatId: string
|
|
||||||
payload: {
|
|
||||||
householdName: string
|
|
||||||
targetDisplayName: string
|
|
||||||
inviteMessageId?: number
|
|
||||||
}
|
|
||||||
status: 'active' | 'pending'
|
|
||||||
ctx: Context
|
|
||||||
}) {
|
|
||||||
if (!input.payload.inviteMessageId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const t = getBotTranslations(input.locale).setup
|
|
||||||
const text =
|
|
||||||
input.status === 'active'
|
|
||||||
? t.inviteJoinCompleted(input.payload.targetDisplayName, input.payload.householdName)
|
|
||||||
: t.inviteJoinRequestSent(input.payload.targetDisplayName, input.payload.householdName)
|
|
||||||
|
|
||||||
try {
|
|
||||||
await input.ctx.api.editMessageText(
|
|
||||||
Number(input.telegramChatId),
|
|
||||||
input.payload.inviteMessageId,
|
|
||||||
text
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
options.logger?.warn(
|
|
||||||
{
|
|
||||||
event: 'household_setup.invite_message_update_failed',
|
|
||||||
telegramChatId: input.telegramChatId,
|
|
||||||
inviteMessageId: input.payload.inviteMessageId,
|
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
},
|
|
||||||
'Failed to update household invite message'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isInviteAuthorized(ctx: Context, householdId: string): Promise<boolean> {
|
async function isInviteAuthorized(ctx: Context, householdId: string): Promise<boolean> {
|
||||||
if (await isGroupAdmin(ctx)) {
|
if (await isGroupAdmin(ctx)) {
|
||||||
return true
|
return true
|
||||||
@@ -806,148 +654,6 @@ export function registerHouseholdSetupCommands(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startPayload = commandArgText(ctx)
|
const startPayload = commandArgText(ctx)
|
||||||
const inviteStart = parseInviteStartPayload(startPayload)
|
|
||||||
if (inviteStart) {
|
|
||||||
if (ctx.from.id.toString() !== inviteStart.targetTelegramUserId) {
|
|
||||||
await ctx.reply(t.setup.inviteJoinWrongUser)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.promptRepository) {
|
|
||||||
await ctx.reply(t.setup.inviteJoinExpired)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const invitePending = await options.promptRepository.getPendingAction(
|
|
||||||
invitePendingChatId(inviteStart.telegramChatId),
|
|
||||||
inviteStart.targetTelegramUserId
|
|
||||||
)
|
|
||||||
const invitePayload =
|
|
||||||
invitePending?.action === GROUP_INVITE_ACTION
|
|
||||||
? parseGroupInvitePayload(invitePending.payload)
|
|
||||||
: null
|
|
||||||
const inviteExpiresAt = invitePending?.expiresAt ?? null
|
|
||||||
|
|
||||||
if (!invitePayload) {
|
|
||||||
await ctx.reply(t.setup.inviteJoinExpired)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const identity = {
|
|
||||||
telegramUserId: ctx.from.id.toString(),
|
|
||||||
displayName:
|
|
||||||
[ctx.from.first_name, ctx.from.last_name].filter(Boolean).join(' ').trim() ||
|
|
||||||
ctx.from.username ||
|
|
||||||
`Telegram ${ctx.from.id}`,
|
|
||||||
...(ctx.from.username
|
|
||||||
? {
|
|
||||||
username: ctx.from.username
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(ctx.from.language_code
|
|
||||||
? {
|
|
||||||
languageCode: ctx.from.language_code
|
|
||||||
}
|
|
||||||
: {})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invitePayload.completed) {
|
|
||||||
const access = await options.householdOnboardingService.getMiniAppAccess({
|
|
||||||
identity,
|
|
||||||
joinToken: invitePayload.joinToken
|
|
||||||
})
|
|
||||||
locale = localeFromAccess(access, fallbackLocale)
|
|
||||||
t = getBotTranslations(locale)
|
|
||||||
|
|
||||||
if (access.status === 'active') {
|
|
||||||
await editGroupInviteCompletion({
|
|
||||||
locale,
|
|
||||||
telegramChatId: inviteStart.telegramChatId,
|
|
||||||
payload: invitePayload,
|
|
||||||
status: 'active',
|
|
||||||
ctx
|
|
||||||
})
|
|
||||||
await ctx.reply(
|
|
||||||
t.setup.alreadyActiveMember(access.member.displayName),
|
|
||||||
miniAppReplyMarkup(locale, options.miniAppUrl, ctx.me.username, invitePayload.joinToken)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (access.status === 'pending') {
|
|
||||||
await editGroupInviteCompletion({
|
|
||||||
locale,
|
|
||||||
telegramChatId: inviteStart.telegramChatId,
|
|
||||||
payload: invitePayload,
|
|
||||||
status: 'pending',
|
|
||||||
ctx
|
|
||||||
})
|
|
||||||
await ctx.reply(
|
|
||||||
t.setup.joinRequestSent(access.household.name),
|
|
||||||
miniAppReplyMarkup(locale, options.miniAppUrl, ctx.me.username, invitePayload.joinToken)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.reply(t.setup.inviteJoinExpired)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await options.householdOnboardingService.joinHousehold({
|
|
||||||
identity,
|
|
||||||
joinToken: invitePayload.joinToken
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status === 'invalid_token') {
|
|
||||||
await ctx.reply(t.setup.inviteJoinExpired)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.status === 'active') {
|
|
||||||
locale = result.member.preferredLocale ?? result.member.householdDefaultLocale
|
|
||||||
t = getBotTranslations(locale)
|
|
||||||
} else {
|
|
||||||
const access = await options.householdOnboardingService.getMiniAppAccess({
|
|
||||||
identity,
|
|
||||||
joinToken: invitePayload.joinToken
|
|
||||||
})
|
|
||||||
locale = localeFromAccess(access, fallbackLocale)
|
|
||||||
t = getBotTranslations(locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
await options.promptRepository.upsertPendingAction({
|
|
||||||
telegramUserId: inviteStart.targetTelegramUserId,
|
|
||||||
telegramChatId: invitePendingChatId(inviteStart.telegramChatId),
|
|
||||||
action: GROUP_INVITE_ACTION,
|
|
||||||
payload: {
|
|
||||||
...invitePayload,
|
|
||||||
completed: true
|
|
||||||
},
|
|
||||||
expiresAt: inviteExpiresAt
|
|
||||||
})
|
|
||||||
|
|
||||||
await editGroupInviteCompletion({
|
|
||||||
locale,
|
|
||||||
telegramChatId: inviteStart.telegramChatId,
|
|
||||||
payload: invitePayload,
|
|
||||||
status: result.status,
|
|
||||||
ctx
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status === 'active') {
|
|
||||||
await ctx.reply(
|
|
||||||
t.setup.alreadyActiveMember(result.member.displayName),
|
|
||||||
miniAppReplyMarkup(locale, options.miniAppUrl, ctx.me.username, invitePayload.joinToken)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await ctx.reply(
|
|
||||||
t.setup.joinRequestSent(result.household.name),
|
|
||||||
miniAppReplyMarkup(locale, options.miniAppUrl, ctx.me.username, invitePayload.joinToken)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!startPayload.startsWith('join_')) {
|
if (!startPayload.startsWith('join_')) {
|
||||||
if (startPayload === 'dashboard') {
|
if (startPayload === 'dashboard') {
|
||||||
@@ -1235,7 +941,7 @@ export function registerHouseholdSetupCommands(options: {
|
|||||||
await ctx.reply(t.setup.approvedMember(result.member.displayName, result.householdName))
|
await ctx.reply(t.setup.approvedMember(result.member.displayName, result.householdName))
|
||||||
})
|
})
|
||||||
|
|
||||||
options.bot.command('invite', async (ctx) => {
|
options.bot.command('join_link', async (ctx) => {
|
||||||
const locale = await resolveReplyLocale({
|
const locale = await resolveReplyLocale({
|
||||||
ctx,
|
ctx,
|
||||||
repository: options.householdConfigurationRepository
|
repository: options.householdConfigurationRepository
|
||||||
@@ -1243,12 +949,12 @@ export function registerHouseholdSetupCommands(options: {
|
|||||||
const t = getBotTranslations(locale)
|
const t = getBotTranslations(locale)
|
||||||
|
|
||||||
if (!isGroupChat(ctx)) {
|
if (!isGroupChat(ctx)) {
|
||||||
await ctx.reply(t.setup.useInviteInGroup)
|
await ctx.reply(t.setup.useJoinLinkInGroup)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.promptRepository || !options.householdConfigurationRepository) {
|
if (!options.householdConfigurationRepository) {
|
||||||
await ctx.reply(t.setup.inviteJoinExpired)
|
await ctx.reply(t.setup.householdNotConfigured)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,35 +971,6 @@ export function registerHouseholdSetupCommands(options: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = repliedTelegramUser(ctx)
|
|
||||||
if (!target) {
|
|
||||||
await ctx.reply(t.setup.inviteUsage)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingMember = await options.householdConfigurationRepository.getHouseholdMember(
|
|
||||||
household.householdId,
|
|
||||||
target.telegramUserId
|
|
||||||
)
|
|
||||||
if (existingMember?.status === 'active') {
|
|
||||||
await ctx.reply(
|
|
||||||
t.setup.inviteAlreadyMember(existingMember.displayName, household.householdName)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingPending =
|
|
||||||
await options.householdConfigurationRepository.getPendingHouseholdMember(
|
|
||||||
household.householdId,
|
|
||||||
target.telegramUserId
|
|
||||||
)
|
|
||||||
if (existingPending) {
|
|
||||||
await ctx.reply(
|
|
||||||
t.setup.inviteAlreadyPending(existingPending.displayName, household.householdName)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const joinToken = await options.householdOnboardingService.ensureHouseholdJoinToken({
|
const joinToken = await options.householdOnboardingService.ensureHouseholdJoinToken({
|
||||||
householdId: household.householdId,
|
householdId: household.householdId,
|
||||||
...(ctx.from?.id
|
...(ctx.from?.id
|
||||||
@@ -1303,57 +980,26 @@ export function registerHouseholdSetupCommands(options: {
|
|||||||
: {})
|
: {})
|
||||||
})
|
})
|
||||||
|
|
||||||
await options.promptRepository.upsertPendingAction({
|
const joinDeepLink = ctx.me.username
|
||||||
telegramUserId: target.telegramUserId,
|
? `https://t.me/${ctx.me.username}?start=join_${encodeURIComponent(joinToken.token)}`
|
||||||
telegramChatId: invitePendingChatId(ctx.chat.id.toString()),
|
: null
|
||||||
action: GROUP_INVITE_ACTION,
|
|
||||||
payload: {
|
|
||||||
joinToken: joinToken.token,
|
|
||||||
householdId: household.householdId,
|
|
||||||
householdName: household.householdName,
|
|
||||||
targetDisplayName: target.displayName
|
|
||||||
},
|
|
||||||
expiresAt: nowInstant().add({ milliseconds: GROUP_INVITE_TTL_MS })
|
|
||||||
})
|
|
||||||
|
|
||||||
const deepLink = buildGroupInviteDeepLink(
|
if (!joinDeepLink) {
|
||||||
ctx.me.username,
|
await ctx.reply(t.setup.joinLinkUnavailable)
|
||||||
ctx.chat.id.toString(),
|
|
||||||
target.telegramUserId
|
|
||||||
)
|
|
||||||
if (!deepLink) {
|
|
||||||
await ctx.reply(t.setup.inviteJoinExpired)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const inviteMessage = await ctx.reply(
|
await ctx.reply(t.setup.joinLinkReady(joinDeepLink, household.householdName), {
|
||||||
t.setup.invitePrepared(target.displayName, household.householdName),
|
|
||||||
{
|
|
||||||
reply_markup: {
|
reply_markup: {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: t.setup.joinHouseholdButton,
|
text: t.setup.joinHouseholdButton,
|
||||||
url: deepLink
|
url: joinDeepLink
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
await options.promptRepository.upsertPendingAction({
|
|
||||||
telegramUserId: target.telegramUserId,
|
|
||||||
telegramChatId: invitePendingChatId(ctx.chat.id.toString()),
|
|
||||||
action: GROUP_INVITE_ACTION,
|
|
||||||
payload: {
|
|
||||||
joinToken: joinToken.token,
|
|
||||||
householdId: household.householdId,
|
|
||||||
householdName: household.householdName,
|
|
||||||
targetDisplayName: target.displayName,
|
|
||||||
inviteMessageId: inviteMessage.message_id
|
|
||||||
},
|
|
||||||
expiresAt: nowInstant().add({ milliseconds: GROUP_INVITE_TTL_MS })
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const enBotTranslations: BotTranslationCatalog = {
|
|||||||
bind_feedback_topic: 'Bind the current topic as feedback',
|
bind_feedback_topic: 'Bind the current topic as feedback',
|
||||||
bind_reminders_topic: 'Bind the current topic as reminders',
|
bind_reminders_topic: 'Bind the current topic as reminders',
|
||||||
bind_payments_topic: 'Bind the current topic as payments',
|
bind_payments_topic: 'Bind the current topic as payments',
|
||||||
invite: 'Invite the replied user into this household',
|
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',
|
||||||
approve_member: 'Approve a pending household member'
|
approve_member: 'Approve a pending household member'
|
||||||
@@ -126,28 +126,16 @@ export const enBotTranslations: BotTranslationCatalog = {
|
|||||||
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>',
|
||||||
useInviteInGroup: 'Use /invite as a reply inside the household group.',
|
|
||||||
onlyInviteAdmins: 'Only Telegram group admins or household admins can invite members.',
|
onlyInviteAdmins: 'Only Telegram group admins or household admins can invite members.',
|
||||||
inviteUsage: 'Reply to a real user message with /invite.',
|
|
||||||
inviteTargetInvalid: 'I can only prepare invites for real group members.',
|
|
||||||
inviteAlreadyMember: (displayName, householdName) =>
|
|
||||||
`${displayName} is already an active member of ${householdName}.`,
|
|
||||||
inviteAlreadyPending: (displayName, householdName) =>
|
|
||||||
`${displayName} already has a pending join request for ${householdName}.`,
|
|
||||||
invitePrepared: (displayName, householdName) =>
|
|
||||||
`Invitation prepared for ${displayName}. Tap below to join ${householdName}.`,
|
|
||||||
invitePreparedToast: (displayName) => `Invite prepared for ${displayName}.`,
|
|
||||||
inviteJoinWrongUser: 'This invite is for a different Telegram user.',
|
|
||||||
inviteJoinExpired: 'This invite is no longer available.',
|
|
||||||
inviteJoinCompleted: (displayName, householdName) =>
|
|
||||||
`${displayName} completed the join flow for ${householdName}.`,
|
|
||||||
inviteJoinRequestSent: (displayName, householdName) =>
|
|
||||||
`${displayName} sent a join request for ${householdName}.`,
|
|
||||||
approvedMember: (displayName, householdName) =>
|
approvedMember: (displayName, householdName) =>
|
||||||
`Approved ${displayName} as an active member of ${householdName}.`,
|
`Approved ${displayName} as an active member of ${householdName}.`,
|
||||||
useButtonInGroup: 'Use this button in the household group.',
|
useButtonInGroup: 'Use this button in the household group.',
|
||||||
unableToIdentifySelectedMember: 'Unable to identify the selected member.',
|
unableToIdentifySelectedMember: 'Unable to identify the selected member.',
|
||||||
approvedMemberToast: (displayName) => `Approved ${displayName}.`
|
approvedMemberToast: (displayName) => `Approved ${displayName}.`,
|
||||||
|
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.`
|
||||||
},
|
},
|
||||||
anonymousFeedback: {
|
anonymousFeedback: {
|
||||||
title: 'Anonymous household note',
|
title: 'Anonymous household note',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const ruBotTranslations: BotTranslationCatalog = {
|
|||||||
bind_feedback_topic: 'Назначить текущий топик для анонимных сообщений',
|
bind_feedback_topic: 'Назначить текущий топик для анонимных сообщений',
|
||||||
bind_reminders_topic: 'Назначить текущий топик для напоминаний',
|
bind_reminders_topic: 'Назначить текущий топик для напоминаний',
|
||||||
bind_payments_topic: 'Назначить текущий топик для оплат',
|
bind_payments_topic: 'Назначить текущий топик для оплат',
|
||||||
invite: 'Пригласить пользователя из сообщения в этот дом',
|
join_link: 'Получить ссылку для приглашения новых участников',
|
||||||
payment_add: 'Подтвердить оплату аренды или коммуналки',
|
payment_add: 'Подтвердить оплату аренды или коммуналки',
|
||||||
pending_members: 'Показать ожидающие заявки на вступление',
|
pending_members: 'Показать ожидающие заявки на вступление',
|
||||||
approve_member: 'Подтвердить участника дома'
|
approve_member: 'Подтвердить участника дома'
|
||||||
@@ -128,28 +128,16 @@ export const ruBotTranslations: BotTranslationCatalog = {
|
|||||||
usePendingMembersInGroup: 'Используйте /pending_members внутри группы дома.',
|
usePendingMembersInGroup: 'Используйте /pending_members внутри группы дома.',
|
||||||
useApproveMemberInGroup: 'Используйте /approve_member внутри группы дома.',
|
useApproveMemberInGroup: 'Используйте /approve_member внутри группы дома.',
|
||||||
approveMemberUsage: 'Использование: /approve_member <telegram_user_id>',
|
approveMemberUsage: 'Использование: /approve_member <telegram_user_id>',
|
||||||
useInviteInGroup: 'Используйте /invite как ответ внутри группы дома.',
|
|
||||||
onlyInviteAdmins: 'Приглашать участников могут только админы Telegram-группы или админы дома.',
|
onlyInviteAdmins: 'Приглашать участников могут только админы Telegram-группы или админы дома.',
|
||||||
inviteUsage: 'Ответьте командой /invite на сообщение реального участника.',
|
|
||||||
inviteTargetInvalid: 'Я могу подготовить приглашение только для реального участника группы.',
|
|
||||||
inviteAlreadyMember: (displayName, householdName) =>
|
|
||||||
`${displayName} уже является активным участником ${householdName}.`,
|
|
||||||
inviteAlreadyPending: (displayName, householdName) =>
|
|
||||||
`${displayName} уже отправил(а) заявку на вступление в ${householdName}.`,
|
|
||||||
invitePrepared: (displayName, householdName) =>
|
|
||||||
`Приглашение для ${displayName} готово. Нажмите кнопку ниже, чтобы вступить в ${householdName}.`,
|
|
||||||
invitePreparedToast: (displayName) => `Приглашение для ${displayName} подготовлено.`,
|
|
||||||
inviteJoinWrongUser: 'Это приглашение предназначено для другого пользователя Telegram.',
|
|
||||||
inviteJoinExpired: 'Это приглашение больше недоступно.',
|
|
||||||
inviteJoinCompleted: (displayName, householdName) =>
|
|
||||||
`${displayName} завершил(а) вступление в ${householdName}.`,
|
|
||||||
inviteJoinRequestSent: (displayName, householdName) =>
|
|
||||||
`${displayName} отправил(а) заявку на вступление в ${householdName}.`,
|
|
||||||
approvedMember: (displayName, householdName) =>
|
approvedMember: (displayName, householdName) =>
|
||||||
`Участник ${displayName} подтверждён как активный участник ${householdName}.`,
|
`Участник ${displayName} подтверждён как активный участник ${householdName}.`,
|
||||||
useButtonInGroup: 'Используйте эту кнопку в группе дома.',
|
useButtonInGroup: 'Используйте эту кнопку в группе дома.',
|
||||||
unableToIdentifySelectedMember: 'Не удалось определить выбранного участника.',
|
unableToIdentifySelectedMember: 'Не удалось определить выбранного участника.',
|
||||||
approvedMemberToast: (displayName) => `${displayName} подтверждён.`
|
approvedMemberToast: (displayName) => `${displayName} подтверждён.`,
|
||||||
|
useJoinLinkInGroup: 'Используйте /join_link внутри группы дома.',
|
||||||
|
joinLinkUnavailable: 'Не удалось сгенерировать ссылку для вступления.',
|
||||||
|
joinLinkReady: (link, householdName) =>
|
||||||
|
`Поделитесь этой ссылкой, чтобы пригласить участников в ${householdName}:\n\n${link}\n\nЛюбой, у кого есть эта ссылка, может подать заявку на вступление.`
|
||||||
},
|
},
|
||||||
anonymousFeedback: {
|
anonymousFeedback: {
|
||||||
title: 'Анонимное сообщение по дому',
|
title: 'Анонимное сообщение по дому',
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export type TelegramCommandName =
|
|||||||
| 'bind_feedback_topic'
|
| 'bind_feedback_topic'
|
||||||
| 'bind_reminders_topic'
|
| 'bind_reminders_topic'
|
||||||
| 'bind_payments_topic'
|
| 'bind_payments_topic'
|
||||||
| 'invite'
|
| 'join_link'
|
||||||
| 'payment_add'
|
| 'payment_add'
|
||||||
| 'pending_members'
|
| 'pending_members'
|
||||||
| 'approve_member'
|
| 'approve_member'
|
||||||
@@ -29,7 +29,7 @@ export interface BotCommandDescriptions {
|
|||||||
bind_feedback_topic: string
|
bind_feedback_topic: string
|
||||||
bind_reminders_topic: string
|
bind_reminders_topic: string
|
||||||
bind_payments_topic: string
|
bind_payments_topic: string
|
||||||
invite: string
|
join_link: string
|
||||||
payment_add: string
|
payment_add: string
|
||||||
pending_members: string
|
pending_members: string
|
||||||
approve_member: string
|
approve_member: string
|
||||||
@@ -115,22 +115,14 @@ export interface BotTranslationCatalog {
|
|||||||
usePendingMembersInGroup: string
|
usePendingMembersInGroup: string
|
||||||
useApproveMemberInGroup: string
|
useApproveMemberInGroup: string
|
||||||
approveMemberUsage: string
|
approveMemberUsage: string
|
||||||
useInviteInGroup: string
|
|
||||||
onlyInviteAdmins: string
|
onlyInviteAdmins: string
|
||||||
inviteUsage: string
|
|
||||||
inviteTargetInvalid: string
|
|
||||||
inviteAlreadyMember: (displayName: string, householdName: string) => string
|
|
||||||
inviteAlreadyPending: (displayName: string, householdName: string) => string
|
|
||||||
invitePrepared: (displayName: string, householdName: string) => string
|
|
||||||
invitePreparedToast: (displayName: string) => string
|
|
||||||
inviteJoinWrongUser: string
|
|
||||||
inviteJoinExpired: string
|
|
||||||
inviteJoinCompleted: (displayName: string, householdName: string) => string
|
|
||||||
inviteJoinRequestSent: (displayName: string, householdName: string) => string
|
|
||||||
approvedMember: (displayName: string, householdName: string) => string
|
approvedMember: (displayName: string, householdName: string) => string
|
||||||
useButtonInGroup: string
|
useButtonInGroup: string
|
||||||
unableToIdentifySelectedMember: string
|
unableToIdentifySelectedMember: string
|
||||||
approvedMemberToast: (displayName: string) => string
|
approvedMemberToast: (displayName: string) => string
|
||||||
|
useJoinLinkInGroup: string
|
||||||
|
joinLinkUnavailable: string
|
||||||
|
joinLinkReady: (link: string, householdName: string) => string
|
||||||
}
|
}
|
||||||
anonymousFeedback: {
|
anonymousFeedback: {
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const GROUP_ADMIN_COMMAND_NAMES = [
|
|||||||
'bind_feedback_topic',
|
'bind_feedback_topic',
|
||||||
'bind_reminders_topic',
|
'bind_reminders_topic',
|
||||||
'bind_payments_topic',
|
'bind_payments_topic',
|
||||||
'invite',
|
'join_link',
|
||||||
'pending_members',
|
'pending_members',
|
||||||
'approve_member'
|
'approve_member'
|
||||||
] as const satisfies readonly TelegramCommandName[]
|
] as const satisfies readonly TelegramCommandName[]
|
||||||
|
|||||||
Reference in New Issue
Block a user