mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
Standardize naming across CLI and server components: - CLI: serverUrl → apiUrl, HAPI_SERVER_URL → HAPI_API_URL - Server: webapp* → listen*, miniAppUrl → publicUrl, WEBAPP_* → HAPI_LISTEN_*, WEBAPP_URL → HAPI_PUBLIC_URL - Rename serverUrlInit.ts → apiUrlInit.ts with updated logic for backward compatibility - Update all imports, function calls, and documentation accordingly
253 lines
7.3 KiB
TypeScript
253 lines
7.3 KiB
TypeScript
/**
|
|
* Telegram Bot for HAPI
|
|
*
|
|
* Simplified bot that only handles notifications (permission requests and ready events).
|
|
* All interactive features are handled by the Telegram Mini App.
|
|
*/
|
|
|
|
import { Bot, Context, InlineKeyboard } from 'grammy'
|
|
import { SyncEngine, Session } from '../sync/syncEngine'
|
|
import { handleCallback, CallbackContext } from './callbacks'
|
|
import { formatSessionNotification, createNotificationKeyboard } from './sessionView'
|
|
import { getAgentName } from '../notifications/sessionInfo'
|
|
import type { NotificationChannel } from '../notifications/notificationTypes'
|
|
import type { Store } from '../store'
|
|
|
|
export interface BotContext extends Context {
|
|
// Extended context for future use
|
|
}
|
|
|
|
export interface HappyBotConfig {
|
|
syncEngine: SyncEngine
|
|
botToken: string
|
|
publicUrl: string
|
|
store: Store
|
|
}
|
|
|
|
/**
|
|
* HAPI Telegram Bot - Notification-only mode
|
|
*/
|
|
export class HappyBot implements NotificationChannel {
|
|
private bot: Bot<BotContext>
|
|
private syncEngine: SyncEngine | null = null
|
|
private isRunning = false
|
|
private readonly publicUrl: string
|
|
private readonly store: Store
|
|
|
|
constructor(config: HappyBotConfig) {
|
|
this.syncEngine = config.syncEngine
|
|
this.publicUrl = config.publicUrl
|
|
this.store = config.store
|
|
|
|
this.bot = new Bot<BotContext>(config.botToken)
|
|
this.setupMiddleware()
|
|
this.setupCommands()
|
|
this.setupCallbacks()
|
|
|
|
if (this.syncEngine) {
|
|
this.setSyncEngine(this.syncEngine)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the sync engine reference (after auth)
|
|
*/
|
|
setSyncEngine(engine: SyncEngine): void {
|
|
this.syncEngine = engine
|
|
}
|
|
|
|
/**
|
|
* Get the underlying bot instance
|
|
*/
|
|
getBot(): Bot<BotContext> {
|
|
return this.bot
|
|
}
|
|
|
|
/**
|
|
* Start the bot
|
|
*/
|
|
async start(): Promise<void> {
|
|
if (this.isRunning) return
|
|
|
|
console.log('[HAPIBot] Starting Telegram bot...')
|
|
this.isRunning = true
|
|
|
|
// Start polling
|
|
this.bot.start({
|
|
onStart: (botInfo) => {
|
|
console.log(`[HAPIBot] Bot @${botInfo.username} started`)
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Stop the bot
|
|
*/
|
|
async stop(): Promise<void> {
|
|
if (!this.isRunning) return
|
|
|
|
console.log('[HAPIBot] Stopping Telegram bot...')
|
|
|
|
await this.bot.stop()
|
|
this.isRunning = false
|
|
}
|
|
|
|
/**
|
|
* Setup middleware
|
|
*/
|
|
private setupMiddleware(): void {
|
|
// Error handling middleware
|
|
this.bot.catch((err) => {
|
|
console.error('[HAPIBot] Error:', err.message)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Setup command handlers
|
|
*/
|
|
private setupCommands(): void {
|
|
// /app - Open Telegram Mini App (primary entry point)
|
|
this.bot.command('app', async (ctx) => {
|
|
const keyboard = new InlineKeyboard().webApp('Open App', this.publicUrl)
|
|
await ctx.reply('Open HAPI Mini App:', { reply_markup: keyboard })
|
|
})
|
|
|
|
// /start - Simple welcome with Mini App link
|
|
this.bot.command('start', async (ctx) => {
|
|
const keyboard = new InlineKeyboard().webApp('Open App', this.publicUrl)
|
|
await ctx.reply(
|
|
'Welcome to HAPI Bot!\n\n' +
|
|
'Use the Mini App for full session management.',
|
|
{ reply_markup: keyboard }
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Setup callback query handlers for notification buttons
|
|
*/
|
|
private setupCallbacks(): void {
|
|
this.bot.on('callback_query:data', async (ctx) => {
|
|
if (!this.syncEngine) {
|
|
await ctx.answerCallbackQuery('Not connected')
|
|
return
|
|
}
|
|
|
|
const namespace = this.getNamespaceForChatId(ctx.from?.id ?? null)
|
|
if (!namespace) {
|
|
await ctx.answerCallbackQuery('Telegram account is not bound')
|
|
return
|
|
}
|
|
|
|
const data = ctx.callbackQuery.data
|
|
|
|
const callbackContext: CallbackContext = {
|
|
syncEngine: this.syncEngine,
|
|
namespace,
|
|
answerCallback: async (text?: string) => {
|
|
await ctx.answerCallbackQuery(text)
|
|
},
|
|
editMessage: async (text, keyboard) => {
|
|
await ctx.editMessageText(text, {
|
|
reply_markup: keyboard
|
|
})
|
|
}
|
|
}
|
|
|
|
await handleCallback(data, callbackContext)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Get bound Telegram chat IDs from storage.
|
|
*/
|
|
private getBoundChatIds(namespace: string): number[] {
|
|
const users = this.store.users.getUsersByPlatformAndNamespace('telegram', namespace)
|
|
const ids = new Set<number>()
|
|
for (const user of users) {
|
|
const chatId = Number(user.platformUserId)
|
|
if (Number.isFinite(chatId)) {
|
|
ids.add(chatId)
|
|
}
|
|
}
|
|
return Array.from(ids)
|
|
}
|
|
|
|
private getNamespaceForChatId(chatId: number | null | undefined): string | null {
|
|
if (!chatId) {
|
|
return null
|
|
}
|
|
const stored = this.store.users.getUser('telegram', String(chatId))
|
|
return stored?.namespace ?? null
|
|
}
|
|
|
|
/**
|
|
* Send a notification when agent is ready for input.
|
|
*/
|
|
async sendReady(session: Session): Promise<void> {
|
|
if (!session.active) {
|
|
return
|
|
}
|
|
|
|
const agentName = getAgentName(session)
|
|
const url = buildMiniAppDeepLink(this.publicUrl, `session_${session.id}`)
|
|
const keyboard = new InlineKeyboard()
|
|
.webApp('Open Session', url)
|
|
|
|
const chatIds = this.getBoundChatIds(session.namespace)
|
|
if (chatIds.length === 0) {
|
|
return
|
|
}
|
|
|
|
for (const chatId of chatIds) {
|
|
try {
|
|
await this.bot.api.sendMessage(
|
|
chatId,
|
|
`It's ready!\n\n${agentName} is waiting for your command`,
|
|
{ reply_markup: keyboard }
|
|
)
|
|
} catch (error) {
|
|
console.error(`[HAPIBot] Failed to send ready notification to chat ${chatId}:`, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send permission notification to all bound chats
|
|
*/
|
|
async sendPermissionRequest(session: Session): Promise<void> {
|
|
if (!session.active) {
|
|
return
|
|
}
|
|
|
|
const text = formatSessionNotification(session)
|
|
const keyboard = createNotificationKeyboard(session, this.publicUrl)
|
|
|
|
const chatIds = this.getBoundChatIds(session.namespace)
|
|
if (chatIds.length === 0) {
|
|
return
|
|
}
|
|
|
|
for (const chatId of chatIds) {
|
|
try {
|
|
await this.bot.api.sendMessage(chatId, text, {
|
|
reply_markup: keyboard
|
|
})
|
|
} catch (error) {
|
|
console.error(`[HAPIBot] Failed to send notification to chat ${chatId}:`, error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildMiniAppDeepLink(baseUrl: string, startParam: string): string {
|
|
try {
|
|
const url = new URL(baseUrl)
|
|
url.searchParams.set('startapp', startParam)
|
|
return url.toString()
|
|
} catch {
|
|
const separator = baseUrl.includes('?') ? '&' : '?'
|
|
return `${baseUrl}${separator}startapp=${encodeURIComponent(startParam)}`
|
|
}
|
|
}
|