From d3ea470d027813ecbe06e23459808091b7c5e013 Mon Sep 17 00:00:00 2001 From: lendry Date: Fri, 26 Jun 2026 11:18:07 +0300 Subject: [PATCH] fix mini app bot father --- apps/api-gateway/src/dto/bot.dto.ts | 3 + .../app/mini-apps/bot-create/content.tsx | 19 +- .../app/mini-apps/bot-create/page.tsx | 24 ++- .../app/mini-apps/bot-father/content.tsx | 171 ++++++++++++++++++ .../app/mini-apps/bot-father/page.tsx | 20 ++ .../app/mini-apps/bot-manage/content.tsx | 30 ++- .../app/mini-apps/bot-manage/page.tsx | 36 +++- .../components/family/family-group-view.tsx | 18 +- .../components/family/mini-family-chat.tsx | 5 +- .../src/domain/auth-grpc.controller.ts | 8 +- .../src/domain/bot/bot-father.constants.ts | 10 +- .../src/domain/bot/bot-menu-button.util.ts | 8 +- 12 files changed, 313 insertions(+), 39 deletions(-) create mode 100644 apps/frontend/app/mini-apps/bot-father/content.tsx create mode 100644 apps/frontend/app/mini-apps/bot-father/page.tsx diff --git a/apps/api-gateway/src/dto/bot.dto.ts b/apps/api-gateway/src/dto/bot.dto.ts index eac3dd1..c068209 100644 --- a/apps/api-gateway/src/dto/bot.dto.ts +++ b/apps/api-gateway/src/dto/bot.dto.ts @@ -1,3 +1,4 @@ +import { Type } from 'class-transformer'; import { IsBoolean, IsInt, IsOptional, IsString, IsUrl, Max, Min, MinLength } from 'class-validator'; export class CreateBotDto { @@ -65,11 +66,13 @@ export class ListAdminBotsQueryDto { search?: string; @IsOptional() + @Type(() => Number) @IsInt() @Min(1) page?: number; @IsOptional() + @Type(() => Number) @IsInt() @Min(1) @Max(100) diff --git a/apps/frontend/app/mini-apps/bot-create/content.tsx b/apps/frontend/app/mini-apps/bot-create/content.tsx index f9d1e3b..38dbe98 100644 --- a/apps/frontend/app/mini-apps/bot-create/content.tsx +++ b/apps/frontend/app/mini-apps/bot-create/content.tsx @@ -1,7 +1,7 @@ 'use client'; import { useMemo, useState } from 'react'; -import { Check, Copy, Loader2, Bot } from 'lucide-react'; +import { Check, Copy, Loader2, Bot, ArrowLeft } from 'lucide-react'; import { useToast } from '@/components/id/toast-provider'; import { usePublicSettings } from '@/components/id/public-settings-provider'; import { Button } from '@/components/ui/button'; @@ -12,7 +12,12 @@ import { cn } from '@/lib/utils'; type Step = 'name' | 'username' | 'done'; -export function BotCreateMiniAppContent() { +interface BotCreateMiniAppContentProps { + onBack?: () => void; + onCreated?: (botId: string) => void; +} + +export function BotCreateMiniAppContent({ onBack, onCreated }: BotCreateMiniAppContentProps = {}) { const { projectName } = usePublicSettings(); const { effectiveToken, canUse, authReady, waitingForBridge, isLoading } = useMiniAppAuth(); const { showToast } = useToast(); @@ -84,6 +89,11 @@ export function BotCreateMiniAppContent() {
+ {onBack ? ( + + ) : null}
@@ -184,6 +194,11 @@ export function BotCreateMiniAppContent() { > Создать ещё одного бота + {onCreated && createdBot.botId ? ( + + ) : null}
) : null}
diff --git a/apps/frontend/app/mini-apps/bot-create/page.tsx b/apps/frontend/app/mini-apps/bot-create/page.tsx index a0b20ed..1a9e15f 100644 --- a/apps/frontend/app/mini-apps/bot-create/page.tsx +++ b/apps/frontend/app/mini-apps/bot-create/page.tsx @@ -1,16 +1,20 @@ 'use client'; -import { Suspense } from 'react'; -import { BotCreateMiniAppContent } from './content'; +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { Loader2 } from 'lucide-react'; + +export default function BotCreateLegacyRedirectPage() { + const router = useRouter(); + + useEffect(() => { + router.replace('/mini-apps/bot-father?mode=create'); + }, [router]); -export default function BotCreateMiniAppPage() { return ( - Загрузка...
- } - > - - +
+ + Перенаправление... +
); } diff --git a/apps/frontend/app/mini-apps/bot-father/content.tsx b/apps/frontend/app/mini-apps/bot-father/content.tsx new file mode 100644 index 0000000..b08d9c7 --- /dev/null +++ b/apps/frontend/app/mini-apps/bot-father/content.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Bot, ChevronRight, Loader2, Plus } from 'lucide-react'; +import { usePublicSettings } from '@/components/id/public-settings-provider'; +import { Button } from '@/components/ui/button'; +import { BotCreateMiniAppContent } from '@/app/mini-apps/bot-create/content'; +import { BotManageMiniAppContent } from '@/app/mini-apps/bot-manage/content'; +import { useMiniAppAuth } from '@/hooks/use-mini-app-auth'; +import { fetchMyBots, getApiErrorMessage, type ManagedBot } from '@/lib/api'; +import { useToast } from '@/components/id/toast-provider'; + +type View = 'list' | 'create' | 'manage'; + +export function BotFatherMiniAppContent() { + const searchParams = useSearchParams(); + const router = useRouter(); + const { projectName } = usePublicSettings(); + const { showToast } = useToast(); + const { effectiveToken, canUse, authReady, waitingForBridge, isLoading } = useMiniAppAuth(); + + const initialBotId = searchParams.get('botId'); + const initialMode = searchParams.get('mode'); + + const [view, setView] = useState(() => { + if (initialBotId) return 'manage'; + if (initialMode === 'create') return 'create'; + return 'list'; + }); + const [selectedBotId, setSelectedBotId] = useState(initialBotId); + const [bots, setBots] = useState([]); + const [loadingBots, setLoadingBots] = useState(false); + + const syncUrl = useCallback( + (nextView: View, botId?: string | null) => { + const params = new URLSearchParams(); + if (nextView === 'create') params.set('mode', 'create'); + if (nextView === 'manage' && botId) params.set('botId', botId); + const query = params.toString(); + router.replace(query ? `/mini-apps/bot-father?${query}` : '/mini-apps/bot-father'); + }, + [router] + ); + + const loadBots = useCallback(async () => { + if (!effectiveToken) return; + setLoadingBots(true); + try { + const response = await fetchMyBots(effectiveToken); + setBots((response.bots ?? []).filter((bot) => !bot.isSystemBot)); + } catch (error) { + showToast(getApiErrorMessage(error, 'Не удалось загрузить список ботов') ?? 'Ошибка'); + } finally { + setLoadingBots(false); + } + }, [effectiveToken, showToast]); + + useEffect(() => { + if (!canUse) return; + if (view === 'list') void loadBots(); + }, [canUse, loadBots, view]); + + const selectedBot = useMemo( + () => bots.find((bot) => bot.id === selectedBotId) ?? null, + [bots, selectedBotId] + ); + + function openManage(botId: string) { + setSelectedBotId(botId); + setView('manage'); + syncUrl('manage', botId); + } + + function openCreate() { + setView('create'); + syncUrl('create'); + } + + function backToList() { + setSelectedBotId(null); + setView('list'); + syncUrl('list'); + void loadBots(); + } + + if ((isLoading || waitingForBridge || !authReady) && !effectiveToken) { + return ( +
+ + Загрузка... +
+ ); + } + + if (!canUse) { + return ( +
+ Войдите в {projectName}, чтобы управлять ботами +
+ ); + } + + if (view === 'create') { + return ( + openManage(botId)} + /> + ); + } + + if (view === 'manage' && selectedBotId) { + return ; + } + + return ( +
+
+
+
+
+ +
+
+

BotFather

+

Создание и настройка ваших ботов

+
+
+
+ + + +
+

Мои боты

+ {loadingBots ? ( +
+ + Загрузка... +
+ ) : bots.length === 0 ? ( +

У вас пока нет ботов

+ ) : ( +
+ {bots.map((bot) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/apps/frontend/app/mini-apps/bot-father/page.tsx b/apps/frontend/app/mini-apps/bot-father/page.tsx new file mode 100644 index 0000000..882b6d9 --- /dev/null +++ b/apps/frontend/app/mini-apps/bot-father/page.tsx @@ -0,0 +1,20 @@ +'use client'; + +import { Suspense } from 'react'; +import { Loader2 } from 'lucide-react'; +import { BotFatherMiniAppContent } from './content'; + +export default function BotFatherMiniAppPage() { + return ( + + + Загрузка... + + } + > + + + ); +} diff --git a/apps/frontend/app/mini-apps/bot-manage/content.tsx b/apps/frontend/app/mini-apps/bot-manage/content.tsx index 88c3759..ac4f927 100644 --- a/apps/frontend/app/mini-apps/bot-manage/content.tsx +++ b/apps/frontend/app/mini-apps/bot-manage/content.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'next/navigation'; -import { Copy, ImageIcon, LayoutGrid, Loader2, RefreshCw, TextQuote, UserCircle2 } from 'lucide-react'; +import { Copy, ImageIcon, LayoutGrid, Loader2, RefreshCw, TextQuote, UserCircle2, ArrowLeft } from 'lucide-react'; import { usePublicSettings } from '@/components/id/public-settings-provider'; import { useToast } from '@/components/id/toast-provider'; import { Button } from '@/components/ui/button'; @@ -75,9 +75,17 @@ function SectionCard({ ); } -export function BotManageMiniAppContent() { +export function BotManageMiniAppContent({ + botId: botIdProp, + onBack, + botHint +}: { + botId?: string; + onBack?: () => void; + botHint?: string; +} = {}) { const searchParams = useSearchParams(); - const botId = searchParams.get('botId') ?? ''; + const botId = botIdProp ?? searchParams.get('botId') ?? ''; const { projectName } = usePublicSettings(); const { effectiveToken, canUse: canUseSession, authReady, waitingForBridge, isLoading } = useMiniAppAuth(); const { showToast } = useToast(); @@ -239,8 +247,20 @@ export function BotManageMiniAppContent() {
-

Настройки бота

-

Интерактивное управление профилем — аналог команд BotFather

+
+ {onBack ? ( + + ) : null} +
+

Настройки бота

+

+ {botHint ? `@${botHint.replace(/_bot$/i, '')}_bot · ` : ''} + Полное управление как в BotFather +

+
+
{loading ? ( diff --git a/apps/frontend/app/mini-apps/bot-manage/page.tsx b/apps/frontend/app/mini-apps/bot-manage/page.tsx index 36fa2dd..e4d09cc 100644 --- a/apps/frontend/app/mini-apps/bot-manage/page.tsx +++ b/apps/frontend/app/mini-apps/bot-manage/page.tsx @@ -1,20 +1,40 @@ -import { Suspense } from 'react'; -import { Loader2 } from 'lucide-react'; -import { BotManageMiniAppContent } from './content'; +'use client'; + +import { useEffect } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { Loader2 } from 'lucide-react'; +import { Suspense } from 'react'; + +function BotManageLegacyRedirectContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const botId = searchParams.get('botId'); + + useEffect(() => { + const target = botId + ? `/mini-apps/bot-father?botId=${encodeURIComponent(botId)}` + : '/mini-apps/bot-father'; + router.replace(target); + }, [botId, router]); -function BotManageFallback() { return (
- Загрузка Mini App... + Перенаправление...
); } -export default function BotManageMiniAppPage() { +export default function BotManageLegacyRedirectPage() { return ( - }> - + + +
+ } + > + ); } diff --git a/apps/frontend/components/family/family-group-view.tsx b/apps/frontend/components/family/family-group-view.tsx index ff726df..8e54d1c 100644 --- a/apps/frontend/components/family/family-group-view.tsx +++ b/apps/frontend/components/family/family-group-view.tsx @@ -365,10 +365,17 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { const loadMessages = useCallback(async (roomId: string) => { if (!token || isPinLocked) return; const room = rooms.find((item) => item.id === roomId); - if (room?.type === 'BOT') return; - const response = await fetchChatMessages(roomId, token); - setMessages((response.messages ?? []).filter((message) => !message.isDeleted)); - }, [isPinLocked, rooms, token]); + if (!room || room.type === 'BOT') return; + try { + const response = await fetchChatMessages(roomId, token); + setMessages((response.messages ?? []).filter((message) => !message.isDeleted)); + } catch (error) { + const message = getApiErrorMessage(error, 'Не удалось загрузить сообщения'); + if (message && !message.includes('Bot API')) { + showToast(message); + } + } + }, [isPinLocked, rooms, showToast, token]); const loadPresence = useCallback(async () => { if (!token || isPinLocked) return; @@ -406,10 +413,11 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { useEffect(() => { if (!activeRoomId || activeRoomIsBot) return; + if (!rooms.some((room) => room.id === activeRoomId)) return; void loadMessages(activeRoomId); setEditingMessageId(null); setDraft(''); - }, [activeRoomId, activeRoomIsBot, loadMessages]); + }, [activeRoomId, activeRoomIsBot, loadMessages, rooms]); useEffect(() => { if (!activeRoomId || !token || !messages.length) return; diff --git a/apps/frontend/components/family/mini-family-chat.tsx b/apps/frontend/components/family/mini-family-chat.tsx index 8fe8fd8..f1ec8a4 100644 --- a/apps/frontend/components/family/mini-family-chat.tsx +++ b/apps/frontend/components/family/mini-family-chat.tsx @@ -394,7 +394,10 @@ export function MiniFamilyChat() { exitSelectionMode(); showToast(ids.length === 1 ? 'Сообщение удалено' : 'Сообщения удалены'); } catch (error) { - if (activeRoom) { + if (view === 'bot' && activeBot) { + const response = await fetchBotChatMessages(activeBot.botRef, token); + setBotMessages(response.messages ?? []); + } else if (activeRoom && activeRoom.type !== 'BOT') { const response = await fetchChatMessages(activeRoom.id, token); setMessages((response.messages ?? []).filter((message) => !message.isDeleted)); } diff --git a/apps/sso-core/src/domain/auth-grpc.controller.ts b/apps/sso-core/src/domain/auth-grpc.controller.ts index b3afe4c..125a523 100644 --- a/apps/sso-core/src/domain/auth-grpc.controller.ts +++ b/apps/sso-core/src/domain/auth-grpc.controller.ts @@ -1160,7 +1160,13 @@ export class AuthGrpcController { @GrpcMethod('BotService', 'ListAllBots') listAllBots(command: { requesterId: string; isSuperAdmin?: boolean; search?: string; page?: number; limit?: number }) { - return this.botFather.listAllBots(command.requesterId, Boolean(command.isSuperAdmin), command.search, command.page, command.limit); + return this.botFather.listAllBots( + command.requesterId, + Boolean(command.isSuperAdmin), + command.search, + Number(command.page) || 1, + Number(command.limit) || 20 + ); } @GrpcMethod('BotService', 'SetBotActive') diff --git a/apps/sso-core/src/domain/bot/bot-father.constants.ts b/apps/sso-core/src/domain/bot/bot-father.constants.ts index ff86b03..7ab9100 100644 --- a/apps/sso-core/src/domain/bot/bot-father.constants.ts +++ b/apps/sso-core/src/domain/bot/bot-father.constants.ts @@ -9,15 +9,19 @@ export function resolvePublicFrontendUrl() { return (process.env.PUBLIC_FRONTEND_URL ?? 'http://localhost:3002').replace(/\/$/, ''); } +export function buildBotFatherWebAppUrl() { + return `${resolvePublicFrontendUrl()}/mini-apps/bot-father`; +} + export function buildBotManageWebAppUrl(botId: string) { - return `${resolvePublicFrontendUrl()}/mini-apps/bot-manage?botId=${botId}`; + return `${resolvePublicFrontendUrl()}/mini-apps/bot-father?botId=${encodeURIComponent(botId)}`; } export function buildBotCreateWebAppUrl() { - return `${resolvePublicFrontendUrl()}/mini-apps/bot-create`; + return `${buildBotFatherWebAppUrl()}?mode=create`; } export function isBotManageWebAppUrl(url: string | null | undefined) { if (!url?.trim()) return false; - return url.includes('/mini-apps/bot-manage'); + return url.includes('/mini-apps/bot-manage') || url.includes('/mini-apps/bot-father'); } diff --git a/apps/sso-core/src/domain/bot/bot-menu-button.util.ts b/apps/sso-core/src/domain/bot/bot-menu-button.util.ts index e69a2d2..98d1df0 100644 --- a/apps/sso-core/src/domain/bot/bot-menu-button.util.ts +++ b/apps/sso-core/src/domain/bot/bot-menu-button.util.ts @@ -1,4 +1,4 @@ -import { BOTFATHER_BOT_USERNAME, buildBotCreateWebAppUrl, isBotManageWebAppUrl } from './bot-father.constants'; +import { BOTFATHER_BOT_USERNAME, buildBotFatherWebAppUrl, isBotManageWebAppUrl } from './bot-father.constants'; export type TelegramMenuButtonDefault = { type: 'default' }; export type TelegramMenuButtonCommands = { type: 'commands' }; @@ -141,11 +141,11 @@ export function resolveComposerMenuButton(options: { } if (options.bot.isSystemBot || options.bot.username === BOTFATHER_BOT_USERNAME) { - const url = buildBotCreateWebAppUrl(); + const url = buildBotFatherWebAppUrl(); return { - menuButton: { type: 'web_app', text: 'Создать бота', web_app: { url } }, + menuButton: { type: 'web_app', text: 'BotFather', web_app: { url } }, composerWebAppUrl: url, - buttonText: 'Создать бота' + buttonText: 'BotFather' }; }