import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, UserListMapper, } from '@manacore/matrix-bot-common'; import { SkilltreeService, Skill, SkillBranch } from '../skilltree/skilltree.service'; import { SessionService } from '@manacore/bot-services'; import { HELP_MESSAGE } from '../config/configuration'; @Injectable() export class MatrixService extends BaseMatrixService { // User list mapper for number-based reference private skillsMapper = new UserListMapper(); // Branch name mappings (German/English) private readonly branchMappings: Record = { intellect: 'intellect', wissen: 'intellect', gehirn: 'intellect', body: 'body', koerper: 'body', fitness: 'body', sport: 'body', creativity: 'creativity', kreativ: 'creativity', kreativitaet: 'creativity', kunst: 'creativity', social: 'social', sozial: 'social', practical: 'practical', praktisch: 'practical', handwerk: 'practical', mindset: 'mindset', achtsamkeit: 'mindset', mental: 'mindset', custom: 'custom', eigene: 'custom', }; constructor( configService: ConfigService, private skilltreeService: SkilltreeService, private sessionService: SessionService ) { super(configService); } protected getConfig(): MatrixBotConfig { return { homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', accessToken: this.configService.get('matrix.accessToken') || '', storagePath: this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } protected async handleTextMessage( roomId: string, event: MatrixRoomEvent, body: string ): Promise { if (!body.startsWith('!')) return; const sender = event.sender; const parts = body.slice(1).split(/\s+/); const command = parts[0].toLowerCase(); const args = parts.slice(1); const argString = args.join(' '); try { switch (command) { case 'help': case 'hilfe': await this.sendMessage(roomId, HELP_MESSAGE); break; case 'login': await this.handleLogin(roomId, sender, args); break; case 'logout': this.sessionService.logout(sender); await this.sendMessage(roomId, '

Erfolgreich abgemeldet.

'); break; case 'status': await this.handleStatus(roomId, sender); break; // Skill commands case 'skills': case 'liste': case 'faehigkeiten': await this.handleListSkills(roomId, sender, args[0]); break; case 'skill': case 'details': await this.handleSkillDetails(roomId, sender, args[0]); break; case 'neu': case 'new': case 'create': await this.handleCreateSkill(roomId, sender, argString); break; case 'loeschen': case 'delete': await this.handleDeleteSkill(roomId, sender, args[0]); break; // XP commands case 'xp': case 'punkte': await this.handleAddXp(roomId, sender, argString); break; // Stats commands case 'stats': case 'statistik': await this.handleStats(roomId, sender); break; // Activity commands case 'aktivitaeten': case 'activities': case 'verlauf': await this.handleActivities(roomId, sender, args[0]); break; default: await this.sendMessage( roomId, `

Unbekannter Befehl: ${command}. Nutze !help fuer Hilfe.

` ); } } catch (error) { this.logger.error(`Error handling command ${command}:`, error); await this.sendMessage(roomId, `

Fehler: ${(error as Error).message}

`); } } private requireAuth(sender: string): string { const token = this.sessionService.getToken(sender); if (!token) { throw new Error('Nicht angemeldet. Nutze !login email passwort'); } return token; } // Auth handlers private async handleLogin(roomId: string, sender: string, args: string[]) { if (args.length < 2) { await this.sendMessage(roomId, '

Verwendung: !login email passwort

'); return; } const [email, password] = args; const result = await this.sessionService.login(sender, email, password); if (result.success) { await this.sendMessage(roomId, `

Erfolgreich angemeldet als ${email}

`); } else { await this.sendMessage(roomId, `

Login fehlgeschlagen: ${result.error}

`); } } private async handleStatus(roomId: string, sender: string) { const backendOk = await this.skilltreeService.checkHealth(); const loggedIn = this.sessionService.isLoggedIn(sender); const sessions = this.sessionService.getSessionCount(); await this.sendMessage( roomId, `

Skilltree Bot Status

  • Backend: ${backendOk ? 'Online' : 'Offline'}
  • Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}
  • Aktive Sessions: ${sessions}
` ); } // Skill handlers private async handleListSkills(roomId: string, sender: string, branchFilter?: string) { const token = this.requireAuth(sender); let branch: string | undefined; if (branchFilter) { branch = this.branchMappings[branchFilter.toLowerCase()]; if (!branch) { await this.sendMessage( roomId, '

Unbekannter Branch. Verfuegbar: intellect, body, creativity, social, practical, mindset, custom

' ); return; } } const result = await this.skilltreeService.getSkills(token, branch); if (result.error) { await this.sendMessage(roomId, `

Fehler: ${result.error}

`); return; } const skills = result.data?.skills || []; this.skillsMapper.setList(sender, skills); if (skills.length === 0) { await this.sendMessage( roomId, '

Keine Skills vorhanden. Erstelle einen mit !neu Name | Branch

' ); return; } let html = '

Deine Skills

    '; for (const skill of skills) { const levelName = this.getLevelName(skill.level); const branchIcon = this.getBranchIcon(skill.branch); const progress = this.getProgressBar(skill.totalXp, skill.level); html += `
  1. ${branchIcon} ${skill.name} - Lvl ${skill.level} (${levelName}) ${progress}
  2. `; } html += '
'; html += '

Nutze !skill [nr] fuer Details oder !xp [nr] 50 Aktivitaet

'; await this.sendMessage(roomId, html); } private async handleSkillDetails(roomId: string, sender: string, numberStr: string) { const token = this.requireAuth(sender); const number = parseInt(numberStr, 10); const skill = this.skillsMapper.getByNumber(sender, number); if (!skill) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !skills

'); return; } const result = await this.skilltreeService.getSkill(token, skill.id); if (result.error) { await this.sendMessage(roomId, `

Fehler: ${result.error}

`); return; } const s = result.data!.skill; const levelName = this.getLevelName(s.level); const nextLevelXp = this.getNextLevelXp(s.level); const branchIcon = this.getBranchIcon(s.branch); let html = `

${branchIcon} ${s.name}

`; if (s.description) html += `

${s.description}

`; html += '
    '; html += `
  • Branch: ${this.translateBranch(s.branch)}
  • `; html += `
  • Level: ${s.level} (${levelName})
  • `; html += `
  • XP: ${s.totalXp.toLocaleString('de-DE')}`; if (nextLevelXp) html += ` / ${nextLevelXp.toLocaleString('de-DE')} (naechstes Level)`; html += '
  • '; html += `
  • Erstellt: ${new Date(s.createdAt).toLocaleDateString('de-DE')}
  • `; html += '
'; html += `

Nutze !xp ${numberStr} [xp] [aktivitaet] um XP hinzuzufuegen

`; await this.sendMessage(roomId, html); } private async handleCreateSkill(roomId: string, sender: string, input: string) { if (!input) { await this.sendMessage( roomId, '

Verwendung: !neu Name | Branch

Branches: intellect, body, creativity, social, practical, mindset, custom

' ); return; } const token = this.requireAuth(sender); const parts = input.split('|').map((s) => s.trim()); const name = parts[0]; const branchInput = parts[1]?.toLowerCase() || 'custom'; const branch = this.branchMappings[branchInput]; if (!branch) { await this.sendMessage( roomId, '

Unbekannter Branch. Verfuegbar: intellect, body, creativity, social, practical, mindset, custom

' ); return; } const description = parts[2]; const result = await this.skilltreeService.createSkill(token, name, branch, description); if (result.error) { await this.sendMessage(roomId, `

Fehler: ${result.error}

`); return; } this.skillsMapper.clearList(sender); const branchIcon = this.getBranchIcon(branch); await this.sendMessage( roomId, `

${branchIcon} Skill ${result.data!.skill.name} erstellt!

Nutze !skills und dann !xp [nr] [xp] [aktivitaet]

` ); } private async handleDeleteSkill(roomId: string, sender: string, numberStr: string) { const token = this.requireAuth(sender); const number = parseInt(numberStr, 10); const skill = this.skillsMapper.getByNumber(sender, number); if (!skill) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !skills

'); return; } const result = await this.skilltreeService.deleteSkill(token, skill.id); if (result.error) { await this.sendMessage(roomId, `

Fehler: ${result.error}

`); return; } this.skillsMapper.clearList(sender); await this.sendMessage(roomId, `

Skill ${skill.name} geloescht.

`); } // XP handler private async handleAddXp(roomId: string, sender: string, argString: string) { const args = argString.split(/\s+/); if (args.length < 3) { await this.sendMessage( roomId, '

Verwendung: !xp [nr] [xp] [aktivitaet]

Optional: --min 60 fuer Dauer

' ); return; } const token = this.requireAuth(sender); const number = parseInt(args[0], 10); const skill = this.skillsMapper.getByNumber(sender, number); if (!skill) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !skills

'); return; } const xp = parseInt(args[1], 10); if (isNaN(xp) || xp < 1 || xp > 10000) { await this.sendMessage(roomId, '

XP muss zwischen 1 und 10000 liegen.

'); return; } // Parse duration (--min N) let duration: number | undefined; const minMatch = argString.match(/--min\s+(\d+)/i); if (minMatch) { duration = parseInt(minMatch[1], 10); } // Get description (everything after xp number, minus --min part) let description = args.slice(2).join(' '); description = description.replace(/--min\s+\d+/i, '').trim(); if (!description) { description = 'Aktivitaet'; } const result = await this.skilltreeService.addXp(token, skill.id, xp, description, duration); if (result.error) { await this.sendMessage(roomId, `

Fehler: ${result.error}

`); return; } const { leveledUp, newLevel } = result.data!; let html = `

+${xp} XP fuer ${skill.name}!

`; html += `

${description}

`; if (leveledUp) { const levelName = this.getLevelName(newLevel); html += `

🎉 LEVEL UP! Du bist jetzt Level ${newLevel} (${levelName})!

`; } await this.sendMessage(roomId, html); } // Stats handler private async handleStats(roomId: string, sender: string) { const token = this.requireAuth(sender); const result = await this.skilltreeService.getStats(token); if (result.error) { await this.sendMessage(roomId, `

Fehler: ${result.error}

`); return; } const stats = result.data!.stats; let html = '

Deine Statistiken

    '; html += `
  • Gesamt-XP: ${stats.totalXp.toLocaleString('de-DE')}
  • `; html += `
  • Skills: ${stats.totalSkills}
  • `; html += `
  • Hoechstes Level: ${stats.highestLevel}
  • `; html += `
  • Streak: ${stats.streakDays} Tage 🔥
  • `; if (stats.lastActivityDate) { html += `
  • Letzte Aktivitaet: ${stats.lastActivityDate}
  • `; } html += '
'; await this.sendMessage(roomId, html); } // Activities handler private async handleActivities(roomId: string, sender: string, numberStr?: string) { const token = this.requireAuth(sender); let result; let skillName = ''; if (numberStr) { const number = parseInt(numberStr, 10); const skill = this.skillsMapper.getByNumber(sender, number); if (!skill) { await this.sendMessage( roomId, '

Ungueltige Nummer. Nutze zuerst !skills

' ); return; } result = await this.skilltreeService.getSkillActivities(token, skill.id); skillName = skill.name; } else { result = await this.skilltreeService.getRecentActivities(token, 10); } if (result.error) { await this.sendMessage(roomId, `

Fehler: ${result.error}

`); return; } const activities = result.data?.activities || []; if (activities.length === 0) { await this.sendMessage(roomId, '

Keine Aktivitaeten vorhanden.

'); return; } const title = skillName ? `Aktivitaeten: ${skillName}` : 'Letzte Aktivitaeten'; let html = `

${title}

    `; for (const activity of activities) { const date = new Date(activity.timestamp).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', }); const duration = activity.duration ? ` (${activity.duration} min)` : ''; html += `
  1. +${activity.xpEarned} XP - ${activity.description}${duration}
    ${date}
  2. `; } html += '
'; await this.sendMessage(roomId, html); } // Helper methods private getLevelName(level: number): string { const names: Record = { 0: 'Unbekannt', 1: 'Anfaenger', 2: 'Fortgeschritten', 3: 'Kompetent', 4: 'Experte', 5: 'Meister', }; return names[level] || `Level ${level}`; } private getNextLevelXp(level: number): number | null { const thresholds: Record = { 0: 100, 1: 500, 2: 1500, 3: 4000, 4: 10000, }; return thresholds[level] || null; } private getBranchIcon(branch: string): string { const icons: Record = { intellect: '🧠', // Brain body: '💪', // Flexed biceps creativity: '🎨', // Artist palette social: '👥', // Busts in silhouette practical: '🔧', // Wrench mindset: '💖', // Heart custom: '⭐', // Star }; return icons[branch] || '⭐'; } private translateBranch(branch: string): string { const translations: Record = { intellect: 'Wissen', body: 'Koerper', creativity: 'Kreativitaet', social: 'Sozial', practical: 'Praktisch', mindset: 'Achtsamkeit', custom: 'Eigene', }; return translations[branch] || branch; } private getProgressBar(totalXp: number, level: number): string { const nextXp = this.getNextLevelXp(level); if (!nextXp) return ''; const prevXp = level > 0 ? this.getNextLevelXp(level - 1) || 0 : 0; const progress = Math.min(100, Math.round(((totalXp - prevXp) / (nextXp - prevXp)) * 100)); return `[${progress}%]`; } }