mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
✨ feat(matrix-zitare-bot): add Matrix bot for daily inspiration quotes
Features: - Random quotes and daily quote of the day - 10 categories (motivation, wisdom, love, life, success, etc.) - Search functionality - Login integration with Zitare backend - Favorites and lists management - Voice note transcription via mana-stt - Natural language command support (German/English)
This commit is contained in:
parent
29595a9d3d
commit
a532790d99
20 changed files with 1834 additions and 0 deletions
6
services/matrix-zitare-bot/.dockerignore
Normal file
6
services/matrix-zitare-bot/.dockerignore
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
dist
|
||||
.git
|
||||
*.log
|
||||
.env*
|
||||
data
|
||||
7
services/matrix-zitare-bot/.gitignore
vendored
Normal file
7
services/matrix-zitare-bot/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
dist
|
||||
.turbo
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
data
|
||||
178
services/matrix-zitare-bot/CLAUDE.md
Normal file
178
services/matrix-zitare-bot/CLAUDE.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# Matrix Zitare Bot - Claude Code Guidelines
|
||||
|
||||
## Overview
|
||||
|
||||
Matrix Zitare Bot provides daily inspirational quotes via Matrix chat. It includes a built-in collection of German quotes and integrates with the Zitare backend for user favorites and lists management.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: NestJS 10
|
||||
- **Matrix**: matrix-bot-sdk
|
||||
- **Storage**: Built-in quotes + Zitare Backend API
|
||||
- **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-zitare-bot/
|
||||
├── src/
|
||||
│ ├── main.ts # Application entry point (port 3317)
|
||||
│ ├── app.module.ts # Root module
|
||||
│ ├── health.controller.ts # Health check endpoint
|
||||
│ ├── config/
|
||||
│ │ └── configuration.ts # Configuration, help text, quotes data
|
||||
│ ├── bot/
|
||||
│ │ ├── bot.module.ts
|
||||
│ │ └── matrix.service.ts # Matrix client & command handlers
|
||||
│ ├── quotes/
|
||||
│ │ ├── quotes.module.ts
|
||||
│ │ ├── quotes.service.ts # Local quotes management
|
||||
│ │ └── zitare.service.ts # Zitare Backend API client
|
||||
│ └── session/
|
||||
│ ├── session.module.ts
|
||||
│ └── session.service.ts # User session & auth management
|
||||
├── Dockerfile
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Bot Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `!help` | Show help message |
|
||||
| `!zitat` | Random quote |
|
||||
| `!heute` | Quote of the day |
|
||||
| `!suche [text]` | Search quotes |
|
||||
| `!kategorie [name]` | Quotes by category |
|
||||
| `!kategorien` | Show all categories |
|
||||
| `!login email pass` | Login to Zitare |
|
||||
| `!logout` | Logout |
|
||||
| `!favorit` | Save last quote to favorites |
|
||||
| `!favoriten` | Show favorites |
|
||||
| `!listen` | Show lists |
|
||||
| `!liste [name]` | Create new list |
|
||||
| `!addliste [nr]` | Add last quote to list |
|
||||
| `!status` | Bot status |
|
||||
|
||||
## Natural Language Keywords
|
||||
|
||||
The bot responds to natural language (German + English):
|
||||
- "zitat", "inspiration" -> Random quote
|
||||
- "heute", "tageszitat" -> Daily quote
|
||||
- "motiviere mich" -> Motivation quote
|
||||
- "guten morgen" -> Motivation quote
|
||||
- "kategorien" -> Show categories
|
||||
- "hilfe", "help" -> Help message
|
||||
|
||||
## Voice Notes
|
||||
|
||||
Voice notes are transcribed via mana-stt service and parsed as commands:
|
||||
- Say category names (e.g., "Motivation", "Liebe") for themed quotes
|
||||
- Say search terms to find matching quotes
|
||||
- Use natural language commands
|
||||
|
||||
## Quote Categories
|
||||
|
||||
- `motivation` - Motivationszitate
|
||||
- `weisheit` - Weisheiten
|
||||
- `liebe` - Liebeszitate
|
||||
- `leben` - Lebenszitate
|
||||
- `erfolg` - Erfolgszitate
|
||||
- `glueck` - Gluckszitate
|
||||
- `freundschaft` - Freundschaft
|
||||
- `mut` - Mutzitate
|
||||
- `hoffnung` - Hoffnungszitate
|
||||
- `natur` - Naturzitate
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```env
|
||||
# Server
|
||||
PORT=3317
|
||||
|
||||
# Matrix
|
||||
MATRIX_HOMESERVER_URL=http://localhost:8008
|
||||
MATRIX_ACCESS_TOKEN=syt_xxx
|
||||
MATRIX_ALLOWED_ROOMS=#zitare:matrix.mana.how
|
||||
MATRIX_STORAGE_PATH=./data/bot-storage.json
|
||||
|
||||
# Zitare Backend (for favorites/lists)
|
||||
ZITARE_BACKEND_URL=http://localhost:3007
|
||||
ZITARE_API_PREFIX=/api/v1
|
||||
|
||||
# Mana Core Auth
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
|
||||
# Speech-to-Text
|
||||
STT_URL=http://localhost:3020
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
# Build locally
|
||||
docker build -f services/matrix-zitare-bot/Dockerfile -t matrix-zitare-bot services/matrix-zitare-bot
|
||||
|
||||
# Run
|
||||
docker run -p 3317:3317 \
|
||||
-e MATRIX_HOMESERVER_URL=http://synapse:8008 \
|
||||
-e MATRIX_ACCESS_TOKEN=syt_xxx \
|
||||
-e ZITARE_BACKEND_URL=http://zitare-backend:3007 \
|
||||
-e MANA_CORE_AUTH_URL=http://mana-core-auth:3001 \
|
||||
-v matrix-zitare-bot-data:/app/data \
|
||||
matrix-zitare-bot
|
||||
```
|
||||
|
||||
## Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3317/health
|
||||
```
|
||||
|
||||
## Getting a Matrix Access Token
|
||||
|
||||
```bash
|
||||
# Login to get access token
|
||||
curl -X POST "https://matrix.mana.how/_matrix/client/v3/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "m.login.password",
|
||||
"user": "zitare-bot",
|
||||
"password": "your-password"
|
||||
}'
|
||||
|
||||
# Response contains: {"access_token": "syt_xxx", ...}
|
||||
```
|
||||
|
||||
## Zitare Backend API Endpoints Used
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/health` | GET | Health check |
|
||||
| `/api/v1/favorites` | GET | Get user favorites |
|
||||
| `/api/v1/favorites` | POST | Add favorite |
|
||||
| `/api/v1/favorites/:id` | DELETE | Remove favorite |
|
||||
| `/api/v1/lists` | GET | Get user lists |
|
||||
| `/api/v1/lists` | POST | Create list |
|
||||
| `/api/v1/lists/:id/quotes` | POST | Add quote to list |
|
||||
|
||||
## GDPR Compliance
|
||||
|
||||
- Built-in quotes stored locally (no external API)
|
||||
- User favorites/lists stored in Zitare Backend database
|
||||
- All data under user control
|
||||
- No third-party tracking
|
||||
41
services/matrix-zitare-bot/Dockerfile
Normal file
41
services/matrix-zitare-bot/Dockerfile
Normal file
|
|
@ -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 3317
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3317/health || exit 1
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "dist/main.js"]
|
||||
8
services/matrix-zitare-bot/nest-cli.json
Normal file
8
services/matrix-zitare-bot/nest-cli.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
39
services/matrix-zitare-bot/package.json
Normal file
39
services/matrix-zitare-bot/package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "@manacore/matrix-zitare-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "Matrix bot for daily inspiration quotes",
|
||||
"private": true,
|
||||
"pnpm": {
|
||||
"neverBuiltDependencies": [
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs"
|
||||
],
|
||||
"overrides": {
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "npm:empty-npm-package@1.0.0"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"@matrix-org/matrix-sdk-crypto-nodejs": "npm:empty-npm-package@1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "rm -rf dist || true",
|
||||
"build": "nest build",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/platform-express": "^10.4.15",
|
||||
"matrix-bot-sdk": "^0.7.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.9",
|
||||
"@types/node": "^22.10.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
17
services/matrix-zitare-bot/src/app.module.ts
Normal file
17
services/matrix-zitare-bot/src/app.module.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import configuration from './config/configuration';
|
||||
import { BotModule } from './bot/bot.module';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
}),
|
||||
BotModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class AppModule {}
|
||||
12
services/matrix-zitare-bot/src/bot/bot.module.ts
Normal file
12
services/matrix-zitare-bot/src/bot/bot.module.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { QuotesModule } from '../quotes/quotes.module';
|
||||
import { SessionModule } from '../session/session.module';
|
||||
import { TranscriptionModule } from '../transcription/transcription.module';
|
||||
|
||||
@Module({
|
||||
imports: [QuotesModule, SessionModule, TranscriptionModule],
|
||||
providers: [MatrixService],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
export class BotModule {}
|
||||
714
services/matrix-zitare-bot/src/bot/matrix.service.ts
Normal file
714
services/matrix-zitare-bot/src/bot/matrix.service.ts
Normal file
|
|
@ -0,0 +1,714 @@
|
|||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
MatrixClient,
|
||||
SimpleFsStorageProvider,
|
||||
RichConsoleLogger,
|
||||
LogService,
|
||||
LogLevel,
|
||||
} from 'matrix-bot-sdk';
|
||||
import { QuotesService } from '../quotes/quotes.service';
|
||||
import { ZitareService } from '../quotes/zitare.service';
|
||||
import { SessionService } from '../session/session.service';
|
||||
import { TranscriptionService } from '../transcription/transcription.service';
|
||||
import { HELP_MESSAGE, Category } from '../config/configuration';
|
||||
|
||||
// Natural language keywords that trigger commands
|
||||
const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [
|
||||
{ keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' },
|
||||
{ keywords: ['zitat', 'quote', 'inspiration', 'inspiriere'], command: 'zitat' },
|
||||
{ keywords: ['heute', 'today', 'tages', 'tageszitat'], command: 'heute' },
|
||||
{ keywords: ['motiviere', 'motivation', 'motivier mich'], command: 'motivation' },
|
||||
{ keywords: ['guten morgen', 'morgen', 'good morning'], command: 'morgen' },
|
||||
{ keywords: ['kategorien', 'categories', 'themen'], command: 'kategorien' },
|
||||
{ keywords: ['favoriten', 'favorites', 'meine favoriten'], command: 'favoriten' },
|
||||
{ keywords: ['listen', 'lists', 'meine listen'], command: 'listen' },
|
||||
{ keywords: ['status', 'info'], command: 'status' },
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class MatrixService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(MatrixService.name);
|
||||
private client!: MatrixClient;
|
||||
private readonly allowedRooms: string[];
|
||||
private botUserId: string = '';
|
||||
|
||||
// Track last shown quote per user for favorites
|
||||
private lastQuotes: Map<string, string> = new Map();
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private quotesService: QuotesService,
|
||||
private zitareService: ZitareService,
|
||||
private sessionService: SessionService,
|
||||
private transcriptionService: TranscriptionService
|
||||
) {
|
||||
this.allowedRooms = this.configService.get<string[]>('matrix.allowedRooms') || [];
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
const homeserverUrl = this.configService.get<string>('matrix.homeserverUrl');
|
||||
const accessToken = this.configService.get<string>('matrix.accessToken');
|
||||
const storagePath = this.configService.get<string>('matrix.storagePath');
|
||||
|
||||
if (!accessToken) {
|
||||
this.logger.error('MATRIX_ACCESS_TOKEN is required');
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup logging
|
||||
LogService.setLogger(new RichConsoleLogger());
|
||||
LogService.setLevel(LogLevel.INFO);
|
||||
|
||||
// Storage for sync token persistence
|
||||
const storage = new SimpleFsStorageProvider(storagePath || './data/bot-storage.json');
|
||||
|
||||
// Create Matrix client
|
||||
this.client = new MatrixClient(homeserverUrl!, accessToken, storage);
|
||||
|
||||
// Auto-join rooms when invited
|
||||
this.client.on('room.invite', async (roomId: string) => {
|
||||
this.logger.log(`Invited to room ${roomId}, joining...`);
|
||||
await this.client.joinRoom(roomId);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.sendBotIntroduction(roomId);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send introduction to ${roomId}:`, error);
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Get bot's user ID
|
||||
this.botUserId = await this.client.getUserId();
|
||||
this.logger.log(`Bot user ID: ${this.botUserId}`);
|
||||
|
||||
// Setup message handler
|
||||
this.client.on('room.message', this.handleRoomMessage.bind(this));
|
||||
|
||||
// Start the client
|
||||
await this.client.start();
|
||||
this.logger.log('Matrix Zitare Bot started successfully');
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.client) {
|
||||
await this.client.stop();
|
||||
this.logger.log('Matrix bot stopped');
|
||||
}
|
||||
}
|
||||
|
||||
private async sendBotIntroduction(roomId: string) {
|
||||
const dailyQuote = this.quotesService.getDailyQuote();
|
||||
|
||||
const introText = `**Zitare Bot - Tagliche Inspiration**
|
||||
|
||||
Ich bringe dir jeden Tag neue Inspiration!
|
||||
|
||||
**Zitat des Tages:**
|
||||
${this.quotesService.formatQuote(dailyQuote)}
|
||||
|
||||
Sag "hilfe" fur alle Befehle!`;
|
||||
|
||||
await this.sendMessage(roomId, introText);
|
||||
}
|
||||
|
||||
private isRoomAllowed(roomId: string): boolean {
|
||||
if (this.allowedRooms.length === 0) return true;
|
||||
return this.allowedRooms.some((allowed) => roomId === allowed || roomId.includes(allowed));
|
||||
}
|
||||
|
||||
private async handleRoomMessage(roomId: string, event: any) {
|
||||
// Ignore messages from self
|
||||
if (event.sender === this.botUserId) return;
|
||||
|
||||
// Check if room is allowed
|
||||
if (!this.isRoomAllowed(roomId)) {
|
||||
this.logger.debug(`Ignoring message from non-allowed room: ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = event.content as { msgtype?: string; body?: string; url?: string };
|
||||
|
||||
// Handle audio/voice messages
|
||||
if (content.msgtype === 'm.audio') {
|
||||
await this.handleAudioMessage(roomId, event.sender, content);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle text messages
|
||||
if (content.msgtype !== 'm.text') return;
|
||||
|
||||
const body = content.body;
|
||||
if (!body) return;
|
||||
|
||||
this.logger.log(`Message from ${event.sender} in ${roomId}: ${body.substring(0, 50)}...`);
|
||||
|
||||
// Handle commands with ! prefix
|
||||
if (body.startsWith('!')) {
|
||||
await this.handleCommand(roomId, event.sender, body);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for natural language keywords
|
||||
const keywordCommand = this.detectKeywordCommand(body);
|
||||
if (keywordCommand) {
|
||||
await this.handleCommand(roomId, event.sender, `!${keywordCommand}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't respond to random messages
|
||||
}
|
||||
|
||||
private detectKeywordCommand(message: string): string | null {
|
||||
const lowerMessage = message.toLowerCase().trim();
|
||||
|
||||
// Only match if the message is short
|
||||
if (lowerMessage.length > 50) return null;
|
||||
|
||||
for (const { keywords, command } of KEYWORD_COMMANDS) {
|
||||
for (const keyword of keywords) {
|
||||
if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) {
|
||||
this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`);
|
||||
return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async handleCommand(roomId: string, sender: string, body: string) {
|
||||
const [command, ...args] = body.slice(1).split(' ');
|
||||
const argString = args.join(' ');
|
||||
|
||||
switch (command.toLowerCase()) {
|
||||
case 'help':
|
||||
case 'hilfe':
|
||||
case 'start':
|
||||
await this.sendHelp(roomId);
|
||||
break;
|
||||
|
||||
case 'zitat':
|
||||
case 'quote':
|
||||
await this.handleRandomQuote(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'heute':
|
||||
case 'today':
|
||||
await this.handleDailyQuote(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'suche':
|
||||
case 'search':
|
||||
await this.handleSearch(roomId, sender, argString);
|
||||
break;
|
||||
|
||||
case 'kategorie':
|
||||
case 'category':
|
||||
await this.handleCategory(roomId, sender, argString);
|
||||
break;
|
||||
|
||||
case 'kategorien':
|
||||
case 'categories':
|
||||
await this.handleCategories(roomId);
|
||||
break;
|
||||
|
||||
case 'motivation':
|
||||
case 'morgen':
|
||||
await this.handleCategoryQuote(roomId, sender, 'motivation');
|
||||
break;
|
||||
|
||||
case 'login':
|
||||
await this.handleLogin(roomId, sender, args);
|
||||
break;
|
||||
|
||||
case 'logout':
|
||||
this.sessionService.logout(sender);
|
||||
await this.sendMessage(roomId, 'Du wurdest abgemeldet.');
|
||||
break;
|
||||
|
||||
case 'favorit':
|
||||
case 'fav':
|
||||
await this.handleAddFavorite(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'favoriten':
|
||||
case 'favorites':
|
||||
await this.handleFavorites(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'listen':
|
||||
case 'lists':
|
||||
await this.handleLists(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'liste':
|
||||
case 'list':
|
||||
await this.handleCreateList(roomId, sender, argString);
|
||||
break;
|
||||
|
||||
case 'addliste':
|
||||
case 'addlist':
|
||||
await this.handleAddToList(roomId, sender, args);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
await this.handleStatus(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'pin':
|
||||
await this.pinHelpMessage(roomId);
|
||||
break;
|
||||
|
||||
default:
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Unbekannter Befehl: !${command}\n\nSag "hilfe" fur alle Befehle.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAudioMessage(
|
||||
roomId: string,
|
||||
sender: string,
|
||||
content: { url?: string; body?: string }
|
||||
) {
|
||||
if (!content.url) {
|
||||
this.logger.warn('Audio message without URL');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Processing voice message from ${sender}`);
|
||||
|
||||
try {
|
||||
// Download audio from Matrix
|
||||
const httpUrl = this.client.mxcToHttp(content.url);
|
||||
const response = await fetch(httpUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download audio: ${response.status}`);
|
||||
}
|
||||
|
||||
const audioBuffer = Buffer.from(await response.arrayBuffer());
|
||||
|
||||
// Transcribe
|
||||
await this.sendMessage(roomId, '🎤 Transkribiere Sprachnotiz...');
|
||||
const transcription = await this.transcriptionService.transcribe(audioBuffer);
|
||||
|
||||
if (!transcription || transcription.trim().length === 0) {
|
||||
await this.sendMessage(roomId, 'Konnte keine Sprache erkennen.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Transcription: ${transcription}`);
|
||||
await this.sendMessage(roomId, `📝 "${transcription}"`);
|
||||
|
||||
// Check for commands in transcription
|
||||
const cleanText = transcription.trim();
|
||||
|
||||
// Check for keyword commands in the transcription
|
||||
const keywordCommand = this.detectKeywordCommand(cleanText);
|
||||
if (keywordCommand) {
|
||||
await this.handleCommand(roomId, sender, `!${keywordCommand}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for category names
|
||||
const category = this.quotesService.getCategoryByName(cleanText);
|
||||
if (category) {
|
||||
await this.handleCategoryQuote(roomId, sender, category);
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for the transcribed text
|
||||
const results = this.quotesService.searchQuotes(cleanText);
|
||||
if (results.length > 0) {
|
||||
const quote = results[0];
|
||||
this.lastQuotes.set(sender, quote.id);
|
||||
await this.sendMessage(roomId, `**Gefunden:**\n\n${this.quotesService.formatQuote(quote)}`);
|
||||
} else {
|
||||
// Default to a random quote
|
||||
await this.handleRandomQuote(roomId, sender);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to process audio message:', error);
|
||||
await this.sendMessage(roomId, 'Fehler bei der Verarbeitung der Sprachnotiz.');
|
||||
}
|
||||
}
|
||||
|
||||
private async sendHelp(roomId: string) {
|
||||
await this.sendMessage(roomId, HELP_MESSAGE);
|
||||
}
|
||||
|
||||
private async handleRandomQuote(roomId: string, sender: string) {
|
||||
const quote = this.quotesService.getRandomQuote();
|
||||
this.lastQuotes.set(sender, quote.id);
|
||||
await this.sendMessage(roomId, this.quotesService.formatQuote(quote));
|
||||
}
|
||||
|
||||
private async handleDailyQuote(roomId: string, sender: string) {
|
||||
const quote = this.quotesService.getDailyQuote();
|
||||
this.lastQuotes.set(sender, quote.id);
|
||||
|
||||
const dateStr = new Date().toLocaleDateString('de-DE', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
});
|
||||
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`**Zitat des Tages - ${dateStr}**\n\n${this.quotesService.formatQuote(quote)}`
|
||||
);
|
||||
}
|
||||
|
||||
private async handleSearch(roomId: string, sender: string, searchText: string) {
|
||||
if (!searchText.trim()) {
|
||||
await this.sendMessage(roomId, '**Verwendung:** `!suche [text]`\n\nBeispiel: `!suche Gluck`');
|
||||
return;
|
||||
}
|
||||
|
||||
const results = this.quotesService.searchQuotes(searchText);
|
||||
|
||||
if (results.length === 0) {
|
||||
await this.sendMessage(roomId, `Keine Zitate gefunden fur: "${searchText}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
let text = `**Suchergebnisse fur "${searchText}" (${results.length}):**\n\n`;
|
||||
|
||||
const maxResults = Math.min(results.length, 5);
|
||||
for (let i = 0; i < maxResults; i++) {
|
||||
const quote = results[i];
|
||||
text += `**${i + 1}.** "${quote.text.substring(0, 80)}${quote.text.length > 80 ? '...' : ''}"\n— *${quote.author}*\n\n`;
|
||||
}
|
||||
|
||||
if (results.length > 5) {
|
||||
text += `_...und ${results.length - 5} weitere_`;
|
||||
}
|
||||
|
||||
// Store first result for favorites
|
||||
if (results.length > 0) {
|
||||
this.lastQuotes.set(sender, results[0].id);
|
||||
}
|
||||
|
||||
await this.sendMessage(roomId, text);
|
||||
}
|
||||
|
||||
private async handleCategory(roomId: string, sender: string, categoryName: string) {
|
||||
if (!categoryName.trim()) {
|
||||
await this.handleCategories(roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
const category = this.quotesService.getCategoryByName(categoryName);
|
||||
if (!category) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Kategorie "${categoryName}" nicht gefunden.\n\nNutze \`!kategorien\` fur alle Kategorien.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.handleCategoryQuote(roomId, sender, category);
|
||||
}
|
||||
|
||||
private async handleCategoryQuote(roomId: string, sender: string, category: Category) {
|
||||
const quote = this.quotesService.getRandomQuoteByCategory(category);
|
||||
if (!quote) {
|
||||
await this.sendMessage(roomId, `Keine Zitate in Kategorie "${category}" gefunden.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastQuotes.set(sender, quote.id);
|
||||
await this.sendMessage(roomId, this.quotesService.formatQuote(quote));
|
||||
}
|
||||
|
||||
private async handleCategories(roomId: string) {
|
||||
const categories = this.quotesService.getAllCategories();
|
||||
|
||||
let text = `**Verfugbare Kategorien:**\n\n`;
|
||||
for (const { category, label, count } of categories) {
|
||||
text += `- **${label}** (\`!kategorie ${category}\`) - ${count} Zitate\n`;
|
||||
}
|
||||
|
||||
text += `\n**Gesamt:** ${this.quotesService.getTotalCount()} Zitate`;
|
||||
|
||||
await this.sendMessage(roomId, text);
|
||||
}
|
||||
|
||||
private async handleLogin(roomId: string, sender: string, args: string[]) {
|
||||
if (args.length < 2) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`**Verwendung:** \`!login email passwort\`\n\nBeispiel: \`!login nutzer@example.com meinpasswort\``
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const [email, password] = args;
|
||||
|
||||
await this.sendMessage(roomId, 'Anmeldung lauft...');
|
||||
|
||||
const result = await this.sessionService.login(sender, email, password);
|
||||
|
||||
if (result.success) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Erfolgreich angemeldet!\n\nDu kannst jetzt Favoriten speichern und Listen verwalten.`
|
||||
);
|
||||
} else {
|
||||
await this.sendMessage(roomId, `Anmeldung fehlgeschlagen: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAddFavorite(roomId: string, sender: string) {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
if (!token) {
|
||||
await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const lastQuoteId = this.lastQuotes.get(sender);
|
||||
if (!lastQuoteId) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Kein Zitat zum Speichern. Lass dir erst ein Zitat mit \`!zitat\` oder \`!heute\` anzeigen.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.zitareService.addFavorite(lastQuoteId, token);
|
||||
const quote = this.quotesService.getQuoteById(lastQuoteId);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Zu Favoriten hinzugefugt!\n\n"${quote?.text.substring(0, 50)}..."`
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleFavorites(roomId: string, sender: string) {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
if (!token) {
|
||||
await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const favorites = await this.zitareService.getFavorites(token);
|
||||
|
||||
if (favorites.length === 0) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Du hast noch keine Favoriten.\n\nNutze \`!favorit\` um das letzte angezeigte Zitat zu speichern.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let text = `**Deine Favoriten (${favorites.length}):**\n\n`;
|
||||
|
||||
for (let i = 0; i < Math.min(favorites.length, 10); i++) {
|
||||
const fav = favorites[i];
|
||||
const quote = this.quotesService.getQuoteById(fav.quoteId);
|
||||
if (quote) {
|
||||
text += `**${i + 1}.** "${quote.text.substring(0, 60)}${quote.text.length > 60 ? '...' : ''}"\n— *${quote.author}*\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (favorites.length > 10) {
|
||||
text += `_...und ${favorites.length - 10} weitere_`;
|
||||
}
|
||||
|
||||
await this.sendMessage(roomId, text);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLists(roomId: string, sender: string) {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
if (!token) {
|
||||
await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const lists = await this.zitareService.getLists(token);
|
||||
|
||||
if (lists.length === 0) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Du hast noch keine Listen.\n\nNutze \`!liste [name]\` um eine neue Liste zu erstellen.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let text = `**Deine Listen (${lists.length}):**\n\n`;
|
||||
|
||||
for (let i = 0; i < lists.length; i++) {
|
||||
const list = lists[i];
|
||||
text += `**${i + 1}. ${list.name}** - ${list.quoteIds.length} Zitate\n`;
|
||||
if (list.description) {
|
||||
text += ` _${list.description}_\n`;
|
||||
}
|
||||
}
|
||||
|
||||
await this.sendMessage(roomId, text);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleCreateList(roomId: string, sender: string, name: string) {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
if (!token) {
|
||||
await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name.trim()) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`**Verwendung:** \`!liste [name]\`\n\nBeispiel: \`!liste Meine Lieblingszitate\``
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const list = await this.zitareService.createList(name.trim(), undefined, token);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Liste "${list.name}" erstellt!\n\nNutze \`!addliste 1 [zitat-id]\` um Zitate hinzuzufugen.`
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAddToList(roomId: string, sender: string, args: string[]) {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
if (!token) {
|
||||
await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.length < 1) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`**Verwendung:** \`!addliste [listen-nr]\`\n\nFugt das letzte angezeigte Zitat zur Liste hinzu.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const listIndex = parseInt(args[0], 10);
|
||||
if (isNaN(listIndex) || listIndex < 1) {
|
||||
await this.sendMessage(roomId, `Ungultige Listennummer.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const lastQuoteId = this.lastQuotes.get(sender);
|
||||
if (!lastQuoteId) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Kein Zitat zum Hinzufugen. Lass dir erst ein Zitat anzeigen.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const lists = await this.zitareService.getLists(token);
|
||||
if (listIndex > lists.length) {
|
||||
await this.sendMessage(roomId, `Liste ${listIndex} existiert nicht.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const list = lists[listIndex - 1];
|
||||
await this.zitareService.addQuoteToList(list.id, lastQuoteId, token);
|
||||
|
||||
const quote = this.quotesService.getQuoteById(lastQuoteId);
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Zitat zu "${list.name}" hinzugefugt!\n\n"${quote?.text.substring(0, 50)}..."`
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleStatus(roomId: string, sender: string) {
|
||||
const backendHealthy = await this.zitareService.checkHealth();
|
||||
const isLoggedIn = this.sessionService.isLoggedIn(sender);
|
||||
const sessionCount = this.sessionService.getSessionCount();
|
||||
const totalQuotes = this.quotesService.getTotalCount();
|
||||
|
||||
const statusText = `**Zitare Bot Status**
|
||||
|
||||
**Backend:** ${backendHealthy ? 'Online' : 'Offline'}
|
||||
**Dein Status:** ${isLoggedIn ? 'Angemeldet' : 'Nicht angemeldet'}
|
||||
**Aktive Sessions:** ${sessionCount}
|
||||
**Verfugbare Zitate:** ${totalQuotes}
|
||||
|
||||
${!isLoggedIn ? 'Nutze `!login email passwort` um dich anzumelden.' : ''}`;
|
||||
|
||||
await this.sendMessage(roomId, statusText);
|
||||
}
|
||||
|
||||
private async pinHelpMessage(roomId: string) {
|
||||
try {
|
||||
const htmlBody = this.markdownToHtml(HELP_MESSAGE);
|
||||
|
||||
const eventId = await this.client.sendMessage(roomId, {
|
||||
msgtype: 'm.text',
|
||||
body: HELP_MESSAGE,
|
||||
format: 'org.matrix.custom.html',
|
||||
formatted_body: htmlBody,
|
||||
});
|
||||
|
||||
await this.client.sendStateEvent(roomId, 'm.room.pinned_events', '', {
|
||||
pinned: [eventId],
|
||||
});
|
||||
|
||||
this.logger.log(`Pinned help message in room ${roomId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to pin help message:`, error);
|
||||
await this.sendMessage(roomId, 'Fehler beim Pinnen der Hilfe.');
|
||||
}
|
||||
}
|
||||
|
||||
private async sendMessage(roomId: string, message: string) {
|
||||
const htmlBody = this.markdownToHtml(message);
|
||||
|
||||
await this.client.sendMessage(roomId, {
|
||||
msgtype: 'm.text',
|
||||
body: message,
|
||||
format: 'org.matrix.custom.html',
|
||||
formatted_body: htmlBody,
|
||||
});
|
||||
}
|
||||
|
||||
private markdownToHtml(markdown: string): string {
|
||||
return (
|
||||
markdown
|
||||
// Code blocks
|
||||
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
||||
// Inline code
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
// Bold
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
// Italic
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
// Underscore italic
|
||||
.replace(/_([^_]+)_/g, '<em>$1</em>')
|
||||
// Line breaks
|
||||
.replace(/\n/g, '<br/>')
|
||||
);
|
||||
}
|
||||
}
|
||||
294
services/matrix-zitare-bot/src/config/configuration.ts
Normal file
294
services/matrix-zitare-bot/src/config/configuration.ts
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
export default () => ({
|
||||
port: parseInt(process.env.PORT || '3317', 10),
|
||||
matrix: {
|
||||
homeserverUrl: process.env.MATRIX_HOMESERVER_URL || 'http://localhost:8008',
|
||||
accessToken: process.env.MATRIX_ACCESS_TOKEN || '',
|
||||
allowedRooms: (process.env.MATRIX_ALLOWED_ROOMS || '').split(',').filter(Boolean),
|
||||
storagePath: process.env.MATRIX_STORAGE_PATH || './data/bot-storage.json',
|
||||
},
|
||||
zitare: {
|
||||
backendUrl: process.env.ZITARE_BACKEND_URL || 'http://localhost:3007',
|
||||
apiPrefix: process.env.ZITARE_API_PREFIX || '/api/v1',
|
||||
},
|
||||
auth: {
|
||||
url: process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001',
|
||||
},
|
||||
stt: {
|
||||
url: process.env.STT_URL || 'http://localhost:3020',
|
||||
},
|
||||
});
|
||||
|
||||
export const HELP_MESSAGE = `**Zitare Bot - Tagliche Inspiration**
|
||||
|
||||
**Zitate:**
|
||||
- \`!zitat\` - Zufalliges Zitat
|
||||
- \`!heute\` - Zitat des Tages
|
||||
- \`!suche [text]\` - Zitate suchen
|
||||
- \`!kategorie [name]\` - Zitate nach Kategorie
|
||||
- \`!kategorien\` - Alle Kategorien
|
||||
|
||||
**Favoriten:** (Login erforderlich)
|
||||
- \`!login email passwort\` - Anmelden
|
||||
- \`!logout\` - Abmelden
|
||||
- \`!favorit\` - Letztes Zitat speichern
|
||||
- \`!favoriten\` - Alle Favoriten anzeigen
|
||||
|
||||
**Listen:** (Login erforderlich)
|
||||
- \`!listen\` - Alle Listen anzeigen
|
||||
- \`!liste [name]\` - Neue Liste erstellen
|
||||
- \`!addliste [nr] [zitat-nr]\` - Zitat zur Liste hinzufugen
|
||||
|
||||
**Sonstiges:**
|
||||
- \`!status\` - Bot-Status
|
||||
- \`!help\` - Diese Hilfe
|
||||
|
||||
**Sprachnotizen:**
|
||||
Sende eine Sprachnotiz mit Befehlen wie "Zitat", "Motivation" oder einem Suchbegriff.
|
||||
|
||||
**Naturliche Sprache:**
|
||||
- "zitat", "inspiration" -> Zufalliges Zitat
|
||||
- "motiviere mich" -> Motivation-Zitat
|
||||
- "guten morgen" -> Morgenzitat`;
|
||||
|
||||
// Quote categories
|
||||
export const CATEGORIES = [
|
||||
'motivation',
|
||||
'weisheit',
|
||||
'liebe',
|
||||
'leben',
|
||||
'erfolg',
|
||||
'glueck',
|
||||
'freundschaft',
|
||||
'mut',
|
||||
'hoffnung',
|
||||
'natur',
|
||||
] as const;
|
||||
|
||||
export type Category = (typeof CATEGORIES)[number];
|
||||
|
||||
// German inspirational quotes collection
|
||||
export interface Quote {
|
||||
id: string;
|
||||
text: string;
|
||||
author: string;
|
||||
category: Category;
|
||||
}
|
||||
|
||||
export const QUOTES: Quote[] = [
|
||||
// Motivation
|
||||
{
|
||||
id: 'mot-1',
|
||||
text: 'Der einzige Weg, grossartige Arbeit zu leisten, ist zu lieben, was man tut.',
|
||||
author: 'Steve Jobs',
|
||||
category: 'motivation',
|
||||
},
|
||||
{
|
||||
id: 'mot-2',
|
||||
text: 'Erfolg ist nicht endgultig, Misserfolg ist nicht fatal: Was zahlt, ist der Mut weiterzumachen.',
|
||||
author: 'Winston Churchill',
|
||||
category: 'motivation',
|
||||
},
|
||||
{
|
||||
id: 'mot-3',
|
||||
text: 'Die Zukunft gehort denen, die an die Schonheit ihrer Traume glauben.',
|
||||
author: 'Eleanor Roosevelt',
|
||||
category: 'motivation',
|
||||
},
|
||||
{
|
||||
id: 'mot-4',
|
||||
text: 'Es ist nie zu spat, das zu werden, was man hatte sein konnen.',
|
||||
author: 'George Eliot',
|
||||
category: 'motivation',
|
||||
},
|
||||
{
|
||||
id: 'mot-5',
|
||||
text: 'Gib jedem Tag die Chance, der schonste deines Lebens zu werden.',
|
||||
author: 'Mark Twain',
|
||||
category: 'motivation',
|
||||
},
|
||||
// Weisheit
|
||||
{
|
||||
id: 'weis-1',
|
||||
text: 'Der Weg ist das Ziel.',
|
||||
author: 'Konfuzius',
|
||||
category: 'weisheit',
|
||||
},
|
||||
{
|
||||
id: 'weis-2',
|
||||
text: 'Wer kampft, kann verlieren. Wer nicht kampft, hat schon verloren.',
|
||||
author: 'Bertolt Brecht',
|
||||
category: 'weisheit',
|
||||
},
|
||||
{
|
||||
id: 'weis-3',
|
||||
text: 'Man sieht nur mit dem Herzen gut. Das Wesentliche ist fur die Augen unsichtbar.',
|
||||
author: 'Antoine de Saint-Exupery',
|
||||
category: 'weisheit',
|
||||
},
|
||||
{
|
||||
id: 'weis-4',
|
||||
text: 'Nicht weil es schwer ist, wagen wir es nicht, sondern weil wir es nicht wagen, ist es schwer.',
|
||||
author: 'Seneca',
|
||||
category: 'weisheit',
|
||||
},
|
||||
{
|
||||
id: 'weis-5',
|
||||
text: 'Wissen ist Macht.',
|
||||
author: 'Francis Bacon',
|
||||
category: 'weisheit',
|
||||
},
|
||||
// Liebe
|
||||
{
|
||||
id: 'liebe-1',
|
||||
text: 'Wo Liebe ist, da ist auch Leben.',
|
||||
author: 'Mahatma Gandhi',
|
||||
category: 'liebe',
|
||||
},
|
||||
{
|
||||
id: 'liebe-2',
|
||||
text: 'Die Liebe allein versteht das Geheimnis, andere zu beschenken und dabei selbst reich zu werden.',
|
||||
author: 'Clemens Brentano',
|
||||
category: 'liebe',
|
||||
},
|
||||
{
|
||||
id: 'liebe-3',
|
||||
text: 'Es gibt nur ein Gluck in diesem Leben: zu lieben und geliebt zu werden.',
|
||||
author: 'George Sand',
|
||||
category: 'liebe',
|
||||
},
|
||||
// Leben
|
||||
{
|
||||
id: 'leben-1',
|
||||
text: 'Das Leben ist wie Fahrrad fahren. Um die Balance zu halten, musst du in Bewegung bleiben.',
|
||||
author: 'Albert Einstein',
|
||||
category: 'leben',
|
||||
},
|
||||
{
|
||||
id: 'leben-2',
|
||||
text: 'Leben ist das, was passiert, wahrend du damit beschaftigt bist, andere Plane zu machen.',
|
||||
author: 'John Lennon',
|
||||
category: 'leben',
|
||||
},
|
||||
{
|
||||
id: 'leben-3',
|
||||
text: 'Das Leben ist zu kurz fur spater.',
|
||||
author: 'Alexandra Reinwarth',
|
||||
category: 'leben',
|
||||
},
|
||||
{
|
||||
id: 'leben-4',
|
||||
text: 'Lebe jeden Tag, als ware es dein letzter.',
|
||||
author: 'Marcus Aurelius',
|
||||
category: 'leben',
|
||||
},
|
||||
// Erfolg
|
||||
{
|
||||
id: 'erfolg-1',
|
||||
text: 'Erfolg besteht darin, dass man genau die Fahigkeiten hat, die im Moment gefragt sind.',
|
||||
author: 'Henry Ford',
|
||||
category: 'erfolg',
|
||||
},
|
||||
{
|
||||
id: 'erfolg-2',
|
||||
text: 'Der Preis des Erfolges ist Hingabe, harte Arbeit und unablassiger Einsatz.',
|
||||
author: 'Frank Lloyd Wright',
|
||||
category: 'erfolg',
|
||||
},
|
||||
{
|
||||
id: 'erfolg-3',
|
||||
text: 'Ich habe nicht versagt. Ich habe nur 10.000 Wege gefunden, die nicht funktionieren.',
|
||||
author: 'Thomas Edison',
|
||||
category: 'erfolg',
|
||||
},
|
||||
// Glueck
|
||||
{
|
||||
id: 'glueck-1',
|
||||
text: 'Gluck ist das Einzige, das sich verdoppelt, wenn man es teilt.',
|
||||
author: 'Albert Schweitzer',
|
||||
category: 'glueck',
|
||||
},
|
||||
{
|
||||
id: 'glueck-2',
|
||||
text: 'Gluck ist kein Ziel, sondern ein Weg.',
|
||||
author: 'Buddha',
|
||||
category: 'glueck',
|
||||
},
|
||||
{
|
||||
id: 'glueck-3',
|
||||
text: 'Nicht die Glucklichen sind dankbar. Es sind die Dankbaren, die glucklich sind.',
|
||||
author: 'Francis Bacon',
|
||||
category: 'glueck',
|
||||
},
|
||||
// Freundschaft
|
||||
{
|
||||
id: 'freund-1',
|
||||
text: 'Ein wahrer Freund ist jemand, der die Melodie deines Herzens kennt und sie dir vorsingt, wenn du sie vergessen hast.',
|
||||
author: 'Albert Einstein',
|
||||
category: 'freundschaft',
|
||||
},
|
||||
{
|
||||
id: 'freund-2',
|
||||
text: 'Freundschaft ist eine Seele in zwei Korpern.',
|
||||
author: 'Aristoteles',
|
||||
category: 'freundschaft',
|
||||
},
|
||||
// Mut
|
||||
{
|
||||
id: 'mut-1',
|
||||
text: 'Mut steht am Anfang des Handelns, Gluck am Ende.',
|
||||
author: 'Demokrit',
|
||||
category: 'mut',
|
||||
},
|
||||
{
|
||||
id: 'mut-2',
|
||||
text: 'Wer wagt, gewinnt.',
|
||||
author: 'Deutsches Sprichwort',
|
||||
category: 'mut',
|
||||
},
|
||||
{
|
||||
id: 'mut-3',
|
||||
text: 'Der Mutige hat nicht weniger Angst, er handelt trotzdem.',
|
||||
author: 'Mark Twain',
|
||||
category: 'mut',
|
||||
},
|
||||
// Hoffnung
|
||||
{
|
||||
id: 'hoff-1',
|
||||
text: 'Hoffnung ist ein Vogel, der singt, wenn die Nacht noch dunkel ist.',
|
||||
author: 'Rabindranath Tagore',
|
||||
category: 'hoffnung',
|
||||
},
|
||||
{
|
||||
id: 'hoff-2',
|
||||
text: 'Nach jedem Sturm scheint auch wieder die Sonne.',
|
||||
author: 'Deutsches Sprichwort',
|
||||
category: 'hoffnung',
|
||||
},
|
||||
// Natur
|
||||
{
|
||||
id: 'natur-1',
|
||||
text: 'In der Natur ist nichts isoliert; alles hangt mit allem zusammen.',
|
||||
author: 'Johann Wolfgang von Goethe',
|
||||
category: 'natur',
|
||||
},
|
||||
{
|
||||
id: 'natur-2',
|
||||
text: 'Schau tief in die Natur, und dann wirst du alles besser verstehen.',
|
||||
author: 'Albert Einstein',
|
||||
category: 'natur',
|
||||
},
|
||||
];
|
||||
|
||||
// Category labels in German
|
||||
export const CATEGORY_LABELS: Record<Category, string> = {
|
||||
motivation: 'Motivation',
|
||||
weisheit: 'Weisheit',
|
||||
liebe: 'Liebe',
|
||||
leben: 'Leben',
|
||||
erfolg: 'Erfolg',
|
||||
glueck: 'Gluck',
|
||||
freundschaft: 'Freundschaft',
|
||||
mut: 'Mut',
|
||||
hoffnung: 'Hoffnung',
|
||||
natur: 'Natur',
|
||||
};
|
||||
13
services/matrix-zitare-bot/src/health.controller.ts
Normal file
13
services/matrix-zitare-bot/src/health.controller.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
@Get()
|
||||
check() {
|
||||
return {
|
||||
status: 'ok',
|
||||
service: 'matrix-zitare-bot',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
17
services/matrix-zitare-bot/src/main.ts
Normal file
17
services/matrix-zitare-bot/src/main.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const logger = new Logger('Bootstrap');
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
const port = process.env.PORT || 3317;
|
||||
await app.listen(port);
|
||||
|
||||
logger.log(`Matrix Zitare Bot running on port ${port}`);
|
||||
logger.log(`Health check: http://localhost:${port}/health`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
9
services/matrix-zitare-bot/src/quotes/quotes.module.ts
Normal file
9
services/matrix-zitare-bot/src/quotes/quotes.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { QuotesService } from './quotes.service';
|
||||
import { ZitareService } from './zitare.service';
|
||||
|
||||
@Module({
|
||||
providers: [QuotesService, ZitareService],
|
||||
exports: [QuotesService, ZitareService],
|
||||
})
|
||||
export class QuotesModule {}
|
||||
113
services/matrix-zitare-bot/src/quotes/quotes.service.ts
Normal file
113
services/matrix-zitare-bot/src/quotes/quotes.service.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { QUOTES, Quote, Category, CATEGORIES, CATEGORY_LABELS } from '../config/configuration';
|
||||
|
||||
@Injectable()
|
||||
export class QuotesService {
|
||||
private readonly logger = new Logger(QuotesService.name);
|
||||
private dailyQuoteCache: { date: string; quote: Quote } | null = null;
|
||||
|
||||
getRandomQuote(): Quote {
|
||||
const index = Math.floor(Math.random() * QUOTES.length);
|
||||
return QUOTES[index];
|
||||
}
|
||||
|
||||
getDailyQuote(): Quote {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Return cached daily quote if same day
|
||||
if (this.dailyQuoteCache && this.dailyQuoteCache.date === today) {
|
||||
return this.dailyQuoteCache.quote;
|
||||
}
|
||||
|
||||
// Generate deterministic quote based on date
|
||||
const dateHash = this.hashDate(today);
|
||||
const index = dateHash % QUOTES.length;
|
||||
const quote = QUOTES[index];
|
||||
|
||||
this.dailyQuoteCache = { date: today, quote };
|
||||
this.logger.log(`Daily quote for ${today}: "${quote.text.substring(0, 30)}..."`);
|
||||
|
||||
return quote;
|
||||
}
|
||||
|
||||
private hashDate(dateStr: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < dateStr.length; i++) {
|
||||
const char = dateStr.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return Math.abs(hash);
|
||||
}
|
||||
|
||||
getQuotesByCategory(category: Category): Quote[] {
|
||||
return QUOTES.filter((q) => q.category === category);
|
||||
}
|
||||
|
||||
getRandomQuoteByCategory(category: Category): Quote | null {
|
||||
const quotes = this.getQuotesByCategory(category);
|
||||
if (quotes.length === 0) return null;
|
||||
const index = Math.floor(Math.random() * quotes.length);
|
||||
return quotes[index];
|
||||
}
|
||||
|
||||
searchQuotes(searchText: string): Quote[] {
|
||||
const lowerSearch = searchText.toLowerCase();
|
||||
return QUOTES.filter(
|
||||
(q) =>
|
||||
q.text.toLowerCase().includes(lowerSearch) || q.author.toLowerCase().includes(lowerSearch)
|
||||
);
|
||||
}
|
||||
|
||||
getQuoteById(id: string): Quote | undefined {
|
||||
return QUOTES.find((q) => q.id === id);
|
||||
}
|
||||
|
||||
getQuoteByIndex(index: number): Quote | null {
|
||||
if (index < 1 || index > QUOTES.length) return null;
|
||||
return QUOTES[index - 1];
|
||||
}
|
||||
|
||||
getAllCategories(): { category: Category; label: string; count: number }[] {
|
||||
return CATEGORIES.map((category) => ({
|
||||
category,
|
||||
label: CATEGORY_LABELS[category],
|
||||
count: QUOTES.filter((q) => q.category === category).length,
|
||||
}));
|
||||
}
|
||||
|
||||
getCategoryByName(name: string): Category | null {
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
// Try exact match first
|
||||
if (CATEGORIES.includes(lowerName as Category)) {
|
||||
return lowerName as Category;
|
||||
}
|
||||
|
||||
// Try partial match
|
||||
for (const category of CATEGORIES) {
|
||||
if (
|
||||
category.startsWith(lowerName) ||
|
||||
CATEGORY_LABELS[category].toLowerCase().startsWith(lowerName)
|
||||
) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getTotalCount(): number {
|
||||
return QUOTES.length;
|
||||
}
|
||||
|
||||
formatQuote(quote: Quote): string {
|
||||
const categoryLabel = CATEGORY_LABELS[quote.category];
|
||||
return `"${quote.text}"\n\n— *${quote.author}*\n\n[${categoryLabel}]`;
|
||||
}
|
||||
|
||||
formatQuoteWithNumber(quote: Quote, number: number): string {
|
||||
const categoryLabel = CATEGORY_LABELS[quote.category];
|
||||
return `**#${number}**\n"${quote.text}"\n\n— *${quote.author}* [${categoryLabel}]`;
|
||||
}
|
||||
}
|
||||
177
services/matrix-zitare-bot/src/quotes/zitare.service.ts
Normal file
177
services/matrix-zitare-bot/src/quotes/zitare.service.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export interface Favorite {
|
||||
id: string;
|
||||
userId: string;
|
||||
quoteId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface UserList {
|
||||
id: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
quoteIds: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ZitareService {
|
||||
private readonly logger = new Logger(ZitareService.name);
|
||||
private readonly baseUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
const backendUrl =
|
||||
this.configService.get<string>('zitare.backendUrl') || 'http://localhost:3007';
|
||||
const apiPrefix = this.configService.get<string>('zitare.apiPrefix') || '/api/v1';
|
||||
this.baseUrl = `${backendUrl}${apiPrefix}`;
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl.replace('/api/v1', '')}/health`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Favorites
|
||||
|
||||
async getFavorites(token: string): Promise<Favorite[]> {
|
||||
const response = await fetch(`${this.baseUrl}/favorites`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get favorites: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.favorites || [];
|
||||
}
|
||||
|
||||
async addFavorite(quoteId: string, token: string): Promise<Favorite> {
|
||||
const response = await fetch(`${this.baseUrl}/favorites`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ quoteId }),
|
||||
});
|
||||
|
||||
if (response.status === 409) {
|
||||
throw new Error('Dieses Zitat ist bereits in deinen Favoriten');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to add favorite: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async removeFavorite(quoteId: string, token: string): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/favorites/${quoteId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to remove favorite: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Lists
|
||||
|
||||
async getLists(token: string): Promise<UserList[]> {
|
||||
const response = await fetch(`${this.baseUrl}/lists`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get lists: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.lists || [];
|
||||
}
|
||||
|
||||
async getList(listId: string, token: string): Promise<UserList> {
|
||||
const response = await fetch(`${this.baseUrl}/lists/${listId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get list: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createList(
|
||||
name: string,
|
||||
description: string | undefined,
|
||||
token: string
|
||||
): Promise<UserList> {
|
||||
const response = await fetch(`${this.baseUrl}/lists`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ name, description }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create list: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async deleteList(listId: string, token: string): Promise<void> {
|
||||
const response = await fetch(`${this.baseUrl}/lists/${listId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete list: ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
async addQuoteToList(listId: string, quoteId: string, token: string): Promise<UserList> {
|
||||
const response = await fetch(`${this.baseUrl}/lists/${listId}/quotes`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ quoteId }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to add quote to list: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async removeQuoteFromList(listId: string, quoteId: string, token: string): Promise<UserList> {
|
||||
const response = await fetch(`${this.baseUrl}/lists/${listId}/quotes/${quoteId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to remove quote from list: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
8
services/matrix-zitare-bot/src/session/session.module.ts
Normal file
8
services/matrix-zitare-bot/src/session/session.module.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SessionService } from './session.service';
|
||||
|
||||
@Module({
|
||||
providers: [SessionService],
|
||||
exports: [SessionService],
|
||||
})
|
||||
export class SessionModule {}
|
||||
113
services/matrix-zitare-bot/src/session/session.service.ts
Normal file
113
services/matrix-zitare-bot/src/session/session.service.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface UserSession {
|
||||
token: string;
|
||||
email: string;
|
||||
expiresAt: Date;
|
||||
lastQuoteId?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
private readonly logger = new Logger(SessionService.name);
|
||||
private sessions: Map<string, UserSession> = new Map();
|
||||
private authUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.authUrl = this.configService.get<string>('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;
|
||||
|
||||
// Check if token expired
|
||||
if (session.expiresAt < new Date()) {
|
||||
this.sessions.delete(matrixUserId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.token;
|
||||
}
|
||||
|
||||
isLoggedIn(matrixUserId: string): boolean {
|
||||
return this.getToken(matrixUserId) !== null;
|
||||
}
|
||||
|
||||
setLastQuoteId(matrixUserId: string, quoteId: string): void {
|
||||
const session = this.sessions.get(matrixUserId);
|
||||
if (session) {
|
||||
session.lastQuoteId = quoteId;
|
||||
}
|
||||
}
|
||||
|
||||
getLastQuoteId(matrixUserId: string): string | null {
|
||||
const session = this.sessions.get(matrixUserId);
|
||||
return session?.lastQuoteId || null;
|
||||
}
|
||||
|
||||
getSessionCount(): number {
|
||||
return this.sessions.size;
|
||||
}
|
||||
|
||||
getLoggedInCount(): number {
|
||||
const now = new Date();
|
||||
let count = 0;
|
||||
for (const session of this.sessions.values()) {
|
||||
if (session.expiresAt > now) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TranscriptionService } from './transcription.service';
|
||||
|
||||
@Module({
|
||||
providers: [TranscriptionService],
|
||||
exports: [TranscriptionService],
|
||||
})
|
||||
export class TranscriptionModule {}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class TranscriptionService {
|
||||
private readonly logger = new Logger(TranscriptionService.name);
|
||||
private readonly sttUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.sttUrl = this.configService.get<string>('stt.url') || 'http://localhost:3020';
|
||||
}
|
||||
|
||||
async transcribe(audioBuffer: Buffer, language: string = 'de'): Promise<string> {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
const blob = new Blob([new Uint8Array(audioBuffer)], { type: 'audio/ogg' });
|
||||
formData.append('file', blob, 'audio.ogg');
|
||||
formData.append('language', language);
|
||||
|
||||
const response = await fetch(`${this.sttUrl}/transcribe`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`STT service error: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as { text: string };
|
||||
this.logger.log(`Transcription result: ${result.text}`);
|
||||
return result.text;
|
||||
} catch (error) {
|
||||
this.logger.error('Transcription failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
services/matrix-zitare-bot/tsconfig.json
Normal file
23
services/matrix-zitare-bot/tsconfig.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"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": true,
|
||||
"strictBindCallApply": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue