mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-18 16:09:44 +02:00
- Remove !login and !logout commands from all 16+ Matrix bots - Remove login/logout references from all help/welcome messages - Disable password login in Synapse (password_config.enabled: false) - System is now OIDC-only via Mana Core authentication Users must authenticate via "Sign in with Mana Core" in Element. Existing bot access tokens remain valid. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
888 lines
24 KiB
TypeScript
888 lines
24 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import {
|
|
BaseMatrixService,
|
|
MatrixBotConfig,
|
|
MatrixRoomEvent,
|
|
KeywordCommandDetector,
|
|
COMMON_KEYWORDS,
|
|
handleCreditCommand,
|
|
handleGiftCommand,
|
|
type CreditCommandsHost,
|
|
type GiftCommandsHost,
|
|
} from '@manacore/matrix-bot-common';
|
|
import {
|
|
TranscriptionService,
|
|
SessionService,
|
|
CreditService,
|
|
GiftService,
|
|
TodoApiService,
|
|
Task as ApiTask,
|
|
I18nService,
|
|
Language,
|
|
LANGUAGE_NAMES,
|
|
} from '@manacore/bot-services';
|
|
import { HELP_TEXT, WELCOME_TEXT, BOT_INTRODUCTION } from '../config/configuration';
|
|
|
|
// Credit cost for task creation (micro-credits)
|
|
const TASK_CREATE_CREDITS = 0.02;
|
|
|
|
// Alias for consistency
|
|
type Task = ApiTask;
|
|
|
|
@Injectable()
|
|
export class MatrixService
|
|
extends BaseMatrixService
|
|
implements CreditCommandsHost, GiftCommandsHost
|
|
{
|
|
// Expose services for credit and gift commands mixins
|
|
public creditService: CreditService;
|
|
public giftService: GiftService;
|
|
public i18nService: I18nService;
|
|
public sessionService: SessionService;
|
|
private readonly keywordDetector = new KeywordCommandDetector(
|
|
[
|
|
...COMMON_KEYWORDS,
|
|
{ keywords: ['was kannst du', 'hilfe', 'help'], command: 'help' },
|
|
{
|
|
keywords: [
|
|
'zeige aufgaben',
|
|
'meine aufgaben',
|
|
'was muss ich',
|
|
'show tasks',
|
|
'list',
|
|
'liste',
|
|
'alle',
|
|
],
|
|
command: 'list',
|
|
},
|
|
{ keywords: ['heute', 'today', 'was steht an'], command: 'today' },
|
|
{ keywords: ['inbox', 'eingang', 'ohne datum'], command: 'inbox' },
|
|
{ keywords: ['projekte', 'projects'], command: 'projects' },
|
|
{ keywords: ['verbindung', 'connection', 'status'], command: 'status' },
|
|
{ keywords: ['neu', 'neue', 'add'], command: 'add' },
|
|
{ keywords: ['erledigt', 'fertig', 'done'], command: 'done' },
|
|
{ keywords: ['löschen', 'entfernen', 'delete'], command: 'delete' },
|
|
{ keywords: ['projekt', 'project'], command: 'project' },
|
|
{ keywords: ['pin'], command: 'pin' },
|
|
{ keywords: ['login', 'anmelden'], command: 'login' },
|
|
{ keywords: ['logout', 'abmelden'], command: 'logout' },
|
|
{ keywords: ['sprache', 'language', 'lang'], command: 'language' },
|
|
{ keywords: ['credits', 'guthaben', 'kontostand'], command: 'credits' },
|
|
{ keywords: ['packages', 'pakete', 'preise'], command: 'packages' },
|
|
{ keywords: ['kaufen', 'buy'], command: 'buy' },
|
|
],
|
|
{ partialMatch: true }
|
|
);
|
|
|
|
constructor(
|
|
configService: ConfigService,
|
|
private todoApiService: TodoApiService,
|
|
private transcriptionService: TranscriptionService,
|
|
sessionService: SessionService,
|
|
creditService: CreditService,
|
|
giftService: GiftService,
|
|
i18nService: I18nService
|
|
) {
|
|
super(configService);
|
|
// Assign to public properties for credit and gift commands mixins
|
|
this.sessionService = sessionService;
|
|
this.creditService = creditService;
|
|
this.giftService = giftService;
|
|
this.i18nService = i18nService;
|
|
}
|
|
|
|
// ============================================================================
|
|
// CreditCommandsHost interface implementation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Send a credit message (delegates to protected sendMessage)
|
|
*/
|
|
async sendCreditMessage(roomId: string, message: string): Promise<void> {
|
|
await this.sendMessage(roomId, message);
|
|
}
|
|
|
|
/**
|
|
* Send a credit reply (delegates to protected sendReply)
|
|
*/
|
|
async sendCreditReply(roomId: string, event: MatrixRoomEvent, message: string): Promise<void> {
|
|
await this.sendReply(roomId, event, message);
|
|
}
|
|
|
|
// ============================================================================
|
|
// GiftCommandsHost interface implementation
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Send a gift message (delegates to protected sendMessage)
|
|
*/
|
|
async sendGiftMessage(roomId: string, message: string): Promise<void> {
|
|
await this.sendMessage(roomId, message);
|
|
}
|
|
|
|
/**
|
|
* Send a gift reply (delegates to protected sendReply)
|
|
*/
|
|
async sendGiftReply(roomId: string, event: MatrixRoomEvent, message: string): Promise<void> {
|
|
await this.sendReply(roomId, event, message);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Private helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if user is logged in and has a valid token for API access
|
|
*/
|
|
private async getToken(userId: string): Promise<string | null> {
|
|
return this.sessionService.getToken(userId);
|
|
}
|
|
|
|
/**
|
|
* Require login - returns token or sends login prompt and returns null
|
|
*/
|
|
private async requireLogin(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
userId: string
|
|
): Promise<string | null> {
|
|
const token = await this.getToken(userId);
|
|
if (!token) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'🔐 **Login erforderlich**\n\n' +
|
|
'Um Aufgaben zu verwalten, melde dich bitte an:\n\n' +
|
|
'`login deine@email.de deinpasswort`\n\n' +
|
|
'Deine Aufgaben werden dann mit der Todo-App synchronisiert.'
|
|
);
|
|
return null;
|
|
}
|
|
return token;
|
|
}
|
|
|
|
/**
|
|
* Normalize task from API format
|
|
*/
|
|
private normalizeTask(task: ApiTask): Task {
|
|
return {
|
|
id: task.id,
|
|
title: task.title,
|
|
completed: task.completed,
|
|
priority: task.priority,
|
|
dueDate: task.dueDate,
|
|
project: task.project,
|
|
labels: task.labels || [],
|
|
createdAt: task.createdAt,
|
|
completedAt: task.completedAt || null,
|
|
userId: task.userId,
|
|
};
|
|
}
|
|
|
|
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') || [],
|
|
};
|
|
}
|
|
|
|
protected getIntroductionMessage(): string {
|
|
return BOT_INTRODUCTION;
|
|
}
|
|
|
|
// Commands that can be used without ! prefix
|
|
private readonly directCommands = [
|
|
'help',
|
|
'hilfe',
|
|
'add',
|
|
'neu',
|
|
'neue',
|
|
'list',
|
|
'liste',
|
|
'alle',
|
|
'heute',
|
|
'today',
|
|
'inbox',
|
|
'eingang',
|
|
'done',
|
|
'erledigt',
|
|
'fertig',
|
|
'delete',
|
|
'löschen',
|
|
'entfernen',
|
|
'projects',
|
|
'projekte',
|
|
'project',
|
|
'projekt',
|
|
'status',
|
|
'pin',
|
|
'login',
|
|
'logout',
|
|
'language',
|
|
'sprache',
|
|
'lang',
|
|
// Credit commands
|
|
'credits',
|
|
'guthaben',
|
|
'packages',
|
|
'pakete',
|
|
'buy',
|
|
'kaufen',
|
|
// Gift commands
|
|
'geschenk',
|
|
'gift',
|
|
'einloesen',
|
|
'redeem',
|
|
'meine-geschenke',
|
|
'my-gifts',
|
|
];
|
|
|
|
protected async handleTextMessage(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
body: string
|
|
): Promise<void> {
|
|
const userId = event.sender;
|
|
|
|
try {
|
|
// Check for ! commands first
|
|
if (body.startsWith('!')) {
|
|
const [command, ...args] = body.slice(1).split(' ');
|
|
await this.executeCommand(roomId, event, userId, command.toLowerCase(), args.join(' '));
|
|
return;
|
|
}
|
|
|
|
// Check for direct commands (without ! prefix)
|
|
const trimmedBody = body.trim();
|
|
const words = trimmedBody.split(/\s+/);
|
|
const firstWord = words[0].toLowerCase();
|
|
|
|
if (this.directCommands.includes(firstWord)) {
|
|
const args = words.slice(1).join(' ');
|
|
await this.executeCommand(roomId, event, userId, firstWord, args);
|
|
return;
|
|
}
|
|
|
|
// Check for natural language keywords
|
|
const keywordCommand = this.keywordDetector.detect(body);
|
|
if (keywordCommand) {
|
|
// For commands that need args, try to extract from the message
|
|
const args = this.extractArgsAfterKeyword(body, keywordCommand);
|
|
await this.executeCommand(roomId, event, userId, keywordCommand, args);
|
|
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.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract arguments after a keyword match
|
|
*/
|
|
private extractArgsAfterKeyword(body: string, command: string): string {
|
|
// Map commands to their trigger keywords
|
|
const commandKeywords: Record<string, string[]> = {
|
|
add: ['neu', 'neue', 'add'],
|
|
done: ['erledigt', 'fertig', 'done'],
|
|
delete: ['löschen', 'entfernen', 'delete'],
|
|
project: ['projekt', 'project'],
|
|
login: ['login', 'anmelden'],
|
|
language: ['sprache', 'language', 'lang'],
|
|
};
|
|
|
|
const keywords = commandKeywords[command];
|
|
if (!keywords) return '';
|
|
|
|
const lowerBody = body.toLowerCase();
|
|
for (const keyword of keywords) {
|
|
const index = lowerBody.indexOf(keyword);
|
|
if (index !== -1) {
|
|
return body.substring(index + keyword.length).trim();
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
protected async handleAudioMessage(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
sender: string
|
|
): Promise<void> {
|
|
const content = event.content;
|
|
if (!content?.url) return;
|
|
|
|
try {
|
|
// Require login for audio messages
|
|
const token = await this.requireLogin(roomId, event, sender);
|
|
if (!token) return;
|
|
|
|
await this.sendReply(roomId, event, 'Verarbeite Sprachnotiz...');
|
|
|
|
// Download audio from Matrix using authenticated API
|
|
const mxcUrl = content.url;
|
|
this.logger.log(`Downloading audio from ${mxcUrl}`);
|
|
|
|
const buffer = await this.downloadMedia(mxcUrl);
|
|
|
|
// Transcribe audio
|
|
const transcription = await this.transcriptionService.transcribe(buffer);
|
|
this.logger.log(`Transcription: ${transcription.substring(0, 50)}...`);
|
|
|
|
if (!transcription.trim()) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Konnte keine Sprache erkennen. Bitte versuche es erneut.'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check credits
|
|
const validation = await this.creditService.validateCredits(token, TASK_CREATE_CREDITS);
|
|
if (!validation.hasCredits) {
|
|
const errorMsg = this.creditService.formatInsufficientCreditsError(
|
|
TASK_CREATE_CREDITS,
|
|
validation.availableCredits,
|
|
'Aufgabe erstellen'
|
|
);
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
`Transkription: "${transcription}"\n\n${errorMsg.text}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Use API service (syncs with todo-web and mobile)
|
|
const { title, priority, dueDate, project } =
|
|
this.todoApiService.parseTaskInput(transcription);
|
|
const apiTask = await this.todoApiService.createTask(token, { title, priority, dueDate });
|
|
if (!apiTask) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
`Transkription: "${transcription}"\n\nFehler beim Erstellen der Aufgabe.`
|
|
);
|
|
return;
|
|
}
|
|
const task = this.normalizeTask(apiTask);
|
|
task.project = project;
|
|
|
|
let responseText = `Aufgabe erstellt: **${task.title}**`;
|
|
|
|
const details: string[] = [];
|
|
if (task.priority < 4) details.push(`Priorität ${task.priority}`);
|
|
if (task.dueDate) details.push(`${this.formatDate(task.dueDate)}`);
|
|
if (task.project) details.push(`#${task.project}`);
|
|
|
|
if (details.length > 0) {
|
|
responseText += ` · ${details.join(' · ')}`;
|
|
}
|
|
|
|
await this.sendMessage(roomId, responseText);
|
|
} catch (error) {
|
|
this.logger.error('Audio processing failed:', error);
|
|
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
|
await this.sendReply(roomId, event, `Fehler bei der Verarbeitung: ${errorMsg}`);
|
|
}
|
|
}
|
|
|
|
private async executeCommand(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
userId: string,
|
|
command: string,
|
|
args: string
|
|
) {
|
|
// Handle credit commands first (credits, packages, buy)
|
|
if (await handleCreditCommand(this, roomId, event, userId, command, args)) {
|
|
return;
|
|
}
|
|
|
|
// Handle gift commands (geschenk, einloesen, meine-geschenke)
|
|
if (await handleGiftCommand(this, roomId, event, userId, command, args)) {
|
|
return;
|
|
}
|
|
|
|
switch (command) {
|
|
case 'help':
|
|
case 'hilfe':
|
|
await this.sendReply(roomId, event, HELP_TEXT);
|
|
break;
|
|
|
|
case 'add':
|
|
case 'neu':
|
|
case 'neue':
|
|
await this.handleAddTask(roomId, event, userId, args);
|
|
break;
|
|
|
|
case 'list':
|
|
case 'liste':
|
|
case 'alle':
|
|
await this.handleListTasks(roomId, event, userId);
|
|
break;
|
|
|
|
case 'today':
|
|
case 'heute':
|
|
await this.handleTodayTasks(roomId, event, userId);
|
|
break;
|
|
|
|
case 'inbox':
|
|
case 'eingang':
|
|
await this.handleInboxTasks(roomId, event, userId);
|
|
break;
|
|
|
|
case 'done':
|
|
case 'erledigt':
|
|
case 'fertig':
|
|
await this.handleCompleteTask(roomId, event, userId, args);
|
|
break;
|
|
|
|
case 'delete':
|
|
case 'löschen':
|
|
case 'entfernen':
|
|
await this.handleDeleteTask(roomId, event, userId, args);
|
|
break;
|
|
|
|
case 'projects':
|
|
case 'projekte':
|
|
await this.handleProjects(roomId, event, userId);
|
|
break;
|
|
|
|
case 'project':
|
|
case 'projekt':
|
|
await this.handleProjectTasks(roomId, event, userId, args);
|
|
break;
|
|
|
|
case 'status':
|
|
await this.handleStatus(roomId, event, userId);
|
|
break;
|
|
|
|
case 'pin':
|
|
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: \`sprache de\` oder / or \`sprache 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,
|
|
userId: string,
|
|
input: string
|
|
) {
|
|
if (!input.trim()) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Bitte gib eine Aufgabe an.\n\nBeispiel: `neu Einkaufen gehen`'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Require login
|
|
const token = await this.requireLogin(roomId, event, userId);
|
|
if (!token) return;
|
|
|
|
// Check credits
|
|
const validation = await this.creditService.validateCredits(token, TASK_CREATE_CREDITS);
|
|
if (!validation.hasCredits) {
|
|
const errorMsg = this.creditService.formatInsufficientCreditsError(
|
|
TASK_CREATE_CREDITS,
|
|
validation.availableCredits,
|
|
'Aufgabe erstellen'
|
|
);
|
|
await this.sendReply(roomId, event, errorMsg.text);
|
|
return;
|
|
}
|
|
|
|
// Use API service (syncs with todo-web and mobile)
|
|
const { title, priority, dueDate, project } = this.todoApiService.parseTaskInput(input);
|
|
const apiTask = await this.todoApiService.createTask(token, { title, priority, dueDate });
|
|
if (!apiTask) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Fehler beim Erstellen der Aufgabe. Bitte versuche es erneut.'
|
|
);
|
|
return;
|
|
}
|
|
const task = this.normalizeTask(apiTask);
|
|
task.project = project; // Note: project handling via API needs project ID lookup
|
|
|
|
let response = `Aufgabe erstellt: **${task.title}**`;
|
|
|
|
const details: string[] = [];
|
|
if (task.priority < 4) details.push(`Priorität ${task.priority}`);
|
|
if (task.dueDate) details.push(`${this.formatDate(task.dueDate)}`);
|
|
if (task.project) details.push(`#${task.project}`);
|
|
|
|
if (details.length > 0) {
|
|
response += ` · ${details.join(' · ')}`;
|
|
}
|
|
|
|
await this.sendMessage(roomId, response);
|
|
}
|
|
|
|
private async handleListTasks(roomId: string, event: MatrixRoomEvent, userId: string) {
|
|
// Require login
|
|
const token = await this.requireLogin(roomId, event, userId);
|
|
if (!token) return;
|
|
|
|
const apiTasks = await this.todoApiService.getTasks(token, { completed: false });
|
|
const tasks = apiTasks.map((t) => this.normalizeTask(t));
|
|
|
|
if (tasks.length === 0) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Keine offenen Aufgaben.\n\nErstelle eine mit `neu [Aufgabe]`'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const response = this.formatTaskList('**Alle offenen Aufgaben:**', tasks);
|
|
await this.sendMessage(roomId, response);
|
|
}
|
|
|
|
private async handleTodayTasks(roomId: string, event: MatrixRoomEvent, userId: string) {
|
|
// Require login
|
|
const token = await this.requireLogin(roomId, event, userId);
|
|
if (!token) return;
|
|
|
|
const apiTodayTasks = await this.todoApiService.getTodayTasks(token);
|
|
const apiInboxTasks = await this.todoApiService.getInboxTasks(token);
|
|
const todayTasks = apiTodayTasks.map((t) => this.normalizeTask(t));
|
|
const inboxTasks = apiInboxTasks.map((t) => this.normalizeTask(t));
|
|
|
|
const hasTodayTasks = todayTasks.length > 0;
|
|
const hasInboxTasks = inboxTasks.length > 0;
|
|
|
|
if (!hasTodayTasks && !hasInboxTasks) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Keine Aufgaben.\n\nErstelle eine mit `neu Aufgabe` oder `neu Aufgabe @heute`'
|
|
);
|
|
return;
|
|
}
|
|
|
|
let response = '';
|
|
|
|
if (hasTodayTasks) {
|
|
response += this.formatTaskList('**Aufgaben fuer heute:**', todayTasks);
|
|
}
|
|
|
|
if (hasInboxTasks) {
|
|
if (hasTodayTasks) {
|
|
response += '\n\n';
|
|
}
|
|
response += this.formatTaskList('**Inbox (ohne Datum):**', inboxTasks);
|
|
}
|
|
|
|
await this.sendMessage(roomId, response);
|
|
}
|
|
|
|
private async handleInboxTasks(roomId: string, event: MatrixRoomEvent, userId: string) {
|
|
// Require login
|
|
const token = await this.requireLogin(roomId, event, userId);
|
|
if (!token) return;
|
|
|
|
const apiTasks = await this.todoApiService.getInboxTasks(token);
|
|
const tasks = apiTasks.map((t) => this.normalizeTask(t));
|
|
|
|
if (tasks.length === 0) {
|
|
await this.sendReply(roomId, event, 'Inbox ist leer.\n\nAufgaben ohne Datum landen hier.');
|
|
return;
|
|
}
|
|
|
|
const response = this.formatTaskList('**Inbox (ohne Datum):**', tasks);
|
|
await this.sendMessage(roomId, response);
|
|
}
|
|
|
|
private async handleCompleteTask(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
userId: string,
|
|
args: string
|
|
) {
|
|
const taskNumber = parseInt(args.trim());
|
|
|
|
if (isNaN(taskNumber) || taskNumber < 1) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Bitte gib eine gueltige Aufgabennummer an.\n\nBeispiel: `erledigt 1`'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Require login
|
|
const token = await this.requireLogin(roomId, event, userId);
|
|
if (!token) return;
|
|
|
|
let task: Task | null = null;
|
|
|
|
// Use API service - need to get task list first to find task by index
|
|
const apiTasks = await this.todoApiService.getTasks(token, { completed: false });
|
|
if (taskNumber > 0 && taskNumber <= apiTasks.length) {
|
|
const targetTask = apiTasks[taskNumber - 1];
|
|
const completedTask = await this.todoApiService.completeTask(token, targetTask.id);
|
|
if (completedTask) {
|
|
task = this.normalizeTask(completedTask);
|
|
}
|
|
}
|
|
|
|
if (!task) {
|
|
await this.sendReply(roomId, event, `Aufgabe #${taskNumber} nicht gefunden.`);
|
|
return;
|
|
}
|
|
|
|
await this.sendMessage(roomId, `✓ ~~${task.title}~~`);
|
|
}
|
|
|
|
private async handleDeleteTask(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
userId: string,
|
|
args: string
|
|
) {
|
|
const taskNumber = parseInt(args.trim());
|
|
|
|
if (isNaN(taskNumber) || taskNumber < 1) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Bitte gib eine gueltige Aufgabennummer an.\n\nBeispiel: `löschen 1`'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Require login
|
|
const token = await this.requireLogin(roomId, event, userId);
|
|
if (!token) return;
|
|
|
|
let task: Task | null = null;
|
|
|
|
// Use API service - need to get task list first to find task by index
|
|
const apiTasks = await this.todoApiService.getTasks(token, { completed: false });
|
|
if (taskNumber > 0 && taskNumber <= apiTasks.length) {
|
|
const targetTask = apiTasks[taskNumber - 1];
|
|
const deleted = await this.todoApiService.deleteTask(token, targetTask.id);
|
|
if (deleted) {
|
|
task = this.normalizeTask(targetTask);
|
|
}
|
|
}
|
|
|
|
if (!task) {
|
|
await this.sendReply(roomId, event, `Aufgabe #${taskNumber} nicht gefunden.`);
|
|
return;
|
|
}
|
|
|
|
await this.sendMessage(roomId, `🗑️ ${task.title}`);
|
|
}
|
|
|
|
private async handleProjects(roomId: string, event: MatrixRoomEvent, userId: string) {
|
|
// Require login
|
|
const token = await this.requireLogin(roomId, event, userId);
|
|
if (!token) return;
|
|
|
|
const projects = await this.todoApiService.getProjects(token);
|
|
|
|
if (projects.length === 0) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Keine Projekte.\n\nErstelle eine Aufgabe mit Projekt: `neu Aufgabe #projektname`'
|
|
);
|
|
return;
|
|
}
|
|
|
|
let response = '**Deine Projekte:**\n\n';
|
|
for (const project of projects) {
|
|
response += `- ${project.name}\n`;
|
|
}
|
|
response += '\nZeige Projektaufgaben mit `projekt [Name]`';
|
|
|
|
await this.sendMessage(roomId, response);
|
|
}
|
|
|
|
private async handleProjectTasks(
|
|
roomId: string,
|
|
event: MatrixRoomEvent,
|
|
userId: string,
|
|
args: string
|
|
) {
|
|
const projectName = args.trim();
|
|
|
|
if (!projectName) {
|
|
await this.sendReply(
|
|
roomId,
|
|
event,
|
|
'Bitte gib einen Projektnamen an.\n\nBeispiel: `projekt Arbeit`'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Require login
|
|
const token = await this.requireLogin(roomId, event, userId);
|
|
if (!token) return;
|
|
|
|
let tasks: Task[] = [];
|
|
|
|
// Use API service - need to find project ID first
|
|
const projects = await this.todoApiService.getProjects(token);
|
|
const project = projects.find((p) => p.name.toLowerCase() === projectName.toLowerCase());
|
|
if (project) {
|
|
const apiTasks = await this.todoApiService.getProjectTasks(token, project.id);
|
|
tasks = apiTasks.map((t) => this.normalizeTask(t));
|
|
}
|
|
|
|
if (tasks.length === 0) {
|
|
await this.sendReply(roomId, event, `Keine Aufgaben im Projekt "${projectName}".`);
|
|
return;
|
|
}
|
|
|
|
const response = this.formatTaskList(`**Projekt: ${projectName}**`, tasks);
|
|
await this.sendMessage(roomId, response);
|
|
}
|
|
|
|
private async handleStatus(roomId: string, event: MatrixRoomEvent, userId: string) {
|
|
const token = await this.getToken(userId);
|
|
const isLoggedIn = await this.sessionService.isLoggedIn(userId);
|
|
const email = this.sessionService.getEmail(userId);
|
|
|
|
if (token) {
|
|
const stats = await this.todoApiService.getStats(token);
|
|
const response = `**Status**
|
|
|
|
👤 ${email}
|
|
|
|
- Offen: ${stats.pending}
|
|
- Heute: ${stats.today}
|
|
- Erledigt: ${stats.completed}`;
|
|
|
|
await this.sendMessage(roomId, response);
|
|
} else {
|
|
const response = `**Status**
|
|
|
|
🔐 Nicht angemeldet
|
|
|
|
\`login email passwort\``;
|
|
|
|
await this.sendMessage(roomId, response);
|
|
}
|
|
}
|
|
|
|
private async handlePinHelp(roomId: string, event: MatrixRoomEvent) {
|
|
try {
|
|
// Send help message
|
|
const helpEventId = await this.sendMessage(roomId, HELP_TEXT);
|
|
|
|
// Pin it
|
|
await this.client.sendStateEvent(roomId, 'm.room.pinned_events', '', {
|
|
pinned: [helpEventId],
|
|
});
|
|
|
|
await this.sendReply(roomId, event, 'Hilfe wurde angepinnt!');
|
|
} catch (error) {
|
|
this.logger.error('Failed to pin help:', error);
|
|
await this.sendReply(roomId, event, 'Konnte Hilfe nicht anpinnen (fehlende Berechtigung?)');
|
|
}
|
|
}
|
|
|
|
private formatTaskList(header: string, tasks: Task[]): string {
|
|
let response = `${header}\n\n`;
|
|
|
|
tasks.forEach((task, index) => {
|
|
const num = index + 1;
|
|
const priority = task.priority < 4 ? `!`.repeat(4 - task.priority) : '';
|
|
const date = task.dueDate ? ` ${this.formatDate(task.dueDate)}` : '';
|
|
const project = task.project ? ` ${task.project}` : '';
|
|
|
|
response += `**${num}.** ${task.title}${priority}${date}${project}\n`;
|
|
});
|
|
|
|
response += `\nErledigen: \`erledigt [Nr]\` | Loeschen: \`löschen [Nr]\``;
|
|
return response;
|
|
}
|
|
|
|
private formatDate(dateStr: string): string {
|
|
const date = new Date(dateStr);
|
|
const today = new Date();
|
|
const tomorrow = new Date(today);
|
|
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
|
|
if (dateStr === today.toISOString().split('T')[0]) {
|
|
return 'Heute';
|
|
} else if (dateStr === tomorrow.toISOString().split('T')[0]) {
|
|
return 'Morgen';
|
|
}
|
|
|
|
return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
|
}
|
|
}
|