♻️ refactor: migrate all remaining bots to shared services

Completed migration of all Matrix bots to @manacore/bot-services:

**SessionService (11 bots migrated):**
- matrix-chat-bot (with conversation/model mapping via setSessionData)
- matrix-contacts-bot
- matrix-skilltree-bot
- matrix-presi-bot
- matrix-questions-bot
- matrix-storage-bot
- matrix-planta-bot
- matrix-manadeck-bot
- matrix-nutriphi-bot (with pendingImage via setSessionData)
- matrix-picture-bot (previous commit)
- matrix-zitare-bot (previous commit)

**TranscriptionService (5 bots migrated):**
- matrix-todo-bot (previous commit)
- matrix-clock-bot (previous commit)
- matrix-zitare-bot (previous commit)
- matrix-nutriphi-bot
- matrix-project-doc-bot

**Code Reduction:**
- Deleted 22 local module files (session + transcription)
- ~1100 lines of duplicate code removed total
- All bots now share identical auth and STT logic

**Special handling:**
- matrix-chat-bot: Extended methods converted to setSessionData/getSessionData
- matrix-nutriphi-bot: pendingImage state via setSessionData
- matrix-project-doc-bot: TranscriptionService used by MediaService

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-01 00:50:48 +01:00
parent 9b61831cb5
commit 2b979d5548
61 changed files with 640 additions and 1393 deletions

View file

@ -1,11 +1,10 @@
import { Module } from '@nestjs/common';
import { MatrixService } from './matrix.service';
import { NutriPhiModule } from '../nutriphi/nutriphi.module';
import { SessionModule } from '../session/session.module';
import { TranscriptionModule } from '../transcription/transcription.module';
import { SessionModule, TranscriptionModule } from '@manacore/bot-services';
@Module({
imports: [NutriPhiModule, SessionModule, TranscriptionModule],
imports: [NutriPhiModule, SessionModule.forRoot(), TranscriptionModule.forRoot()],
providers: [MatrixService],
exports: [MatrixService],
})

View file

@ -13,8 +13,7 @@ import {
DailySummary,
WeeklyStats,
} from '../nutriphi/nutriphi.service';
import { SessionService } from '../session/session.service';
import { TranscriptionService } from '../transcription/transcription.service';
import { SessionService, TranscriptionService } from '@manacore/bot-services';
import { HELP_MESSAGE, MEAL_TYPE_LABELS } from '../config/configuration';
// Natural language keywords that trigger commands (German + English)
@ -136,11 +135,10 @@ Sag "hilfe" fur alle Befehle!`;
// Handle image messages
if (content.msgtype === 'm.image' && content.url) {
this.sessionService.setPendingImage(
event.sender,
content.url,
content.info?.mimetype || 'image/png'
);
this.sessionService.setSessionData(event.sender, 'pendingImage', {
url: content.url,
mimeType: content.info?.mimetype || 'image/png',
});
this.logger.log(`Image received from ${event.sender}`);
await this.sendMessage(
roomId,
@ -298,7 +296,10 @@ Sag "hilfe" fur alle Befehle!`;
return;
}
const pendingImage = this.sessionService.getPendingImage(sender);
const pendingImage = this.sessionService.getSessionData<{ url: string; mimeType: string }>(
sender,
'pendingImage'
);
// If no image and no description, show help
if (!pendingImage && !description.trim()) {
@ -319,7 +320,7 @@ Sag "hilfe" fur alle Befehle!`;
await this.sendMessage(roomId, 'Analysiere Bild...');
const imageData = await this.downloadMatrixImage(pendingImage.url);
result = await this.nutriphiService.analyzePhoto(imageData, pendingImage.mimeType, token);
this.sessionService.clearPendingImage(sender);
this.sessionService.setSessionData(sender, 'pendingImage', null);
} else {
// Analyze text
await this.sendMessage(roomId, `Analysiere: "${description}"...`);
@ -634,7 +635,7 @@ Sag "hilfe" fur alle Befehle!`;
const backendHealthy = await this.nutriphiService.checkHealth();
const isLoggedIn = this.sessionService.isLoggedIn(sender);
const sessionCount = this.sessionService.getSessionCount();
const loggedInCount = this.sessionService.getLoggedInCount();
const loggedInCount = this.sessionService.getActiveSessionCount();
const statusText = `**NutriPhi Bot Status**

View file

@ -1,8 +0,0 @@
import { Module } from '@nestjs/common';
import { SessionService } from './session.service';
@Module({
providers: [SessionService],
exports: [SessionService],
})
export class SessionModule {}

View file

@ -1,152 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
export interface UserSession {
matrixUserId: string;
jwtToken?: string;
tokenExpiry?: Date;
pendingImage?: { url: string; mimeType: string };
lastActivity: Date;
}
export interface LoginResult {
success: boolean;
token?: string;
error?: string;
}
@Injectable()
export class SessionService {
private readonly logger = new Logger(SessionService.name);
private sessions: Map<string, UserSession> = new Map();
private readonly authUrl: string;
private readonly devBypass: boolean;
private readonly devUserId: string;
constructor(private configService: ConfigService) {
this.authUrl = this.configService.get<string>('auth.url') || 'http://localhost:3001';
this.devBypass = this.configService.get<boolean>('auth.devBypass') || false;
this.devUserId = this.configService.get<string>('auth.devUserId') || '';
}
getSession(matrixUserId: string): UserSession {
if (!this.sessions.has(matrixUserId)) {
this.sessions.set(matrixUserId, {
matrixUserId,
lastActivity: new Date(),
});
}
const session = this.sessions.get(matrixUserId)!;
session.lastActivity = new Date();
return session;
}
isLoggedIn(matrixUserId: string): boolean {
if (this.devBypass && this.devUserId) {
return true;
}
const session = this.sessions.get(matrixUserId);
if (!session?.jwtToken || !session.tokenExpiry) {
return false;
}
// Check if token is expired (with 5 minute buffer)
const now = new Date();
const expiryBuffer = new Date(session.tokenExpiry.getTime() - 5 * 60 * 1000);
return now < expiryBuffer;
}
getToken(matrixUserId: string): string | null {
if (this.devBypass && this.devUserId) {
// In dev mode, return a mock token (the backend should also bypass auth)
return 'dev-bypass-token';
}
const session = this.sessions.get(matrixUserId);
if (!session?.jwtToken || !this.isLoggedIn(matrixUserId)) {
return null;
}
return session.jwtToken;
}
async login(matrixUserId: string, email: string, password: string): Promise<LoginResult> {
try {
const response = await fetch(`${this.authUrl}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.text();
this.logger.warn(`Login failed for ${matrixUserId}: ${response.status}`);
return { success: false, error: `Login fehlgeschlagen: ${error}` };
}
const data = await response.json();
const { accessToken, expiresIn } = data;
if (!accessToken) {
return { success: false, error: 'Kein Token erhalten' };
}
// Calculate expiry time (expiresIn is in seconds)
const expiryTime = expiresIn
? new Date(Date.now() + expiresIn * 1000)
: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // Default: 7 days
const session = this.getSession(matrixUserId);
session.jwtToken = accessToken;
session.tokenExpiry = expiryTime;
this.logger.log(`User ${matrixUserId} logged in successfully`);
return { success: true, token: accessToken };
} catch (error) {
this.logger.error(`Login error for ${matrixUserId}:`, error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unbekannter Fehler',
};
}
}
logout(matrixUserId: string): void {
const session = this.sessions.get(matrixUserId);
if (session) {
session.jwtToken = undefined;
session.tokenExpiry = undefined;
}
this.logger.log(`User ${matrixUserId} logged out`);
}
setPendingImage(matrixUserId: string, url: string, mimeType: string): void {
const session = this.getSession(matrixUserId);
session.pendingImage = { url, mimeType };
}
getPendingImage(matrixUserId: string): { url: string; mimeType: string } | undefined {
return this.sessions.get(matrixUserId)?.pendingImage;
}
clearPendingImage(matrixUserId: string): void {
const session = this.sessions.get(matrixUserId);
if (session) {
session.pendingImage = undefined;
}
}
getSessionCount(): number {
return this.sessions.size;
}
getLoggedInCount(): number {
let count = 0;
for (const [userId] of this.sessions) {
if (this.isLoggedIn(userId)) {
count++;
}
}
return count;
}
}

View file

@ -1,8 +0,0 @@
import { Module } from '@nestjs/common';
import { TranscriptionService } from './transcription.service';
@Module({
providers: [TranscriptionService],
exports: [TranscriptionService],
})
export class TranscriptionModule {}

View file

@ -1,54 +0,0 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
interface SttResponse {
text: string;
language?: string;
model?: string;
}
@Injectable()
export class TranscriptionService {
private readonly logger = new Logger(TranscriptionService.name);
private readonly sttUrl: string;
constructor(private configService: ConfigService) {
this.sttUrl = this.configService.get<string>('stt.url') || 'http://localhost:3020';
this.logger.log(`STT Service URL: ${this.sttUrl}`);
}
async transcribe(audioBuffer: Buffer, language: string = 'de'): Promise<string> {
const formData = new FormData();
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/ogg' });
formData.append('file', blob, 'audio.ogg');
formData.append('language', language);
try {
const response = await fetch(`${this.sttUrl}/transcribe`, {
method: 'POST',
body: formData,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`STT service error: ${response.status} - ${errorText}`);
}
const result: SttResponse = await response.json();
this.logger.log(`Transcription completed: ${result.text.substring(0, 50)}...`);
return result.text;
} catch (error) {
this.logger.error('Transcription failed:', error);
throw error;
}
}
async checkHealth(): Promise<boolean> {
try {
const response = await fetch(`${this.sttUrl}/health`);
return response.ok;
} catch {
return false;
}
}
}