mirror of
https://github.com/whekin/household-bot.git
synced 2026-03-31 14:04:04 +00:00
fix(bot): hand off reminder dashboard opens through dm
This commit is contained in:
@@ -572,7 +572,7 @@ describe('buildJoinMiniAppUrl', () => {
|
||||
'join-token'
|
||||
)
|
||||
|
||||
expect(url).toBe('https://household-dev-mini-app.example.app/?join=join-token&bot=kojori_bot')
|
||||
expect(url).toBe('https://household-dev-mini-app.example.app/?bot=kojori_bot&join=join-token')
|
||||
})
|
||||
|
||||
test('returns null when no mini app url is configured', () => {
|
||||
@@ -581,6 +581,80 @@ describe('buildJoinMiniAppUrl', () => {
|
||||
})
|
||||
|
||||
describe('registerHouseholdSetupCommands', () => {
|
||||
test('opens the mini app from a dashboard start payload in private chat', 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: false
|
||||
}
|
||||
|
||||
bot.api.config.use(async (_prev, method, payload) => {
|
||||
calls.push({ method, payload })
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
result: {
|
||||
message_id: calls.length,
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
chat: {
|
||||
id: 123456,
|
||||
type: 'private'
|
||||
},
|
||||
text: 'ok'
|
||||
}
|
||||
} as never
|
||||
})
|
||||
|
||||
registerHouseholdSetupCommands({
|
||||
bot,
|
||||
householdSetupService: createRejectedHouseholdSetupService(),
|
||||
householdOnboardingService: {
|
||||
async ensureHouseholdJoinToken() {
|
||||
throw new Error('not used')
|
||||
},
|
||||
async getMiniAppAccess() {
|
||||
throw new Error('not used')
|
||||
},
|
||||
async joinHousehold() {
|
||||
throw new Error('not used')
|
||||
}
|
||||
},
|
||||
householdAdminService: createHouseholdAdminService(),
|
||||
miniAppUrl: 'https://miniapp.example.app'
|
||||
})
|
||||
|
||||
await bot.handleUpdate(startUpdate('/start dashboard') as never)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]?.payload).toMatchObject({
|
||||
chat_id: 123456,
|
||||
text: 'Open the mini app from the button below.',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{
|
||||
text: 'Open mini app',
|
||||
web_app: {
|
||||
url: 'https://miniapp.example.app/?bot=household_test_bot'
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('offers an Open mini app button after a DM join request', async () => {
|
||||
const bot = createTelegramBot('000000:test-token')
|
||||
const calls: Array<{ method: string; payload: unknown }> = []
|
||||
@@ -662,7 +736,7 @@ describe('registerHouseholdSetupCommands', () => {
|
||||
{
|
||||
text: 'Open mini app',
|
||||
web_app: {
|
||||
url: 'https://miniapp.example.app/?join=join-token&bot=household_test_bot'
|
||||
url: 'https://miniapp.example.app/?bot=household_test_bot&join=join-token'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -750,7 +824,7 @@ describe('registerHouseholdSetupCommands', () => {
|
||||
{
|
||||
text: 'Открыть мини-приложение',
|
||||
web_app: {
|
||||
url: 'https://miniapp.example.app/?join=join-token&bot=household_test_bot'
|
||||
url: 'https://miniapp.example.app/?bot=household_test_bot&join=join-token'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { Bot, Context } from 'grammy'
|
||||
|
||||
import { getBotTranslations, type BotLocale } from './i18n'
|
||||
import { resolveReplyLocale } from './bot-locale'
|
||||
import { buildBotStartDeepLink } from './telegram-deep-links'
|
||||
|
||||
const APPROVE_MEMBER_CALLBACK_PREFIX = 'approve_member:'
|
||||
const SETUP_CREATE_TOPIC_CALLBACK_PREFIX = 'setup_topic:create:'
|
||||
@@ -442,18 +443,15 @@ function buildGroupInviteDeepLink(
|
||||
telegramChatId: string,
|
||||
targetTelegramUserId: string
|
||||
): string | null {
|
||||
const normalizedBotUsername = botUsername?.trim()
|
||||
if (!normalizedBotUsername) {
|
||||
return null
|
||||
return buildBotStartDeepLink(
|
||||
botUsername,
|
||||
`${GROUP_INVITE_START_PREFIX}${telegramChatId}_${targetTelegramUserId}`
|
||||
)
|
||||
}
|
||||
|
||||
return `https://t.me/${normalizedBotUsername}?start=${GROUP_INVITE_START_PREFIX}${telegramChatId}_${targetTelegramUserId}`
|
||||
}
|
||||
|
||||
export function buildJoinMiniAppUrl(
|
||||
function buildMiniAppBaseUrl(
|
||||
miniAppUrl: string | undefined,
|
||||
botUsername: string | undefined,
|
||||
joinToken: string
|
||||
botUsername?: string | undefined
|
||||
): string | null {
|
||||
const normalizedMiniAppUrl = miniAppUrl?.trim()
|
||||
if (!normalizedMiniAppUrl) {
|
||||
@@ -461,7 +459,6 @@ export function buildJoinMiniAppUrl(
|
||||
}
|
||||
|
||||
const url = new URL(normalizedMiniAppUrl)
|
||||
url.searchParams.set('join', joinToken)
|
||||
|
||||
if (botUsername && botUsername.trim().length > 0) {
|
||||
url.searchParams.set('bot', botUsername.trim())
|
||||
@@ -470,6 +467,29 @@ export function buildJoinMiniAppUrl(
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
export function buildJoinMiniAppUrl(
|
||||
miniAppUrl: string | undefined,
|
||||
botUsername: string | undefined,
|
||||
joinToken: string
|
||||
): string | null {
|
||||
const baseUrl = buildMiniAppBaseUrl(miniAppUrl, botUsername)
|
||||
if (!baseUrl) {
|
||||
return null
|
||||
}
|
||||
|
||||
const url = new URL(baseUrl)
|
||||
url.searchParams.set('join', joinToken)
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
function buildOpenMiniAppUrl(
|
||||
miniAppUrl: string | undefined,
|
||||
botUsername: string | undefined
|
||||
): string | null {
|
||||
return buildMiniAppBaseUrl(miniAppUrl, botUsername)
|
||||
}
|
||||
|
||||
function miniAppReplyMarkup(
|
||||
locale: BotLocale,
|
||||
miniAppUrl: string | undefined,
|
||||
@@ -497,6 +517,32 @@ function miniAppReplyMarkup(
|
||||
}
|
||||
}
|
||||
|
||||
function openMiniAppReplyMarkup(
|
||||
locale: BotLocale,
|
||||
miniAppUrl: string | undefined,
|
||||
botUsername: string | undefined
|
||||
) {
|
||||
const webAppUrl = buildOpenMiniAppUrl(miniAppUrl, botUsername)
|
||||
if (!webAppUrl) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{
|
||||
text: getBotTranslations(locale).setup.openMiniAppButton,
|
||||
web_app: {
|
||||
url: webAppUrl
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerHouseholdSetupCommands(options: {
|
||||
bot: Bot
|
||||
householdSetupService: HouseholdSetupService
|
||||
@@ -895,6 +941,19 @@ export function registerHouseholdSetupCommands(options: {
|
||||
}
|
||||
|
||||
if (!startPayload.startsWith('join_')) {
|
||||
if (startPayload === 'dashboard') {
|
||||
if (!options.miniAppUrl) {
|
||||
await ctx.reply(t.setup.openMiniAppUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
await ctx.reply(
|
||||
t.setup.openMiniAppFromPrivateChat,
|
||||
openMiniAppReplyMarkup(locale, options.miniAppUrl, ctx.me.username)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await ctx.reply(t.common.useHelp)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export const enBotTranslations: BotTranslationCatalog = {
|
||||
pendingMemberLine: (member, index) =>
|
||||
`${index + 1}. ${member.displayName} (${member.telegramUserId})${member.username ? ` @${member.username}` : ''}`,
|
||||
openMiniAppButton: 'Open mini app',
|
||||
openMiniAppFromPrivateChat: 'Open the mini app from the button below.',
|
||||
openMiniAppUnavailable: 'The mini app is not configured right now.',
|
||||
joinHouseholdButton: 'Join household',
|
||||
approveMemberButton: (displayName) => `Approve ${displayName}`,
|
||||
telegramIdentityRequired: 'Telegram user identity is required to join a household.',
|
||||
|
||||
@@ -44,6 +44,8 @@ export const ruBotTranslations: BotTranslationCatalog = {
|
||||
pendingMemberLine: (member, index) =>
|
||||
`${index + 1}. ${member.displayName} (${member.telegramUserId})${member.username ? ` @${member.username}` : ''}`,
|
||||
openMiniAppButton: 'Открыть мини-приложение',
|
||||
openMiniAppFromPrivateChat: 'Откройте мини-приложение по кнопке ниже.',
|
||||
openMiniAppUnavailable: 'Мини-приложение сейчас не настроено.',
|
||||
joinHouseholdButton: 'Вступить в дом',
|
||||
approveMemberButton: (displayName) => `Подтвердить ${displayName}`,
|
||||
telegramIdentityRequired: 'Чтобы вступить в дом, нужна Telegram-учётка пользователя.',
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface BotTranslationCatalog {
|
||||
pendingMembersEmpty: (householdName: string) => string
|
||||
pendingMemberLine: (member: PendingMemberSummary, index: number) => string
|
||||
openMiniAppButton: string
|
||||
openMiniAppFromPrivateChat: string
|
||||
openMiniAppUnavailable: string
|
||||
joinHouseholdButton: string
|
||||
approveMemberButton: (displayName: string) => string
|
||||
telegramIdentityRequired: string
|
||||
|
||||
@@ -92,6 +92,7 @@ const bot = createTelegramBot(
|
||||
getLogger('telegram'),
|
||||
householdConfigurationRepositoryClient?.repository
|
||||
)
|
||||
bot.botInfo = await bot.api.getMe()
|
||||
const webhookHandler = webhookCallback(bot, 'std/http', {
|
||||
onTimeout: 'return'
|
||||
})
|
||||
@@ -359,6 +360,11 @@ const reminderJobs = runtime.reminderJobsEnabled
|
||||
miniAppUrl: runtime.miniAppAllowedOrigins[0]
|
||||
}
|
||||
: {}),
|
||||
...(bot.botInfo?.username
|
||||
? {
|
||||
botUsername: bot.botInfo.username
|
||||
}
|
||||
: {}),
|
||||
logger: getLogger('scheduler')
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -42,7 +42,8 @@ describe('createReminderJobsHandler', () => {
|
||||
releaseReminderDispatch: mock(async () => {}),
|
||||
sendReminderMessage,
|
||||
reminderService,
|
||||
now: () => fixedNow
|
||||
now: () => fixedNow,
|
||||
botUsername: 'household_test_bot'
|
||||
})
|
||||
|
||||
const response = await handler.handle(
|
||||
@@ -72,6 +73,12 @@ describe('createReminderJobsHandler', () => {
|
||||
text: 'Шаблон',
|
||||
callback_data: 'reminder_util:template'
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
text: 'Открыть дашборд',
|
||||
url: 'https://t.me/household_test_bot?start=dashboard'
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ export function createReminderJobsHandler(options: {
|
||||
forceDryRun?: boolean
|
||||
now?: () => Temporal.Instant
|
||||
miniAppUrl?: string
|
||||
botUsername?: string
|
||||
logger?: Logger
|
||||
}): {
|
||||
handle: (request: Request, rawReminderType: string) => Promise<Response>
|
||||
@@ -109,7 +110,18 @@ export function createReminderJobsHandler(options: {
|
||||
case 'utilities':
|
||||
return {
|
||||
text: t.utilities(period),
|
||||
replyMarkup: buildUtilitiesReminderReplyMarkup(target.locale, options.miniAppUrl)
|
||||
replyMarkup: buildUtilitiesReminderReplyMarkup(target.locale, {
|
||||
...(options.miniAppUrl
|
||||
? {
|
||||
miniAppUrl: options.miniAppUrl
|
||||
}
|
||||
: {}),
|
||||
...(options.botUsername
|
||||
? {
|
||||
botUsername: options.botUsername
|
||||
}
|
||||
: {})
|
||||
})
|
||||
}
|
||||
case 'rent-warning':
|
||||
return {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { InlineKeyboardMarkup } from 'grammy/types'
|
||||
|
||||
import { getBotTranslations, type BotLocale } from './i18n'
|
||||
import { resolveReplyLocale } from './bot-locale'
|
||||
import { buildBotStartDeepLink } from './telegram-deep-links'
|
||||
|
||||
export const REMINDER_UTILITY_GUIDED_CALLBACK = 'reminder_util:guided'
|
||||
export const REMINDER_UTILITY_TEMPLATE_CALLBACK = 'reminder_util:template'
|
||||
@@ -358,10 +359,13 @@ async function resolveReminderContext(
|
||||
|
||||
export function buildUtilitiesReminderReplyMarkup(
|
||||
locale: BotLocale,
|
||||
options?: {
|
||||
miniAppUrl?: string
|
||||
botUsername?: string
|
||||
}
|
||||
): InlineKeyboardMarkup {
|
||||
const t = getBotTranslations(locale).reminders
|
||||
const dashboardUrl = miniAppUrl?.trim()
|
||||
const dashboardUrl = buildBotStartDeepLink(options?.botUsername, 'dashboard')
|
||||
|
||||
return {
|
||||
inline_keyboard: [
|
||||
@@ -380,10 +384,8 @@ export function buildUtilitiesReminderReplyMarkup(
|
||||
[
|
||||
{
|
||||
text: t.openDashboardButton,
|
||||
web_app: {
|
||||
url: dashboardUrl
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
: [])
|
||||
|
||||
13
apps/bot/src/telegram-deep-links.ts
Normal file
13
apps/bot/src/telegram-deep-links.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function buildBotStartDeepLink(
|
||||
botUsername: string | undefined,
|
||||
payload: string
|
||||
): string | null {
|
||||
const normalizedBotUsername = botUsername?.trim()
|
||||
const normalizedPayload = payload.trim()
|
||||
|
||||
if (!normalizedBotUsername || !normalizedPayload) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `https://t.me/${normalizedBotUsername}?start=${encodeURIComponent(normalizedPayload)}`
|
||||
}
|
||||
@@ -2437,7 +2437,7 @@ function App() {
|
||||
</div>
|
||||
<Show when={readySession()?.mode === 'live'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="secondary"
|
||||
class="app-context-row__action"
|
||||
onClick={() => setProfileEditorOpen(true)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user