diff --git a/services/matrix-chat-bot/.env.example b/services/matrix-chat-bot/.env.example new file mode 100644 index 000000000..501ebc0f8 --- /dev/null +++ b/services/matrix-chat-bot/.env.example @@ -0,0 +1,15 @@ +# Server +PORT=3327 + +# Matrix +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN=syt_xxx +MATRIX_ALLOWED_ROOMS=#chat:matrix.mana.how +MATRIX_STORAGE_PATH=./data/bot-storage.json + +# Chat Backend +CHAT_BACKEND_URL=http://localhost:3002 +CHAT_API_PREFIX= + +# Mana Core Auth +MANA_CORE_AUTH_URL=http://localhost:3001 diff --git a/services/matrix-chat-bot/.gitignore b/services/matrix-chat-bot/.gitignore new file mode 100644 index 000000000..1d986408b --- /dev/null +++ b/services/matrix-chat-bot/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +data/ +*.log diff --git a/services/matrix-chat-bot/CLAUDE.md b/services/matrix-chat-bot/CLAUDE.md new file mode 100644 index 000000000..ea531ffb4 --- /dev/null +++ b/services/matrix-chat-bot/CLAUDE.md @@ -0,0 +1,226 @@ +# Matrix Chat Bot - Claude Code Guidelines + +## Overview + +Matrix Chat Bot provides AI chat capabilities via Matrix chat. It integrates with the Chat backend for conversations, AI completions, and message history. + +## Tech Stack + +- **Framework**: NestJS 10 +- **Matrix**: matrix-bot-sdk +- **Backend**: Chat API (port 3002) +- **Auth**: Mana Core Auth (JWT) + +## Commands + +```bash +# Development +pnpm install +pnpm start:dev # Start with hot reload + +# Build +pnpm build # Production build + +# Type check +pnpm type-check # Check TypeScript types +``` + +## Project Structure + +``` +services/matrix-chat-bot/ +├── src/ +│ ├── main.ts # Application entry point (port 3327) +│ ├── app.module.ts # Root module +│ ├── health.controller.ts # Health check endpoint +│ ├── config/ +│ │ └── configuration.ts # Configuration & help messages +│ ├── bot/ +│ │ ├── bot.module.ts +│ │ └── matrix.service.ts # Matrix client & command handlers +│ ├── chat/ +│ │ ├── chat.module.ts +│ │ └── chat.service.ts # Chat Backend API client +│ └── session/ +│ ├── session.module.ts +│ └── session.service.ts # User session & auth management +├── Dockerfile +└── package.json +``` + +## Bot Commands + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!help` | hilfe | Show help message | +| `!login email pass` | - | Login | +| `!logout` | - | Logout | +| `!status` | - | Bot status | + +### Quick Chat (Stateless) + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!chat [message]` | fragen, ask | Quick AI response (no history) | + +### Conversation Management + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!neu [titel]` | new | Create new conversation | +| `!gespraeche` | conversations, liste | List all conversations | +| `!gespraech [nr]` | conversation, select | Select/view conversation | +| `!senden [message]` | send, s | Send message in current conversation | +| `!verlauf` | history, nachrichten | Show message history | + +### Conversation Actions + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!titel [nr] [title]` | title | Change conversation title | +| `!archiv [nr]` | archive | Archive conversation | +| `!archiviert` | archived | List archived conversations | +| `!wiederherstellen [nr]` | restore, unarchive | Restore from archive | +| `!pin [nr]` | - | Pin conversation | +| `!unpin [nr]` | - | Unpin conversation | +| `!loeschen [nr]` | delete | Delete conversation | + +### Model Selection + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!modelle` | models | List available AI models | +| `!modell [nr]` | model | Select model for new conversations | + +## Model Providers + +| Provider | Icon | Description | +|----------|------|-------------| +| `ollama` | 🏠 | Local models (self-hosted) | +| `openrouter` | ☁️ | Cloud models via OpenRouter | +| `openai` | 🤖 | OpenAI models | +| `anthropic` | 🧠 | Anthropic Claude models | + +## Example Usage + +``` +# Login +!login max@example.com mypassword + +# Quick chat (no conversation needed) +!chat Was ist die Hauptstadt von Frankreich? + +# Create a conversation +!neu Programmierung Hilfe + +# Send message in conversation +!senden Erklaere mir Python Listen + +# View message history +!verlauf + +# List conversations +!gespraeche + +# Select conversation +!gespraech 1 + +# Change model +!modelle +!modell 2 + +# Archive and restore +!archiv 1 +!archiviert +!wiederherstellen 1 + +# Pin conversation +!pin 1 +!unpin 1 + +# Delete conversation +!loeschen 1 +``` + +## Environment Variables + +```env +# Server +PORT=3327 + +# Matrix +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN=syt_xxx +MATRIX_ALLOWED_ROOMS=#chat:matrix.mana.how +MATRIX_STORAGE_PATH=./data/bot-storage.json + +# Chat Backend +CHAT_BACKEND_URL=http://localhost:3002 +CHAT_API_PREFIX= + +# Mana Core Auth +MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +## Docker + +```bash +# Build locally +docker build -f services/matrix-chat-bot/Dockerfile -t matrix-chat-bot services/matrix-chat-bot + +# Run +docker run -p 3327:3327 \ + -e MATRIX_HOMESERVER_URL=http://synapse:8008 \ + -e MATRIX_ACCESS_TOKEN=syt_xxx \ + -e CHAT_BACKEND_URL=http://chat-backend:3002 \ + -e MANA_CORE_AUTH_URL=http://mana-core-auth:3001 \ + -v matrix-chat-bot-data:/app/data \ + matrix-chat-bot +``` + +## Health Check + +```bash +curl http://localhost:3327/health +``` + +## Chat Backend API Endpoints Used + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check | +| `/models` | GET | List AI models (public) | +| `/models/:id` | GET | Get model details (public) | +| `/chat/completions` | POST | Create AI completion | +| `/conversations` | GET | List conversations | +| `/conversations` | POST | Create conversation | +| `/conversations/archived` | GET | List archived | +| `/conversations/:id` | GET | Get conversation | +| `/conversations/:id` | DELETE | Delete conversation | +| `/conversations/:id/messages` | GET | Get messages | +| `/conversations/:id/messages` | POST | Add message | +| `/conversations/:id/title` | PATCH | Update title | +| `/conversations/:id/archive` | PATCH | Archive | +| `/conversations/:id/unarchive` | PATCH | Unarchive | +| `/conversations/:id/pin` | PATCH | Pin | +| `/conversations/:id/unpin` | PATCH | Unpin | + +## Chat Modes + +The bot supports different ways to chat: + +1. **Quick Chat** (`!chat`): Stateless, single message/response, no history +2. **Conversation Chat** (`!senden`): Stateful, maintains message history, context-aware + +## Number-Based Reference System + +The bot uses a number-based reference system for ease of use: +1. User runs `!gespraeche` or `!modelle` to get a list +2. Bot stores the list internally for the user +3. User can reference items by their list number +4. Numbers are valid until the user runs a new list command + +This allows simple commands like: +- `!gespraech 3` - Select conversation #3 +- `!archiv 1` - Archive conversation #1 +- `!modell 2` - Select model #2 diff --git a/services/matrix-chat-bot/Dockerfile b/services/matrix-chat-bot/Dockerfile new file mode 100644 index 000000000..eefc03383 --- /dev/null +++ b/services/matrix-chat-bot/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package files and install production dependencies only +COPY package.json ./ +RUN npm install --omit=dev + +# Copy built application from builder +COPY --from=builder /app/dist ./dist + +# Create data directory +RUN mkdir -p /app/data + +# Expose port +EXPOSE 3327 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3327/health || exit 1 + +# Start the application +CMD ["node", "dist/main.js"] diff --git a/services/matrix-chat-bot/nest-cli.json b/services/matrix-chat-bot/nest-cli.json new file mode 100644 index 000000000..95538fb90 --- /dev/null +++ b/services/matrix-chat-bot/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/services/matrix-chat-bot/package.json b/services/matrix-chat-bot/package.json new file mode 100644 index 000000000..057828aeb --- /dev/null +++ b/services/matrix-chat-bot/package.json @@ -0,0 +1,27 @@ +{ + "name": "@mana-bot/matrix-chat", + "version": "1.0.0", + "description": "Matrix bot for AI chat conversations", + "main": "dist/main.js", + "scripts": { + "build": "nest build", + "start": "node dist/main.js", + "start:dev": "nest start --watch", + "start:prod": "node dist/main.js", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@nestjs/common": "^10.3.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.3.0", + "@nestjs/platform-express": "^10.3.0", + "matrix-bot-sdk": "^0.7.1", + "reflect-metadata": "^0.2.1", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.3.0", + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + } +} diff --git a/services/matrix-chat-bot/src/app.module.ts b/services/matrix-chat-bot/src/app.module.ts new file mode 100644 index 000000000..52d5de4a4 --- /dev/null +++ b/services/matrix-chat-bot/src/app.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { BotModule } from './bot/bot.module'; +import { HealthController } from './health.controller'; +import configuration from './config/configuration'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [configuration], + }), + BotModule, + ], + controllers: [HealthController], +}) +export class AppModule {} diff --git a/services/matrix-chat-bot/src/bot/bot.module.ts b/services/matrix-chat-bot/src/bot/bot.module.ts new file mode 100644 index 000000000..3aef8b8d3 --- /dev/null +++ b/services/matrix-chat-bot/src/bot/bot.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { MatrixService } from './matrix.service'; +import { ChatModule } from '../chat/chat.module'; +import { SessionModule } from '../session/session.module'; + +@Module({ + imports: [ChatModule, SessionModule], + providers: [MatrixService], + exports: [MatrixService], +}) +export class BotModule {} diff --git a/services/matrix-chat-bot/src/bot/matrix.service.ts b/services/matrix-chat-bot/src/bot/matrix.service.ts new file mode 100644 index 000000000..5a1a7f90a --- /dev/null +++ b/services/matrix-chat-bot/src/bot/matrix.service.ts @@ -0,0 +1,754 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + MatrixClient, + SimpleFsStorageProvider, + AutojoinRoomsMixin, + RichReply, +} from 'matrix-bot-sdk'; +import { ChatService, Model, Conversation, Message } from '../chat/chat.service'; +import { SessionService } from '../session/session.service'; +import { HELP_MESSAGE, BRANCH_ICONS } from '../config/configuration'; + +@Injectable() +export class MatrixService implements OnModuleInit { + private readonly logger = new Logger(MatrixService.name); + private client: MatrixClient; + private allowedRooms: string[]; + + constructor( + private configService: ConfigService, + private chatService: ChatService, + private sessionService: SessionService + ) {} + + async onModuleInit() { + const homeserverUrl = this.configService.get('matrix.homeserverUrl'); + const accessToken = this.configService.get('matrix.accessToken'); + const storagePath = this.configService.get('matrix.storagePath'); + this.allowedRooms = this.configService.get('matrix.allowedRooms') || []; + + if (!accessToken) { + this.logger.warn('No Matrix access token configured, bot disabled'); + return; + } + + const storage = new SimpleFsStorageProvider(storagePath); + this.client = new MatrixClient(homeserverUrl, accessToken, storage); + AutojoinRoomsMixin.setupOnClient(this.client); + + this.client.on('room.message', this.handleMessage.bind(this)); + + await this.client.start(); + this.logger.log('Matrix Chat Bot started'); + } + + private async handleMessage(roomId: string, event: any) { + if (event.sender === (await this.client.getUserId())) return; + if (event.content?.msgtype !== 'm.text') return; + + const body = event.content.body?.trim(); + if (!body?.startsWith('!')) return; + + if (this.allowedRooms.length > 0 && !this.allowedRooms.includes(roomId)) { + return; + } + + const sender = event.sender; + const [command, ...args] = body.slice(1).split(/\s+/); + const argString = args.join(' '); + + try { + let response: string; + + switch (command.toLowerCase()) { + case 'help': + case 'hilfe': + response = HELP_MESSAGE; + break; + + case 'login': + response = await this.handleLogin(sender, args); + break; + + case 'logout': + response = this.handleLogout(sender); + break; + + case 'status': + response = this.handleStatus(sender); + break; + + case 'chat': + case 'fragen': + case 'ask': + response = await this.handleQuickChat(sender, argString); + break; + + case 'neu': + case 'new': + response = await this.handleNewConversation(sender, argString); + break; + + case 'gespraeche': + case 'gespräche': + case 'conversations': + case 'liste': + response = await this.handleListConversations(sender); + break; + + case 'gespraech': + case 'gespräch': + case 'conversation': + case 'select': + response = await this.handleSelectConversation(sender, args[0]); + break; + + case 'senden': + case 'send': + case 's': + response = await this.handleSendMessage(sender, argString); + break; + + case 'verlauf': + case 'history': + case 'nachrichten': + response = await this.handleShowHistory(sender, args[0]); + break; + + case 'titel': + case 'title': + response = await this.handleUpdateTitle(sender, args[0], args.slice(1).join(' ')); + break; + + case 'archiv': + case 'archive': + response = await this.handleArchive(sender, args[0]); + break; + + case 'archiviert': + case 'archived': + response = await this.handleListArchived(sender); + break; + + case 'wiederherstellen': + case 'restore': + case 'unarchive': + response = await this.handleUnarchive(sender, args[0]); + break; + + case 'pin': + response = await this.handlePin(sender, args[0]); + break; + + case 'unpin': + response = await this.handleUnpin(sender, args[0]); + break; + + case 'loeschen': + case 'löschen': + case 'delete': + response = await this.handleDelete(sender, args[0]); + break; + + case 'modelle': + case 'models': + response = await this.handleListModels(sender); + break; + + case 'modell': + case 'model': + response = await this.handleSelectModel(sender, args[0]); + break; + + default: + response = `Unbekannter Befehl: ${command}\nNutze \`!help\` fuer eine Uebersicht.`; + } + + await this.sendReply(roomId, event, response); + } catch (error) { + this.logger.error(`Error handling command ${command}:`, error); + await this.sendReply(roomId, event, 'Ein Fehler ist aufgetreten. Bitte versuche es erneut.'); + } + } + + private async sendReply(roomId: string, event: any, message: string) { + const reply = RichReply.createFor(roomId, event, message, message); + reply.msgtype = 'm.text'; + await this.client.sendMessage(roomId, reply); + } + + // Auth handlers + private async handleLogin(sender: string, args: string[]): Promise { + if (args.length < 2) { + return 'Verwendung: `!login email passwort`'; + } + + const [email, password] = args; + const result = await this.sessionService.login(sender, email, password); + + if (result.success) { + return `Erfolgreich angemeldet als **${email}**`; + } + return `Anmeldung fehlgeschlagen: ${result.error}`; + } + + private handleLogout(sender: string): string { + this.sessionService.logout(sender); + return 'Erfolgreich abgemeldet.'; + } + + private handleStatus(sender: string): string { + const isLoggedIn = this.sessionService.isLoggedIn(sender); + const currentConv = this.sessionService.getCurrentConversation(sender); + const selectedModel = this.sessionService.getSelectedModel(sender); + + let status = `**Bot Status**\n`; + status += `- Angemeldet: ${isLoggedIn ? 'Ja' : 'Nein'}\n`; + if (currentConv) { + status += `- Aktuelles Gespraech: ${currentConv.substring(0, 8)}...\n`; + } + if (selectedModel) { + status += `- Gewaehltes Modell: ${selectedModel.substring(0, 8)}...\n`; + } + status += `- Aktive Sessions: ${this.sessionService.getSessionCount()}`; + return status; + } + + // Quick chat (stateless) + private async handleQuickChat(sender: string, message: string): Promise { + if (!message) { + return 'Verwendung: `!chat [deine nachricht]`'; + } + + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + // Get models to find default + const modelsResult = await this.chatService.getModels(); + if (modelsResult.error || !modelsResult.data?.length) { + return 'Keine AI-Modelle verfuegbar.'; + } + + const selectedModelId = this.sessionService.getSelectedModel(sender); + const modelId = selectedModelId || modelsResult.data.find((m) => m.isDefault)?.id || modelsResult.data[0].id; + + const result = await this.chatService.createCompletion( + token, + [{ role: 'user', content: message }], + modelId + ); + + if (result.error) { + return `Fehler: ${result.error}`; + } + + let response = result.data.content; + if (result.data.usage) { + response += `\n\n_Tokens: ${result.data.usage.total_tokens}_`; + } + return response; + } + + // Conversation management + private async handleNewConversation(sender: string, title: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + // Get models to find default + const modelsResult = await this.chatService.getModels(); + if (modelsResult.error || !modelsResult.data?.length) { + return 'Keine AI-Modelle verfuegbar.'; + } + + const selectedModelId = this.sessionService.getSelectedModel(sender); + const modelId = selectedModelId || modelsResult.data.find((m) => m.isDefault)?.id || modelsResult.data[0].id; + + const convTitle = title || `Matrix Chat ${new Date().toLocaleDateString('de-DE')}`; + const result = await this.chatService.createConversation(token, { + title: convTitle, + modelId, + conversationMode: 'free', + }); + + if (result.error) { + return `Fehler: ${result.error}`; + } + + this.sessionService.setCurrentConversation(sender, result.data.id); + return `Neues Gespraech erstellt: **${result.data.title}**\nNutze \`!senden [nachricht]\` um zu chatten.`; + } + + private async handleListConversations(sender: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + const result = await this.chatService.getConversations(token); + if (result.error) { + return `Fehler: ${result.error}`; + } + + if (!result.data?.length) { + return 'Keine Gespraeche vorhanden. Erstelle eines mit `!neu [titel]`'; + } + + // Sort: pinned first, then by date + const sorted = result.data.sort((a, b) => { + if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1; + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + }); + + // Store mapping + this.sessionService.setConversationMapping( + sender, + sorted.map((c) => c.id) + ); + + const currentId = this.sessionService.getCurrentConversation(sender); + + let response = '**Deine Gespraeche:**\n\n'; + sorted.forEach((conv, index) => { + const pin = conv.isPinned ? '📌 ' : ''; + const current = conv.id === currentId ? ' ◀️' : ''; + const date = new Date(conv.updatedAt).toLocaleDateString('de-DE'); + response += `${index + 1}. ${pin}**${conv.title}**${current}\n _${date}_\n`; + }); + + response += '\nNutze `!gespraech [nr]` zum Auswaehlen.'; + return response; + } + + private async handleSelectConversation(sender: string, numberStr: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + if (!numberStr) { + // Show current conversation + const currentId = this.sessionService.getCurrentConversation(sender); + if (!currentId) { + return 'Kein Gespraech ausgewaehlt. Nutze `!gespraeche` und dann `!gespraech [nr]`'; + } + + const result = await this.chatService.getConversation(token, currentId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + return this.formatConversationDetails(result.data); + } + + const number = parseInt(numberStr, 10); + if (isNaN(number)) { + return 'Bitte eine gueltige Nummer angeben.'; + } + + const conversationId = this.sessionService.getConversationId(sender, number); + if (!conversationId) { + return 'Ungueltige Nummer. Nutze `!gespraeche` fuer eine aktuelle Liste.'; + } + + const result = await this.chatService.getConversation(token, conversationId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + this.sessionService.setCurrentConversation(sender, conversationId); + return `Gespraech ausgewaehlt: **${result.data.title}**\n\n${this.formatConversationDetails(result.data)}`; + } + + private formatConversationDetails(conv: Conversation): string { + const pin = conv.isPinned ? '📌 Angepinnt' : ''; + const created = new Date(conv.createdAt).toLocaleDateString('de-DE'); + const updated = new Date(conv.updatedAt).toLocaleDateString('de-DE'); + + return `**${conv.title}** ${pin} +- Modus: ${conv.conversationMode} +- Dokument-Modus: ${conv.documentMode ? 'Ja' : 'Nein'} +- Erstellt: ${created} +- Aktualisiert: ${updated} + +Nutze \`!senden [nachricht]\` um zu chatten oder \`!verlauf\` fuer den Nachrichtenverlauf.`; + } + + private async handleSendMessage(sender: string, message: string): Promise { + if (!message) { + return 'Verwendung: `!senden [deine nachricht]`'; + } + + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + const conversationId = this.sessionService.getCurrentConversation(sender); + if (!conversationId) { + return 'Kein Gespraech ausgewaehlt. Nutze `!gespraeche` und `!gespraech [nr]` oder `!neu [titel]`'; + } + + // Add user message + const userMsgResult = await this.chatService.addMessage(token, conversationId, message, 'user'); + if (userMsgResult.error) { + return `Fehler: ${userMsgResult.error}`; + } + + // Get conversation for model ID + const convResult = await this.chatService.getConversation(token, conversationId); + if (convResult.error) { + return `Fehler: ${convResult.error}`; + } + + // Get message history for context + const historyResult = await this.chatService.getMessages(token, conversationId); + const messages = (historyResult.data || []).map((m) => ({ + role: m.sender as 'user' | 'assistant' | 'system', + content: m.messageText, + })); + + // Get AI response + const completionResult = await this.chatService.createCompletion(token, messages, convResult.data.modelId); + if (completionResult.error) { + return `Fehler bei AI-Antwort: ${completionResult.error}`; + } + + // Save assistant response + await this.chatService.addMessage(token, conversationId, completionResult.data.content, 'assistant'); + + let response = completionResult.data.content; + if (completionResult.data.usage) { + response += `\n\n_Tokens: ${completionResult.data.usage.total_tokens}_`; + } + return response; + } + + private async handleShowHistory(sender: string, numberStr?: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + let conversationId = this.sessionService.getCurrentConversation(sender); + + if (numberStr) { + const number = parseInt(numberStr, 10); + if (!isNaN(number)) { + const id = this.sessionService.getConversationId(sender, number); + if (id) conversationId = id; + } + } + + if (!conversationId) { + return 'Kein Gespraech ausgewaehlt. Nutze `!gespraeche` und `!gespraech [nr]`'; + } + + const result = await this.chatService.getMessages(token, conversationId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + if (!result.data?.length) { + return 'Noch keine Nachrichten in diesem Gespraech.'; + } + + let response = '**Nachrichtenverlauf:**\n\n'; + const recentMessages = result.data.slice(-10); // Last 10 messages + + recentMessages.forEach((msg) => { + const icon = msg.sender === 'user' ? '👤' : msg.sender === 'assistant' ? '🤖' : '⚙️'; + const text = msg.messageText.length > 200 ? msg.messageText.substring(0, 200) + '...' : msg.messageText; + response += `${icon} **${msg.sender}:**\n${text}\n\n`; + }); + + if (result.data.length > 10) { + response += `_...und ${result.data.length - 10} weitere Nachrichten_`; + } + + return response; + } + + // Conversation management actions + private async handleUpdateTitle(sender: string, numberStr: string, title: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + if (!numberStr || !title) { + return 'Verwendung: `!titel [nr] [neuer titel]`'; + } + + const number = parseInt(numberStr, 10); + if (isNaN(number)) { + return 'Bitte eine gueltige Nummer angeben.'; + } + + const conversationId = this.sessionService.getConversationId(sender, number); + if (!conversationId) { + return 'Ungueltige Nummer. Nutze `!gespraeche` fuer eine aktuelle Liste.'; + } + + const result = await this.chatService.updateTitle(token, conversationId, title); + if (result.error) { + return `Fehler: ${result.error}`; + } + + return `Titel geaendert zu: **${result.data.title}**`; + } + + private async handleArchive(sender: string, numberStr: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + if (!numberStr) { + return 'Verwendung: `!archiv [nr]`'; + } + + const number = parseInt(numberStr, 10); + if (isNaN(number)) { + return 'Bitte eine gueltige Nummer angeben.'; + } + + const conversationId = this.sessionService.getConversationId(sender, number); + if (!conversationId) { + return 'Ungueltige Nummer. Nutze `!gespraeche` fuer eine aktuelle Liste.'; + } + + const result = await this.chatService.archiveConversation(token, conversationId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + return `Gespraech **${result.data.title}** archiviert.`; + } + + private async handleListArchived(sender: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + const result = await this.chatService.getArchivedConversations(token); + if (result.error) { + return `Fehler: ${result.error}`; + } + + if (!result.data?.length) { + return 'Keine archivierten Gespraeche.'; + } + + // Store mapping for restore + this.sessionService.setConversationMapping( + sender, + result.data.map((c) => c.id) + ); + + let response = '**Archivierte Gespraeche:**\n\n'; + result.data.forEach((conv, index) => { + const date = new Date(conv.updatedAt).toLocaleDateString('de-DE'); + response += `${index + 1}. **${conv.title}**\n _${date}_\n`; + }); + + response += '\nNutze `!wiederherstellen [nr]` zum Wiederherstellen.'; + return response; + } + + private async handleUnarchive(sender: string, numberStr: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + if (!numberStr) { + return 'Verwendung: `!wiederherstellen [nr]`'; + } + + const number = parseInt(numberStr, 10); + if (isNaN(number)) { + return 'Bitte eine gueltige Nummer angeben.'; + } + + const conversationId = this.sessionService.getConversationId(sender, number); + if (!conversationId) { + return 'Ungueltige Nummer. Nutze `!archiviert` fuer eine aktuelle Liste.'; + } + + const result = await this.chatService.unarchiveConversation(token, conversationId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + return `Gespraech **${result.data.title}** wiederhergestellt.`; + } + + private async handlePin(sender: string, numberStr: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + if (!numberStr) { + return 'Verwendung: `!pin [nr]`'; + } + + const number = parseInt(numberStr, 10); + if (isNaN(number)) { + return 'Bitte eine gueltige Nummer angeben.'; + } + + const conversationId = this.sessionService.getConversationId(sender, number); + if (!conversationId) { + return 'Ungueltige Nummer. Nutze `!gespraeche` fuer eine aktuelle Liste.'; + } + + const result = await this.chatService.pinConversation(token, conversationId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + return `Gespraech **${result.data.title}** angepinnt. 📌`; + } + + private async handleUnpin(sender: string, numberStr: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + if (!numberStr) { + return 'Verwendung: `!unpin [nr]`'; + } + + const number = parseInt(numberStr, 10); + if (isNaN(number)) { + return 'Bitte eine gueltige Nummer angeben.'; + } + + const conversationId = this.sessionService.getConversationId(sender, number); + if (!conversationId) { + return 'Ungueltige Nummer. Nutze `!gespraeche` fuer eine aktuelle Liste.'; + } + + const result = await this.chatService.unpinConversation(token, conversationId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + return `Pin fuer **${result.data.title}** entfernt.`; + } + + private async handleDelete(sender: string, numberStr: string): Promise { + const token = this.sessionService.getToken(sender); + if (!token) { + return 'Bitte zuerst anmelden mit `!login email passwort`'; + } + + if (!numberStr) { + return 'Verwendung: `!loeschen [nr]`'; + } + + const number = parseInt(numberStr, 10); + if (isNaN(number)) { + return 'Bitte eine gueltige Nummer angeben.'; + } + + const conversationId = this.sessionService.getConversationId(sender, number); + if (!conversationId) { + return 'Ungueltige Nummer. Nutze `!gespraeche` fuer eine aktuelle Liste.'; + } + + // Get title before deletion + const convResult = await this.chatService.getConversation(token, conversationId); + const title = convResult.data?.title || 'Gespraech'; + + const result = await this.chatService.deleteConversation(token, conversationId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + // Clear current conversation if it was the deleted one + if (this.sessionService.getCurrentConversation(sender) === conversationId) { + this.sessionService.setCurrentConversation(sender, null); + } + + return `Gespraech **${title}** geloescht.`; + } + + // Model management + private async handleListModels(sender: string): Promise { + const result = await this.chatService.getModels(); + if (result.error) { + return `Fehler: ${result.error}`; + } + + if (!result.data?.length) { + return 'Keine AI-Modelle verfuegbar.'; + } + + const activeModels = result.data.filter((m) => m.isActive); + + // Store mapping + this.sessionService.setModelMapping( + sender, + activeModels.map((m) => m.id) + ); + + const selectedModelId = this.sessionService.getSelectedModel(sender); + + let response = '**Verfuegbare AI-Modelle:**\n\n'; + activeModels.forEach((model, index) => { + const icon = BRANCH_ICONS[model.provider] || BRANCH_ICONS.default; + const isDefault = model.isDefault ? ' (Standard)' : ''; + const selected = model.id === selectedModelId ? ' ◀️' : ''; + const desc = model.description ? `\n _${model.description}_` : ''; + response += `${index + 1}. ${icon} **${model.name}**${isDefault}${selected}${desc}\n`; + }); + + response += '\nNutze `!modell [nr]` zum Auswaehlen.'; + return response; + } + + private async handleSelectModel(sender: string, numberStr: string): Promise { + if (!numberStr) { + const selectedModelId = this.sessionService.getSelectedModel(sender); + if (!selectedModelId) { + return 'Kein Modell ausgewaehlt (Standard wird verwendet). Nutze `!modelle` und `!modell [nr]`'; + } + + const result = await this.chatService.getModel(selectedModelId); + if (result.error) { + return 'Ausgewaehltes Modell nicht gefunden.'; + } + + const icon = BRANCH_ICONS[result.data.provider] || BRANCH_ICONS.default; + return `Aktuelles Modell: ${icon} **${result.data.name}**`; + } + + const number = parseInt(numberStr, 10); + if (isNaN(number)) { + return 'Bitte eine gueltige Nummer angeben.'; + } + + const modelId = this.sessionService.getModelId(sender, number); + if (!modelId) { + return 'Ungueltige Nummer. Nutze `!modelle` fuer eine aktuelle Liste.'; + } + + const result = await this.chatService.getModel(modelId); + if (result.error) { + return `Fehler: ${result.error}`; + } + + this.sessionService.setSelectedModel(sender, modelId); + const icon = BRANCH_ICONS[result.data.provider] || BRANCH_ICONS.default; + return `Modell gewaehlt: ${icon} **${result.data.name}**\nWird fuer neue Gespraeche und Quick-Chat verwendet.`; + } +} diff --git a/services/matrix-chat-bot/src/chat/chat.module.ts b/services/matrix-chat-bot/src/chat/chat.module.ts new file mode 100644 index 000000000..4e19ed38c --- /dev/null +++ b/services/matrix-chat-bot/src/chat/chat.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ChatService } from './chat.service'; + +@Module({ + providers: [ChatService], + exports: [ChatService], +}) +export class ChatModule {} diff --git a/services/matrix-chat-bot/src/chat/chat.service.ts b/services/matrix-chat-bot/src/chat/chat.service.ts new file mode 100644 index 000000000..320eccba3 --- /dev/null +++ b/services/matrix-chat-bot/src/chat/chat.service.ts @@ -0,0 +1,220 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +export interface Model { + id: string; + name: string; + description?: string; + provider: string; + isActive: boolean; + isDefault: boolean; +} + +export interface Conversation { + id: string; + userId: string; + modelId: string; + title: string; + conversationMode: 'free' | 'guided' | 'template'; + documentMode: boolean; + isArchived: boolean; + isPinned: boolean; + createdAt: string; + updatedAt: string; +} + +export interface Message { + id: string; + conversationId: string; + sender: 'user' | 'assistant' | 'system'; + messageText: string; + createdAt: string; +} + +export interface ChatCompletionResponse { + content: string; + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} + +@Injectable() +export class ChatService { + private readonly logger = new Logger(ChatService.name); + private baseUrl: string; + private apiPrefix: string; + + constructor(private configService: ConfigService) { + this.baseUrl = this.configService.get('chat.url') || 'http://localhost:3002'; + this.apiPrefix = this.configService.get('chat.apiPrefix') || ''; + } + + private getUrl(path: string): string { + return `${this.baseUrl}${this.apiPrefix}${path}`; + } + + private async request( + path: string, + token: string, + options: RequestInit = {} + ): Promise<{ data?: T; error?: string }> { + try { + const response = await fetch(this.getUrl(path), { + ...options, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + return { error: errorData.message || `HTTP ${response.status}` }; + } + + const data = await response.json(); + return { data }; + } catch (error) { + this.logger.error(`Request failed: ${path}`, error); + return { error: 'Verbindung zum Chat-Server fehlgeschlagen' }; + } + } + + // Models (public endpoints) + async getModels(): Promise<{ data?: Model[]; error?: string }> { + try { + const response = await fetch(this.getUrl('/models')); + if (!response.ok) { + return { error: `HTTP ${response.status}` }; + } + const data = await response.json(); + return { data }; + } catch (error) { + this.logger.error('Failed to fetch models', error); + return { error: 'Verbindung zum Chat-Server fehlgeschlagen' }; + } + } + + async getModel(id: string): Promise<{ data?: Model; error?: string }> { + try { + const response = await fetch(this.getUrl(`/models/${id}`)); + if (!response.ok) { + return { error: `HTTP ${response.status}` }; + } + const data = await response.json(); + return { data }; + } catch (error) { + this.logger.error(`Failed to fetch model ${id}`, error); + return { error: 'Modell nicht gefunden' }; + } + } + + // Chat Completions + async createCompletion( + token: string, + messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>, + modelId: string, + options?: { temperature?: number; maxTokens?: number } + ): Promise<{ data?: ChatCompletionResponse; error?: string }> { + return this.request('/chat/completions', token, { + method: 'POST', + body: JSON.stringify({ + messages, + modelId, + temperature: options?.temperature, + maxTokens: options?.maxTokens, + }), + }); + } + + // Conversations + async getConversations( + token: string, + spaceId?: string + ): Promise<{ data?: Conversation[]; error?: string }> { + const query = spaceId ? `?spaceId=${spaceId}` : ''; + return this.request(`/conversations${query}`, token); + } + + async getArchivedConversations(token: string): Promise<{ data?: Conversation[]; error?: string }> { + return this.request('/conversations/archived', token); + } + + async getConversation(token: string, id: string): Promise<{ data?: Conversation; error?: string }> { + return this.request(`/conversations/${id}`, token); + } + + async getMessages(token: string, conversationId: string): Promise<{ data?: Message[]; error?: string }> { + return this.request(`/conversations/${conversationId}/messages`, token); + } + + async createConversation( + token: string, + data: { + title: string; + modelId: string; + conversationMode?: 'free' | 'guided' | 'template'; + } + ): Promise<{ data?: Conversation; error?: string }> { + return this.request('/conversations', token, { + method: 'POST', + body: JSON.stringify(data), + }); + } + + async addMessage( + token: string, + conversationId: string, + messageText: string, + sender: 'user' | 'assistant' = 'user' + ): Promise<{ data?: Message; error?: string }> { + return this.request(`/conversations/${conversationId}/messages`, token, { + method: 'POST', + body: JSON.stringify({ messageText, sender }), + }); + } + + async updateTitle( + token: string, + conversationId: string, + title: string + ): Promise<{ data?: Conversation; error?: string }> { + return this.request(`/conversations/${conversationId}/title`, token, { + method: 'PATCH', + body: JSON.stringify({ title }), + }); + } + + async archiveConversation(token: string, conversationId: string): Promise<{ data?: Conversation; error?: string }> { + return this.request(`/conversations/${conversationId}/archive`, token, { + method: 'PATCH', + }); + } + + async unarchiveConversation(token: string, conversationId: string): Promise<{ data?: Conversation; error?: string }> { + return this.request(`/conversations/${conversationId}/unarchive`, token, { + method: 'PATCH', + }); + } + + async pinConversation(token: string, conversationId: string): Promise<{ data?: Conversation; error?: string }> { + return this.request(`/conversations/${conversationId}/pin`, token, { + method: 'PATCH', + }); + } + + async unpinConversation(token: string, conversationId: string): Promise<{ data?: Conversation; error?: string }> { + return this.request(`/conversations/${conversationId}/unpin`, token, { + method: 'PATCH', + }); + } + + async deleteConversation(token: string, conversationId: string): Promise<{ error?: string }> { + return this.request(`/conversations/${conversationId}`, token, { + method: 'DELETE', + }); + } +} diff --git a/services/matrix-chat-bot/src/config/configuration.ts b/services/matrix-chat-bot/src/config/configuration.ts new file mode 100644 index 000000000..fcb89498a --- /dev/null +++ b/services/matrix-chat-bot/src/config/configuration.ts @@ -0,0 +1,68 @@ +export default () => ({ + port: parseInt(process.env.PORT, 10) || 3327, + matrix: { + homeserverUrl: process.env.MATRIX_HOMESERVER_URL || 'http://localhost:8008', + accessToken: process.env.MATRIX_ACCESS_TOKEN, + allowedRooms: process.env.MATRIX_ALLOWED_ROOMS?.split(',') || [], + storagePath: process.env.MATRIX_STORAGE_PATH || './data/bot-storage.json', + }, + chat: { + url: process.env.CHAT_BACKEND_URL || 'http://localhost:3002', + apiPrefix: process.env.CHAT_API_PREFIX || '', + }, + auth: { + url: process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001', + }, +}); + +export const HELP_MESSAGE = `**AI Chat Bot - Hilfe** + +**Authentifizierung:** +- \`!login email passwort\` - Anmelden +- \`!logout\` - Abmelden +- \`!status\` - Bot-Status anzeigen + +**Schnell-Chat:** +- \`!chat [nachricht]\` - Schnelle AI-Antwort (ohne Verlauf) +- \`!fragen [nachricht]\` - Alias fuer !chat + +**Gespraeche:** +- \`!neu [titel]\` - Neues Gespraech starten +- \`!gespraeche\` - Alle Gespraeche auflisten +- \`!gespraech [nr]\` - Gespraech auswaehlen/anzeigen +- \`!senden [nachricht]\` - Nachricht im aktuellen Gespraech senden +- \`!verlauf\` - Nachrichtenverlauf anzeigen + +**Gespraechsverwaltung:** +- \`!titel [nr] [neuer titel]\` - Titel aendern +- \`!archiv [nr]\` - Gespraech archivieren +- \`!archiviert\` - Archivierte Gespraeche anzeigen +- \`!wiederherstellen [nr]\` - Aus Archiv wiederherstellen +- \`!pin [nr]\` - Gespraech anpinnen +- \`!unpin [nr]\` - Pin entfernen +- \`!loeschen [nr]\` - Gespraech loeschen + +**Modelle:** +- \`!modelle\` - Verfuegbare AI-Modelle auflisten +- \`!modell [nr]\` - Modell fuer neues Gespraech waehlen + +**Beispiele:** +\`\`\` +!login max@example.com meinpasswort +!chat Was ist die Hauptstadt von Frankreich? +!neu Programmierung +!senden Erklaere mir Python Listen +!gespraeche +!gespraech 1 +!verlauf +!modelle +\`\`\` +`; + +export const BRANCH_ICONS: Record = { + ollama: '🏠', + openrouter: '☁️', + openai: '🤖', + anthropic: '🧠', + default: '🔮', +}; diff --git a/services/matrix-chat-bot/src/health.controller.ts b/services/matrix-chat-bot/src/health.controller.ts new file mode 100644 index 000000000..2c57d35ec --- /dev/null +++ b/services/matrix-chat-bot/src/health.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('health') +export class HealthController { + @Get() + check() { + return { status: 'ok', service: 'matrix-chat-bot' }; + } +} diff --git a/services/matrix-chat-bot/src/main.ts b/services/matrix-chat-bot/src/main.ts new file mode 100644 index 000000000..fe4169919 --- /dev/null +++ b/services/matrix-chat-bot/src/main.ts @@ -0,0 +1,10 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const port = process.env.PORT || 3327; + await app.listen(port); + console.log(`Matrix Chat Bot running on port ${port}`); +} +bootstrap(); diff --git a/services/matrix-chat-bot/src/session/session.module.ts b/services/matrix-chat-bot/src/session/session.module.ts new file mode 100644 index 000000000..834b715eb --- /dev/null +++ b/services/matrix-chat-bot/src/session/session.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { SessionService } from './session.service'; + +@Module({ + providers: [SessionService], + exports: [SessionService], +}) +export class SessionModule {} diff --git a/services/matrix-chat-bot/src/session/session.service.ts b/services/matrix-chat-bot/src/session/session.service.ts new file mode 100644 index 000000000..0ea5c854a --- /dev/null +++ b/services/matrix-chat-bot/src/session/session.service.ts @@ -0,0 +1,142 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +interface UserSession { + token: string; + email: string; + expiresAt: Date; + currentConversationId?: string; + selectedModelId?: string; +} + +@Injectable() +export class SessionService { + private readonly logger = new Logger(SessionService.name); + private sessions: Map = new Map(); + private authUrl: string; + + // Store conversation list mappings per user + private conversationMappings: Map = new Map(); + private modelMappings: Map = new Map(); + + constructor(private configService: ConfigService) { + this.authUrl = this.configService.get('auth.url') || 'http://localhost:3001'; + } + + async login( + matrixUserId: string, + email: string, + password: string + ): Promise<{ success: boolean; error?: string }> { + 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 errorData = await response.json().catch(() => ({})); + return { + success: false, + error: errorData.message || 'Authentifizierung fehlgeschlagen', + }; + } + + const data = await response.json(); + const token = data.accessToken || data.token; + + if (!token) { + return { success: false, error: 'Kein Token erhalten' }; + } + + this.sessions.set(matrixUserId, { + token, + email, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }); + + this.logger.log(`User ${matrixUserId} logged in as ${email}`); + return { success: true }; + } catch (error) { + this.logger.error(`Login failed for ${matrixUserId}:`, error); + return { + success: false, + error: 'Verbindung zum Auth-Server fehlgeschlagen', + }; + } + } + + logout(matrixUserId: string): void { + this.sessions.delete(matrixUserId); + this.conversationMappings.delete(matrixUserId); + this.modelMappings.delete(matrixUserId); + this.logger.log(`User ${matrixUserId} logged out`); + } + + getToken(matrixUserId: string): string | null { + const session = this.sessions.get(matrixUserId); + if (!session) return null; + if (session.expiresAt < new Date()) { + this.sessions.delete(matrixUserId); + return null; + } + return session.token; + } + + isLoggedIn(matrixUserId: string): boolean { + return this.getToken(matrixUserId) !== null; + } + + getSessionCount(): number { + return this.sessions.size; + } + + // Current conversation management + setCurrentConversation(matrixUserId: string, conversationId: string): void { + const session = this.sessions.get(matrixUserId); + if (session) { + session.currentConversationId = conversationId; + } + } + + getCurrentConversation(matrixUserId: string): string | null { + const session = this.sessions.get(matrixUserId); + return session?.currentConversationId || null; + } + + // Selected model management + setSelectedModel(matrixUserId: string, modelId: string): void { + const session = this.sessions.get(matrixUserId); + if (session) { + session.selectedModelId = modelId; + } + } + + getSelectedModel(matrixUserId: string): string | null { + const session = this.sessions.get(matrixUserId); + return session?.selectedModelId || null; + } + + // Conversation number mapping + setConversationMapping(matrixUserId: string, ids: string[]): void { + this.conversationMappings.set(matrixUserId, ids); + } + + getConversationId(matrixUserId: string, number: number): string | null { + const ids = this.conversationMappings.get(matrixUserId); + if (!ids || number < 1 || number > ids.length) return null; + return ids[number - 1]; + } + + // Model number mapping + setModelMapping(matrixUserId: string, ids: string[]): void { + this.modelMappings.set(matrixUserId, ids); + } + + getModelId(matrixUserId: string, number: number): string | null { + const ids = this.modelMappings.get(matrixUserId); + if (!ids || number < 1 || number > ids.length) return null; + return ids[number - 1]; + } +} diff --git a/services/matrix-chat-bot/tsconfig.json b/services/matrix-chat-bot/tsconfig.json new file mode 100644 index 000000000..94f1e9493 --- /dev/null +++ b/services/matrix-chat-bot/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true + } +}