From 087d34c552325f6f61d5c5926a4aef2680c2a8e0 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:29:36 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(matrix-bots):=20enhance=20stat?= =?UTF-8?q?s=20and=20todo=20bots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add credit commands to todo-bot - Enhance stats-bot with improved metrics - Add Umami analytics improvements --- .../src/bot/matrix.service.ts | 36 ++++++-- .../src/umami/umami.service.ts | 41 +++++++-- .../matrix-todo-bot/src/bot/bot.module.ts | 2 + .../matrix-todo-bot/src/bot/matrix.service.ts | 91 ++++++++++++++++++- 4 files changed, 151 insertions(+), 19 deletions(-) diff --git a/services/matrix-stats-bot/src/bot/matrix.service.ts b/services/matrix-stats-bot/src/bot/matrix.service.ts index 3a182c228..e40e64077 100644 --- a/services/matrix-stats-bot/src/bot/matrix.service.ts +++ b/services/matrix-stats-bot/src/bot/matrix.service.ts @@ -155,25 +155,45 @@ Daten von Umami Analytics (self-hosted).`; private async sendStats(roomId: string) { await this.sendMessage(roomId, 'πŸ“Š Lade Statistiken...'); - const report = await this.analyticsService.generateStatsOverview(); - await this.sendMessage(roomId, report); + try { + const report = await this.analyticsService.generateStatsOverview(); + await this.sendMessage(roomId, report); + } catch (error) { + this.logger.error('Failed to generate stats overview:', error); + await this.sendMessage(roomId, `❌ Fehler beim Laden der Statistiken: ${error instanceof Error ? error.message : String(error)}`); + } } private async sendToday(roomId: string) { await this.sendMessage(roomId, 'πŸ“Š Lade heutige Statistiken...'); - const report = await this.analyticsService.generateDailyReport(); - await this.sendMessage(roomId, report); + try { + const report = await this.analyticsService.generateDailyReport(); + await this.sendMessage(roomId, report); + } catch (error) { + this.logger.error('Failed to generate daily report:', error); + await this.sendMessage(roomId, `❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`); + } } private async sendWeek(roomId: string) { await this.sendMessage(roomId, 'πŸ“Š Lade Wochenstatistiken...'); - const report = await this.analyticsService.generateWeeklyReport(); - await this.sendMessage(roomId, report); + try { + const report = await this.analyticsService.generateWeeklyReport(); + await this.sendMessage(roomId, report); + } catch (error) { + this.logger.error('Failed to generate weekly report:', error); + await this.sendMessage(roomId, `❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`); + } } private async sendRealtime(roomId: string) { - const report = await this.analyticsService.generateRealtimeReport(); - await this.sendMessage(roomId, report); + try { + const report = await this.analyticsService.generateRealtimeReport(); + await this.sendMessage(roomId, report); + } catch (error) { + this.logger.error('Failed to generate realtime report:', error); + await this.sendMessage(roomId, `❌ Fehler beim Laden: ${error instanceof Error ? error.message : String(error)}`); + } } private async sendUsers(roomId: string) { diff --git a/services/matrix-stats-bot/src/umami/umami.service.ts b/services/matrix-stats-bot/src/umami/umami.service.ts index 44b61406e..a16c46b23 100644 --- a/services/matrix-stats-bot/src/umami/umami.service.ts +++ b/services/matrix-stats-bot/src/umami/umami.service.ts @@ -30,11 +30,18 @@ export class UmamiService implements OnModuleInit { } async onModuleInit() { - await this.authenticate(); + try { + await this.authenticate(); + } catch (error) { + this.logger.warn('Initial Umami auth failed, will retry on first request'); + } } private async authenticate(): Promise { try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + const response = await fetch(`${this.apiUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -42,10 +49,13 @@ export class UmamiService implements OnModuleInit { username: this.username, password: this.password, }), + signal: controller.signal, }); + clearTimeout(timeoutId); + if (!response.ok) { - throw new Error(`Auth failed: ${response.status}`); + throw new Error(`Umami auth failed: ${response.status}`); } const data = await response.json(); @@ -53,34 +63,51 @@ export class UmamiService implements OnModuleInit { this.logger.log('Umami authenticated successfully'); } catch (error) { this.logger.error('Failed to authenticate with Umami:', error); + this.accessToken = null; + throw error instanceof Error ? error : new Error('Umami authentication failed'); } } - private async request(endpoint: string): Promise { + private async request(endpoint: string, retryCount = 0): Promise { if (!this.accessToken) { await this.authenticate(); } + if (!this.accessToken) { + throw new Error('Umami nicht authentifiziert - prΓΌfe UMAMI_API_URL und Credentials'); + } + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout + const response = await fetch(`${this.apiUrl}${endpoint}`, { headers: { Authorization: `Bearer ${this.accessToken}`, }, + signal: controller.signal, }); - if (response.status === 401) { + clearTimeout(timeoutId); + + if (response.status === 401 && retryCount < 1) { + this.accessToken = null; await this.authenticate(); - return this.request(endpoint); + return this.request(endpoint, retryCount + 1); } if (!response.ok) { - throw new Error(`Request failed: ${response.status}`); + throw new Error(`Umami API Fehler: ${response.status}`); } return response.json(); } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + this.logger.error(`Umami request timeout: ${endpoint}`); + throw new Error('Umami API Timeout - Server nicht erreichbar?'); + } this.logger.error(`Umami request failed: ${endpoint}`, error); - return null; + throw error; } } diff --git a/services/matrix-todo-bot/src/bot/bot.module.ts b/services/matrix-todo-bot/src/bot/bot.module.ts index 45bac5931..dbe4280e0 100644 --- a/services/matrix-todo-bot/src/bot/bot.module.ts +++ b/services/matrix-todo-bot/src/bot/bot.module.ts @@ -5,6 +5,7 @@ import { TranscriptionModule, SessionModule, CreditModule, + GiftModule, TodoApiService, I18nModule, } from '@manacore/bot-services'; @@ -25,6 +26,7 @@ const todoApiServiceProvider = { TranscriptionModule.forRoot(), SessionModule.forRoot({ storageMode: 'redis' }), CreditModule.forRoot(), + GiftModule.forRoot(), I18nModule.forRoot(), ], providers: [MatrixService, todoApiServiceProvider], diff --git a/services/matrix-todo-bot/src/bot/matrix.service.ts b/services/matrix-todo-bot/src/bot/matrix.service.ts index 5bc1ef656..0b894b64e 100644 --- a/services/matrix-todo-bot/src/bot/matrix.service.ts +++ b/services/matrix-todo-bot/src/bot/matrix.service.ts @@ -6,11 +6,16 @@ import { MatrixRoomEvent, KeywordCommandDetector, COMMON_KEYWORDS, + handleCreditCommand, + handleGiftCommand, + type CreditCommandsHost, + type GiftCommandsHost, } from '@manacore/matrix-bot-common'; import { TranscriptionService, SessionService, CreditService, + GiftService, TodoApiService, Task as ApiTask, I18nService, @@ -26,7 +31,12 @@ const TASK_CREATE_CREDITS = 0.02; type Task = ApiTask; @Injectable() -export class MatrixService extends BaseMatrixService { +export class MatrixService extends BaseMatrixService implements CreditCommandsHost, GiftCommandsHost { + // Expose services for credit and gift commands mixins + public creditService: CreditService; + public giftService: GiftService; + public i18nService: I18nService; + public sessionService: SessionService; private readonly keywordDetector = new KeywordCommandDetector( [ ...COMMON_KEYWORDS, @@ -55,6 +65,9 @@ export class MatrixService extends BaseMatrixService { { keywords: ['login', 'anmelden'], command: 'login' }, { keywords: ['logout', 'abmelden'], command: 'logout' }, { keywords: ['sprache', 'language', 'lang'], command: 'language' }, + { keywords: ['credits', 'guthaben', 'kontostand'], command: 'credits' }, + { keywords: ['packages', 'pakete', 'preise'], command: 'packages' }, + { keywords: ['kaufen', 'buy'], command: 'buy' }, ], { partialMatch: true } ); @@ -63,13 +76,59 @@ export class MatrixService extends BaseMatrixService { configService: ConfigService, private todoApiService: TodoApiService, private transcriptionService: TranscriptionService, - private sessionService: SessionService, - private creditService: CreditService, - private i18nService: I18nService + sessionService: SessionService, + creditService: CreditService, + giftService: GiftService, + i18nService: I18nService ) { super(configService); + // Assign to public properties for credit and gift commands mixins + this.sessionService = sessionService; + this.creditService = creditService; + this.giftService = giftService; + this.i18nService = i18nService; } + // ============================================================================ + // CreditCommandsHost interface implementation + // ============================================================================ + + /** + * Send a credit message (delegates to protected sendMessage) + */ + async sendCreditMessage(roomId: string, message: string): Promise { + await this.sendMessage(roomId, message); + } + + /** + * Send a credit reply (delegates to protected sendReply) + */ + async sendCreditReply(roomId: string, event: MatrixRoomEvent, message: string): Promise { + await this.sendReply(roomId, event, message); + } + + // ============================================================================ + // GiftCommandsHost interface implementation + // ============================================================================ + + /** + * Send a gift message (delegates to protected sendMessage) + */ + async sendGiftMessage(roomId: string, message: string): Promise { + await this.sendMessage(roomId, message); + } + + /** + * Send a gift reply (delegates to protected sendReply) + */ + async sendGiftReply(roomId: string, event: MatrixRoomEvent, message: string): Promise { + await this.sendReply(roomId, event, message); + } + + // ============================================================================ + // Private helpers + // ============================================================================ + /** * Check if user is logged in and has a valid token for API access */ @@ -164,6 +223,20 @@ export class MatrixService extends BaseMatrixService { 'language', 'sprache', 'lang', + // Credit commands + 'credits', + 'guthaben', + 'packages', + 'pakete', + 'buy', + 'kaufen', + // Gift commands + 'geschenk', + 'gift', + 'einloesen', + 'redeem', + 'meine-geschenke', + 'my-gifts', ]; protected async handleTextMessage( @@ -333,6 +406,16 @@ export class MatrixService extends BaseMatrixService { command: string, args: string ) { + // Handle credit commands first (credits, packages, buy) + if (await handleCreditCommand(this, roomId, event, userId, command, args)) { + return; + } + + // Handle gift commands (geschenk, einloesen, meine-geschenke) + if (await handleGiftCommand(this, roomId, event, userId, command, args)) { + return; + } + switch (command) { case 'help': case 'hilfe':