From f04c27fe263dbb768b1a81084d7993d9a42f18a6 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:57:21 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20migrate=205=20?= =?UTF-8?q?Matrix=20bots=20to=20shared=20utilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate bots to use KeywordCommandDetector and UserListMapper from @manacore/matrix-bot-common, reducing duplicate code. KeywordCommandDetector (natural language command detection): - matrix-ollama-bot - matrix-nutriphi-bot - matrix-contacts-bot UserListMapper (number-based reference system): - matrix-presi-bot (decks + themes) - matrix-skilltree-bot (skills) - matrix-contacts-bot (contacts) Co-Authored-By: Claude Opus 4.5 --- .../src/bot/matrix.service.ts | 291 +++++++++++------- .../src/bot/matrix.service.ts | 44 +-- .../src/bot/matrix.service.ts | 42 +-- .../src/bot/matrix.service.ts | 129 +++++--- .../src/bot/matrix.service.ts | 65 ++-- 5 files changed, 325 insertions(+), 246 deletions(-) diff --git a/services/matrix-contacts-bot/src/bot/matrix.service.ts b/services/matrix-contacts-bot/src/bot/matrix.service.ts index 86e625290..b34e37769 100644 --- a/services/matrix-contacts-bot/src/bot/matrix.service.ts +++ b/services/matrix-contacts-bot/src/bot/matrix.service.ts @@ -1,23 +1,29 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common'; +import { + BaseMatrixService, + MatrixBotConfig, + MatrixRoomEvent, + KeywordCommandDetector, + COMMON_KEYWORDS, + UserListMapper, +} from '@manacore/matrix-bot-common'; import { ContactsService, Contact } from '../contacts/contacts.service'; import { SessionService } from '@manacore/bot-services'; import { HELP_MESSAGE } from '../config/configuration'; -// Natural language keywords -const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ - { keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' }, +// Natural language keyword detector +const keywordDetector = new KeywordCommandDetector([ + ...COMMON_KEYWORDS, { keywords: ['kontakte', 'contacts', 'alle'], command: 'kontakte' }, { keywords: ['favoriten', 'favorites', 'favs'], command: 'favoriten' }, { keywords: ['suche', 'search', 'finde'], command: 'suche' }, - { keywords: ['status', 'info'], command: 'status' }, -]; +]); @Injectable() export class MatrixService extends BaseMatrixService { - // Store last shown contacts per user for reference by number - private lastContactsList: Map = new Map(); + // User list mapper for number-based reference + private contactsMapper = new UserListMapper(); constructor( configService: ConfigService, @@ -29,9 +35,11 @@ export class MatrixService extends BaseMatrixService { protected getConfig(): MatrixBotConfig { return { - homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', + 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', + storagePath: + this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } @@ -60,29 +68,20 @@ Sag "hilfe" fur alle Befehle!`; return; } - const keywordCommand = this.detectKeywordCommand(message); - if (keywordCommand) { - await this.handleCommand(roomId, event, sender, `!${keywordCommand}`); + const detectedCommand = keywordDetector.detect(message); + if (detectedCommand) { + this.logger.log(`Detected keyword command: ${detectedCommand}`); + await this.handleCommand(roomId, event, sender, `!${detectedCommand}`); return; } } - private detectKeywordCommand(message: string): string | null { - const lowerMessage = message.toLowerCase().trim(); - - if (lowerMessage.length > 30) return null; - - for (const { keywords, command } of KEYWORD_COMMANDS) { - for (const keyword of keywords) { - if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) { - return command; - } - } - } - return null; - } - - private async handleCommand(roomId: string, event: MatrixRoomEvent, sender: string, body: string) { + private async handleCommand( + roomId: string, + event: MatrixRoomEvent, + sender: string, + body: string + ) { const [command, ...args] = body.slice(1).split(' '); const argString = args.join(' '); @@ -191,12 +190,13 @@ Sag "hilfe" fur alle Befehle!`; } // Store for reference - this.lastContactsList.set(sender, contacts); + this.contactsMapper.setList(sender, contacts); let text = `**Deine Kontakte (${result.total}):**\n\n`; for (let i = 0; i < contacts.length; i++) { const c = contacts[i]; - const name = c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt'; + const name = + c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt'; const favIcon = c.isFavorite ? ' ★' : ''; const company = c.company ? ` - ${c.company}` : ''; text += `**${i + 1}.** ${name}${favIcon}${company}\n`; @@ -215,7 +215,12 @@ Sag "hilfe" fur alle Befehle!`; } } - private async handleSearch(roomId: string, event: MatrixRoomEvent, sender: string, searchTerm: string) { + private async handleSearch( + roomId: string, + event: MatrixRoomEvent, + sender: string, + searchTerm: string + ) { const token = this.sessionService.getToken(sender); if (!token) { await this.sendReply(roomId, event, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); @@ -223,12 +228,19 @@ Sag "hilfe" fur alle Befehle!`; } if (!searchTerm.trim()) { - await this.sendReply(roomId, event, `**Verwendung:** \`!suche [text]\`\n\nBeispiel: \`!suche Max\``); + await this.sendReply( + roomId, + event, + `**Verwendung:** \`!suche [text]\`\n\nBeispiel: \`!suche Max\`` + ); return; } try { - const result = await this.contactsService.getContacts(token, { search: searchTerm, limit: 20 }); + const result = await this.contactsService.getContacts(token, { + search: searchTerm, + limit: 20, + }); const contacts = result.contacts; if (contacts.length === 0) { @@ -236,12 +248,13 @@ Sag "hilfe" fur alle Befehle!`; return; } - this.lastContactsList.set(sender, contacts); + this.contactsMapper.setList(sender, contacts); let text = `**Suchergebnisse fur "${searchTerm}" (${contacts.length}):**\n\n`; for (let i = 0; i < contacts.length; i++) { const c = contacts[i]; - const name = c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt'; + const name = + c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt'; const favIcon = c.isFavorite ? ' ★' : ''; const email = c.email ? ` (${c.email})` : ''; text += `**${i + 1}.** ${name}${favIcon}${email}\n`; @@ -274,12 +287,13 @@ Sag "hilfe" fur alle Befehle!`; return; } - this.lastContactsList.set(sender, contacts); + this.contactsMapper.setList(sender, contacts); let text = `**Deine Favoriten (${contacts.length}):**\n\n`; for (let i = 0; i < contacts.length; i++) { const c = contacts[i]; - const name = c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt'; + const name = + c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt'; const phone = c.phone || c.mobile || ''; text += `**${i + 1}.** ★ ${name}${phone ? ` - ${phone}` : ''}\n`; } @@ -291,7 +305,12 @@ Sag "hilfe" fur alle Befehle!`; } } - private async handleContactDetails(roomId: string, event: MatrixRoomEvent, sender: string, args: string[]) { + private async handleContactDetails( + roomId: string, + event: MatrixRoomEvent, + sender: string, + args: string[] + ) { const token = this.sessionService.getToken(sender); if (!token) { await this.sendReply(roomId, event, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); @@ -299,24 +318,26 @@ Sag "hilfe" fur alle Befehle!`; } if (args.length < 1) { - await this.sendReply(roomId, event, `**Verwendung:** \`!kontakt [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.`); + await this.sendReply( + roomId, + event, + `**Verwendung:** \`!kontakt [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.` + ); return; } - const index = parseInt(args[0], 10); - if (isNaN(index) || index < 1) { - await this.sendReply(roomId, event, `Ungultige Nummer.`); + const number = parseInt(args[0], 10); + const contact = this.contactsMapper.getByNumber(sender, number); + + if (!contact) { + await this.sendReply( + roomId, + event, + `Kontakt ${args[0]} nicht gefunden. Nutze \`!kontakte\` zuerst.` + ); return; } - const contacts = this.lastContactsList.get(sender); - if (!contacts || index > contacts.length) { - await this.sendReply(roomId, event, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`); - return; - } - - const contact = contacts[index - 1]; - try { const details = await this.contactsService.getContact(token, contact.id); @@ -334,14 +355,19 @@ Sag "hilfe" fur alle Befehle!`; if (details.mobile) text += `**Mobil:** ${details.mobile}\n`; if (details.street || details.city) { - const address = [details.street, `${details.postalCode || ''} ${details.city || ''}`.trim(), details.country] + const address = [ + details.street, + `${details.postalCode || ''} ${details.city || ''}`.trim(), + details.country, + ] .filter(Boolean) .join(', '); if (address) text += `**Adresse:** ${address}\n`; } if (details.website) text += `**Website:** ${details.website}\n`; - if (details.birthday) text += `**Geburtstag:** ${new Date(details.birthday).toLocaleDateString('de-DE')}\n`; + if (details.birthday) + text += `**Geburtstag:** ${new Date(details.birthday).toLocaleDateString('de-DE')}\n`; if (details.notes) text += `\n**Notizen:** ${details.notes}\n`; await this.sendReply(roomId, event, text); @@ -351,7 +377,12 @@ Sag "hilfe" fur alle Befehle!`; } } - private async handleCreateContact(roomId: string, event: MatrixRoomEvent, sender: string, args: string[]) { + private async handleCreateContact( + roomId: string, + event: MatrixRoomEvent, + sender: string, + args: string[] + ) { const token = this.sessionService.getToken(sender); if (!token) { await this.sendReply(roomId, event, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); @@ -388,7 +419,12 @@ Sag "hilfe" fur alle Befehle!`; } } - private async handleEditContact(roomId: string, event: MatrixRoomEvent, sender: string, args: string[]) { + private async handleEditContact( + roomId: string, + event: MatrixRoomEvent, + sender: string, + args: string[] + ) { const token = this.sessionService.getToken(sender); if (!token) { await this.sendReply(roomId, event, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); @@ -404,23 +440,20 @@ Sag "hilfe" fur alle Befehle!`; return; } - const index = parseInt(args[0], 10); + const number = parseInt(args[0], 10); const field = args[1].toLowerCase(); const value = args.slice(2).join(' '); - if (isNaN(index) || index < 1) { - await this.sendReply(roomId, event, `Ungultige Nummer.`); + const contact = this.contactsMapper.getByNumber(sender, number); + if (!contact) { + await this.sendReply( + roomId, + event, + `Kontakt ${args[0]} nicht gefunden. Nutze \`!kontakte\` zuerst.` + ); return; } - const contacts = this.lastContactsList.get(sender); - if (!contacts || index > contacts.length) { - await this.sendReply(roomId, event, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`); - return; - } - - const contact = contacts[index - 1]; - const fieldMap: Record = { email: 'email', phone: 'phone', @@ -455,7 +488,11 @@ Sag "hilfe" fur alle Befehle!`; const mappedField = fieldMap[field]; if (!mappedField) { - await this.sendReply(roomId, event, `Unbekanntes Feld: ${field}\n\n**Gultige Felder:** email, phone, mobile, company, job, website, street, city, zip, country, notes, birthday`); + await this.sendReply( + roomId, + event, + `Unbekanntes Feld: ${field}\n\n**Gultige Felder:** email, phone, mobile, company, job, website, street, city, zip, country, notes, birthday` + ); return; } @@ -464,15 +501,25 @@ Sag "hilfe" fur alle Befehle!`; [mappedField]: value, }); - const name = updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim(); - await this.sendReply(roomId, event, `Kontakt **${name}** aktualisiert!\n\n**${field}:** ${value}`); + const name = + updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim(); + await this.sendReply( + roomId, + event, + `Kontakt **${name}** aktualisiert!\n\n**${field}:** ${value}` + ); } catch (error) { const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler'; await this.sendReply(roomId, event, `Fehler: ${errorMsg}`); } } - private async handleDeleteContact(roomId: string, event: MatrixRoomEvent, sender: string, args: string[]) { + private async handleDeleteContact( + roomId: string, + event: MatrixRoomEvent, + sender: string, + args: string[] + ) { const token = this.sessionService.getToken(sender); if (!token) { await this.sendReply(roomId, event, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); @@ -480,24 +527,27 @@ Sag "hilfe" fur alle Befehle!`; } if (args.length < 1) { - await this.sendReply(roomId, event, `**Verwendung:** \`!loeschen [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.`); + await this.sendReply( + roomId, + event, + `**Verwendung:** \`!loeschen [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.` + ); return; } - const index = parseInt(args[0], 10); - if (isNaN(index) || index < 1) { - await this.sendReply(roomId, event, `Ungultige Nummer.`); + const number = parseInt(args[0], 10); + const contact = this.contactsMapper.getByNumber(sender, number); + if (!contact) { + await this.sendReply( + roomId, + event, + `Kontakt ${args[0]} nicht gefunden. Nutze \`!kontakte\` zuerst.` + ); return; } - const contacts = this.lastContactsList.get(sender); - if (!contacts || index > contacts.length) { - await this.sendReply(roomId, event, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`); - return; - } - - const contact = contacts[index - 1]; - const name = contact.displayName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(); + const name = + contact.displayName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim(); try { await this.contactsService.deleteContact(token, contact.id); @@ -508,7 +558,12 @@ Sag "hilfe" fur alle Befehle!`; } } - private async handleToggleFavorite(roomId: string, event: MatrixRoomEvent, sender: string, args: string[]) { + private async handleToggleFavorite( + roomId: string, + event: MatrixRoomEvent, + sender: string, + args: string[] + ) { const token = this.sessionService.getToken(sender); if (!token) { await this.sendReply(roomId, event, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); @@ -516,27 +571,29 @@ Sag "hilfe" fur alle Befehle!`; } if (args.length < 1) { - await this.sendReply(roomId, event, `**Verwendung:** \`!fav [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.`); + await this.sendReply( + roomId, + event, + `**Verwendung:** \`!fav [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.` + ); return; } - const index = parseInt(args[0], 10); - if (isNaN(index) || index < 1) { - await this.sendReply(roomId, event, `Ungultige Nummer.`); + const number = parseInt(args[0], 10); + const contact = this.contactsMapper.getByNumber(sender, number); + if (!contact) { + await this.sendReply( + roomId, + event, + `Kontakt ${args[0]} nicht gefunden. Nutze \`!kontakte\` zuerst.` + ); return; } - const contacts = this.lastContactsList.get(sender); - if (!contacts || index > contacts.length) { - await this.sendReply(roomId, event, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`); - return; - } - - const contact = contacts[index - 1]; - try { const updated = await this.contactsService.toggleFavorite(token, contact.id); - const name = updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim(); + const name = + updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim(); const status = updated.isFavorite ? 'als Favorit markiert ★' : 'aus Favoriten entfernt'; await this.sendReply(roomId, event, `**${name}** ${status}`); } catch (error) { @@ -545,7 +602,12 @@ Sag "hilfe" fur alle Befehle!`; } } - private async handleToggleArchive(roomId: string, event: MatrixRoomEvent, sender: string, args: string[]) { + private async handleToggleArchive( + roomId: string, + event: MatrixRoomEvent, + sender: string, + args: string[] + ) { const token = this.sessionService.getToken(sender); if (!token) { await this.sendReply(roomId, event, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); @@ -553,27 +615,29 @@ Sag "hilfe" fur alle Befehle!`; } if (args.length < 1) { - await this.sendReply(roomId, event, `**Verwendung:** \`!archiv [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.`); + await this.sendReply( + roomId, + event, + `**Verwendung:** \`!archiv [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.` + ); return; } - const index = parseInt(args[0], 10); - if (isNaN(index) || index < 1) { - await this.sendReply(roomId, event, `Ungultige Nummer.`); + const number = parseInt(args[0], 10); + const contact = this.contactsMapper.getByNumber(sender, number); + if (!contact) { + await this.sendReply( + roomId, + event, + `Kontakt ${args[0]} nicht gefunden. Nutze \`!kontakte\` zuerst.` + ); return; } - const contacts = this.lastContactsList.get(sender); - if (!contacts || index > contacts.length) { - await this.sendReply(roomId, event, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`); - return; - } - - const contact = contacts[index - 1]; - try { const updated = await this.contactsService.toggleArchive(token, contact.id); - const name = updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim(); + const name = + updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim(); const status = updated.isArchived ? 'archiviert' : 'aus dem Archiv geholt'; await this.sendReply(roomId, event, `**${name}** ${status}`); } catch (error) { @@ -582,7 +646,12 @@ Sag "hilfe" fur alle Befehle!`; } } - private async handleLogin(roomId: string, event: MatrixRoomEvent, sender: string, args: string[]) { + private async handleLogin( + roomId: string, + event: MatrixRoomEvent, + sender: string, + args: string[] + ) { if (args.length < 2) { await this.sendReply( roomId, diff --git a/services/matrix-nutriphi-bot/src/bot/matrix.service.ts b/services/matrix-nutriphi-bot/src/bot/matrix.service.ts index 0c8c3e2fc..7e3b85b1a 100644 --- a/services/matrix-nutriphi-bot/src/bot/matrix.service.ts +++ b/services/matrix-nutriphi-bot/src/bot/matrix.service.ts @@ -4,6 +4,8 @@ import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, + KeywordCommandDetector, + COMMON_KEYWORDS, } from '@manacore/matrix-bot-common'; import { NutriPhiService, @@ -14,16 +16,16 @@ import { import { SessionService, TranscriptionService } from '@manacore/bot-services'; import { HELP_MESSAGE, MEAL_TYPE_LABELS } from '../config/configuration'; -// Natural language keywords that trigger commands (German + English) -const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ - { keywords: ['hilfe', 'help', 'was kannst du', 'befehle', 'commands'], command: 'help' }, +// Natural language keyword detector +const keywordDetector = new KeywordCommandDetector([ + ...COMMON_KEYWORDS, { keywords: ['heute', 'today', 'tages', 'tagesübersicht'], command: 'today' }, { keywords: ['woche', 'week', 'wochen', 'wochenübersicht'], command: 'week' }, { keywords: ['ziele', 'goals', 'meine ziele'], command: 'goals' }, { keywords: ['favoriten', 'favorites', 'lieblings'], command: 'favorites' }, { keywords: ['tipps', 'tips', 'empfehlungen', 'ratschläge'], command: 'tips' }, - { keywords: ['status', 'verbindung'], command: 'status' }, -]; + { keywords: ['verbindung'], command: 'status' }, +]); @Injectable() export class MatrixService extends BaseMatrixService { @@ -38,9 +40,11 @@ export class MatrixService extends BaseMatrixService { protected getConfig(): MatrixBotConfig { return { - homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', + 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', + storagePath: + this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } @@ -65,7 +69,7 @@ Sag "hilfe" fur alle Befehle!`; // Handle image messages this.client.on('room.message', async (roomId: string, event: any) => { - if (event.sender === await this.client.getUserId()) return; + if (event.sender === (await this.client.getUserId())) return; const content = event.content as { msgtype?: string; @@ -159,32 +163,16 @@ Sag "hilfe" fur alle Befehle!`; } // Check for natural language keywords - const keywordCommand = this.detectKeywordCommand(message); - if (keywordCommand) { - await this.handleCommand(roomId, sender, `!${keywordCommand}`); + const detectedCommand = keywordDetector.detect(message); + if (detectedCommand) { + this.logger.log(`Detected keyword command: ${detectedCommand}`); + await this.handleCommand(roomId, sender, `!${detectedCommand}`); return; } // Don't respond to random messages - only commands } - private detectKeywordCommand(message: string): string | null { - const lowerMessage = message.toLowerCase().trim(); - - // Only match if the message is short - if (lowerMessage.length > 50) return null; - - for (const { keywords, command } of KEYWORD_COMMANDS) { - for (const keyword of keywords) { - if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) { - this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`); - return command; - } - } - } - return null; - } - private async handleCommand(roomId: string, sender: string, body: string) { const [command, ...args] = body.slice(1).split(' '); const argString = args.join(' '); diff --git a/services/matrix-ollama-bot/src/bot/matrix.service.ts b/services/matrix-ollama-bot/src/bot/matrix.service.ts index b1ed0375f..e11a6e78f 100644 --- a/services/matrix-ollama-bot/src/bot/matrix.service.ts +++ b/services/matrix-ollama-bot/src/bot/matrix.service.ts @@ -4,6 +4,8 @@ import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent, + KeywordCommandDetector, + COMMON_KEYWORDS, } from '@manacore/matrix-bot-common'; import { OllamaService } from '../ollama/ollama.service'; import { SYSTEM_PROMPTS } from '../config/configuration'; @@ -21,13 +23,13 @@ const NON_CHAT_MODELS = ['deepseek-r1:1.5b']; // Models that support vision/image input const VISION_MODELS = ['llava', 'llava:7b', 'llava:13b', 'bakllava', 'moondream']; -// Natural language keywords that trigger commands (German + English) -const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ - { keywords: ['hilfe', 'help', 'was kannst du', 'befehle', 'commands'], command: 'help' }, +// Natural language keyword detector +const keywordDetector = new KeywordCommandDetector([ + ...COMMON_KEYWORDS, { keywords: ['modelle', 'models', 'welche modelle', 'liste modelle'], command: 'models' }, - { keywords: ['status', 'verbindung', 'connection', 'online'], command: 'status' }, + { keywords: ['verbindung', 'connection', 'online'], command: 'status' }, { keywords: ['lösche verlauf', 'clear', 'neustart', 'reset', 'vergiss alles'], command: 'clear' }, -]; +]); @Injectable() export class MatrixService extends BaseMatrixService { @@ -42,9 +44,11 @@ export class MatrixService extends BaseMatrixService { protected getConfig(): MatrixBotConfig { return { - homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', + 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', + storagePath: + this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } @@ -159,9 +163,10 @@ Viel Spass!`; } // Check for natural language keywords - const keywordCommand = this.detectKeywordCommand(message); - if (keywordCommand) { - await this.handleCommand(roomId, sender, `!${keywordCommand}`); + const detectedCommand = keywordDetector.detect(message); + if (detectedCommand) { + this.logger.log(`Detected keyword command: ${detectedCommand}`); + await this.handleCommand(roomId, sender, `!${detectedCommand}`); return; } @@ -169,23 +174,6 @@ Viel Spass!`; await this.handleChat(roomId, sender, message); } - private detectKeywordCommand(message: string): string | null { - const lowerMessage = message.toLowerCase().trim(); - - // Only match if the message is short (likely a command, not a question containing a keyword) - if (lowerMessage.length > 50) return null; - - for (const { keywords, command } of KEYWORD_COMMANDS) { - for (const keyword of keywords) { - if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) { - this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`); - return command; - } - } - } - return null; - } - private async handleCommand(roomId: string, sender: string, body: string) { const [command, ...args] = body.slice(1).split(' '); const argString = args.join(' '); diff --git a/services/matrix-presi-bot/src/bot/matrix.service.ts b/services/matrix-presi-bot/src/bot/matrix.service.ts index be78193f3..ee603e86a 100644 --- a/services/matrix-presi-bot/src/bot/matrix.service.ts +++ b/services/matrix-presi-bot/src/bot/matrix.service.ts @@ -1,15 +1,20 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common'; +import { + BaseMatrixService, + MatrixBotConfig, + MatrixRoomEvent, + UserListMapper, +} from '@manacore/matrix-bot-common'; import { PresiService, Deck, Theme, SlideContent } from '../presi/presi.service'; import { SessionService } from '@manacore/bot-services'; import { HELP_MESSAGE } from '../config/configuration'; @Injectable() export class MatrixService extends BaseMatrixService { - // Store last shown items per user for reference by number - private lastDecksList: Map = new Map(); - private lastThemesList: Map = new Map(); + // User list mappers for number-based reference + private decksMapper = new UserListMapper(); + private themesMapper = new UserListMapper(); constructor( configService: ConfigService, @@ -21,9 +26,11 @@ export class MatrixService extends BaseMatrixService { protected getConfig(): MatrixBotConfig { return { - homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', + 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', + storagePath: + this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } @@ -187,7 +194,7 @@ export class MatrixService extends BaseMatrixService { } const decks = result.data || []; - this.lastDecksList.set(sender, decks); + this.decksMapper.setList(sender, decks); if (decks.length === 0) { await this.sendMessage( @@ -211,7 +218,8 @@ export class MatrixService extends BaseMatrixService { private async handleDeckDetails(roomId: string, sender: string, numberStr: string) { const token = this.requireAuth(sender); - const deck = this.getDeckByNumber(sender, numberStr); + const number = parseInt(numberStr, 10); + const deck = this.decksMapper.getByNumber(sender, number); if (!deck) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !presis

'); @@ -238,7 +246,8 @@ export class MatrixService extends BaseMatrixService { if (d.slides && d.slides.length > 0) { html += '

Folien:

    '; for (const slide of d.slides) { - const title = slide.content.title || slide.content.body?.substring(0, 30) || `(${slide.content.type})`; + const title = + slide.content.title || slide.content.body?.substring(0, 30) || `(${slide.content.type})`; html += `
  1. ${title}
  2. `; } html += '
'; @@ -267,7 +276,7 @@ export class MatrixService extends BaseMatrixService { return; } - this.lastDecksList.delete(sender); + this.decksMapper.clearList(sender); await this.sendMessage( roomId, `

Praesentation ${result.data!.title} erstellt!

@@ -277,7 +286,8 @@ export class MatrixService extends BaseMatrixService { private async handleDeleteDeck(roomId: string, sender: string, numberStr: string) { const token = this.requireAuth(sender); - const deck = this.getDeckByNumber(sender, numberStr); + const number = parseInt(numberStr, 10); + const deck = this.decksMapper.getByNumber(sender, number); if (!deck) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !presis

'); @@ -291,18 +301,30 @@ export class MatrixService extends BaseMatrixService { return; } - this.lastDecksList.delete(sender); - await this.sendMessage(roomId, `

Praesentation ${deck.title} geloescht.

`); + this.decksMapper.clearList(sender); + await this.sendMessage( + roomId, + `

Praesentation ${deck.title} geloescht.

` + ); } - private async handleRenameDeck(roomId: string, sender: string, numberStr: string, newTitle: string) { + private async handleRenameDeck( + roomId: string, + sender: string, + numberStr: string, + newTitle: string + ) { if (!newTitle) { - await this.sendMessage(roomId, '

Verwendung: !umbenennen [nr] Neuer Titel

'); + await this.sendMessage( + roomId, + '

Verwendung: !umbenennen [nr] Neuer Titel

' + ); return; } const token = this.requireAuth(sender); - const deck = this.getDeckByNumber(sender, numberStr); + const number = parseInt(numberStr, 10); + const deck = this.decksMapper.getByNumber(sender, number); if (!deck) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !presis

'); @@ -338,7 +360,8 @@ export class MatrixService extends BaseMatrixService { } const token = this.requireAuth(sender); - const deck = this.getDeckByNumber(sender, args[0]); + const number = parseInt(args[0], 10); + const deck = this.decksMapper.getByNumber(sender, number); if (!deck) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !presis

'); @@ -412,14 +435,23 @@ export class MatrixService extends BaseMatrixService { ); } - private async handleDeleteSlide(roomId: string, sender: string, deckNumStr: string, slideNumStr: string) { + private async handleDeleteSlide( + roomId: string, + sender: string, + deckNumStr: string, + slideNumStr: string + ) { if (!deckNumStr || !slideNumStr) { - await this.sendMessage(roomId, '

Verwendung: !folieloeschen [presi-nr] [folien-nr]

'); + await this.sendMessage( + roomId, + '

Verwendung: !folieloeschen [presi-nr] [folien-nr]

' + ); return; } const token = this.requireAuth(sender); - const deck = this.getDeckByNumber(sender, deckNumStr); + const deckNumber = parseInt(deckNumStr, 10); + const deck = this.decksMapper.getByNumber(sender, deckNumber); if (!deck) { await this.sendMessage(roomId, '

Ungueltige Praesentation-Nummer.

'); @@ -447,7 +479,10 @@ export class MatrixService extends BaseMatrixService { return; } - await this.sendMessage(roomId, `

Folie ${slideNumStr} aus ${deck.title} geloescht.

`); + await this.sendMessage( + roomId, + `

Folie ${slideNumStr} aus ${deck.title} geloescht.

` + ); } // Theme handlers @@ -460,7 +495,7 @@ export class MatrixService extends BaseMatrixService { } const themes = result.data || []; - this.lastThemesList.set(sender, themes); + this.themesMapper.setList(sender, themes); if (themes.length === 0) { await this.sendMessage(roomId, '

Keine Themes verfuegbar.

'); @@ -478,15 +513,25 @@ export class MatrixService extends BaseMatrixService { await this.sendMessage(roomId, html); } - private async handleApplyTheme(roomId: string, sender: string, deckNumStr: string, themeNumStr: string) { + private async handleApplyTheme( + roomId: string, + sender: string, + deckNumStr: string, + themeNumStr: string + ) { if (!deckNumStr || !themeNumStr) { - await this.sendMessage(roomId, '

Verwendung: !theme [presi-nr] [theme-nr]

'); + await this.sendMessage( + roomId, + '

Verwendung: !theme [presi-nr] [theme-nr]

' + ); return; } const token = this.requireAuth(sender); - const deck = this.getDeckByNumber(sender, deckNumStr); - const theme = this.getThemeByNumber(sender, themeNumStr); + const deckNumber = parseInt(deckNumStr, 10); + const themeNumber = parseInt(themeNumStr, 10); + const deck = this.decksMapper.getByNumber(sender, deckNumber); + const theme = this.themesMapper.getByNumber(sender, themeNumber); if (!deck) { await this.sendMessage(roomId, '

Ungueltige Praesentation-Nummer.

'); @@ -494,7 +539,10 @@ export class MatrixService extends BaseMatrixService { } if (!theme) { - await this.sendMessage(roomId, '

Ungueltige Theme-Nummer. Nutze zuerst !themes

'); + await this.sendMessage( + roomId, + '

Ungueltige Theme-Nummer. Nutze zuerst !themes

' + ); return; } @@ -517,7 +565,8 @@ export class MatrixService extends BaseMatrixService { const numberStr = args[0]; const token = this.requireAuth(sender); - const deck = this.getDeckByNumber(sender, numberStr); + const number = parseInt(numberStr, 10); + const deck = this.decksMapper.getByNumber(sender, number); if (!deck) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !presis

'); @@ -559,7 +608,8 @@ export class MatrixService extends BaseMatrixService { } const token = this.requireAuth(sender); - const deck = this.getDeckByNumber(sender, numberStr); + const number = parseInt(numberStr, 10); + const deck = this.decksMapper.getByNumber(sender, number); if (!deck) { await this.sendMessage(roomId, '

Ungueltige Nummer. Nutze zuerst !presis

'); @@ -595,25 +645,4 @@ export class MatrixService extends BaseMatrixService { await this.sendMessage(roomId, html); } - - // Helper methods - private getDeckByNumber(sender: string, numberStr: string): Deck | null { - const decks = this.lastDecksList.get(sender); - if (!decks) return null; - - const index = parseInt(numberStr, 10) - 1; - if (isNaN(index) || index < 0 || index >= decks.length) return null; - - return decks[index]; - } - - private getThemeByNumber(sender: string, numberStr: string): Theme | null { - const themes = this.lastThemesList.get(sender); - if (!themes) return null; - - const index = parseInt(numberStr, 10) - 1; - if (isNaN(index) || index < 0 || index >= themes.length) return null; - - return themes[index]; - } } diff --git a/services/matrix-skilltree-bot/src/bot/matrix.service.ts b/services/matrix-skilltree-bot/src/bot/matrix.service.ts index 6ed840459..fffb1d8e9 100644 --- a/services/matrix-skilltree-bot/src/bot/matrix.service.ts +++ b/services/matrix-skilltree-bot/src/bot/matrix.service.ts @@ -1,14 +1,19 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } from '@manacore/matrix-bot-common'; +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 { - // Store last shown skills per user for reference by number - private lastSkillsList: Map = new Map(); + // User list mapper for number-based reference + private skillsMapper = new UserListMapper(); // Branch name mappings (German/English) private readonly branchMappings: Record = { @@ -45,9 +50,11 @@ export class MatrixService extends BaseMatrixService { protected getConfig(): MatrixBotConfig { return { - homeserverUrl: this.configService.get('matrix.homeserverUrl') || 'http://localhost:8008', + 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', + storagePath: + this.configService.get('matrix.storagePath') || './data/bot-storage.json', allowedRooms: this.configService.get('matrix.allowedRooms') || [], }; } @@ -204,7 +211,7 @@ export class MatrixService extends BaseMatrixService { } const skills = result.data?.skills || []; - this.lastSkillsList.set(sender, skills); + this.skillsMapper.setList(sender, skills); if (skills.length === 0) { await this.sendMessage( @@ -222,14 +229,16 @@ export class MatrixService extends BaseMatrixService { html += `
  • ${branchIcon} ${skill.name} - Lvl ${skill.level} (${levelName}) ${progress}
  • `; } html += ''; - html += '

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

    '; + 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 skill = this.getSkillByNumber(sender, numberStr); + const number = parseInt(numberStr, 10); + const skill = this.skillsMapper.getByNumber(sender, number); if (!skill) { await this.sendMessage(roomId, '

    Ungueltige Nummer. Nutze zuerst !skills

    '); @@ -296,7 +305,7 @@ export class MatrixService extends BaseMatrixService { return; } - this.lastSkillsList.delete(sender); + this.skillsMapper.clearList(sender); const branchIcon = this.getBranchIcon(branch); await this.sendMessage( roomId, @@ -307,7 +316,8 @@ export class MatrixService extends BaseMatrixService { private async handleDeleteSkill(roomId: string, sender: string, numberStr: string) { const token = this.requireAuth(sender); - const skill = this.getSkillByNumber(sender, numberStr); + const number = parseInt(numberStr, 10); + const skill = this.skillsMapper.getByNumber(sender, number); if (!skill) { await this.sendMessage(roomId, '

    Ungueltige Nummer. Nutze zuerst !skills

    '); @@ -321,7 +331,7 @@ export class MatrixService extends BaseMatrixService { return; } - this.lastSkillsList.delete(sender); + this.skillsMapper.clearList(sender); await this.sendMessage(roomId, `

    Skill ${skill.name} geloescht.

    `); } @@ -338,7 +348,8 @@ export class MatrixService extends BaseMatrixService { } const token = this.requireAuth(sender); - const skill = this.getSkillByNumber(sender, args[0]); + const number = parseInt(args[0], 10); + const skill = this.skillsMapper.getByNumber(sender, number); if (!skill) { await this.sendMessage(roomId, '

    Ungueltige Nummer. Nutze zuerst !skills

    '); @@ -417,9 +428,13 @@ export class MatrixService extends BaseMatrixService { let skillName = ''; if (numberStr) { - const skill = this.getSkillByNumber(sender, numberStr); + const number = parseInt(numberStr, 10); + const skill = this.skillsMapper.getByNumber(sender, number); if (!skill) { - await this.sendMessage(roomId, '

    Ungueltige Nummer. Nutze zuerst !skills

    '); + await this.sendMessage( + roomId, + '

    Ungueltige Nummer. Nutze zuerst !skills

    ' + ); return; } result = await this.skilltreeService.getSkillActivities(token, skill.id); @@ -459,16 +474,6 @@ export class MatrixService extends BaseMatrixService { } // Helper methods - private getSkillByNumber(sender: string, numberStr: string): Skill | null { - const skills = this.lastSkillsList.get(sender); - if (!skills) return null; - - const index = parseInt(numberStr, 10) - 1; - if (isNaN(index) || index < 0 || index >= skills.length) return null; - - return skills[index]; - } - private getLevelName(level: number): string { const names: Record = { 0: 'Unbekannt', @@ -494,13 +499,13 @@ export class MatrixService extends BaseMatrixService { private getBranchIcon(branch: string): string { const icons: Record = { - intellect: '🧠', // Brain - body: '💪', // Flexed biceps + intellect: '🧠', // Brain + body: '💪', // Flexed biceps creativity: '🎨', // Artist palette - social: '👥', // Busts in silhouette - practical: '🔧', // Wrench - mindset: '💖', // Heart - custom: '⭐', // Star + social: '👥', // Busts in silhouette + practical: '🔧', // Wrench + mindset: '💖', // Heart + custom: '⭐', // Star }; return icons[branch] || '⭐'; }