feat(matrix-bots): enhance stats and todo bots

- Add credit commands to todo-bot
- Enhance stats-bot with improved metrics
- Add Umami analytics improvements
This commit is contained in:
Till-JS 2026-02-13 23:29:36 +01:00
parent e8c3b97f8f
commit 087d34c552
4 changed files with 151 additions and 19 deletions

View file

@ -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) {

View file

@ -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<void> {
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<T>(endpoint: string): Promise<T | null> {
private async request<T>(endpoint: string, retryCount = 0): Promise<T | null> {
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;
}
}

View file

@ -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],

View file

@ -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<void> {
await this.sendMessage(roomId, message);
}
/**
* Send a credit reply (delegates to protected sendReply)
*/
async sendCreditReply(roomId: string, event: MatrixRoomEvent, message: string): Promise<void> {
await this.sendReply(roomId, event, message);
}
// ============================================================================
// GiftCommandsHost interface implementation
// ============================================================================
/**
* Send a gift message (delegates to protected sendMessage)
*/
async sendGiftMessage(roomId: string, message: string): Promise<void> {
await this.sendMessage(roomId, message);
}
/**
* Send a gift reply (delegates to protected sendReply)
*/
async sendGiftReply(roomId: string, event: MatrixRoomEvent, message: string): Promise<void> {
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':