managarten/services/matrix-mana-bot/src/bot/command-router.service.ts
Till-JS dc0d425f61 🐛 fix(matrix-web): handle Matrix SSO loginToken callback
Add loginWithLoginToken function to exchange Matrix SSO loginToken for credentials.
The app layout now detects the loginToken URL parameter and completes the SSO flow.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:02:47 +01:00

303 lines
8.8 KiB
TypeScript

import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
import { KeywordCommandDetector, COMMON_KEYWORDS } from '@manacore/matrix-bot-common';
import { AiHandler } from '../handlers/ai.handler';
import { TodoHandler } from '../handlers/todo.handler';
import { CalendarHandler } from '../handlers/calendar.handler';
import { ClockHandler } from '../handlers/clock.handler';
import { HelpHandler } from '../handlers/help.handler';
import { VoiceHandler } from '../handlers/voice.handler';
import { OrchestrationService } from '../orchestration/orchestration.service';
export interface CommandContext {
roomId: string;
userId: string;
message: string;
event: any;
isVoice?: boolean; // True if message came from voice input
}
interface CommandRoute {
patterns: (string | RegExp)[];
handler: (ctx: CommandContext, args: string) => Promise<string>;
description: string;
}
@Injectable()
export class CommandRouterService {
private readonly keywordDetector = new KeywordCommandDetector([
...COMMON_KEYWORDS,
{ keywords: ['modelle', 'models', 'welche modelle', 'ai models'], command: 'models' },
{ keywords: ['meine aufgaben', 'zeige aufgaben', 'todo liste', 'was muss ich', 'aufgaben'], command: 'list' },
{ keywords: ['heute', 'was steht heute an', 'today'], command: 'today' },
{ keywords: ['termine', 'kalender', 'meine termine', 'calendar'], command: 'cal' },
{ keywords: ['timer', 'stoppuhr', 'zeitmesser'], command: 'timers' },
{ keywords: ['zusammenfassung', 'wie war mein tag', 'tagesrueckblick', 'summary'], command: 'summary' },
{ keywords: ['todo', 'aufgabe', 'neue aufgabe', 'task'], command: 'todo' },
{ keywords: ['alarm', 'wecker', 'alarme'], command: 'alarms' },
{ keywords: ['clear', 'loeschen', 'verlauf loeschen', 'reset'], command: 'clear' },
]);
private readonly logger = new Logger(CommandRouterService.name);
private routes: CommandRoute[] = [];
constructor(
@Inject(forwardRef(() => AiHandler))
private aiHandler: AiHandler,
@Inject(forwardRef(() => TodoHandler))
private todoHandler: TodoHandler,
@Inject(forwardRef(() => CalendarHandler))
private calendarHandler: CalendarHandler,
@Inject(forwardRef(() => ClockHandler))
private clockHandler: ClockHandler,
@Inject(forwardRef(() => HelpHandler))
private helpHandler: HelpHandler,
@Inject(forwardRef(() => VoiceHandler))
private voiceHandler: VoiceHandler,
@Inject(forwardRef(() => OrchestrationService))
private orchestration: OrchestrationService
) {
this.initializeRoutes();
}
private initializeRoutes() {
this.routes = [
// Help
{
patterns: ['!help', '!start', '!hilfe'],
handler: (ctx) => this.helpHandler.showHelp(ctx),
description: 'Show help',
},
// Auth Commands
{
patterns: ['!login'],
handler: (ctx, args) => this.helpHandler.handleLogin(ctx, args),
description: 'Login with email and password',
},
{
patterns: ['!logout'],
handler: (ctx) => this.helpHandler.handleLogout(ctx),
description: 'Logout',
},
// AI Commands
{
patterns: ['!models', '!modelle'],
handler: (ctx) => this.aiHandler.listModels(ctx),
description: 'List AI models',
},
{
patterns: ['!model'],
handler: (ctx, args) => this.aiHandler.setModel(ctx, args),
description: 'Switch AI model',
},
{
patterns: ['!all'],
handler: (ctx, args) => this.aiHandler.compareAll(ctx, args),
description: 'Compare all models',
},
{
patterns: ['!clear', '!reset'],
handler: (ctx) => this.aiHandler.clearHistory(ctx),
description: 'Clear chat history',
},
// Todo Commands
{
patterns: ['!todo', '!add', '!neu'],
handler: (ctx, args) => this.todoHandler.create(ctx, args),
description: 'Create todo',
},
{
patterns: ['!list', '!liste', '!alle'],
handler: (ctx) => this.todoHandler.list(ctx),
description: 'List todos',
},
{
patterns: ['!today', '!heute'],
handler: (ctx) => this.todoHandler.today(ctx),
description: "Today's todos",
},
{
patterns: ['!inbox'],
handler: (ctx) => this.todoHandler.inbox(ctx),
description: 'Inbox todos',
},
{
patterns: ['!done', '!erledigt', '!fertig'],
handler: (ctx, args) => this.todoHandler.complete(ctx, args),
description: 'Complete todo',
},
{
patterns: ['!delete', '!löschen'],
handler: (ctx, args) => this.todoHandler.delete(ctx, args),
description: 'Delete todo',
},
{
patterns: ['!projects', '!projekte'],
handler: (ctx) => this.todoHandler.projects(ctx),
description: 'List projects',
},
// Calendar Commands
{
patterns: ['!cal', '!termine'],
handler: (ctx) => this.calendarHandler.today(ctx),
description: "Today's events",
},
{
patterns: ['!week', '!woche'],
handler: (ctx) => this.calendarHandler.week(ctx),
description: 'Week events',
},
{
patterns: ['!event', '!termin'],
handler: (ctx, args) => this.calendarHandler.create(ctx, args),
description: 'Create event',
},
{
patterns: ['!calendars', '!kalender'],
handler: (ctx) => this.calendarHandler.listCalendars(ctx),
description: 'List calendars',
},
// Clock Commands
{
patterns: ['!timer'],
handler: (ctx, args) => this.clockHandler.startTimer(ctx, args),
description: 'Start timer',
},
{
patterns: ['!timers'],
handler: (ctx) => this.clockHandler.listTimers(ctx),
description: 'List timers',
},
{
patterns: ['!alarm'],
handler: (ctx, args) => this.clockHandler.setAlarm(ctx, args),
description: 'Set alarm',
},
{
patterns: ['!alarms'],
handler: (ctx) => this.clockHandler.listAlarms(ctx),
description: 'List alarms',
},
{
patterns: ['!time', '!zeit'],
handler: (ctx, args) => this.clockHandler.worldClock(ctx, args),
description: 'World clock',
},
{
patterns: ['!stop'],
handler: (ctx, args) => this.clockHandler.stopTimer(ctx, args),
description: 'Stop timer',
},
// Cross-Feature (Orchestration)
{
patterns: ['!summary', '!zusammenfassung'],
handler: (ctx) => this.orchestration.dailySummary(ctx),
description: 'Daily summary',
},
{
patterns: ['!ai-todo'],
handler: (ctx, args) => this.orchestration.aiToTodos(ctx, args),
description: 'AI extracts todos',
},
// Status
{
patterns: ['!status'],
handler: (ctx) => this.helpHandler.showStatus(ctx),
description: 'Show status',
},
// Voice Commands
{
patterns: ['!voice', '!sprache'],
handler: (ctx, args) => this.voiceHandler.voiceSettings(ctx, args),
description: 'Voice settings',
},
{
patterns: ['!stimmen', '!voices'],
handler: (ctx) => this.voiceHandler.listVoices(ctx),
description: 'List voices',
},
{
patterns: ['!stimme'],
handler: (ctx, args) => this.voiceHandler.setVoice(ctx, args),
description: 'Set voice',
},
{
patterns: ['!speed', '!tempo', '!geschwindigkeit'],
handler: (ctx, args) => this.voiceHandler.setSpeed(ctx, args),
description: 'Set speech speed',
},
];
}
async route(ctx: CommandContext): Promise<string | null> {
const message = ctx.message.trim();
// Check for natural language keywords first
const keywordCommand = this.detectKeywordCommand(message);
if (keywordCommand) {
return this.routeCommand({ ...ctx, message: keywordCommand });
}
// Check for ! commands
if (message.startsWith('!')) {
return this.routeCommand(ctx);
}
// Default: treat as AI chat
return this.aiHandler.chat(ctx, message);
}
private async routeCommand(ctx: CommandContext): Promise<string | null> {
const { command, args } = this.parseCommand(ctx.message);
for (const route of this.routes) {
if (this.matchesPattern(command, route.patterns)) {
this.logger.debug(`Routing "${command}" to ${route.description}`);
try {
return await route.handler(ctx, args);
} catch (error) {
this.logger.error(`Error in handler for "${command}":`, error);
return `❌ Fehler: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`;
}
}
}
// Unknown command
return null;
}
private detectKeywordCommand(message: string): string | null {
const command = this.keywordDetector.detect(message);
if (command) {
this.logger.debug(`Detected keyword -> "!${command}"`);
return `!${command}`;
}
return null;
}
private matchesPattern(command: string, patterns: (string | RegExp)[]): boolean {
for (const pattern of patterns) {
if (typeof pattern === 'string') {
if (command === pattern) return true;
} else if (pattern.test(command)) {
return true;
}
}
return false;
}
private parseCommand(message: string): { command: string; args: string } {
const trimmed = message.trim();
if (trimmed.startsWith('!')) {
const [cmd, ...rest] = trimmed.split(' ');
return { command: cmd.toLowerCase(), args: rest.join(' ') };
}
return { command: '', args: trimmed };
}
}