♻️ refactor: migrate all 18 Matrix bots to extend BaseMatrixService

All Matrix bots now extend BaseMatrixService from @manacore/matrix-bot-common:
- matrix-calendar-bot
- matrix-chat-bot
- matrix-clock-bot
- matrix-contacts-bot
- matrix-mana-bot
- matrix-manadeck-bot
- matrix-nutriphi-bot
- matrix-ollama-bot
- matrix-picture-bot
- matrix-planta-bot
- matrix-presi-bot
- matrix-project-doc-bot
- matrix-questions-bot
- matrix-skilltree-bot
- matrix-storage-bot
- matrix-todo-bot
- matrix-tts-bot
- matrix-zitare-bot

Consolidated code:
- Matrix client initialization (onModuleInit)
- Graceful shutdown (onModuleDestroy)
- sendMessage/sendReply/sendNotice methods
- markdownToHtml conversion
- Room permission checking
- Media upload/download

Estimated code reduction: ~1,500+ lines of duplicate code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-01 02:47:11 +01:00
parent f4d8ed491c
commit 2567ea622c
18 changed files with 1472 additions and 2721 deletions

View file

@ -1,20 +1,12 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
MatrixClient,
SimpleFsStorageProvider,
AutojoinRoomsMixin,
} from 'matrix-bot-sdk';
import { BaseMatrixService, MatrixBotConfig, MatrixRoomEvent } 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 implements OnModuleInit {
private readonly logger = new Logger(MatrixService.name);
private client: MatrixClient;
private allowedRooms: string[];
export class MatrixService extends BaseMatrixService {
// Store last shown skills per user for reference by number
private lastSkillsList: Map<string, Skill[]> = new Map();
@ -44,43 +36,28 @@ export class MatrixService implements OnModuleInit {
};
constructor(
private configService: ConfigService,
configService: ConfigService,
private skilltreeService: SkilltreeService,
private sessionService: SessionService
) {}
async onModuleInit() {
const homeserverUrl = this.configService.get<string>('matrix.homeserverUrl');
const accessToken = this.configService.get<string>('matrix.accessToken');
const storagePath = this.configService.get<string>('matrix.storagePath');
this.allowedRooms = this.configService.get<string[]>('matrix.allowedRooms') || [];
if (!accessToken) {
this.logger.warn('No Matrix access token configured, bot disabled');
return;
}
const storage = new SimpleFsStorageProvider(storagePath || './data/bot-storage.json');
this.client = new MatrixClient(homeserverUrl || 'http://localhost:8008', accessToken, storage);
AutojoinRoomsMixin.setupOnClient(this.client);
this.client.on('room.message', this.handleMessage.bind(this));
await this.client.start();
this.logger.log('Matrix Skilltree Bot started');
) {
super(configService);
}
private async handleMessage(roomId: string, event: any) {
if (event.sender === (await this.client.getUserId())) return;
if (event.content?.msgtype !== 'm.text') return;
protected getConfig(): MatrixBotConfig {
return {
homeserverUrl: this.configService.get<string>('matrix.homeserverUrl') || 'http://localhost:8008',
accessToken: this.configService.get<string>('matrix.accessToken') || '',
storagePath: this.configService.get<string>('matrix.storagePath') || './data/bot-storage.json',
allowedRooms: this.configService.get<string[]>('matrix.allowedRooms') || [],
};
}
const body = event.content.body?.trim();
if (!body?.startsWith('!')) return;
if (this.allowedRooms.length > 0 && !this.allowedRooms.includes(roomId)) {
return;
}
protected async handleTextMessage(
roomId: string,
event: MatrixRoomEvent,
body: string
): Promise<void> {
if (!body.startsWith('!')) return;
const sender = event.sender;
const parts = body.slice(1).split(/\s+/);
@ -92,7 +69,7 @@ export class MatrixService implements OnModuleInit {
switch (command) {
case 'help':
case 'hilfe':
await this.sendHtml(roomId, HELP_MESSAGE);
await this.sendMessage(roomId, HELP_MESSAGE);
break;
case 'login':
@ -101,7 +78,7 @@ export class MatrixService implements OnModuleInit {
case 'logout':
this.sessionService.logout(sender);
await this.sendHtml(roomId, '<p>Erfolgreich abgemeldet.</p>');
await this.sendMessage(roomId, '<p>Erfolgreich abgemeldet.</p>');
break;
case 'status':
@ -151,26 +128,17 @@ export class MatrixService implements OnModuleInit {
break;
default:
await this.sendHtml(
await this.sendMessage(
roomId,
`<p>Unbekannter Befehl: <code>${command}</code>. Nutze <code>!help</code> fuer Hilfe.</p>`
);
}
} catch (error) {
this.logger.error(`Error handling command ${command}:`, error);
await this.sendHtml(roomId, `<p>Fehler: ${error.message}</p>`);
await this.sendMessage(roomId, `<p>Fehler: ${(error as Error).message}</p>`);
}
}
private async sendHtml(roomId: string, html: string) {
await this.client.sendMessage(roomId, {
msgtype: 'm.text',
body: html.replace(/<[^>]*>/g, ''),
format: 'org.matrix.custom.html',
formatted_body: html,
});
}
private requireAuth(sender: string): string {
const token = this.sessionService.getToken(sender);
if (!token) {
@ -182,7 +150,7 @@ export class MatrixService implements OnModuleInit {
// Auth handlers
private async handleLogin(roomId: string, sender: string, args: string[]) {
if (args.length < 2) {
await this.sendHtml(roomId, '<p>Verwendung: <code>!login email passwort</code></p>');
await this.sendMessage(roomId, '<p>Verwendung: <code>!login email passwort</code></p>');
return;
}
@ -190,9 +158,9 @@ export class MatrixService implements OnModuleInit {
const result = await this.sessionService.login(sender, email, password);
if (result.success) {
await this.sendHtml(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
await this.sendMessage(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
} else {
await this.sendHtml(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
}
}
@ -201,7 +169,7 @@ export class MatrixService implements OnModuleInit {
const loggedIn = this.sessionService.isLoggedIn(sender);
const sessions = this.sessionService.getSessionCount();
await this.sendHtml(
await this.sendMessage(
roomId,
`<h3>Skilltree Bot Status</h3>
<ul>
@ -220,7 +188,7 @@ export class MatrixService implements OnModuleInit {
if (branchFilter) {
branch = this.branchMappings[branchFilter.toLowerCase()];
if (!branch) {
await this.sendHtml(
await this.sendMessage(
roomId,
'<p>Unbekannter Branch. Verfuegbar: intellect, body, creativity, social, practical, mindset, custom</p>'
);
@ -231,7 +199,7 @@ export class MatrixService implements OnModuleInit {
const result = await this.skilltreeService.getSkills(token, branch);
if (result.error) {
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
return;
}
@ -239,7 +207,7 @@ export class MatrixService implements OnModuleInit {
this.lastSkillsList.set(sender, skills);
if (skills.length === 0) {
await this.sendHtml(
await this.sendMessage(
roomId,
'<p>Keine Skills vorhanden. Erstelle einen mit <code>!neu Name | Branch</code></p>'
);
@ -256,7 +224,7 @@ export class MatrixService implements OnModuleInit {
html += '</ol>';
html += '<p><em>Nutze <code>!skill [nr]</code> fuer Details oder <code>!xp [nr] 50 Aktivitaet</code></em></p>';
await this.sendHtml(roomId, html);
await this.sendMessage(roomId, html);
}
private async handleSkillDetails(roomId: string, sender: string, numberStr: string) {
@ -264,13 +232,13 @@ export class MatrixService implements OnModuleInit {
const skill = this.getSkillByNumber(sender, numberStr);
if (!skill) {
await this.sendHtml(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!skills</code></p>');
await this.sendMessage(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!skills</code></p>');
return;
}
const result = await this.skilltreeService.getSkill(token, skill.id);
if (result.error) {
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
return;
}
@ -293,12 +261,12 @@ export class MatrixService implements OnModuleInit {
html += `<p><em>Nutze <code>!xp ${numberStr} [xp] [aktivitaet]</code> um XP hinzuzufuegen</em></p>`;
await this.sendHtml(roomId, html);
await this.sendMessage(roomId, html);
}
private async handleCreateSkill(roomId: string, sender: string, input: string) {
if (!input) {
await this.sendHtml(
await this.sendMessage(
roomId,
'<p>Verwendung: <code>!neu Name | Branch</code></p><p>Branches: intellect, body, creativity, social, practical, mindset, custom</p>'
);
@ -312,7 +280,7 @@ export class MatrixService implements OnModuleInit {
const branch = this.branchMappings[branchInput];
if (!branch) {
await this.sendHtml(
await this.sendMessage(
roomId,
'<p>Unbekannter Branch. Verfuegbar: intellect, body, creativity, social, practical, mindset, custom</p>'
);
@ -324,13 +292,13 @@ export class MatrixService implements OnModuleInit {
const result = await this.skilltreeService.createSkill(token, name, branch, description);
if (result.error) {
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
return;
}
this.lastSkillsList.delete(sender);
const branchIcon = this.getBranchIcon(branch);
await this.sendHtml(
await this.sendMessage(
roomId,
`<p>${branchIcon} Skill <strong>${result.data!.skill.name}</strong> erstellt!</p>
<p><em>Nutze <code>!skills</code> und dann <code>!xp [nr] [xp] [aktivitaet]</code></em></p>`
@ -342,19 +310,19 @@ export class MatrixService implements OnModuleInit {
const skill = this.getSkillByNumber(sender, numberStr);
if (!skill) {
await this.sendHtml(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!skills</code></p>');
await this.sendMessage(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!skills</code></p>');
return;
}
const result = await this.skilltreeService.deleteSkill(token, skill.id);
if (result.error) {
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
return;
}
this.lastSkillsList.delete(sender);
await this.sendHtml(roomId, `<p>Skill <strong>${skill.name}</strong> geloescht.</p>`);
await this.sendMessage(roomId, `<p>Skill <strong>${skill.name}</strong> geloescht.</p>`);
}
// XP handler
@ -362,7 +330,7 @@ export class MatrixService implements OnModuleInit {
const args = argString.split(/\s+/);
if (args.length < 3) {
await this.sendHtml(
await this.sendMessage(
roomId,
'<p>Verwendung: <code>!xp [nr] [xp] [aktivitaet]</code></p><p>Optional: <code>--min 60</code> fuer Dauer</p>'
);
@ -373,13 +341,13 @@ export class MatrixService implements OnModuleInit {
const skill = this.getSkillByNumber(sender, args[0]);
if (!skill) {
await this.sendHtml(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!skills</code></p>');
await this.sendMessage(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!skills</code></p>');
return;
}
const xp = parseInt(args[1], 10);
if (isNaN(xp) || xp < 1 || xp > 10000) {
await this.sendHtml(roomId, '<p>XP muss zwischen 1 und 10000 liegen.</p>');
await this.sendMessage(roomId, '<p>XP muss zwischen 1 und 10000 liegen.</p>');
return;
}
@ -401,7 +369,7 @@ export class MatrixService implements OnModuleInit {
const result = await this.skilltreeService.addXp(token, skill.id, xp, description, duration);
if (result.error) {
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
return;
}
@ -414,7 +382,7 @@ export class MatrixService implements OnModuleInit {
html += `<p>&#127881; <strong>LEVEL UP!</strong> Du bist jetzt Level ${newLevel} (${levelName})!</p>`;
}
await this.sendHtml(roomId, html);
await this.sendMessage(roomId, html);
}
// Stats handler
@ -423,7 +391,7 @@ export class MatrixService implements OnModuleInit {
const result = await this.skilltreeService.getStats(token);
if (result.error) {
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
return;
}
@ -438,7 +406,7 @@ export class MatrixService implements OnModuleInit {
}
html += '</ul>';
await this.sendHtml(roomId, html);
await this.sendMessage(roomId, html);
}
// Activities handler
@ -451,7 +419,7 @@ export class MatrixService implements OnModuleInit {
if (numberStr) {
const skill = this.getSkillByNumber(sender, numberStr);
if (!skill) {
await this.sendHtml(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!skills</code></p>');
await this.sendMessage(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!skills</code></p>');
return;
}
result = await this.skilltreeService.getSkillActivities(token, skill.id);
@ -461,14 +429,14 @@ export class MatrixService implements OnModuleInit {
}
if (result.error) {
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
await this.sendMessage(roomId, `<p>Fehler: ${result.error}</p>`);
return;
}
const activities = result.data?.activities || [];
if (activities.length === 0) {
await this.sendHtml(roomId, '<p>Keine Aktivitaeten vorhanden.</p>');
await this.sendMessage(roomId, '<p>Keine Aktivitaeten vorhanden.</p>');
return;
}
@ -487,7 +455,7 @@ export class MatrixService implements OnModuleInit {
}
html += '</ol>';
await this.sendHtml(roomId, html);
await this.sendMessage(roomId, html);
}
// Helper methods