mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 02:39:41 +02:00
feat(matrix-contacts-bot): add Matrix bot for contact management
- List, search, and view contact details - Create, edit, and delete contacts - Toggle favorites and archive status - Number-based reference system for easy commands - German and English command aliases - Login/logout via mana-core-auth Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8950692cfd
commit
64535373ac
18 changed files with 1572 additions and 0 deletions
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
|
|
@ -5800,6 +5800,40 @@ importers:
|
|||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
|
||||
services/matrix-contacts-bot:
|
||||
dependencies:
|
||||
'@nestjs/common':
|
||||
specifier: ^10.4.15
|
||||
version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/config':
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
|
||||
'@nestjs/core':
|
||||
specifier: ^10.4.15
|
||||
version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/platform-express':
|
||||
specifier: ^10.4.15
|
||||
version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
|
||||
matrix-bot-sdk:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
rxjs:
|
||||
specifier: ^7.8.1
|
||||
version: 7.8.2
|
||||
devDependencies:
|
||||
'@nestjs/cli':
|
||||
specifier: ^10.4.9
|
||||
version: 10.4.9
|
||||
'@types/node':
|
||||
specifier: ^22.10.2
|
||||
version: 22.19.1
|
||||
typescript:
|
||||
specifier: ^5.7.2
|
||||
version: 5.9.3
|
||||
|
||||
services/matrix-mana-bot:
|
||||
dependencies:
|
||||
'@manacore/bot-services':
|
||||
|
|
|
|||
15
services/matrix-contacts-bot/.env.example
Normal file
15
services/matrix-contacts-bot/.env.example
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Server
|
||||
PORT=3320
|
||||
|
||||
# Matrix
|
||||
MATRIX_HOMESERVER_URL=http://localhost:8008
|
||||
MATRIX_ACCESS_TOKEN=syt_xxx
|
||||
MATRIX_ALLOWED_ROOMS=#contacts:matrix.mana.how
|
||||
MATRIX_STORAGE_PATH=./data/bot-storage.json
|
||||
|
||||
# Contacts Backend
|
||||
CONTACTS_BACKEND_URL=http://localhost:3015
|
||||
CONTACTS_API_PREFIX=/api/v1
|
||||
|
||||
# Mana Core Auth
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
29
services/matrix-contacts-bot/.gitignore
vendored
Normal file
29
services/matrix-contacts-bot/.gitignore
vendored
Normal file
|
|
@ -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
|
||||
186
services/matrix-contacts-bot/CLAUDE.md
Normal file
186
services/matrix-contacts-bot/CLAUDE.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# Matrix Contacts Bot - Claude Code Guidelines
|
||||
|
||||
## Overview
|
||||
|
||||
Matrix Contacts Bot provides contact management via Matrix chat. It integrates with the Contacts backend for full CRUD operations, search, favorites, and archiving.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: NestJS 10
|
||||
- **Matrix**: matrix-bot-sdk
|
||||
- **Backend**: Contacts API (port 3015)
|
||||
- **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-contacts-bot/
|
||||
├── src/
|
||||
│ ├── main.ts # Application entry point (port 3320)
|
||||
│ ├── 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
|
||||
│ ├── contacts/
|
||||
│ │ ├── contacts.module.ts
|
||||
│ │ └── contacts.service.ts # Contacts 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 |
|
||||
| `!kontakte` | contacts, liste | List all contacts |
|
||||
| `!suche [text]` | search | Search contacts |
|
||||
| `!favoriten` | favorites | Show favorites |
|
||||
| `!kontakt [nr]` | contact, details | Show contact details |
|
||||
| `!neu Vorname [Nachname]` | new, add | Create new contact |
|
||||
| `!edit [nr] [feld] [wert]` | bearbeiten | Edit contact field |
|
||||
| `!loeschen [nr]` | delete | Delete contact |
|
||||
| `!fav [nr]` | favorit | Toggle favorite |
|
||||
| `!archiv [nr]` | archive | Toggle archive |
|
||||
| `!login email pass` | - | Login |
|
||||
| `!logout` | - | Logout |
|
||||
| `!status` | - | Bot status |
|
||||
|
||||
## Editable Fields
|
||||
|
||||
| Field | Aliases | Description |
|
||||
|-------|---------|-------------|
|
||||
| `email` | - | Email address |
|
||||
| `phone` | telefon | Phone number |
|
||||
| `mobile` | mobil, handy | Mobile number |
|
||||
| `company` | firma | Company name |
|
||||
| `job` | jobtitle, beruf | Job title |
|
||||
| `website` | web | Website URL |
|
||||
| `street` | strasse | Street address |
|
||||
| `city` | stadt | City |
|
||||
| `zip` | plz | Postal code |
|
||||
| `country` | land | Country |
|
||||
| `notes` | notizen | Notes |
|
||||
| `birthday` | geburtstag | Birthday (YYYY-MM-DD) |
|
||||
|
||||
## Example Usage
|
||||
|
||||
```
|
||||
# Create a contact
|
||||
!neu Max Mustermann
|
||||
|
||||
# Add email
|
||||
!edit 1 email max@example.com
|
||||
|
||||
# Add phone
|
||||
!edit 1 phone +49 123 456789
|
||||
|
||||
# Mark as favorite
|
||||
!fav 1
|
||||
|
||||
# Search
|
||||
!suche Muster
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```env
|
||||
# Server
|
||||
PORT=3320
|
||||
|
||||
# Matrix
|
||||
MATRIX_HOMESERVER_URL=http://localhost:8008
|
||||
MATRIX_ACCESS_TOKEN=syt_xxx
|
||||
MATRIX_ALLOWED_ROOMS=#contacts:matrix.mana.how
|
||||
MATRIX_STORAGE_PATH=./data/bot-storage.json
|
||||
|
||||
# Contacts Backend
|
||||
CONTACTS_BACKEND_URL=http://localhost:3015
|
||||
CONTACTS_API_PREFIX=/api/v1
|
||||
|
||||
# Mana Core Auth
|
||||
MANA_CORE_AUTH_URL=http://localhost:3001
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
# Build locally
|
||||
docker build -f services/matrix-contacts-bot/Dockerfile -t matrix-contacts-bot services/matrix-contacts-bot
|
||||
|
||||
# Run
|
||||
docker run -p 3320:3320 \
|
||||
-e MATRIX_HOMESERVER_URL=http://synapse:8008 \
|
||||
-e MATRIX_ACCESS_TOKEN=syt_xxx \
|
||||
-e CONTACTS_BACKEND_URL=http://contacts-backend:3015 \
|
||||
-e MANA_CORE_AUTH_URL=http://mana-core-auth:3001 \
|
||||
-v matrix-contacts-bot-data:/app/data \
|
||||
matrix-contacts-bot
|
||||
```
|
||||
|
||||
## Health Check
|
||||
|
||||
```bash
|
||||
curl http://localhost:3320/health
|
||||
```
|
||||
|
||||
## Getting a Matrix Access Token
|
||||
|
||||
```bash
|
||||
# Create bot user first, then login
|
||||
curl -X POST "https://matrix.mana.how/_matrix/client/v3/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "m.login.password",
|
||||
"user": "contacts-bot",
|
||||
"password": "your-password"
|
||||
}'
|
||||
|
||||
# Response contains: {"access_token": "syt_xxx", ...}
|
||||
```
|
||||
|
||||
## Contacts Backend API Endpoints Used
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/health` | GET | Health check |
|
||||
| `/api/v1/contacts` | GET | List contacts with filters |
|
||||
| `/api/v1/contacts` | POST | Create contact |
|
||||
| `/api/v1/contacts/:id` | GET | Get contact details |
|
||||
| `/api/v1/contacts/:id` | PATCH | Update contact |
|
||||
| `/api/v1/contacts/:id` | DELETE | Delete contact |
|
||||
| `/api/v1/contacts/:id/favorite` | POST | Toggle favorite |
|
||||
| `/api/v1/contacts/:id/archive` | POST | Toggle archive |
|
||||
|
||||
## Number-Based Reference System
|
||||
|
||||
The bot uses a number-based reference system for ease of use:
|
||||
1. User runs `!kontakte` or `!suche` to get a list
|
||||
2. Bot stores the list internally for the user
|
||||
3. User can reference contacts by their list number
|
||||
4. Numbers are valid until the user runs a new list command
|
||||
|
||||
This allows simple commands like:
|
||||
- `!kontakt 3` - Show details for contact #3 in the list
|
||||
- `!edit 1 email new@email.com` - Edit contact #1
|
||||
- `!fav 2` - Toggle favorite for contact #2
|
||||
41
services/matrix-contacts-bot/Dockerfile
Normal file
41
services/matrix-contacts-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 3320
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3320/health || exit 1
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "dist/main.js"]
|
||||
8
services/matrix-contacts-bot/nest-cli.json
Normal file
8
services/matrix-contacts-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-contacts-bot/package.json
Normal file
39
services/matrix-contacts-bot/package.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "@manacore/matrix-contacts-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "Matrix bot for contact management via Contacts backend",
|
||||
"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-contacts-bot/src/app.module.ts
Normal file
17
services/matrix-contacts-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 {}
|
||||
11
services/matrix-contacts-bot/src/bot/bot.module.ts
Normal file
11
services/matrix-contacts-bot/src/bot/bot.module.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { MatrixService } from './matrix.service';
|
||||
import { ContactsModule } from '../contacts/contacts.module';
|
||||
import { SessionModule } from '../session/session.module';
|
||||
|
||||
@Module({
|
||||
imports: [ContactsModule, SessionModule],
|
||||
providers: [MatrixService],
|
||||
exports: [MatrixService],
|
||||
})
|
||||
export class BotModule {}
|
||||
733
services/matrix-contacts-bot/src/bot/matrix.service.ts
Normal file
733
services/matrix-contacts-bot/src/bot/matrix.service.ts
Normal file
|
|
@ -0,0 +1,733 @@
|
|||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
MatrixClient,
|
||||
SimpleFsStorageProvider,
|
||||
RichConsoleLogger,
|
||||
LogService,
|
||||
LogLevel,
|
||||
} from 'matrix-bot-sdk';
|
||||
import { ContactsService, Contact } from '../contacts/contacts.service';
|
||||
import { SessionService } from '../session/session.service';
|
||||
import { HELP_MESSAGE } from '../config/configuration';
|
||||
|
||||
// Natural language keywords
|
||||
const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [
|
||||
{ keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' },
|
||||
{ keywords: ['kontakte', 'contacts', 'alle'], command: 'kontakte' },
|
||||
{ keywords: ['favoriten', 'favorites', 'favs'], command: 'favoriten' },
|
||||
{ keywords: ['suche', 'search', 'finde'], command: 'suche' },
|
||||
{ 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 = '';
|
||||
|
||||
// Store last shown contacts per user for reference by number
|
||||
private lastContactsList: Map<string, Contact[]> = new Map();
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private contactsService: ContactsService,
|
||||
private sessionService: SessionService
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
LogService.setLogger(new RichConsoleLogger());
|
||||
LogService.setLevel(LogLevel.INFO);
|
||||
|
||||
const storage = new SimpleFsStorageProvider(storagePath || './data/bot-storage.json');
|
||||
this.client = new MatrixClient(homeserverUrl!, accessToken, storage);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
this.botUserId = await this.client.getUserId();
|
||||
this.logger.log(`Bot user ID: ${this.botUserId}`);
|
||||
|
||||
this.client.on('room.message', this.handleRoomMessage.bind(this));
|
||||
|
||||
await this.client.start();
|
||||
this.logger.log('Matrix Contacts Bot started successfully');
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
if (this.client) {
|
||||
await this.client.stop();
|
||||
this.logger.log('Matrix bot stopped');
|
||||
}
|
||||
}
|
||||
|
||||
private async sendBotIntroduction(roomId: string) {
|
||||
const introText = `**Contacts Bot - Kontaktverwaltung**
|
||||
|
||||
Ich helfe dir, deine Kontakte zu verwalten!
|
||||
|
||||
**Schnellstart:**
|
||||
\`!kontakte\` - Alle Kontakte anzeigen
|
||||
\`!suche Max\` - Kontakte suchen
|
||||
\`!neu Vorname Nachname\` - Neuen Kontakt
|
||||
|
||||
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) {
|
||||
if (event.sender === this.botUserId) return;
|
||||
|
||||
if (!this.isRoomAllowed(roomId)) {
|
||||
this.logger.debug(`Ignoring message from non-allowed room: ${roomId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = event.content as { msgtype?: string; body?: string };
|
||||
|
||||
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)}...`);
|
||||
|
||||
if (body.startsWith('!')) {
|
||||
await this.handleCommand(roomId, event.sender, body);
|
||||
return;
|
||||
}
|
||||
|
||||
const keywordCommand = this.detectKeywordCommand(body);
|
||||
if (keywordCommand) {
|
||||
await this.handleCommand(roomId, event.sender, `!${keywordCommand}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private detectKeywordCommand(message: string): string | null {
|
||||
const lowerMessage = message.toLowerCase().trim();
|
||||
|
||||
if (lowerMessage.length > 30) return null;
|
||||
|
||||
for (const { keywords, command } of KEYWORD_COMMANDS) {
|
||||
for (const keyword of keywords) {
|
||||
if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) {
|
||||
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 'kontakte':
|
||||
case 'contacts':
|
||||
case 'liste':
|
||||
case 'list':
|
||||
await this.handleListContacts(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'suche':
|
||||
case 'search':
|
||||
await this.handleSearch(roomId, sender, argString);
|
||||
break;
|
||||
|
||||
case 'favoriten':
|
||||
case 'favorites':
|
||||
case 'favs':
|
||||
await this.handleFavorites(roomId, sender);
|
||||
break;
|
||||
|
||||
case 'kontakt':
|
||||
case 'contact':
|
||||
case 'details':
|
||||
await this.handleContactDetails(roomId, sender, args);
|
||||
break;
|
||||
|
||||
case 'neu':
|
||||
case 'new':
|
||||
case 'add':
|
||||
await this.handleCreateContact(roomId, sender, args);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
case 'bearbeiten':
|
||||
await this.handleEditContact(roomId, sender, args);
|
||||
break;
|
||||
|
||||
case 'loeschen':
|
||||
case 'delete':
|
||||
case 'del':
|
||||
await this.handleDeleteContact(roomId, sender, args);
|
||||
break;
|
||||
|
||||
case 'fav':
|
||||
case 'favorit':
|
||||
await this.handleToggleFavorite(roomId, sender, args);
|
||||
break;
|
||||
|
||||
case 'archiv':
|
||||
case 'archive':
|
||||
await this.handleToggleArchive(roomId, sender, args);
|
||||
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 '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 handleListContacts(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 result = await this.contactsService.getContacts(token, { limit: 20 });
|
||||
const contacts = result.contacts;
|
||||
|
||||
if (contacts.length === 0) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Du hast noch keine Kontakte.\n\nNutze \`!neu Vorname Nachname\` um einen zu erstellen.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store for reference
|
||||
this.lastContactsList.set(sender, contacts);
|
||||
|
||||
let text = `**Deine Kontakte (${result.total}):**\n\n`;
|
||||
for (let i = 0; i < contacts.length; i++) {
|
||||
const c = contacts[i];
|
||||
const name = c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt';
|
||||
const favIcon = c.isFavorite ? ' ★' : '';
|
||||
const company = c.company ? ` - ${c.company}` : '';
|
||||
text += `**${i + 1}.** ${name}${favIcon}${company}\n`;
|
||||
}
|
||||
|
||||
if (result.total > 20) {
|
||||
text += `\n_...und ${result.total - 20} weitere_`;
|
||||
}
|
||||
|
||||
text += `\n\nNutze \`!kontakt [nr]\` fur Details.`;
|
||||
|
||||
await this.sendMessage(roomId, text);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSearch(roomId: string, sender: string, searchTerm: string) {
|
||||
const token = this.sessionService.getToken(sender);
|
||||
if (!token) {
|
||||
await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!searchTerm.trim()) {
|
||||
await this.sendMessage(roomId, `**Verwendung:** \`!suche [text]\`\n\nBeispiel: \`!suche Max\``);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.contactsService.getContacts(token, { search: searchTerm, limit: 20 });
|
||||
const contacts = result.contacts;
|
||||
|
||||
if (contacts.length === 0) {
|
||||
await this.sendMessage(roomId, `Keine Kontakte gefunden fur: "${searchTerm}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastContactsList.set(sender, contacts);
|
||||
|
||||
let text = `**Suchergebnisse fur "${searchTerm}" (${contacts.length}):**\n\n`;
|
||||
for (let i = 0; i < contacts.length; i++) {
|
||||
const c = contacts[i];
|
||||
const name = c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt';
|
||||
const favIcon = c.isFavorite ? ' ★' : '';
|
||||
const email = c.email ? ` (${c.email})` : '';
|
||||
text += `**${i + 1}.** ${name}${favIcon}${email}\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 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 result = await this.contactsService.getContacts(token, { isFavorite: true, limit: 20 });
|
||||
const contacts = result.contacts;
|
||||
|
||||
if (contacts.length === 0) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Du hast noch keine Favoriten.\n\nNutze \`!fav [nr]\` um einen Kontakt als Favorit zu markieren.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastContactsList.set(sender, contacts);
|
||||
|
||||
let text = `**Deine Favoriten (${contacts.length}):**\n\n`;
|
||||
for (let i = 0; i < contacts.length; i++) {
|
||||
const c = contacts[i];
|
||||
const name = c.displayName || `${c.firstName || ''} ${c.lastName || ''}`.trim() || 'Unbenannt';
|
||||
const phone = c.phone || c.mobile || '';
|
||||
text += `**${i + 1}.** ★ ${name}${phone ? ` - ${phone}` : ''}\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 handleContactDetails(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:** \`!kontakt [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(args[0], 10);
|
||||
if (isNaN(index) || index < 1) {
|
||||
await this.sendMessage(roomId, `Ungultige Nummer.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contacts = this.lastContactsList.get(sender);
|
||||
if (!contacts || index > contacts.length) {
|
||||
await this.sendMessage(roomId, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contact = contacts[index - 1];
|
||||
|
||||
try {
|
||||
const details = await this.contactsService.getContact(token, contact.id);
|
||||
|
||||
let text = `**${details.displayName || `${details.firstName || ''} ${details.lastName || ''}`.trim()}**\n\n`;
|
||||
|
||||
if (details.isFavorite) text += `★ Favorit\n\n`;
|
||||
|
||||
if (details.company || details.jobTitle) {
|
||||
const job = [details.jobTitle, details.company].filter(Boolean).join(' bei ');
|
||||
text += `**Beruf:** ${job}\n`;
|
||||
}
|
||||
|
||||
if (details.email) text += `**E-Mail:** ${details.email}\n`;
|
||||
if (details.phone) text += `**Telefon:** ${details.phone}\n`;
|
||||
if (details.mobile) text += `**Mobil:** ${details.mobile}\n`;
|
||||
|
||||
if (details.street || details.city) {
|
||||
const address = [details.street, `${details.postalCode || ''} ${details.city || ''}`.trim(), details.country]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
if (address) text += `**Adresse:** ${address}\n`;
|
||||
}
|
||||
|
||||
if (details.website) text += `**Website:** ${details.website}\n`;
|
||||
if (details.birthday) text += `**Geburtstag:** ${new Date(details.birthday).toLocaleDateString('de-DE')}\n`;
|
||||
if (details.notes) text += `\n**Notizen:** ${details.notes}\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 handleCreateContact(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:** \`!neu Vorname [Nachname]\`\n\nBeispiel: \`!neu Max Mustermann\``
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstName = args[0];
|
||||
const lastName = args.slice(1).join(' ') || undefined;
|
||||
|
||||
try {
|
||||
const contact = await this.contactsService.createContact(token, {
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
|
||||
const name = contact.displayName || `${firstName} ${lastName || ''}`.trim();
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`Kontakt **${name}** erstellt!\n\nNutze \`!kontakte\` um die Liste zu sehen oder \`!edit\` um weitere Daten hinzuzufugen.`
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleEditContact(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 < 3) {
|
||||
await this.sendMessage(
|
||||
roomId,
|
||||
`**Verwendung:** \`!edit [nr] [feld] [wert]\`\n\n**Felder:** email, phone, mobile, company, job, website, street, city, zip, country, notes, birthday\n\n**Beispiel:** \`!edit 1 email max@example.com\``
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(args[0], 10);
|
||||
const field = args[1].toLowerCase();
|
||||
const value = args.slice(2).join(' ');
|
||||
|
||||
if (isNaN(index) || index < 1) {
|
||||
await this.sendMessage(roomId, `Ungultige Nummer.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contacts = this.lastContactsList.get(sender);
|
||||
if (!contacts || index > contacts.length) {
|
||||
await this.sendMessage(roomId, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contact = contacts[index - 1];
|
||||
|
||||
const fieldMap: Record<string, string> = {
|
||||
email: 'email',
|
||||
phone: 'phone',
|
||||
telefon: 'phone',
|
||||
mobile: 'mobile',
|
||||
mobil: 'mobile',
|
||||
handy: 'mobile',
|
||||
company: 'company',
|
||||
firma: 'company',
|
||||
job: 'jobTitle',
|
||||
jobtitle: 'jobTitle',
|
||||
beruf: 'jobTitle',
|
||||
website: 'website',
|
||||
web: 'website',
|
||||
street: 'street',
|
||||
strasse: 'street',
|
||||
city: 'city',
|
||||
stadt: 'city',
|
||||
zip: 'postalCode',
|
||||
plz: 'postalCode',
|
||||
country: 'country',
|
||||
land: 'country',
|
||||
notes: 'notes',
|
||||
notizen: 'notes',
|
||||
birthday: 'birthday',
|
||||
geburtstag: 'birthday',
|
||||
firstname: 'firstName',
|
||||
vorname: 'firstName',
|
||||
lastname: 'lastName',
|
||||
nachname: 'lastName',
|
||||
};
|
||||
|
||||
const mappedField = fieldMap[field];
|
||||
if (!mappedField) {
|
||||
await this.sendMessage(roomId, `Unbekanntes Feld: ${field}\n\n**Gultige Felder:** email, phone, mobile, company, job, website, street, city, zip, country, notes, birthday`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const updated = await this.contactsService.updateContact(token, contact.id, {
|
||||
[mappedField]: value,
|
||||
});
|
||||
|
||||
const name = updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim();
|
||||
await this.sendMessage(roomId, `Kontakt **${name}** aktualisiert!\n\n**${field}:** ${value}`);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleDeleteContact(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:** \`!loeschen [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(args[0], 10);
|
||||
if (isNaN(index) || index < 1) {
|
||||
await this.sendMessage(roomId, `Ungultige Nummer.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contacts = this.lastContactsList.get(sender);
|
||||
if (!contacts || index > contacts.length) {
|
||||
await this.sendMessage(roomId, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contact = contacts[index - 1];
|
||||
const name = contact.displayName || `${contact.firstName || ''} ${contact.lastName || ''}`.trim();
|
||||
|
||||
try {
|
||||
await this.contactsService.deleteContact(token, contact.id);
|
||||
await this.sendMessage(roomId, `Kontakt **${name}** geloscht.`);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleToggleFavorite(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:** \`!fav [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(args[0], 10);
|
||||
if (isNaN(index) || index < 1) {
|
||||
await this.sendMessage(roomId, `Ungultige Nummer.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contacts = this.lastContactsList.get(sender);
|
||||
if (!contacts || index > contacts.length) {
|
||||
await this.sendMessage(roomId, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contact = contacts[index - 1];
|
||||
|
||||
try {
|
||||
const updated = await this.contactsService.toggleFavorite(token, contact.id);
|
||||
const name = updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim();
|
||||
const status = updated.isFavorite ? 'als Favorit markiert ★' : 'aus Favoriten entfernt';
|
||||
await this.sendMessage(roomId, `**${name}** ${status}`);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleToggleArchive(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:** \`!archiv [nr]\`\n\nNutze \`!kontakte\` um die Liste zu sehen.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(args[0], 10);
|
||||
if (isNaN(index) || index < 1) {
|
||||
await this.sendMessage(roomId, `Ungultige Nummer.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contacts = this.lastContactsList.get(sender);
|
||||
if (!contacts || index > contacts.length) {
|
||||
await this.sendMessage(roomId, `Kontakt ${index} nicht gefunden. Nutze \`!kontakte\` zuerst.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const contact = contacts[index - 1];
|
||||
|
||||
try {
|
||||
const updated = await this.contactsService.toggleArchive(token, contact.id);
|
||||
const name = updated.displayName || `${updated.firstName || ''} ${updated.lastName || ''}`.trim();
|
||||
const status = updated.isArchived ? 'archiviert' : 'aus dem Archiv geholt';
|
||||
await this.sendMessage(roomId, `**${name}** ${status}`);
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
await this.sendMessage(roomId, `Fehler: ${errorMsg}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendHelp(roomId: string) {
|
||||
await this.sendMessage(roomId, HELP_MESSAGE);
|
||||
}
|
||||
|
||||
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\nNutze \`!kontakte\` um deine Kontakte zu sehen.`
|
||||
);
|
||||
} else {
|
||||
await this.sendMessage(roomId, `Anmeldung fehlgeschlagen: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleStatus(roomId: string, sender: string) {
|
||||
const backendHealthy = await this.contactsService.checkHealth();
|
||||
const isLoggedIn = this.sessionService.isLoggedIn(sender);
|
||||
const sessionCount = this.sessionService.getSessionCount();
|
||||
|
||||
const statusText = `**Contacts Bot Status**
|
||||
|
||||
**Backend:** ${backendHealthy ? 'Online' : 'Offline'}
|
||||
**Dein Status:** ${isLoggedIn ? 'Angemeldet' : 'Nicht angemeldet'}
|
||||
**Aktive Sessions:** ${sessionCount}
|
||||
|
||||
${!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
|
||||
.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>')
|
||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
||||
.replace(/_([^_]+)_/g, '<em>$1</em>')
|
||||
.replace(/\n/g, '<br/>')
|
||||
);
|
||||
}
|
||||
}
|
||||
48
services/matrix-contacts-bot/src/config/configuration.ts
Normal file
48
services/matrix-contacts-bot/src/config/configuration.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
export default () => ({
|
||||
port: parseInt(process.env.PORT || '3320', 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',
|
||||
},
|
||||
contacts: {
|
||||
backendUrl: process.env.CONTACTS_BACKEND_URL || 'http://localhost:3015',
|
||||
apiPrefix: process.env.CONTACTS_API_PREFIX || '/api/v1',
|
||||
},
|
||||
auth: {
|
||||
url: process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001',
|
||||
},
|
||||
});
|
||||
|
||||
export const HELP_MESSAGE = `**Contacts Bot - Kontaktverwaltung**
|
||||
|
||||
**Kontakte anzeigen:**
|
||||
- \`!kontakte\` - Alle Kontakte anzeigen
|
||||
- \`!suche [text]\` - Kontakte suchen
|
||||
- \`!favoriten\` - Favoriten anzeigen
|
||||
- \`!kontakt [nr]\` - Kontakt-Details
|
||||
|
||||
**Kontakte verwalten:** (Login erforderlich)
|
||||
- \`!neu Vorname Nachname\` - Neuen Kontakt erstellen
|
||||
- \`!edit [nr] [feld] [wert]\` - Kontakt bearbeiten
|
||||
- \`!loeschen [nr]\` - Kontakt loschen
|
||||
- \`!fav [nr]\` - Favorit umschalten
|
||||
- \`!archiv [nr]\` - Archivieren umschalten
|
||||
|
||||
**Felder fur !edit:**
|
||||
- \`email\`, \`phone\`, \`mobile\`
|
||||
- \`company\`, \`job\`, \`website\`
|
||||
- \`street\`, \`city\`, \`zip\`, \`country\`
|
||||
- \`notes\`, \`birthday\`
|
||||
|
||||
**Beispiele:**
|
||||
\`!neu Max Mustermann\`
|
||||
\`!edit 1 email max@example.com\`
|
||||
\`!edit 1 phone +49 123 456789\`
|
||||
|
||||
**Sonstiges:**
|
||||
- \`!login email passwort\` - Anmelden
|
||||
- \`!logout\` - Abmelden
|
||||
- \`!status\` - Bot-Status
|
||||
- \`!help\` - Diese Hilfe`;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ContactsService } from './contacts.service';
|
||||
|
||||
@Module({
|
||||
providers: [ContactsService],
|
||||
exports: [ContactsService],
|
||||
})
|
||||
export class ContactsModule {}
|
||||
252
services/matrix-contacts-bot/src/contacts/contacts.service.ts
Normal file
252
services/matrix-contacts-bot/src/contacts/contacts.service.ts
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export interface Contact {
|
||||
id: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
displayName?: string | null;
|
||||
nickname?: string | null;
|
||||
email?: string | null;
|
||||
phone?: string | null;
|
||||
mobile?: string | null;
|
||||
street?: string | null;
|
||||
city?: string | null;
|
||||
postalCode?: string | null;
|
||||
country?: string | null;
|
||||
company?: string | null;
|
||||
jobTitle?: string | null;
|
||||
department?: string | null;
|
||||
website?: string | null;
|
||||
birthday?: string | null;
|
||||
notes?: string | null;
|
||||
photoUrl?: string | null;
|
||||
isFavorite: boolean;
|
||||
isArchived: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ContactFilters {
|
||||
search?: string;
|
||||
isFavorite?: boolean;
|
||||
isArchived?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface ContactsResult {
|
||||
contacts: Contact[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface CreateContactDto {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
company?: string;
|
||||
jobTitle?: string;
|
||||
website?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ContactsService {
|
||||
private readonly logger = new Logger(ContactsService.name);
|
||||
private readonly backendUrl: string;
|
||||
private readonly apiPrefix: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.backendUrl =
|
||||
this.configService.get<string>('contacts.backendUrl') || 'http://localhost:3015';
|
||||
this.apiPrefix = this.configService.get<string>('contacts.apiPrefix') || '/api/v1';
|
||||
}
|
||||
|
||||
private getApiUrl(path: string): string {
|
||||
return `${this.backendUrl}${this.apiPrefix}${path}`;
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.backendUrl}/health`);
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
this.logger.error('Health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async getContacts(token: string, filters: ContactFilters = {}): Promise<ContactsResult> {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (filters.search) params.set('search', filters.search);
|
||||
if (filters.isFavorite !== undefined) params.set('isFavorite', String(filters.isFavorite));
|
||||
if (filters.isArchived !== undefined) params.set('isArchived', String(filters.isArchived));
|
||||
if (filters.limit) params.set('limit', String(filters.limit));
|
||||
if (filters.offset) params.set('offset', String(filters.offset));
|
||||
|
||||
const url = `${this.getApiUrl('/contacts')}?${params.toString()}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch contacts: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to fetch contacts:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getContact(token: string, contactId: string): Promise<Contact> {
|
||||
try {
|
||||
const response = await fetch(this.getApiUrl(`/contacts/${contactId}`), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Kontakt nicht gefunden');
|
||||
}
|
||||
throw new Error(`Failed to fetch contact: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.contact;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to fetch contact ${contactId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createContact(token: string, data: CreateContactDto): Promise<Contact> {
|
||||
try {
|
||||
const response = await fetch(this.getApiUrl('/contacts'), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `Failed to create contact: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.contact;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to create contact:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateContact(token: string, contactId: string, data: Partial<CreateContactDto>): Promise<Contact> {
|
||||
try {
|
||||
const response = await fetch(this.getApiUrl(`/contacts/${contactId}`), {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Kontakt nicht gefunden');
|
||||
}
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.message || `Failed to update contact: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.contact;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to update contact ${contactId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteContact(token: string, contactId: string): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(this.getApiUrl(`/contacts/${contactId}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Kontakt nicht gefunden');
|
||||
}
|
||||
throw new Error(`Failed to delete contact: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to delete contact ${contactId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async toggleFavorite(token: string, contactId: string): Promise<Contact> {
|
||||
try {
|
||||
const response = await fetch(this.getApiUrl(`/contacts/${contactId}/favorite`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Kontakt nicht gefunden');
|
||||
}
|
||||
throw new Error(`Failed to toggle favorite: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.contact;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to toggle favorite for ${contactId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async toggleArchive(token: string, contactId: string): Promise<Contact> {
|
||||
try {
|
||||
const response = await fetch(this.getApiUrl(`/contacts/${contactId}/archive`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
throw new Error('Kontakt nicht gefunden');
|
||||
}
|
||||
throw new Error(`Failed to toggle archive: ${response.status}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.contact;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to toggle archive for ${contactId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
services/matrix-contacts-bot/src/health.controller.ts
Normal file
13
services/matrix-contacts-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-contacts-bot',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
17
services/matrix-contacts-bot/src/main.ts
Normal file
17
services/matrix-contacts-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 || 3320;
|
||||
await app.listen(port);
|
||||
|
||||
logger.log(`Matrix Contacts Bot running on port ${port}`);
|
||||
logger.log(`Health check: http://localhost:${port}/health`);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SessionService } from './session.service';
|
||||
|
||||
@Module({
|
||||
providers: [SessionService],
|
||||
exports: [SessionService],
|
||||
})
|
||||
export class SessionModule {}
|
||||
90
services/matrix-contacts-bot/src/session/session.service.ts
Normal file
90
services/matrix-contacts-bot/src/session/session.service.ts
Normal file
|
|
@ -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<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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
23
services/matrix-contacts-bot/tsconfig.json
Normal file
23
services/matrix-contacts-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