feat(bot): implement /app and /keyboard commands, add dashboard links

This commit is contained in:
2026-03-15 02:13:32 +04:00
parent 531e52b238
commit 3c53ab9e1a
6 changed files with 159 additions and 11 deletions

View File

@@ -67,6 +67,8 @@ function isGroupChat(ctx: Context): boolean {
export function createFinanceCommandsService(options: { export function createFinanceCommandsService(options: {
householdConfigurationRepository: HouseholdConfigurationRepository householdConfigurationRepository: HouseholdConfigurationRepository
financeServiceForHousehold: (householdId: string) => FinanceCommandService financeServiceForHousehold: (householdId: string) => FinanceCommandService
miniAppUrl?: string
botUsername?: string
}): { }): {
register: (bot: Bot) => void register: (bot: Bot) => void
} { } {
@@ -253,7 +255,28 @@ export function createFinanceCommandsService(options: {
resolved.householdId resolved.householdId
) )
await ctx.reply(formatHouseholdStatus(locale, dashboard, settings.rentDueDay)) const webAppUrl =
options.miniAppUrl && ctx.me.username
? `${options.miniAppUrl}${options.miniAppUrl.includes('?') ? '&' : '?'}bot=${ctx.me.username}`
: options.miniAppUrl
await ctx.reply(
formatHouseholdStatus(locale, dashboard, settings.rentDueDay),
webAppUrl
? {
reply_markup: {
inline_keyboard: [
[
{
text: getBotTranslations(locale).setup.openMiniAppButton,
web_app: { url: webAppUrl }
}
]
]
}
}
: {}
)
} catch (error) { } catch (error) {
await ctx.reply(t.statementFailed((error as Error).message)) await ctx.reply(t.statementFailed((error as Error).message))
} }

View File

@@ -147,8 +147,11 @@ function setupKeyboard(input: {
locale: BotLocale locale: BotLocale
joinDeepLink: string | null joinDeepLink: string | null
bindings: readonly HouseholdTopicBindingRecord[] bindings: readonly HouseholdTopicBindingRecord[]
miniAppUrl: string | undefined
botUsername: string | undefined
}) { }) {
const t = getBotTranslations(input.locale).setup const t = getBotTranslations(input.locale).setup
const kt = getBotTranslations(input.locale).keyboard
const configuredRoles = new Set(input.bindings.map((binding) => binding.role)) const configuredRoles = new Set(input.bindings.map((binding) => binding.role))
const rows: Array< const rows: Array<
Array< Array<
@@ -160,6 +163,10 @@ function setupKeyboard(input: {
text: string text: string
callback_data: string callback_data: string
} }
| {
text: string
web_app: { url: string }
}
> >
> = [] > = []
@@ -189,6 +196,19 @@ function setupKeyboard(input: {
]) ])
} }
// Add dashboard button
const webAppUrl = buildOpenMiniAppUrl(input.miniAppUrl, input.botUsername)
if (webAppUrl) {
rows.push([
{
text: kt.dashboardButton,
web_app: {
url: webAppUrl
}
}
])
}
return rows.length > 0 return rows.length > 0
? { ? {
reply_markup: { reply_markup: {
@@ -234,6 +254,8 @@ function setupReply(input: {
created: boolean created: boolean
joinDeepLink: string | null joinDeepLink: string | null
bindings: readonly HouseholdTopicBindingRecord[] bindings: readonly HouseholdTopicBindingRecord[]
miniAppUrl: string | undefined
botUsername: string | undefined
}) { }) {
const t = getBotTranslations(input.locale).setup const t = getBotTranslations(input.locale).setup
return { return {
@@ -250,7 +272,9 @@ function setupReply(input: {
...setupKeyboard({ ...setupKeyboard({
locale: input.locale, locale: input.locale,
joinDeepLink: input.joinDeepLink, joinDeepLink: input.joinDeepLink,
bindings: input.bindings bindings: input.bindings,
miniAppUrl: input.miniAppUrl,
botUsername: input.botUsername
}) })
} }
} }
@@ -382,6 +406,8 @@ export function registerHouseholdSetupCommands(options: {
locale: BotLocale locale: BotLocale
household: HouseholdTelegramChatRecord household: HouseholdTelegramChatRecord
created: boolean created: boolean
miniAppUrl: string | undefined
botUsername: string | undefined
}) { }) {
const joinToken = await options.householdOnboardingService.ensureHouseholdJoinToken({ const joinToken = await options.householdOnboardingService.ensureHouseholdJoinToken({
householdId: input.household.householdId, householdId: input.household.householdId,
@@ -407,7 +433,9 @@ export function registerHouseholdSetupCommands(options: {
household: input.household, household: input.household,
created: input.created, created: input.created,
joinDeepLink, joinDeepLink,
bindings bindings,
miniAppUrl: input.miniAppUrl,
botUsername: input.botUsername
}) })
} }
@@ -563,7 +591,9 @@ export function registerHouseholdSetupCommands(options: {
ctx, ctx,
locale, locale,
household: result.household, household: result.household,
created: result.status === 'created' created: result.status === 'created',
miniAppUrl: options.miniAppUrl,
botUsername: ctx.me.username
}) })
const sent = await ctx.reply( const sent = await ctx.reply(
reply.text, reply.text,
@@ -966,7 +996,9 @@ export function registerHouseholdSetupCommands(options: {
ctx, ctx,
locale, locale,
household: result.household, household: result.household,
created: false created: false,
miniAppUrl: options.miniAppUrl,
botUsername: ctx.me.username
}) })
await ctx.answerCallbackQuery({ await ctx.answerCallbackQuery({
@@ -1068,7 +1100,9 @@ export function registerHouseholdSetupCommands(options: {
ctx, ctx,
locale, locale,
household: result.household, household: result.household,
created: false created: false,
miniAppUrl: options.miniAppUrl,
botUsername: ctx.me.username
}) })
try { try {
@@ -1091,6 +1125,50 @@ export function registerHouseholdSetupCommands(options: {
} }
) )
} }
options.bot.command(['app', 'dashboard'], async (ctx) => {
const locale = await resolveReplyLocale({
ctx,
repository: options.householdConfigurationRepository
})
const t = getBotTranslations(locale)
if (!options.miniAppUrl) {
await ctx.reply(t.setup.openMiniAppUnavailable)
return
}
await ctx.reply(
t.setup.openMiniAppFromPrivateChat,
openMiniAppReplyMarkup(locale, options.miniAppUrl, ctx.me.username)
)
})
options.bot.command('keyboard', async (ctx) => {
const locale = await resolveReplyLocale({
ctx,
repository: options.householdConfigurationRepository
})
const t = getBotTranslations(locale)
if (!options.miniAppUrl) {
await ctx.reply(t.setup.openMiniAppUnavailable)
return
}
const webAppUrl = buildOpenMiniAppUrl(options.miniAppUrl, ctx.me.username)
if (!webAppUrl) {
await ctx.reply(t.setup.openMiniAppUnavailable)
return
}
await ctx.reply(t.keyboard.enabled, {
reply_markup: {
keyboard: [[{ text: t.keyboard.dashboardButton, web_app: { url: webAppUrl } }]],
resize_keyboard: true,
is_persistent: true
}
})
})
} }
function localeFromAccess(access: HouseholdMiniAppAccess, fallback: BotLocale): BotLocale { function localeFromAccess(access: HouseholdMiniAppAccess, fallback: BotLocale): BotLocale {

View File

@@ -13,7 +13,9 @@ export const enBotTranslations: BotTranslationCatalog = {
join_link: 'Get a shareable link for new members to join', join_link: 'Get a shareable link for new members to join',
payment_add: 'Record your rent or utilities payment', payment_add: 'Record your rent or utilities payment',
pending_members: 'List pending household join requests', pending_members: 'List pending household join requests',
approve_member: 'Approve a pending household member' approve_member: 'Approve a pending household member',
app: 'Open the Kojori mini app',
keyboard: 'Toggle persistent dashboard button'
}, },
help: { help: {
intro: 'Household bot is live.', intro: 'Household bot is live.',
@@ -121,6 +123,11 @@ export const enBotTranslations: BotTranslationCatalog = {
joinLinkReady: (link, householdName) => joinLinkReady: (link, householdName) =>
`Join link for ${householdName}:\n${link}\n\nAnyone with this link can join the household. Share it carefully.` `Join link for ${householdName}:\n${link}\n\nAnyone with this link can join the household. Share it carefully.`
}, },
keyboard: {
dashboardButton: '🏡 Dashboard',
enabled: 'Persistent dashboard button enabled.',
disabled: 'Persistent dashboard button disabled.'
},
anonymousFeedback: { anonymousFeedback: {
title: 'Anonymous household note', title: 'Anonymous household note',
cancelButton: 'Cancel', cancelButton: 'Cancel',
@@ -236,7 +243,7 @@ export const enBotTranslations: BotTranslationCatalog = {
reminders: { reminders: {
utilities: (period) => `Utilities reminder for ${period}`, utilities: (period) => `Utilities reminder for ${period}`,
rentWarning: (period) => `Rent reminder for ${period}: payment is coming up soon.`, rentWarning: (period) => `Rent reminder for ${period}: payment is coming up soon.`,
rentDue: (period) => `Rent due reminder for ${period}: please settle payment today.`, rentDue: (period) => `Rent is due for period ${period}. Request sent to the reminders topic.`,
guidedEntryButton: 'Guided entry', guidedEntryButton: 'Guided entry',
copyTemplateButton: 'Copy template', copyTemplateButton: 'Copy template',
openDashboardButton: 'Open dashboard', openDashboardButton: 'Open dashboard',

View File

@@ -13,7 +13,9 @@ export const ruBotTranslations: BotTranslationCatalog = {
join_link: 'Получить ссылку для приглашения новых участников', join_link: 'Получить ссылку для приглашения новых участников',
payment_add: 'Подтвердить оплату аренды или коммуналки', payment_add: 'Подтвердить оплату аренды или коммуналки',
pending_members: 'Показать ожидающие заявки на вступление', pending_members: 'Показать ожидающие заявки на вступление',
approve_member: 'Подтвердить участника дома' approve_member: 'Подтвердить участника дома',
app: 'Открыть мини-приложение Kojori',
keyboard: 'Вкл/выкл кнопку дашборда'
}, },
help: { help: {
intro: 'Бот для дома подключен.', intro: 'Бот для дома подключен.',
@@ -123,6 +125,11 @@ export const ruBotTranslations: BotTranslationCatalog = {
joinLinkReady: (link, householdName) => joinLinkReady: (link, householdName) =>
`Поделитесь этой ссылкой, чтобы пригласить участников в ${householdName}:\n\n${link}\n\nЛюбой, у кого есть эта ссылка, может подать заявку на вступление.` `Поделитесь этой ссылкой, чтобы пригласить участников в ${householdName}:\n\n${link}\n\nЛюбой, у кого есть эта ссылка, может подать заявку на вступление.`
}, },
keyboard: {
dashboardButton: '🏡 Дашборд',
enabled: 'Кнопка дашборда включена.',
disabled: 'Кнопка дашборда выключена.'
},
anonymousFeedback: { anonymousFeedback: {
title: 'Анонимное сообщение по дому', title: 'Анонимное сообщение по дому',
cancelButton: 'Отменить', cancelButton: 'Отменить',

View File

@@ -12,6 +12,8 @@ export type TelegramCommandName =
| 'payment_add' | 'payment_add'
| 'pending_members' | 'pending_members'
| 'approve_member' | 'approve_member'
| 'app'
| 'keyboard'
export interface BotCommandDescriptions { export interface BotCommandDescriptions {
help: string help: string
@@ -25,6 +27,8 @@ export interface BotCommandDescriptions {
payment_add: string payment_add: string
pending_members: string pending_members: string
approve_member: string approve_member: string
app: string
keyboard: string
} }
export interface PendingMemberSummary { export interface PendingMemberSummary {
@@ -107,6 +111,11 @@ export interface BotTranslationCatalog {
joinLinkUnavailable: string joinLinkUnavailable: string
joinLinkReady: (link: string, householdName: string) => string joinLinkReady: (link: string, householdName: string) => string
} }
keyboard: {
dashboardButton: string
enabled: string
disabled: string
}
anonymousFeedback: { anonymousFeedback: {
title: string title: string
cancelButton: string cancelButton: string

View File

@@ -125,11 +125,35 @@ export function createReminderJobsHandler(options: {
} }
case 'rent-warning': case 'rent-warning':
return { return {
text: t.rentWarning(period) text: t.rentWarning(period),
replyMarkup: {
inline_keyboard: [
[
{
text: t.openDashboardButton,
url: options.botUsername
? `https://t.me/${options.botUsername}?start=dashboard`
: '#'
}
]
]
}
} }
case 'rent-due': case 'rent-due':
return { return {
text: t.rentDue(period) text: t.rentDue(period),
replyMarkup: {
inline_keyboard: [
[
{
text: t.openDashboardButton,
url: options.botUsername
? `https://t.me/${options.botUsername}?start=dashboard`
: '#'
}
]
]
}
} }
} }
} }