From c5476447ecab592a14298e6c15e2fbfd7b55ce9a Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:48:56 +0100 Subject: [PATCH] feat(matrix-questions-bot): add Matrix bot for Q&A research management - Full NestJS bot with matrix-bot-sdk integration - Question management: create, list, view, delete, archive - Research: start quick/standard/deep research via mana-search - Results: view summaries, key points, follow-up questions - Sources: view ranked sources with relevance scores - Answers: view, rate (1-5), accept as solution - Collections: list and create for organization - Search: full-text search across questions - Status tracking: open, researching, answered, archived - Priority levels: low, normal, high, urgent - German/English command aliases - Number-based reference system - JWT auth via mana-core-auth - Runs on port 3324 Co-Authored-By: Claude Opus 4.5 --- services/matrix-questions-bot/.env.example | 15 + services/matrix-questions-bot/.gitignore | 29 + services/matrix-questions-bot/CLAUDE.md | 234 ++++++ services/matrix-questions-bot/Dockerfile | 41 + services/matrix-questions-bot/nest-cli.json | 5 + services/matrix-questions-bot/package.json | 27 + .../matrix-questions-bot/src/app.module.ts | 21 + .../src/bot/bot.module.ts | 11 + .../src/bot/matrix.service.ts | 745 ++++++++++++++++++ .../src/config/configuration.ts | 69 ++ .../src/health.controller.ts | 9 + services/matrix-questions-bot/src/main.ts | 10 + .../src/questions/questions.module.ts | 8 + .../src/questions/questions.service.ts | 219 +++++ .../src/session/session.module.ts | 8 + .../src/session/session.service.ts | 90 +++ services/matrix-questions-bot/tsconfig.json | 22 + 17 files changed, 1563 insertions(+) create mode 100644 services/matrix-questions-bot/.env.example create mode 100644 services/matrix-questions-bot/.gitignore create mode 100644 services/matrix-questions-bot/CLAUDE.md create mode 100644 services/matrix-questions-bot/Dockerfile create mode 100644 services/matrix-questions-bot/nest-cli.json create mode 100644 services/matrix-questions-bot/package.json create mode 100644 services/matrix-questions-bot/src/app.module.ts create mode 100644 services/matrix-questions-bot/src/bot/bot.module.ts create mode 100644 services/matrix-questions-bot/src/bot/matrix.service.ts create mode 100644 services/matrix-questions-bot/src/config/configuration.ts create mode 100644 services/matrix-questions-bot/src/health.controller.ts create mode 100644 services/matrix-questions-bot/src/main.ts create mode 100644 services/matrix-questions-bot/src/questions/questions.module.ts create mode 100644 services/matrix-questions-bot/src/questions/questions.service.ts create mode 100644 services/matrix-questions-bot/src/session/session.module.ts create mode 100644 services/matrix-questions-bot/src/session/session.service.ts create mode 100644 services/matrix-questions-bot/tsconfig.json diff --git a/services/matrix-questions-bot/.env.example b/services/matrix-questions-bot/.env.example new file mode 100644 index 000000000..e18a1dbd6 --- /dev/null +++ b/services/matrix-questions-bot/.env.example @@ -0,0 +1,15 @@ +# Server +PORT=3324 + +# Matrix +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN=syt_xxx +MATRIX_ALLOWED_ROOMS=#questions:matrix.mana.how +MATRIX_STORAGE_PATH=./data/bot-storage.json + +# Questions Backend +QUESTIONS_BACKEND_URL=http://localhost:3011 +QUESTIONS_API_PREFIX=/api/v1 + +# Mana Core Auth +MANA_CORE_AUTH_URL=http://localhost:3001 diff --git a/services/matrix-questions-bot/.gitignore b/services/matrix-questions-bot/.gitignore new file mode 100644 index 000000000..2d508e5ec --- /dev/null +++ b/services/matrix-questions-bot/.gitignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment +.env +.env.local + +# Data +data/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# TypeScript +*.tsbuildinfo diff --git a/services/matrix-questions-bot/CLAUDE.md b/services/matrix-questions-bot/CLAUDE.md new file mode 100644 index 000000000..f94adf6ab --- /dev/null +++ b/services/matrix-questions-bot/CLAUDE.md @@ -0,0 +1,234 @@ +# Matrix Questions Bot - Claude Code Guidelines + +## Overview + +Matrix Questions Bot provides Q&A research management via Matrix chat. It integrates with the Questions backend for question management, web research via mana-search, answer tracking, and collection organization. + +## Tech Stack + +- **Framework**: NestJS 10 +- **Matrix**: matrix-bot-sdk +- **Backend**: Questions API (port 3011) +- **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-questions-bot/ +├── src/ +│ ├── main.ts # Application entry point (port 3324) +│ ├── 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 +│ ├── questions/ +│ │ ├── questions.module.ts +│ │ └── questions.service.ts # Questions 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 | + +### Question Management + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!fragen` | questions, liste | List all questions | +| `!fragen offen` | - | Filter by status | +| `!frage [nr]` | question, details | Show question details | +| `!neu Frage?` | new, ask | Create new question | +| `!loeschen [nr]` | delete | Delete question | +| `!archivieren [nr]` | archive | Archive question | + +### Research + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!recherche [nr]` | research | Start quick research | +| `!recherche [nr] standard` | - | Standard research (15 sources) | +| `!recherche [nr] deep` | - | Deep research (30 sources) | +| `!ergebnis [nr]` | result | Show research result | +| `!quellen [nr]` | sources | Show sources | + +### Answers + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!antwort [nr]` | answer | Show answer | +| `!bewerten [nr] 1-5` | rate | Rate answer | +| `!akzeptieren [nr]` | accept | Accept as solution | + +### Collections + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!sammlungen` | collections | List collections | +| `!sammlung Name` | collection | Create collection | + +### Search + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!suche Begriff` | search | Search questions | + +## Research Depths + +| Depth | Sources | Content Extraction | Categories | +|-------|---------|-------------------|------------| +| `quick` | 5 | No | general | +| `standard` | 15 | Yes | general, news | +| `deep` | 30 | Yes | general, news, science, it | + +## Question Status + +| Status | Emoji | Description | +|--------|-------|-------------| +| `open` | ❓ | New question | +| `researching` | 🔍 | Research in progress | +| `answered` | ✅ | Has answer | +| `archived` | 📦 | Archived | + +## Priority Levels + +| Priority | Indicator | +|----------|-----------| +| `urgent` | 🔴 | +| `high` | 🟠 | +| `normal` | (none) | +| `low` | (none) | + +## Example Usage + +``` +# Login +!login max@example.com mypassword + +# Create a new question +!neu Was ist Quantencomputing? + +# List questions +!fragen + +# Start research +!recherche 1 standard + +# View sources +!quellen 1 + +# View answer +!antwort 1 + +# Rate the answer +!bewerten 1 5 + +# Accept as solution +!akzeptieren 1 + +# Search questions +!suche quantum + +# Create collection +!sammlung Wissenschaft +``` + +## Environment Variables + +```env +# Server +PORT=3324 + +# Matrix +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN=syt_xxx +MATRIX_ALLOWED_ROOMS=#questions:matrix.mana.how +MATRIX_STORAGE_PATH=./data/bot-storage.json + +# Questions Backend +QUESTIONS_BACKEND_URL=http://localhost:3011 +QUESTIONS_API_PREFIX=/api/v1 + +# Mana Core Auth +MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +## Docker + +```bash +# Build locally +docker build -f services/matrix-questions-bot/Dockerfile -t matrix-questions-bot services/matrix-questions-bot + +# Run +docker run -p 3324:3324 \ + -e MATRIX_HOMESERVER_URL=http://synapse:8008 \ + -e MATRIX_ACCESS_TOKEN=syt_xxx \ + -e QUESTIONS_BACKEND_URL=http://questions-backend:3011 \ + -e MANA_CORE_AUTH_URL=http://mana-core-auth:3001 \ + -v matrix-questions-bot-data:/app/data \ + matrix-questions-bot +``` + +## Health Check + +```bash +curl http://localhost:3324/health +``` + +## Questions Backend API Endpoints Used + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check | +| `/api/v1/questions` | GET | List questions | +| `/api/v1/questions` | POST | Create question | +| `/api/v1/questions/:id` | GET | Get question | +| `/api/v1/questions/:id` | DELETE | Delete question | +| `/api/v1/questions/:id/status` | PUT | Update status | +| `/api/v1/research/start` | POST | Start research | +| `/api/v1/research/question/:id` | GET | Get research results | +| `/api/v1/sources/question/:id` | GET | Get sources | +| `/api/v1/answers/question/:id` | GET | Get answers | +| `/api/v1/answers/:id/rate` | POST | Rate answer | +| `/api/v1/answers/:id/accept` | POST | Accept answer | +| `/api/v1/collections` | GET | List collections | +| `/api/v1/collections` | POST | Create collection | + +## Number-Based Reference System + +The bot uses a number-based reference system for ease of use: +1. User runs `!fragen` to get a list of questions +2. Bot stores the list internally for the user +3. User can reference questions by their list number +4. Numbers are valid until the user runs a new list command + +This allows simple commands like: +- `!frage 3` - Show details for question #3 +- `!recherche 1 deep` - Start deep research for question #1 +- `!antwort 2` - Show answer for question #2 diff --git a/services/matrix-questions-bot/Dockerfile b/services/matrix-questions-bot/Dockerfile new file mode 100644 index 000000000..eaf57520b --- /dev/null +++ b/services/matrix-questions-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 3324 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3324/health || exit 1 + +# Start the application +CMD ["node", "dist/main.js"] diff --git a/services/matrix-questions-bot/nest-cli.json b/services/matrix-questions-bot/nest-cli.json new file mode 100644 index 000000000..5c06bb8c3 --- /dev/null +++ b/services/matrix-questions-bot/nest-cli.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json-schema.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} diff --git a/services/matrix-questions-bot/package.json b/services/matrix-questions-bot/package.json new file mode 100644 index 000000000..1bf3a5bce --- /dev/null +++ b/services/matrix-questions-bot/package.json @@ -0,0 +1,27 @@ +{ + "name": "@mana-bots/matrix-questions-bot", + "version": "1.0.0", + "description": "Matrix bot for Q&A research management", + "main": "dist/main.js", + "scripts": { + "build": "nest build", + "start": "nest start", + "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.1.13", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.3.0", + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + } +} diff --git a/services/matrix-questions-bot/src/app.module.ts b/services/matrix-questions-bot/src/app.module.ts new file mode 100644 index 000000000..8042c3990 --- /dev/null +++ b/services/matrix-questions-bot/src/app.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { HealthController } from './health.controller'; +import { BotModule } from './bot/bot.module'; +import { QuestionsModule } from './questions/questions.module'; +import { SessionModule } from './session/session.module'; +import configuration from './config/configuration'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [configuration], + }), + BotModule, + QuestionsModule, + SessionModule, + ], + controllers: [HealthController], +}) +export class AppModule {} diff --git a/services/matrix-questions-bot/src/bot/bot.module.ts b/services/matrix-questions-bot/src/bot/bot.module.ts new file mode 100644 index 000000000..76c464e96 --- /dev/null +++ b/services/matrix-questions-bot/src/bot/bot.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { MatrixService } from './matrix.service'; +import { QuestionsModule } from '../questions/questions.module'; +import { SessionModule } from '../session/session.module'; + +@Module({ + imports: [QuestionsModule, SessionModule], + providers: [MatrixService], + exports: [MatrixService], +}) +export class BotModule {} diff --git a/services/matrix-questions-bot/src/bot/matrix.service.ts b/services/matrix-questions-bot/src/bot/matrix.service.ts new file mode 100644 index 000000000..f8d963390 --- /dev/null +++ b/services/matrix-questions-bot/src/bot/matrix.service.ts @@ -0,0 +1,745 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + MatrixClient, + SimpleFsStorageProvider, + AutojoinRoomsMixin, +} from 'matrix-bot-sdk'; +import { QuestionsService, Question, Collection, Answer } from '../questions/questions.service'; +import { SessionService } from '../session/session.service'; +import { HELP_MESSAGE } from '../config/configuration'; + +@Injectable() +export class MatrixService implements OnModuleInit { + private readonly logger = new Logger(MatrixService.name); + private client: MatrixClient; + private allowedRooms: string[]; + + // Store last shown items per user for reference by number + private lastQuestionsList: Map = new Map(); + private lastCollectionsList: Map = new Map(); + private lastAnswersList: Map = new Map(); + + constructor( + private configService: ConfigService, + private questionsService: QuestionsService, + 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 Questions 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; + + // Check allowed rooms + if (this.allowedRooms.length > 0 && !this.allowedRooms.includes(roomId)) { + return; + } + + const sender = event.sender; + const parts = body.slice(1).split(/\s+/); + const command = parts[0].toLowerCase(); + const args = parts.slice(1); + const argString = args.join(' '); + + try { + switch (command) { + case 'help': + case 'hilfe': + await this.sendHtml(roomId, HELP_MESSAGE); + break; + + case 'login': + await this.handleLogin(roomId, sender, args); + break; + + case 'logout': + this.sessionService.logout(sender); + await this.sendHtml(roomId, '

Erfolgreich abgemeldet.

'); + break; + + case 'status': + await this.handleStatus(roomId, sender); + break; + + // Question commands + case 'fragen': + case 'questions': + case 'liste': + await this.handleListQuestions(roomId, sender, args[0]); + break; + + case 'frage': + case 'question': + case 'details': + await this.handleQuestionDetails(roomId, sender, args[0]); + break; + + case 'neu': + case 'new': + case 'ask': + await this.handleCreateQuestion(roomId, sender, argString); + break; + + case 'loeschen': + case 'delete': + await this.handleDeleteQuestion(roomId, sender, args[0]); + break; + + case 'archivieren': + case 'archive': + await this.handleArchiveQuestion(roomId, sender, args[0]); + break; + + // Research commands + case 'recherche': + case 'research': + await this.handleStartResearch(roomId, sender, args[0], args[1]); + break; + + case 'ergebnis': + case 'result': + await this.handleResearchResult(roomId, sender, args[0]); + break; + + case 'quellen': + case 'sources': + await this.handleSources(roomId, sender, args[0]); + break; + + // Answer commands + case 'antwort': + case 'answer': + await this.handleAnswer(roomId, sender, args[0]); + break; + + case 'bewerten': + case 'rate': + await this.handleRateAnswer(roomId, sender, args[0], args[1]); + break; + + case 'akzeptieren': + case 'accept': + await this.handleAcceptAnswer(roomId, sender, args[0]); + break; + + // Collection commands + case 'sammlungen': + case 'collections': + await this.handleListCollections(roomId, sender); + break; + + case 'sammlung': + case 'collection': + await this.handleCreateCollection(roomId, sender, argString); + break; + + // Search + case 'suche': + case 'search': + await this.handleSearch(roomId, sender, argString); + break; + + default: + await this.sendHtml( + roomId, + `

Unbekannter Befehl: ${command}. Nutze !help fuer Hilfe.

` + ); + } + } catch (error) { + this.logger.error(`Error handling command ${command}:`, error); + await this.sendHtml(roomId, `

Fehler: ${error.message}

`); + } + } + + private async sendHtml(roomId: string, html: string) { + await this.client.sendMessage(roomId, { + msgtype: 'm.text', + body: html.replace(/<[^>]*>/g, ''), + format: 'org.matrix.custom.html', + formatted_body: html, + }); + } + + private requireAuth(sender: string): string { + const token = this.sessionService.getToken(sender); + if (!token) { + throw new Error('Nicht angemeldet. Nutze !login email passwort'); + } + return token; + } + + // Auth handlers + private async handleLogin(roomId: string, sender: string, args: string[]) { + if (args.length < 2) { + await this.sendHtml(roomId, '

Verwendung: !login email passwort

'); + return; + } + + const [email, password] = args; + const result = await this.sessionService.login(sender, email, password); + + if (result.success) { + await this.sendHtml(roomId, `

Erfolgreich angemeldet als ${email}

`); + } else { + await this.sendHtml(roomId, `

Login fehlgeschlagen: ${result.error}

`); + } + } + + private async handleStatus(roomId: string, sender: string) { + const backendOk = await this.questionsService.checkHealth(); + const loggedIn = this.sessionService.isLoggedIn(sender); + const sessions = this.sessionService.getSessionCount(); + + await this.sendHtml( + roomId, + `

Questions Bot Status

+
    +
  • Backend: ${backendOk ? 'Online' : 'Offline'}
  • +
  • Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}
  • +
  • Aktive Sessions: ${sessions}
  • +
` + ); + } + + // Question handlers + private async handleListQuestions(roomId: string, sender: string, statusFilter?: string) { + const token = this.requireAuth(sender); + + const options: any = {}; + if (statusFilter) { + const statusMap: Record = { + offen: 'open', + open: 'open', + recherche: 'researching', + researching: 'researching', + beantwortet: 'answered', + answered: 'answered', + archiviert: 'archived', + archived: 'archived', + }; + options.status = statusMap[statusFilter.toLowerCase()] || statusFilter; + } + + const result = await this.questionsService.getQuestions(token, options); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const questions = result.data || []; + this.lastQuestionsList.set(sender, questions); + + if (questions.length === 0) { + await this.sendHtml( + roomId, + '

Keine Fragen vorhanden. Stelle eine mit !neu Frage?

' + ); + return; + } + + let html = '

Deine Fragen

    '; + for (const q of questions) { + const status = this.getStatusEmoji(q.status); + const priority = this.getPriorityIndicator(q.priority); + html += `
  1. ${status} ${priority}${q.title}
  2. `; + } + html += '
'; + html += '

Nutze !frage [nr] fuer Details oder !recherche [nr]

'; + + await this.sendHtml(roomId, html); + } + + private async handleQuestionDetails(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const question = this.getQuestionByNumber(sender, numberStr); + + if (!question) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !fragen

'); + return; + } + + const result = await this.questionsService.getQuestion(token, question.id); + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const q = result.data!; + const status = this.getStatusEmoji(q.status); + let html = `

${status} ${q.title}

`; + + if (q.description) html += `

${q.description}

`; + + html += '
    '; + html += `
  • Status: ${this.translateStatus(q.status)}
  • `; + html += `
  • Prioritaet: ${this.translatePriority(q.priority)}
  • `; + html += `
  • Recherche-Tiefe: ${q.researchDepth}
  • `; + if (q.tags?.length) html += `
  • Tags: ${q.tags.join(', ')}
  • `; + if (q.category) html += `
  • Kategorie: ${q.category}
  • `; + html += `
  • Erstellt: ${new Date(q.createdAt).toLocaleDateString('de-DE')}
  • `; + if (q.answeredAt) html += `
  • Beantwortet: ${new Date(q.answeredAt).toLocaleDateString('de-DE')}
  • `; + html += '
'; + + html += `

Nutze !recherche ${numberStr} um eine Recherche zu starten

`; + + await this.sendHtml(roomId, html); + } + + private async handleCreateQuestion(roomId: string, sender: string, title: string) { + if (!title) { + await this.sendHtml(roomId, '

Verwendung: !neu Deine Frage?

'); + return; + } + + const token = this.requireAuth(sender); + const result = await this.questionsService.createQuestion(token, title); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + this.lastQuestionsList.delete(sender); + await this.sendHtml( + roomId, + `

Frage erstellt: ${result.data!.title}

+

Nutze !fragen und dann !recherche [nr] um zu recherchieren.

` + ); + } + + private async handleDeleteQuestion(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const question = this.getQuestionByNumber(sender, numberStr); + + if (!question) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !fragen

'); + return; + } + + const result = await this.questionsService.deleteQuestion(token, question.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + this.lastQuestionsList.delete(sender); + await this.sendHtml(roomId, `

Frage geloescht: ${question.title}

`); + } + + private async handleArchiveQuestion(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const question = this.getQuestionByNumber(sender, numberStr); + + if (!question) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !fragen

'); + return; + } + + const result = await this.questionsService.updateQuestionStatus(token, question.id, 'archived'); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + await this.sendHtml(roomId, `

Frage archiviert: ${question.title}

`); + } + + // Research handlers + private async handleStartResearch(roomId: string, sender: string, numberStr: string, depthStr?: string) { + const token = this.requireAuth(sender); + const question = this.getQuestionByNumber(sender, numberStr); + + if (!question) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !fragen

'); + return; + } + + const depthMap: Record = { + schnell: 'quick', + quick: 'quick', + standard: 'standard', + normal: 'standard', + tief: 'deep', + deep: 'deep', + }; + const depth = depthMap[depthStr?.toLowerCase() || ''] || 'quick'; + + await this.sendHtml(roomId, `

Starte ${depth}-Recherche fuer: ${question.title}...

`); + + const result = await this.questionsService.startResearch(token, question.id, depth); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const research = result.data!; + let html = `

Recherche abgeschlossen

`; + + if (research.summary) { + html += `

Zusammenfassung:

${research.summary}

`; + } + + if (research.keyPoints?.length) { + html += '

Wichtige Punkte:

    '; + for (const point of research.keyPoints.slice(0, 5)) { + html += `
  • ${point}
  • `; + } + html += '
'; + } + + if (research.followUpQuestions?.length) { + html += '

Folge-Fragen:

    '; + for (const fq of research.followUpQuestions.slice(0, 3)) { + html += `
  • ${fq}
  • `; + } + html += '
'; + } + + html += `

Nutze !quellen ${numberStr} fuer die Quellen

`; + + await this.sendHtml(roomId, html); + } + + private async handleResearchResult(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const question = this.getQuestionByNumber(sender, numberStr); + + if (!question) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !fragen

'); + return; + } + + const result = await this.questionsService.getResearchResults(token, question.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const results = result.data || []; + + if (results.length === 0) { + await this.sendHtml( + roomId, + `

Keine Recherche-Ergebnisse. Nutze !recherche ${numberStr}

` + ); + return; + } + + const latest = results[0]; + let html = `

Recherche-Ergebnis

`; + html += `

Tiefe: ${latest.researchDepth}

`; + + if (latest.summary) { + html += `

${latest.summary}

`; + } + + if (latest.keyPoints?.length) { + html += '

Wichtige Punkte:

    '; + for (const point of latest.keyPoints) { + html += `
  • ${point}
  • `; + } + html += '
'; + } + + await this.sendHtml(roomId, html); + } + + private async handleSources(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const question = this.getQuestionByNumber(sender, numberStr); + + if (!question) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !fragen

'); + return; + } + + const result = await this.questionsService.getSources(token, question.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const sources = result.data || []; + + if (sources.length === 0) { + await this.sendHtml(roomId, '

Keine Quellen vorhanden.

'); + return; + } + + let html = `

Quellen fuer: ${question.title}

    `; + for (const source of sources.slice(0, 10)) { + const relevance = source.relevanceScore ? ` (${Math.round(source.relevanceScore * 100)}%)` : ''; + html += `
  1. ${source.title}${relevance}
    ${source.domain}
  2. `; + } + html += '
'; + + if (sources.length > 10) { + html += `

...und ${sources.length - 10} weitere Quellen

`; + } + + await this.sendHtml(roomId, html); + } + + // Answer handlers + private async handleAnswer(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const question = this.getQuestionByNumber(sender, numberStr); + + if (!question) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !fragen

'); + return; + } + + const result = await this.questionsService.getAnswers(token, question.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const answers = result.data || []; + this.lastAnswersList.set(sender, answers); + + if (answers.length === 0) { + await this.sendHtml( + roomId, + `

Keine Antworten. Starte zuerst eine Recherche mit !recherche ${numberStr}

` + ); + return; + } + + // Show the first (most recent) answer + const answer = answers[0]; + const accepted = answer.isAccepted ? ' ✅' : ''; + const rating = answer.rating ? ` (${answer.rating}/5 Sterne)` : ''; + const confidence = answer.confidence ? ` [${Math.round(answer.confidence * 100)}% Konfidenz]` : ''; + + let html = `

Antwort${accepted}${rating}

`; + html += `

Model: ${answer.modelId}${confidence}

`; + + if (answer.summary) { + html += `

Zusammenfassung: ${answer.summary}

`; + } + + html += `

${answer.contentMarkdown || answer.content}

`; + + if (answer.sourceCount) { + html += `

Basierend auf ${answer.sourceCount} Quellen

`; + } + + html += `

Nutze !bewerten ${numberStr} 1-5 zum Bewerten

`; + + await this.sendHtml(roomId, html); + } + + private async handleRateAnswer(roomId: string, sender: string, numberStr: string, ratingStr: string) { + const token = this.requireAuth(sender); + const answers = this.lastAnswersList.get(sender); + + if (!answers || answers.length === 0) { + await this.sendHtml(roomId, '

Zeige zuerst eine Antwort mit !antwort [nr]

'); + return; + } + + const rating = parseInt(ratingStr, 10); + if (isNaN(rating) || rating < 1 || rating > 5) { + await this.sendHtml(roomId, '

Bewertung muss zwischen 1 und 5 sein.

'); + return; + } + + const answer = answers[0]; + const result = await this.questionsService.rateAnswer(token, answer.id, rating); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + await this.sendHtml(roomId, `

Antwort mit ${rating} Sternen bewertet.

`); + } + + private async handleAcceptAnswer(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const answers = this.lastAnswersList.get(sender); + + if (!answers || answers.length === 0) { + await this.sendHtml(roomId, '

Zeige zuerst eine Antwort mit !antwort [nr]

'); + return; + } + + const answer = answers[0]; + const result = await this.questionsService.acceptAnswer(token, answer.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + await this.sendHtml(roomId, '

Antwort als Loesung akzeptiert. ✅

'); + } + + // Collection handlers + private async handleListCollections(roomId: string, sender: string) { + const token = this.requireAuth(sender); + const result = await this.questionsService.getCollections(token); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const collections = result.data || []; + this.lastCollectionsList.set(sender, collections); + + if (collections.length === 0) { + await this.sendHtml( + roomId, + '

Keine Sammlungen. Erstelle eine mit !sammlung Name

' + ); + return; + } + + let html = '

Sammlungen

    '; + for (const c of collections) { + const defaultMark = c.isDefault ? ' (Standard)' : ''; + const count = c.questionCount !== undefined ? ` [${c.questionCount} Fragen]` : ''; + html += `
  1. ${c.name}${defaultMark}${count}
  2. `; + } + html += '
'; + + await this.sendHtml(roomId, html); + } + + private async handleCreateCollection(roomId: string, sender: string, name: string) { + if (!name) { + await this.sendHtml(roomId, '

Verwendung: !sammlung Name

'); + return; + } + + const token = this.requireAuth(sender); + const result = await this.questionsService.createCollection(token, name); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + this.lastCollectionsList.delete(sender); + await this.sendHtml(roomId, `

Sammlung ${result.data!.name} erstellt.

`); + } + + // Search handler + private async handleSearch(roomId: string, sender: string, query: string) { + if (!query) { + await this.sendHtml(roomId, '

Verwendung: !suche Begriff

'); + return; + } + + const token = this.requireAuth(sender); + const result = await this.questionsService.getQuestions(token, { search: query }); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const questions = result.data || []; + this.lastQuestionsList.set(sender, questions); + + if (questions.length === 0) { + await this.sendHtml(roomId, `

Keine Fragen gefunden fuer "${query}"

`); + return; + } + + let html = `

Suchergebnisse: "${query}"

    `; + for (const q of questions) { + const status = this.getStatusEmoji(q.status); + html += `
  1. ${status} ${q.title}
  2. `; + } + html += '
'; + + await this.sendHtml(roomId, html); + } + + // Helper methods + private getQuestionByNumber(sender: string, numberStr: string): Question | null { + const questions = this.lastQuestionsList.get(sender); + if (!questions) return null; + + const index = parseInt(numberStr, 10) - 1; + if (isNaN(index) || index < 0 || index >= questions.length) return null; + + return questions[index]; + } + + private getStatusEmoji(status: string): string { + const map: Record = { + open: '❓', // Question mark + researching: '🔍', // Magnifying glass + answered: '✅', // Check mark + archived: '📦', // Package + }; + return map[status] || '❓'; + } + + private translateStatus(status: string): string { + const map: Record = { + open: 'Offen', + researching: 'In Recherche', + answered: 'Beantwortet', + archived: 'Archiviert', + }; + return map[status] || status; + } + + private getPriorityIndicator(priority: string): string { + const map: Record = { + urgent: '🔴 ', // Red circle + high: '🟠 ', // Orange circle + normal: '', + low: '', + }; + return map[priority] || ''; + } + + private translatePriority(priority: string): string { + const map: Record = { + low: 'Niedrig', + normal: 'Normal', + high: 'Hoch', + urgent: 'Dringend', + }; + return map[priority] || priority; + } +} diff --git a/services/matrix-questions-bot/src/config/configuration.ts b/services/matrix-questions-bot/src/config/configuration.ts new file mode 100644 index 000000000..0b8418e4c --- /dev/null +++ b/services/matrix-questions-bot/src/config/configuration.ts @@ -0,0 +1,69 @@ +export default () => ({ + port: parseInt(process.env.PORT, 10) || 3324, + 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', + }, + questions: { + backendUrl: process.env.QUESTIONS_BACKEND_URL || 'http://localhost:3011', + apiPrefix: process.env.QUESTIONS_API_PREFIX || '/api/v1', + }, + auth: { + url: process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001', + }, +}); + +export const HELP_MESSAGE = `

Questions Bot - Befehle

+ +

Authentifizierung

+
    +
  • !login email passwort - Anmelden
  • +
  • !logout - Abmelden
  • +
  • !status - Bot-Status anzeigen
  • +
+ +

Fragen

+
    +
  • !fragen - Alle Fragen auflisten
  • +
  • !fragen offen - Offene Fragen
  • +
  • !frage [nr] - Frage-Details anzeigen
  • +
  • !neu Frage? - Neue Frage stellen
  • +
  • !loeschen [nr] - Frage loeschen
  • +
  • !archivieren [nr] - Frage archivieren
  • +
+ +

Recherche

+
    +
  • !recherche [nr] - Recherche starten (quick)
  • +
  • !recherche [nr] standard - Standard-Recherche
  • +
  • !recherche [nr] deep - Tiefe Recherche
  • +
  • !ergebnis [nr] - Recherche-Ergebnis anzeigen
  • +
  • !quellen [nr] - Quellen anzeigen
  • +
+ +

Antworten

+
    +
  • !antwort [nr] - Antwort zur Frage anzeigen
  • +
  • !bewerten [nr] 1-5 - Antwort bewerten
  • +
  • !akzeptieren [nr] - Antwort akzeptieren
  • +
+ +

Sammlungen

+
    +
  • !sammlungen - Alle Sammlungen
  • +
  • !sammlung [name] - Neue Sammlung erstellen
  • +
+ +

Suche

+
    +
  • !suche Begriff - Fragen durchsuchen
  • +
+ +

Weitere Befehle

+
    +
  • !help - Diese Hilfe anzeigen
  • +
+ +

Tipp: Nutze Nummern aus der zuletzt angezeigten Liste.

`; diff --git a/services/matrix-questions-bot/src/health.controller.ts b/services/matrix-questions-bot/src/health.controller.ts new file mode 100644 index 000000000..6feedb57c --- /dev/null +++ b/services/matrix-questions-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-questions-bot' }; + } +} diff --git a/services/matrix-questions-bot/src/main.ts b/services/matrix-questions-bot/src/main.ts new file mode 100644 index 000000000..5ae0c7777 --- /dev/null +++ b/services/matrix-questions-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 || 3324; + await app.listen(port); + console.log(`Matrix Questions Bot running on port ${port}`); +} +bootstrap(); diff --git a/services/matrix-questions-bot/src/questions/questions.module.ts b/services/matrix-questions-bot/src/questions/questions.module.ts new file mode 100644 index 000000000..0c14b7637 --- /dev/null +++ b/services/matrix-questions-bot/src/questions/questions.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { QuestionsService } from './questions.service'; + +@Module({ + providers: [QuestionsService], + exports: [QuestionsService], +}) +export class QuestionsModule {} diff --git a/services/matrix-questions-bot/src/questions/questions.service.ts b/services/matrix-questions-bot/src/questions/questions.service.ts new file mode 100644 index 000000000..65e0dc526 --- /dev/null +++ b/services/matrix-questions-bot/src/questions/questions.service.ts @@ -0,0 +1,219 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +export interface Question { + id: string; + title: string; + description?: string; + status: 'open' | 'researching' | 'answered' | 'archived'; + priority: 'low' | 'normal' | 'high' | 'urgent'; + tags: string[]; + category?: string; + researchDepth: 'quick' | 'standard' | 'deep'; + collectionId?: string; + createdAt: string; + updatedAt: string; + answeredAt?: string; +} + +export interface Collection { + id: string; + name: string; + description?: string; + color: string; + icon: string; + isDefault: boolean; + questionCount?: number; + createdAt: string; +} + +export interface ResearchResult { + id: string; + questionId: string; + researchDepth: string; + summary?: string; + keyPoints?: string[]; + followUpQuestions?: string[]; + createdAt: string; + durationMs?: number; +} + +export interface Source { + id: string; + url: string; + title: string; + snippet?: string; + domain: string; + relevanceScore?: number; + position: number; + engine: string; +} + +export interface Answer { + id: string; + questionId: string; + content: string; + contentMarkdown?: string; + summary?: string; + modelId: string; + provider: string; + confidence?: number; + sourceCount?: number; + rating?: number; + isAccepted: boolean; + createdAt: string; +} + +@Injectable() +export class QuestionsService { + private readonly logger = new Logger(QuestionsService.name); + private backendUrl: string; + private apiPrefix: string; + + constructor(private configService: ConfigService) { + this.backendUrl = this.configService.get('questions.backendUrl') || 'http://localhost:3011'; + this.apiPrefix = this.configService.get('questions.apiPrefix') || '/api/v1'; + } + + private async request( + token: string, + endpoint: string, + options: RequestInit = {} + ): Promise<{ data?: T; error?: string }> { + try { + const url = `${this.backendUrl}${this.apiPrefix}${endpoint}`; + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + return { error: errorData.message || `Fehler: ${response.status}` }; + } + + const data = await response.json(); + return { data }; + } catch (error) { + this.logger.error(`Request failed: ${endpoint}`, error); + return { error: 'Verbindung zum Backend fehlgeschlagen' }; + } + } + + // Question operations + async getQuestions( + token: string, + options: { status?: string; search?: string; collectionId?: string } = {} + ): Promise<{ data?: Question[]; error?: string }> { + const params = new URLSearchParams(); + if (options.status) params.set('status', options.status); + if (options.search) params.set('search', options.search); + if (options.collectionId) params.set('collectionId', options.collectionId); + const query = params.toString() ? `?${params.toString()}` : ''; + return this.request(token, `/questions${query}`); + } + + async getQuestion(token: string, questionId: string): Promise<{ data?: Question; error?: string }> { + return this.request(token, `/questions/${questionId}`); + } + + async createQuestion( + token: string, + title: string, + options: { description?: string; priority?: string; tags?: string[]; collectionId?: string } = {} + ): Promise<{ data?: Question; error?: string }> { + return this.request(token, '/questions', { + method: 'POST', + body: JSON.stringify({ title, ...options }), + }); + } + + async updateQuestionStatus( + token: string, + questionId: string, + status: string + ): Promise<{ data?: Question; error?: string }> { + return this.request(token, `/questions/${questionId}/status`, { + method: 'PUT', + body: JSON.stringify({ status }), + }); + } + + async deleteQuestion(token: string, questionId: string): Promise<{ error?: string }> { + return this.request(token, `/questions/${questionId}`, { method: 'DELETE' }); + } + + // Research operations + async startResearch( + token: string, + questionId: string, + depth: 'quick' | 'standard' | 'deep' = 'quick' + ): Promise<{ data?: ResearchResult; error?: string }> { + return this.request(token, '/research/start', { + method: 'POST', + body: JSON.stringify({ questionId, depth }), + }); + } + + async getResearchResults(token: string, questionId: string): Promise<{ data?: ResearchResult[]; error?: string }> { + return this.request(token, `/research/question/${questionId}`); + } + + async getResearchResult(token: string, researchId: string): Promise<{ data?: ResearchResult; error?: string }> { + return this.request(token, `/research/${researchId}`); + } + + // Source operations + async getSources(token: string, questionId: string): Promise<{ data?: Source[]; error?: string }> { + return this.request(token, `/sources/question/${questionId}`); + } + + // Answer operations + async getAnswers(token: string, questionId: string): Promise<{ data?: Answer[]; error?: string }> { + return this.request(token, `/answers/question/${questionId}`); + } + + async getAcceptedAnswer(token: string, questionId: string): Promise<{ data?: Answer; error?: string }> { + return this.request(token, `/answers/question/${questionId}/accepted`); + } + + async rateAnswer(token: string, answerId: string, rating: number): Promise<{ data?: Answer; error?: string }> { + return this.request(token, `/answers/${answerId}/rate`, { + method: 'POST', + body: JSON.stringify({ rating }), + }); + } + + async acceptAnswer(token: string, answerId: string): Promise<{ data?: Answer; error?: string }> { + return this.request(token, `/answers/${answerId}/accept`, { method: 'POST' }); + } + + // Collection operations + async getCollections(token: string): Promise<{ data?: Collection[]; error?: string }> { + return this.request(token, '/collections'); + } + + async createCollection( + token: string, + name: string, + options: { description?: string; color?: string } = {} + ): Promise<{ data?: Collection; error?: string }> { + return this.request(token, '/collections', { + method: 'POST', + body: JSON.stringify({ name, ...options }), + }); + } + + async checkHealth(): Promise { + try { + const response = await fetch(`${this.backendUrl}/health`); + return response.ok; + } catch { + return false; + } + } +} diff --git a/services/matrix-questions-bot/src/session/session.module.ts b/services/matrix-questions-bot/src/session/session.module.ts new file mode 100644 index 000000000..834b715eb --- /dev/null +++ b/services/matrix-questions-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-questions-bot/src/session/session.service.ts b/services/matrix-questions-bot/src/session/session.service.ts new file mode 100644 index 000000000..f1bed7852 --- /dev/null +++ b/services/matrix-questions-bot/src/session/session.service.ts @@ -0,0 +1,90 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +interface UserSession { + token: string; + email: string; + expiresAt: Date; +} + +@Injectable() +export class SessionService { + private readonly logger = new Logger(SessionService.name); + private sessions: Map = new Map(); + private authUrl: string; + + 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' }; + } + + // Store session (7 days expiry) + 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.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; + } +} diff --git a/services/matrix-questions-bot/tsconfig.json b/services/matrix-questions-bot/tsconfig.json new file mode 100644 index 000000000..94f1e9493 --- /dev/null +++ b/services/matrix-questions-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 + } +}