mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 22:21:10 +02:00
🔧 chore(shared-vite-config): add build step for Docker compatibility
- Add build script to compile TypeScript to dist/ - Update exports to point to compiled files - Update calendar-web Dockerfile to build shared-vite-config Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4a66341e08
commit
369415527f
17 changed files with 1645 additions and 0 deletions
15
services/matrix-storage-bot/.env.example
Normal file
15
services/matrix-storage-bot/.env.example
Normal file
|
|
@ -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
|
||||
29
services/matrix-storage-bot/.gitignore
vendored
Normal file
29
services/matrix-storage-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
|
||||
225
services/matrix-storage-bot/CLAUDE.md
Normal file
225
services/matrix-storage-bot/CLAUDE.md
Normal file
|
|
@ -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
|
||||
41
services/matrix-storage-bot/Dockerfile
Normal file
41
services/matrix-storage-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 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"]
|
||||
5
services/matrix-storage-bot/nest-cli.json
Normal file
5
services/matrix-storage-bot/nest-cli.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
||||
27
services/matrix-storage-bot/package.json
Normal file
27
services/matrix-storage-bot/package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
21
services/matrix-storage-bot/src/app.module.ts
Normal file
21
services/matrix-storage-bot/src/app.module.ts
Normal file
|
|
@ -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 {}
|
||||
11
services/matrix-storage-bot/src/bot/bot.module.ts
Normal file
11
services/matrix-storage-bot/src/bot/bot.module.ts
Normal file
|
|
@ -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 {}
|
||||
845
services/matrix-storage-bot/src/bot/matrix.service.ts
Normal file
845
services/matrix-storage-bot/src/bot/matrix.service.ts
Normal file
|
|
@ -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<string, StorageFile[]> = new Map();
|
||||
private lastFoldersList: Map<string, Folder[]> = new Map();
|
||||
private lastSharesList: Map<string, ShareLink[]> = new Map();
|
||||
private lastTrashList: Map<string, TrashItem[]> = new Map();
|
||||
private currentFolder: Map<string, string | null> = new Map();
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private storageService: StorageService,
|
||||
private sessionService: SessionService
|
||||
) {}
|
||||
|
||||
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');
|
||||
this.allowedRooms = this.configService.get<string[]>('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, '<p>Erfolgreich abgemeldet.</p>');
|
||||
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,
|
||||
`<p>Unbekannter Befehl: <code>${command}</code>. Nutze <code>!help</code> fuer Hilfe.</p>`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Error handling command ${command}:`, error);
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${error.message}</p>`);
|
||||
}
|
||||
}
|
||||
|
||||
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 <code>!login email passwort</code>');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
// Auth handlers
|
||||
private async handleLogin(roomId: string, sender: string, args: string[]) {
|
||||
if (args.length < 2) {
|
||||
await this.sendHtml(roomId, '<p>Verwendung: <code>!login email passwort</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const [email, password] = args;
|
||||
const result = await this.sessionService.login(sender, email, password);
|
||||
|
||||
if (result.success) {
|
||||
await this.sendHtml(roomId, `<p>Erfolgreich angemeldet als <strong>${email}</strong></p>`);
|
||||
} else {
|
||||
await this.sendHtml(roomId, `<p>Login fehlgeschlagen: ${result.error}</p>`);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
`<h3>Storage Bot Status</h3>
|
||||
<ul>
|
||||
<li>Backend: ${backendOk ? 'Online' : 'Offline'}</li>
|
||||
<li>Angemeldet: ${loggedIn ? 'Ja' : 'Nein'}</li>
|
||||
<li>Aktive Sessions: ${sessions}</li>
|
||||
</ul>`
|
||||
);
|
||||
}
|
||||
|
||||
// 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, '<p>Ungueltige Ordner-Nummer.</p>');
|
||||
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, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const files = result.data || [];
|
||||
this.lastFilesList.set(sender, files);
|
||||
|
||||
if (files.length === 0) {
|
||||
await this.sendHtml(roomId, '<p>Keine Dateien vorhanden.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<h3>Dateien</h3><ol>';
|
||||
for (const file of files) {
|
||||
const size = this.formatSize(file.size);
|
||||
const fav = file.isFavorite ? ' ⭐' : '';
|
||||
html += `<li><strong>${file.name}</strong> (${size})${fav}</li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
html += '<p><em>Nutze <code>!datei [nr]</code> fuer Details oder <code>!download [nr]</code></em></p>';
|
||||
|
||||
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, '<p>Ungueltige Nummer. Nutze zuerst <code>!dateien</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.storageService.getFile(token, file.id);
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const f = result.data!;
|
||||
const fav = f.isFavorite ? ' ⭐' : '';
|
||||
let html = `<h3>${f.name}${fav}</h3>`;
|
||||
html += '<ul>';
|
||||
html += `<li>Typ: ${f.mimeType}</li>`;
|
||||
html += `<li>Groesse: ${this.formatSize(f.size)}</li>`;
|
||||
html += `<li>Erstellt: ${new Date(f.createdAt).toLocaleDateString('de-DE')}</li>`;
|
||||
html += `<li>Aktualisiert: ${new Date(f.updatedAt).toLocaleDateString('de-DE')}</li>`;
|
||||
html += '</ul>';
|
||||
html += `<p><em>Nutze <code>!download ${numberStr}</code> fuer Download-Link</em></p>`;
|
||||
|
||||
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, '<p>Ungueltige Nummer. Nutze zuerst <code>!dateien</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.storageService.getDownloadUrl(token, file.id);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendHtml(
|
||||
roomId,
|
||||
`<p><strong>${file.name}</strong></p><p>Download: <a href="${result.data!.url}">${result.data!.url}</a></p>`
|
||||
);
|
||||
}
|
||||
|
||||
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, '<p>Ungueltige Nummer. Nutze zuerst <code>!dateien</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.storageService.deleteFile(token, file.id);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastFilesList.delete(sender);
|
||||
await this.sendHtml(roomId, `<p><strong>${file.name}</strong> in Papierkorb verschoben.</p>`);
|
||||
}
|
||||
|
||||
private async handleRenameFile(roomId: string, sender: string, numberStr: string, newName: string) {
|
||||
if (!newName) {
|
||||
await this.sendHtml(roomId, '<p>Verwendung: <code>!umbenennen [nr] neuer name</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = this.requireAuth(sender);
|
||||
const file = this.getFileByNumber(sender, numberStr);
|
||||
|
||||
if (!file) {
|
||||
await this.sendHtml(roomId, '<p>Ungueltige Nummer. Nutze zuerst <code>!dateien</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.storageService.renameFile(token, file.id, newName);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendHtml(roomId, `<p><strong>${file.name}</strong> umbenannt zu <strong>${newName}</strong></p>`);
|
||||
}
|
||||
|
||||
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, '<p>Ungueltige Datei-Nummer.</p>');
|
||||
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, '<p>Ungueltige Ordner-Nummer. Nutze 0 oder root fuer Root.</p>');
|
||||
return;
|
||||
}
|
||||
parentFolderId = folder.id;
|
||||
folderName = folder.name;
|
||||
}
|
||||
|
||||
const result = await this.storageService.moveFile(token, file.id, parentFolderId);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendHtml(roomId, `<p><strong>${file.name}</strong> nach <strong>${folderName}</strong> verschoben.</p>`);
|
||||
}
|
||||
|
||||
// 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, '<p>Ungueltige Ordner-Nummer.</p>');
|
||||
return;
|
||||
}
|
||||
parentFolderId = folder.id;
|
||||
}
|
||||
|
||||
const result = await this.storageService.getFolders(token, parentFolderId);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const folders = result.data || [];
|
||||
this.lastFoldersList.set(sender, folders);
|
||||
|
||||
if (folders.length === 0) {
|
||||
await this.sendHtml(roomId, '<p>Keine Ordner vorhanden.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<h3>Ordner</h3><ol>';
|
||||
for (const folder of folders) {
|
||||
const fav = folder.isFavorite ? ' ⭐' : '';
|
||||
const color = folder.color ? ` [${folder.color}]` : '';
|
||||
html += `<li><strong>${folder.name}</strong>${color}${fav}</li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
html += '<p><em>Nutze <code>!dateien [nr]</code> um Dateien im Ordner zu sehen</em></p>';
|
||||
|
||||
await this.sendHtml(roomId, html);
|
||||
}
|
||||
|
||||
private async handleCreateFolder(roomId: string, sender: string, args: string[]) {
|
||||
if (args.length === 0) {
|
||||
await this.sendHtml(roomId, '<p>Verwendung: <code>!neuordner Name [in-ordner-nr]</code></p>');
|
||||
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, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastFoldersList.delete(sender);
|
||||
await this.sendHtml(roomId, `<p>Ordner <strong>${result.data!.name}</strong> erstellt.</p>`);
|
||||
}
|
||||
|
||||
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, '<p>Ungueltige Nummer. Nutze zuerst <code>!ordner</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.storageService.deleteFolder(token, folder.id);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastFoldersList.delete(sender);
|
||||
await this.sendHtml(roomId, `<p>Ordner <strong>${folder.name}</strong> in Papierkorb verschoben.</p>`);
|
||||
}
|
||||
|
||||
// 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, '<p>Ungueltige Nummer. Nutze zuerst <code>!dateien</code></p>');
|
||||
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, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const share = result.data!;
|
||||
const shareUrl = `${this.configService.get('storage.backendUrl')}/public/shares/${share.shareToken}`;
|
||||
|
||||
let html = `<p><strong>${file.name}</strong> wird geteilt:</p>`;
|
||||
html += `<p><a href="${shareUrl}">${shareUrl}</a></p>`;
|
||||
if (options.expiresInDays) html += `<p><em>Gueltig: ${options.expiresInDays} Tage</em></p>`;
|
||||
if (options.password) html += `<p><em>Passwort geschuetzt</em></p>`;
|
||||
if (options.maxDownloads) html += `<p><em>Max Downloads: ${options.maxDownloads}</em></p>`;
|
||||
|
||||
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, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const shares = result.data || [];
|
||||
this.lastSharesList.set(sender, shares);
|
||||
|
||||
if (shares.length === 0) {
|
||||
await this.sendHtml(roomId, '<p>Keine Share-Links vorhanden.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<h3>Share-Links</h3><ol>';
|
||||
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 += `<li>${share.shareType}${expires}${downloads}</li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
html += '<p><em>Nutze <code>!linkloeschen [nr]</code> zum Loeschen</em></p>';
|
||||
|
||||
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, '<p>Nutze zuerst <code>!links</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(numberStr, 10) - 1;
|
||||
if (isNaN(index) || index < 0 || index >= shares.length) {
|
||||
await this.sendHtml(roomId, '<p>Ungueltige Nummer.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const share = shares[index];
|
||||
const result = await this.storageService.deleteShare(token, share.id);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastSharesList.delete(sender);
|
||||
await this.sendHtml(roomId, '<p>Share-Link geloescht.</p>');
|
||||
}
|
||||
|
||||
// Search & Favorites
|
||||
private async handleSearch(roomId: string, sender: string, query: string) {
|
||||
if (!query) {
|
||||
await this.sendHtml(roomId, '<p>Verwendung: <code>!suche Begriff</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = this.requireAuth(sender);
|
||||
const result = await this.storageService.search(token, query);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
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, `<p>Keine Ergebnisse fuer "${query}"</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `<h3>Suchergebnisse: "${query}"</h3>`;
|
||||
|
||||
if (folders.length > 0) {
|
||||
html += '<p><strong>Ordner:</strong></p><ol>';
|
||||
for (const folder of folders) {
|
||||
html += `<li>${folder.name}</li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
html += '<p><strong>Dateien:</strong></p><ol>';
|
||||
for (const file of files) {
|
||||
html += `<li>${file.name} (${this.formatSize(file.size)})</li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
}
|
||||
|
||||
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, `<p>Fehler: ${result.error}</p>`);
|
||||
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, '<p>Keine Favoriten vorhanden.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<h3>Favoriten ⭐</h3>';
|
||||
|
||||
if (folders.length > 0) {
|
||||
html += '<p><strong>Ordner:</strong></p><ol>';
|
||||
for (const folder of folders) {
|
||||
html += `<li>${folder.name}</li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
html += '<p><strong>Dateien:</strong></p><ol>';
|
||||
for (const file of files) {
|
||||
html += `<li>${file.name}</li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
}
|
||||
|
||||
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, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
const status = result.data!.isFavorite ? 'hinzugefuegt' : 'entfernt';
|
||||
await this.sendHtml(roomId, `<p><strong>${file.name}</strong>: Favorit ${status}</p>`);
|
||||
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, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
const status = result.data!.isFavorite ? 'hinzugefuegt' : 'entfernt';
|
||||
await this.sendHtml(roomId, `<p><strong>${folder.name}</strong>: Favorit ${status}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.sendHtml(roomId, '<p>Ungueltige Nummer.</p>');
|
||||
}
|
||||
|
||||
// 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, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = result.data || [];
|
||||
this.lastTrashList.set(sender, items);
|
||||
|
||||
if (items.length === 0) {
|
||||
await this.sendHtml(roomId, '<p>Papierkorb ist leer.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<h3>Papierkorb</h3><ol>';
|
||||
for (const item of items) {
|
||||
const type = item.type === 'folder' ? '📁' : '📄';
|
||||
const deleted = new Date(item.deletedAt).toLocaleDateString('de-DE');
|
||||
html += `<li>${type} <strong>${item.name}</strong> (geloescht: ${deleted})</li>`;
|
||||
}
|
||||
html += '</ol>';
|
||||
html += '<p><em>Nutze <code>!wiederherstellen [nr]</code> oder <code>!leeren</code></em></p>';
|
||||
|
||||
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, '<p>Nutze zuerst <code>!papierkorb</code></p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const index = parseInt(numberStr, 10) - 1;
|
||||
if (isNaN(index) || index < 0 || index >= items.length) {
|
||||
await this.sendHtml(roomId, '<p>Ungueltige Nummer.</p>');
|
||||
return;
|
||||
}
|
||||
|
||||
const item = items[index];
|
||||
const result = await this.storageService.restoreFromTrash(token, item.id, item.type);
|
||||
|
||||
if (result.error) {
|
||||
await this.sendHtml(roomId, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastTrashList.delete(sender);
|
||||
await this.sendHtml(roomId, `<p><strong>${item.name}</strong> wiederhergestellt.</p>`);
|
||||
}
|
||||
|
||||
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, `<p>Fehler: ${result.error}</p>`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastTrashList.delete(sender);
|
||||
await this.sendHtml(roomId, '<p>Papierkorb geleert.</p>');
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
71
services/matrix-storage-bot/src/config/configuration.ts
Normal file
71
services/matrix-storage-bot/src/config/configuration.ts
Normal file
|
|
@ -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 = `<h2>Storage Bot - Befehle</h2>
|
||||
|
||||
<h3>Authentifizierung</h3>
|
||||
<ul>
|
||||
<li><code>!login email passwort</code> - Anmelden</li>
|
||||
<li><code>!logout</code> - Abmelden</li>
|
||||
<li><code>!status</code> - Bot-Status anzeigen</li>
|
||||
</ul>
|
||||
|
||||
<h3>Dateien</h3>
|
||||
<ul>
|
||||
<li><code>!dateien</code> - Dateien im Root auflisten</li>
|
||||
<li><code>!dateien [ordner-nr]</code> - Dateien in Ordner</li>
|
||||
<li><code>!datei [nr]</code> - Datei-Details anzeigen</li>
|
||||
<li><code>!download [nr]</code> - Download-Link erhalten</li>
|
||||
<li><code>!loeschen [nr]</code> - Datei in Papierkorb</li>
|
||||
<li><code>!umbenennen [nr] neuer name</code> - Datei umbenennen</li>
|
||||
<li><code>!verschieben [nr] [ordner-nr]</code> - In Ordner verschieben</li>
|
||||
</ul>
|
||||
|
||||
<h3>Ordner</h3>
|
||||
<ul>
|
||||
<li><code>!ordner</code> - Ordner im Root auflisten</li>
|
||||
<li><code>!ordner [nr]</code> - Unterordner anzeigen</li>
|
||||
<li><code>!neuordner Name</code> - Neuen Ordner erstellen</li>
|
||||
<li><code>!neuordner Name [in-ordner-nr]</code> - Unterordner erstellen</li>
|
||||
<li><code>!ordnerloeschen [nr]</code> - Ordner loeschen</li>
|
||||
</ul>
|
||||
|
||||
<h3>Teilen</h3>
|
||||
<ul>
|
||||
<li><code>!teilen [nr]</code> - Datei teilen (Link erstellen)</li>
|
||||
<li><code>!teilen [nr] --tage 7</code> - Mit Ablaufdatum</li>
|
||||
<li><code>!teilen [nr] --passwort abc</code> - Mit Passwort</li>
|
||||
<li><code>!links</code> - Alle Share-Links anzeigen</li>
|
||||
<li><code>!linkloeschen [nr]</code> - Share-Link loeschen</li>
|
||||
</ul>
|
||||
|
||||
<h3>Organisation</h3>
|
||||
<ul>
|
||||
<li><code>!suche Begriff</code> - Dateien/Ordner suchen</li>
|
||||
<li><code>!favoriten</code> - Favoriten anzeigen</li>
|
||||
<li><code>!fav [nr]</code> - Favorit umschalten</li>
|
||||
<li><code>!papierkorb</code> - Papierkorb anzeigen</li>
|
||||
<li><code>!wiederherstellen [nr]</code> - Aus Papierkorb holen</li>
|
||||
<li><code>!leeren</code> - Papierkorb leeren</li>
|
||||
</ul>
|
||||
|
||||
<h3>Weitere Befehle</h3>
|
||||
<ul>
|
||||
<li><code>!help</code> - Diese Hilfe anzeigen</li>
|
||||
</ul>
|
||||
|
||||
<p><em>Tipp: Nutze Nummern aus der zuletzt angezeigten Liste.</em></p>`;
|
||||
9
services/matrix-storage-bot/src/health.controller.ts
Normal file
9
services/matrix-storage-bot/src/health.controller.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
@Get()
|
||||
check() {
|
||||
return { status: 'ok', service: 'matrix-storage-bot' };
|
||||
}
|
||||
}
|
||||
10
services/matrix-storage-bot/src/main.ts
Normal file
10
services/matrix-storage-bot/src/main.ts
Normal file
|
|
@ -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();
|
||||
|
|
@ -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-storage-bot/src/session/session.service.ts
Normal file
90
services/matrix-storage-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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
@Module({
|
||||
providers: [StorageService],
|
||||
exports: [StorageService],
|
||||
})
|
||||
export class StorageModule {}
|
||||
208
services/matrix-storage-bot/src/storage/storage.service.ts
Normal file
208
services/matrix-storage-bot/src/storage/storage.service.ts
Normal file
|
|
@ -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<string>('storage.backendUrl') || 'http://localhost:3016';
|
||||
this.apiPrefix = this.configService.get<string>('storage.apiPrefix') || '/api/v1';
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
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<StorageFile[]>(token, `/files${query}`);
|
||||
}
|
||||
|
||||
async getFile(token: string, fileId: string): Promise<{ data?: StorageFile; error?: string }> {
|
||||
return this.request<StorageFile>(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<StorageFile>(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<StorageFile>(token, `/files/${fileId}/move`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ parentFolderId }),
|
||||
});
|
||||
}
|
||||
|
||||
async toggleFileFavorite(token: string, fileId: string): Promise<{ data?: StorageFile; error?: string }> {
|
||||
return this.request<StorageFile>(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<Folder[]>(token, `/folders${query}`);
|
||||
}
|
||||
|
||||
async getFolder(token: string, folderId: string): Promise<{ data?: Folder; error?: string }> {
|
||||
return this.request<Folder>(token, `/folders/${folderId}`);
|
||||
}
|
||||
|
||||
async createFolder(
|
||||
token: string,
|
||||
name: string,
|
||||
parentFolderId?: string
|
||||
): Promise<{ data?: Folder; error?: string }> {
|
||||
return this.request<Folder>(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<Folder>(token, `/folders/${folderId}/favorite`, { method: 'POST' });
|
||||
}
|
||||
|
||||
// Share operations
|
||||
async getShares(token: string): Promise<{ data?: ShareLink[]; error?: string }> {
|
||||
return this.request<ShareLink[]>(token, '/shares');
|
||||
}
|
||||
|
||||
async createShare(
|
||||
token: string,
|
||||
fileId: string,
|
||||
options: { expiresInDays?: number; password?: string; maxDownloads?: number } = {}
|
||||
): Promise<{ data?: ShareLink; error?: string }> {
|
||||
return this.request<ShareLink>(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<TrashItem[]>(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<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.backendUrl}${this.apiPrefix}/health`);
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
services/matrix-storage-bot/tsconfig.json
Normal file
22
services/matrix-storage-bot/tsconfig.json
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue