diff --git a/services/matrix-storage-bot/.env.example b/services/matrix-storage-bot/.env.example new file mode 100644 index 000000000..b2be60948 --- /dev/null +++ b/services/matrix-storage-bot/.env.example @@ -0,0 +1,15 @@ +# Server +PORT=3323 + +# Matrix +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN=syt_xxx +MATRIX_ALLOWED_ROOMS=#storage:matrix.mana.how +MATRIX_STORAGE_PATH=./data/bot-storage.json + +# Storage Backend +STORAGE_BACKEND_URL=http://localhost:3016 +STORAGE_API_PREFIX=/api/v1 + +# Mana Core Auth +MANA_CORE_AUTH_URL=http://localhost:3001 diff --git a/services/matrix-storage-bot/.gitignore b/services/matrix-storage-bot/.gitignore new file mode 100644 index 000000000..2d508e5ec --- /dev/null +++ b/services/matrix-storage-bot/.gitignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment +.env +.env.local + +# Data +data/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# TypeScript +*.tsbuildinfo diff --git a/services/matrix-storage-bot/CLAUDE.md b/services/matrix-storage-bot/CLAUDE.md new file mode 100644 index 000000000..d387c0818 --- /dev/null +++ b/services/matrix-storage-bot/CLAUDE.md @@ -0,0 +1,225 @@ +# Matrix Storage Bot - Claude Code Guidelines + +## Overview + +Matrix Storage Bot provides cloud storage management via Matrix chat. It integrates with the Storage backend for file/folder management, sharing, favorites, search, and trash operations. + +## Tech Stack + +- **Framework**: NestJS 10 +- **Matrix**: matrix-bot-sdk +- **Backend**: Storage API (port 3016) +- **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-storage-bot/ +├── src/ +│ ├── main.ts # Application entry point (port 3323) +│ ├── 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 +│ ├── storage/ +│ │ ├── storage.module.ts +│ │ └── storage.service.ts # Storage Backend API client +│ └── session/ +│ ├── session.module.ts +│ └── session.service.ts # User session & auth management +├── Dockerfile +└── package.json +``` + +## Bot Commands + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!help` | hilfe | Show help message | +| `!login email pass` | - | Login | +| `!logout` | - | Logout | +| `!status` | - | Bot status | + +### File Management + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!dateien` | files, ls | List files in root | +| `!dateien [ordner-nr]` | - | List files in folder | +| `!datei [nr]` | file, info | Show file details | +| `!download [nr]` | dl | Get download link | +| `!loeschen [nr]` | delete, rm | Move file to trash | +| `!umbenennen [nr] name` | rename, mv | Rename file | +| `!verschieben [nr] [ordner-nr]` | move | Move to folder | + +### Folder Management + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!ordner` | folders, dir | List root folders | +| `!ordner [nr]` | - | List subfolders | +| `!neuordner Name` | mkdir, newfolder | Create folder | +| `!neuordner Name [in-nr]` | - | Create subfolder | +| `!ordnerloeschen [nr]` | rmdir | Delete folder | + +### Sharing + +| Command | Options | Description | +|---------|---------|-------------| +| `!teilen [nr]` | share | Share file (create link) | +| `--tage N` | - | Expire in N days | +| `--passwort abc` | - | Password protect | +| `--downloads N` | - | Limit downloads | +| `!links` | shares | List share links | +| `!linkloeschen [nr]` | unshare | Delete share link | + +### Organization + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!suche Begriff` | search, find | Search files/folders | +| `!favoriten` | favorites, favs | Show favorites | +| `!fav [nr]` | favorit | Toggle favorite | + +### Trash + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!papierkorb` | trash | Show trash | +| `!wiederherstellen [nr]` | restore | Restore from trash | +| `!leeren` | emptytrash | Empty trash | + +## Example Usage + +``` +# Login +!login max@example.com mypassword + +# List files +!dateien + +# Create a folder +!neuordner Dokumente + +# List folders +!ordner + +# Move file to folder +!verschieben 1 1 + +# Share a file with expiration +!teilen 1 --tage 7 --passwort geheim + +# Search for files +!suche bericht + +# View favorites +!favoriten + +# Toggle favorite +!fav 1 + +# View trash +!papierkorb + +# Restore from trash +!wiederherstellen 1 +``` + +## Environment Variables + +```env +# Server +PORT=3323 + +# Matrix +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN=syt_xxx +MATRIX_ALLOWED_ROOMS=#storage:matrix.mana.how +MATRIX_STORAGE_PATH=./data/bot-storage.json + +# Storage Backend +STORAGE_BACKEND_URL=http://localhost:3016 +STORAGE_API_PREFIX=/api/v1 + +# Mana Core Auth +MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +## Docker + +```bash +# Build locally +docker build -f services/matrix-storage-bot/Dockerfile -t matrix-storage-bot services/matrix-storage-bot + +# Run +docker run -p 3323:3323 \ + -e MATRIX_HOMESERVER_URL=http://synapse:8008 \ + -e MATRIX_ACCESS_TOKEN=syt_xxx \ + -e STORAGE_BACKEND_URL=http://storage-backend:3016 \ + -e MANA_CORE_AUTH_URL=http://mana-core-auth:3001 \ + -v matrix-storage-bot-data:/app/data \ + matrix-storage-bot +``` + +## Health Check + +```bash +curl http://localhost:3323/health +``` + +## Storage Backend API Endpoints Used + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/v1/health` | GET | Health check | +| `/api/v1/files` | GET | List files | +| `/api/v1/files/:id` | GET | Get file details | +| `/api/v1/files/:id/download` | GET | Get download URL | +| `/api/v1/files/:id` | PATCH | Rename file | +| `/api/v1/files/:id/move` | PATCH | Move file | +| `/api/v1/files/:id` | DELETE | Delete file | +| `/api/v1/files/:id/favorite` | POST | Toggle favorite | +| `/api/v1/folders` | GET | List folders | +| `/api/v1/folders` | POST | Create folder | +| `/api/v1/folders/:id` | DELETE | Delete folder | +| `/api/v1/folders/:id/favorite` | POST | Toggle favorite | +| `/api/v1/shares` | GET | List shares | +| `/api/v1/shares` | POST | Create share | +| `/api/v1/shares/:id` | DELETE | Delete share | +| `/api/v1/search` | GET | Search files/folders | +| `/api/v1/favorites` | GET | Get favorites | +| `/api/v1/trash` | GET | List trash | +| `/api/v1/trash/:id/restore` | POST | Restore from trash | +| `/api/v1/trash` | DELETE | Empty trash | + +## Number-Based Reference System + +The bot uses a number-based reference system for ease of use: +1. User runs `!dateien` or `!ordner` to get a list +2. Bot stores the list internally for the user +3. User can reference items by their list number +4. Numbers are valid until the user runs a new list command + +This allows simple commands like: +- `!datei 3` - Show details for file #3 +- `!download 1` - Get download link for file #1 +- `!dateien 2` - List files in folder #2 +- `!verschieben 1 3` - Move file #1 to folder #3 diff --git a/services/matrix-storage-bot/Dockerfile b/services/matrix-storage-bot/Dockerfile new file mode 100644 index 000000000..92d866e9b --- /dev/null +++ b/services/matrix-storage-bot/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package files and install production dependencies only +COPY package.json ./ +RUN npm install --omit=dev + +# Copy built application from builder +COPY --from=builder /app/dist ./dist + +# Create data directory +RUN mkdir -p /app/data + +# Expose port +EXPOSE 3323 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3323/health || exit 1 + +# Start the application +CMD ["node", "dist/main.js"] diff --git a/services/matrix-storage-bot/nest-cli.json b/services/matrix-storage-bot/nest-cli.json new file mode 100644 index 000000000..5c06bb8c3 --- /dev/null +++ b/services/matrix-storage-bot/nest-cli.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json-schema.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} diff --git a/services/matrix-storage-bot/package.json b/services/matrix-storage-bot/package.json new file mode 100644 index 000000000..1f525c4bf --- /dev/null +++ b/services/matrix-storage-bot/package.json @@ -0,0 +1,27 @@ +{ + "name": "@mana-bots/matrix-storage-bot", + "version": "1.0.0", + "description": "Matrix bot for cloud storage management", + "main": "dist/main.js", + "scripts": { + "build": "nest build", + "start": "nest start", + "start:dev": "nest start --watch", + "start:prod": "node dist/main.js", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@nestjs/common": "^10.3.0", + "@nestjs/config": "^3.1.1", + "@nestjs/core": "^10.3.0", + "@nestjs/platform-express": "^10.3.0", + "matrix-bot-sdk": "^0.7.1", + "reflect-metadata": "^0.1.13", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.3.0", + "@types/node": "^20.10.0", + "typescript": "^5.3.0" + } +} diff --git a/services/matrix-storage-bot/src/app.module.ts b/services/matrix-storage-bot/src/app.module.ts new file mode 100644 index 000000000..d67fe2f63 --- /dev/null +++ b/services/matrix-storage-bot/src/app.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { HealthController } from './health.controller'; +import { BotModule } from './bot/bot.module'; +import { StorageModule } from './storage/storage.module'; +import { SessionModule } from './session/session.module'; +import configuration from './config/configuration'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [configuration], + }), + BotModule, + StorageModule, + SessionModule, + ], + controllers: [HealthController], +}) +export class AppModule {} diff --git a/services/matrix-storage-bot/src/bot/bot.module.ts b/services/matrix-storage-bot/src/bot/bot.module.ts new file mode 100644 index 000000000..cf0c6aea1 --- /dev/null +++ b/services/matrix-storage-bot/src/bot/bot.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { MatrixService } from './matrix.service'; +import { StorageModule } from '../storage/storage.module'; +import { SessionModule } from '../session/session.module'; + +@Module({ + imports: [StorageModule, SessionModule], + providers: [MatrixService], + exports: [MatrixService], +}) +export class BotModule {} diff --git a/services/matrix-storage-bot/src/bot/matrix.service.ts b/services/matrix-storage-bot/src/bot/matrix.service.ts new file mode 100644 index 000000000..026d2c806 --- /dev/null +++ b/services/matrix-storage-bot/src/bot/matrix.service.ts @@ -0,0 +1,845 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + MatrixClient, + SimpleFsStorageProvider, + AutojoinRoomsMixin, +} from 'matrix-bot-sdk'; +import { StorageService, StorageFile, Folder, ShareLink, TrashItem } from '../storage/storage.service'; +import { SessionService } from '../session/session.service'; +import { HELP_MESSAGE } from '../config/configuration'; + +type ListItem = StorageFile | Folder; + +@Injectable() +export class MatrixService implements OnModuleInit { + private readonly logger = new Logger(MatrixService.name); + private client: MatrixClient; + private allowedRooms: string[]; + + // Store last shown items per user for reference by number + private lastFilesList: Map = new Map(); + private lastFoldersList: Map = new Map(); + private lastSharesList: Map = new Map(); + private lastTrashList: Map = new Map(); + private currentFolder: Map = new Map(); + + constructor( + private configService: ConfigService, + private storageService: StorageService, + private sessionService: SessionService + ) {} + + async onModuleInit() { + const homeserverUrl = this.configService.get('matrix.homeserverUrl'); + const accessToken = this.configService.get('matrix.accessToken'); + const storagePath = this.configService.get('matrix.storagePath'); + this.allowedRooms = this.configService.get('matrix.allowedRooms') || []; + + if (!accessToken) { + this.logger.warn('No Matrix access token configured, bot disabled'); + return; + } + + const storage = new SimpleFsStorageProvider(storagePath); + this.client = new MatrixClient(homeserverUrl, accessToken, storage); + + AutojoinRoomsMixin.setupOnClient(this.client); + + this.client.on('room.message', this.handleMessage.bind(this)); + + await this.client.start(); + this.logger.log('Matrix Storage Bot started'); + } + + private async handleMessage(roomId: string, event: any) { + if (event.sender === (await this.client.getUserId())) return; + if (event.content?.msgtype !== 'm.text') return; + + const body = event.content.body?.trim(); + if (!body?.startsWith('!')) return; + + // Check allowed rooms + if (this.allowedRooms.length > 0 && !this.allowedRooms.includes(roomId)) { + return; + } + + const sender = event.sender; + const parts = body.slice(1).split(/\s+/); + const command = parts[0].toLowerCase(); + const args = parts.slice(1); + const argString = args.join(' '); + + try { + switch (command) { + case 'help': + case 'hilfe': + await this.sendHtml(roomId, HELP_MESSAGE); + break; + + case 'login': + await this.handleLogin(roomId, sender, args); + break; + + case 'logout': + this.sessionService.logout(sender); + await this.sendHtml(roomId, '

Erfolgreich abgemeldet.

'); + break; + + case 'status': + await this.handleStatus(roomId, sender); + break; + + // File commands + case 'dateien': + case 'files': + case 'ls': + await this.handleListFiles(roomId, sender, args[0]); + break; + + case 'datei': + case 'file': + case 'info': + await this.handleFileDetails(roomId, sender, args[0]); + break; + + case 'download': + case 'dl': + await this.handleDownload(roomId, sender, args[0]); + break; + + case 'loeschen': + case 'delete': + case 'rm': + await this.handleDeleteFile(roomId, sender, args[0]); + break; + + case 'umbenennen': + case 'rename': + case 'mv': + await this.handleRenameFile(roomId, sender, args[0], args.slice(1).join(' ')); + break; + + case 'verschieben': + case 'move': + await this.handleMoveFile(roomId, sender, args[0], args[1]); + break; + + // Folder commands + case 'ordner': + case 'folders': + case 'dir': + await this.handleListFolders(roomId, sender, args[0]); + break; + + case 'neuordner': + case 'mkdir': + case 'newfolder': + await this.handleCreateFolder(roomId, sender, args); + break; + + case 'ordnerloeschen': + case 'rmdir': + await this.handleDeleteFolder(roomId, sender, args[0]); + break; + + // Share commands + case 'teilen': + case 'share': + await this.handleShareFile(roomId, sender, argString); + break; + + case 'links': + case 'shares': + await this.handleListShares(roomId, sender); + break; + + case 'linkloeschen': + case 'unshare': + await this.handleDeleteShare(roomId, sender, args[0]); + break; + + // Organization + case 'suche': + case 'search': + case 'find': + await this.handleSearch(roomId, sender, argString); + break; + + case 'favoriten': + case 'favorites': + case 'favs': + await this.handleFavorites(roomId, sender); + break; + + case 'fav': + case 'favorit': + await this.handleToggleFavorite(roomId, sender, args[0]); + break; + + // Trash + case 'papierkorb': + case 'trash': + await this.handleTrash(roomId, sender); + break; + + case 'wiederherstellen': + case 'restore': + await this.handleRestore(roomId, sender, args[0]); + break; + + case 'leeren': + case 'emptytrash': + await this.handleEmptyTrash(roomId, sender); + break; + + default: + await this.sendHtml( + roomId, + `

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

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

Fehler: ${error.message}

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

Verwendung: !login email passwort

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

Erfolgreich angemeldet als ${email}

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

Login fehlgeschlagen: ${result.error}

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

Storage Bot Status

+
    +
  • Backend: ${backendOk ? 'Online' : 'Offline'}
  • +
  • Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}
  • +
  • Aktive Sessions: ${sessions}
  • +
` + ); + } + + // File handlers + private async handleListFiles(roomId: string, sender: string, folderNumStr?: string) { + const token = this.requireAuth(sender); + + let parentFolderId: string | undefined; + if (folderNumStr) { + const folder = this.getFolderByNumber(sender, folderNumStr); + if (!folder) { + await this.sendHtml(roomId, '

Ungueltige Ordner-Nummer.

'); + return; + } + parentFolderId = folder.id; + this.currentFolder.set(sender, folder.id); + } else { + this.currentFolder.set(sender, null); + } + + const result = await this.storageService.getFiles(token, parentFolderId); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

Keine Dateien vorhanden.

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

Dateien

    '; + for (const file of files) { + const size = this.formatSize(file.size); + const fav = file.isFavorite ? ' ⭐' : ''; + html += `
  1. ${file.name} (${size})${fav}
  2. `; + } + html += '
'; + html += '

Nutze !datei [nr] fuer Details oder !download [nr]

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

Ungueltige Nummer. Nutze zuerst !dateien

'); + return; + } + + const result = await this.storageService.getFile(token, file.id); + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const f = result.data!; + const fav = f.isFavorite ? ' ⭐' : ''; + let html = `

${f.name}${fav}

`; + html += '
    '; + html += `
  • Typ: ${f.mimeType}
  • `; + html += `
  • Groesse: ${this.formatSize(f.size)}
  • `; + html += `
  • Erstellt: ${new Date(f.createdAt).toLocaleDateString('de-DE')}
  • `; + html += `
  • Aktualisiert: ${new Date(f.updatedAt).toLocaleDateString('de-DE')}
  • `; + html += '
'; + html += `

Nutze !download ${numberStr} fuer Download-Link

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

Ungueltige Nummer. Nutze zuerst !dateien

'); + return; + } + + const result = await this.storageService.getDownloadUrl(token, file.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

${file.name}

Download: ${result.data!.url}

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

Ungueltige Nummer. Nutze zuerst !dateien

'); + return; + } + + const result = await this.storageService.deleteFile(token, file.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

${file.name} in Papierkorb verschoben.

`); + } + + private async handleRenameFile(roomId: string, sender: string, numberStr: string, newName: string) { + if (!newName) { + await this.sendHtml(roomId, '

Verwendung: !umbenennen [nr] neuer name

'); + return; + } + + const token = this.requireAuth(sender); + const file = this.getFileByNumber(sender, numberStr); + + if (!file) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !dateien

'); + return; + } + + const result = await this.storageService.renameFile(token, file.id, newName); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

${file.name} umbenannt zu ${newName}

`); + } + + private async handleMoveFile(roomId: string, sender: string, fileNumStr: string, folderNumStr: string) { + const token = this.requireAuth(sender); + const file = this.getFileByNumber(sender, fileNumStr); + + if (!file) { + await this.sendHtml(roomId, '

Ungueltige Datei-Nummer.

'); + return; + } + + let parentFolderId: string | null = null; + let folderName = 'Root'; + + if (folderNumStr && folderNumStr !== '0' && folderNumStr.toLowerCase() !== 'root') { + const folder = this.getFolderByNumber(sender, folderNumStr); + if (!folder) { + await this.sendHtml(roomId, '

Ungueltige Ordner-Nummer. Nutze 0 oder root fuer Root.

'); + return; + } + parentFolderId = folder.id; + folderName = folder.name; + } + + const result = await this.storageService.moveFile(token, file.id, parentFolderId); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

${file.name} nach ${folderName} verschoben.

`); + } + + // Folder handlers + private async handleListFolders(roomId: string, sender: string, folderNumStr?: string) { + const token = this.requireAuth(sender); + + let parentFolderId: string | undefined; + if (folderNumStr) { + const folder = this.getFolderByNumber(sender, folderNumStr); + if (!folder) { + await this.sendHtml(roomId, '

Ungueltige Ordner-Nummer.

'); + return; + } + parentFolderId = folder.id; + } + + const result = await this.storageService.getFolders(token, parentFolderId); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

Keine Ordner vorhanden.

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

Ordner

    '; + for (const folder of folders) { + const fav = folder.isFavorite ? ' ⭐' : ''; + const color = folder.color ? ` [${folder.color}]` : ''; + html += `
  1. ${folder.name}${color}${fav}
  2. `; + } + html += '
'; + html += '

Nutze !dateien [nr] um Dateien im Ordner zu sehen

'; + + await this.sendHtml(roomId, html); + } + + private async handleCreateFolder(roomId: string, sender: string, args: string[]) { + if (args.length === 0) { + await this.sendHtml(roomId, '

Verwendung: !neuordner Name [in-ordner-nr]

'); + return; + } + + const token = this.requireAuth(sender); + + // Check if last arg is a number (parent folder) + let parentFolderId: string | undefined; + let name = args.join(' '); + + const lastArg = args[args.length - 1]; + if (/^\d+$/.test(lastArg) && args.length > 1) { + const folder = this.getFolderByNumber(sender, lastArg); + if (folder) { + parentFolderId = folder.id; + name = args.slice(0, -1).join(' '); + } + } + + const result = await this.storageService.createFolder(token, name, parentFolderId); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

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

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

Ungueltige Nummer. Nutze zuerst !ordner

'); + return; + } + + const result = await this.storageService.deleteFolder(token, folder.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

Ordner ${folder.name} in Papierkorb verschoben.

`); + } + + // Share handlers + private async handleShareFile(roomId: string, sender: string, argString: string) { + const token = this.requireAuth(sender); + + // Parse arguments + const args = argString.split(/\s+/); + const numberStr = args[0]; + const file = this.getFileByNumber(sender, numberStr); + + if (!file) { + await this.sendHtml(roomId, '

Ungueltige Nummer. Nutze zuerst !dateien

'); + return; + } + + const options: any = {}; + + // Parse --tage N + const daysMatch = argString.match(/--tage\s+(\d+)/i); + if (daysMatch) { + options.expiresInDays = parseInt(daysMatch[1], 10); + } + + // Parse --passwort XXX + const passMatch = argString.match(/--passwort\s+(\S+)/i); + if (passMatch) { + options.password = passMatch[1]; + } + + // Parse --downloads N + const dlMatch = argString.match(/--downloads\s+(\d+)/i); + if (dlMatch) { + options.maxDownloads = parseInt(dlMatch[1], 10); + } + + const result = await this.storageService.createShare(token, file.id, options); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const share = result.data!; + const shareUrl = `${this.configService.get('storage.backendUrl')}/public/shares/${share.shareToken}`; + + let html = `

${file.name} wird geteilt:

`; + html += `

${shareUrl}

`; + if (options.expiresInDays) html += `

Gueltig: ${options.expiresInDays} Tage

`; + if (options.password) html += `

Passwort geschuetzt

`; + if (options.maxDownloads) html += `

Max Downloads: ${options.maxDownloads}

`; + + await this.sendHtml(roomId, html); + } + + private async handleListShares(roomId: string, sender: string) { + const token = this.requireAuth(sender); + const result = await this.storageService.getShares(token); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

Keine Share-Links vorhanden.

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

Share-Links

    '; + for (const share of shares) { + const expires = share.expiresAt ? ` (bis ${new Date(share.expiresAt).toLocaleDateString('de-DE')})` : ''; + const downloads = share.maxDownloads ? ` [${share.downloadCount}/${share.maxDownloads}]` : ` [${share.downloadCount} DL]`; + html += `
  1. ${share.shareType}${expires}${downloads}
  2. `; + } + html += '
'; + html += '

Nutze !linkloeschen [nr] zum Loeschen

'; + + await this.sendHtml(roomId, html); + } + + private async handleDeleteShare(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const shares = this.lastSharesList.get(sender); + + if (!shares) { + await this.sendHtml(roomId, '

Nutze zuerst !links

'); + return; + } + + const index = parseInt(numberStr, 10) - 1; + if (isNaN(index) || index < 0 || index >= shares.length) { + await this.sendHtml(roomId, '

Ungueltige Nummer.

'); + return; + } + + const share = shares[index]; + const result = await this.storageService.deleteShare(token, share.id); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

Share-Link geloescht.

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

Verwendung: !suche Begriff

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

Fehler: ${result.error}

`); + return; + } + + const { files, folders } = result.data!; + this.lastFilesList.set(sender, files); + this.lastFoldersList.set(sender, folders); + + if (files.length === 0 && folders.length === 0) { + await this.sendHtml(roomId, `

Keine Ergebnisse fuer "${query}"

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

Suchergebnisse: "${query}"

`; + + if (folders.length > 0) { + html += '

Ordner:

    '; + for (const folder of folders) { + html += `
  1. ${folder.name}
  2. `; + } + html += '
'; + } + + if (files.length > 0) { + html += '

Dateien:

    '; + for (const file of files) { + html += `
  1. ${file.name} (${this.formatSize(file.size)})
  2. `; + } + html += '
'; + } + + await this.sendHtml(roomId, html); + } + + private async handleFavorites(roomId: string, sender: string) { + const token = this.requireAuth(sender); + const result = await this.storageService.getFavorites(token); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + + const { files, folders } = result.data!; + this.lastFilesList.set(sender, files); + this.lastFoldersList.set(sender, folders); + + if (files.length === 0 && folders.length === 0) { + await this.sendHtml(roomId, '

Keine Favoriten vorhanden.

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

Favoriten ⭐

'; + + if (folders.length > 0) { + html += '

Ordner:

    '; + for (const folder of folders) { + html += `
  1. ${folder.name}
  2. `; + } + html += '
'; + } + + if (files.length > 0) { + html += '

Dateien:

    '; + for (const file of files) { + html += `
  1. ${file.name}
  2. `; + } + html += '
'; + } + + await this.sendHtml(roomId, html); + } + + private async handleToggleFavorite(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + + // Try file first + const file = this.getFileByNumber(sender, numberStr); + if (file) { + const result = await this.storageService.toggleFileFavorite(token, file.id); + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + const status = result.data!.isFavorite ? 'hinzugefuegt' : 'entfernt'; + await this.sendHtml(roomId, `

${file.name}: Favorit ${status}

`); + return; + } + + // Try folder + const folder = this.getFolderByNumber(sender, numberStr); + if (folder) { + const result = await this.storageService.toggleFolderFavorite(token, folder.id); + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

`); + return; + } + const status = result.data!.isFavorite ? 'hinzugefuegt' : 'entfernt'; + await this.sendHtml(roomId, `

${folder.name}: Favorit ${status}

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

Ungueltige Nummer.

'); + } + + // Trash handlers + private async handleTrash(roomId: string, sender: string) { + const token = this.requireAuth(sender); + const result = await this.storageService.getTrash(token); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

Papierkorb ist leer.

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

Papierkorb

    '; + for (const item of items) { + const type = item.type === 'folder' ? '📁' : '📄'; + const deleted = new Date(item.deletedAt).toLocaleDateString('de-DE'); + html += `
  1. ${type} ${item.name} (geloescht: ${deleted})
  2. `; + } + html += '
'; + html += '

Nutze !wiederherstellen [nr] oder !leeren

'; + + await this.sendHtml(roomId, html); + } + + private async handleRestore(roomId: string, sender: string, numberStr: string) { + const token = this.requireAuth(sender); + const items = this.lastTrashList.get(sender); + + if (!items) { + await this.sendHtml(roomId, '

Nutze zuerst !papierkorb

'); + return; + } + + const index = parseInt(numberStr, 10) - 1; + if (isNaN(index) || index < 0 || index >= items.length) { + await this.sendHtml(roomId, '

Ungueltige Nummer.

'); + return; + } + + const item = items[index]; + const result = await this.storageService.restoreFromTrash(token, item.id, item.type); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

${item.name} wiederhergestellt.

`); + } + + private async handleEmptyTrash(roomId: string, sender: string) { + const token = this.requireAuth(sender); + const result = await this.storageService.emptyTrash(token); + + if (result.error) { + await this.sendHtml(roomId, `

Fehler: ${result.error}

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

Papierkorb geleert.

'); + } + + // Helper methods + private getFileByNumber(sender: string, numberStr: string): StorageFile | null { + const files = this.lastFilesList.get(sender); + if (!files) return null; + + const index = parseInt(numberStr, 10) - 1; + if (isNaN(index) || index < 0 || index >= files.length) return null; + + return files[index]; + } + + private getFolderByNumber(sender: string, numberStr: string): Folder | null { + const folders = this.lastFoldersList.get(sender); + if (!folders) return null; + + const index = parseInt(numberStr, 10) - 1; + if (isNaN(index) || index < 0 || index >= folders.length) return null; + + return folders[index]; + } + + private formatSize(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; + } +} diff --git a/services/matrix-storage-bot/src/config/configuration.ts b/services/matrix-storage-bot/src/config/configuration.ts new file mode 100644 index 000000000..9bb0c43cb --- /dev/null +++ b/services/matrix-storage-bot/src/config/configuration.ts @@ -0,0 +1,71 @@ +export default () => ({ + port: parseInt(process.env.PORT, 10) || 3323, + matrix: { + homeserverUrl: process.env.MATRIX_HOMESERVER_URL || 'http://localhost:8008', + accessToken: process.env.MATRIX_ACCESS_TOKEN, + allowedRooms: process.env.MATRIX_ALLOWED_ROOMS?.split(',') || [], + storagePath: process.env.MATRIX_STORAGE_PATH || './data/bot-storage.json', + }, + storage: { + backendUrl: process.env.STORAGE_BACKEND_URL || 'http://localhost:3016', + apiPrefix: process.env.STORAGE_API_PREFIX || '/api/v1', + }, + auth: { + url: process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001', + }, +}); + +export const HELP_MESSAGE = `

Storage Bot - Befehle

+ +

Authentifizierung

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

Dateien

+
    +
  • !dateien - Dateien im Root auflisten
  • +
  • !dateien [ordner-nr] - Dateien in Ordner
  • +
  • !datei [nr] - Datei-Details anzeigen
  • +
  • !download [nr] - Download-Link erhalten
  • +
  • !loeschen [nr] - Datei in Papierkorb
  • +
  • !umbenennen [nr] neuer name - Datei umbenennen
  • +
  • !verschieben [nr] [ordner-nr] - In Ordner verschieben
  • +
+ +

Ordner

+
    +
  • !ordner - Ordner im Root auflisten
  • +
  • !ordner [nr] - Unterordner anzeigen
  • +
  • !neuordner Name - Neuen Ordner erstellen
  • +
  • !neuordner Name [in-ordner-nr] - Unterordner erstellen
  • +
  • !ordnerloeschen [nr] - Ordner loeschen
  • +
+ +

Teilen

+
    +
  • !teilen [nr] - Datei teilen (Link erstellen)
  • +
  • !teilen [nr] --tage 7 - Mit Ablaufdatum
  • +
  • !teilen [nr] --passwort abc - Mit Passwort
  • +
  • !links - Alle Share-Links anzeigen
  • +
  • !linkloeschen [nr] - Share-Link loeschen
  • +
+ +

Organisation

+
    +
  • !suche Begriff - Dateien/Ordner suchen
  • +
  • !favoriten - Favoriten anzeigen
  • +
  • !fav [nr] - Favorit umschalten
  • +
  • !papierkorb - Papierkorb anzeigen
  • +
  • !wiederherstellen [nr] - Aus Papierkorb holen
  • +
  • !leeren - Papierkorb leeren
  • +
+ +

Weitere Befehle

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

Tipp: Nutze Nummern aus der zuletzt angezeigten Liste.

`; diff --git a/services/matrix-storage-bot/src/health.controller.ts b/services/matrix-storage-bot/src/health.controller.ts new file mode 100644 index 000000000..e6669d7ab --- /dev/null +++ b/services/matrix-storage-bot/src/health.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('health') +export class HealthController { + @Get() + check() { + return { status: 'ok', service: 'matrix-storage-bot' }; + } +} diff --git a/services/matrix-storage-bot/src/main.ts b/services/matrix-storage-bot/src/main.ts new file mode 100644 index 000000000..9a6668472 --- /dev/null +++ b/services/matrix-storage-bot/src/main.ts @@ -0,0 +1,10 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + const port = process.env.PORT || 3323; + await app.listen(port); + console.log(`Matrix Storage Bot running on port ${port}`); +} +bootstrap(); diff --git a/services/matrix-storage-bot/src/session/session.module.ts b/services/matrix-storage-bot/src/session/session.module.ts new file mode 100644 index 000000000..834b715eb --- /dev/null +++ b/services/matrix-storage-bot/src/session/session.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { SessionService } from './session.service'; + +@Module({ + providers: [SessionService], + exports: [SessionService], +}) +export class SessionModule {} diff --git a/services/matrix-storage-bot/src/session/session.service.ts b/services/matrix-storage-bot/src/session/session.service.ts new file mode 100644 index 000000000..f1bed7852 --- /dev/null +++ b/services/matrix-storage-bot/src/session/session.service.ts @@ -0,0 +1,90 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +interface UserSession { + token: string; + email: string; + expiresAt: Date; +} + +@Injectable() +export class SessionService { + private readonly logger = new Logger(SessionService.name); + private sessions: Map = new Map(); + private authUrl: string; + + constructor(private configService: ConfigService) { + this.authUrl = this.configService.get('auth.url') || 'http://localhost:3001'; + } + + async login( + matrixUserId: string, + email: string, + password: string + ): Promise<{ success: boolean; error?: string }> { + try { + const response = await fetch(`${this.authUrl}/api/v1/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + return { + success: false, + error: errorData.message || 'Authentifizierung fehlgeschlagen', + }; + } + + const data = await response.json(); + const token = data.accessToken || data.token; + + if (!token) { + return { success: false, error: 'Kein Token erhalten' }; + } + + // Store session (7 days expiry) + this.sessions.set(matrixUserId, { + token, + email, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }); + + this.logger.log(`User ${matrixUserId} logged in as ${email}`); + return { success: true }; + } catch (error) { + this.logger.error(`Login failed for ${matrixUserId}:`, error); + return { + success: false, + error: 'Verbindung zum Auth-Server fehlgeschlagen', + }; + } + } + + logout(matrixUserId: string): void { + this.sessions.delete(matrixUserId); + this.logger.log(`User ${matrixUserId} logged out`); + } + + getToken(matrixUserId: string): string | null { + const session = this.sessions.get(matrixUserId); + + if (!session) return null; + + if (session.expiresAt < new Date()) { + this.sessions.delete(matrixUserId); + return null; + } + + return session.token; + } + + isLoggedIn(matrixUserId: string): boolean { + return this.getToken(matrixUserId) !== null; + } + + getSessionCount(): number { + return this.sessions.size; + } +} diff --git a/services/matrix-storage-bot/src/storage/storage.module.ts b/services/matrix-storage-bot/src/storage/storage.module.ts new file mode 100644 index 000000000..2e07e3a15 --- /dev/null +++ b/services/matrix-storage-bot/src/storage/storage.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { StorageService } from './storage.service'; + +@Module({ + providers: [StorageService], + exports: [StorageService], +}) +export class StorageModule {} diff --git a/services/matrix-storage-bot/src/storage/storage.service.ts b/services/matrix-storage-bot/src/storage/storage.service.ts new file mode 100644 index 000000000..c6b3a1808 --- /dev/null +++ b/services/matrix-storage-bot/src/storage/storage.service.ts @@ -0,0 +1,208 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +export interface StorageFile { + id: string; + name: string; + originalName: string; + mimeType: string; + size: number; + parentFolderId?: string; + isFavorite: boolean; + isDeleted: boolean; + createdAt: string; + updatedAt: string; +} + +export interface Folder { + id: string; + name: string; + color?: string; + description?: string; + parentFolderId?: string; + path: string; + depth: number; + isFavorite: boolean; + isDeleted: boolean; + createdAt: string; +} + +export interface ShareLink { + id: string; + fileId?: string; + folderId?: string; + shareType: 'file' | 'folder'; + shareToken: string; + accessLevel: 'view' | 'edit' | 'download'; + password?: string; + maxDownloads?: number; + downloadCount: number; + expiresAt?: string; + isActive: boolean; + createdAt: string; +} + +export interface TrashItem { + id: string; + name: string; + type: 'file' | 'folder'; + deletedAt: string; +} + +@Injectable() +export class StorageService { + private readonly logger = new Logger(StorageService.name); + private backendUrl: string; + private apiPrefix: string; + + constructor(private configService: ConfigService) { + this.backendUrl = this.configService.get('storage.backendUrl') || 'http://localhost:3016'; + this.apiPrefix = this.configService.get('storage.apiPrefix') || '/api/v1'; + } + + private async request( + token: string, + endpoint: string, + options: RequestInit = {} + ): Promise<{ data?: T; error?: string }> { + try { + const url = `${this.backendUrl}${this.apiPrefix}${endpoint}`; + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + return { error: errorData.message || `Fehler: ${response.status}` }; + } + + const data = await response.json(); + return { data }; + } catch (error) { + this.logger.error(`Request failed: ${endpoint}`, error); + return { error: 'Verbindung zum Backend fehlgeschlagen' }; + } + } + + // File operations + async getFiles(token: string, parentFolderId?: string): Promise<{ data?: StorageFile[]; error?: string }> { + const query = parentFolderId ? `?parentFolderId=${parentFolderId}` : ''; + return this.request(token, `/files${query}`); + } + + async getFile(token: string, fileId: string): Promise<{ data?: StorageFile; error?: string }> { + return this.request(token, `/files/${fileId}`); + } + + async getDownloadUrl(token: string, fileId: string): Promise<{ data?: { url: string }; error?: string }> { + return this.request<{ url: string }>(token, `/files/${fileId}/download?url=true`); + } + + async deleteFile(token: string, fileId: string): Promise<{ error?: string }> { + return this.request(token, `/files/${fileId}`, { method: 'DELETE' }); + } + + async renameFile(token: string, fileId: string, name: string): Promise<{ data?: StorageFile; error?: string }> { + return this.request(token, `/files/${fileId}`, { + method: 'PATCH', + body: JSON.stringify({ name }), + }); + } + + async moveFile(token: string, fileId: string, parentFolderId: string | null): Promise<{ data?: StorageFile; error?: string }> { + return this.request(token, `/files/${fileId}/move`, { + method: 'PATCH', + body: JSON.stringify({ parentFolderId }), + }); + } + + async toggleFileFavorite(token: string, fileId: string): Promise<{ data?: StorageFile; error?: string }> { + return this.request(token, `/files/${fileId}/favorite`, { method: 'POST' }); + } + + // Folder operations + async getFolders(token: string, parentFolderId?: string): Promise<{ data?: Folder[]; error?: string }> { + const query = parentFolderId ? `?parentFolderId=${parentFolderId}` : ''; + return this.request(token, `/folders${query}`); + } + + async getFolder(token: string, folderId: string): Promise<{ data?: Folder; error?: string }> { + return this.request(token, `/folders/${folderId}`); + } + + async createFolder( + token: string, + name: string, + parentFolderId?: string + ): Promise<{ data?: Folder; error?: string }> { + return this.request(token, '/folders', { + method: 'POST', + body: JSON.stringify({ name, parentFolderId }), + }); + } + + async deleteFolder(token: string, folderId: string): Promise<{ error?: string }> { + return this.request(token, `/folders/${folderId}`, { method: 'DELETE' }); + } + + async toggleFolderFavorite(token: string, folderId: string): Promise<{ data?: Folder; error?: string }> { + return this.request(token, `/folders/${folderId}/favorite`, { method: 'POST' }); + } + + // Share operations + async getShares(token: string): Promise<{ data?: ShareLink[]; error?: string }> { + return this.request(token, '/shares'); + } + + async createShare( + token: string, + fileId: string, + options: { expiresInDays?: number; password?: string; maxDownloads?: number } = {} + ): Promise<{ data?: ShareLink; error?: string }> { + return this.request(token, '/shares', { + method: 'POST', + body: JSON.stringify({ fileId, accessLevel: 'download', ...options }), + }); + } + + async deleteShare(token: string, shareId: string): Promise<{ error?: string }> { + return this.request(token, `/shares/${shareId}`, { method: 'DELETE' }); + } + + // Search + async search(token: string, query: string): Promise<{ data?: { files: StorageFile[]; folders: Folder[] }; error?: string }> { + return this.request<{ files: StorageFile[]; folders: Folder[] }>(token, `/search?q=${encodeURIComponent(query)}`); + } + + // Favorites + async getFavorites(token: string): Promise<{ data?: { files: StorageFile[]; folders: Folder[] }; error?: string }> { + return this.request<{ files: StorageFile[]; folders: Folder[] }>(token, '/favorites'); + } + + // Trash + async getTrash(token: string): Promise<{ data?: TrashItem[]; error?: string }> { + return this.request(token, '/trash'); + } + + async restoreFromTrash(token: string, id: string, type: 'file' | 'folder'): Promise<{ error?: string }> { + return this.request(token, `/trash/${id}/restore?type=${type}`, { method: 'POST' }); + } + + async emptyTrash(token: string): Promise<{ error?: string }> { + return this.request(token, '/trash', { method: 'DELETE' }); + } + + async checkHealth(): Promise { + try { + const response = await fetch(`${this.backendUrl}${this.apiPrefix}/health`); + return response.ok; + } catch { + return false; + } + } +} diff --git a/services/matrix-storage-bot/tsconfig.json b/services/matrix-storage-bot/tsconfig.json new file mode 100644 index 000000000..94f1e9493 --- /dev/null +++ b/services/matrix-storage-bot/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true + } +}