mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
✨ feat(matrix-bots): add i18n system and direct message fallback
- Add I18nService with per-user language preferences (de/en) - Add !language/!sprache command to all 4 bots (todo, calendar, contacts, clock) - Add fallback behavior: messages without commands create tasks/events/contacts/timers - Improve clock bot duration parsing to accept bare numbers as minutes (e.g. "25" = 25min) - Add support for more duration formats: "25 minuten", "1 stunde", etc. Language preferences stored in SessionService, default configurable via BOT_DEFAULT_LANGUAGE env var.
This commit is contained in:
parent
5c688d713e
commit
c2c80efc50
17 changed files with 1626 additions and 18 deletions
|
|
@ -7,6 +7,7 @@ import {
|
|||
SessionModule,
|
||||
CreditModule,
|
||||
CalendarApiService,
|
||||
I18nModule,
|
||||
} from '@manacore/bot-services';
|
||||
|
||||
// Factory provider for CalendarApiService
|
||||
|
|
@ -28,6 +29,7 @@ const calendarApiServiceProvider = {
|
|||
}),
|
||||
SessionModule.forRoot({ storageMode: 'redis' }),
|
||||
CreditModule.forRoot(),
|
||||
I18nModule.forRoot(),
|
||||
],
|
||||
providers: [MatrixService, calendarApiServiceProvider],
|
||||
exports: [MatrixService],
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import {
|
|||
CreditService,
|
||||
CalendarApiService,
|
||||
CalendarEvent as ApiCalendarEvent,
|
||||
I18nService,
|
||||
Language,
|
||||
LANGUAGE_NAMES,
|
||||
} from '@manacore/bot-services';
|
||||
import { CalendarService, CalendarEvent } from '../calendar/calendar.service';
|
||||
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
|
||||
|
|
@ -43,7 +46,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
private calendarService: CalendarService,
|
||||
private calendarApiService: CalendarApiService,
|
||||
private sessionService: SessionService,
|
||||
private creditService: CreditService
|
||||
private creditService: CreditService,
|
||||
private i18nService: I18nService
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
|
@ -132,6 +136,9 @@ export class MatrixService extends BaseMatrixService {
|
|||
await this.executeCommand(roomId, event, sender, keywordCommand, '');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: treat any message as an event
|
||||
await this.handleCreateEvent(roomId, event, sender, message);
|
||||
}
|
||||
|
||||
private async executeCommand(
|
||||
|
|
@ -207,6 +214,12 @@ export class MatrixService extends BaseMatrixService {
|
|||
await this.handleLogout(roomId, event, userId);
|
||||
break;
|
||||
|
||||
case 'language':
|
||||
case 'sprache':
|
||||
case 'lang':
|
||||
await this.handleLanguage(roomId, event, userId, args);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown command - ignore silently
|
||||
break;
|
||||
|
|
@ -695,4 +708,47 @@ export class MatrixService extends BaseMatrixService {
|
|||
this.logger.error(`Failed to send welcome message: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLanguage(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const lang = args.trim().toLowerCase();
|
||||
|
||||
if (!lang) {
|
||||
const currentLang = await this.i18nService.getLanguage(userId);
|
||||
const langName = LANGUAGE_NAMES[currentLang];
|
||||
const available = this.i18nService
|
||||
.getAvailableLanguages()
|
||||
.map((l) => `${l} (${LANGUAGE_NAMES[l]})`)
|
||||
.join(', ');
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`**Sprache / Language:** ${langName}\n\n**Verfügbar / Available:** ${available}\n\nÄndern / Change: \`!language de\` oder / or \`!language en\``
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.i18nService.isValidLanguage(lang)) {
|
||||
const available = this.i18nService.getAvailableLanguages().join(', ');
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`Unbekannte Sprache / Unknown language: ${lang}\n\nVerfügbar / Available: ${available}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.i18nService.setLanguage(userId, lang as Language);
|
||||
const langName = LANGUAGE_NAMES[lang as Language];
|
||||
|
||||
if (lang === 'de') {
|
||||
await this.sendReply(roomId, event, `Sprache geändert zu: **${langName}**`);
|
||||
} else {
|
||||
await this.sendReply(roomId, event, `Language changed to: **${langName}**`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { ClockModule } from '../clock/clock.module';
|
||||
import { TranscriptionModule, SessionModule, CreditModule } from '@manacore/bot-services';
|
||||
import {
|
||||
TranscriptionModule,
|
||||
SessionModule,
|
||||
CreditModule,
|
||||
I18nModule,
|
||||
} from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [ClockModule, TranscriptionModule.forRoot(), SessionModule.forRoot(), CreditModule.forRoot()],
|
||||
imports: [
|
||||
ClockModule,
|
||||
TranscriptionModule.forRoot(),
|
||||
SessionModule.forRoot(),
|
||||
CreditModule.forRoot(),
|
||||
I18nModule.forRoot(),
|
||||
],
|
||||
providers: [MatrixService],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -8,7 +8,14 @@ import {
|
|||
COMMON_KEYWORDS,
|
||||
} from '@manacore/matrix-bot-common';
|
||||
import { ClockService } from '../clock/clock.service';
|
||||
import { TranscriptionService, SessionService, CreditService } from '@manacore/bot-services';
|
||||
import {
|
||||
TranscriptionService,
|
||||
SessionService,
|
||||
CreditService,
|
||||
I18nService,
|
||||
Language,
|
||||
LANGUAGE_NAMES,
|
||||
} from '@manacore/bot-services';
|
||||
import { HELP_TEXT, WELCOME_TEXT } from '../config/configuration';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -30,7 +37,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
private clockService: ClockService,
|
||||
private transcriptionService: TranscriptionService,
|
||||
private sessionService: SessionService,
|
||||
private creditService: CreditService
|
||||
private creditService: CreditService,
|
||||
private i18nService: I18nService
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
|
@ -218,6 +226,12 @@ export class MatrixService extends BaseMatrixService {
|
|||
await this.handleWorldClocksCommand(roomId, event, userId);
|
||||
break;
|
||||
|
||||
case 'language':
|
||||
case 'sprache':
|
||||
case 'lang':
|
||||
await this.handleLanguage(roomId, event, userId, args);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Silently ignore unknown commands
|
||||
break;
|
||||
|
|
@ -676,7 +690,12 @@ export class MatrixService extends BaseMatrixService {
|
|||
}
|
||||
}
|
||||
|
||||
// No match - don't respond to random messages
|
||||
// Fallback: try to parse any message as a timer duration
|
||||
const duration = this.clockService.parseDuration(text);
|
||||
if (duration) {
|
||||
await this.handleTimerCommand(roomId, event, userId, text);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private async getToken(userId: string): Promise<string | null> {
|
||||
|
|
@ -691,4 +710,47 @@ export class MatrixService extends BaseMatrixService {
|
|||
// Entwicklungs-Fallback
|
||||
return this.demoToken || null;
|
||||
}
|
||||
|
||||
private async handleLanguage(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const lang = args.trim().toLowerCase();
|
||||
|
||||
if (!lang) {
|
||||
const currentLang = await this.i18nService.getLanguage(userId);
|
||||
const langName = LANGUAGE_NAMES[currentLang];
|
||||
const available = this.i18nService
|
||||
.getAvailableLanguages()
|
||||
.map((l) => `${l} (${LANGUAGE_NAMES[l]})`)
|
||||
.join(', ');
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`**Sprache / Language:** ${langName}\n\n**Verfügbar / Available:** ${available}\n\nÄndern / Change: \`!language de\` oder / or \`!language en\``
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.i18nService.isValidLanguage(lang)) {
|
||||
const available = this.i18nService.getAvailableLanguages().join(', ');
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`Unbekannte Sprache / Unknown language: ${lang}\n\nVerfügbar / Available: ${available}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.i18nService.setLanguage(userId, lang as Language);
|
||||
const langName = LANGUAGE_NAMES[lang as Language];
|
||||
|
||||
if (lang === 'de') {
|
||||
await this.sendReply(roomId, event, `Sprache geändert zu: **${langName}**`);
|
||||
} else {
|
||||
await this.sendReply(roomId, event, `Language changed to: **${langName}**`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -186,27 +186,27 @@ export class ClockService {
|
|||
parseDuration(input: string): number | null {
|
||||
let totalSeconds = 0;
|
||||
|
||||
// Match hours
|
||||
const hoursMatch = input.match(/(\d+)\s*h/i);
|
||||
// Match hours: 1h, 1 h, 1 stunde, 1 stunden, 1 hour, 1 hours
|
||||
const hoursMatch = input.match(/(\d+)\s*(?:h|stunde[n]?|hour[s]?)\b/i);
|
||||
if (hoursMatch) {
|
||||
totalSeconds += parseInt(hoursMatch[1], 10) * 3600;
|
||||
}
|
||||
|
||||
// Match minutes
|
||||
const minutesMatch = input.match(/(\d+)\s*m(?:in)?/i);
|
||||
// Match minutes: 25m, 25 m, 25min, 25 min, 25 minuten, 25 minute, 25 minutes
|
||||
const minutesMatch = input.match(/(\d+)\s*(?:m|min|minute[n]?|minutes?)\b/i);
|
||||
if (minutesMatch) {
|
||||
totalSeconds += parseInt(minutesMatch[1], 10) * 60;
|
||||
}
|
||||
|
||||
// Match seconds
|
||||
const secondsMatch = input.match(/(\d+)\s*s(?:ec)?/i);
|
||||
// Match seconds: 30s, 30 s, 30sec, 30 sec, 30 sekunden, 30 seconds
|
||||
const secondsMatch = input.match(/(\d+)\s*(?:s|sec|sekunde[n]?|seconds?)\b/i);
|
||||
if (secondsMatch) {
|
||||
totalSeconds += parseInt(secondsMatch[1], 10);
|
||||
}
|
||||
|
||||
// If just a number, assume minutes
|
||||
// If just a number (with optional whitespace), assume minutes
|
||||
if (totalSeconds === 0) {
|
||||
const justNumber = input.match(/^(\d+)$/);
|
||||
const justNumber = input.trim().match(/^(\d+)$/);
|
||||
if (justNumber) {
|
||||
totalSeconds = parseInt(justNumber[1], 10) * 60;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { ContactsModule } from '../contacts/contacts.module';
|
||||
import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-services';
|
||||
import {
|
||||
SessionModule,
|
||||
TranscriptionModule,
|
||||
CreditModule,
|
||||
I18nModule,
|
||||
} from '@manacore/bot-services';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -11,6 +16,7 @@ import { SessionModule, TranscriptionModule, CreditModule } from '@manacore/bot-
|
|||
sttUrl: process.env.STT_URL || 'http://localhost:3020',
|
||||
}),
|
||||
CreditModule.forRoot(),
|
||||
I18nModule.forRoot(),
|
||||
],
|
||||
providers: [MatrixService],
|
||||
exports: [MatrixService],
|
||||
|
|
|
|||
|
|
@ -9,7 +9,14 @@ import {
|
|||
UserListMapper,
|
||||
} from '@manacore/matrix-bot-common';
|
||||
import { ContactsService, Contact } from '../contacts/contacts.service';
|
||||
import { SessionService, TranscriptionService, CreditService } from '@manacore/bot-services';
|
||||
import {
|
||||
SessionService,
|
||||
TranscriptionService,
|
||||
CreditService,
|
||||
I18nService,
|
||||
Language,
|
||||
LANGUAGE_NAMES,
|
||||
} from '@manacore/bot-services';
|
||||
import { HELP_MESSAGE } from '../config/configuration';
|
||||
|
||||
const CONTACT_CREATE_CREDITS = 0.02;
|
||||
|
|
@ -32,7 +39,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
private readonly transcriptionService: TranscriptionService,
|
||||
private contactsService: ContactsService,
|
||||
private sessionService: SessionService,
|
||||
private creditService: CreditService
|
||||
private creditService: CreditService,
|
||||
private i18nService: I18nService
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
|
@ -102,6 +110,10 @@ Sag "hilfe" fur alle Befehle!`;
|
|||
await this.handleCommand(roomId, event, sender, `!${detectedCommand}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: treat any message as a new contact
|
||||
const args = message.trim().split(/\s+/);
|
||||
await this.handleCreateContact(roomId, event, sender, args);
|
||||
}
|
||||
|
||||
private async handleCommand(
|
||||
|
|
@ -188,6 +200,12 @@ Sag "hilfe" fur alle Befehle!`;
|
|||
await this.pinHelpMessage(roomId, event);
|
||||
break;
|
||||
|
||||
case 'language':
|
||||
case 'sprache':
|
||||
case 'lang':
|
||||
await this.handleLanguage(roomId, event, sender, argString);
|
||||
break;
|
||||
|
||||
default:
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
|
|
@ -766,4 +784,47 @@ Sag "hilfe" fur alle Befehle!`;
|
|||
await this.sendReply(roomId, event, 'Fehler beim Pinnen der Hilfe.');
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLanguage(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const lang = args.trim().toLowerCase();
|
||||
|
||||
if (!lang) {
|
||||
const currentLang = await this.i18nService.getLanguage(userId);
|
||||
const langName = LANGUAGE_NAMES[currentLang];
|
||||
const available = this.i18nService
|
||||
.getAvailableLanguages()
|
||||
.map((l) => `${l} (${LANGUAGE_NAMES[l]})`)
|
||||
.join(', ');
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`**Sprache / Language:** ${langName}\n\n**Verfügbar / Available:** ${available}\n\nÄndern / Change: \`!language de\` oder / or \`!language en\``
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.i18nService.isValidLanguage(lang)) {
|
||||
const available = this.i18nService.getAvailableLanguages().join(', ');
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`Unbekannte Sprache / Unknown language: ${lang}\n\nVerfügbar / Available: ${available}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.i18nService.setLanguage(userId, lang as Language);
|
||||
const langName = LANGUAGE_NAMES[lang as Language];
|
||||
|
||||
if (lang === 'de') {
|
||||
await this.sendReply(roomId, event, `Sprache geändert zu: **${langName}**`);
|
||||
} else {
|
||||
await this.sendReply(roomId, event, `Language changed to: **${langName}**`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
SessionModule,
|
||||
CreditModule,
|
||||
TodoApiService,
|
||||
I18nModule,
|
||||
} from '@manacore/bot-services';
|
||||
|
||||
// Factory provider for TodoApiService
|
||||
|
|
@ -26,6 +27,7 @@ const todoApiServiceProvider = {
|
|||
TranscriptionModule.forRoot(),
|
||||
SessionModule.forRoot({ storageMode: 'redis' }),
|
||||
CreditModule.forRoot(),
|
||||
I18nModule.forRoot(),
|
||||
],
|
||||
providers: [MatrixService, todoApiServiceProvider],
|
||||
exports: [MatrixService],
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ import {
|
|||
CreditService,
|
||||
TodoApiService,
|
||||
Task as ApiTask,
|
||||
I18nService,
|
||||
Language,
|
||||
LANGUAGE_NAMES,
|
||||
} from '@manacore/bot-services';
|
||||
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
|
||||
|
||||
|
|
@ -44,7 +47,8 @@ export class MatrixService extends BaseMatrixService {
|
|||
private todoApiService: TodoApiService,
|
||||
private transcriptionService: TranscriptionService,
|
||||
private sessionService: SessionService,
|
||||
private creditService: CreditService
|
||||
private creditService: CreditService,
|
||||
private i18nService: I18nService
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
|
@ -108,7 +112,11 @@ export class MatrixService extends BaseMatrixService {
|
|||
if (body.startsWith('!')) {
|
||||
const [command, ...args] = body.slice(1).split(' ');
|
||||
await this.executeCommand(roomId, event, userId, command.toLowerCase(), args.join(' '));
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: treat any message as a task
|
||||
await this.handleAddTask(roomId, event, userId, body);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error handling message: ${error}`);
|
||||
await this.sendReply(roomId, event, 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.');
|
||||
|
|
@ -299,12 +307,64 @@ export class MatrixService extends BaseMatrixService {
|
|||
await this.handlePinHelp(roomId, event);
|
||||
break;
|
||||
|
||||
case 'language':
|
||||
case 'sprache':
|
||||
case 'lang':
|
||||
await this.handleLanguage(roomId, event, userId, args);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown command - ignore silently or send help
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLanguage(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
userId: string,
|
||||
args: string
|
||||
) {
|
||||
const lang = args.trim().toLowerCase();
|
||||
|
||||
// Show current language if no argument
|
||||
if (!lang) {
|
||||
const currentLang = await this.i18nService.getLanguage(userId);
|
||||
const langName = LANGUAGE_NAMES[currentLang];
|
||||
const available = this.i18nService
|
||||
.getAvailableLanguages()
|
||||
.map((l) => `${l} (${LANGUAGE_NAMES[l]})`)
|
||||
.join(', ');
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`**Sprache / Language:** ${langName}\n\n**Verfügbar / Available:** ${available}\n\nÄndern / Change: \`!language de\` oder / or \`!language en\``
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and set language
|
||||
if (!this.i18nService.isValidLanguage(lang)) {
|
||||
const available = this.i18nService.getAvailableLanguages().join(', ');
|
||||
await this.sendReply(
|
||||
roomId,
|
||||
event,
|
||||
`Unbekannte Sprache / Unknown language: ${lang}\n\nVerfügbar / Available: ${available}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.i18nService.setLanguage(userId, lang as Language);
|
||||
const langName = LANGUAGE_NAMES[lang as Language];
|
||||
|
||||
// Respond in the new language
|
||||
if (lang === 'de') {
|
||||
await this.sendReply(roomId, event, `Sprache geändert zu: **${langName}**`);
|
||||
} else {
|
||||
await this.sendReply(roomId, event, `Language changed to: **${langName}**`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAddTask(
|
||||
roomId: string,
|
||||
event: MatrixRoomEvent,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue