import { Controller, Post, Get, Delete, Body, Param, Headers, UseGuards, Request, Logger, Patch, Res, } from '@nestjs/common'; import { Response } from 'express'; import { FeishuService } from './feishu.service'; import { FeishuAssessmentService } from './services/feishu-assessment.service'; import { CreateFeishuBotDto } from './dto/create-bot.dto'; import { CombinedAuthGuard } from '../auth/combined-auth.guard'; import { Public } from '../auth/public.decorator'; import * as fs from 'fs'; import * as path from 'path'; @Controller('feishu') export class FeishuController { private readonly logger = new Logger(FeishuController.name); constructor( private readonly feishuService: FeishuService, private readonly feishuAssessmentService: FeishuAssessmentService, ) {} // ─── Bot Management Endpoints (JWT-protected) ───────────────────────────── /** GET /feishu/bots - List user's bots, masking sensitive fields */ @Get('bots') @UseGuards(CombinedAuthGuard) async listBots(@Request() req) { const bots = await this.feishuService.getUserBots( req.user.id, req.user.tenantId, ); return bots.map((bot) => ({ id: bot.id, appId: bot.appId, botName: bot.botName, enabled: bot.enabled, isDefault: bot.isDefault, webhookUrl: `/api/feishu/webhook/${bot.appId}`, createdAt: bot.createdAt, })); } /** POST /feishu/bots - Create or update a bot */ @Post('bots') @UseGuards(CombinedAuthGuard) async createBot(@Request() req, @Body() dto: CreateFeishuBotDto) { const bot = await this.feishuService.createBot( req.user.id, req.user.tenantId, dto, ); return { id: bot.id, appId: bot.appId, botName: bot.botName, enabled: bot.enabled, webhookUrl: `/api/feishu/webhook/${bot.appId}`, }; } /** PATCH /feishu/bots/:id/toggle - Enable or disable a bot */ @Patch('bots/:id/toggle') @UseGuards(CombinedAuthGuard) async toggleBot( @Request() req, @Param('id') botId: string, @Body() body: { enabled: boolean }, ) { const bot = await this.feishuService.setBotEnabled(botId, body.enabled); return { id: bot.id, enabled: bot.enabled }; } /** DELETE /feishu/bots/:id - Delete a bot */ @Delete('bots/:id') @UseGuards(CombinedAuthGuard) async deleteBot(@Request() req, @Param('id') botId: string) { await this.feishuService.deleteBot(req.user.id, botId); return { success: true }; } // ─── WebSocket Management Endpoints ──────────────────────────────────── /** POST /feishu/bots/:id/ws/connect - Start WebSocket connection */ @Post('bots/:id/ws/connect') @UseGuards(CombinedAuthGuard) async connectWs(@Request() req, @Param('id') botId: string) { const bot = await this.feishuService.getBotById(botId); if (!bot || bot.userId !== req.user.id) { return { success: false, error: 'Bot not found' }; } try { await this.feishuService.startWsConnection(botId); return { success: true, botId, status: 'connecting' }; } catch (error: any) { return { success: false, botId, error: error?.message || 'Failed to connect', }; } } /** POST /feishu/bots/:id/ws/disconnect - Stop WebSocket connection */ @Post('bots/:id/ws/disconnect') @UseGuards(CombinedAuthGuard) async disconnectWs(@Request() req, @Param('id') botId: string) { const bot = await this.feishuService.getBotById(botId); if (!bot || bot.userId !== req.user.id) { return { success: false, error: 'Bot not found' }; } try { await this.feishuService.stopWsConnection(botId); return { success: true, botId, status: 'disconnected' }; } catch (error: any) { return { success: false, botId, error: error?.message || 'Failed to disconnect', }; } } /** GET /feishu/bots/:id/ws/status - Get connection status */ @Get('bots/:id/ws/status') @UseGuards(CombinedAuthGuard) async getWsStatus(@Request() req, @Param('id') botId: string) { const bot = await this.feishuService.getBotById(botId); if (!bot || bot.userId !== req.user.id) { return { success: false, error: 'Bot not found' }; } const status = await this.feishuService.getWsStatus(botId); if (!status) { return { botId, state: 'disconnected' }; } return { botId: status.botId, state: status.state, connectedAt: status.connectedAt?.toISOString(), lastHeartbeat: status.lastHeartbeat?.toISOString(), error: status.error, }; } /** GET /feishu/ws/status - Get all active WS connection statuses */ @Get('ws/status') @UseGuards(CombinedAuthGuard) async getAllWsStatus() { const statuses = await this.feishuService.getAllWsStatuses(); return { connections: statuses.map((s) => ({ botId: s.botId, state: s.state, connectedAt: s.connectedAt?.toISOString(), lastHeartbeat: s.lastHeartbeat?.toISOString(), error: s.error, })), }; } // ─── Feishu Webhook Endpoint (Public) ──────────────────────────────────── @Get('webhook/:appId') @Post('webhook/:appId') @Public() async handleWebhook( @Param('appId') appId: string, @Body() body: any, @Headers() headers: any, @Request() req: any, @Res() res: Response, ) { const logEntry = `\n[${new Date().toISOString()}] ${req.method} /api/feishu/webhook/${appId}\nHeaders: ${JSON.stringify(headers)}\nBody: ${JSON.stringify(body)}\n`; fs.appendFileSync('feishu_webhook.log', logEntry); this.logger.log( `Incoming Feishu webhook [${req.method}] for appId: ${appId}`, ); // GET request for simple connection test if (req.method === 'GET') { return res.status(200).json({ status: 'ok', message: 'AuraK Feishu Webhook is active.', appId, timestamp: new Date().toISOString(), }); } // Step 1: URL verification handshake const challenge = body?.challenge || body?.event?.challenge; if (body?.type === 'url_verification' || challenge) { this.logger.log( `URL verification active for appId: ${appId}, challenge: ${challenge}`, ); return res.status(200).json({ challenge }); } // Step 2: Return 200 immediately for all other events res.status(200).json({ success: true }); // Step 3: Process the event asynchronously if (body?.type === 'event_callback' || body?.header?.event_type) { setImmediate(() => this._processEvent(appId, body).catch((e) => this.logger.error('Failed to process Feishu event async', e), ), ); } } // ─── Private Event Processor ───────────────────────────────────────────── private async _processEvent(appId: string, body: any): Promise { const { type, event, header } = body; if (type !== 'event_callback') return; const eventType = header?.event_type || body.event_type; const eventId = header?.event_id; this.logger.log( `Processing Feishu event [${eventId}]: ${eventType} for appId: ${appId}`, ); const bot = await this.feishuService.getBotByAppId(appId); if (!bot || !bot.enabled) { this.logger.warn(`Bot not found or disabled for appId: ${appId}`); return; } switch (eventType) { case 'im.message.receive_v1': case 'im.message.p2p_msg_received': case 'im.message.group_at_msg_received': await this._handleMessage(bot, event); break; default: this.logger.log(`Unhandled event type: ${eventType}`); } } /** * Parse incoming IM message and route to chatService via FeishuService. * Implements Chunk 5 integration. */ private async _handleMessage(bot: any, event: any): Promise { const message = event?.message; if (!message) return; const messageId = message.message_id; const openId = event?.sender?.sender_id?.open_id; if (!openId) { this.logger.warn('No sender open_id found in Feishu event'); return; } // Parse text content let userText = ''; try { const content = JSON.parse(message.content || '{}'); userText = content.text || ''; } catch { this.logger.warn('Failed to parse Feishu message content'); return; } if (!userText.trim()) return; try { // Centralized routing via FeishuService await this.feishuService.handleIncomingMessage( bot, openId, messageId, userText, ); } catch (error) { this.logger.error('Message handling failed', error); } } }