From a341aa1b1373689207798e6bf9b639f8764a01fd Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:17:14 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A5=20remove:=20Telegram=20bots=20-=20?= =?UTF-8?q?Matrix-only=20strategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove all 6 Telegram bot services to focus on Matrix as the sole messaging platform for full UI/UX control and DSGVO compliance. Removed services: - telegram-nutriphi-bot - telegram-ollama-bot - telegram-project-doc-bot - telegram-stats-bot - telegram-todo-bot - telegram-zitare-bot Also: - Remove Telegram bot scripts from package.json - Remove telegram-stats-bot from docker-compose.macmini.yml - Disable Watchtower Telegram notifications - Remove Telegram devlog - Add comprehensive MATRIX_BOT_ARCHITECTURE.md documentation The Matrix-only approach provides: - Full control over user experience - Complete DSGVO compliance (all data on own servers) - No dependency on third-party platforms - Unified command patterns across all bots --- .../002-infrastructure-audit-improvements.md | 602 ++++++++++ .../devlog/2026-01-27-telegram-matrix-bots.md | 396 ------ docker-compose.macmini.yml | 40 +- docs/MATRIX_BOT_ARCHITECTURE.md | 975 +++++++++++++++ .../003-infrastructure-audit-improvements.md | 74 ++ package.json | 16 - pnpm-lock.yaml | 1069 ++++++++--------- services/telegram-nutriphi-bot/.env.example | 12 - services/telegram-nutriphi-bot/CLAUDE.md | 294 ----- .../telegram-nutriphi-bot/drizzle.config.ts | 9 - services/telegram-nutriphi-bot/nest-cli.json | 8 - services/telegram-nutriphi-bot/package.json | 43 - .../src/analysis/analysis.module.ts | 8 - .../src/analysis/gemini.service.ts | 175 --- .../telegram-nutriphi-bot/src/app.module.ts | 27 - .../src/bot/bot.module.ts | 12 - .../src/bot/bot.update.ts | 513 -------- .../src/config/configuration.ts | 35 - .../src/database/database.module.ts | 24 - .../src/database/schema.ts | 93 -- .../src/goals/goals.module.ts | 8 - .../src/goals/goals.service.ts | 56 - .../src/health.controller.ts | 13 - services/telegram-nutriphi-bot/src/main.ts | 18 - .../src/meals/meals.module.ts | 8 - .../src/meals/meals.service.ts | 159 --- .../src/stats/stats.module.ts | 8 - .../src/stats/stats.service.ts | 194 --- services/telegram-nutriphi-bot/tsconfig.json | 22 - services/telegram-ollama-bot/CLAUDE.md | 130 -- services/telegram-ollama-bot/Dockerfile | 44 - services/telegram-ollama-bot/nest-cli.json | 8 - services/telegram-ollama-bot/package.json | 35 - .../telegram-ollama-bot/src/app.module.ts | 27 - .../telegram-ollama-bot/src/bot/bot.module.ts | 9 - .../telegram-ollama-bot/src/bot/bot.update.ts | 278 ----- .../src/config/configuration.ts | 25 - .../src/health.controller.ts | 21 - services/telegram-ollama-bot/src/main.ts | 19 - .../src/ollama/ollama.module.ts | 8 - .../src/ollama/ollama.service.ts | 171 --- services/telegram-ollama-bot/tsconfig.json | 22 - .../telegram-project-doc-bot/.env.example | 29 - services/telegram-project-doc-bot/CLAUDE.md | 245 ---- services/telegram-project-doc-bot/Dockerfile | 41 - .../drizzle.config.ts | 10 - .../telegram-project-doc-bot/nest-cli.json | 8 - .../telegram-project-doc-bot/package.json | 44 - .../src/app.module.ts | 27 - .../src/bot/bot.module.ts | 11 - .../src/bot/bot.update.ts | 490 -------- .../src/config/configuration.ts | 58 - .../src/database/database.module.ts | 24 - .../src/database/schema.ts | 94 -- .../src/generation/generation.module.ts | 8 - .../src/generation/generation.service.ts | 206 ---- .../src/health.controller.ts | 13 - services/telegram-project-doc-bot/src/main.ts | 18 - .../src/media/media.module.ts | 11 - .../src/media/media.service.ts | 164 --- .../src/media/storage.service.ts | 77 -- .../src/project/project.module.ts | 8 - .../src/project/project.service.ts | 90 -- .../src/transcription/transcription.module.ts | 8 - .../transcription/transcription.service.ts | 116 -- .../telegram-project-doc-bot/tsconfig.json | 22 - services/telegram-stats-bot/.env.example | 15 - services/telegram-stats-bot/CLAUDE.md | 150 --- services/telegram-stats-bot/Dockerfile | 58 - services/telegram-stats-bot/nest-cli.json | 8 - services/telegram-stats-bot/package.json | 38 - .../src/analytics/analytics.module.ts | 11 - .../src/analytics/analytics.service.ts | 135 --- .../src/analytics/formatters.ts | 305 ----- services/telegram-stats-bot/src/app.module.ts | 36 - .../telegram-stats-bot/src/bot/bot.module.ts | 12 - .../telegram-stats-bot/src/bot/bot.service.ts | 40 - .../telegram-stats-bot/src/bot/bot.update.ts | 87 -- .../src/config/configuration.ts | 71 -- .../src/health.controller.ts | 13 - services/telegram-stats-bot/src/main.ts | 18 - .../src/scheduler/report.scheduler.ts | 66 - .../src/scheduler/scheduler.module.ts | 10 - .../src/umami/umami.module.ts | 8 - .../src/umami/umami.service.ts | 135 --- .../src/users/users.module.ts | 8 - .../src/users/users.service.ts | 120 -- services/telegram-stats-bot/tsconfig.json | 28 - services/telegram-todo-bot/.env.example | 14 - services/telegram-todo-bot/CLAUDE.md | 209 ---- services/telegram-todo-bot/drizzle.config.ts | 9 - services/telegram-todo-bot/nest-cli.json | 8 - services/telegram-todo-bot/package.json | 43 - services/telegram-todo-bot/src/app.module.ts | 29 - .../telegram-todo-bot/src/bot/bot.module.ts | 10 - .../telegram-todo-bot/src/bot/bot.update.ts | 460 ------- .../src/config/configuration.ts | 15 - .../src/database/database.module.ts | 24 - .../telegram-todo-bot/src/database/schema.ts | 23 - .../src/health.controller.ts | 13 - services/telegram-todo-bot/src/main.ts | 18 - .../src/scheduler/reminder.scheduler.ts | 108 -- .../src/scheduler/scheduler.module.ts | 11 - .../src/todo-client/todo-client.module.ts | 8 - .../src/todo-client/todo-client.service.ts | 121 -- .../src/todo-client/types.ts | 60 - .../telegram-todo-bot/src/user/user.module.ts | 8 - .../src/user/user.service.ts | 226 ---- services/telegram-todo-bot/tsconfig.json | 23 - services/telegram-zitare-bot/.env.example | 8 - services/telegram-zitare-bot/CLAUDE.md | 161 --- .../telegram-zitare-bot/drizzle.config.ts | 9 - services/telegram-zitare-bot/nest-cli.json | 10 - services/telegram-zitare-bot/package.json | 43 - .../telegram-zitare-bot/src/app.module.ts | 29 - .../telegram-zitare-bot/src/bot/bot.module.ts | 10 - .../telegram-zitare-bot/src/bot/bot.update.ts | 242 ---- .../src/config/configuration.ts | 9 - .../src/database/database.module.ts | 24 - .../src/database/schema.ts | 44 - .../src/health.controller.ts | 13 - services/telegram-zitare-bot/src/main.ts | 18 - .../src/quotes/data/authors.json | 44 - .../src/quotes/data/quotes.json | 166 --- .../src/quotes/quotes.module.ts | 8 - .../src/quotes/quotes.service.ts | 106 -- .../telegram-zitare-bot/src/quotes/types.ts | 17 - .../src/scheduler/daily.scheduler.ts | 61 - .../src/scheduler/scheduler.module.ts | 11 - .../src/user/user.module.ts | 8 - .../src/user/user.service.ts | 146 --- services/telegram-zitare-bot/tsconfig.json | 23 - 132 files changed, 2133 insertions(+), 9419 deletions(-) create mode 100644 apps/manacore/apps/landing/src/content/blueprints/002-infrastructure-audit-improvements.md delete mode 100644 apps/manacore/apps/landing/src/content/devlog/2026-01-27-telegram-matrix-bots.md create mode 100644 docs/MATRIX_BOT_ARCHITECTURE.md create mode 100644 docs/decisions/003-infrastructure-audit-improvements.md delete mode 100644 services/telegram-nutriphi-bot/.env.example delete mode 100644 services/telegram-nutriphi-bot/CLAUDE.md delete mode 100644 services/telegram-nutriphi-bot/drizzle.config.ts delete mode 100644 services/telegram-nutriphi-bot/nest-cli.json delete mode 100644 services/telegram-nutriphi-bot/package.json delete mode 100644 services/telegram-nutriphi-bot/src/analysis/analysis.module.ts delete mode 100644 services/telegram-nutriphi-bot/src/analysis/gemini.service.ts delete mode 100644 services/telegram-nutriphi-bot/src/app.module.ts delete mode 100644 services/telegram-nutriphi-bot/src/bot/bot.module.ts delete mode 100644 services/telegram-nutriphi-bot/src/bot/bot.update.ts delete mode 100644 services/telegram-nutriphi-bot/src/config/configuration.ts delete mode 100644 services/telegram-nutriphi-bot/src/database/database.module.ts delete mode 100644 services/telegram-nutriphi-bot/src/database/schema.ts delete mode 100644 services/telegram-nutriphi-bot/src/goals/goals.module.ts delete mode 100644 services/telegram-nutriphi-bot/src/goals/goals.service.ts delete mode 100644 services/telegram-nutriphi-bot/src/health.controller.ts delete mode 100644 services/telegram-nutriphi-bot/src/main.ts delete mode 100644 services/telegram-nutriphi-bot/src/meals/meals.module.ts delete mode 100644 services/telegram-nutriphi-bot/src/meals/meals.service.ts delete mode 100644 services/telegram-nutriphi-bot/src/stats/stats.module.ts delete mode 100644 services/telegram-nutriphi-bot/src/stats/stats.service.ts delete mode 100644 services/telegram-nutriphi-bot/tsconfig.json delete mode 100644 services/telegram-ollama-bot/CLAUDE.md delete mode 100644 services/telegram-ollama-bot/Dockerfile delete mode 100644 services/telegram-ollama-bot/nest-cli.json delete mode 100644 services/telegram-ollama-bot/package.json delete mode 100644 services/telegram-ollama-bot/src/app.module.ts delete mode 100644 services/telegram-ollama-bot/src/bot/bot.module.ts delete mode 100644 services/telegram-ollama-bot/src/bot/bot.update.ts delete mode 100644 services/telegram-ollama-bot/src/config/configuration.ts delete mode 100644 services/telegram-ollama-bot/src/health.controller.ts delete mode 100644 services/telegram-ollama-bot/src/main.ts delete mode 100644 services/telegram-ollama-bot/src/ollama/ollama.module.ts delete mode 100644 services/telegram-ollama-bot/src/ollama/ollama.service.ts delete mode 100644 services/telegram-ollama-bot/tsconfig.json delete mode 100644 services/telegram-project-doc-bot/.env.example delete mode 100644 services/telegram-project-doc-bot/CLAUDE.md delete mode 100644 services/telegram-project-doc-bot/Dockerfile delete mode 100644 services/telegram-project-doc-bot/drizzle.config.ts delete mode 100644 services/telegram-project-doc-bot/nest-cli.json delete mode 100644 services/telegram-project-doc-bot/package.json delete mode 100644 services/telegram-project-doc-bot/src/app.module.ts delete mode 100644 services/telegram-project-doc-bot/src/bot/bot.module.ts delete mode 100644 services/telegram-project-doc-bot/src/bot/bot.update.ts delete mode 100644 services/telegram-project-doc-bot/src/config/configuration.ts delete mode 100644 services/telegram-project-doc-bot/src/database/database.module.ts delete mode 100644 services/telegram-project-doc-bot/src/database/schema.ts delete mode 100644 services/telegram-project-doc-bot/src/generation/generation.module.ts delete mode 100644 services/telegram-project-doc-bot/src/generation/generation.service.ts delete mode 100644 services/telegram-project-doc-bot/src/health.controller.ts delete mode 100644 services/telegram-project-doc-bot/src/main.ts delete mode 100644 services/telegram-project-doc-bot/src/media/media.module.ts delete mode 100644 services/telegram-project-doc-bot/src/media/media.service.ts delete mode 100644 services/telegram-project-doc-bot/src/media/storage.service.ts delete mode 100644 services/telegram-project-doc-bot/src/project/project.module.ts delete mode 100644 services/telegram-project-doc-bot/src/project/project.service.ts delete mode 100644 services/telegram-project-doc-bot/src/transcription/transcription.module.ts delete mode 100644 services/telegram-project-doc-bot/src/transcription/transcription.service.ts delete mode 100644 services/telegram-project-doc-bot/tsconfig.json delete mode 100644 services/telegram-stats-bot/.env.example delete mode 100644 services/telegram-stats-bot/CLAUDE.md delete mode 100644 services/telegram-stats-bot/Dockerfile delete mode 100644 services/telegram-stats-bot/nest-cli.json delete mode 100644 services/telegram-stats-bot/package.json delete mode 100644 services/telegram-stats-bot/src/analytics/analytics.module.ts delete mode 100644 services/telegram-stats-bot/src/analytics/analytics.service.ts delete mode 100644 services/telegram-stats-bot/src/analytics/formatters.ts delete mode 100644 services/telegram-stats-bot/src/app.module.ts delete mode 100644 services/telegram-stats-bot/src/bot/bot.module.ts delete mode 100644 services/telegram-stats-bot/src/bot/bot.service.ts delete mode 100644 services/telegram-stats-bot/src/bot/bot.update.ts delete mode 100644 services/telegram-stats-bot/src/config/configuration.ts delete mode 100644 services/telegram-stats-bot/src/health.controller.ts delete mode 100644 services/telegram-stats-bot/src/main.ts delete mode 100644 services/telegram-stats-bot/src/scheduler/report.scheduler.ts delete mode 100644 services/telegram-stats-bot/src/scheduler/scheduler.module.ts delete mode 100644 services/telegram-stats-bot/src/umami/umami.module.ts delete mode 100644 services/telegram-stats-bot/src/umami/umami.service.ts delete mode 100644 services/telegram-stats-bot/src/users/users.module.ts delete mode 100644 services/telegram-stats-bot/src/users/users.service.ts delete mode 100644 services/telegram-stats-bot/tsconfig.json delete mode 100644 services/telegram-todo-bot/.env.example delete mode 100644 services/telegram-todo-bot/CLAUDE.md delete mode 100644 services/telegram-todo-bot/drizzle.config.ts delete mode 100644 services/telegram-todo-bot/nest-cli.json delete mode 100644 services/telegram-todo-bot/package.json delete mode 100644 services/telegram-todo-bot/src/app.module.ts delete mode 100644 services/telegram-todo-bot/src/bot/bot.module.ts delete mode 100644 services/telegram-todo-bot/src/bot/bot.update.ts delete mode 100644 services/telegram-todo-bot/src/config/configuration.ts delete mode 100644 services/telegram-todo-bot/src/database/database.module.ts delete mode 100644 services/telegram-todo-bot/src/database/schema.ts delete mode 100644 services/telegram-todo-bot/src/health.controller.ts delete mode 100644 services/telegram-todo-bot/src/main.ts delete mode 100644 services/telegram-todo-bot/src/scheduler/reminder.scheduler.ts delete mode 100644 services/telegram-todo-bot/src/scheduler/scheduler.module.ts delete mode 100644 services/telegram-todo-bot/src/todo-client/todo-client.module.ts delete mode 100644 services/telegram-todo-bot/src/todo-client/todo-client.service.ts delete mode 100644 services/telegram-todo-bot/src/todo-client/types.ts delete mode 100644 services/telegram-todo-bot/src/user/user.module.ts delete mode 100644 services/telegram-todo-bot/src/user/user.service.ts delete mode 100644 services/telegram-todo-bot/tsconfig.json delete mode 100644 services/telegram-zitare-bot/.env.example delete mode 100644 services/telegram-zitare-bot/CLAUDE.md delete mode 100644 services/telegram-zitare-bot/drizzle.config.ts delete mode 100644 services/telegram-zitare-bot/nest-cli.json delete mode 100644 services/telegram-zitare-bot/package.json delete mode 100644 services/telegram-zitare-bot/src/app.module.ts delete mode 100644 services/telegram-zitare-bot/src/bot/bot.module.ts delete mode 100644 services/telegram-zitare-bot/src/bot/bot.update.ts delete mode 100644 services/telegram-zitare-bot/src/config/configuration.ts delete mode 100644 services/telegram-zitare-bot/src/database/database.module.ts delete mode 100644 services/telegram-zitare-bot/src/database/schema.ts delete mode 100644 services/telegram-zitare-bot/src/health.controller.ts delete mode 100644 services/telegram-zitare-bot/src/main.ts delete mode 100644 services/telegram-zitare-bot/src/quotes/data/authors.json delete mode 100644 services/telegram-zitare-bot/src/quotes/data/quotes.json delete mode 100644 services/telegram-zitare-bot/src/quotes/quotes.module.ts delete mode 100644 services/telegram-zitare-bot/src/quotes/quotes.service.ts delete mode 100644 services/telegram-zitare-bot/src/quotes/types.ts delete mode 100644 services/telegram-zitare-bot/src/scheduler/daily.scheduler.ts delete mode 100644 services/telegram-zitare-bot/src/scheduler/scheduler.module.ts delete mode 100644 services/telegram-zitare-bot/src/user/user.module.ts delete mode 100644 services/telegram-zitare-bot/src/user/user.service.ts delete mode 100644 services/telegram-zitare-bot/tsconfig.json diff --git a/apps/manacore/apps/landing/src/content/blueprints/002-infrastructure-audit-improvements.md b/apps/manacore/apps/landing/src/content/blueprints/002-infrastructure-audit-improvements.md new file mode 100644 index 000000000..7e4b751fc --- /dev/null +++ b/apps/manacore/apps/landing/src/content/blueprints/002-infrastructure-audit-improvements.md @@ -0,0 +1,602 @@ +--- +title: 'Infrastruktur-Audit & Architektur-Verbesserungen' +description: 'Umfassende Analyse der aktuellen Mac Mini Infrastruktur mit detaillierten Verbesserungsvorschlägen für Port-Verteilung, Volumes, Dependencies und Service-Konsolidierung vor der K8s-Migration.' +date: 2026-01-31 +author: 'Till Schneider' +category: 'infrastructure' +status: 'accepted' +tags: + [ + 'docker', + 'infrastructure', + 'audit', + 'ports', + 'volumes', + 'dependencies', + 'migration', + 'mac-mini', + 'architecture', + ] +featured: true +readTime: 25 +relatedBlueprints: ['001-mana-cluster-federation-architecture'] +decisionDate: 2026-01-31 +--- + +## Executive Summary + +Dieses Dokument analysiert den aktuellen Zustand der ManaCore-Infrastruktur auf dem Mac Mini und identifiziert Verbesserungspotentiale vor der geplanten Migration zu einem selbstheilenden K8s-Cluster. Die Analyse umfasst **52 Docker-Container** plus **3 native macOS-Services**. + +## Aktueller Infrastruktur-Zustand + +### Service-Kategorien Übersicht + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ ManaCore Infrastructure │ +│ (Mac Mini M4 Pro) │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ PostgreSQL │ │ Redis │ │ MinIO │ │ +│ │ :5432 │ │ :6379 │ │ :9000/:9001 │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +│ │ │ │ │ +│ └────────────────┼────────────────┘ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ mana-core-auth (:3001) │ │ +│ │ (Central Authentication) │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────┼────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ API Gateway │ │ mana-search │ │ App Suite │ │ +│ │ (:3030) │ │ (:3021) │ │ (10 Apps) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ Matrix Stack (14 Services) │ │ +│ │ Synapse │ Element │ 10 Bots │ Custom Web Client │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────────────┐ │ +│ │ Monitoring & Tools (8 Services) │ │ +│ │ VictoriaMetrics │ Grafana │ Exporters │ Umami │ n8n │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Vollständige Service-Inventar + +#### Tier 0: Infrastruktur (3 Services) + +| Service | Container | Port(s) | Volume | Bemerkung | +| ------------- | ----------------- | ---------- | ----------------- | ----------------------------- | +| PostgreSQL 16 | manacore-postgres | 5432 | manacore-postgres | Alle DBs in einer Instanz | +| Redis 7 | manacore-redis | 6379 | manacore-redis | Session-Cache + Pub/Sub | +| MinIO | manacore-minio | 9000, 9001 | manacore-minio | S3-kompatibler Object Storage | + +#### Tier 1: Core Auth (1 Service) + +| Service | Container | Port | Dependencies | Bemerkung | +| -------------- | -------------- | ---- | --------------- | ----------------------- | +| mana-core-auth | mana-core-auth | 3001 | postgres, redis | Better Auth + EdDSA JWT | + +#### Tier 2: Gateway & Suche (3 Services) + +| Service | Container | Port | Dependencies | Bemerkung | +| ----------- | ---------------- | ------------- | ----------------------------- | --------------------- | +| API Gateway | mana-api-gateway | 3030 | auth, search, postgres, redis | Monetarisierung | +| SearXNG | mana-searxng | (intern 8080) | - | Meta-Suchmaschine | +| mana-search | mana-search | 3021 | searxng, redis | NestJS Search Service | + +#### Tier 3: App-Backends (10 Services) + +| App | Container | Port | Datenbank | Spezial | +| --------- | ----------------- | ---- | --------- | ------------------ | +| Chat | chat-backend | 3002 | chat | Ollama-Integration | +| Presi | presi-backend | 3008 | presi | - | +| Contacts | contacts-backend | 3015 | contacts | MinIO (Fotos) | +| Calendar | calendar-backend | 3016 | calendar | - | +| Clock | clock-backend | 3017 | clock | - | +| Todo | todo-backend | 3018 | todo | - | +| Storage | storage-backend | 3019 | storage | MinIO (Dateien) | +| NutriPhi | nutriphi-backend | 3023 | nutriphi | Gemini API | +| SkillTree | skilltree-backend | 3024 | skilltree | - | + +#### Tier 4: Web-Frontends (11 Services) + +| App | Container | Port | Backend-Port | +| ------------------ | -------------- | ---- | ------------ | +| ManaCore Dashboard | manacore-web | 5173 | - | +| Chat | chat-web | 3000 | 3002 | +| Presi | presi-web | 5178 | 3008 | +| Matrix | matrix-web | 5180 | 8008 | +| Contacts | contacts-web | 5184 | 3015 | +| Storage | storage-web | 5185 | 3019 | +| Calendar | calendar-web | 5186 | 3016 | +| Clock | clock-web | 5187 | 3017 | +| Todo | todo-web | 5188 | 3018 | +| NutriPhi | nutriphi-web | 5189 | 3023 | +| LLM Playground | llm-playground | 5190 | - | +| SkillTree | skilltree-web | 5195 | 3024 | + +#### Tier 5: Matrix Stack (14 Services) + +| Service | Container | Port | Funktion | +| --------------- | ------------------------------- | ---------- | ----------------------- | +| Synapse | manacore-synapse | 8008, 9002 | Homeserver | +| Element Web | manacore-element | 8087 | Standard Client | +| Matrix Web | manacore-matrix-web | 5180 | Custom SvelteKit Client | +| Mana Bot | manacore-matrix-mana-bot | 3310 | Unified Gateway Bot | +| Ollama Bot | manacore-matrix-ollama-bot | 3311 | AI Chat | +| Stats Bot | manacore-matrix-stats-bot | 3312 | Analytics | +| Project Doc Bot | manacore-matrix-project-doc-bot | 3313 | Dokumentation | +| Todo Bot | manacore-matrix-todo-bot | 3314 | Task Management | +| Calendar Bot | manacore-matrix-calendar-bot | 3315 | Termine | +| NutriPhi Bot | manacore-matrix-nutriphi-bot | 3316 | Nutrition | +| Zitare Bot | manacore-matrix-zitare-bot | 3317 | Quotes | +| Clock Bot | manacore-matrix-clock-bot | 3318 | Time Tracking | +| TTS Bot | manacore-matrix-tts-bot | 3033 | Text-to-Speech | + +#### Tier 6: Monitoring & Tools (8 Services) + +| Service | Container | Port | Funktion | +| ------------------ | --------------------------- | ---- | ------------------- | +| VictoriaMetrics | manacore-victoriametrics | 8428 | Metriken-DB | +| Grafana | manacore-grafana | 3100 | Dashboards | +| Pushgateway | manacore-pushgateway | 9091 | Batch-Metriken | +| Node Exporter | manacore-node-exporter | 9100 | Host-Metriken | +| cAdvisor | manacore-cadvisor | 8080 | Container-Metriken | +| Postgres Exporter | manacore-postgres-exporter | 9187 | DB-Metriken | +| Redis Exporter | manacore-redis-exporter | 9121 | Cache-Metriken | +| Umami | manacore-umami | 3200 | Web Analytics | +| n8n | manacore-n8n | 5678 | Workflow Automation | +| Telegram Stats Bot | manacore-telegram-stats-bot | 3300 | Telegram Reports | + +#### Native macOS Services (3 Services) + +| Service | Port | Zugriff | Bemerkung | +| -------------- | ----- | -------------------- | ----------------------- | +| Ollama | 11434 | host.docker.internal | Gemma 3 4B, LLM Runtime | +| mana-image-gen | 3025 | host.docker.internal | Stable Diffusion | +| mana-tts | 3022 | host.docker.internal | Kokoro TTS | +| mana-stt | 3020 | host.docker.internal | Whisper STT | + +#### Auto-Update (1 Service) + +| Service | Container | Funktion | +| ---------- | ------------------- | ----------------------------- | +| Watchtower | manacore-watchtower | Auto-Update + Telegram Notify | + +--- + +## Identifizierte Probleme + +### 1. Port-Chaos + +Die aktuelle Port-Verteilung folgt keinem erkennbaren Schema: + +``` +AKTUELL (Chaotisch): +┌──────────────────────────────────────────────────────────────────┐ +│ 3000 chat-web │ Web Frontend im Backend-Bereich! │ +│ 3001 mana-core-auth │ OK │ +│ 3002 chat-backend │ OK │ +│ 3003-3007 LÜCKE │ Verschwendet │ +│ 3008 presi-backend │ OK │ +│ 3009-3014 LÜCKE │ Verschwendet │ +│ 3015-3019 Backends │ OK │ +│ 3020-3022 AI Services │ OK (nativ) │ +│ 3023-3024 Backends │ OK │ +│ 3025 mana-image-gen │ OK (nativ) │ +│ 3026-3029 LÜCKE │ │ +│ 3030 api-gateway │ OK │ +│ 3031-3032 LÜCKE │ │ +│ 3033 matrix-tts-bot │ Bot im falschen Bereich! │ +│ 3100 grafana │ Monitoring im Backend-Bereich! │ +│ 3200 umami │ Warum 3200? │ +│ 3300-3301 Telegram │ Telegram im 33xx Bereich │ +│ 3310-3318 Matrix Bots │ OK, aber 3033 fehlt! │ +├──────────────────────────────────────────────────────────────────┤ +│ 5173-5195 Web UIs │ Größtenteils OK, aber Lücken │ +│ 5678 n8n │ Warum hier? │ +├──────────────────────────────────────────────────────────────────┤ +│ 8008 synapse │ Matrix Homeserver │ +│ 8080 cadvisor │ Konflikt mit SearXNG intern! │ +│ 8087 element-web │ Random? │ +│ 8428 victoriametrics │ OK │ +├──────────────────────────────────────────────────────────────────┤ +│ 9000-9001 minio │ OK │ +│ 9002 synapse-metrics │ Ausweichport wegen MinIO │ +│ 9091 pushgateway │ OK │ +│ 9100 node-exporter │ OK │ +│ 9121 redis-exporter │ OK │ +│ 9187 postgres-exporter │ OK │ +└──────────────────────────────────────────────────────────────────┘ +``` + +**Probleme:** + +- `chat-web` auf Port 3000 (sollte bei 51xx sein) +- `grafana` auf 3100 (sollte bei 8xxx oder 9xxx sein) +- `matrix-tts-bot` auf 3033 (sollte bei 331x sein) +- Große Lücken verschwenden Adressraum +- Keine klare Trennung zwischen Service-Typen + +### 2. Inkonsistente Container-Benennung + +``` +AKTUELL: +manacore-postgres ✓ Konsistent +manacore-redis ✓ Konsistent +mana-core-auth ✗ Bindestrich-Variante +mana-api-gateway ✗ Bindestrich-Variante +chat-backend ✗ Kein Präfix +chat-web ✗ Kein Präfix +manacore-matrix-mana-bot ✓ Konsistent +manacore-synapse ✓ Konsistent +``` + +### 3. Nicht-optimale Dependencies + +Einige Services haben übermäßige Abhängigkeiten: + +```yaml +# AKTUELL: api-gateway wartet auf 4 Services +api-gateway: + depends_on: + mana-core-auth: # Notwendig + mana-search: # Nur für Search-Features + postgres: # Kann später verbinden + redis: # Kann später verbinden +``` + +### 4. Duplizierte Bot-Funktionalität + +Viele Matrix-Bots teilen ähnlichen Code: + +- `matrix-todo-bot` und `matrix-calendar-bot` haben ähnliche Reminder-Logik +- `matrix-nutriphi-bot` und `matrix-zitare-bot` sind Wrapper für Backends +- `matrix-mana-bot` sollte theoretisch alle anderen ersetzen + +### 5. Volume-Fragmentierung + +```yaml +# 20 separate Volumes, davon 10 nur für Matrix-Bots: +volumes: + matrix_mana_bot_data: + matrix_ollama_bot_data: + matrix_stats_bot_data: + matrix_project_doc_bot_data: + matrix_calendar_bot_data: + matrix_todo_bot_data: + matrix_nutriphi_bot_data: + matrix_zitare_bot_data: + matrix_clock_bot_data: + matrix_tts_bot_data: +``` + +--- + +## Verbesserungsvorschläge + +### 1. Neues Port-Schema + +Ein strukturiertes Port-Schema für bessere Übersicht: + +``` +NEU (Strukturiert): +┌─────────────────────────────────────────────────────────────────────┐ +│ PORT-ALLOCATION-SCHEMA │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ 3000-3099: Core Services & Backends │ +│ ├── 3001: mana-core-auth (Auth) │ +│ ├── 3010: api-gateway (Gateway) │ +│ ├── 3020: mana-search (Search) │ +│ ├── 3021: mana-stt (Speech-to-Text) │ +│ ├── 3022: mana-tts (Text-to-Speech) │ +│ ├── 3025: mana-image-gen (Image Generation) │ +│ ├── 3030: chat-backend │ +│ ├── 3031: todo-backend │ +│ ├── 3032: calendar-backend │ +│ ├── 3033: clock-backend │ +│ ├── 3034: contacts-backend │ +│ ├── 3035: storage-backend │ +│ ├── 3036: presi-backend │ +│ ├── 3037: nutriphi-backend │ +│ └── 3038: skilltree-backend │ +│ │ +│ 4000-4099: Matrix Stack │ +│ ├── 4000: synapse (Homeserver) │ +│ ├── 4001: synapse-metrics │ +│ ├── 4010: matrix-mana-bot (Unified Bot) │ +│ ├── 4011: matrix-ollama-bot (AI Chat) │ +│ ├── 4012: matrix-stats-bot (Analytics) │ +│ ├── 4013: matrix-project-doc-bot (Docs) │ +│ ├── 4080: element-web (Standard Client) │ +│ └── 4090: matrix-web (Custom Client) │ +│ │ +│ 5000-5099: Web Frontends │ +│ ├── 5000: manacore-web (Dashboard) │ +│ ├── 5010: chat-web │ +│ ├── 5011: todo-web │ +│ ├── 5012: calendar-web │ +│ ├── 5013: clock-web │ +│ ├── 5014: contacts-web │ +│ ├── 5015: storage-web │ +│ ├── 5016: presi-web │ +│ ├── 5017: nutriphi-web │ +│ ├── 5018: skilltree-web │ +│ └── 5090: llm-playground │ +│ │ +│ 6000-6099: Automation & Workflows │ +│ ├── 6000: n8n │ +│ ├── 6010: telegram-stats-bot │ +│ └── 6011: telegram-ollama-bot │ +│ │ +│ 8000-8099: Monitoring Dashboards │ +│ ├── 8000: grafana │ +│ ├── 8010: umami │ +│ └── 8080: victoriametrics-ui │ +│ │ +│ 9000-9199: Infrastructure & Exporters │ +│ ├── 9000: minio-api │ +│ ├── 9001: minio-console │ +│ ├── 9090: victoriametrics │ +│ ├── 9091: pushgateway │ +│ ├── 9100: node-exporter │ +│ ├── 9121: redis-exporter │ +│ └── 9187: postgres-exporter │ +│ │ +│ 11000+: Native Services (macOS) │ +│ └── 11434: ollama │ +│ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 2. Konsistente Benennung + +**Container-Namen:** + +``` +manacore-{category}-{service} + +Beispiele: +- manacore-infra-postgres +- manacore-infra-redis +- manacore-core-auth +- manacore-api-gateway +- manacore-app-chat-backend +- manacore-app-chat-web +- manacore-matrix-synapse +- manacore-matrix-bot-mana +- manacore-mon-grafana +- manacore-mon-victoria +``` + +**Volume-Namen:** + +``` +manacore-{service}-data + +Beispiele: +- manacore-postgres-data +- manacore-redis-data +- manacore-matrix-bots-data (konsolidiert!) +- manacore-grafana-data +``` + +### 3. Service-Konsolidierung + +**Matrix-Bots → Unified Bot Architecture:** + +``` +AKTUELL: 10 separate Bot-Container +┌────────────────────────────────────────────────────────────┐ +│ matrix-mana-bot │ matrix-ollama-bot │ ... │ +│ matrix-stats-bot │ matrix-project-doc │ │ +│ matrix-todo-bot │ matrix-calendar-bot │ │ +│ matrix-nutriphi-bot │ matrix-zitare-bot │ │ +│ matrix-clock-bot │ matrix-tts-bot │ │ +└────────────────────────────────────────────────────────────┘ + +NEU: 3 konsolidierte Bots +┌────────────────────────────────────────────────────────────┐ +│ │ +│ matrix-mana-bot (Unified) │ +│ ├── !mana → AI Chat (Ollama) │ +│ ├── !todo → Task Management │ +│ ├── !cal → Calendar │ +│ ├── !clock → Time Tracking │ +│ ├── !nutri → Nutrition │ +│ ├── !zitat → Quotes │ +│ └── !tts → Text-to-Speech │ +│ │ +│ matrix-stats-bot │ +│ └── Scheduled Reports (bleibt separat) │ +│ │ +│ matrix-project-doc-bot │ +│ └── RAG + Embeddings (bleibt separat wegen DB) │ +│ │ +└────────────────────────────────────────────────────────────┘ +``` + +**Einsparung:** 7 Container, 7 Volumes, ~700MB RAM + +### 4. Optimierte Dependencies + +```yaml +# NEU: Lazy Dependencies mit Retry-Logic +services: + api-gateway: + depends_on: + mana-core-auth: + condition: service_healthy # Kritisch + environment: + # Andere Services werden lazy verbunden + SEARCH_URL: http://mana-search:3020 + DB_RETRY_ATTEMPTS: 5 + DB_RETRY_DELAY: 3000 +``` + +### 5. Service-Tiers für Startup-Reihenfolge + +```yaml +# docker-compose.yml mit deploy.order +services: + postgres: + deploy: + order: 1 + + redis: + deploy: + order: 1 + + minio: + deploy: + order: 1 + + mana-core-auth: + deploy: + order: 2 + + api-gateway: + deploy: + order: 3 + + # ... App-Backends + deploy: + order: 4 + + # ... Web-Frontends + deploy: + order: 5 +``` + +--- + +## Migration zu Kubernetes + +### Phase 0: Docker Compose Cleanup (vor K8s) + +Diese Verbesserungen sollten **vor** der K8s-Migration durchgeführt werden: + +| Aufgabe | Priorität | Aufwand | Auswirkung | +| -------------------------------- | --------- | ------- | ------------------- | +| Port-Schema umstellen | Hoch | 2h | Alle Services | +| Container-Namen vereinheitlichen | Mittel | 1h | Alle Services | +| Matrix-Bots konsolidieren | Hoch | 8h | 7 Container weniger | +| Volume-Namen vereinheitlichen | Niedrig | 30min | Cleanup | +| Dependencies optimieren | Mittel | 2h | Schnellerer Start | + +### Phase 1: Headscale + K3s (nach Cleanup) + +Mit dem neuen Schema ist die Migration zu Kubernetes wesentlich einfacher: + +```yaml +# Kubernetes Namespace-Struktur +namespaces: + - manacore-infra # postgres, redis, minio + - manacore-core # auth, gateway, search + - manacore-apps # chat, todo, calendar, etc. + - manacore-matrix # synapse, bots, element + - manacore-monitoring # grafana, victoria, exporters + - manacore-tools # n8n, telegram-bots +``` + +### Kubernetes Service-Typen + +```yaml +# Mapping Docker → Kubernetes +┌────────────────────┬─────────────────────────────────┐ +│ Docker Compose │ Kubernetes │ +├────────────────────┼─────────────────────────────────┤ +│ ports: "3001:3001" │ Service (ClusterIP) │ +│ restart: always │ Deployment + ReplicaSet │ +│ volumes: │ PersistentVolumeClaim │ +│ depends_on: │ initContainers + readinessProbe │ +│ healthcheck: │ livenessProbe + readinessProbe │ +│ environment: │ ConfigMap + Secret │ +└────────────────────┴─────────────────────────────────┘ +``` + +--- + +## Ressourcen-Analyse + +### Aktuelle RAM-Nutzung (geschätzt) + +``` +┌────────────────────────────────────────────────────────────┐ +│ Service-Kategorie │ Container │ RAM (geschätzt) │ +├────────────────────────────┼───────────┼──────────────────┤ +│ Infrastructure │ 3 │ ~1.5 GB │ +│ Core Services │ 4 │ ~800 MB │ +│ App Backends │ 10 │ ~2.0 GB │ +│ Web Frontends │ 11 │ ~1.1 GB │ +│ Matrix Stack │ 14 │ ~2.8 GB │ +│ Monitoring │ 8 │ ~1.5 GB │ +├────────────────────────────┼───────────┼──────────────────┤ +│ GESAMT Docker │ 50 │ ~9.7 GB │ +├────────────────────────────┼───────────┼──────────────────┤ +│ Native Services (Ollama) │ - │ ~4.0 GB │ +│ macOS System │ - │ ~2.0 GB │ +├────────────────────────────┼───────────┼──────────────────┤ +│ GESAMT │ │ ~15.7 GB │ +└────────────────────────────────────────────────────────────┘ +``` + +### Nach Konsolidierung (geschätzt) + +``` +┌────────────────────────────────────────────────────────────┐ +│ Service-Kategorie │ Container │ RAM (geschätzt) │ +├────────────────────────────┼───────────┼──────────────────┤ +│ Infrastructure │ 3 │ ~1.5 GB │ +│ Core Services │ 4 │ ~800 MB │ +│ App Backends │ 10 │ ~2.0 GB │ +│ Web Frontends │ 11 │ ~1.1 GB │ +│ Matrix Stack (konsolidiert)│ 7 │ ~1.4 GB │ +│ Monitoring │ 8 │ ~1.5 GB │ +├────────────────────────────┼───────────┼──────────────────┤ +│ GESAMT Docker │ 43 │ ~8.3 GB │ +├────────────────────────────┼───────────┼──────────────────┤ +│ EINSPARUNG │ -7 │ ~1.4 GB │ +└────────────────────────────────────────────────────────────┘ +``` + +--- + +## Nächste Schritte + +### Sofort (Phase 0) + +1. **Port-Mapping-Dokument erstellen** - Diese Datei als Referenz +2. **Cloudflare Tunnel-Konfiguration anpassen** - Neue Port-Mappings +3. **Matrix-Bots konsolidieren** - Code-Merge in matrix-mana-bot + +### Kurzfristig (Phase 1) + +1. **docker-compose.macmini.yml refactoren** - Neue Ports + Namen +2. **Schrittweise Migration** - Ein Service nach dem anderen +3. **Health-Check-Validierung** - Alle Services testen + +### Mittelfristig (Phase 2) + +1. **K8s Manifests erstellen** - Basierend auf neuem Schema +2. **Helm Charts entwickeln** - Für B2B-Deployment +3. **CI/CD Pipeline anpassen** - Neue Container-Namen + +--- + +## Referenzen + +- [Blueprint 001: Cluster & Federation Architecture](/blueprints/001-mana-cluster-federation-architecture) +- [Docker Compose Best Practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) +- [Kubernetes Port Allocation](https://kubernetes.io/docs/concepts/services-networking/service/) +- [Matrix Synapse Deployment](https://matrix-org.github.io/synapse/latest/setup/installation.html) diff --git a/apps/manacore/apps/landing/src/content/devlog/2026-01-27-telegram-matrix-bots.md b/apps/manacore/apps/landing/src/content/devlog/2026-01-27-telegram-matrix-bots.md deleted file mode 100644 index 829069f6c..000000000 --- a/apps/manacore/apps/landing/src/content/devlog/2026-01-27-telegram-matrix-bots.md +++ /dev/null @@ -1,396 +0,0 @@ ---- -title: 'Bot-Offensive: 3 Telegram Bots + Matrix Self-Hosting für DSGVO-Compliance' -description: 'Telegram Calendar/Contacts/Chat Bots, Matrix Synapse Self-Hosting, Model Comparison Feature, Guest Welcome Modal und Local STT Integration' -date: 2026-01-27 -author: 'Till Schneider' -category: 'feature' -tags: - [ - 'telegram-bot', - 'matrix', - 'synapse', - 'dsgvo', - 'gdpr', - 'self-hosting', - 'chat', - 'ai', - 'speech-to-text', - 'guest-mode', - ] -featured: true -commits: 20 -readTime: 12 ---- - -Intensiver Tag (und Nacht) mit Fokus auf **Bot-Entwicklung** und **DSGVO-konforme Messaging-Infrastruktur**. Die wichtigsten Errungenschaften: - -- **3 neue Telegram Bots** (Calendar, Contacts, Chat) -- **Matrix Self-Hosting** mit Synapse + Element Web -- **4 Matrix Bots** für DSGVO-Compliance -- **Model Comparison** Feature in Chat App -- **Guest Welcome Modal** für alle Apps -- **Local STT Integration** für Project Doc Bot -- **Codebase Cleanup** (Maerchenzauber entfernt) - ---- - -## Telegram Bots - -### telegram-calendar-bot - -Neuer Bot für Kalender-Integration via Telegram: - -``` -services/telegram-calendar-bot/ -├── src/ -│ ├── bot/ # Telegraf Bot Logic -│ ├── calendar/ # Calendar API Client -│ ├── reminder/ # Reminder Scheduler -│ └── user/ # User Linking Service -``` - -**Commands:** - -| Command | Beschreibung | -| ------------------- | ------------------------ | -| `/today` | Heutige Termine | -| `/tomorrow` | Termine für morgen | -| `/week` | Wochenübersicht | -| `/next [n]` | Nächste n Termine | -| `/calendars` | Kalender auflisten | -| `/remind` | Erinnerungseinstellungen | -| `/add` | Termin hinzufügen | -| `/link` / `/unlink` | Account verknüpfen | - -**Features:** - -- Deutsche Lokalisierung -- Morning Briefing Support -- Reminder Scheduler mit Push-Benachrichtigungen -- Account-Linking mit Calendar Web App - -### telegram-contacts-bot - -Bot für schnellen Kontakt-Zugriff: - -``` -services/telegram-contacts-bot/ -├── src/ -│ ├── bot/ # Telegraf Bot Logic -│ ├── contacts/ # Contacts API Client -│ └── user/ # User Linking Service -``` - -**Commands:** - -| Command | Beschreibung | -| ----------------------- | ---------------------- | -| `/search [name]` | Kontakte suchen | -| `/favorites` | Favoriten anzeigen | -| `/recent` | Zuletzt hinzugefügt | -| `/birthdays` | Anstehende Geburtstage | -| `/tags` / `/tag [name]` | Tag-Verwaltung | -| `/stats` | Kontakt-Statistiken | -| `/add [name]` | Kontakt hinzufügen | - -**Features:** - -- Geburtstags-Erkennung und -Erinnerungen -- Tag-basierte Filterung -- Schnellsuche mit Formatierung - -### telegram-chat-bot - -AI Chat Bot mit Multi-Model Support: - -``` -services/telegram-chat-bot/ -├── src/ -│ ├── bot/ # Telegraf Bot Logic -│ ├── chat/ # Chat API Client -│ └── user/ # User Service -``` - -**Commands:** - -| Command | Beschreibung | -| --------------- | --------------------------- | -| `/models` | Verfügbare Modelle anzeigen | -| `/model [name]` | Modell wechseln | -| `/new [title]` | Neue Konversation | -| `/convos` | Konversationen auflisten | -| `/history` | Letzte Nachrichten | -| `/clear` | Kontext löschen | - -**Features:** - -- Unterstützt lokale (Gemma) + Cloud-Modelle (Claude, GPT, etc.) -- Synchronisation mit Chat Web/Mobile App -- Message Splitting für lange Antworten -- Conversation History - ---- - -## Matrix Self-Hosting (DSGVO-Compliance) - -Als datenschutzkonforme Alternative zu Telegram wurde eine komplette Matrix-Infrastruktur aufgesetzt. - -### Warum Matrix statt Telegram? - -| Aspekt | Telegram | Matrix | -| ------------------ | -------------------------------- | --------------------------- | -| **Datenhaltung** | Telegram Server (Russland/Dubai) | Self-Hosted (Deutschland) | -| **DSGVO** | Problematisch | Vollständig konform | -| **Bot-Daten** | Bei Telegram | Bei uns | -| **Data Retention** | Unklar | Konfigurierbar (1-365 Tage) | - -### Infrastruktur - -```yaml -# docker-compose.macmini.yml -services: - synapse: # Matrix Homeserver (Port 8008) - synapse-db: # PostgreSQL für Synapse - element-web: # Element Web Client (Port 8087) -``` - -**Konfigurationsdateien:** - -- `docker/matrix/homeserver.yaml` - Synapse Konfiguration -- `docker/matrix/log.config.yaml` - Logging mit Rotation -- `docker/matrix/element-config.json` - Element Web Settings - -### Matrix Bots - -Alle Telegram-Bots wurden als Matrix-Varianten implementiert: - -| Bot | Port | Funktion | -| -------------------------- | ---- | ----------------------- | -| **matrix-ollama-bot** | 3311 | Lokale LLM-Inferenz | -| **matrix-stats-bot** | 3312 | Umami Analytics Reports | -| **matrix-project-doc-bot** | 3313 | Projektdokumentation | - -#### matrix-ollama-bot - -DSGVO-konformer Ersatz für telegram-ollama-bot: - -``` -services/matrix-ollama-bot/ -├── src/ -│ ├── bot/ # matrix-bot-sdk -│ ├── ollama/ # Ollama API Client -│ └── main.ts -``` - -**Commands:** `!help`, `!models`, `!model`, `!mode`, `!clear`, `!status` - -**System Prompts:** default, classify, summarize, translate, code - -#### matrix-stats-bot - -Analytics-Reports im Matrix Room: - -- Tägliche/Wöchentliche Reports -- Umami Integration -- Commands: `!stats`, `!today`, `!week`, `!realtime`, `!users` - -#### matrix-project-doc-bot - -Projektdokumentation mit AI-Unterstützung: - -- Voice Transcription via OpenAI Whisper -- Blog-Generierung in 5 Stilen -- PostgreSQL + S3 Storage -- Commands: `!new`, `!projects`, `!switch`, `!status`, `!generate`, `!export` - -### Setup Script - -```bash -scripts/mac-mini/setup-matrix.sh -``` - -Automatisiert: - -- Synapse-Initialisierung -- Admin-User Erstellung -- Element Web Konfiguration -- Bot-User Registration - ---- - -## Chat App: Model Comparison - -Neues Feature zum Vergleichen von AI-Modellen: - -``` -apps/chat/apps/web/src/ -├── lib/ -│ ├── components/compare/ -│ │ ├── CompareInput.svelte # Prompt Eingabe -│ │ ├── CompareProgress.svelte # Progress Bar -│ │ ├── ModelResponseCard.svelte # Response Karte -│ │ └── ModelResponseGrid.svelte # Grid Layout -│ └── stores/compare.svelte.ts # Svelte 5 Runes Store -└── routes/(protected)/compare/ # Route -``` - -**Features:** - -- Gleichzeitige Anfrage an mehrere Modelle -- Side-by-Side Vergleich -- Response-Metriken (Zeit, Tokens) -- Temperature/Max Tokens Kontrolle -- Cancel-Funktionalität - ---- - -## Guest Welcome Modal - -Einheitliches Welcome-Modal für alle Apps im Guest Mode: - -``` -packages/shared-auth-ui/src/ -├── components/GuestWelcomeModal.svelte -└── utils/guestWelcome.ts -``` - -**Features:** - -- App-Icon und -Name aus shared-branding -- Feature-Liste (DE/EN lokalisiert) -- Warnung über lokale Datenspeicherung -- Buttons: Login, Register, Hilfe, "Als Gast fortfahren" -- localStorage für "nicht mehr anzeigen" - -**Integriert in:** - -- Calendar -- Chat -- Clock -- Contacts -- Todo - ---- - -## Local STT Integration - -Integration von lokaler Speech-to-Text in telegram-project-doc-bot: - -```typescript -// transcription.service.ts -// STT Provider: local (mana-stt) oder openai -// Fallback wenn lokal nicht verfügbar -``` - -**Konfiguration:** - -```env -STT_PROVIDER=local # local oder openai -STT_LOCAL_URL=http://localhost:8000 -STT_LOCAL_MODEL=large-v3-turbo -``` - -**Grafana Dashboards:** - -- `services/mana-stt/grafana-dashboard.json` -- `services/ollama-metrics-proxy/grafana-dashboard.json` - ---- - -## Developer Experience - -### Dev-Credentials Pre-Fill - -Login-Seite zeigt jetzt Dev-Credentials im Development-Modus: - -- Email: `dev@manacore.local` -- Password vorgefüllt -- Seed Script: `pnpm db:seed:dev` in mana-core-auth - -### Ollama URL in .env.development - -```env -OLLAMA_URL=http://mac-mini.local:11434 -``` - ---- - -## Codebase Cleanup - -### Maerchenzauber entfernt - -Komplettes Entfernen der Maerchenzauber-App: - -| Entfernt | Dateien | -| --------------- | ---------------------------------------- | -| App Definition | MANA_APPS, APP_URLS, AppId | -| Branding | app-icons.ts, config.ts, StorytellerLogo | -| Theme | maerchenzauber.css | -| Landing Content | maerchenzauber-de.md | -| Env Config | generate-env.mjs | - ---- - -## Bugfixes - -### Health Check Pfade - -Korrektur des presi-backend Health Endpoints: - -```diff -- /api/health -+ /api/v1/health -``` - -Betroffen: - -- `docker-compose.macmini.yml` -- `scripts/mac-mini/health-check.sh` - -### Telegram User ID Type - -Fix für große Telegram User IDs: - -```diff -- telegram_user_id: integer() -+ telegram_user_id: bigint() -``` - ---- - -## Zusammenfassung - -| Bereich | Commits | Highlights | -| ------------- | ------- | ----------------------------- | -| Telegram Bots | 6 | Calendar, Contacts, Chat Bots | -| Matrix | 5 | Synapse, Element, 3 Bots | -| Chat App | 2 | Model Comparison | -| Auth UI | 2 | Guest Welcome Modal | -| STT | 1 | Local STT Integration | -| Cleanup | 2 | Maerchenzauber entfernt | -| Bugfixes | 2 | Health Checks, User ID Type | - ---- - -## Neue Services - -| Service | Port | Typ | Beschreibung | -| ---------------------- | ---- | -------- | -------------------- | -| telegram-calendar-bot | 3303 | Telegram | Kalender-Integration | -| telegram-contacts-bot | 3304 | Telegram | Kontakte-Zugriff | -| telegram-chat-bot | 3305 | Telegram | AI Chat | -| synapse | 8008 | Matrix | Homeserver | -| element-web | 8087 | Matrix | Web Client | -| matrix-ollama-bot | 3311 | Matrix | LLM Chat | -| matrix-stats-bot | 3312 | Matrix | Analytics | -| matrix-project-doc-bot | 3313 | Matrix | Projekt-Docs | - ---- - -## Nächste Schritte - -1. **Matrix Bots deployen** auf Mac Mini -2. **Cloudflare Tunnel** für Matrix-Subdomains -3. **Telegram Calendar Bot** mit Reminder-Notifications testen -4. **Model Comparison** mit mehr Metriken erweitern -5. **Mobile Apps** mit neuen Bot-Features integrieren diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index 5858374c2..5efba3ddb 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -957,38 +957,6 @@ services: retries: 3 start_period: 40s - # ============================================ - # Telegram Stats Bot - # ============================================ - - telegram-stats-bot: - image: ghcr.io/memo-2023/telegram-stats-bot:latest - container_name: manacore-telegram-stats-bot - restart: always - depends_on: - postgres: - condition: service_healthy - umami: - condition: service_healthy - environment: - NODE_ENV: production - PORT: 3300 - TZ: Europe/Berlin - TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} - TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID} - UMAMI_API_URL: http://umami:3000 - UMAMI_USERNAME: ${UMAMI_USERNAME:-admin} - UMAMI_PASSWORD: ${UMAMI_PASSWORD} - DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-manacore123}@postgres:5432/manacore_auth - ports: - - "3300:3300" - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3300/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - # ============================================ # Matrix Synapse (Homeserver) - DSGVO-konform # ============================================ @@ -1420,10 +1388,10 @@ services: - WATCHTOWER_POLL_INTERVAL=300 - WATCHTOWER_CLEANUP=true - WATCHTOWER_INCLUDE_STOPPED=false - - WATCHTOWER_NOTIFICATIONS=shoutrrr - - WATCHTOWER_NOTIFICATION_URL=telegram://${TELEGRAM_BOT_TOKEN}@telegram?chats=${TELEGRAM_CHAT_ID} - - WATCHTOWER_NOTIFICATION_REPORT=true - - WATCHTOWER_NOTIFICATION_TEMPLATE={{- if .Report -}}{{- with .Report -}}{{- if or .Updated .Failed -}}🐳 ManaCore Watchtower{{- if .Updated }} | ✅ Updated:{{range .Updated}} {{.Name}}{{end}}{{- end -}}{{- if .Failed }} | ❌ Failed:{{range .Failed}} {{.Name}}{{end}}{{- end -}}{{- end -}}{{- end -}}{{- end -}} + # Notifications disabled (Telegram removed) + # Configure Matrix notifications if needed: + # - WATCHTOWER_NOTIFICATIONS=shoutrrr + # - WATCHTOWER_NOTIFICATION_URL=matrix://user:password@matrix.mana.how:8008/!roomid:mana.how # ============================================ # Volumes diff --git a/docs/MATRIX_BOT_ARCHITECTURE.md b/docs/MATRIX_BOT_ARCHITECTURE.md new file mode 100644 index 000000000..1cb12c0ee --- /dev/null +++ b/docs/MATRIX_BOT_ARCHITECTURE.md @@ -0,0 +1,975 @@ +# ManaCore Matrix Bot Architecture + +**Status:** Production +**Datum:** 1. Februar 2026 +**Autor:** Till Schneider +**Letzte Aktualisierung:** 1. Februar 2026 + +--- + +## Executive Summary + +ManaCore setzt auf **Matrix** als primäre Messaging-Plattform für Bot-Interaktionen. Mit 19 spezialisierten Matrix-Bots und einem Gateway-Bot bieten wir eine vollständig dezentrale, DSGVO-konforme Alternative zu Cloud-basierten Chat-Diensten. + +**Kernprinzipien:** +- **Volle Kontrolle** - Eigene Infrastruktur, eigene Daten +- **DSGVO-Konformität** - Alle Daten auf eigenen Servern +- **Unabhängigkeit** - Keine Abhängigkeit von Drittanbieter-Plattformen +- **Einheitliche UX** - Konsistente Erfahrung über alle Bots + +--- + +## 1. Warum Matrix? + +### 1.1 Die Entscheidung gegen Telegram/Discord/Slack + +Bei der Wahl der Messaging-Plattform für ManaCore hatten wir mehrere Optionen: + +| Plattform | Vorteile | Nachteile | +|-----------|----------|-----------| +| **Telegram** | Große Reichweite, einfache API | Zentral, Daten bei Telegram, keine Kontrolle über UX | +| **Discord** | Gaming-Community, Webhooks | US-basiert, DSGVO-Bedenken, Werbung | +| **Slack** | Business-Standard | Teuer, Vendor Lock-in, keine Self-Hosting Option | +| **Matrix** | Dezentral, Self-Hosted, E2E-Verschlüsselung | Kleinere Community, mehr Setup-Aufwand | + +**Unsere Entscheidung:** Matrix bietet die einzige Möglichkeit, eine **vollständig unabhängige** Plattform zu betreiben mit: +- Voller Kontrolle über Nutzerdaten +- Eigener UI/UX (Element, eigene Clients) +- End-to-End-Verschlüsselung +- Federation für Inter-Server-Kommunikation + +### 1.2 Matrix Grundkonzepte + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Matrix Ökosystem │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Homeserver │<───>│ Homeserver │ Federation │ +│ │ (mana.how) │ │ (matrix.org) │ │ +│ └────────┬────────┘ └─────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ Räume ││ +│ ├─────────────────────────────────────────────────────────────┤│ +│ │ !abc:mana.how │ Bot-Interaktion (1:1) ││ +│ │ !xyz:mana.how │ Gruppen-Chat (Multi-User) ││ +│ │ #public:mana.how │ Öffentlicher Raum ││ +│ └─────────────────────────────────────────────────────────────┘│ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐│ +│ │ Clients ││ +│ ├─────────────────────────────────────────────────────────────┤│ +│ │ Element (Web/Desktop/Mobile) ││ +│ │ FluffyChat, Nheko, SchildiChat, ... ││ +│ │ ManaCore Bots (matrix-bot-sdk) ││ +│ └─────────────────────────────────────────────────────────────┘│ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**Kernkonzepte:** +- **Homeserver:** Der Server, der Nutzerkonten und Räume hostet (wir nutzen Synapse) +- **Räume:** Container für Nachrichten, Events und State +- **Federation:** Server können miteinander kommunizieren +- **E2E-Verschlüsselung:** Megolm/Olm für sichere Kommunikation + +--- + +## 2. Bot-Architektur Übersicht + +### 2.1 Gesamtarchitektur + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ManaCore Bot Ecosystem │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ @manacore/bot-services (Shared Business Logic) │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │ +│ │ │ TodoSvc │ │ CalSvc │ │ AiSvc │ │ ClockSvc │ │ ... │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ Matrix Transport Layer │ │ +│ │ (matrix-bot-sdk) │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────┼──────────────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ 19 Matrix Bots │ │ Gateway Bot │ │ Shared Services │ │ +│ │ (Specialized) │ │ (All-in-One) │ │ (mana-llm, etc) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ Backend APIs │ │ +│ │ chat │ todo │ contacts │ calendar │ clock │ picture │ ... │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────┐ │ +│ │ Data Layer │ │ +│ │ PostgreSQL │ S3/MinIO │ JSON Files │ Redis │ Ollama │ │ +│ └──────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Bot-Typen + +Wir unterscheiden drei Hauptkategorien von Bots: + +#### Typ 1: Backend-integrierte Bots +Diese Bots fungieren als Interface zu bestehenden NestJS-Backend-APIs: + +``` +User → Matrix Bot → REST API → PostgreSQL +``` + +**Beispiele:** +- `matrix-contacts-bot` → Contacts Backend (Port 3015) +- `matrix-chat-bot` → Chat Backend (Port 3002) +- `matrix-picture-bot` → Picture Backend (Port 3006) + +**Vorteile:** +- Konsistente Geschäftslogik (Web + Bot identisch) +- Zentralisierte Datenhaltung +- Einheitliche Auth via JWT + +#### Typ 2: DSGVO-konforme Standalone-Bots +Diese Bots speichern Daten lokal ohne externe Services: + +``` +User → Matrix Bot → JSON File (lokal) +``` + +**Beispiele:** +- `matrix-todo-bot` → Lokale JSON-Datei +- `matrix-calendar-bot` → Lokale JSON-Datei +- `matrix-ollama-bot` → In-Memory + lokales Ollama + +**Vorteile:** +- Keine Daten verlassen den Server +- Volle DSGVO-Konformität +- Offline-fähig + +#### Typ 3: Gateway-Bot +Kombiniert alle Features in einem Bot: + +``` +User → matrix-mana-bot → @manacore/bot-services → Multiple Backends +``` + +**Features:** +- Einheitlicher Einstiegspunkt (`!mana`) +- Intelligentes Command-Routing +- Cross-Feature-Integration (z.B. "Termin mit Kontakt erstellen") + +--- + +## 3. Shared Package: @manacore/bot-services + +### 3.1 Architektur + +Das Package `@manacore/bot-services` stellt transport-agnostische Geschäftslogik bereit: + +```typescript +// Exportierte Services +export { TodoModule, TodoService } from './todo'; +export { CalendarModule, CalendarService } from './calendar'; +export { AiModule, AiService } from './ai'; +export { ClockModule, ClockService } from './clock'; + +// Storage Provider (pluggable) +export { FileStorageProvider } from './shared/storage/file-storage.provider'; +export { MemoryStorageProvider } from './shared/storage/memory-storage.provider'; + +// Utilities +export { generateId, getTodayISO, formatDateDE } from './shared/utils'; +export { parseGermanDateKeyword } from './shared/date-parser'; +``` + +### 3.2 TodoService + +Vollständige Aufgabenverwaltung mit deutscher Sprachunterstützung: + +```typescript +interface TodoService { + // CRUD + addTask(userId: string, text: string): Promise; + listTasks(userId: string, filter?: TaskFilter): Promise; + completeTask(userId: string, taskId: string): Promise; + deleteTask(userId: string, taskId: string): Promise; + + // Projekte + createProject(userId: string, name: string): Promise; + listProjects(userId: string): Promise; + + // Filter + getTasksDueToday(userId: string): Promise; + getTasksByPriority(userId: string, priority: Priority): Promise; +} + +// Deutsche Eingabeverarbeitung +"Morgen Arzt anrufen #gesundheit !hoch" +→ { text: "Arzt anrufen", dueDate: tomorrow, project: "gesundheit", priority: "high" } +``` + +### 3.3 CalendarService + +Terminverwaltung mit natürlicher Spracheingabe: + +```typescript +interface CalendarService { + // Events + createEvent(userId: string, input: string): Promise; + getEventsForDate(userId: string, date: Date): Promise; + getEventsInRange(userId: string, start: Date, end: Date): Promise; + + // Kalender + createCalendar(userId: string, name: string): Promise; + listCalendars(userId: string): Promise; +} + +// Natürliche Eingabe +"Meeting morgen um 14 Uhr im Büro" +→ { title: "Meeting", date: tomorrow, time: "14:00", location: "Büro" } +``` + +### 3.4 AiService + +Integration mit lokalem LLM (Ollama) und mana-llm: + +```typescript +interface AiService { + chat(userId: string, message: string): Promise; + setModel(userId: string, model: string): Promise; + setSystemPrompt(userId: string, mode: SystemMode): Promise; + clearHistory(userId: string): Promise; + + // Vision (für Bildanalyse) + analyzeImage(userId: string, imageUrl: string, prompt: string): Promise; +} + +type SystemMode = 'default' | 'classify' | 'summarize' | 'translate' | 'code'; +``` + +### 3.5 Storage Provider Pattern + +Pluggable Storage für flexible Datenhaltung: + +```typescript +interface StorageProvider { + get(key: string): Promise; + set(key: string, value: T): Promise; + delete(key: string): Promise; + list(prefix?: string): Promise; +} + +// Implementierungen +class FileStorageProvider implements StorageProvider { + constructor(private basePath: string) {} + // Speichert als JSON-Dateien +} + +class MemoryStorageProvider implements StorageProvider { + private store = new Map(); + // In-Memory für Tests +} + +// Zukünftig möglich: +class PostgresStorageProvider implements StorageProvider { } +class RedisStorageProvider implements StorageProvider { } +``` + +--- + +## 4. Matrix Bot Implementation + +### 4.1 Technologie-Stack + +Alle Matrix-Bots nutzen einen einheitlichen Stack: + +| Komponente | Technologie | Version | +|------------|-------------|---------| +| **Framework** | NestJS | 10.x | +| **Matrix SDK** | matrix-bot-sdk | 0.7.1 | +| **Language** | TypeScript | 5.x | +| **Runtime** | Node.js | 20.x | +| **Build** | tsc + Docker | - | + +### 4.2 Bot-Struktur + +``` +services/matrix-{name}-bot/ +├── src/ +│ ├── app.module.ts # NestJS Root Module +│ ├── main.ts # Bootstrap +│ ├── matrix/ +│ │ ├── matrix.module.ts # Matrix SDK Integration +│ │ ├── matrix.service.ts # Bot-Logik & Command-Handling +│ │ └── matrix.constants.ts # Konfiguration +│ ├── services/ # Optionale lokale Services +│ └── utils/ # Hilfsfunktionen +├── Dockerfile +├── package.json +└── tsconfig.json +``` + +### 4.3 Matrix Service Pattern + +```typescript +@Injectable() +export class MatrixService implements OnModuleInit, OnModuleDestroy { + private client: MatrixClient; + private storage: SimpleFsStorageProvider; + + async onModuleInit() { + // Storage für Sync-State + this.storage = new SimpleFsStorageProvider('./data/matrix-state.json'); + + // Client initialisieren + this.client = new MatrixClient( + this.configService.get('MATRIX_HOMESERVER_URL'), + this.configService.get('MATRIX_ACCESS_TOKEN'), + this.storage, + ); + + // Crypto für E2E (optional) + const cryptoStore = new RustSdkCryptoStorageProvider('./data/crypto'); + await this.client.crypto.prepare(cryptoStore); + + // Event-Handler registrieren + this.client.on('room.message', this.handleMessage.bind(this)); + + // Sync starten + await this.client.start(); + } + + private async handleMessage(roomId: string, event: any) { + if (event.sender === this.client.getUserId()) return; + + const body = event.content?.body; + if (!body?.startsWith('!')) return; + + const [command, ...args] = body.slice(1).split(' '); + + switch (command.toLowerCase()) { + case 'help': + case 'hilfe': + await this.sendHelp(roomId); + break; + case 'add': + case 'hinzufuegen': + await this.handleAdd(roomId, event.sender, args.join(' ')); + break; + // ... weitere Commands + } + } + + private async sendMessage(roomId: string, message: string) { + await this.client.sendMessage(roomId, { + msgtype: 'm.text', + body: message, + format: 'org.matrix.custom.html', + formatted_body: this.markdownToHtml(message), + }); + } +} +``` + +### 4.4 Command-Pattern + +Alle Bots nutzen ein einheitliches Command-Schema: + +``` +!command [args] # Englisch +!befehl [argumente] # Deutsch (Aliase) +``` + +**Beispiele:** + +| Bot | Command | Alias | Beschreibung | +|-----|---------|-------|--------------| +| todo | `!add Task` | `!hinzufuegen` | Aufgabe erstellen | +| todo | `!list` | `!liste` | Aufgaben anzeigen | +| todo | `!done 1` | `!erledigt` | Aufgabe abschließen | +| calendar | `!today` | `!heute` | Termine heute | +| calendar | `!add Meeting morgen 14:00` | `!termin` | Termin erstellen | +| contacts | `!search Max` | `!suche` | Kontakt suchen | + +### 4.5 Nummer-basiertes Referenzsystem + +Für intuitive Interaktion nutzen Bots ein Listen-Referenz-System: + +``` +User: !kontakte +Bot: 1. Max Mustermann (max@example.com) + 2. Anna Schmidt (anna@example.com) + 3. Peter Meyer (peter@example.com) + +User: !anrufen 2 +Bot: Anruf an Anna Schmidt wird vorbereitet... + Telefon: +49 123 456789 +``` + +**Implementierung:** +```typescript +// Pro User wird die letzte Liste gespeichert +private listCache = new Map(); + +async handleList(roomId: string, userId: string) { + const contacts = await this.contactsApi.list(userId); + this.listCache.set(userId, contacts); + + const message = contacts + .map((c, i) => `${i + 1}. ${c.name} (${c.email})`) + .join('\n'); + + await this.sendMessage(roomId, message); +} + +async handleCall(roomId: string, userId: string, index: number) { + const contacts = this.listCache.get(userId); + if (!contacts || index < 1 || index > contacts.length) { + return this.sendMessage(roomId, 'Ungültige Nummer'); + } + + const contact = contacts[index - 1]; + // ... Anruf-Logik +} +``` + +--- + +## 5. Bot-Katalog + +### 5.1 Produktivitäts-Bots + +| Bot | Port | Storage | Beschreibung | +|-----|------|---------|--------------| +| **matrix-mana-bot** | 3310 | JSON | Gateway - alle Features vereint | +| **matrix-todo-bot** | 3314 | JSON | Aufgabenverwaltung mit Projekten | +| **matrix-calendar-bot** | 3315 | JSON | Terminverwaltung mit Erinnerungen | +| **matrix-clock-bot** | 3318 | API | Timer, Alarme, Weltuhren | + +### 5.2 KI & Medien-Bots + +| Bot | Port | Backend | Beschreibung | +|-----|------|---------|--------------| +| **matrix-chat-bot** | 3327 | chat:3002 | KI-Konversationen | +| **matrix-ollama-bot** | 3311 | mana-llm:3025 | Lokales LLM (DSGVO) | +| **matrix-picture-bot** | 3319 | picture:3006 | AI-Bildgenerierung | +| **matrix-tts-bot** | 3023 | mana-tts:3022 | Text-to-Speech | +| **matrix-project-doc-bot** | 3313 | PostgreSQL+S3 | Projektdoku → Blog | + +### 5.3 App-Integrations-Bots + +| Bot | Port | Backend | Beschreibung | +|-----|------|---------|--------------| +| **matrix-contacts-bot** | 3320 | contacts:3015 | Kontaktverwaltung | +| **matrix-storage-bot** | 3323 | storage:3016 | Cloud-Speicher | +| **matrix-nutriphi-bot** | 3316 | nutriphi:3023 | Ernährungstracking | +| **matrix-zitare-bot** | 3321 | zitare:3019 | Tägliche Zitate | +| **matrix-questions-bot** | 3324 | questions:3011 | Q&A mit Web-Recherche | +| **matrix-manadeck-bot** | 3321 | manadeck:3009 | Kartendecks & Lernen | +| **matrix-planta-bot** | 3322 | planta:3022 | Pflanzenpflege | +| **matrix-skilltree-bot** | 3324 | skilltree:3024 | Skill Tree & XP | +| **matrix-presi-bot** | 3308 | presi:3008 | Präsentationen | +| **matrix-stats-bot** | 3312 | Umami | Analytics-Reports | + +--- + +## 6. Authentifizierung + +### 6.1 Zwei Auth-Modelle + +Wir unterstützen zwei Authentifizierungsmodelle: + +#### Modell A: Matrix User ID (DSGVO-optimiert) +Für Standalone-Bots ohne Backend-Anbindung: + +``` +Matrix User ID → Isolierte Daten pro User +@till:mana.how → /data/till-mana-how/todos.json +``` + +**Vorteile:** +- Kein Login erforderlich +- Daten strikt isoliert +- Funktioniert offline + +**Verwendung:** matrix-todo-bot, matrix-calendar-bot, matrix-ollama-bot + +#### Modell B: Mana Core Auth (JWT) +Für Backend-integrierte Bots: + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Matrix User │────>│ Matrix Bot │────>│ mana-core-auth │ +│ !login x y │ │ │ │ (Port 3001) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ + │ JWT Token │ + ▼ │ + ┌─────────────────┐ │ + │ In-Memory Map │ │ + │ @user → token │ │ + └─────────────────┘ │ + │ │ + ▼ │ + ┌─────────────────┐ │ + │ Backend API │◀──────────┘ + │ (JWT Validate) │ + └─────────────────┘ +``` + +**Login-Flow:** +``` +User: !login till@mana.how geheimespasswort +Bot: Login erfolgreich! Token gültig für 7 Tage. + Nutze !logout zum Abmelden. + +User: !kontakte +Bot: [Zeigt Kontakte aus Backend] +``` + +**Verwendung:** matrix-contacts-bot, matrix-chat-bot, matrix-picture-bot, etc. + +### 6.2 Token-Management + +```typescript +@Injectable() +export class AuthService { + private tokens = new Map(); + + async login(matrixUserId: string, email: string, password: string): Promise { + 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) return false; + + const { accessToken, expiresIn } = await response.json(); + this.tokens.set(matrixUserId, { + token: accessToken, + expiresAt: Date.now() + expiresIn * 1000, + }); + + return true; + } + + getToken(matrixUserId: string): string | null { + const data = this.tokens.get(matrixUserId); + if (!data || Date.now() > data.expiresAt) return null; + return data.token; + } + + logout(matrixUserId: string): void { + this.tokens.delete(matrixUserId); + } +} +``` + +--- + +## 7. Datenbank-Anbindung + +### 7.1 Vier Speichermodelle + +| Modell | Technologie | Bots | Use Case | +|--------|-------------|------|----------| +| **Stateless** | Keine eigene | contacts, chat, picture | Backend delegiert | +| **JSON Files** | Lokale Dateien | todo, calendar, mana-bot | DSGVO, einfach | +| **PostgreSQL** | Drizzle ORM | project-doc-bot | Komplexe Relationen | +| **S3/MinIO** | AWS SDK | project-doc-bot | Medien-Speicherung | + +### 7.2 JSON File Storage (DSGVO) + +```typescript +// Struktur +/data/ +├── {sanitized-matrix-user-id}/ +│ ├── todos.json +│ ├── calendar.json +│ └── settings.json +``` + +```typescript +// FileStorageProvider +class FileStorageProvider { + constructor(private basePath: string) {} + + private getPath(key: string): string { + return path.join(this.basePath, `${key}.json`); + } + + async get(key: string): Promise { + const filePath = this.getPath(key); + if (!fs.existsSync(filePath)) return null; + const data = await fs.promises.readFile(filePath, 'utf-8'); + return JSON.parse(data); + } + + async set(key: string, value: T): Promise { + const filePath = this.getPath(key); + await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); + await fs.promises.writeFile(filePath, JSON.stringify(value, null, 2)); + } +} +``` + +### 7.3 PostgreSQL + Drizzle (Komplexe Bots) + +```typescript +// schema.ts (project-doc-bot) +export const projects = pgTable('projects', { + id: uuid('id').primaryKey().defaultRandom(), + userId: varchar('user_id', { length: 255 }).notNull(), + name: varchar('name', { length: 255 }).notNull(), + description: text('description'), + createdAt: timestamp('created_at').defaultNow(), +}); + +export const mediaItems = pgTable('media_items', { + id: uuid('id').primaryKey().defaultRandom(), + projectId: uuid('project_id').references(() => projects.id), + type: varchar('type', { length: 50 }).notNull(), // photo, voice, text + s3Key: varchar('s3_key', { length: 500 }), + transcription: text('transcription'), + createdAt: timestamp('created_at').defaultNow(), +}); +``` + +--- + +## 8. Matrix-spezifische Features + +### 8.1 Rich Media Support + +Matrix-Bots können verschiedene Nachrichtentypen senden: + +```typescript +// Text mit Markdown/HTML +await client.sendMessage(roomId, { + msgtype: 'm.text', + body: 'Plain text fallback', + format: 'org.matrix.custom.html', + formatted_body: 'Bold and code', +}); + +// Bilder +await client.sendMessage(roomId, { + msgtype: 'm.image', + body: 'Generated image', + url: await client.uploadContent(imageBuffer, 'image/png'), + info: { w: 512, h: 512, mimetype: 'image/png' }, +}); + +// Dateien +await client.sendMessage(roomId, { + msgtype: 'm.file', + body: 'report.pdf', + url: await client.uploadContent(pdfBuffer, 'application/pdf'), + info: { mimetype: 'application/pdf', size: pdfBuffer.length }, +}); + +// Audio (für TTS) +await client.sendMessage(roomId, { + msgtype: 'm.audio', + body: 'Voice message', + url: await client.uploadContent(audioBuffer, 'audio/mp3'), + info: { mimetype: 'audio/mp3', duration: 5000 }, +}); +``` + +### 8.2 Reactions + +Bots können auf Nachrichten reagieren: + +```typescript +// Bestätigung +await client.sendEvent(roomId, 'm.reaction', { + 'm.relates_to': { + rel_type: 'm.annotation', + event_id: originalEventId, + key: '✅', + }, +}); + +// Fehler +await client.sendEvent(roomId, 'm.reaction', { + 'm.relates_to': { + rel_type: 'm.annotation', + event_id: originalEventId, + key: '❌', + }, +}); +``` + +### 8.3 Reply Threading + +```typescript +await client.sendMessage(roomId, { + msgtype: 'm.text', + body: '> Original message\n\nMy reply', + format: 'org.matrix.custom.html', + formatted_body: '...My reply', + 'm.relates_to': { + 'm.in_reply_to': { + event_id: originalEventId, + }, + }, +}); +``` + +### 8.4 End-to-End Encryption + +```typescript +// Crypto Storage initialisieren +const cryptoStore = new RustSdkCryptoStorageProvider('./data/crypto'); + +// Client mit E2E +const client = new MatrixClient(homeserverUrl, accessToken, storage); +await client.crypto.prepare(cryptoStore); + +// Verschlüsselten Raum beitreten +await client.joinRoom(encryptedRoomId); + +// Nachrichten werden automatisch ver-/entschlüsselt +await client.sendMessage(encryptedRoomId, { + msgtype: 'm.text', + body: 'This will be encrypted', +}); +``` + +--- + +## 9. Deployment + +### 9.1 Docker Configuration + +```dockerfile +# Dockerfile +FROM node:20-alpine AS builder + +WORKDIR /app + +# Workspace files +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./ + +# Shared packages +COPY packages/bot-services ./packages/bot-services + +# Bot +COPY services/matrix-todo-bot ./services/matrix-todo-bot + +RUN corepack enable && corepack prepare pnpm@9.15.0 --activate +RUN pnpm install --frozen-lockfile +RUN pnpm --filter @manacore/bot-services build +RUN pnpm --filter matrix-todo-bot build + +# Production +FROM node:20-alpine AS production + +WORKDIR /app/services/matrix-todo-bot + +COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm +COPY --from=builder /app/services/matrix-todo-bot/node_modules ./node_modules +COPY --from=builder /app/services/matrix-todo-bot/dist ./dist +COPY --from=builder /app/services/matrix-todo-bot/package.json ./ + +# Data volume für persistente Speicherung +VOLUME /app/data + +ENV NODE_ENV=production +EXPOSE 3314 + +CMD ["node", "dist/main.js"] +``` + +### 9.2 Environment Variables + +```env +# Matrix Connection +MATRIX_HOMESERVER_URL=https://matrix.mana.how +MATRIX_ACCESS_TOKEN=syt_xxx... +MATRIX_USER_ID=@todo-bot:mana.how + +# Auth (für Backend-Integration) +MANA_CORE_AUTH_URL=http://mana-core-auth:3001 + +# Storage +DATA_PATH=/app/data + +# Optional: Backend URLs +TODO_BACKEND_URL=http://todo-backend:3018 +CONTACTS_BACKEND_URL=http://contacts-backend:3015 + +# Optional: AI Services +MANA_LLM_URL=http://mana-llm:3025 +``` + +### 9.3 docker-compose.yml + +```yaml +version: '3.8' + +services: + matrix-todo-bot: + build: + context: . + dockerfile: services/matrix-todo-bot/Dockerfile + environment: + - MATRIX_HOMESERVER_URL=${MATRIX_HOMESERVER_URL} + - MATRIX_ACCESS_TOKEN=${MATRIX_TODO_BOT_TOKEN} + - MATRIX_USER_ID=@todo-bot:mana.how + volumes: + - todo-bot-data:/app/data + networks: + - manacore + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:3314/health"] + interval: 30s + timeout: 10s + retries: 3 + + matrix-calendar-bot: + # ... analog + + matrix-mana-bot: + # Gateway mit allen Services + depends_on: + - mana-llm + - todo-backend + - contacts-backend + +volumes: + todo-bot-data: + calendar-bot-data: + mana-bot-data: + +networks: + manacore: + external: true +``` + +--- + +## 10. Port-Allokation + +### Matrix Bots (3308-3327) + +| Port | Service | Beschreibung | +|------|---------|--------------| +| 3308 | matrix-presi-bot | Präsentationen | +| 3310 | matrix-mana-bot | Gateway (All-in-One) | +| 3311 | matrix-ollama-bot | Lokales LLM | +| 3312 | matrix-stats-bot | Analytics | +| 3313 | matrix-project-doc-bot | Projektdoku | +| 3314 | matrix-todo-bot | Aufgaben | +| 3315 | matrix-calendar-bot | Termine | +| 3316 | matrix-nutriphi-bot | Ernährung | +| 3318 | matrix-clock-bot | Timer/Alarme | +| 3319 | matrix-picture-bot | Bildgenerierung | +| 3320 | matrix-contacts-bot | Kontakte | +| 3321 | matrix-zitare-bot | Zitate | +| 3322 | matrix-planta-bot | Pflanzen | +| 3323 | matrix-storage-bot | Cloud-Speicher | +| 3324 | matrix-questions-bot | Q&A | +| 3327 | matrix-chat-bot | KI-Chat | + +### Supporting Services + +| Port | Service | Beschreibung | +|------|---------|--------------| +| 3001 | mana-core-auth | Authentifizierung | +| 3020 | mana-stt | Speech-to-Text | +| 3021 | mana-search | Web-Recherche | +| 3022 | mana-tts | Text-to-Speech | +| 3025 | mana-llm | LLM-Abstraction | + +--- + +## 11. Vorteile gegenüber Drittanbieter-Plattformen + +### 11.1 Vollständige Kontrolle + +| Aspekt | Telegram/Discord | ManaCore Matrix | +|--------|------------------|-----------------| +| **Datenhoheit** | Bei Anbieter | Bei uns | +| **Verfügbarkeit** | Abhängig von Anbieter | Eigene Infrastruktur | +| **API-Änderungen** | Anbieter entscheidet | Wir entscheiden | +| **Preisänderungen** | Anbieter entscheidet | Keine | +| **Zensur/Sperrung** | Möglich | Nicht möglich | + +### 11.2 DSGVO-Konformität + +``` +┌────────────────────────────────────────────────────────────────┐ +│ DSGVO-Compliance │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ ✅ Datenverarbeitung nur auf eigenen Servern │ +│ ✅ Keine Weitergabe an Dritte │ +│ ✅ Löschung auf Anfrage (Art. 17) │ +│ ✅ Auskunft über gespeicherte Daten (Art. 15) │ +│ ✅ Datenportabilität (Art. 20) │ +│ ✅ Auftragsverarbeitungsvertrag nicht nötig │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +### 11.3 Einheitliche UX + +Da wir beide Seiten kontrollieren (Bot + Client), können wir: +- Konsistente Command-Syntax über alle Bots +- Deutsche Sprachunterstützung überall +- Einheitliches Fehler-Handling +- Nahtlose Cross-Bot-Integration + +--- + +## 12. Zukünftige Entwicklung + +### 12.1 Geplante Erweiterungen + +- **Widget-Integration:** Interaktive UIs direkt in Element +- **Voice-Bot:** Sprachsteuerung via Matrix Calls +- **Bot-Discovery:** Automatische Bot-Erkennung in Räumen +- **Mehr @manacore/bot-services:** Nutrition, Stats, Docs Services + +### 12.2 Konsolidierung + +Der Fokus liegt auf der Konsolidierung der Bot-Services in `@manacore/bot-services`: +- Alle wiederkehrende Logik zentral +- Einheitliche Storage-Abstraction +- Transport-agnostische Services + +--- + +## 13. Fazit + +ManaCore's Matrix-Bot-Architektur bietet eine **vollständig unabhängige, DSGVO-konforme** Alternative zu Cloud-basierten Chat-Diensten. Mit 19 spezialisierten Bots und einem Gateway-Bot decken wir alle Produktivitäts- und App-Integrationsszenarien ab. + +**Kernvorteile:** +1. **Volle Kontrolle** über Daten und Infrastruktur +2. **DSGVO-Konformität** durch lokale Datenhaltung +3. **Einheitliche UX** durch konsistente Command-Patterns +4. **Skalierbarkeit** durch Microservices-Architektur +5. **Erweiterbarkeit** durch @manacore/bot-services + +--- + +*Dokument erstellt am 1. Februar 2026* +*Letzte Aktualisierung: 1. Februar 2026* diff --git a/docs/decisions/003-infrastructure-audit-improvements.md b/docs/decisions/003-infrastructure-audit-improvements.md new file mode 100644 index 000000000..e5a103017 --- /dev/null +++ b/docs/decisions/003-infrastructure-audit-improvements.md @@ -0,0 +1,74 @@ +# ADR-003: Infrastructure Audit & Port Schema + +**Status:** Accepted +**Date:** 2026-01-31 +**Author:** Till Schneider +**Category:** Infrastructure + +## Context + +Die aktuelle Docker-Compose-Konfiguration auf dem Mac Mini hat über die Zeit 52 Container angesammelt mit chaotischer Port-Verteilung, inkonsistenter Benennung und fragmentierten Volumes. Vor der Migration zu K8s ist ein Cleanup notwendig. + +## Decision + +### 1. Neues Port-Schema + +| Range | Kategorie | Beispiele | +|-------|-----------|-----------| +| 3000-3099 | Core Services & Backends | 3001 auth, 3010 gateway, 3030+ backends | +| 4000-4099 | Matrix Stack | 4000 synapse, 401x bots, 4080 element | +| 5000-5099 | Web Frontends | 5000 dashboard, 501x app webs | +| 6000-6099 | Automation | 6000 n8n, 601x telegram | +| 8000-8099 | Monitoring UI | 8000 grafana, 8010 umami | +| 9000-9199 | Infra & Exporters | 9000 minio, 909x metrics | +| 11000+ | Native macOS | 11434 ollama | + +### 2. Container-Naming + +``` +manacore-{category}-{service} + +Categories: infra, core, app, matrix, mon, auto +``` + +### 3. Matrix-Bot Konsolidierung + +**Vorher:** 10 separate Bot-Container +**Nachher:** 3 Bots (mana-bot unified, stats-bot, project-doc-bot) + +**Einsparung:** 7 Container, ~1.4GB RAM + +### 4. Volume-Naming + +``` +manacore-{service}-data +``` + +Matrix-Bot-Volumes werden zu einem konsolidiert: `manacore-matrix-bots-data` + +## Consequences + +### Positive + +- Klare Port-Zuordnung erleichtert Debugging +- Konsistente Namen verbessern Übersicht +- Weniger Container = weniger Ressourcenverbrauch +- Vorbereitung für K8s-Migration + +### Negative + +- Einmaliger Migrationsaufwand +- Cloudflare Tunnel muss angepasst werden +- Matrix-Bot Code-Merge erforderlich + +## Migration Steps + +1. Port-Mapping dokumentieren (erledigt) +2. Matrix-Bots konsolidieren +3. docker-compose.yml refactoren +4. Cloudflare Tunnel anpassen +5. Services schrittweise migrieren + +## Full Documentation + +Siehe: `apps/manacore/apps/landing/src/content/blueprints/002-infrastructure-audit-improvements.md` diff --git a/package.json b/package.json index be00ed905..202db513d 100644 --- a/package.json +++ b/package.json @@ -236,22 +236,6 @@ "dev:skilltree:full": "./scripts/setup-databases.sh skilltree && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:skilltree:backend\" \"pnpm dev:skilltree:web\"", "skilltree:db:push": "pnpm --filter @skilltree/backend db:push", "skilltree:db:studio": "pnpm --filter @skilltree/backend db:studio", - "dev:projectdoc": "pnpm --filter @manacore/telegram-project-doc-bot start:dev", - "dev:projectdoc:full": "./scripts/setup-databases.sh projectdoc && pnpm dev:projectdoc", - "projectdoc:db:push": "pnpm --filter @manacore/telegram-project-doc-bot db:push", - "projectdoc:db:studio": "pnpm --filter @manacore/telegram-project-doc-bot db:studio", - "dev:zitare-bot": "pnpm --filter @manacore/telegram-zitare-bot start:dev", - "dev:zitare-bot:full": "./scripts/setup-databases.sh zitare_bot && pnpm dev:zitare-bot", - "zitare-bot:db:push": "pnpm --filter @manacore/telegram-zitare-bot db:push", - "zitare-bot:db:studio": "pnpm --filter @manacore/telegram-zitare-bot db:studio", - "dev:todo-bot": "pnpm --filter @manacore/telegram-todo-bot start:dev", - "dev:todo-bot:full": "./scripts/setup-databases.sh todo_bot && ./scripts/setup-databases.sh todo && ./scripts/setup-databases.sh auth && concurrently -n auth,todo-be,bot -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:todo:backend\" \"pnpm dev:todo-bot\"", - "todo-bot:db:push": "pnpm --filter @manacore/telegram-todo-bot db:push", - "todo-bot:db:studio": "pnpm --filter @manacore/telegram-todo-bot db:studio", - "dev:nutriphi-bot": "pnpm --filter @manacore/telegram-nutriphi-bot start:dev", - "dev:nutriphi-bot:full": "./scripts/setup-databases.sh nutriphi_bot && pnpm dev:nutriphi-bot", - "nutriphi-bot:db:push": "pnpm --filter @manacore/telegram-nutriphi-bot db:push", - "nutriphi-bot:db:studio": "pnpm --filter @manacore/telegram-nutriphi-bot db:studio", "dev:matrix:mana": "pnpm --filter matrix-mana-bot start:dev", "dev:matrix:ollama": "pnpm --filter matrix-ollama-bot start:dev", "dev:matrix:todo": "pnpm --filter matrix-todo-bot start:dev", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd65e0424..610cb9fe0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,7 +137,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9 + version: 10.4.9(esbuild@0.19.12) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -185,10 +185,10 @@ importers: version: 0.5.21 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) + version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -212,14 +212,14 @@ importers: version: link:../../../../packages/shared-landing-ui astro: specifier: ^5.16.0 - version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.9.2 version: 5.9.3 devDependencies: '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.18 version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) @@ -228,13 +228,13 @@ importers: version: 20.19.25 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) + version: 9.1.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-astro: specifier: ^1.0.0 - version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) + version: 1.5.0(eslint@9.39.1(jiti@1.21.7)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -609,19 +609,19 @@ importers: version: 18.3.27 '@typescript-eslint/eslint-plugin': specifier: ^7.7.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^7.7.0 - version: 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + version: 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) dotenv: specifier: ^16.4.7 version: 16.6.1 eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-universe: specifier: ^12.0.1 - version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3) + version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -2333,7 +2333,7 @@ importers: version: 0.5.21 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) @@ -5208,7 +5208,7 @@ importers: version: 1.57.0 jest: specifier: ^29.0.0 - version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + version: 29.7.0(@types/node@24.10.1) vitest: specifier: ^3.0.0 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -5340,7 +5340,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -5500,7 +5500,7 @@ importers: version: 7.1.4 ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) @@ -5612,7 +5612,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -5712,7 +5712,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -5785,7 +5785,7 @@ importers: version: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -6490,336 +6490,6 @@ importers: specifier: ^5.7.2 version: 5.9.3 - services/telegram-nutriphi-bot: - dependencies: - '@google/generative-ai': - specifier: ^0.21.0 - version: 0.21.0 - '@nestjs/common': - specifier: ^10.4.15 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/config': - specifier: ^3.3.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) - '@nestjs/core': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - drizzle-orm: - specifier: ^0.38.3 - version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0) - nestjs-telegraf: - specifier: ^2.8.0 - version: 2.9.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(telegraf@4.16.3(encoding@0.1.13))(typescript@5.9.3) - postgres: - specifier: ^3.4.5 - version: 3.4.7 - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - rxjs: - specifier: ^7.8.1 - version: 7.8.2 - telegraf: - specifier: ^4.16.3 - version: 4.16.3(encoding@0.1.13) - devDependencies: - '@manacore/shared-drizzle-config': - specifier: workspace:* - version: link:../../packages/shared-drizzle-config - '@nestjs/cli': - specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) - '@nestjs/schematics': - specifier: ^10.2.3 - version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) - '@types/node': - specifier: ^22.10.5 - version: 22.19.1 - drizzle-kit: - specifier: ^0.30.1 - version: 0.30.6 - rimraf: - specifier: ^6.0.1 - version: 6.1.2 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - services/telegram-ollama-bot: - dependencies: - '@nestjs/common': - specifier: ^10.4.15 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/config': - specifier: ^3.3.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) - '@nestjs/core': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - nestjs-telegraf: - specifier: ^2.8.0 - version: 2.9.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(telegraf@4.16.3(encoding@0.1.13))(typescript@5.9.3) - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - rxjs: - specifier: ^7.8.1 - version: 7.8.2 - telegraf: - specifier: ^4.16.3 - version: 4.16.3(encoding@0.1.13) - devDependencies: - '@nestjs/cli': - specifier: ^10.4.9 - version: 10.4.9 - '@nestjs/schematics': - specifier: ^10.2.3 - version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) - '@types/node': - specifier: ^22.10.5 - version: 22.19.1 - rimraf: - specifier: ^6.0.1 - version: 6.1.2 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - services/telegram-project-doc-bot: - dependencies: - '@aws-sdk/client-s3': - specifier: ^3.721.0 - version: 3.940.0 - '@aws-sdk/s3-request-presigner': - specifier: ^3.721.0 - version: 3.940.0 - '@nestjs/common': - specifier: ^10.4.15 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/config': - specifier: ^3.3.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) - '@nestjs/core': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - drizzle-orm: - specifier: ^0.38.3 - version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0) - nestjs-telegraf: - specifier: ^2.8.0 - version: 2.9.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(telegraf@4.16.3(encoding@0.1.13))(typescript@5.9.3) - openai: - specifier: ^4.77.0 - version: 4.104.0(encoding@0.1.13)(ws@8.18.3)(zod@3.25.76) - postgres: - specifier: ^3.4.5 - version: 3.4.7 - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - rxjs: - specifier: ^7.8.1 - version: 7.8.2 - telegraf: - specifier: ^4.16.3 - version: 4.16.3(encoding@0.1.13) - devDependencies: - '@nestjs/cli': - specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) - '@nestjs/schematics': - specifier: ^10.2.3 - version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) - '@types/node': - specifier: ^22.10.5 - version: 22.19.1 - drizzle-kit: - specifier: ^0.30.1 - version: 0.30.6 - rimraf: - specifier: ^6.0.1 - version: 6.1.2 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - services/telegram-stats-bot: - dependencies: - '@nestjs/common': - specifier: ^10.4.15 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/config': - specifier: ^3.3.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) - '@nestjs/core': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - '@nestjs/schedule': - specifier: ^4.1.2 - version: 4.1.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - drizzle-orm: - specifier: ^0.38.3 - version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0) - nestjs-telegraf: - specifier: ^2.8.0 - version: 2.9.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(telegraf@4.16.3(encoding@0.1.13))(typescript@5.9.3) - postgres: - specifier: ^3.4.5 - version: 3.4.7 - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - rxjs: - specifier: ^7.8.1 - version: 7.8.2 - telegraf: - specifier: ^4.16.3 - version: 4.16.3(encoding@0.1.13) - devDependencies: - '@nestjs/cli': - specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) - '@nestjs/schematics': - specifier: ^10.2.3 - version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) - '@types/node': - specifier: ^22.10.5 - version: 22.19.1 - rimraf: - specifier: ^6.0.1 - version: 6.1.2 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - services/telegram-todo-bot: - dependencies: - '@nestjs/common': - specifier: ^10.4.15 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/config': - specifier: ^3.3.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) - '@nestjs/core': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - '@nestjs/schedule': - specifier: ^4.1.2 - version: 4.1.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - drizzle-orm: - specifier: ^0.38.3 - version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0) - nestjs-telegraf: - specifier: ^2.8.0 - version: 2.9.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(telegraf@4.16.3(encoding@0.1.13))(typescript@5.9.3) - postgres: - specifier: ^3.4.5 - version: 3.4.7 - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - rxjs: - specifier: ^7.8.1 - version: 7.8.2 - telegraf: - specifier: ^4.16.3 - version: 4.16.3(encoding@0.1.13) - devDependencies: - '@manacore/shared-drizzle-config': - specifier: workspace:* - version: link:../../packages/shared-drizzle-config - '@nestjs/cli': - specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) - '@nestjs/schematics': - specifier: ^10.2.3 - version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) - '@types/node': - specifier: ^22.10.5 - version: 22.19.1 - drizzle-kit: - specifier: ^0.30.1 - version: 0.30.6 - rimraf: - specifier: ^6.0.1 - version: 6.1.2 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - - services/telegram-zitare-bot: - dependencies: - '@nestjs/common': - specifier: ^10.4.15 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/config': - specifier: ^3.3.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) - '@nestjs/core': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/platform-express': - specifier: ^10.4.15 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - '@nestjs/schedule': - specifier: ^4.1.2 - version: 4.1.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - drizzle-orm: - specifier: ^0.38.3 - version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.7)(expo-sqlite@15.2.14(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(kysely@0.28.8)(postgres@3.4.7)(react@19.1.0) - nestjs-telegraf: - specifier: ^2.8.0 - version: 2.9.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(telegraf@4.16.3(encoding@0.1.13))(typescript@5.9.3) - postgres: - specifier: ^3.4.5 - version: 3.4.7 - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - rxjs: - specifier: ^7.8.1 - version: 7.8.2 - telegraf: - specifier: ^4.16.3 - version: 4.16.3(encoding@0.1.13) - devDependencies: - '@manacore/shared-drizzle-config': - specifier: workspace:* - version: link:../../packages/shared-drizzle-config - '@nestjs/cli': - specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) - '@nestjs/schematics': - specifier: ^10.2.3 - version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) - '@types/node': - specifier: ^22.10.5 - version: 22.19.1 - drizzle-kit: - specifier: ^0.30.1 - version: 0.30.6 - rimraf: - specifier: ^6.0.1 - version: 6.1.2 - typescript: - specifier: ^5.7.3 - version: 5.9.3 - packages: '@0no-co/graphql.web@1.2.0': @@ -9179,7 +8849,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -12322,9 +11992,6 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 - '@telegraf/types@7.1.0': - resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==} - '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -19349,15 +19016,6 @@ packages: reflect-metadata: '*' rxjs: '>= 7' - nestjs-telegraf@2.9.1: - resolution: {integrity: sha512-iFy0beaRvEJWo4rtIfLK6mbrxlrhPsDKoXT/yyQDRfu9lmFKjada4bbR3COXua6VdbMd/NXIzMqSukJRsTkIjQ==} - peerDependencies: - '@nestjs/common': ^10.0.0 || ^11.0.0 - '@nestjs/core': ^10.0.0 || ^11.0.0 - reflect-metadata: ^0.2.2 - telegraf: ^4.0.0 - typescript: ^4.1.2 || ^5.0.0 - next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -19679,10 +19337,6 @@ packages: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} - p-timeout@4.1.0: - resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==} - engines: {node: '>=10'} - p-timeout@6.1.4: resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} engines: {node: '>=14.16'} @@ -21082,9 +20736,6 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-compare@1.1.4: - resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==} - safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -21100,10 +20751,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sandwich-stream@2.0.2: - resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==} - engines: {node: '>= 0.10'} - sanitize-html@2.17.0: resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} @@ -21730,11 +21377,6 @@ packages: tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} - telegraf@4.16.3: - resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==} - engines: {node: ^12.20.0 || >=14.13.1} - hasBin: true - temp-dir@2.0.0: resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} engines: {node: '>=8'} @@ -23657,6 +23299,16 @@ snapshots: transitivePeerDependencies: - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': + dependencies: + astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + autoprefixer: 10.4.22(postcss@8.5.6) + postcss: 8.5.6 + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -26726,7 +26378,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(vmxlpuhz6xqbe2ee7fdabyqx3y) + expo-router: 6.0.15(7mqaurqidri6vkknnsci36yp4e) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -28034,7 +27686,7 @@ snapshots: jest-util: 30.2.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': + '@jest/core@29.7.0': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -28048,7 +27700,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@22.19.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -28069,7 +27721,7 @@ snapshots: - supports-color - ts-node - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -28083,7 +27735,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -28622,6 +28274,32 @@ snapshots: - uglify-js - webpack-cli + '@nestjs/cli@10.4.9(esbuild@0.19.12)': + dependencies: + '@angular-devkit/core': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) + '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) + chalk: 4.1.2 + chokidar: 3.6.0 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)) + glob: 10.4.5 + inquirer: 8.2.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.7.2 + webpack: 5.97.1(esbuild@0.19.12) + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - esbuild + - uglify-js + - webpack-cli + '@nestjs/cli@10.4.9(esbuild@0.27.0)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -32206,8 +31884,6 @@ snapshots: tailwindcss: 4.1.17 vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) - '@telegraf/types@7.1.0': {} - '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.27.1 @@ -32219,6 +31895,19 @@ snapshots: picocolors: 1.1.1 pretty-format: 27.5.1 + '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + optional: true + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 @@ -32789,16 +32478,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -32847,15 +32536,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -32947,14 +32636,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -32986,14 +32675,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -33119,12 +32808,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -33155,12 +32844,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -33342,15 +33031,15 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -33381,13 +33070,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -34286,6 +33975,108 @@ snapshots: transitivePeerDependencies: - supports-color + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + '@astrojs/compiler': 2.13.0 + '@astrojs/internal-helpers': 0.7.5 + '@astrojs/markdown-remark': 6.3.9 + '@astrojs/telemetry': 3.3.0 + '@capsizecss/unpack': 3.0.1 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.3.1 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 1.1.0 + cssesc: 3.0.0 + debug: 4.4.3 + deterministic-object-hash: 2.0.2 + devalue: 5.5.0 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.25.12 + estree-walker: 3.0.3 + flattie: 1.1.1 + fontace: 0.3.1 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + import-meta-resolve: 4.2.0 + js-yaml: 4.1.1 + magic-string: 0.30.21 + magicast: 0.5.1 + mrmime: 2.0.1 + neotraverse: 0.6.18 + p-limit: 6.2.0 + p-queue: 8.1.1 + package-manager-detector: 1.5.0 + piccolore: 0.1.3 + picomatch: 4.0.3 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.7.3 + shiki: 3.15.0 + smol-toml: 1.5.2 + svgo: 4.0.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tsconfck: 3.1.6(typescript@5.9.3) + ultrahtml: 1.6.0 + unifont: 0.6.0 + unist-util-visit: 5.0.0 + unstorage: 1.17.3(@netlify/blobs@10.4.1)(ioredis@5.9.2) + vfile: 6.0.3 + vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.3 + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 @@ -35572,13 +35363,13 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + create-jest@29.7.0(@types/node@24.10.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -36682,6 +36473,11 @@ snapshots: escape-string-regexp@5.0.0: {} + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + semver: 7.7.3 + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -36692,9 +36488,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -36709,9 +36505,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -36729,14 +36525,14 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) + eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -36761,17 +36557,17 @@ snapshots: - supports-color - typescript - eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3): + eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@1.21.7)) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + eslint: 9.39.1(jiti@2.6.1) + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@2.6.1)) optionalDependencies: prettier: 3.6.2 transitivePeerDependencies: @@ -36809,7 +36605,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -36820,7 +36616,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -36834,12 +36645,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color @@ -36854,25 +36665,39 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@1.21.7)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@jridgewell/sourcemap-codec': 1.5.5 + '@typescript-eslint/types': 8.48.0 + astro-eslint-parser: 1.2.2 + eslint: 9.39.1(jiti@1.21.7) + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@1.21.7)) + globals: 16.5.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 transitivePeerDependencies: - supports-color @@ -36896,12 +36721,6 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-utils: 2.1.0 - regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -36955,7 +36774,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -36964,9 +36783,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -36978,7 +36797,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -37013,7 +36832,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -37024,7 +36843,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -37042,7 +36861,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -37053,7 +36872,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -37081,16 +36900,6 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-plugin-es: 3.0.1(eslint@9.39.1(jiti@1.21.7)) - eslint-utils: 2.1.0 - ignore: 5.3.2 - minimatch: 3.1.2 - resolve: 1.22.11 - semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37121,16 +36930,6 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 8.10.2(eslint@8.57.1) - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - prettier: 3.6.2 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 - optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37155,10 +36954,6 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -37189,28 +36984,6 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 9.39.1(jiti@1.21.7) - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)): dependencies: array-includes: 3.1.9 @@ -38353,6 +38126,53 @@ snapshots: - react-native - supports-color + expo-router@6.0.15(7mqaurqidri6vkknnsci36yp4e): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + optional: true + expo-router@6.0.15(k2muy65dii4k2uiuhg4mwyy6ki): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) @@ -39475,6 +39295,23 @@ snapshots: forever-agent@0.6.1: {} + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 8.3.6(typescript@5.7.2) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.3 + tapable: 2.3.0 + typescript: 5.7.2 + webpack: 5.97.1(esbuild@0.19.12) + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -40930,16 +40767,16 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-cli@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + create-jest: 29.7.0(@types/node@24.10.1) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-config: 29.7.0(@types/node@24.10.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -41027,6 +40864,36 @@ snapshots: - ts-node optional: true + jest-config@29.7.0(@types/node@22.19.1): + dependencies: + '@babel/core': 7.28.5 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -41058,38 +40925,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-config@29.7.0(@types/node@24.10.1): dependencies: '@babel/core': 7.28.5 '@jest/test-sequencer': 29.7.0 @@ -41115,7 +40951,6 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 24.10.1 - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -41702,12 +41537,12 @@ snapshots: - supports-color - ts-node - jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest@29.7.0(@types/node@24.10.1): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-cli: 29.7.0(@types/node@24.10.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -43975,15 +43810,6 @@ snapshots: reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-telegraf@2.9.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(reflect-metadata@0.2.2)(telegraf@4.16.3(encoding@0.1.13))(typescript@5.9.3): - dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - lodash: 4.17.21 - reflect-metadata: 0.2.2 - telegraf: 4.16.3(encoding@0.1.13) - typescript: 5.9.3 - next-tick@1.1.0: {} nice-try@1.0.5: {} @@ -44335,8 +44161,6 @@ snapshots: '@types/retry': 0.12.0 retry: 0.13.1 - p-timeout@4.1.0: {} - p-timeout@6.1.4: {} p-try@2.2.0: {} @@ -46092,6 +45916,16 @@ snapshots: webpack-sources: 3.3.3 optional: true + react-server-dom-webpack@19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + acorn-loose: 8.5.2 + neo-async: 2.6.2 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + webpack: 5.97.1(esbuild@0.19.12) + webpack-sources: 3.3.3 + optional: true + react-style-singleton@2.2.3(@types/react@18.3.27)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -46663,10 +46497,6 @@ snapshots: safe-buffer@5.2.1: {} - safe-compare@1.1.4: - dependencies: - buffer-alloc: 1.2.0 - safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -46682,8 +46512,6 @@ snapshots: safer-buffer@2.1.2: {} - sandwich-stream@2.0.2: {} - sanitize-html@2.17.0: dependencies: deepmerge: 4.3.1 @@ -47540,20 +47368,6 @@ snapshots: dependencies: bintrees: 1.0.2 - telegraf@4.16.3(encoding@0.1.13): - dependencies: - '@telegraf/types': 7.1.0 - abort-controller: 3.0.0 - debug: 4.4.3 - mri: 1.2.0 - node-fetch: 2.7.0(encoding@0.1.13) - p-timeout: 4.1.0 - safe-compare: 1.1.4 - sandwich-stream: 2.0.2 - transitivePeerDependencies: - - encoding - - supports-color - temp-dir@2.0.0: {} temp@0.8.4: @@ -47573,6 +47387,17 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 + terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + serialize-javascript: 6.0.2 + terser: 5.44.1 + webpack: 5.97.1(esbuild@0.19.12) + optionalDependencies: + esbuild: 0.19.12 + terser-webpack-plugin@5.3.14(esbuild@0.27.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -47773,6 +47598,27 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + esbuild: 0.19.12 + jest-util: 30.2.0 + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -47815,26 +47661,6 @@ snapshots: esbuild: 0.27.0 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.3 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - jest-util: 30.2.0 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: chalk: 4.1.2 @@ -47855,6 +47681,16 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 + semver: 7.7.3 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.97.1(esbuild@0.19.12) + ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -48535,6 +48371,23 @@ snapshots: lightningcss: 1.30.2 terser: 5.44.1 + vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.25 + fsevents: 2.3.3 + jiti: 1.21.7 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.20.6 + yaml: 2.8.1 + vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -48638,6 +48491,10 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): + optionalDependencies: + vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) @@ -49092,6 +48949,36 @@ snapshots: - esbuild - uglify-js + webpack@5.97.1(esbuild@0.19.12): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.15.0 + browserslist: 4.28.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.18.3 + es-module-lexer: 1.7.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.3.0 + terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)) + watchpack: 2.4.4 + webpack-sources: 3.3.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + webpack@5.97.1(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7 diff --git a/services/telegram-nutriphi-bot/.env.example b/services/telegram-nutriphi-bot/.env.example deleted file mode 100644 index a187689e8..000000000 --- a/services/telegram-nutriphi-bot/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# Server -PORT=3303 - -# Telegram -TELEGRAM_BOT_TOKEN=xxx # Bot Token from @BotFather -TELEGRAM_ALLOWED_USERS= # Optional: Comma-separated user IDs - -# Database -DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/nutriphi_bot - -# AI -GEMINI_API_KEY=xxx # Google AI Studio API Key diff --git a/services/telegram-nutriphi-bot/CLAUDE.md b/services/telegram-nutriphi-bot/CLAUDE.md deleted file mode 100644 index 3a94d3f45..000000000 --- a/services/telegram-nutriphi-bot/CLAUDE.md +++ /dev/null @@ -1,294 +0,0 @@ -# Telegram NutriPhi Bot - -Telegram Bot für NutriPhi - KI-gestützte Ernährungsanalyse per Foto oder Text. - -## Tech Stack - -- **Framework**: NestJS 10 -- **Telegram**: nestjs-telegraf + Telegraf -- **Database**: PostgreSQL + Drizzle ORM -- **AI**: Google Gemini 2.0 Flash - -## Commands - -```bash -# Development -pnpm start:dev # Start with hot reload - -# Build -pnpm build # Production build - -# Type check -pnpm type-check # Check TypeScript types - -# Database -pnpm db:generate # Generate migrations -pnpm db:push # Push schema to database -pnpm db:studio # Open Drizzle Studio -``` - -## Telegram Commands - -| Command | Beschreibung | -|---------|--------------| -| `/start` | Willkommensnachricht | -| `/hilfe` | Hilfe anzeigen | -| `/heute` | Heutige Mahlzeiten & Fortschritt | -| `/woche` | Wochenstatistik | -| `/ziele` | Ziele anzeigen | -| `/ziele [kcal] [P] [K] [F]` | Ziele setzen | -| `/favorit [Name]` | Letzte Mahlzeit speichern | -| `/favoriten` | Gespeicherte Mahlzeiten anzeigen | -| `/essen [Nr]` | Favorit als Mahlzeit eintragen | -| `/delfav [Nr]` | Favorit löschen | -| `/loeschen` | Letzte Mahlzeit löschen | -| **Foto senden** | Automatische Analyse | -| **Text senden** | Automatische Analyse | - -## User Flow - -``` -1. /start → Willkommen -2. 📷 Foto einer Mahlzeit senden → Nährwertanalyse -3. /favorit Morgenmüsli → Als Favorit speichern -4. /heute → Tagesübersicht -5. /ziele 2000 100 200 70 → Ziele setzen -6. /woche → Wochenstatistik -``` - -## Environment Variables - -```env -# Server -PORT=3303 - -# Telegram -TELEGRAM_BOT_TOKEN=xxx # Bot Token von @BotFather -TELEGRAM_ALLOWED_USERS= # Optional: Komma-separierte User IDs - -# Database -DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/nutriphi_bot - -# AI -GEMINI_API_KEY=xxx # Google AI Studio API Key -``` - -## Projekt-Struktur - -``` -services/telegram-nutriphi-bot/ -├── src/ -│ ├── main.ts # Entry point -│ ├── app.module.ts # Root module -│ ├── health.controller.ts # Health endpoint -│ ├── config/ -│ │ └── configuration.ts # Config -│ ├── database/ -│ │ ├── database.module.ts # Drizzle connection -│ │ └── schema.ts # DB schema -│ ├── bot/ -│ │ ├── bot.module.ts -│ │ └── bot.update.ts # Command handlers -│ ├── analysis/ -│ │ ├── analysis.module.ts -│ │ └── gemini.service.ts # Gemini AI Integration -│ ├── meals/ -│ │ ├── meals.module.ts -│ │ └── meals.service.ts # Mahlzeiten CRUD -│ ├── goals/ -│ │ ├── goals.module.ts -│ │ └── goals.service.ts # Nutzerziele -│ └── stats/ -│ ├── stats.module.ts -│ └── stats.service.ts # Statistiken -├── drizzle/ # Migrations -├── drizzle.config.ts -├── package.json -└── .env.example -``` - -## Lokale Entwicklung - -### 1. Bot bei Telegram erstellen - -1. Öffne @BotFather in Telegram -2. Sende `/newbot` -3. Wähle einen Namen (z.B. "NutriPhi Bot") -4. Wähle einen Username (z.B. "nutriphi_tracker_bot") -5. Kopiere den Token - -### 2. Gemini API Key holen - -1. Gehe zu https://aistudio.google.com/apikey -2. Erstelle einen API Key -3. Kopiere den Key - -### 3. Umgebung vorbereiten - -```bash -# Docker Services starten (PostgreSQL) -pnpm docker:up - -# Datenbank erstellen -psql -h localhost -U manacore -d postgres -c "CREATE DATABASE nutriphi_bot;" - -# In das Verzeichnis wechseln -cd services/telegram-nutriphi-bot - -# .env erstellen -cp .env.example .env -# Token und API Key eintragen - -# Schema pushen -pnpm db:push -``` - -### 4. Bot starten - -```bash -pnpm start:dev -``` - -## Features - -- **Foto-Analyse**: Mahlzeit fotografieren → Gemini analysiert → Nährwerte -- **Text-Analyse**: Mahlzeit beschreiben → Gemini schätzt → Nährwerte -- **Tages-Tracking**: Alle Mahlzeiten speichern, Tagesübersicht -- **Wochenstatistik**: 7-Tage-Übersicht mit Durchschnittswerten -- **Ziele**: Kalorienziel und Makros setzen -- **Favoriten**: Häufige Mahlzeiten speichern und wiederverwenden -- **Fortschrittsanzeige**: Visuelle Balken für Zielerreichung - -## Datenbank-Schema - -``` -user_goals -├── id (UUID) -├── telegram_user_id (BIGINT, unique) -├── daily_calories (INT, default 2000) -├── daily_protein (INT, default 50) -├── daily_carbs (INT, default 250) -├── daily_fat (INT, default 65) -├── daily_fiber (INT, default 30) -├── created_at, updated_at - -meals -├── id (UUID) -├── telegram_user_id (BIGINT) -├── date (DATE) -├── meal_type (TEXT: breakfast/lunch/dinner/snack) -├── input_type (TEXT: photo/text) -├── description (TEXT) -├── calories (INT) -├── protein, carbohydrates, fat, fiber, sugar (REAL) -├── confidence (REAL, 0-1) -├── raw_response (JSONB) -├── created_at - -favorite_meals -├── id (UUID) -├── telegram_user_id (BIGINT) -├── name (TEXT) -├── description (TEXT) -├── nutrition (JSONB) -├── usage_count (INT) -├── created_at -``` - -## Health Check - -```bash -curl http://localhost:3303/health -``` - -## Gemini Integration - -Der Bot verwendet Gemini 2.0 Flash für: - -1. **Foto-Analyse** - - Erkennt alle sichtbaren Lebensmittel - - Schätzt Portionsgrößen - - Berechnet Nährwerte pro Lebensmittel - - Summiert Gesamtnährwerte - -2. **Text-Analyse** - - Interpretiert Mahlzeitbeschreibungen - - Schätzt realistische Portionsgrößen - - Berechnet Nährwerte - -**Response-Format:** -```json -{ - "foods": [ - {"name": "Spaghetti", "quantity": "200g", "calories": 314, "confidence": 0.9}, - {"name": "Bolognese-Sauce", "quantity": "150g", "calories": 180, "confidence": 0.85} - ], - "totalNutrition": { - "calories": 494, - "protein": 22, - "carbohydrates": 65, - "fat": 15, - "fiber": 4, - "sugar": 8 - }, - "description": "Spaghetti Bolognese", - "confidence": 0.87 -} -``` - -## Beispiel-Ausgaben - -**Foto-Analyse:** -``` -🍽️ Spaghetti Bolognese mit Parmesan - -Erkannt: -• Spaghetti (200g) -• Bolognese-Sauce (150g) -• Parmesan (20g) - -Nährwerte: -Kalorien: 580 kcal -Protein: 28g -Kohlenhydrate: 68g -Fett: 20g -Ballaststoffe: 5g -Zucker: 8g - -Genauigkeit: 87% - -Als Favorit speichern: /favorit [Name] -``` - -**Tagesübersicht (/heute):** -``` -📊 Heute (28.01.2026) - -1. Frühstück (08:15) - Haferflocken mit Banane und Milch - 420 kcal - -2. Mittagessen (12:30) - Spaghetti Bolognese - 580 kcal - -───────────────── -Gesamt: 1000 kcal - -Fortschritt: -Kalorien: ████████░░ 50% -Protein: ██████░░░░ 60% -Kohlenhydr.: ███████░░░ 70% -Fett: █████░░░░░ 50% - -Verbleibend: 1000 kcal -``` - -## Roadmap - -- [ ] Mahlzeit-Typ manuell wählen -- [ ] Foto-Beschreibung als Caption -- [ ] Mehrere Fotos pro Mahlzeit -- [ ] Export als CSV/JSON -- [ ] Erinnerungen für Mahlzeiten -- [ ] Wassertracking diff --git a/services/telegram-nutriphi-bot/drizzle.config.ts b/services/telegram-nutriphi-bot/drizzle.config.ts deleted file mode 100644 index fe1ff4bb4..000000000 --- a/services/telegram-nutriphi-bot/drizzle.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createDrizzleConfig } from '@manacore/shared-drizzle-config'; - -export default createDrizzleConfig({ - dbName: 'nutriphi_bot', - schemaPath: './src/database/schema.ts', - outDir: './drizzle', - verbose: false, - strict: false, -}); diff --git a/services/telegram-nutriphi-bot/nest-cli.json b/services/telegram-nutriphi-bot/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/services/telegram-nutriphi-bot/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/services/telegram-nutriphi-bot/package.json b/services/telegram-nutriphi-bot/package.json deleted file mode 100644 index 027d9281a..000000000 --- a/services/telegram-nutriphi-bot/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@manacore/telegram-nutriphi-bot", - "version": "1.0.0", - "description": "Telegram bot for NutriPhi - AI-powered nutrition tracking", - "private": true, - "license": "MIT", - "scripts": { - "prebuild": "rimraf dist", - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit", - "db:generate": "drizzle-kit generate", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" - }, - "dependencies": { - "@google/generative-ai": "^0.21.0", - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "drizzle-orm": "^0.38.3", - "nestjs-telegraf": "^2.8.0", - "postgres": "^3.4.5", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "telegraf": "^4.16.3" - }, - "devDependencies": { - "@manacore/shared-drizzle-config": "workspace:*", - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@types/node": "^22.10.5", - "drizzle-kit": "^0.30.1", - "rimraf": "^6.0.1", - "typescript": "^5.7.3" - } -} diff --git a/services/telegram-nutriphi-bot/src/analysis/analysis.module.ts b/services/telegram-nutriphi-bot/src/analysis/analysis.module.ts deleted file mode 100644 index 748ea23ee..000000000 --- a/services/telegram-nutriphi-bot/src/analysis/analysis.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { GeminiService } from './gemini.service'; - -@Module({ - providers: [GeminiService], - exports: [GeminiService], -}) -export class AnalysisModule {} diff --git a/services/telegram-nutriphi-bot/src/analysis/gemini.service.ts b/services/telegram-nutriphi-bot/src/analysis/gemini.service.ts deleted file mode 100644 index c97ce8cb1..000000000 --- a/services/telegram-nutriphi-bot/src/analysis/gemini.service.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { GoogleGenerativeAI, type GenerativeModel } from '@google/generative-ai'; - -export interface AnalysisFood { - name: string; - quantity: string; - calories: number; - confidence: number; -} - -export interface AnalysisResult { - foods: AnalysisFood[]; - totalNutrition: { - calories: number; - protein: number; - carbohydrates: number; - fat: number; - fiber: number; - sugar: number; - }; - description: string; - confidence: number; - warnings?: string[]; -} - -const PHOTO_ANALYSIS_PROMPT = `Du bist ein Ernährungsexperte. Analysiere das Bild dieser Mahlzeit und liefere eine detaillierte Nährwertanalyse. - -Aufgaben: -1. Identifiziere alle sichtbaren Lebensmittel -2. Schätze die Portionsgröße (in Gramm) basierend auf visuellen Hinweisen -3. Berechne die Nährwerte für jedes Lebensmittel -4. Summiere die Gesamtnährwerte - -Antworte NUR mit einem validen JSON-Objekt im folgenden Format: -{ - "foods": [ - { - "name": "Lebensmittelname", - "quantity": "geschätzte Menge (z.B. '150g', '1 Tasse')", - "calories": 123, - "confidence": 0.85 - } - ], - "totalNutrition": { - "calories": 500, - "protein": 25, - "carbohydrates": 60, - "fat": 15, - "fiber": 5, - "sugar": 10 - }, - "description": "Kurze Beschreibung der Mahlzeit auf Deutsch", - "confidence": 0.8, - "warnings": ["Optional: Warnungen falls etwas unklar ist"] -} - -Wichtig: -- Alle Nährwerte als Zahlen (keine Strings) -- Kalorien in kcal -- Protein, Kohlenhydrate, Fett, Ballaststoffe, Zucker in Gramm -- Confidence-Werte zwischen 0 und 1 -- Beschreibung auf Deutsch`; - -const TEXT_ANALYSIS_PROMPT = `Du bist ein Ernährungsexperte. Analysiere die folgende Mahlzeitbeschreibung und liefere eine Nährwertschätzung. - -Mahlzeit: {INPUT} - -Antworte NUR mit einem validen JSON-Objekt im folgenden Format: -{ - "foods": [ - { - "name": "Lebensmittelname", - "quantity": "geschätzte Menge", - "calories": 123, - "confidence": 0.85 - } - ], - "totalNutrition": { - "calories": 500, - "protein": 25, - "carbohydrates": 60, - "fat": 15, - "fiber": 5, - "sugar": 10 - }, - "description": "Aufbereitete Beschreibung der Mahlzeit", - "confidence": 0.75 -} - -Wichtig: -- Alle Nährwerte als Zahlen (keine Strings) -- Kalorien in kcal -- Protein, Kohlenhydrate, Fett, Ballaststoffe, Zucker in Gramm -- Confidence-Werte zwischen 0 und 1 -- Beschreibung auf Deutsch -- Schätze realistische Portionsgrößen`; - -@Injectable() -export class GeminiService implements OnModuleInit { - private readonly logger = new Logger(GeminiService.name); - private model: GenerativeModel | null = null; - - constructor(private configService: ConfigService) {} - - onModuleInit() { - const apiKey = this.configService.get('gemini.apiKey'); - if (apiKey) { - const genAI = new GoogleGenerativeAI(apiKey); - this.model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp' }); - this.logger.log('Gemini service initialized'); - } else { - this.logger.warn('Gemini API key not configured'); - } - } - - isAvailable(): boolean { - return this.model !== null; - } - - async analyzeImage(imageBase64: string, mimeType = 'image/jpeg'): Promise { - if (!this.model) { - throw new Error('Gemini API nicht konfiguriert'); - } - - this.logger.log('Analyzing image...'); - - const result = await this.model.generateContent([ - PHOTO_ANALYSIS_PROMPT, - { - inlineData: { - mimeType, - data: imageBase64, - }, - }, - ]); - - const response = result.response; - const text = response.text(); - - return this.parseResponse(text); - } - - async analyzeText(description: string): Promise { - if (!this.model) { - throw new Error('Gemini API nicht konfiguriert'); - } - - this.logger.log(`Analyzing text: ${description.substring(0, 50)}...`); - - const prompt = TEXT_ANALYSIS_PROMPT.replace('{INPUT}', description); - const result = await this.model.generateContent(prompt); - - const response = result.response; - const text = response.text(); - - return this.parseResponse(text); - } - - private parseResponse(text: string): AnalysisResult { - // Extract JSON from response (handle markdown code blocks) - const jsonMatch = text.match(/\{[\s\S]*\}/); - if (!jsonMatch) { - this.logger.error('Failed to parse response:', text); - throw new Error('Konnte Antwort nicht parsen'); - } - - try { - return JSON.parse(jsonMatch[0]) as AnalysisResult; - } catch (error) { - this.logger.error('JSON parse error:', error); - throw new Error('Ungültiges JSON in Antwort'); - } - } -} diff --git a/services/telegram-nutriphi-bot/src/app.module.ts b/services/telegram-nutriphi-bot/src/app.module.ts deleted file mode 100644 index 58c1dc80e..000000000 --- a/services/telegram-nutriphi-bot/src/app.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { TelegrafModule } from 'nestjs-telegraf'; -import configuration from './config/configuration'; -import { DatabaseModule } from './database/database.module'; -import { BotModule } from './bot/bot.module'; -import { HealthController } from './health.controller'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [configuration], - }), - TelegrafModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - token: configService.get('telegram.token') || '', - }), - inject: [ConfigService], - }), - DatabaseModule, - BotModule, - ], - controllers: [HealthController], -}) -export class AppModule {} diff --git a/services/telegram-nutriphi-bot/src/bot/bot.module.ts b/services/telegram-nutriphi-bot/src/bot/bot.module.ts deleted file mode 100644 index 6bff5098d..000000000 --- a/services/telegram-nutriphi-bot/src/bot/bot.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BotUpdate } from './bot.update'; -import { AnalysisModule } from '../analysis/analysis.module'; -import { MealsModule } from '../meals/meals.module'; -import { GoalsModule } from '../goals/goals.module'; -import { StatsModule } from '../stats/stats.module'; - -@Module({ - imports: [AnalysisModule, MealsModule, GoalsModule, StatsModule], - providers: [BotUpdate], -}) -export class BotModule {} diff --git a/services/telegram-nutriphi-bot/src/bot/bot.update.ts b/services/telegram-nutriphi-bot/src/bot/bot.update.ts deleted file mode 100644 index 3179823ba..000000000 --- a/services/telegram-nutriphi-bot/src/bot/bot.update.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { Update, Ctx, Start, Help, Command, On, Message } from 'nestjs-telegraf'; -import { Context } from 'telegraf'; -import { ConfigService } from '@nestjs/config'; -import { GeminiService } from '../analysis/gemini.service'; -import { MealsService } from '../meals/meals.service'; -import { GoalsService } from '../goals/goals.service'; -import { StatsService } from '../stats/stats.service'; -import { MEAL_TYPES, MealType } from '../config/configuration'; -import { Meal, NutritionData } from '../database/schema'; - -interface PhotoSize { - file_id: string; - file_unique_id: string; - width: number; - height: number; - file_size?: number; -} - -@Update() -export class BotUpdate { - private readonly logger = new Logger(BotUpdate.name); - private readonly allowedUsers: number[]; - private readonly telegramApiUrl: string; - - // Track last meal for /favorit command - private lastMeal: Map = new Map(); - - constructor( - private readonly geminiService: GeminiService, - private readonly mealsService: MealsService, - private readonly goalsService: GoalsService, - private readonly statsService: StatsService, - private configService: ConfigService - ) { - this.allowedUsers = this.configService.get('telegram.allowedUsers') || []; - const token = this.configService.get('telegram.token'); - this.telegramApiUrl = `https://api.telegram.org/bot${token}`; - } - - private isAllowed(userId: number): boolean { - if (this.allowedUsers.length === 0) return true; - return this.allowedUsers.includes(userId); - } - - private formatHelp(): string { - return `🥗 NutriPhi Bot - -Dein KI-gestützter Ernährungs-Tracker. - -Mahlzeit erfassen: -📷 Foto senden - Automatische Analyse -💬 Text senden - z.B. "Spaghetti Bolognese" - -Übersicht: -/heute - Heutige Mahlzeiten & Fortschritt -/woche - Wochenstatistik - -Ziele: -/ziele - Aktuelle Ziele anzeigen -/ziele [kcal] [P] [K] [F] - Ziele setzen - Beispiel: /ziele 2000 100 200 70 - -Favoriten: -/favorit [Name] - Letzte Mahlzeit speichern -/favoriten - Gespeicherte Mahlzeiten anzeigen -/essen [Nr] - Favorit als Mahlzeit eintragen -/delfav [Nr] - Favorit löschen - -Sonstiges: -/loeschen - Letzte Mahlzeit löschen -/hilfe - Diese Hilfe anzeigen - -Tipp: Starte mit einem Foto deiner Mahlzeit!`; - } - - @Start() - async start(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - // Ensure user has goals - await this.goalsService.ensureGoals(userId); - - this.logger.log(`/start from user ${userId}`); - await ctx.replyWithHTML(this.formatHelp()); - } - - @Help() - async help(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - await ctx.replyWithHTML(this.formatHelp()); - } - - @Command('hilfe') - async hilfe(@Ctx() ctx: Context) { - await this.help(ctx); - } - - @Command('heute') - async today(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const summary = await this.statsService.getDailySummary(userId); - - if (summary.meals.length === 0) { - await ctx.reply( - '📭 Noch keine Mahlzeiten heute.\n\nSende ein Foto oder beschreibe deine Mahlzeit!' - ); - return; - } - - // Format meals list - const mealsList = summary.meals - .map((m, i) => { - const type = MEAL_TYPES[m.mealType as MealType] || m.mealType; - const time = new Date(m.createdAt).toLocaleTimeString('de-DE', { - hour: '2-digit', - minute: '2-digit', - }); - return `${i + 1}. ${type} (${time})\n ${m.description}\n ${m.calories} kcal`; - }) - .join('\n\n'); - - // Format totals and progress - let response = - `📊 Heute (${new Date().toLocaleDateString('de-DE')})\n\n` + - `${mealsList}\n\n` + - `─────────────────\n` + - `Gesamt: ${summary.totals.calories} kcal\n\n`; - - if (summary.goals) { - response += - `Fortschritt:\n` + - `Kalorien: ${StatsService.formatProgressBar(summary.progress.calories)}\n` + - `Protein: ${StatsService.formatProgressBar(summary.progress.protein)}\n` + - `Kohlenhydr.: ${StatsService.formatProgressBar(summary.progress.carbohydrates)}\n` + - `Fett: ${StatsService.formatProgressBar(summary.progress.fat)}\n\n` + - `Verbleibend: ${Math.max(0, summary.goals.dailyCalories - summary.totals.calories)} kcal`; - } - - await ctx.replyWithHTML(response); - } - - @Command('woche') - async week(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const summary = await this.statsService.getWeeklySummary(userId); - - if (summary.totalMeals === 0) { - await ctx.reply('📭 Keine Mahlzeiten in den letzten 7 Tagen.'); - return; - } - - // Format days chart - const maxCal = Math.max(...summary.days.map((d) => d.calories), 1); - const dayNames = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; - - const chart = summary.days - .map((d) => { - const date = new Date(d.date); - const dayName = dayNames[date.getDay()]; - const barLen = Math.round((d.calories / maxCal) * 8); - const bar = '█'.repeat(barLen) + '░'.repeat(8 - barLen); - return `${dayName} ${bar} ${d.calories}`; - }) - .join('\n'); - - const response = - `📈 Wochenübersicht\n\n` + - `${chart}\n\n` + - `Durchschnitt:\n` + - `Kalorien: ${summary.averages.calories} kcal\n` + - `Protein: ${summary.averages.protein}g\n` + - `Kohlenhydrate: ${summary.averages.carbohydrates}g\n` + - `Fett: ${summary.averages.fat}g\n\n` + - `Gesamt: ${summary.totalMeals} Mahlzeiten`; - - await ctx.replyWithHTML(response); - } - - @Command('ziele') - async goals(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const args = text.replace('/ziele', '').trim(); - - // If no args, show current goals - if (!args) { - const goals = await this.goalsService.ensureGoals(userId); - await ctx.replyWithHTML( - `🎯 Deine Tagesziele\n\n` + - `Kalorien: ${goals.dailyCalories} kcal\n` + - `Protein: ${goals.dailyProtein}g\n` + - `Kohlenhydrate: ${goals.dailyCarbs}g\n` + - `Fett: ${goals.dailyFat}g\n` + - `Ballaststoffe: ${goals.dailyFiber}g\n\n` + - `Ändern:\n/ziele [kcal] [Protein] [Kohlenhydrate] [Fett]\nBeispiel: /ziele 2000 100 200 70` - ); - return; - } - - // Parse new goals - const parts = args.split(/\s+/).map((n) => parseInt(n, 10)); - if (parts.length < 4 || parts.some(isNaN)) { - await ctx.reply( - 'Verwendung: /ziele [kcal] [Protein] [Kohlenhydrate] [Fett]\n\n' + - 'Beispiel: /ziele 2000 100 200 70' - ); - return; - } - - const [calories, protein, carbs, fat] = parts; - const fiber = parts[4] || 30; // Optional 5th parameter - - await this.goalsService.setGoals(userId, { - dailyCalories: calories, - dailyProtein: protein, - dailyCarbs: carbs, - dailyFat: fat, - dailyFiber: fiber, - }); - - await ctx.replyWithHTML( - `✅ Ziele aktualisiert!\n\n` + - `Kalorien: ${calories} kcal\n` + - `Protein: ${protein}g\n` + - `Kohlenhydrate: ${carbs}g\n` + - `Fett: ${fat}g\n` + - `Ballaststoffe: ${fiber}g` - ); - } - - @Command('favorit') - async saveFavorite(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const name = text.replace('/favorit', '').trim(); - if (!name) { - await ctx.reply('Verwendung: /favorit [Name]\n\nBeispiel: /favorit Morgenmüsli'); - return; - } - - const lastMeal = this.lastMeal.get(userId); - if (!lastMeal) { - await ctx.reply('Keine aktuelle Mahlzeit zum Speichern.\n\nErfasse erst eine Mahlzeit.'); - return; - } - - await this.mealsService.saveAsFavorite(userId, lastMeal, name); - await ctx.reply(`⭐ "${name}" als Favorit gespeichert!`); - } - - @Command('favoriten') - async listFavorites(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const favorites = await this.mealsService.getFavorites(userId); - - if (favorites.length === 0) { - await ctx.reply( - 'Keine Favoriten gespeichert.\n\n' + 'Speichere eine Mahlzeit mit /favorit [Name]' - ); - return; - } - - const list = favorites - .map((f, i) => { - const nutrition = f.nutrition as NutritionData; - return `${i + 1}. ${f.name}\n ${nutrition.calories} kcal | ${nutrition.protein}g P | ${nutrition.carbohydrates}g K | ${nutrition.fat}g F`; - }) - .join('\n\n'); - - await ctx.replyWithHTML( - `⭐ Deine Favoriten\n\n${list}\n\n` + `Verwenden: /essen [Nr]\nLöschen: /delfav [Nr]` - ); - } - - @Command('essen') - async useFavorite(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const indexStr = text.replace('/essen', '').trim(); - const index = parseInt(indexStr, 10); - - if (!indexStr || isNaN(index)) { - await ctx.reply('Verwendung: /essen [Nr]\n\nZeige Favoriten mit /favoriten'); - return; - } - - const favorite = await this.mealsService.getFavoriteByIndex(userId, index); - if (!favorite) { - await ctx.reply(`Favorit #${index} nicht gefunden.`); - return; - } - - const meal = await this.mealsService.createFromFavorite(userId, favorite); - this.lastMeal.set(userId, meal); - - const nutrition = favorite.nutrition as NutritionData; - await ctx.replyWithHTML( - `✅ ${favorite.name} eingetragen!\n\n` + - `${nutrition.calories} kcal | ${nutrition.protein}g P | ${nutrition.carbohydrates}g K | ${nutrition.fat}g F\n\n` + - `Übersicht: /heute` - ); - } - - @Command('delfav') - async deleteFavorite(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const indexStr = text.replace('/delfav', '').trim(); - const index = parseInt(indexStr, 10); - - if (!indexStr || isNaN(index)) { - await ctx.reply('Verwendung: /delfav [Nr]\n\nZeige Favoriten mit /favoriten'); - return; - } - - const favorite = await this.mealsService.getFavoriteByIndex(userId, index); - if (!favorite) { - await ctx.reply(`Favorit #${index} nicht gefunden.`); - return; - } - - await this.mealsService.deleteFavorite(favorite.id); - await ctx.reply(`✅ "${favorite.name}" gelöscht.`); - } - - @Command('loeschen') - async deleteLastMeal(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const deleted = await this.mealsService.deleteLastMeal(userId); - if (deleted) { - this.lastMeal.delete(userId); - await ctx.reply('✅ Letzte Mahlzeit gelöscht.'); - } else { - await ctx.reply('Keine Mahlzeit zum Löschen gefunden.'); - } - } - - @On('photo') - async onPhoto(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - if (!this.geminiService.isAvailable()) { - await ctx.reply('❌ Analyse nicht verfügbar (API nicht konfiguriert).'); - return; - } - - const message = ctx.message as { photo?: PhotoSize[]; caption?: string }; - const photos = message.photo; - if (!photos || photos.length === 0) return; - - // Get largest photo - const photo = photos[photos.length - 1]; - - await ctx.reply('🔍 Analysiere Mahlzeit...'); - await ctx.sendChatAction('typing'); - - try { - // Download photo from Telegram - const imageBase64 = await this.downloadTelegramFile(photo.file_id); - - // Analyze with Gemini - const analysis = await this.geminiService.analyzeImage(imageBase64); - - // Save meal - const meal = await this.mealsService.createFromAnalysis(userId, 'photo', analysis); - this.lastMeal.set(userId, meal); - - // Format response - const foodsList = analysis.foods.map((f) => `• ${f.name} (${f.quantity})`).join('\n'); - - const n = analysis.totalNutrition; - const confidence = Math.round(analysis.confidence * 100); - - await ctx.replyWithHTML( - `🍽️ ${analysis.description}\n\n` + - `Erkannt:\n${foodsList}\n\n` + - `Nährwerte:\n` + - `Kalorien: ${n.calories} kcal\n` + - `Protein: ${n.protein}g\n` + - `Kohlenhydrate: ${n.carbohydrates}g\n` + - `Fett: ${n.fat}g\n` + - `Ballaststoffe: ${n.fiber}g\n` + - `Zucker: ${n.sugar}g\n\n` + - `Genauigkeit: ${confidence}%\n\n` + - `Als Favorit speichern: /favorit [Name]` - ); - } catch (error) { - this.logger.error('Photo analysis failed:', error); - const message = error instanceof Error ? error.message : 'Unbekannter Fehler'; - await ctx.reply(`❌ Analyse fehlgeschlagen: ${message}`); - } - } - - @On('text') - async onText(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - // Ignore commands - if (text.startsWith('/')) return; - - if (!this.geminiService.isAvailable()) { - await ctx.reply('❌ Analyse nicht verfügbar (API nicht konfiguriert).'); - return; - } - - // Analyze text as meal description - await ctx.reply('🔍 Analysiere...'); - await ctx.sendChatAction('typing'); - - try { - const analysis = await this.geminiService.analyzeText(text); - - // Save meal - const meal = await this.mealsService.createFromAnalysis(userId, 'text', analysis); - this.lastMeal.set(userId, meal); - - // Format response - const n = analysis.totalNutrition; - const confidence = Math.round(analysis.confidence * 100); - - await ctx.replyWithHTML( - `✅ ${analysis.description}\n\n` + - `Nährwerte:\n` + - `Kalorien: ${n.calories} kcal\n` + - `Protein: ${n.protein}g\n` + - `Kohlenhydrate: ${n.carbohydrates}g\n` + - `Fett: ${n.fat}g\n` + - `Ballaststoffe: ${n.fiber}g\n` + - `Zucker: ${n.sugar}g\n\n` + - `Genauigkeit: ${confidence}%\n\n` + - `Als Favorit speichern: /favorit [Name]` - ); - } catch (error) { - this.logger.error('Text analysis failed:', error); - const message = error instanceof Error ? error.message : 'Unbekannter Fehler'; - await ctx.reply(`❌ Analyse fehlgeschlagen: ${message}`); - } - } - - // Download file from Telegram and return Base64 - private async downloadTelegramFile(fileId: string): Promise { - // Get file path - const fileResponse = await fetch(`${this.telegramApiUrl}/getFile?file_id=${fileId}`); - const fileData = await fileResponse.json(); - - if (!fileData.ok) { - throw new Error(`Telegram API error: ${fileData.description}`); - } - - // Download file - const token = this.configService.get('telegram.token'); - const fileUrl = `https://api.telegram.org/file/bot${token}/${fileData.result.file_path}`; - - const response = await fetch(fileUrl); - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - return buffer.toString('base64'); - } -} diff --git a/services/telegram-nutriphi-bot/src/config/configuration.ts b/services/telegram-nutriphi-bot/src/config/configuration.ts deleted file mode 100644 index 8706a6ad8..000000000 --- a/services/telegram-nutriphi-bot/src/config/configuration.ts +++ /dev/null @@ -1,35 +0,0 @@ -export default () => ({ - port: parseInt(process.env.PORT || '3303', 10), - telegram: { - token: process.env.TELEGRAM_BOT_TOKEN, - allowedUsers: process.env.TELEGRAM_ALLOWED_USERS - ? process.env.TELEGRAM_ALLOWED_USERS.split(',').map((id) => parseInt(id.trim(), 10)) - : [], - }, - database: { - url: - process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/nutriphi_bot', - }, - gemini: { - apiKey: process.env.GEMINI_API_KEY, - }, -}); - -// Meal type labels -export const MEAL_TYPES = { - breakfast: 'Frühstück', - lunch: 'Mittagessen', - dinner: 'Abendessen', - snack: 'Snack', -} as const; - -export type MealType = keyof typeof MEAL_TYPES; - -// Get suggested meal type based on current time -export function suggestMealType(): MealType { - const hour = new Date().getHours(); - if (hour >= 5 && hour < 11) return 'breakfast'; - if (hour >= 11 && hour < 15) return 'lunch'; - if (hour >= 17 && hour < 21) return 'dinner'; - return 'snack'; -} diff --git a/services/telegram-nutriphi-bot/src/database/database.module.ts b/services/telegram-nutriphi-bot/src/database/database.module.ts deleted file mode 100644 index 905674bf6..000000000 --- a/services/telegram-nutriphi-bot/src/database/database.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from './schema'; - -export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_CONNECTION, - useFactory: (configService: ConfigService) => { - const connectionString = configService.get('database.url'); - const client = postgres(connectionString!); - return drizzle(client, { schema }); - }, - inject: [ConfigService], - }, - ], - exports: [DATABASE_CONNECTION], -}) -export class DatabaseModule {} diff --git a/services/telegram-nutriphi-bot/src/database/schema.ts b/services/telegram-nutriphi-bot/src/database/schema.ts deleted file mode 100644 index 53a64c2c8..000000000 --- a/services/telegram-nutriphi-bot/src/database/schema.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - pgTable, - uuid, - text, - timestamp, - bigint, - integer, - real, - date, - jsonb, -} from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; - -// User goals - daily nutrition targets -export const userGoals = pgTable('user_goals', { - id: uuid('id').primaryKey().defaultRandom(), - telegramUserId: bigint('telegram_user_id', { mode: 'number' }).notNull().unique(), - dailyCalories: integer('daily_calories').default(2000).notNull(), - dailyProtein: integer('daily_protein').default(50).notNull(), - dailyCarbs: integer('daily_carbs').default(250).notNull(), - dailyFat: integer('daily_fat').default(65).notNull(), - dailyFiber: integer('daily_fiber').default(30).notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}); - -// Meals - tracked meals with nutrition data -export const meals = pgTable('meals', { - id: uuid('id').primaryKey().defaultRandom(), - telegramUserId: bigint('telegram_user_id', { mode: 'number' }).notNull(), - date: date('date').notNull(), - mealType: text('meal_type').notNull(), // breakfast, lunch, dinner, snack - inputType: text('input_type').notNull(), // photo, text - description: text('description'), - calories: integer('calories').default(0).notNull(), - protein: real('protein').default(0).notNull(), - carbohydrates: real('carbohydrates').default(0).notNull(), - fat: real('fat').default(0).notNull(), - fiber: real('fiber').default(0).notNull(), - sugar: real('sugar').default(0).notNull(), - confidence: real('confidence').default(0).notNull(), - rawResponse: jsonb('raw_response'), - createdAt: timestamp('created_at').defaultNow().notNull(), -}); - -// Favorite meals - saved for quick re-use -export const favoriteMeals = pgTable('favorite_meals', { - id: uuid('id').primaryKey().defaultRandom(), - telegramUserId: bigint('telegram_user_id', { mode: 'number' }).notNull(), - name: text('name').notNull(), - description: text('description'), - nutrition: jsonb('nutrition').notNull(), - usageCount: integer('usage_count').default(0).notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), -}); - -// Relations -export const userGoalsRelations = relations(userGoals, ({ many }) => ({ - meals: many(meals), - favorites: many(favoriteMeals), -})); - -export const mealsRelations = relations(meals, ({ one }) => ({ - userGoals: one(userGoals, { - fields: [meals.telegramUserId], - references: [userGoals.telegramUserId], - }), -})); - -export const favoriteMealsRelations = relations(favoriteMeals, ({ one }) => ({ - userGoals: one(userGoals, { - fields: [favoriteMeals.telegramUserId], - references: [userGoals.telegramUserId], - }), -})); - -// Types -export type UserGoals = typeof userGoals.$inferSelect; -export type NewUserGoals = typeof userGoals.$inferInsert; -export type Meal = typeof meals.$inferSelect; -export type NewMeal = typeof meals.$inferInsert; -export type FavoriteMeal = typeof favoriteMeals.$inferSelect; -export type NewFavoriteMeal = typeof favoriteMeals.$inferInsert; - -// Nutrition data structure for favorites -export interface NutritionData { - calories: number; - protein: number; - carbohydrates: number; - fat: number; - fiber: number; - sugar: number; -} diff --git a/services/telegram-nutriphi-bot/src/goals/goals.module.ts b/services/telegram-nutriphi-bot/src/goals/goals.module.ts deleted file mode 100644 index f9c681e2e..000000000 --- a/services/telegram-nutriphi-bot/src/goals/goals.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { GoalsService } from './goals.service'; - -@Module({ - providers: [GoalsService], - exports: [GoalsService], -}) -export class GoalsModule {} diff --git a/services/telegram-nutriphi-bot/src/goals/goals.service.ts b/services/telegram-nutriphi-bot/src/goals/goals.service.ts deleted file mode 100644 index 895862ea8..000000000 --- a/services/telegram-nutriphi-bot/src/goals/goals.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { eq } from 'drizzle-orm'; -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import * as schema from '../database/schema'; -import { UserGoals, NewUserGoals } from '../database/schema'; - -@Injectable() -export class GoalsService { - private readonly logger = new Logger(GoalsService.name); - - constructor( - @Inject(DATABASE_CONNECTION) - private db: PostgresJsDatabase - ) {} - - async getGoals(telegramUserId: number): Promise { - const goals = await this.db.query.userGoals.findFirst({ - where: eq(schema.userGoals.telegramUserId, telegramUserId), - }); - return goals || null; - } - - async ensureGoals(telegramUserId: number): Promise { - let goals = await this.getGoals(telegramUserId); - if (!goals) { - const [newGoals] = await this.db - .insert(schema.userGoals) - .values({ telegramUserId }) - .returning(); - goals = newGoals; - this.logger.log(`Created default goals for user ${telegramUserId}`); - } - return goals; - } - - async setGoals( - telegramUserId: number, - data: Partial> - ): Promise { - // Ensure user has goals first - await this.ensureGoals(telegramUserId); - - const [updated] = await this.db - .update(schema.userGoals) - .set({ - ...data, - updatedAt: new Date(), - }) - .where(eq(schema.userGoals.telegramUserId, telegramUserId)) - .returning(); - - this.logger.log(`Updated goals for user ${telegramUserId}`); - return updated; - } -} diff --git a/services/telegram-nutriphi-bot/src/health.controller.ts b/services/telegram-nutriphi-bot/src/health.controller.ts deleted file mode 100644 index ce9e400bc..000000000 --- a/services/telegram-nutriphi-bot/src/health.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller('health') -export class HealthController { - @Get() - check() { - return { - status: 'ok', - service: 'telegram-nutriphi-bot', - timestamp: new Date().toISOString(), - }; - } -} diff --git a/services/telegram-nutriphi-bot/src/main.ts b/services/telegram-nutriphi-bot/src/main.ts deleted file mode 100644 index cbc00255d..000000000 --- a/services/telegram-nutriphi-bot/src/main.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const logger = new Logger('Bootstrap'); - const app = await NestFactory.create(AppModule); - - const configService = app.get(ConfigService); - const port = configService.get('port') || 3303; - - await app.listen(port); - logger.log(`Telegram NutriPhi Bot running on port ${port}`); - logger.log(`Health check: http://localhost:${port}/health`); -} - -bootstrap(); diff --git a/services/telegram-nutriphi-bot/src/meals/meals.module.ts b/services/telegram-nutriphi-bot/src/meals/meals.module.ts deleted file mode 100644 index 38c743520..000000000 --- a/services/telegram-nutriphi-bot/src/meals/meals.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MealsService } from './meals.service'; - -@Module({ - providers: [MealsService], - exports: [MealsService], -}) -export class MealsModule {} diff --git a/services/telegram-nutriphi-bot/src/meals/meals.service.ts b/services/telegram-nutriphi-bot/src/meals/meals.service.ts deleted file mode 100644 index 4fa0dea2b..000000000 --- a/services/telegram-nutriphi-bot/src/meals/meals.service.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { eq, and, sql } from 'drizzle-orm'; -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import * as schema from '../database/schema'; -import { Meal, NewMeal, FavoriteMeal, NutritionData } from '../database/schema'; -import { AnalysisResult } from '../analysis/gemini.service'; -import { MealType, suggestMealType } from '../config/configuration'; - -@Injectable() -export class MealsService { - private readonly logger = new Logger(MealsService.name); - - constructor( - @Inject(DATABASE_CONNECTION) - private db: PostgresJsDatabase - ) {} - - // Create a meal from analysis result - async createFromAnalysis( - telegramUserId: number, - inputType: 'photo' | 'text', - analysis: AnalysisResult, - mealType?: MealType - ): Promise { - const today = new Date().toISOString().split('T')[0]; - - const [meal] = await this.db - .insert(schema.meals) - .values({ - telegramUserId, - date: today, - mealType: mealType || suggestMealType(), - inputType, - description: analysis.description, - calories: analysis.totalNutrition.calories, - protein: analysis.totalNutrition.protein, - carbohydrates: analysis.totalNutrition.carbohydrates, - fat: analysis.totalNutrition.fat, - fiber: analysis.totalNutrition.fiber, - sugar: analysis.totalNutrition.sugar, - confidence: analysis.confidence, - rawResponse: analysis, - }) - .returning(); - - this.logger.log(`Created meal for user ${telegramUserId}: ${analysis.description}`); - return meal; - } - - // Create a meal from favorite - async createFromFavorite(telegramUserId: number, favorite: FavoriteMeal): Promise { - const today = new Date().toISOString().split('T')[0]; - const nutrition = favorite.nutrition as NutritionData; - - const [meal] = await this.db - .insert(schema.meals) - .values({ - telegramUserId, - date: today, - mealType: suggestMealType(), - inputType: 'text', - description: favorite.name, - calories: nutrition.calories, - protein: nutrition.protein, - carbohydrates: nutrition.carbohydrates, - fat: nutrition.fat, - fiber: nutrition.fiber, - sugar: nutrition.sugar, - confidence: 1.0, // From saved data, so high confidence - }) - .returning(); - - // Increment usage count - await this.db - .update(schema.favoriteMeals) - .set({ - usageCount: sql`${schema.favoriteMeals.usageCount} + 1`, - }) - .where(eq(schema.favoriteMeals.id, favorite.id)); - - this.logger.log(`Created meal from favorite for user ${telegramUserId}: ${favorite.name}`); - return meal; - } - - // Get meals for a specific date - async getMealsByDate(telegramUserId: number, date: string): Promise { - return this.db.query.meals.findMany({ - where: and(eq(schema.meals.telegramUserId, telegramUserId), eq(schema.meals.date, date)), - orderBy: (meals, { asc }) => [asc(meals.createdAt)], - }); - } - - // Get today's meals - async getTodaysMeals(telegramUserId: number): Promise { - const today = new Date().toISOString().split('T')[0]; - return this.getMealsByDate(telegramUserId, today); - } - - // Delete last meal - async deleteLastMeal(telegramUserId: number): Promise { - const todaysMeals = await this.getTodaysMeals(telegramUserId); - if (todaysMeals.length === 0) return false; - - const lastMeal = todaysMeals[todaysMeals.length - 1]; - await this.db.delete(schema.meals).where(eq(schema.meals.id, lastMeal.id)); - - this.logger.log(`Deleted last meal for user ${telegramUserId}`); - return true; - } - - // Save meal as favorite - async saveAsFavorite(telegramUserId: number, meal: Meal, name: string): Promise { - const nutrition: NutritionData = { - calories: meal.calories, - protein: meal.protein, - carbohydrates: meal.carbohydrates, - fat: meal.fat, - fiber: meal.fiber, - sugar: meal.sugar, - }; - - const [favorite] = await this.db - .insert(schema.favoriteMeals) - .values({ - telegramUserId, - name, - description: meal.description, - nutrition, - }) - .returning(); - - this.logger.log(`Saved favorite for user ${telegramUserId}: ${name}`); - return favorite; - } - - // Get all favorites - async getFavorites(telegramUserId: number): Promise { - return this.db.query.favoriteMeals.findMany({ - where: eq(schema.favoriteMeals.telegramUserId, telegramUserId), - orderBy: (fav, { desc }) => [desc(fav.usageCount), desc(fav.createdAt)], - }); - } - - // Get favorite by index (1-based for user display) - async getFavoriteByIndex(telegramUserId: number, index: number): Promise { - const favorites = await this.getFavorites(telegramUserId); - if (index < 1 || index > favorites.length) return null; - return favorites[index - 1]; - } - - // Delete favorite - async deleteFavorite(favoriteId: string): Promise { - const result = await this.db - .delete(schema.favoriteMeals) - .where(eq(schema.favoriteMeals.id, favoriteId)); - return (result as unknown as { rowCount: number }).rowCount > 0; - } -} diff --git a/services/telegram-nutriphi-bot/src/stats/stats.module.ts b/services/telegram-nutriphi-bot/src/stats/stats.module.ts deleted file mode 100644 index a4e4375c0..000000000 --- a/services/telegram-nutriphi-bot/src/stats/stats.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { StatsService } from './stats.service'; - -@Module({ - providers: [StatsService], - exports: [StatsService], -}) -export class StatsModule {} diff --git a/services/telegram-nutriphi-bot/src/stats/stats.service.ts b/services/telegram-nutriphi-bot/src/stats/stats.service.ts deleted file mode 100644 index f6ee5f6b5..000000000 --- a/services/telegram-nutriphi-bot/src/stats/stats.service.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { eq, and, gte, lte, sql } from 'drizzle-orm'; -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import * as schema from '../database/schema'; -import { Meal, UserGoals } from '../database/schema'; - -export interface DailySummary { - date: string; - meals: Meal[]; - totals: { - calories: number; - protein: number; - carbohydrates: number; - fat: number; - fiber: number; - sugar: number; - }; - goals: UserGoals | null; - progress: { - calories: number; - protein: number; - carbohydrates: number; - fat: number; - fiber: number; - }; -} - -export interface WeeklySummary { - startDate: string; - endDate: string; - days: { - date: string; - calories: number; - mealsCount: number; - }[]; - averages: { - calories: number; - protein: number; - carbohydrates: number; - fat: number; - }; - totalMeals: number; -} - -@Injectable() -export class StatsService { - private readonly logger = new Logger(StatsService.name); - - constructor( - @Inject(DATABASE_CONNECTION) - private db: PostgresJsDatabase - ) {} - - // Get daily summary for a user - async getDailySummary(telegramUserId: number, date?: string): Promise { - const targetDate = date || new Date().toISOString().split('T')[0]; - - // Get meals for the day - const meals = await this.db.query.meals.findMany({ - where: and( - eq(schema.meals.telegramUserId, telegramUserId), - eq(schema.meals.date, targetDate) - ), - orderBy: (meals, { asc }) => [asc(meals.createdAt)], - }); - - // Get user goals - const goals = await this.db.query.userGoals.findFirst({ - where: eq(schema.userGoals.telegramUserId, telegramUserId), - }); - - // Calculate totals - const totals = meals.reduce( - (acc, meal) => ({ - calories: acc.calories + meal.calories, - protein: acc.protein + meal.protein, - carbohydrates: acc.carbohydrates + meal.carbohydrates, - fat: acc.fat + meal.fat, - fiber: acc.fiber + meal.fiber, - sugar: acc.sugar + meal.sugar, - }), - { calories: 0, protein: 0, carbohydrates: 0, fat: 0, fiber: 0, sugar: 0 } - ); - - // Calculate progress (percentage of goals) - const progress = { - calories: goals ? (totals.calories / goals.dailyCalories) * 100 : 0, - protein: goals ? (totals.protein / goals.dailyProtein) * 100 : 0, - carbohydrates: goals ? (totals.carbohydrates / goals.dailyCarbs) * 100 : 0, - fat: goals ? (totals.fat / goals.dailyFat) * 100 : 0, - fiber: goals ? (totals.fiber / goals.dailyFiber) * 100 : 0, - }; - - return { - date: targetDate, - meals, - totals, - goals: goals || null, - progress, - }; - } - - // Get weekly summary - async getWeeklySummary(telegramUserId: number): Promise { - const endDate = new Date(); - const startDate = new Date(); - startDate.setDate(startDate.getDate() - 6); - - const startStr = startDate.toISOString().split('T')[0]; - const endStr = endDate.toISOString().split('T')[0]; - - // Get all meals for the week - const meals = await this.db.query.meals.findMany({ - where: and( - eq(schema.meals.telegramUserId, telegramUserId), - gte(schema.meals.date, startStr), - lte(schema.meals.date, endStr) - ), - }); - - // Group by date - const byDate = new Map< - string, - { calories: number; protein: number; carbohydrates: number; fat: number; count: number } - >(); - - // Initialize all 7 days - for (let i = 0; i < 7; i++) { - const d = new Date(startDate); - d.setDate(d.getDate() + i); - const dateStr = d.toISOString().split('T')[0]; - byDate.set(dateStr, { calories: 0, protein: 0, carbohydrates: 0, fat: 0, count: 0 }); - } - - // Sum up meals - for (const meal of meals) { - const existing = byDate.get(meal.date) || { - calories: 0, - protein: 0, - carbohydrates: 0, - fat: 0, - count: 0, - }; - byDate.set(meal.date, { - calories: existing.calories + meal.calories, - protein: existing.protein + meal.protein, - carbohydrates: existing.carbohydrates + meal.carbohydrates, - fat: existing.fat + meal.fat, - count: existing.count + 1, - }); - } - - // Convert to array - const days = Array.from(byDate.entries()).map(([date, data]) => ({ - date, - calories: Math.round(data.calories), - mealsCount: data.count, - })); - - // Calculate averages (only for days with meals) - const daysWithMeals = Array.from(byDate.values()).filter((d) => d.count > 0); - const numDays = daysWithMeals.length || 1; - - const averages = { - calories: Math.round(daysWithMeals.reduce((sum, d) => sum + d.calories, 0) / numDays), - protein: Math.round(daysWithMeals.reduce((sum, d) => sum + d.protein, 0) / numDays), - carbohydrates: Math.round( - daysWithMeals.reduce((sum, d) => sum + d.carbohydrates, 0) / numDays - ), - fat: Math.round(daysWithMeals.reduce((sum, d) => sum + d.fat, 0) / numDays), - }; - - return { - startDate: startStr, - endDate: endStr, - days, - averages, - totalMeals: meals.length, - }; - } - - // Get progress bar for display - static formatProgressBar(percentage: number, length = 10): string { - const capped = Math.min(percentage, 100); - const filled = Math.round((capped / 100) * length); - const empty = length - filled; - const bar = '█'.repeat(filled) + '░'.repeat(empty); - - // Add indicator if over goal - const indicator = percentage > 100 ? ' ⚠️' : ''; - return `${bar} ${Math.round(percentage)}%${indicator}`; - } -} diff --git a/services/telegram-nutriphi-bot/tsconfig.json b/services/telegram-nutriphi-bot/tsconfig.json deleted file mode 100644 index edf10cd0d..000000000 --- a/services/telegram-nutriphi-bot/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2021", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true - } -} diff --git a/services/telegram-ollama-bot/CLAUDE.md b/services/telegram-ollama-bot/CLAUDE.md deleted file mode 100644 index 37b537fe1..000000000 --- a/services/telegram-ollama-bot/CLAUDE.md +++ /dev/null @@ -1,130 +0,0 @@ -# Telegram Ollama Bot - -Telegram Bot für lokale LLM-Inferenz via Ollama auf dem Mac Mini Server. - -## Tech Stack - -- **Framework**: NestJS 10 -- **Telegram**: nestjs-telegraf + Telegraf -- **LLM**: mana-llm service (supports Ollama + cloud providers) - -## Commands - -```bash -# Development -pnpm start:dev # Start with hot reload - -# Build -pnpm build # Production build - -# Type check -pnpm type-check # Check TypeScript types -``` - -## Telegram Commands - -| Command | Beschreibung | -|---------|--------------| -| `/start` | Hilfe anzeigen | -| `/help` | Hilfe anzeigen | -| `/models` | Verfügbare Modelle auflisten | -| `/model [name]` | Modell wechseln | -| `/mode [modus]` | System-Prompt ändern | -| `/clear` | Chat-Verlauf löschen | -| `/status` | Ollama-Status prüfen | - -## Modi - -| Modus | Beschreibung | -|-------|--------------| -| `default` | Allgemeiner Assistent | -| `classify` | Text-Klassifizierung | -| `summarize` | Zusammenfassungen | -| `translate` | Übersetzungen | -| `code` | Programmier-Hilfe | - -## Environment Variables - -```env -# Server -PORT=3301 - -# Telegram -TELEGRAM_BOT_TOKEN=xxx # Bot Token von @BotFather -TELEGRAM_ALLOWED_USERS=123,456 # Optional: Nur diese User IDs erlauben - -# LLM (via mana-llm service) -MANA_LLM_URL=http://localhost:3025 # mana-llm service URL -LLM_MODEL=ollama/gemma3:4b # Standard-Modell (provider/model format) -LLM_TIMEOUT=120000 # Timeout in ms -``` - -## Projekt-Struktur - -``` -services/telegram-ollama-bot/ -├── src/ -│ ├── main.ts # Entry point -│ ├── app.module.ts # Root module -│ ├── health.controller.ts # Health endpoint -│ ├── config/ -│ │ └── configuration.ts # Config & System Prompts -│ ├── bot/ -│ │ ├── bot.module.ts -│ │ └── bot.update.ts # Command handlers -│ └── ollama/ -│ ├── ollama.module.ts -│ └── ollama.service.ts # Ollama API client -└── Dockerfile -``` - -## Deployment auf Mac Mini - -### Option 1: Docker - -```yaml -# In docker-compose.macmini.yml -telegram-ollama-bot: - image: ghcr.io/memo-2023/telegram-ollama-bot:latest - container_name: manacore-telegram-ollama-bot - restart: always - environment: - PORT: 3301 - TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} - MANA_LLM_URL: http://mana-llm:3025 - LLM_MODEL: ollama/gemma3:4b - ports: - - "3301:3301" -``` - -### Option 2: Nativ - -```bash -# Auf dem Mac Mini -cd ~/projects/manacore-monorepo/services/telegram-ollama-bot -pnpm install -pnpm build -TELEGRAM_BOT_TOKEN=xxx MANA_LLM_URL=http://localhost:3025 pnpm start:prod -``` - -## Neuen Bot erstellen - -1. Öffne @BotFather in Telegram -2. Sende `/newbot` -3. Wähle einen Namen (z.B. "ManaCore Ollama") -4. Wähle einen Username (z.B. "manacore_ollama_bot") -5. Kopiere den Token - -## Health Check - -```bash -curl http://localhost:3301/health -``` - -## Features - -- **Chat-Verlauf**: Behält die letzten 10 Nachrichten für Kontext -- **Mehrere Modi**: Verschiedene System-Prompts für unterschiedliche Aufgaben -- **Modell-Wechsel**: Dynamisch zwischen installierten Modellen wechseln -- **User-Beschränkung**: Optional nur bestimmte Telegram-User erlauben -- **Lange Antworten**: Automatisches Splitting bei >4000 Zeichen diff --git a/services/telegram-ollama-bot/Dockerfile b/services/telegram-ollama-bot/Dockerfile deleted file mode 100644 index 6ecb3e028..000000000 --- a/services/telegram-ollama-bot/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -# Build stage -FROM node:20-alpine AS builder - -WORKDIR /app - -# Install pnpm -RUN npm install -g pnpm - -# Copy package files -COPY package.json pnpm-lock.yaml* ./ - -# Install dependencies -RUN pnpm install --frozen-lockfile || pnpm install - -# Copy source -COPY . . - -# Build -RUN pnpm build - -# Production stage -FROM node:20-alpine AS runner - -WORKDIR /app - -# Install pnpm -RUN npm install -g pnpm - -# Copy package files -COPY package.json pnpm-lock.yaml* ./ - -# Install production dependencies only -RUN pnpm install --prod --frozen-lockfile || pnpm install --prod - -# Copy built application -COPY --from=builder /app/dist ./dist - -# Set environment -ENV NODE_ENV=production -ENV PORT=3301 - -EXPOSE 3301 - -CMD ["node", "dist/main.js"] diff --git a/services/telegram-ollama-bot/nest-cli.json b/services/telegram-ollama-bot/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/services/telegram-ollama-bot/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/services/telegram-ollama-bot/package.json b/services/telegram-ollama-bot/package.json deleted file mode 100644 index 17cc0e3d1..000000000 --- a/services/telegram-ollama-bot/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@manacore/telegram-ollama-bot", - "version": "1.0.0", - "description": "Telegram bot for local LLM inference via Ollama", - "private": true, - "license": "MIT", - "scripts": { - "prebuild": "rimraf dist", - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "nestjs-telegraf": "^2.8.0", - "telegraf": "^4.16.3", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@types/node": "^22.10.5", - "rimraf": "^6.0.1", - "typescript": "^5.7.3" - } -} diff --git a/services/telegram-ollama-bot/src/app.module.ts b/services/telegram-ollama-bot/src/app.module.ts deleted file mode 100644 index 1f3fb0920..000000000 --- a/services/telegram-ollama-bot/src/app.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { TelegrafModule } from 'nestjs-telegraf'; -import configuration from './config/configuration'; -import { BotModule } from './bot/bot.module'; -import { OllamaModule } from './ollama/ollama.module'; -import { HealthController } from './health.controller'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [configuration], - }), - TelegrafModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - token: configService.get('telegram.token') || '', - }), - inject: [ConfigService], - }), - BotModule, - OllamaModule, - ], - controllers: [HealthController], -}) -export class AppModule {} diff --git a/services/telegram-ollama-bot/src/bot/bot.module.ts b/services/telegram-ollama-bot/src/bot/bot.module.ts deleted file mode 100644 index 38eec038a..000000000 --- a/services/telegram-ollama-bot/src/bot/bot.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BotUpdate } from './bot.update'; -import { OllamaModule } from '../ollama/ollama.module'; - -@Module({ - imports: [OllamaModule], - providers: [BotUpdate], -}) -export class BotModule {} diff --git a/services/telegram-ollama-bot/src/bot/bot.update.ts b/services/telegram-ollama-bot/src/bot/bot.update.ts deleted file mode 100644 index e210845db..000000000 --- a/services/telegram-ollama-bot/src/bot/bot.update.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { Update, Ctx, Start, Help, Command, On, Message } from 'nestjs-telegraf'; -import { Context } from 'telegraf'; -import { ConfigService } from '@nestjs/config'; -import { OllamaService } from '../ollama/ollama.service'; -import { SYSTEM_PROMPTS } from '../config/configuration'; - -interface UserSession { - systemPrompt: string; - model: string; - history: { role: 'user' | 'assistant'; content: string }[]; -} - -@Update() -export class BotUpdate { - private readonly logger = new Logger(BotUpdate.name); - private readonly allowedUsers: number[]; - private sessions: Map = new Map(); - - constructor( - private readonly ollamaService: OllamaService, - private configService: ConfigService - ) { - this.allowedUsers = this.configService.get('telegram.allowedUsers') || []; - } - - private isAllowed(userId: number): boolean { - // If no users configured, allow all - if (this.allowedUsers.length === 0) return true; - return this.allowedUsers.includes(userId); - } - - private getSession(userId: number): UserSession { - if (!this.sessions.has(userId)) { - this.sessions.set(userId, { - systemPrompt: SYSTEM_PROMPTS.default, - model: this.ollamaService.getDefaultModel(), - history: [], - }); - } - return this.sessions.get(userId)!; - } - - private formatHelp(): string { - return `Ollama Bot - Lokale KI - -Commands: -/start - Diese Hilfe anzeigen -/help - Diese Hilfe anzeigen -/models - Verfügbare Modelle anzeigen -/model [name] - Modell wechseln -/mode [modus] - System-Prompt ändern -/clear - Chat-Verlauf löschen -/status - Ollama Status prüfen - -Modi: -• default - Allgemeiner Assistent -• classify - Text-Klassifizierung -• summarize - Zusammenfassungen -• translate - Übersetzungen -• code - Programmier-Hilfe - -Verwendung: -Schreibe einfach eine Nachricht und ich antworte! - -Aktuelles Modell: ${this.ollamaService.getDefaultModel()}`; - } - - @Start() - async start(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - this.logger.log(`/start from user ${userId}`); - await ctx.replyWithHTML(this.formatHelp()); - } - - @Help() - async help(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - await ctx.replyWithHTML(this.formatHelp()); - } - - @Command('models') - async models(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - this.logger.log(`/models from user ${userId}`); - - const models = await this.ollamaService.listModels(); - if (models.length === 0) { - await ctx.reply('Keine Modelle gefunden. Ist Ollama gestartet?'); - return; - } - - const session = this.getSession(userId); - const modelList = models - .map((m) => { - const sizeMB = (m.size / 1024 / 1024).toFixed(0); - const active = m.name === session.model ? ' ✓' : ''; - return `• ${m.name} (${sizeMB} MB)${active}`; - }) - .join('\n'); - - await ctx.replyWithHTML( - `Verfügbare Modelle:\n\n${modelList}\n\nWechseln mit: /model [name]` - ); - } - - @Command('model') - async setModel(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const modelName = text.replace('/model', '').trim(); - if (!modelName) { - const session = this.getSession(userId); - await ctx.reply(`Aktuelles Modell: ${session.model}\n\nVerwendung: /model gemma3:4b`); - return; - } - - const models = await this.ollamaService.listModels(); - const exists = models.some((m) => m.name === modelName); - - if (!exists) { - await ctx.reply( - `Modell "${modelName}" nicht gefunden. Verfügbar: ${models.map((m) => m.name).join(', ')}` - ); - return; - } - - const session = this.getSession(userId); - session.model = modelName; - session.history = []; // Clear history on model change - - this.logger.log(`User ${userId} switched to model ${modelName}`); - await ctx.reply(`Modell gewechselt zu: ${modelName}`); - } - - @Command('mode') - async setMode(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const mode = text.replace('/mode', '').trim().toLowerCase(); - const availableModes = Object.keys(SYSTEM_PROMPTS); - - if (!mode) { - const session = this.getSession(userId); - const currentMode = - Object.entries(SYSTEM_PROMPTS).find(([_, v]) => v === session.systemPrompt)?.[0] || - 'custom'; - await ctx.reply(`Aktueller Modus: ${currentMode}\n\nVerfügbar: ${availableModes.join(', ')}`); - return; - } - - if (!SYSTEM_PROMPTS[mode]) { - await ctx.reply(`Unbekannter Modus: ${mode}\n\nVerfügbar: ${availableModes.join(', ')}`); - return; - } - - const session = this.getSession(userId); - session.systemPrompt = SYSTEM_PROMPTS[mode]; - session.history = []; // Clear history on mode change - - this.logger.log(`User ${userId} switched to mode ${mode}`); - await ctx.reply(`Modus gewechselt zu: ${mode}`); - } - - @Command('clear') - async clear(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const session = this.getSession(userId); - session.history = []; - - this.logger.log(`User ${userId} cleared history`); - await ctx.reply('Chat-Verlauf gelöscht.'); - } - - @Command('status') - async status(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const connected = await this.ollamaService.checkConnection(); - const models = await this.ollamaService.listModels(); - const session = this.getSession(userId); - - const statusText = `Ollama Status - -Verbindung: ${connected ? '✅ Online' : '❌ Offline'} -Modelle: ${models.length} -Dein Modell: ${session.model} -Chat-Verlauf: ${session.history.length} Nachrichten`; - - await ctx.replyWithHTML(statusText); - } - - @On('text') - async onMessage(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - // Ignore commands - if (text.startsWith('/')) return; - - this.logger.log(`Message from user ${userId}: ${text.substring(0, 50)}...`); - - const session = this.getSession(userId); - - // Show typing indicator - await ctx.sendChatAction('typing'); - - try { - // Add user message to history - session.history.push({ role: 'user', content: text }); - - // Keep only last 10 messages to avoid context overflow - if (session.history.length > 10) { - session.history = session.history.slice(-10); - } - - // Build messages with system prompt - const messages: { role: 'user' | 'assistant' | 'system'; content: string }[] = [ - { role: 'system', content: session.systemPrompt }, - ...session.history, - ]; - - const response = await this.ollamaService.chat(messages, session.model); - - // Add assistant response to history - session.history.push({ role: 'assistant', content: response }); - - // Split long messages (Telegram limit is 4096 chars) - if (response.length > 4000) { - const chunks = response.match(/.{1,4000}/gs) || []; - for (const chunk of chunks) { - await ctx.reply(chunk); - } - } else { - await ctx.reply(response); - } - } catch (error) { - this.logger.error(`Error processing message:`, error); - const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler'; - await ctx.reply(`Fehler: ${errorMessage}`); - } - } -} diff --git a/services/telegram-ollama-bot/src/config/configuration.ts b/services/telegram-ollama-bot/src/config/configuration.ts deleted file mode 100644 index be2728189..000000000 --- a/services/telegram-ollama-bot/src/config/configuration.ts +++ /dev/null @@ -1,25 +0,0 @@ -export default () => ({ - port: parseInt(process.env.PORT || '3301', 10), - telegram: { - token: process.env.TELEGRAM_BOT_TOKEN, - allowedUsers: - process.env.TELEGRAM_ALLOWED_USERS?.split(',').map((id) => parseInt(id, 10)) || [], - }, - llm: { - url: process.env.MANA_LLM_URL || 'http://localhost:3025', - model: process.env.LLM_MODEL || 'ollama/gemma3:4b', - timeout: parseInt(process.env.LLM_TIMEOUT || '120000', 10), - }, -}); - -export const SYSTEM_PROMPTS: Record = { - default: - 'Du bist ein hilfreicher Assistent. Antworte präzise und auf Deutsch, wenn der User Deutsch schreibt.', - classify: - 'Du bist ein Klassifikations-Experte. Analysiere den gegebenen Text und ordne ihn einer passenden Kategorie zu. Antworte kurz und präzise.', - summarize: - 'Du bist ein Zusammenfassungs-Experte. Fasse den gegebenen Text kurz und prägnant zusammen. Behalte die wichtigsten Informationen bei.', - translate: - 'Du bist ein Übersetzer. Übersetze den Text in die gewünschte Sprache. Behalte den Ton und Stil bei.', - code: 'Du bist ein Programmier-Assistent. Hilf bei Code-Fragen, erkläre Konzepte und schlage Verbesserungen vor.', -}; diff --git a/services/telegram-ollama-bot/src/health.controller.ts b/services/telegram-ollama-bot/src/health.controller.ts deleted file mode 100644 index e6128cf81..000000000 --- a/services/telegram-ollama-bot/src/health.controller.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { OllamaService } from './ollama/ollama.service'; - -@Controller() -export class HealthController { - constructor(private readonly ollamaService: OllamaService) {} - - @Get('health') - async health() { - const ollamaConnected = await this.ollamaService.checkConnection(); - - return { - status: ollamaConnected ? 'ok' : 'degraded', - timestamp: new Date().toISOString(), - ollama: { - connected: ollamaConnected, - model: this.ollamaService.getDefaultModel(), - }, - }; - } -} diff --git a/services/telegram-ollama-bot/src/main.ts b/services/telegram-ollama-bot/src/main.ts deleted file mode 100644 index 0f444fde6..000000000 --- a/services/telegram-ollama-bot/src/main.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const logger = new Logger('Bootstrap'); - const app = await NestFactory.create(AppModule); - - const configService = app.get(ConfigService); - const port = configService.get('port') || 3301; - - await app.listen(port); - logger.log(`Telegram Ollama Bot running on port ${port}`); - logger.log(`Ollama URL: ${configService.get('ollama.url')}`); - logger.log(`Default model: ${configService.get('ollama.model')}`); -} - -bootstrap(); diff --git a/services/telegram-ollama-bot/src/ollama/ollama.module.ts b/services/telegram-ollama-bot/src/ollama/ollama.module.ts deleted file mode 100644 index a0ae211c4..000000000 --- a/services/telegram-ollama-bot/src/ollama/ollama.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { OllamaService } from './ollama.service'; - -@Module({ - providers: [OllamaService], - exports: [OllamaService], -}) -export class OllamaModule {} diff --git a/services/telegram-ollama-bot/src/ollama/ollama.service.ts b/services/telegram-ollama-bot/src/ollama/ollama.service.ts deleted file mode 100644 index 751378d7c..000000000 --- a/services/telegram-ollama-bot/src/ollama/ollama.service.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; - -interface LlmModel { - id: string; - owned_by: string; -} - -interface ChatCompletionResponse { - id: string; - model: string; - choices: { - message: { role: string; content: string }; - finish_reason: string; - }[]; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; -} - -@Injectable() -export class OllamaService implements OnModuleInit { - private readonly logger = new Logger(OllamaService.name); - private readonly baseUrl: string; - private readonly defaultModel: string; - private readonly timeout: number; - - constructor(private configService: ConfigService) { - this.baseUrl = this.configService.get('llm.url') || 'http://localhost:3025'; - this.defaultModel = this.configService.get('llm.model') || 'ollama/gemma3:4b'; - this.timeout = this.configService.get('llm.timeout') || 120000; - } - - async onModuleInit() { - await this.checkConnection(); - } - - async checkConnection(): Promise { - try { - const response = await fetch(`${this.baseUrl}/health`, { - signal: AbortSignal.timeout(5000), - }); - const data = await response.json(); - this.logger.log( - `mana-llm connected: ${data.status}, providers: ${Object.keys(data.providers || {}).join(', ')}` - ); - return data.status === 'healthy' || data.status === 'degraded'; - } catch (error) { - this.logger.error(`Failed to connect to mana-llm at ${this.baseUrl}:`, error); - return false; - } - } - - async listModels(): Promise<{ name: string; size: number; modified_at: string }[]> { - try { - const response = await fetch(`${this.baseUrl}/v1/models`); - const data = await response.json(); - - // Convert OpenAI format to legacy Ollama format for compatibility - return (data.data || []).map((m: LlmModel) => ({ - name: m.id, - size: 0, - modified_at: new Date().toISOString(), - })); - } catch (error) { - this.logger.error('Failed to list models:', error); - return []; - } - } - - async generate(prompt: string, systemPrompt?: string, model?: string): Promise { - const selectedModel = model ? this.normalizeModel(model) : this.defaultModel; - - // Convert generate to chat format - const messages: { role: 'user' | 'assistant' | 'system'; content: string }[] = []; - if (systemPrompt) { - messages.push({ role: 'system', content: systemPrompt }); - } - messages.push({ role: 'user', content: prompt }); - - try { - const response = await fetch(`${this.baseUrl}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: selectedModel, - messages, - stream: false, - }), - signal: AbortSignal.timeout(this.timeout), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`mana-llm API error: ${response.status} - ${errorText}`); - } - - const data: ChatCompletionResponse = await response.json(); - - // Log performance metrics - if (data.usage) { - this.logger.debug( - `Generated ${data.usage.completion_tokens} tokens (total: ${data.usage.total_tokens})` - ); - } - - return data.choices[0]?.message?.content || ''; - } catch (error) { - if (error instanceof Error && error.name === 'TimeoutError') { - throw new Error('LLM Timeout - Antwort dauerte zu lange'); - } - throw error; - } - } - - async chat( - messages: { role: 'user' | 'assistant' | 'system'; content: string }[], - model?: string - ): Promise { - const selectedModel = model ? this.normalizeModel(model) : this.defaultModel; - - try { - const response = await fetch(`${this.baseUrl}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: selectedModel, - messages, - stream: false, - }), - signal: AbortSignal.timeout(this.timeout), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`mana-llm API error: ${response.status} - ${errorText}`); - } - - const data: ChatCompletionResponse = await response.json(); - - if (data.usage) { - this.logger.debug( - `Generated ${data.usage.completion_tokens} tokens (total: ${data.usage.total_tokens})` - ); - } - - return data.choices[0]?.message?.content || ''; - } catch (error) { - if (error instanceof Error && error.name === 'TimeoutError') { - throw new Error('LLM Timeout - Antwort dauerte zu lange'); - } - throw error; - } - } - - getDefaultModel(): string { - return this.defaultModel; - } - - /** - * Normalize model name to include provider prefix if missing. - */ - private normalizeModel(model: string): string { - if (model.includes('/')) { - return model; - } - return `ollama/${model}`; - } -} diff --git a/services/telegram-ollama-bot/tsconfig.json b/services/telegram-ollama-bot/tsconfig.json deleted file mode 100644 index 94f1e9493..000000000 --- a/services/telegram-ollama-bot/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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 - } -} diff --git a/services/telegram-project-doc-bot/.env.example b/services/telegram-project-doc-bot/.env.example deleted file mode 100644 index ecc6c0fc0..000000000 --- a/services/telegram-project-doc-bot/.env.example +++ /dev/null @@ -1,29 +0,0 @@ -# Server -PORT=3302 - -# Telegram -TELEGRAM_BOT_TOKEN=your-bot-token-from-botfather -TELEGRAM_ALLOWED_USERS= # Optional: comma-separated user IDs - -# Database -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/projectdoc - -# Storage (MinIO) -S3_ENDPOINT=http://localhost:9000 -S3_REGION=us-east-1 -S3_ACCESS_KEY=minioadmin -S3_SECRET_KEY=minioadmin -S3_BUCKET=projectdoc-storage - -# AI - Transcription (STT) -STT_PROVIDER=local # local | openai -STT_LOCAL_URL=http://localhost:3020 # mana-stt service URL -STT_MODEL=whisper # whisper | voxtral - -# OpenAI (optional fallback for STT, required if STT_PROVIDER=openai) -OPENAI_API_KEY=sk-your-openai-key - -# AI - Generation -LLM_PROVIDER=ollama # ollama | openai -OLLAMA_URL=http://localhost:11435 # Use :11435 for metrics proxy, :11434 for direct -OLLAMA_MODEL=gemma3:4b diff --git a/services/telegram-project-doc-bot/CLAUDE.md b/services/telegram-project-doc-bot/CLAUDE.md deleted file mode 100644 index 625ec6efd..000000000 --- a/services/telegram-project-doc-bot/CLAUDE.md +++ /dev/null @@ -1,245 +0,0 @@ -# Telegram Project Doc Bot - -Telegram Bot zum Sammeln von Projektdokumentation (Fotos, Sprachnotizen, Text) und automatischer Blogbeitrag-Generierung. - -## Tech Stack - -- **Framework**: NestJS 10 -- **Telegram**: nestjs-telegraf + Telegraf -- **Database**: PostgreSQL + Drizzle ORM -- **Storage**: S3 (MinIO lokal, Hetzner in Produktion) -- **AI - Transcription**: OpenAI Whisper -- **AI - Generation**: mana-llm service oder OpenAI GPT - -## Commands - -```bash -# Development -pnpm start:dev # Start with hot reload - -# Build -pnpm build # Production build - -# Type check -pnpm type-check # Check TypeScript types - -# Database -pnpm db:generate # Generate migrations -pnpm db:push # Push schema to database -pnpm db:studio # Open Drizzle Studio -``` - -## Telegram Commands - -| Command | Beschreibung | -|---------|--------------| -| `/start` | Hilfe anzeigen | -| `/help` | Hilfe anzeigen | -| `/new [Name]` | Neues Projekt erstellen | -| `/projects` | Alle Projekte auflisten | -| `/switch [ID]` | Projekt wechseln | -| `/status` | Status des aktiven Projekts | -| `/archive` | Projekt archivieren | -| `/generate` | Blogbeitrag generieren | -| `/generate [Stil]` | Mit bestimmtem Stil generieren | -| `/styles` | Verfügbare Stile anzeigen | -| `/export` | Letzte Generierung als Datei | - -## Blog-Stile - -| Stil | Beschreibung | -|------|--------------| -| `casual` | Locker & persönlich | -| `formal` | Professionell & sachlich | -| `tutorial` | Anleitung mit Schritten | -| `diary` | Tagebuch-Stil | - -## User Flow - -``` -1. /new Gartenhaus-Renovierung → Projekt erstellen -2. 📷 Foto senden → Wird gespeichert -3. 🎤 Sprachnotiz senden → Transkribiert + gespeichert -4. "Heute das Fundament gegossen" → Text-Notiz -5. /status → Übersicht -6. /generate tutorial → Blogbeitrag erstellen -7. /export → Als .md Datei -``` - -## Environment Variables - -```env -# Server -PORT=3302 - -# Telegram -TELEGRAM_BOT_TOKEN=xxx # Bot Token von @BotFather -TELEGRAM_ALLOWED_USERS=123,456 # Optional: Nur diese User IDs erlauben - -# Database -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/projectdoc - -# Storage (MinIO) -S3_ENDPOINT=http://localhost:9000 -S3_REGION=us-east-1 -S3_ACCESS_KEY=minioadmin -S3_SECRET_KEY=minioadmin -S3_BUCKET=projectdoc-storage - -# AI - Transcription (optional, aber empfohlen) -OPENAI_API_KEY=sk-xxx - -# AI - Generation -LLM_PROVIDER=mana-llm # mana-llm oder openai -MANA_LLM_URL=http://localhost:3025 # mana-llm service URL -LLM_MODEL=ollama/gemma3:4b # Model with provider prefix -``` - -## Projekt-Struktur - -``` -services/telegram-project-doc-bot/ -├── src/ -│ ├── main.ts # Entry point -│ ├── app.module.ts # Root module -│ ├── health.controller.ts # Health endpoint -│ ├── config/ -│ │ └── configuration.ts # Config + Blog-Stile -│ ├── database/ -│ │ ├── database.module.ts # Drizzle connection -│ │ └── schema.ts # DB schema -│ ├── bot/ -│ │ ├── bot.module.ts -│ │ └── bot.update.ts # Command handlers -│ ├── project/ -│ │ ├── project.module.ts -│ │ └── project.service.ts # Projekt CRUD -│ ├── media/ -│ │ ├── media.module.ts -│ │ ├── media.service.ts # Foto/Voice/Text verarbeiten -│ │ └── storage.service.ts # S3 Upload/Download -│ ├── transcription/ -│ │ ├── transcription.module.ts -│ │ └── transcription.service.ts # Whisper API -│ └── generation/ -│ ├── generation.module.ts -│ └── generation.service.ts # Blogpost AI -├── drizzle/ # Migrations -├── drizzle.config.ts -├── package.json -└── Dockerfile -``` - -## Lokale Entwicklung - -### 1. Bot bei Telegram erstellen - -1. Öffne @BotFather in Telegram -2. Sende `/newbot` -3. Wähle einen Namen (z.B. "Project Doc Bot") -4. Wähle einen Username (z.B. "my_projectdoc_bot") -5. Kopiere den Token - -### 2. Umgebung vorbereiten - -```bash -# Docker Services starten (PostgreSQL, MinIO, Ollama) -pnpm docker:up - -# Datenbank erstellen -psql -h localhost -U postgres -c "CREATE DATABASE projectdoc;" - -# Schema pushen -cd services/telegram-project-doc-bot -cp .env.example .env -# Token und Keys eintragen -pnpm db:push -``` - -### 3. Bot starten - -```bash -pnpm start:dev -``` - -## Features - -- **Multi-Projekt**: Mehrere Projekte pro User -- **Foto-Speicherung**: Fotos in S3 mit Metadaten -- **Voice-Transkription**: Automatisch via Whisper -- **Text-Notizen**: Einfache Nachrichten werden gespeichert -- **Chronologisch**: Alle Einträge behalten ihre Reihenfolge -- **Mehrere Stile**: casual, formal, tutorial, diary -- **Export**: Markdown-Datei zum Download - -## Datenbank-Schema - -``` -projects -├── id (UUID) -├── telegram_user_id (INT) -├── name (TEXT) -├── description (TEXT) -├── status (TEXT: active, archived, completed) -├── created_at, updated_at - -media_items -├── id (UUID) -├── project_id (UUID FK) -├── type (TEXT: photo, voice, text) -├── storage_key (TEXT) -├── caption (TEXT) -├── transcription (TEXT) -├── ai_description (TEXT) -├── metadata (JSONB) -├── telegram_file_id (TEXT) -├── order_index (INT) -├── created_at - -generations -├── id (UUID) -├── project_id (UUID FK) -├── style (TEXT) -├── content (TEXT - Markdown) -├── pdf_key (TEXT) -├── is_latest (BOOL) -├── created_at -``` - -## Health Check - -```bash -curl http://localhost:3302/health -``` - -## Deployment - -### Docker (empfohlen) - -```yaml -# docker-compose.yml -telegram-project-doc-bot: - build: ./services/telegram-project-doc-bot - restart: always - environment: - PORT: 3302 - TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN} - DATABASE_URL: ${DATABASE_URL} - S3_ENDPOINT: ${S3_ENDPOINT} - S3_ACCESS_KEY: ${S3_ACCESS_KEY} - S3_SECRET_KEY: ${S3_SECRET_KEY} - S3_BUCKET: projectdoc-storage - LLM_PROVIDER: ollama - OLLAMA_URL: http://ollama:11434 - ports: - - "3302:3302" -``` - -## Roadmap - -- [ ] Foto-Vision-Analyse (was ist auf dem Bild?) -- [ ] PDF-Export -- [ ] Bilder im Blogpost einbetten -- [ ] Projekt-Templates -- [ ] Web-Dashboard zur Ansicht -- [ ] Telegram Mini App für bessere UX diff --git a/services/telegram-project-doc-bot/Dockerfile b/services/telegram-project-doc-bot/Dockerfile deleted file mode 100644 index fade07891..000000000 --- a/services/telegram-project-doc-bot/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate - -# Copy package files -COPY package.json pnpm-lock.yaml* ./ - -# Install dependencies -RUN pnpm install --frozen-lockfile - -# Copy source -COPY . . - -# Build -RUN pnpm build - -# Production image -FROM node:20-alpine AS runner - -WORKDIR /app - -# Install pnpm -RUN corepack enable && corepack prepare pnpm@9.15.0 --activate - -# Copy package files and install prod dependencies only -COPY package.json pnpm-lock.yaml* ./ -RUN pnpm install --prod --frozen-lockfile - -# Copy built app -COPY --from=builder /app/dist ./dist - -# Set environment -ENV NODE_ENV=production -ENV PORT=3302 - -EXPOSE 3302 - -CMD ["node", "dist/main.js"] diff --git a/services/telegram-project-doc-bot/drizzle.config.ts b/services/telegram-project-doc-bot/drizzle.config.ts deleted file mode 100644 index 194347802..000000000 --- a/services/telegram-project-doc-bot/drizzle.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'drizzle-kit'; - -export default defineConfig({ - schema: './src/database/schema.ts', - out: './drizzle', - dialect: 'postgresql', - dbCredentials: { - url: process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/projectdoc', - }, -}); diff --git a/services/telegram-project-doc-bot/nest-cli.json b/services/telegram-project-doc-bot/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/services/telegram-project-doc-bot/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/services/telegram-project-doc-bot/package.json b/services/telegram-project-doc-bot/package.json deleted file mode 100644 index 5022fb3d1..000000000 --- a/services/telegram-project-doc-bot/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@manacore/telegram-project-doc-bot", - "version": "1.0.0", - "description": "Telegram bot for project documentation - collect photos and voice notes, generate blog posts", - "private": true, - "license": "MIT", - "scripts": { - "prebuild": "rimraf dist", - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit", - "db:generate": "drizzle-kit generate", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" - }, - "dependencies": { - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "@aws-sdk/client-s3": "^3.721.0", - "@aws-sdk/s3-request-presigner": "^3.721.0", - "drizzle-orm": "^0.38.3", - "nestjs-telegraf": "^2.8.0", - "openai": "^4.77.0", - "postgres": "^3.4.5", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "telegraf": "^4.16.3" - }, - "devDependencies": { - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@types/node": "^22.10.5", - "drizzle-kit": "^0.30.1", - "rimraf": "^6.0.1", - "typescript": "^5.7.3" - } -} diff --git a/services/telegram-project-doc-bot/src/app.module.ts b/services/telegram-project-doc-bot/src/app.module.ts deleted file mode 100644 index 58c1dc80e..000000000 --- a/services/telegram-project-doc-bot/src/app.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { TelegrafModule } from 'nestjs-telegraf'; -import configuration from './config/configuration'; -import { DatabaseModule } from './database/database.module'; -import { BotModule } from './bot/bot.module'; -import { HealthController } from './health.controller'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [configuration], - }), - TelegrafModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - token: configService.get('telegram.token') || '', - }), - inject: [ConfigService], - }), - DatabaseModule, - BotModule, - ], - controllers: [HealthController], -}) -export class AppModule {} diff --git a/services/telegram-project-doc-bot/src/bot/bot.module.ts b/services/telegram-project-doc-bot/src/bot/bot.module.ts deleted file mode 100644 index 1541be516..000000000 --- a/services/telegram-project-doc-bot/src/bot/bot.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BotUpdate } from './bot.update'; -import { ProjectModule } from '../project/project.module'; -import { MediaModule } from '../media/media.module'; -import { GenerationModule } from '../generation/generation.module'; - -@Module({ - imports: [ProjectModule, MediaModule, GenerationModule], - providers: [BotUpdate], -}) -export class BotModule {} diff --git a/services/telegram-project-doc-bot/src/bot/bot.update.ts b/services/telegram-project-doc-bot/src/bot/bot.update.ts deleted file mode 100644 index b8abf4c97..000000000 --- a/services/telegram-project-doc-bot/src/bot/bot.update.ts +++ /dev/null @@ -1,490 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { Update, Ctx, Start, Help, Command, On, Message } from 'nestjs-telegraf'; -import { Context } from 'telegraf'; -import { ConfigService } from '@nestjs/config'; -import { ProjectService } from '../project/project.service'; -import { MediaService } from '../media/media.service'; -import { GenerationService } from '../generation/generation.service'; -import { BLOG_STYLES } from '../config/configuration'; - -interface PhotoSize { - file_id: string; - file_unique_id: string; - width: number; - height: number; - file_size?: number; -} - -interface Voice { - file_id: string; - file_unique_id: string; - duration: number; - mime_type?: string; - file_size?: number; -} - -@Update() -export class BotUpdate { - private readonly logger = new Logger(BotUpdate.name); - private readonly allowedUsers: number[]; - - // Active project per user (userId -> projectId) - private activeProjects: Map = new Map(); - - constructor( - private readonly projectService: ProjectService, - private readonly mediaService: MediaService, - private readonly generationService: GenerationService, - private configService: ConfigService - ) { - this.allowedUsers = this.configService.get('telegram.allowedUsers') || []; - } - - private isAllowed(userId: number): boolean { - if (this.allowedUsers.length === 0) return true; - return this.allowedUsers.includes(userId); - } - - private formatHelp(): string { - const styles = Object.entries(BLOG_STYLES) - .map(([key, value]) => `• ${key} - ${value.name}`) - .join('\n'); - - return `📸 Project Doc Bot - -Sammle Fotos, Sprachnotizen und Text für deine Projekte und erstelle daraus Blogbeiträge. - -Projekt-Commands: -/new [Name] - Neues Projekt starten -/projects - Alle Projekte anzeigen -/switch [ID] - Projekt wechseln -/status - Status des aktiven Projekts -/archive - Aktives Projekt archivieren - -Content: -📷 Foto senden - Wird gespeichert -🎤 Sprachnotiz - Wird transkribiert -💬 Text-Nachricht - Als Notiz gespeichert - -Generierung: -/generate - Blogbeitrag erstellen -/generate [Stil] - Mit bestimmtem Stil -/styles - Verfügbare Stile anzeigen -/export - Letzte Generierung exportieren - -Verfügbare Stile: -${styles} - -Tipp: Starte mit /new Projektname`; - } - - @Start() - async start(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - this.logger.log(`/start from user ${userId}`); - await ctx.replyWithHTML(this.formatHelp()); - } - - @Help() - async help(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - await ctx.replyWithHTML(this.formatHelp()); - } - - @Command('new') - async newProject(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const name = text.replace('/new', '').trim(); - if (!name) { - await ctx.reply('Verwendung: /new Projektname\n\nBeispiel: /new Gartenhaus-Renovierung'); - return; - } - - try { - this.logger.log(`Creating project "${name}" for user ${userId}`); - - const project = await this.projectService.create({ - telegramUserId: userId, - name, - }); - - this.activeProjects.set(userId, project.id); - this.logger.log(`User ${userId} created project "${name}" with id ${project.id}`); - - await ctx.replyWithHTML( - `✅ Projekt erstellt!\n\n` + - `Name: ${project.name}\n` + - `ID: ${project.id.slice(0, 8)}\n\n` + - `Sende jetzt:\n` + - `📷 Fotos\n` + - `🎤 Sprachnotizen\n` + - `💬 Text-Nachrichten\n\n` + - `Mit /generate erstellst du den Blogbeitrag.` - ); - } catch (error) { - this.logger.error('Failed to create project:', error); - const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler'; - await ctx.reply(`Fehler beim Erstellen des Projekts: ${errorMsg}`); - } - } - - @Command('projects') - async listProjects(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const projects = await this.projectService.findByUser(userId); - - if (projects.length === 0) { - await ctx.reply('Keine Projekte gefunden.\n\nStarte mit: /new Projektname'); - return; - } - - const activeId = this.activeProjects.get(userId); - - const projectList = await Promise.all( - projects.map(async (p) => { - const stats = await this.projectService.getStats(p.id); - const active = p.id === activeId ? ' ✓' : ''; - const status = p.status === 'archived' ? ' 📦' : ''; - return `• ${p.name}${active}${status}\n ID: ${p.id.slice(0, 8)} | ${stats.total} Einträge`; - }) - ); - - await ctx.replyWithHTML( - `📂 Deine Projekte:\n\n${projectList.join('\n\n')}\n\n` + `Wechseln mit: /switch [ID]` - ); - } - - @Command('switch') - async switchProject(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const idPrefix = text.replace('/switch', '').trim(); - if (!idPrefix) { - await ctx.reply('Verwendung: /switch [ID]\n\nZeige Projekte mit /projects'); - return; - } - - // Find project by ID prefix - const projects = await this.projectService.findByUser(userId); - const project = projects.find((p) => p.id.startsWith(idPrefix)); - - if (!project) { - await ctx.reply(`Projekt mit ID "${idPrefix}" nicht gefunden.`); - return; - } - - this.activeProjects.set(userId, project.id); - const stats = await this.projectService.getStats(project.id); - - await ctx.replyWithHTML( - `✅ Gewechselt zu: ${project.name}\n\n` + - `📷 ${stats.photos} Fotos\n` + - `🎤 ${stats.voices} Sprachnotizen\n` + - `📝 ${stats.texts} Textnotizen` - ); - } - - @Command('status') - async status(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const projectId = this.activeProjects.get(userId); - if (!projectId) { - await ctx.reply('Kein aktives Projekt.\n\nStarte mit: /new Projektname'); - return; - } - - const project = await this.projectService.findById(projectId); - if (!project) { - this.activeProjects.delete(userId); - await ctx.reply('Projekt nicht gefunden. Starte ein neues mit /new'); - return; - } - - const stats = await this.projectService.getStats(projectId); - const latest = await this.generationService.getLatestGeneration(projectId); - - let statusText = - `📊 Projekt-Status\n\n` + - `Name: ${project.name}\n` + - `Status: ${project.status}\n` + - `Erstellt: ${project.createdAt.toLocaleDateString('de-DE')}\n\n` + - `Inhalte:\n` + - `📷 ${stats.photos} Fotos\n` + - `🎤 ${stats.voices} Sprachnotizen\n` + - `📝 ${stats.texts} Textnotizen\n` + - `Gesamt: ${stats.total} Einträge`; - - if (latest) { - statusText += `\n\nLetzte Generierung:\n${latest.createdAt.toLocaleString('de-DE')} (${latest.style})`; - } - - await ctx.replyWithHTML(statusText); - } - - @Command('archive') - async archiveProject(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const projectId = this.activeProjects.get(userId); - if (!projectId) { - await ctx.reply('Kein aktives Projekt.'); - return; - } - - await this.projectService.update(projectId, { status: 'archived' }); - this.activeProjects.delete(userId); - - await ctx.reply('📦 Projekt archiviert.\n\nStarte ein neues mit /new'); - } - - @Command('styles') - async showStyles(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const styles = Object.entries(BLOG_STYLES) - .map( - ([key, value]) => `${key} - ${value.name}\n${value.prompt.slice(0, 80)}...` - ) - .join('\n\n'); - - await ctx.replyWithHTML( - `📝 Verfügbare Blog-Stile:\n\n${styles}\n\nVerwendung: /generate [stil]` - ); - } - - @Command('generate') - async generate(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const projectId = this.activeProjects.get(userId); - if (!projectId) { - await ctx.reply('Kein aktives Projekt.\n\nStarte mit: /new Projektname'); - return; - } - - const style = text.replace('/generate', '').trim().toLowerCase() || 'casual'; - const validStyles = Object.keys(BLOG_STYLES); - - if (!validStyles.includes(style)) { - await ctx.reply( - `Unbekannter Stil: "${style}"\n\nVerfügbar: ${validStyles.join(', ')}\n\nZeige Details mit /styles` - ); - return; - } - - await ctx.reply('🚀 Generiere Blogbeitrag...\n\nDas kann einen Moment dauern.'); - await ctx.sendChatAction('typing'); - - try { - const content = await this.generationService.generateBlogpost( - projectId, - style as keyof typeof BLOG_STYLES - ); - - // Split if too long for Telegram - if (content.length <= 4000) { - await ctx.reply(content); - } else { - // Send as document - const buffer = Buffer.from(content, 'utf-8'); - await ctx.replyWithDocument( - { - source: buffer, - filename: 'blogpost.md', - }, - { - caption: '📄 Blogbeitrag (zu lang für Telegram-Nachricht)', - } - ); - - // Also send a preview - const preview = content.slice(0, 1000) + '\n\n[...gekürzt, siehe Datei]'; - await ctx.reply(preview); - } - - await ctx.reply('✅ Blogbeitrag erstellt!\n\nExportieren mit /export'); - } catch (error) { - this.logger.error('Generation failed:', error); - const message = error instanceof Error ? error.message : 'Unbekannter Fehler'; - await ctx.reply(`❌ Fehler: ${message}`); - } - } - - @Command('export') - async exportGeneration(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const projectId = this.activeProjects.get(userId); - if (!projectId) { - await ctx.reply('Kein aktives Projekt.'); - return; - } - - const latest = await this.generationService.getLatestGeneration(projectId); - if (!latest) { - await ctx.reply('Noch kein Blogbeitrag generiert.\n\nErstelle einen mit /generate'); - return; - } - - const project = await this.projectService.findById(projectId); - const filename = `${project?.name.replace(/[^a-zA-Z0-9]/g, '_') || 'blogpost'}.md`; - - const buffer = Buffer.from(latest.content, 'utf-8'); - await ctx.replyWithDocument( - { - source: buffer, - filename, - }, - { - caption: `📄 ${filename}\nGeneriert: ${latest.createdAt.toLocaleString('de-DE')}`, - } - ); - } - - @On('photo') - async onPhoto(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const projectId = this.activeProjects.get(userId); - if (!projectId) { - await ctx.reply('Kein aktives Projekt.\n\nStarte mit: /new Projektname'); - return; - } - - const message = ctx.message as { photo?: PhotoSize[]; caption?: string }; - const photos = message.photo; - if (!photos || photos.length === 0) return; - - // Get largest photo - const photo = photos[photos.length - 1]; - const caption = message.caption; - - await ctx.sendChatAction('upload_photo'); - - try { - await this.mediaService.processPhoto(projectId, photo.file_id, caption); - - const stats = await this.projectService.getStats(projectId); - await ctx.reply(`📷 Foto gespeichert! (${stats.photos} Fotos gesamt)`); - } catch (error) { - this.logger.error('Failed to process photo:', error); - await ctx.reply('❌ Fehler beim Speichern des Fotos.'); - } - } - - @On('voice') - async onVoice(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - const projectId = this.activeProjects.get(userId); - if (!projectId) { - await ctx.reply('Kein aktives Projekt.\n\nStarte mit: /new Projektname'); - return; - } - - const message = ctx.message as { voice?: Voice }; - const voice = message.voice; - if (!voice) return; - - await ctx.reply('🎤 Verarbeite Sprachnotiz...'); - await ctx.sendChatAction('typing'); - - try { - const item = await this.mediaService.processVoice(projectId, voice.file_id, voice.duration); - - const stats = await this.projectService.getStats(projectId); - let reply = `✅ Sprachnotiz gespeichert! (${stats.voices} gesamt)`; - - if (item.transcription) { - reply += `\n\n📝 Transkription:\n"${item.transcription}"`; - } - - await ctx.reply(reply); - } catch (error) { - this.logger.error('Failed to process voice:', error); - await ctx.reply('❌ Fehler beim Verarbeiten der Sprachnotiz.'); - } - } - - @On('text') - async onText(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId || !this.isAllowed(userId)) { - await ctx.reply('Zugriff verweigert.'); - return; - } - - // Ignore commands - if (text.startsWith('/')) return; - - const projectId = this.activeProjects.get(userId); - if (!projectId) { - // No active project - show hint - await ctx.reply('💡 Tipp: Starte ein Projekt mit /new Projektname'); - return; - } - - try { - await this.mediaService.addTextNote(projectId, text); - - const stats = await this.projectService.getStats(projectId); - await ctx.reply(`📝 Notiz gespeichert! (${stats.texts} Notizen gesamt)`); - } catch (error) { - this.logger.error('Failed to add text note:', error); - await ctx.reply('❌ Fehler beim Speichern der Notiz.'); - } - } -} diff --git a/services/telegram-project-doc-bot/src/config/configuration.ts b/services/telegram-project-doc-bot/src/config/configuration.ts deleted file mode 100644 index d59aa34a1..000000000 --- a/services/telegram-project-doc-bot/src/config/configuration.ts +++ /dev/null @@ -1,58 +0,0 @@ -export default () => ({ - port: parseInt(process.env.PORT || '3302', 10), - telegram: { - token: process.env.TELEGRAM_BOT_TOKEN, - allowedUsers: - process.env.TELEGRAM_ALLOWED_USERS?.split(',') - .map((id) => parseInt(id.trim(), 10)) - .filter((id) => !isNaN(id)) || [], - }, - database: { - url: process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/projectdoc', - }, - s3: { - endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000', - region: process.env.S3_REGION || 'us-east-1', - accessKey: process.env.S3_ACCESS_KEY || 'minioadmin', - secretKey: process.env.S3_SECRET_KEY || 'minioadmin', - bucket: process.env.S3_BUCKET || 'projectdoc-storage', - }, - openai: { - apiKey: process.env.OPENAI_API_KEY, - }, - stt: { - provider: process.env.STT_PROVIDER || 'local', // 'local' or 'openai' - localUrl: process.env.STT_LOCAL_URL || 'http://localhost:3020', - model: process.env.STT_MODEL || 'whisper', // 'whisper' or 'voxtral' - }, - llm: { - provider: process.env.LLM_PROVIDER || 'mana-llm', - manaLlm: { - url: process.env.MANA_LLM_URL || 'http://localhost:3025', - model: process.env.LLM_MODEL || 'ollama/gemma3:4b', - }, - }, -}); - -export const BLOG_STYLES = { - casual: { - name: 'Locker & Persönlich', - prompt: - 'Schreibe einen lockeren, persönlichen Blogbeitrag. Verwende "ich" und erzähle die Geschichte authentisch.', - }, - formal: { - name: 'Professionell', - prompt: - 'Schreibe einen professionellen, sachlichen Blogbeitrag. Verwende eine neutrale Sprache.', - }, - tutorial: { - name: 'Anleitung/Tutorial', - prompt: - 'Schreibe einen anleitenden Blogbeitrag mit klaren Schritten. Nummeriere die Schritte und gib praktische Tipps.', - }, - diary: { - name: 'Tagebuch', - prompt: - 'Schreibe einen Tagebuch-Eintrag mit persönlichen Eindrücken und Gefühlen. Sehr authentisch und emotional.', - }, -}; diff --git a/services/telegram-project-doc-bot/src/database/database.module.ts b/services/telegram-project-doc-bot/src/database/database.module.ts deleted file mode 100644 index 905674bf6..000000000 --- a/services/telegram-project-doc-bot/src/database/database.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from './schema'; - -export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_CONNECTION, - useFactory: (configService: ConfigService) => { - const connectionString = configService.get('database.url'); - const client = postgres(connectionString!); - return drizzle(client, { schema }); - }, - inject: [ConfigService], - }, - ], - exports: [DATABASE_CONNECTION], -}) -export class DatabaseModule {} diff --git a/services/telegram-project-doc-bot/src/database/schema.ts b/services/telegram-project-doc-bot/src/database/schema.ts deleted file mode 100644 index 371bc9c20..000000000 --- a/services/telegram-project-doc-bot/src/database/schema.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { - pgTable, - uuid, - text, - timestamp, - integer, - bigint, - jsonb, - boolean, -} from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; - -// Projects table -export const projects = pgTable('projects', { - id: uuid('id').primaryKey().defaultRandom(), - telegramUserId: bigint('telegram_user_id', { mode: 'number' }).notNull(), - name: text('name').notNull(), - description: text('description'), - status: text('status').default('active').notNull(), // active, archived, completed - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}); - -// Media items (photos, voice notes, text) -export const mediaItems = pgTable('media_items', { - id: uuid('id').primaryKey().defaultRandom(), - projectId: uuid('project_id') - .references(() => projects.id, { onDelete: 'cascade' }) - .notNull(), - type: text('type').notNull(), // photo, voice, text - - // Storage - storageKey: text('storage_key'), // S3 key for photo/voice - thumbnailKey: text('thumbnail_key'), // Thumbnail for photos - - // Content - caption: text('caption'), // Original caption/text - transcription: text('transcription'), // Voice → Text - aiDescription: text('ai_description'), // Vision → Description - - // Metadata - metadata: jsonb('metadata').$type<{ - width?: number; - height?: number; - duration?: number; - mimeType?: string; - fileSize?: number; - }>(), - telegramFileId: text('telegram_file_id'), - orderIndex: integer('order_index').default(0).notNull(), - - createdAt: timestamp('created_at').defaultNow().notNull(), -}); - -// Generated blog posts -export const generations = pgTable('generations', { - id: uuid('id').primaryKey().defaultRandom(), - projectId: uuid('project_id') - .references(() => projects.id, { onDelete: 'cascade' }) - .notNull(), - style: text('style').default('casual').notNull(), - content: text('content').notNull(), // Generated markdown - pdfKey: text('pdf_key'), // S3 key for PDF export - isLatest: boolean('is_latest').default(true).notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), -}); - -// Relations -export const projectsRelations = relations(projects, ({ many }) => ({ - mediaItems: many(mediaItems), - generations: many(generations), -})); - -export const mediaItemsRelations = relations(mediaItems, ({ one }) => ({ - project: one(projects, { - fields: [mediaItems.projectId], - references: [projects.id], - }), -})); - -export const generationsRelations = relations(generations, ({ one }) => ({ - project: one(projects, { - fields: [generations.projectId], - references: [projects.id], - }), -})); - -// Types -export type Project = typeof projects.$inferSelect; -export type NewProject = typeof projects.$inferInsert; -export type MediaItem = typeof mediaItems.$inferSelect; -export type NewMediaItem = typeof mediaItems.$inferInsert; -export type Generation = typeof generations.$inferSelect; -export type NewGeneration = typeof generations.$inferInsert; diff --git a/services/telegram-project-doc-bot/src/generation/generation.module.ts b/services/telegram-project-doc-bot/src/generation/generation.module.ts deleted file mode 100644 index fccb8adfd..000000000 --- a/services/telegram-project-doc-bot/src/generation/generation.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { GenerationService } from './generation.service'; - -@Module({ - providers: [GenerationService], - exports: [GenerationService], -}) -export class GenerationModule {} diff --git a/services/telegram-project-doc-bot/src/generation/generation.service.ts b/services/telegram-project-doc-bot/src/generation/generation.service.ts deleted file mode 100644 index 9536e59c7..000000000 --- a/services/telegram-project-doc-bot/src/generation/generation.service.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { eq, desc } from 'drizzle-orm'; -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import OpenAI from 'openai'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import * as schema from '../database/schema'; -import { Generation, Project, MediaItem } from '../database/schema'; -import { BLOG_STYLES } from '../config/configuration'; - -type BlogStyle = keyof typeof BLOG_STYLES; - -@Injectable() -export class GenerationService { - private readonly logger = new Logger(GenerationService.name); - private readonly llmProvider: string; - private readonly manaLlmUrl: string; - private readonly manaLlmModel: string; - private readonly openai: OpenAI | null; - - constructor( - @Inject(DATABASE_CONNECTION) - private db: PostgresJsDatabase, - private configService: ConfigService - ) { - this.llmProvider = this.configService.get('llm.provider') || 'mana-llm'; - this.manaLlmUrl = this.configService.get('llm.manaLlm.url') || 'http://localhost:3025'; - this.manaLlmModel = - this.configService.get('llm.manaLlm.model') || 'ollama/gemma3:4b'; - - const apiKey = this.configService.get('openai.apiKey'); - this.openai = apiKey ? new OpenAI({ apiKey }) : null; - - this.logger.log(`LLM Provider: ${this.llmProvider}`); - } - - async generateBlogpost(projectId: string, style: BlogStyle = 'casual'): Promise { - // 1. Load project - const project = await this.db.query.projects.findFirst({ - where: eq(schema.projects.id, projectId), - }); - - if (!project) { - throw new Error('Projekt nicht gefunden'); - } - - // 2. Load all media items - const items = await this.db.query.mediaItems.findMany({ - where: eq(schema.mediaItems.projectId, projectId), - orderBy: [schema.mediaItems.orderIndex, schema.mediaItems.createdAt], - }); - - if (items.length === 0) { - throw new Error( - 'Keine Inhalte im Projekt. Füge zuerst Fotos, Sprachnotizen oder Text hinzu.' - ); - } - - // 3. Build context from media items - const context = this.buildContext(items); - - // 4. Build prompt - const styleConfig = BLOG_STYLES[style] || BLOG_STYLES.casual; - const prompt = this.buildPrompt(project, context, styleConfig.prompt); - - // 5. Generate with LLM - this.logger.log(`Generating blogpost for "${project.name}" with style "${style}"`); - const content = await this.callLlm(prompt); - - // 6. Mark previous generations as not latest - await this.db - .update(schema.generations) - .set({ isLatest: false }) - .where(eq(schema.generations.projectId, projectId)); - - // 7. Save generation - const [generation] = await this.db - .insert(schema.generations) - .values({ - projectId, - style, - content, - isLatest: true, - }) - .returning(); - - this.logger.log(`Generated blogpost: ${generation.id} (${content.length} chars)`); - return content; - } - - private buildContext(items: MediaItem[]): string { - return items - .map((item, index) => { - const num = index + 1; - const timestamp = item.createdAt.toLocaleString('de-DE', { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit', - }); - - if (item.type === 'photo') { - const desc = item.aiDescription || item.caption || 'Keine Beschreibung'; - return `[Foto ${num}] (${timestamp})\n${desc}`; - } - - if (item.type === 'voice') { - const text = item.transcription || '(Keine Transkription verfügbar)'; - return `[Sprachnotiz ${num}] (${timestamp})\n"${text}"`; - } - - // text - return `[Notiz ${num}] (${timestamp})\n${item.caption}`; - }) - .join('\n\n---\n\n'); - } - - private buildPrompt(project: Project, context: string, stylePrompt: string): string { - return `Du bist ein erfahrener Blogger und Content Creator. - -${stylePrompt} - -## Projekt-Informationen -**Name:** ${project.name} -${project.description ? `**Beschreibung:** ${project.description}` : ''} - -## Gesammelte Inhalte (chronologisch) - -${context} - -## Aufgabe - -Erstelle einen gut strukturierten Blogbeitrag in Markdown basierend auf den obigen Inhalten. - -**Anforderungen:** -- Verwende eine passende, ansprechende Überschrift (# Titel) -- Strukturiere den Beitrag mit Zwischenüberschriften (## Abschnitte) -- Verweise im Text auf die Fotos mit [Foto X], damit sie später eingebettet werden können -- Integriere die Sprachnotizen und Textnotizen natürlich in den Fließtext -- Füge am Ende eine kurze Zusammenfassung oder "Lessons Learned" hinzu -- Schreibe auf Deutsch -- Der Beitrag sollte authentisch und persönlich klingen - -Beginne direkt mit dem Blogbeitrag (ohne Einleitung wie "Hier ist der Blogbeitrag"):`; - } - - private async callLlm(prompt: string): Promise { - if (this.llmProvider === 'openai' && this.openai) { - return this.callOpenAI(prompt); - } - - return this.callManaLlm(prompt); - } - - private async callOpenAI(prompt: string): Promise { - if (!this.openai) { - throw new Error('OpenAI not configured'); - } - - const response = await this.openai.chat.completions.create({ - model: 'gpt-4o-mini', - messages: [{ role: 'user', content: prompt }], - temperature: 0.7, - max_tokens: 4000, - }); - - return response.choices[0]?.message?.content || ''; - } - - private async callManaLlm(prompt: string): Promise { - const response = await fetch(`${this.manaLlmUrl}/v1/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: this.manaLlmModel, - messages: [{ role: 'user', content: prompt }], - temperature: 0.7, - max_tokens: 4000, - stream: false, - }), - signal: AbortSignal.timeout(180000), // 3 minutes timeout - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`mana-llm API error: ${response.status} - ${errorText}`); - } - - const data = await response.json(); - return data.choices?.[0]?.message?.content || ''; - } - - async getLatestGeneration(projectId: string): Promise { - return this.db.query.generations.findFirst({ - where: eq(schema.generations.projectId, projectId), - orderBy: [desc(schema.generations.createdAt)], - }); - } - - getAvailableStyles(): { key: string; name: string }[] { - return Object.entries(BLOG_STYLES).map(([key, value]) => ({ - key, - name: value.name, - })); - } -} diff --git a/services/telegram-project-doc-bot/src/health.controller.ts b/services/telegram-project-doc-bot/src/health.controller.ts deleted file mode 100644 index 59bc025e7..000000000 --- a/services/telegram-project-doc-bot/src/health.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller('health') -export class HealthController { - @Get() - check() { - return { - status: 'ok', - service: 'telegram-project-doc-bot', - timestamp: new Date().toISOString(), - }; - } -} diff --git a/services/telegram-project-doc-bot/src/main.ts b/services/telegram-project-doc-bot/src/main.ts deleted file mode 100644 index 559bf11d4..000000000 --- a/services/telegram-project-doc-bot/src/main.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const logger = new Logger('Bootstrap'); - const app = await NestFactory.create(AppModule); - - const configService = app.get(ConfigService); - const port = configService.get('port') || 3302; - - await app.listen(port); - logger.log(`Telegram Project Doc Bot running on port ${port}`); - logger.log(`LLM Provider: ${configService.get('llm.provider')}`); -} - -bootstrap(); diff --git a/services/telegram-project-doc-bot/src/media/media.module.ts b/services/telegram-project-doc-bot/src/media/media.module.ts deleted file mode 100644 index 7d62a4e77..000000000 --- a/services/telegram-project-doc-bot/src/media/media.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { MediaService } from './media.service'; -import { StorageService } from './storage.service'; -import { TranscriptionModule } from '../transcription/transcription.module'; - -@Module({ - imports: [TranscriptionModule], - providers: [MediaService, StorageService], - exports: [MediaService, StorageService], -}) -export class MediaModule {} diff --git a/services/telegram-project-doc-bot/src/media/media.service.ts b/services/telegram-project-doc-bot/src/media/media.service.ts deleted file mode 100644 index 1b4c964c1..000000000 --- a/services/telegram-project-doc-bot/src/media/media.service.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { eq, asc } from 'drizzle-orm'; -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import * as schema from '../database/schema'; -import { MediaItem, NewMediaItem } from '../database/schema'; -import { StorageService } from './storage.service'; -import { TranscriptionService } from '../transcription/transcription.service'; - -@Injectable() -export class MediaService { - private readonly logger = new Logger(MediaService.name); - private readonly telegramApiUrl: string; - - constructor( - @Inject(DATABASE_CONNECTION) - private db: PostgresJsDatabase, - private storageService: StorageService, - private transcriptionService: TranscriptionService, - private configService: ConfigService - ) { - const token = this.configService.get('telegram.token'); - this.telegramApiUrl = `https://api.telegram.org/bot${token}`; - } - - // Get file URL from Telegram - private async getTelegramFileUrl(fileId: string): Promise { - const response = await fetch(`${this.telegramApiUrl}/getFile?file_id=${fileId}`); - const data = await response.json(); - - if (!data.ok) { - throw new Error(`Telegram API error: ${data.description}`); - } - - const token = this.configService.get('telegram.token'); - return `https://api.telegram.org/file/bot${token}/${data.result.file_path}`; - } - - // Download file from URL - private async downloadFile(url: string): Promise { - const response = await fetch(url); - const arrayBuffer = await response.arrayBuffer(); - return Buffer.from(arrayBuffer); - } - - // Process a photo from Telegram - async processPhoto(projectId: string, fileId: string, caption?: string): Promise { - this.logger.log(`Processing photo for project ${projectId}`); - - // 1. Download from Telegram - const fileUrl = await this.getTelegramFileUrl(fileId); - const buffer = await this.downloadFile(fileUrl); - - // 2. Generate storage key and upload - const filename = `photo_${Date.now()}.jpg`; - const storageKey = this.storageService.generateKey(projectId, 'photo', filename); - await this.storageService.upload(storageKey, buffer, 'image/jpeg'); - - // 3. Get next order index - const orderIndex = await this.getNextOrderIndex(projectId); - - // 4. Save to database - const [item] = await this.db - .insert(schema.mediaItems) - .values({ - projectId, - type: 'photo', - storageKey, - caption, - telegramFileId: fileId, - orderIndex, - metadata: { fileSize: buffer.length }, - }) - .returning(); - - this.logger.log(`Photo saved: ${item.id}`); - return item; - } - - // Process a voice note from Telegram - async processVoice(projectId: string, fileId: string, duration?: number): Promise { - this.logger.log(`Processing voice for project ${projectId}`); - - // 1. Download from Telegram - const fileUrl = await this.getTelegramFileUrl(fileId); - const buffer = await this.downloadFile(fileUrl); - - // 2. Transcribe with Whisper - let transcription: string | undefined; - if (this.transcriptionService.isAvailable()) { - try { - transcription = await this.transcriptionService.transcribe(buffer); - } catch (error) { - this.logger.warn('Transcription failed, saving without:', error); - } - } - - // 3. Generate storage key and upload - const filename = `voice_${Date.now()}.ogg`; - const storageKey = this.storageService.generateKey(projectId, 'voice', filename); - await this.storageService.upload(storageKey, buffer, 'audio/ogg'); - - // 4. Get next order index - const orderIndex = await this.getNextOrderIndex(projectId); - - // 5. Save to database - const [item] = await this.db - .insert(schema.mediaItems) - .values({ - projectId, - type: 'voice', - storageKey, - transcription, - telegramFileId: fileId, - orderIndex, - metadata: { duration, fileSize: buffer.length }, - }) - .returning(); - - this.logger.log(`Voice saved: ${item.id}, transcription: ${transcription ? 'yes' : 'no'}`); - return item; - } - - // Add a text note - async addTextNote(projectId: string, text: string): Promise { - const orderIndex = await this.getNextOrderIndex(projectId); - - const [item] = await this.db - .insert(schema.mediaItems) - .values({ - projectId, - type: 'text', - caption: text, - orderIndex, - }) - .returning(); - - this.logger.log(`Text note saved: ${item.id}`); - return item; - } - - // Get all media items for a project - async getByProject(projectId: string): Promise { - return this.db.query.mediaItems.findMany({ - where: eq(schema.mediaItems.projectId, projectId), - orderBy: [asc(schema.mediaItems.orderIndex), asc(schema.mediaItems.createdAt)], - }); - } - - // Get next order index for a project - private async getNextOrderIndex(projectId: string): Promise { - const items = await this.db.query.mediaItems.findMany({ - where: eq(schema.mediaItems.projectId, projectId), - }); - return items.length; - } - - // Delete a media item - async delete(id: string): Promise { - const result = await this.db.delete(schema.mediaItems).where(eq(schema.mediaItems.id, id)); - return (result as unknown as { rowCount: number }).rowCount > 0; - } -} diff --git a/services/telegram-project-doc-bot/src/media/storage.service.ts b/services/telegram-project-doc-bot/src/media/storage.service.ts deleted file mode 100644 index 5280d6fd1..000000000 --- a/services/telegram-project-doc-bot/src/media/storage.service.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { - S3Client, - PutObjectCommand, - GetObjectCommand, - HeadBucketCommand, - CreateBucketCommand, -} from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; - -@Injectable() -export class StorageService implements OnModuleInit { - private readonly logger = new Logger(StorageService.name); - private readonly s3: S3Client; - private readonly bucket: string; - - constructor(private configService: ConfigService) { - this.bucket = this.configService.get('s3.bucket')!; - - this.s3 = new S3Client({ - endpoint: this.configService.get('s3.endpoint'), - region: this.configService.get('s3.region'), - credentials: { - accessKeyId: this.configService.get('s3.accessKey')!, - secretAccessKey: this.configService.get('s3.secretKey')!, - }, - forcePathStyle: true, // Required for MinIO - }); - } - - async onModuleInit() { - await this.ensureBucket(); - } - - private async ensureBucket(): Promise { - try { - await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket })); - this.logger.log(`Bucket "${this.bucket}" exists`); - } catch (error: unknown) { - if (error && typeof error === 'object' && 'name' in error && error.name === 'NotFound') { - this.logger.log(`Creating bucket "${this.bucket}"...`); - await this.s3.send(new CreateBucketCommand({ Bucket: this.bucket })); - this.logger.log(`Bucket "${this.bucket}" created`); - } else { - this.logger.warn(`Could not check bucket: ${error}`); - } - } - } - - async upload(key: string, buffer: Buffer, contentType: string): Promise { - await this.s3.send( - new PutObjectCommand({ - Bucket: this.bucket, - Key: key, - Body: buffer, - ContentType: contentType, - }) - ); - - this.logger.debug(`Uploaded ${key} (${buffer.length} bytes)`); - return key; - } - - async getSignedUrl(key: string, expiresIn = 3600): Promise { - const command = new GetObjectCommand({ - Bucket: this.bucket, - Key: key, - }); - - return getSignedUrl(this.s3, command, { expiresIn }); - } - - generateKey(projectId: string, type: 'photo' | 'voice' | 'pdf', filename: string): string { - return `${projectId}/${type}/${filename}`; - } -} diff --git a/services/telegram-project-doc-bot/src/project/project.module.ts b/services/telegram-project-doc-bot/src/project/project.module.ts deleted file mode 100644 index c1b3f70d8..000000000 --- a/services/telegram-project-doc-bot/src/project/project.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ProjectService } from './project.service'; - -@Module({ - providers: [ProjectService], - exports: [ProjectService], -}) -export class ProjectModule {} diff --git a/services/telegram-project-doc-bot/src/project/project.service.ts b/services/telegram-project-doc-bot/src/project/project.service.ts deleted file mode 100644 index e82ec9ccf..000000000 --- a/services/telegram-project-doc-bot/src/project/project.service.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { eq, and, desc } from 'drizzle-orm'; -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import * as schema from '../database/schema'; -import { Project, NewProject } from '../database/schema'; - -@Injectable() -export class ProjectService { - private readonly logger = new Logger(ProjectService.name); - - constructor( - @Inject(DATABASE_CONNECTION) - private db: PostgresJsDatabase - ) {} - - async create(data: { - telegramUserId: number; - name: string; - description?: string; - }): Promise { - const [project] = await this.db - .insert(schema.projects) - .values({ - telegramUserId: data.telegramUserId, - name: data.name, - description: data.description, - }) - .returning(); - - this.logger.log(`Created project "${project.name}" for user ${data.telegramUserId}`); - return project; - } - - async findById(id: string): Promise { - return this.db.query.projects.findFirst({ - where: eq(schema.projects.id, id), - }); - } - - async findByUser(telegramUserId: number): Promise { - return this.db.query.projects.findMany({ - where: eq(schema.projects.telegramUserId, telegramUserId), - orderBy: [desc(schema.projects.updatedAt)], - }); - } - - async findActiveByUser(telegramUserId: number): Promise { - return this.db.query.projects.findMany({ - where: and( - eq(schema.projects.telegramUserId, telegramUserId), - eq(schema.projects.status, 'active') - ), - orderBy: [desc(schema.projects.updatedAt)], - }); - } - - async update(id: string, data: Partial): Promise { - const [project] = await this.db - .update(schema.projects) - .set({ ...data, updatedAt: new Date() }) - .where(eq(schema.projects.id, id)) - .returning(); - - return project; - } - - async delete(id: string): Promise { - const result = await this.db.delete(schema.projects).where(eq(schema.projects.id, id)); - return (result as unknown as { rowCount: number }).rowCount > 0; - } - - async getStats(projectId: string): Promise<{ - photos: number; - voices: number; - texts: number; - total: number; - }> { - const items = await this.db.query.mediaItems.findMany({ - where: eq(schema.mediaItems.projectId, projectId), - }); - - return { - photos: items.filter((i) => i.type === 'photo').length, - voices: items.filter((i) => i.type === 'voice').length, - texts: items.filter((i) => i.type === 'text').length, - total: items.length, - }; - } -} diff --git a/services/telegram-project-doc-bot/src/transcription/transcription.module.ts b/services/telegram-project-doc-bot/src/transcription/transcription.module.ts deleted file mode 100644 index fb5aeeaf1..000000000 --- a/services/telegram-project-doc-bot/src/transcription/transcription.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TranscriptionService } from './transcription.service'; - -@Module({ - providers: [TranscriptionService], - exports: [TranscriptionService], -}) -export class TranscriptionModule {} diff --git a/services/telegram-project-doc-bot/src/transcription/transcription.service.ts b/services/telegram-project-doc-bot/src/transcription/transcription.service.ts deleted file mode 100644 index 74d7f5af9..000000000 --- a/services/telegram-project-doc-bot/src/transcription/transcription.service.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import OpenAI from 'openai'; - -interface LocalSTTResponse { - text: string; - language?: string; - model: string; -} - -@Injectable() -export class TranscriptionService { - private readonly logger = new Logger(TranscriptionService.name); - private readonly openai: OpenAI | null; - private readonly provider: 'local' | 'openai'; - private readonly localUrl: string; - private readonly sttModel: string; - - constructor(private configService: ConfigService) { - this.provider = this.configService.get('stt.provider', 'local') as 'local' | 'openai'; - this.localUrl = this.configService.get('stt.localUrl', 'http://localhost:3020'); - this.sttModel = this.configService.get('stt.model', 'whisper'); - - const apiKey = this.configService.get('openai.apiKey'); - - if (apiKey) { - this.openai = new OpenAI({ apiKey }); - this.logger.log('OpenAI Whisper available as fallback'); - } else { - this.openai = null; - } - - this.logger.log( - `STT Provider: ${this.provider}, URL: ${this.localUrl}, Model: ${this.sttModel}` - ); - } - - async transcribe(audioBuffer: Buffer, filename = 'audio.ogg'): Promise { - // Try local STT first if configured - if (this.provider === 'local') { - try { - return await this.transcribeLocal(audioBuffer, filename); - } catch (error) { - this.logger.warn(`Local STT failed, trying OpenAI fallback: ${error}`); - if (this.openai) { - return await this.transcribeOpenAI(audioBuffer, filename); - } - throw error; - } - } - - // Use OpenAI - if (this.openai) { - return await this.transcribeOpenAI(audioBuffer, filename); - } - - throw new Error('No STT provider available'); - } - - private async transcribeLocal(audioBuffer: Buffer, filename: string): Promise { - const endpoint = this.sttModel === 'voxtral' ? '/transcribe/voxtral' : '/transcribe'; - const url = `${this.localUrl}${endpoint}`; - - this.logger.debug(`Calling local STT: ${url}`); - - const formData = new FormData(); - const uint8Array = new Uint8Array(audioBuffer); - const blob = new Blob([uint8Array], { type: 'audio/ogg' }); - formData.append('file', blob, filename); - formData.append('language', 'de'); - - const response = await fetch(url, { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Local STT error: ${response.status} - ${error}`); - } - - const result: LocalSTTResponse = await response.json(); - this.logger.debug(`Local STT result: ${result.text.length} chars, model: ${result.model}`); - - return result.text; - } - - private async transcribeOpenAI(audioBuffer: Buffer, filename: string): Promise { - if (!this.openai) { - throw new Error('OpenAI not configured'); - } - - try { - const uint8Array = new Uint8Array(audioBuffer); - const file = new File([uint8Array], filename, { type: 'audio/ogg' }); - - const response = await this.openai.audio.transcriptions.create({ - file, - model: 'whisper-1', - language: 'de', - }); - - this.logger.debug( - `OpenAI transcribed ${audioBuffer.length} bytes -> ${response.text.length} chars` - ); - return response.text; - } catch (error) { - this.logger.error('OpenAI transcription failed:', error); - throw new Error('Transkription fehlgeschlagen'); - } - } - - isAvailable(): boolean { - return this.provider === 'local' || this.openai !== null; - } -} diff --git a/services/telegram-project-doc-bot/tsconfig.json b/services/telegram-project-doc-bot/tsconfig.json deleted file mode 100644 index 94f1e9493..000000000 --- a/services/telegram-project-doc-bot/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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 - } -} diff --git a/services/telegram-stats-bot/.env.example b/services/telegram-stats-bot/.env.example deleted file mode 100644 index 9d6580395..000000000 --- a/services/telegram-stats-bot/.env.example +++ /dev/null @@ -1,15 +0,0 @@ -# Server -PORT=3300 -TZ=Europe/Berlin - -# Telegram Bot -TELEGRAM_BOT_TOKEN=your-telegram-bot-token -TELEGRAM_CHAT_ID=your-telegram-chat-id - -# Umami Analytics -UMAMI_API_URL=http://localhost:3200 -UMAMI_USERNAME=admin -UMAMI_PASSWORD=your-umami-password - -# Database (for user counts - optional) -DATABASE_URL=postgresql://postgres:password@localhost:5432/manacore_auth diff --git a/services/telegram-stats-bot/CLAUDE.md b/services/telegram-stats-bot/CLAUDE.md deleted file mode 100644 index ed06a0103..000000000 --- a/services/telegram-stats-bot/CLAUDE.md +++ /dev/null @@ -1,150 +0,0 @@ -# Telegram Stats Bot - Claude Code Guidelines - -## Overview - -Telegram Stats Bot delivers analytics and statistics from Umami (stats.mana.how) via Telegram. It provides both automated scheduled reports and on-demand commands. - -## Tech Stack - -- **Framework**: NestJS 10 -- **Telegram**: nestjs-telegraf + Telegraf -- **Scheduling**: @nestjs/schedule -- **Analytics**: Umami API - -## Commands - -```bash -# Development -pnpm start:dev # Start with hot reload - -# Build -pnpm build # Production build - -# Type check -pnpm type-check # Check TypeScript types -``` - -## Project Structure - -``` -services/telegram-stats-bot/ -├── src/ -│ ├── main.ts # Application entry point -│ ├── app.module.ts # Root module -│ ├── health.controller.ts # Health check endpoint -│ ├── config/ -│ │ └── configuration.ts # Configuration & website IDs -│ ├── bot/ -│ │ ├── bot.module.ts -│ │ ├── bot.service.ts # Send messages to Telegram -│ │ └── bot.update.ts # Command handlers -│ ├── umami/ -│ │ ├── umami.module.ts -│ │ └── umami.service.ts # Umami API client -│ ├── analytics/ -│ │ ├── analytics.module.ts -│ │ ├── analytics.service.ts # Data aggregation -│ │ └── formatters.ts # Message formatters -│ ├── users/ -│ │ ├── users.module.ts -│ │ └── users.service.ts # User count from auth DB -│ └── scheduler/ -│ ├── scheduler.module.ts -│ └── report.scheduler.ts # Cron jobs -└── Dockerfile -``` - -## Telegram Commands - -| Command | Description | -|---------|-------------| -| `/start` | Show help | -| `/stats` | Overview of all apps (last 30 days) | -| `/today` | Today's statistics | -| `/week` | This week's statistics | -| `/realtime` | Active visitors right now | -| `/users` | Registered user statistics | -| `/help` | Show available commands | - -## Scheduled Reports - -| Report | Schedule | Timezone | -|--------|----------|----------| -| Daily | 09:00 | Europe/Berlin | -| Weekly | Monday 09:00 | Europe/Berlin | - -## Environment Variables - -```env -# Server -PORT=3300 -TZ=Europe/Berlin - -# Telegram -TELEGRAM_BOT_TOKEN=xxx -TELEGRAM_CHAT_ID=xxx - -# Umami -UMAMI_API_URL=http://umami:3000 -UMAMI_USERNAME=admin -UMAMI_PASSWORD=xxx - -# Database (optional, for user counts) -DATABASE_URL=postgresql://... -``` - -## Adding New Website IDs - -Edit `src/config/configuration.ts`: - -```typescript -export const WEBSITE_IDS: Record = { - 'new-app-webapp': 'uuid-from-umami', -}; - -export const DISPLAY_NAMES: Record = { - 'new-app-webapp': 'New App', -}; -``` - -## Docker - -```bash -# Build locally -docker build -f services/telegram-stats-bot/Dockerfile -t telegram-stats-bot . - -# Run -docker run -p 3300:3300 \ - -e TELEGRAM_BOT_TOKEN=xxx \ - -e TELEGRAM_CHAT_ID=xxx \ - -e UMAMI_API_URL=http://umami:3000 \ - -e UMAMI_USERNAME=admin \ - -e UMAMI_PASSWORD=xxx \ - telegram-stats-bot -``` - -## Health Check - -```bash -curl http://localhost:3300/health -``` - -## Testing Bot Commands - -In Telegram, send commands to your bot: - -``` -/start # Shows help message -/today # Gets today's stats -/week # Gets weekly stats -/realtime # Shows active visitors -``` - -## Key Files - -| File | Purpose | -|------|---------| -| `src/config/configuration.ts` | All Umami website IDs | -| `src/analytics/formatters.ts` | Report formatting | -| `src/scheduler/report.scheduler.ts` | Cron job definitions | -| `src/umami/umami.service.ts` | Umami API authentication | diff --git a/services/telegram-stats-bot/Dockerfile b/services/telegram-stats-bot/Dockerfile deleted file mode 100644 index 0400c3e39..000000000 --- a/services/telegram-stats-bot/Dockerfile +++ /dev/null @@ -1,58 +0,0 @@ -# Build stage -FROM node:20-alpine AS builder - -# Install pnpm -RUN npm install -g pnpm@9.15.0 - -WORKDIR /app - -# Copy package files for telegram-stats-bot only (standalone build) -COPY services/telegram-stats-bot/package.json ./ - -# Install all dependencies (including devDependencies for build) -RUN pnpm install - -# Copy source code -COPY services/telegram-stats-bot/src ./src -COPY services/telegram-stats-bot/tsconfig*.json ./ -COPY services/telegram-stats-bot/nest-cli.json ./ - -# Build the application -RUN pnpm build - -# Production stage -FROM node:20-alpine AS production - -# Install pnpm -RUN npm install -g pnpm@9.15.0 - -WORKDIR /app - -# Copy package files -COPY --from=builder /app/package.json ./ - -# Install production dependencies only -RUN pnpm install --prod - -# Copy built application -COPY --from=builder /app/dist ./dist - -# Create non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S nestjs -u 1001 - -# Change ownership -RUN chown -R nestjs:nodejs /app - -# Switch to non-root user -USER nestjs - -# Expose port -EXPOSE 3300 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ - CMD node -e "require('http').get('http://localhost:3300/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" - -# Start the application -CMD ["node", "dist/main"] diff --git a/services/telegram-stats-bot/nest-cli.json b/services/telegram-stats-bot/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/services/telegram-stats-bot/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/services/telegram-stats-bot/package.json b/services/telegram-stats-bot/package.json deleted file mode 100644 index 4b51b0c62..000000000 --- a/services/telegram-stats-bot/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@manacore/telegram-stats-bot", - "version": "1.0.0", - "description": "Telegram bot for ManaCore analytics and statistics from Umami", - "private": true, - "license": "MIT", - "scripts": { - "prebuild": "rimraf dist", - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit" - }, - "dependencies": { - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "@nestjs/schedule": "^4.1.2", - "nestjs-telegraf": "^2.8.0", - "telegraf": "^4.16.3", - "drizzle-orm": "^0.38.3", - "postgres": "^3.4.5", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" - }, - "devDependencies": { - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@types/node": "^22.10.5", - "rimraf": "^6.0.1", - "typescript": "^5.7.3" - } -} diff --git a/services/telegram-stats-bot/src/analytics/analytics.module.ts b/services/telegram-stats-bot/src/analytics/analytics.module.ts deleted file mode 100644 index 116d4409c..000000000 --- a/services/telegram-stats-bot/src/analytics/analytics.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UmamiModule } from '../umami/umami.module'; -import { UsersModule } from '../users/users.module'; -import { AnalyticsService } from './analytics.service'; - -@Module({ - imports: [UmamiModule, UsersModule], - providers: [AnalyticsService], - exports: [AnalyticsService], -}) -export class AnalyticsModule {} diff --git a/services/telegram-stats-bot/src/analytics/analytics.service.ts b/services/telegram-stats-bot/src/analytics/analytics.service.ts deleted file mode 100644 index c1fd01e98..000000000 --- a/services/telegram-stats-bot/src/analytics/analytics.service.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { UmamiService, UmamiStats } from '../umami/umami.service'; -import { UsersService, UserStats } from '../users/users.service'; -import { - formatDailyReport, - formatWeeklyReport, - formatRealtimeReport, - formatStatsOverview, - formatUsersReportCompact, -} from './formatters'; - -@Injectable() -export class AnalyticsService { - private readonly logger = new Logger(AnalyticsService.name); - - constructor( - private readonly umamiService: UmamiService, - private readonly usersService: UsersService - ) {} - - private getStartOfDay(date: Date = new Date()): Date { - const start = new Date(date); - start.setHours(0, 0, 0, 0); - return start; - } - - private getEndOfDay(date: Date = new Date()): Date { - const end = new Date(date); - end.setHours(23, 59, 59, 999); - return end; - } - - private getStartOfWeek(date: Date = new Date()): Date { - const start = new Date(date); - const day = start.getDay(); - const diff = start.getDate() - day + (day === 0 ? -6 : 1); // Adjust for Monday start - start.setDate(diff); - start.setHours(0, 0, 0, 0); - return start; - } - - private getEndOfWeek(date: Date = new Date()): Date { - const end = this.getStartOfWeek(date); - end.setDate(end.getDate() + 6); - end.setHours(23, 59, 59, 999); - return end; - } - - async getTodayStats(): Promise> { - const startAt = this.getStartOfDay().getTime(); - const endAt = this.getEndOfDay().getTime(); - return this.umamiService.getAllWebsiteStats(startAt, endAt); - } - - async getYesterdayStats(): Promise> { - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); - const startAt = this.getStartOfDay(yesterday).getTime(); - const endAt = this.getEndOfDay(yesterday).getTime(); - return this.umamiService.getAllWebsiteStats(startAt, endAt); - } - - async getWeekStats(): Promise> { - const startAt = this.getStartOfWeek().getTime(); - const endAt = this.getEndOfWeek().getTime(); - return this.umamiService.getAllWebsiteStats(startAt, endAt); - } - - async getPreviousWeekStats(): Promise> { - const prevWeekStart = this.getStartOfWeek(); - prevWeekStart.setDate(prevWeekStart.getDate() - 7); - const prevWeekEnd = this.getEndOfWeek(prevWeekStart); - return this.umamiService.getAllWebsiteStats(prevWeekStart.getTime(), prevWeekEnd.getTime()); - } - - async getRealtimeStats(): Promise> { - return this.umamiService.getAllActiveVisitors(); - } - - async generateDailyReport(): Promise { - try { - const stats = await this.getTodayStats(); - let report = formatDailyReport(stats, new Date()); - - // Add user stats to daily report - const userStats = await this.usersService.getUserStats(); - if (userStats) { - report += formatUsersReportCompact(userStats); - } - - return report; - } catch (error) { - this.logger.error('Failed to generate daily report:', error); - return '❌ Fehler beim Erstellen des Daily Reports'; - } - } - - async generateWeeklyReport(): Promise { - try { - const stats = await this.getWeekStats(); - const prevStats = await this.getPreviousWeekStats(); - const weekStart = this.getStartOfWeek(); - const weekEnd = this.getEndOfWeek(); - return formatWeeklyReport(stats, weekStart, weekEnd, prevStats); - } catch (error) { - this.logger.error('Failed to generate weekly report:', error); - return '❌ Fehler beim Erstellen des Weekly Reports'; - } - } - - async generateRealtimeReport(): Promise { - try { - const activeVisitors = await this.getRealtimeStats(); - return formatRealtimeReport(activeVisitors); - } catch (error) { - this.logger.error('Failed to generate realtime report:', error); - return '❌ Fehler beim Abrufen der Realtime-Daten'; - } - } - - async generateStatsOverview(): Promise { - try { - // Get last 30 days stats for overview - const thirtyDaysAgo = new Date(); - thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); - const startAt = this.getStartOfDay(thirtyDaysAgo).getTime(); - const endAt = this.getEndOfDay().getTime(); - const stats = await this.umamiService.getAllWebsiteStats(startAt, endAt); - return formatStatsOverview(stats); - } catch (error) { - this.logger.error('Failed to generate stats overview:', error); - return '❌ Fehler beim Abrufen der Statistiken'; - } - } -} diff --git a/services/telegram-stats-bot/src/analytics/formatters.ts b/services/telegram-stats-bot/src/analytics/formatters.ts deleted file mode 100644 index 89912c25f..000000000 --- a/services/telegram-stats-bot/src/analytics/formatters.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { DISPLAY_NAMES } from '../config/configuration'; -import { UmamiStats } from '../umami/umami.service'; - -export function formatNumber(num: number): string { - return num.toLocaleString('de-DE'); -} - -export function formatChange(change: number): string { - if (change === 0) return '→'; - const sign = change > 0 ? '+' : ''; - return `${sign}${Math.round(change)}%`; -} - -export function formatChangeEmoji(change: number): string { - if (change > 10) return '📈'; - if (change > 0) return '↗'; - if (change < -10) return '📉'; - if (change < 0) return '↘'; - return '→'; -} - -export function getDisplayName(websiteKey: string): string { - return DISPLAY_NAMES[websiteKey] || websiteKey; -} - -export function formatDate(date: Date, format: 'short' | 'long' = 'short'): string { - const options: Intl.DateTimeFormatOptions = - format === 'short' - ? { day: 'numeric', month: 'numeric', year: 'numeric' } - : { day: 'numeric', month: 'long', year: 'numeric' }; - return date.toLocaleDateString('de-DE', options); -} - -export function formatWeekNumber(date: Date): string { - const startOfYear = new Date(date.getFullYear(), 0, 1); - const days = Math.floor((date.getTime() - startOfYear.getTime()) / (24 * 60 * 60 * 1000)); - const weekNumber = Math.ceil((days + startOfYear.getDay() + 1) / 7); - return `KW ${weekNumber}`; -} - -export function formatDailyReport(stats: Map, date: Date): string { - const lines: string[] = [ - '📊 ManaCore Daily Report', - '━━━━━━━━━━━━━━━━━━━━', - '', - `📅 ${formatDate(date, 'long')}`, - '', - '📈 Besucher heute:', - ]; - - // Sort by visitors (descending) - const sortedStats = Array.from(stats.entries()) - .filter(([key]) => key.endsWith('-webapp')) - .sort((a, b) => b[1].visitors.value - a[1].visitors.value); - - let totalVisitors = 0; - let totalPageviews = 0; - - for (const [key, stat] of sortedStats) { - const name = getDisplayName(key).padEnd(12); - const visitors = stat.visitors.value; - const change = formatChange(stat.visitors.change); - const emoji = formatChangeEmoji(stat.visitors.change); - - totalVisitors += visitors; - totalPageviews += stat.pageviews.value; - - lines.push(` ${name}: ${formatNumber(visitors)} (${change}) ${emoji}`); - } - - lines.push(''); - lines.push(`📄 Pageviews: ${formatNumber(totalPageviews)}`); - lines.push(`👥 Besucher gesamt: ${formatNumber(totalVisitors)}`); - - return lines.join('\n'); -} - -export function formatWeeklyReport( - stats: Map, - weekStart: Date, - weekEnd: Date, - prevStats?: Map -): string { - const lines: string[] = [ - '📊 ManaCore Weekly Report', - '━━━━━━━━━━━━━━━━━━━━', - '', - `📅 ${formatWeekNumber(weekStart)} (${formatDate(weekStart)} - ${formatDate(weekEnd)})`, - '', - ' Besucher Pageviews', - ]; - - // Sort by visitors (descending) - const sortedStats = Array.from(stats.entries()) - .filter(([key]) => key.endsWith('-webapp')) - .sort((a, b) => b[1].visitors.value - a[1].visitors.value); - - let totalVisitors = 0; - let totalPageviews = 0; - - for (const [key, stat] of sortedStats) { - const name = getDisplayName(key).padEnd(12); - const visitors = formatNumber(stat.visitors.value).padStart(6); - const pageviews = formatNumber(stat.pageviews.value).padStart(9); - - totalVisitors += stat.visitors.value; - totalPageviews += stat.pageviews.value; - - lines.push(`${name}: ${visitors} ${pageviews}`); - } - - lines.push('────────────────────────────'); - lines.push( - `Total: ${formatNumber(totalVisitors).padStart(6)} ${formatNumber(totalPageviews).padStart(9)}` - ); - - // Calculate week-over-week change if previous stats available - if (prevStats) { - let prevTotal = 0; - for (const [key, stat] of prevStats.entries()) { - if (key.endsWith('-webapp')) { - prevTotal += stat.visitors.value; - } - } - if (prevTotal > 0) { - const change = ((totalVisitors - prevTotal) / prevTotal) * 100; - lines.push(''); - lines.push(`📊 vs. Vorwoche: ${formatChange(change)} ${formatChangeEmoji(change)}`); - } - } - - return lines.join('\n'); -} - -export function formatRealtimeReport(activeVisitors: Map): string { - const lines: string[] = ['🔴 Realtime - Aktive Besucher', '━━━━━━━━━━━━━━━━━━━━', '']; - - // Sort by active visitors (descending) - const sortedVisitors = Array.from(activeVisitors.entries()) - .filter(([key]) => key.endsWith('-webapp')) - .sort((a, b) => b[1] - a[1]); - - let total = 0; - - for (const [key, count] of sortedVisitors) { - const name = getDisplayName(key).padEnd(12); - total += count; - const indicator = count > 0 ? '🟢' : '⚪'; - lines.push(`${indicator} ${name}: ${count}`); - } - - lines.push(''); - lines.push(`👥 Gesamt aktiv: ${total}`); - - return lines.join('\n'); -} - -export function formatStatsOverview(stats: Map): string { - const lines: string[] = ['📊 ManaCore Stats Übersicht', '━━━━━━━━━━━━━━━━━━━━', '']; - - // Group by type - const webapps = Array.from(stats.entries()) - .filter(([key]) => key.endsWith('-webapp')) - .sort((a, b) => b[1].visitors.value - a[1].visitors.value); - - const landings = Array.from(stats.entries()) - .filter(([key]) => key.endsWith('-landing')) - .sort((a, b) => b[1].visitors.value - a[1].visitors.value); - - lines.push('🌐 Web Apps:'); - for (const [key, stat] of webapps) { - const name = getDisplayName(key).padEnd(12); - lines.push(` ${name}: ${formatNumber(stat.visitors.value)} visitors`); - } - - if (landings.length > 0) { - lines.push(''); - lines.push('🏠 Landing Pages:'); - for (const [key, stat] of landings) { - const name = getDisplayName(key).padEnd(12); - lines.push(` ${name}: ${formatNumber(stat.visitors.value)} visitors`); - } - } - - return lines.join('\n'); -} - -export function formatHelpMessage(): string { - return `🤖 ManaCore Stats Bot -━━━━━━━━━━━━━━━━━━━━ - -Verfügbare Befehle: - -/stats - Übersicht aller Apps -/today - Heutige Statistiken -/week - Wochenstatistiken -/realtime - Aktive Besucher jetzt -/users - Registrierte User -/help - Diese Hilfe anzeigen - -📅 Automatische Reports: -• Daily: Jeden Tag um 9:00 -• Weekly: Jeden Montag um 9:00`; -} - -export interface DailyRegistration { - date: string; - count: number; -} - -export interface UserStats { - totalUsers: number; - verifiedUsers: number; - todayNewUsers: number; - yesterdayNewUsers: number; - weekNewUsers: number; - lastWeekNewUsers: number; - monthNewUsers: number; - dailyRegistrations: DailyRegistration[]; -} - -function createMiniBarChart(dailyRegistrations: DailyRegistration[]): string[] { - if (dailyRegistrations.length === 0) return []; - - const maxCount = Math.max(...dailyRegistrations.map((d) => d.count), 1); - const barChars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; - - // Fill in missing days and sort - const last7Days: DailyRegistration[] = []; - for (let i = 6; i >= 0; i--) { - const date = new Date(); - date.setDate(date.getDate() - i); - const dateStr = date.toISOString().split('T')[0]; - const found = dailyRegistrations.find((d) => d.date === dateStr); - last7Days.push({ date: dateStr, count: found?.count || 0 }); - } - - const bars = last7Days.map((d) => { - const index = Math.floor((d.count / maxCount) * (barChars.length - 1)); - return barChars[Math.max(0, index)]; - }); - - const dayLabels = last7Days.map((d) => { - const date = new Date(d.date); - return ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'][date.getDay()]; - }); - - return [`${bars.join('')}`, `${dayLabels.join('')}`]; -} - -export function formatUsersReport(stats: UserStats): string { - const verificationRate = - stats.totalUsers > 0 ? Math.round((stats.verifiedUsers / stats.totalUsers) * 100) : 0; - - // Calculate trends - const dailyTrend = - stats.yesterdayNewUsers > 0 - ? ((stats.todayNewUsers - stats.yesterdayNewUsers) / stats.yesterdayNewUsers) * 100 - : stats.todayNewUsers > 0 - ? 100 - : 0; - - const weeklyTrend = - stats.lastWeekNewUsers > 0 - ? ((stats.weekNewUsers - stats.lastWeekNewUsers) / stats.lastWeekNewUsers) * 100 - : stats.weekNewUsers > 0 - ? 100 - : 0; - - const lines: string[] = [ - '👥 ManaCore User Statistics', - '━━━━━━━━━━━━━━━━━━━━', - '', - '📊 Übersicht', - ` 👤 Gesamt: ${formatNumber(stats.totalUsers)}`, - ` ✅ Verifiziert: ${formatNumber(stats.verifiedUsers)} (${verificationRate}%)`, - '', - '📈 Neue Registrierungen', - ` Heute: +${formatNumber(stats.todayNewUsers)} ${formatChangeEmoji(dailyTrend)}`, - ` Gestern: +${formatNumber(stats.yesterdayNewUsers)}`, - ` Diese Woche: +${formatNumber(stats.weekNewUsers)} ${formatChange(weeklyTrend)} ${formatChangeEmoji(weeklyTrend)}`, - ` Dieser Monat: +${formatNumber(stats.monthNewUsers)}`, - ]; - - // Add mini bar chart for last 7 days - if (stats.dailyRegistrations.length > 0) { - lines.push(''); - lines.push('📅 Letzte 7 Tage'); - lines.push(...createMiniBarChart(stats.dailyRegistrations)); - } - - return lines.join('\n'); -} - -export function formatUsersReportCompact(stats: UserStats): string { - const verificationRate = - stats.totalUsers > 0 ? Math.round((stats.verifiedUsers / stats.totalUsers) * 100) : 0; - - return [ - '', - '👥 Registrierte User', - ` Gesamt: ${formatNumber(stats.totalUsers)} (${verificationRate}% verifiziert)`, - ` Heute: +${formatNumber(stats.todayNewUsers)} | Woche: +${formatNumber(stats.weekNewUsers)} | Monat: +${formatNumber(stats.monthNewUsers)}`, - ].join('\n'); -} diff --git a/services/telegram-stats-bot/src/app.module.ts b/services/telegram-stats-bot/src/app.module.ts deleted file mode 100644 index df0fc3beb..000000000 --- a/services/telegram-stats-bot/src/app.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { ScheduleModule } from '@nestjs/schedule'; -import { TelegrafModule } from 'nestjs-telegraf'; -import configuration from './config/configuration'; -import { BotModule } from './bot/bot.module'; -import { UmamiModule } from './umami/umami.module'; -import { AnalyticsModule } from './analytics/analytics.module'; -import { SchedulerModule } from './scheduler/scheduler.module'; -import { HealthController } from './health.controller'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [configuration], - }), - ScheduleModule.forRoot(), - TelegrafModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - token: configService.get('telegram.botToken') || '', - launchOptions: { - dropPendingUpdates: true, - }, - }), - inject: [ConfigService], - }), - BotModule, - UmamiModule, - AnalyticsModule, - SchedulerModule, - ], - controllers: [HealthController], -}) -export class AppModule {} diff --git a/services/telegram-stats-bot/src/bot/bot.module.ts b/services/telegram-stats-bot/src/bot/bot.module.ts deleted file mode 100644 index f6134cccc..000000000 --- a/services/telegram-stats-bot/src/bot/bot.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AnalyticsModule } from '../analytics/analytics.module'; -import { UsersModule } from '../users/users.module'; -import { BotService } from './bot.service'; -import { BotUpdate } from './bot.update'; - -@Module({ - imports: [AnalyticsModule, UsersModule], - providers: [BotService, BotUpdate], - exports: [BotService], -}) -export class BotModule {} diff --git a/services/telegram-stats-bot/src/bot/bot.service.ts b/services/telegram-stats-bot/src/bot/bot.service.ts deleted file mode 100644 index 42eddae36..000000000 --- a/services/telegram-stats-bot/src/bot/bot.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { InjectBot } from 'nestjs-telegraf'; -import { Telegraf, Context } from 'telegraf'; - -@Injectable() -export class BotService { - private readonly logger = new Logger(BotService.name); - private readonly chatId: string; - - constructor( - @InjectBot() private readonly bot: Telegraf, - private readonly configService: ConfigService - ) { - this.chatId = this.configService.get('telegram.chatId') || ''; - } - - async sendMessage(message: string, chatId?: string): Promise { - const targetChatId = chatId || this.chatId; - - if (!targetChatId) { - this.logger.warn('No chat ID configured, skipping message'); - return; - } - - try { - await this.bot.telegram.sendMessage(targetChatId, message, { - parse_mode: 'HTML', - }); - this.logger.log(`Message sent to chat ${targetChatId}`); - } catch (error) { - this.logger.error(`Failed to send message: ${error}`); - throw error; - } - } - - async sendReport(report: string): Promise { - return this.sendMessage(report); - } -} diff --git a/services/telegram-stats-bot/src/bot/bot.update.ts b/services/telegram-stats-bot/src/bot/bot.update.ts deleted file mode 100644 index 8cb5b47b8..000000000 --- a/services/telegram-stats-bot/src/bot/bot.update.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { Update, Ctx, Start, Help, Command } from 'nestjs-telegraf'; -import { Context } from 'telegraf'; -import { AnalyticsService } from '../analytics/analytics.service'; -import { UsersService } from '../users/users.service'; -import { formatHelpMessage, formatUsersReport } from '../analytics/formatters'; - -@Update() -export class BotUpdate { - private readonly logger = new Logger(BotUpdate.name); - - constructor( - private readonly analyticsService: AnalyticsService, - private readonly usersService: UsersService - ) {} - - @Start() - async start(@Ctx() ctx: Context) { - this.logger.log(`/start command from ${ctx.from?.id}`); - await ctx.replyWithHTML(formatHelpMessage()); - } - - @Help() - async help(@Ctx() ctx: Context) { - this.logger.log(`/help command from ${ctx.from?.id}`); - await ctx.replyWithHTML(formatHelpMessage()); - } - - @Command('stats') - async stats(@Ctx() ctx: Context) { - this.logger.log(`/stats command from ${ctx.from?.id}`); - await ctx.reply('📊 Lade Statistiken...'); - - const report = await this.analyticsService.generateStatsOverview(); - await ctx.replyWithHTML(report); - } - - @Command('today') - async today(@Ctx() ctx: Context) { - this.logger.log(`/today command from ${ctx.from?.id}`); - await ctx.reply('📊 Lade heutige Statistiken...'); - - const report = await this.analyticsService.generateDailyReport(); - await ctx.replyWithHTML(report); - } - - @Command('week') - async week(@Ctx() ctx: Context) { - this.logger.log(`/week command from ${ctx.from?.id}`); - await ctx.reply('📊 Lade Wochenstatistiken...'); - - const report = await this.analyticsService.generateWeeklyReport(); - await ctx.replyWithHTML(report); - } - - @Command('realtime') - async realtime(@Ctx() ctx: Context) { - this.logger.log(`/realtime command from ${ctx.from?.id}`); - await ctx.reply('🔴 Lade Realtime-Daten...'); - - const report = await this.analyticsService.generateRealtimeReport(); - await ctx.replyWithHTML(report); - } - - @Command('users') - async users(@Ctx() ctx: Context) { - this.logger.log(`/users command from ${ctx.from?.id}`); - await ctx.reply('👥 Lade User-Statistiken...'); - - try { - const stats = await this.usersService.getUserStats(); - if (!stats) { - this.logger.warn('User stats returned null - database may not be configured'); - await ctx.reply('❌ Datenbank nicht verfügbar. Prüfe DATABASE_URL Konfiguration.'); - return; - } - - const report = formatUsersReport(stats); - await ctx.replyWithHTML(report); - } catch (error) { - this.logger.error('Failed to get user stats:', error); - await ctx.reply( - `❌ Fehler beim Laden der User-Statistiken: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}` - ); - } - } -} diff --git a/services/telegram-stats-bot/src/config/configuration.ts b/services/telegram-stats-bot/src/config/configuration.ts deleted file mode 100644 index e284628bd..000000000 --- a/services/telegram-stats-bot/src/config/configuration.ts +++ /dev/null @@ -1,71 +0,0 @@ -export default () => ({ - port: parseInt(process.env.PORT || '3300', 10), - timezone: process.env.TZ || 'Europe/Berlin', - telegram: { - botToken: process.env.TELEGRAM_BOT_TOKEN, - chatId: process.env.TELEGRAM_CHAT_ID, - }, - umami: { - apiUrl: process.env.UMAMI_API_URL || 'http://localhost:3200', - username: process.env.UMAMI_USERNAME || 'admin', - password: process.env.UMAMI_PASSWORD, - }, - database: { - url: process.env.DATABASE_URL, - }, -}); - -export const WEBSITE_IDS: Record = { - // Landing Pages - 'chat-landing': 'a264b165-80d2-47ab-91f4-2efc01de0b66', - 'manacore-landing': 'cef3798d-85ae-47df-a44a-e9bee09dbcf9', - 'clock-landing': '0332b471-a022-46af-a726-0f45932bfd58', - - // Web Apps - 'chat-webapp': '5cf9d569-3266-4a57-80dd-3a652dc32786', - 'manacore-webapp': '4a14016d-394a-44e0-8ecc-67271f63ffb0', - 'todo-webapp': 'ac021d98-778e-46cf-b6b2-2f650ea78f07', - 'calendar-webapp': '884fc0a8-3b67-43bd-903b-2be531c66792', - 'clock-webapp': '1e7b5006-87a5-4547-8a3d-ab30eac15dd4', - 'contacts-webapp': 'ab89a839-be15-4949-99b4-e72492cee4ff', - 'picture-webapp': 'bc552bd2-667d-44b4-a717-0dce6a8db98f', - 'manadeck-webapp': '314fc57a-c63d-4008-b19e-5e272c0329d6', - 'planta-webapp': '876f30bd-43e3-405a-9697-6157db67ca6b', - 'zitare-landing': '17e7f92d-8f85-4e78-a4f5-10f0b47e8fb8', - 'zitare-webapp': '8ad3c21f-6e9b-4d1e-b3a2-5c8f7d6e9a4b', -}; - -// Grouped websites for reporting -export const WEBSITE_GROUPS = { - landings: ['chat-landing', 'manacore-landing', 'clock-landing', 'zitare-landing'], - webapps: [ - 'manacore-webapp', - 'chat-webapp', - 'todo-webapp', - 'calendar-webapp', - 'clock-webapp', - 'contacts-webapp', - 'picture-webapp', - 'manadeck-webapp', - 'planta-webapp', - 'zitare-webapp', - ], -}; - -// Display names for reports -export const DISPLAY_NAMES: Record = { - 'chat-landing': 'Chat Landing', - 'chat-webapp': 'Chat', - 'manacore-landing': 'ManaCore Landing', - 'manacore-webapp': 'ManaCore', - 'todo-webapp': 'Todo', - 'calendar-webapp': 'Calendar', - 'clock-landing': 'Clock Landing', - 'clock-webapp': 'Clock', - 'contacts-webapp': 'Contacts', - 'picture-webapp': 'Picture', - 'manadeck-webapp': 'ManaDeck', - 'planta-webapp': 'Planta', - 'zitare-landing': 'Zitare Landing', - 'zitare-webapp': 'Zitare', -}; diff --git a/services/telegram-stats-bot/src/health.controller.ts b/services/telegram-stats-bot/src/health.controller.ts deleted file mode 100644 index 33c9821ce..000000000 --- a/services/telegram-stats-bot/src/health.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller() -export class HealthController { - @Get('health') - health() { - return { - status: 'ok', - service: 'telegram-stats-bot', - timestamp: new Date().toISOString(), - }; - } -} diff --git a/services/telegram-stats-bot/src/main.ts b/services/telegram-stats-bot/src/main.ts deleted file mode 100644 index 2aeb47dde..000000000 --- a/services/telegram-stats-bot/src/main.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const logger = new Logger('Bootstrap'); - const app = await NestFactory.create(AppModule); - - const configService = app.get(ConfigService); - const port = configService.get('port') || 3300; - - await app.listen(port); - logger.log(`Telegram Stats Bot running on port ${port}`); - logger.log(`Timezone: ${configService.get('timezone')}`); -} - -bootstrap(); diff --git a/services/telegram-stats-bot/src/scheduler/report.scheduler.ts b/services/telegram-stats-bot/src/scheduler/report.scheduler.ts deleted file mode 100644 index c6e38a19b..000000000 --- a/services/telegram-stats-bot/src/scheduler/report.scheduler.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; -import { AnalyticsService } from '../analytics/analytics.service'; -import { BotService } from '../bot/bot.service'; - -@Injectable() -export class ReportScheduler { - private readonly logger = new Logger(ReportScheduler.name); - - constructor( - private readonly analyticsService: AnalyticsService, - private readonly botService: BotService - ) {} - - /** - * Daily Report - Every day at 9:00 AM Europe/Berlin - * Cron: minute hour day month weekday - */ - @Cron('0 9 * * *', { - name: 'daily-report', - timeZone: 'Europe/Berlin', - }) - async sendDailyReport(): Promise { - this.logger.log('Starting daily report...'); - - try { - const report = await this.analyticsService.generateDailyReport(); - await this.botService.sendReport(report); - this.logger.log('Daily report sent successfully'); - } catch (error) { - this.logger.error('Failed to send daily report:', error); - } - } - - /** - * Weekly Report - Every Monday at 9:00 AM Europe/Berlin - * Cron: minute hour day month weekday (1 = Monday) - */ - @Cron('0 9 * * 1', { - name: 'weekly-report', - timeZone: 'Europe/Berlin', - }) - async sendWeeklyReport(): Promise { - this.logger.log('Starting weekly report...'); - - try { - const report = await this.analyticsService.generateWeeklyReport(); - await this.botService.sendReport(report); - this.logger.log('Weekly report sent successfully'); - } catch (error) { - this.logger.error('Failed to send weekly report:', error); - } - } - - /** - * Health check log - Every hour - * Useful for debugging and ensuring the scheduler is running - */ - @Cron('0 * * * *', { - name: 'scheduler-health', - timeZone: 'Europe/Berlin', - }) - healthCheck(): void { - this.logger.debug('Scheduler health check - running'); - } -} diff --git a/services/telegram-stats-bot/src/scheduler/scheduler.module.ts b/services/telegram-stats-bot/src/scheduler/scheduler.module.ts deleted file mode 100644 index a76e6c92e..000000000 --- a/services/telegram-stats-bot/src/scheduler/scheduler.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AnalyticsModule } from '../analytics/analytics.module'; -import { BotModule } from '../bot/bot.module'; -import { ReportScheduler } from './report.scheduler'; - -@Module({ - imports: [AnalyticsModule, BotModule], - providers: [ReportScheduler], -}) -export class SchedulerModule {} diff --git a/services/telegram-stats-bot/src/umami/umami.module.ts b/services/telegram-stats-bot/src/umami/umami.module.ts deleted file mode 100644 index b9bb7b2bd..000000000 --- a/services/telegram-stats-bot/src/umami/umami.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UmamiService } from './umami.service'; - -@Module({ - providers: [UmamiService], - exports: [UmamiService], -}) -export class UmamiModule {} diff --git a/services/telegram-stats-bot/src/umami/umami.service.ts b/services/telegram-stats-bot/src/umami/umami.service.ts deleted file mode 100644 index eb2d52e54..000000000 --- a/services/telegram-stats-bot/src/umami/umami.service.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { WEBSITE_IDS } from '../config/configuration'; - -export interface UmamiStats { - pageviews: { value: number; change: number }; - visitors: { value: number; change: number }; - visits: { value: number; change: number }; - bounces: { value: number; change: number }; - totalTime: { value: number; change: number }; -} - -export interface ActiveVisitors { - websiteId: string; - visitors: number; -} - -@Injectable() -export class UmamiService implements OnModuleInit { - private readonly logger = new Logger(UmamiService.name); - private apiUrl: string; - private username: string; - private password: string; - private authToken: string | null = null; - private tokenExpiry: Date | null = null; - - constructor(private configService: ConfigService) { - this.apiUrl = this.configService.get('umami.apiUrl') || 'http://localhost:3200'; - this.username = this.configService.get('umami.username') || 'admin'; - this.password = this.configService.get('umami.password') || ''; - } - - async onModuleInit() { - try { - await this.authenticate(); - this.logger.log('Successfully authenticated with Umami'); - } catch (error) { - this.logger.warn( - 'Failed to authenticate with Umami on startup. Will retry on first request.' - ); - } - } - - private async authenticate(): Promise { - const response = await fetch(`${this.apiUrl}/api/auth/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - username: this.username, - password: this.password, - }), - }); - - if (!response.ok) { - throw new Error(`Umami auth failed: ${response.status}`); - } - - const data = await response.json(); - this.authToken = data.token; - // Token is valid for 24 hours, refresh after 23 hours - this.tokenExpiry = new Date(Date.now() + 23 * 60 * 60 * 1000); - } - - private async getAuthToken(): Promise { - if (!this.authToken || !this.tokenExpiry || this.tokenExpiry < new Date()) { - await this.authenticate(); - } - return this.authToken!; - } - - private async apiRequest(endpoint: string): Promise { - const token = await this.getAuthToken(); - const response = await fetch(`${this.apiUrl}${endpoint}`, { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error(`Umami API error: ${response.status} ${await response.text()}`); - } - - return response.json(); - } - - async getWebsiteStats(websiteId: string, startAt: number, endAt: number): Promise { - return this.apiRequest( - `/api/websites/${websiteId}/stats?startAt=${startAt}&endAt=${endAt}` - ); - } - - async getActiveVisitors(websiteId: string): Promise { - try { - const result = await this.apiRequest(`/api/websites/${websiteId}/active`); - return result?.[0]?.visitors || 0; - } catch { - return 0; - } - } - - async getAllWebsiteStats(startAt: number, endAt: number): Promise> { - const results = new Map(); - - for (const [name, id] of Object.entries(WEBSITE_IDS)) { - try { - const stats = await this.getWebsiteStats(id, startAt, endAt); - results.set(name, stats); - } catch (error) { - this.logger.warn(`Failed to get stats for ${name}: ${error}`); - } - } - - return results; - } - - async getAllActiveVisitors(): Promise> { - const results = new Map(); - - for (const [name, id] of Object.entries(WEBSITE_IDS)) { - try { - const visitors = await this.getActiveVisitors(id); - results.set(name, visitors); - } catch (error) { - this.logger.warn(`Failed to get active visitors for ${name}: ${error}`); - } - } - - return results; - } - - getWebsiteId(name: string): string | undefined { - return WEBSITE_IDS[name]; - } -} diff --git a/services/telegram-stats-bot/src/users/users.module.ts b/services/telegram-stats-bot/src/users/users.module.ts deleted file mode 100644 index 00ef465ea..000000000 --- a/services/telegram-stats-bot/src/users/users.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UsersService } from './users.service'; - -@Module({ - providers: [UsersService], - exports: [UsersService], -}) -export class UsersModule {} diff --git a/services/telegram-stats-bot/src/users/users.service.ts b/services/telegram-stats-bot/src/users/users.service.ts deleted file mode 100644 index c87479c51..000000000 --- a/services/telegram-stats-bot/src/users/users.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import postgres from 'postgres'; - -export interface UserStats { - totalUsers: number; - verifiedUsers: number; - todayNewUsers: number; - weekNewUsers: number; - monthNewUsers: number; - yesterdayNewUsers: number; - lastWeekNewUsers: number; - dailyRegistrations: DailyRegistration[]; -} - -export interface DailyRegistration { - date: string; - count: number; -} - -@Injectable() -export class UsersService implements OnModuleInit { - private readonly logger = new Logger(UsersService.name); - private sql: postgres.Sql | null = null; - private databaseUrl: string | undefined; - - constructor(private configService: ConfigService) { - this.databaseUrl = this.configService.get('database.url'); - } - - async onModuleInit() { - if (this.databaseUrl) { - try { - // Mask password in logs - const maskedUrl = this.databaseUrl.replace(/:([^@]+)@/, ':****@'); - this.logger.log(`Connecting to database: ${maskedUrl}`); - this.sql = postgres(this.databaseUrl); - // Test connection - await this.sql`SELECT 1`; - this.logger.log('Database connection initialized and tested successfully'); - } catch (error) { - this.logger.error('Failed to initialize database connection:', error); - this.sql = null; - } - } else { - this.logger.warn('DATABASE_URL not configured, user stats will be unavailable'); - } - } - - async getUserStats(): Promise { - if (!this.sql) { - return null; - } - - try { - const now = new Date(); - const startOfToday = new Date(now); - startOfToday.setHours(0, 0, 0, 0); - - const startOfYesterday = new Date(startOfToday); - startOfYesterday.setDate(startOfYesterday.getDate() - 1); - - const startOfWeek = new Date(now); - const day = startOfWeek.getDay(); - const diff = startOfWeek.getDate() - day + (day === 0 ? -6 : 1); - startOfWeek.setDate(diff); - startOfWeek.setHours(0, 0, 0, 0); - - const startOfLastWeek = new Date(startOfWeek); - startOfLastWeek.setDate(startOfLastWeek.getDate() - 7); - - const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); - - // Main stats query - const [result] = await this.sql` - SELECT - COUNT(*) as total_users, - COUNT(*) FILTER (WHERE email_verified = true) as verified_users, - COUNT(*) FILTER (WHERE created_at >= ${startOfToday.toISOString()}) as today_new_users, - COUNT(*) FILTER (WHERE created_at >= ${startOfYesterday.toISOString()} AND created_at < ${startOfToday.toISOString()}) as yesterday_new_users, - COUNT(*) FILTER (WHERE created_at >= ${startOfWeek.toISOString()}) as week_new_users, - COUNT(*) FILTER (WHERE created_at >= ${startOfLastWeek.toISOString()} AND created_at < ${startOfWeek.toISOString()}) as last_week_new_users, - COUNT(*) FILTER (WHERE created_at >= ${startOfMonth.toISOString()}) as month_new_users - FROM auth.users - WHERE deleted_at IS NULL - `; - - // Get daily registrations for last 7 days - const dailyStats = await this.sql` - SELECT - DATE(created_at) as date, - COUNT(*) as count - FROM auth.users - WHERE deleted_at IS NULL - AND created_at >= ${new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString()} - GROUP BY DATE(created_at) - ORDER BY date DESC - `; - - const dailyRegistrations: DailyRegistration[] = dailyStats.map((row) => ({ - date: new Date(row.date).toISOString().split('T')[0], - count: Number(row.count), - })); - - return { - totalUsers: Number(result.total_users), - verifiedUsers: Number(result.verified_users), - todayNewUsers: Number(result.today_new_users), - yesterdayNewUsers: Number(result.yesterday_new_users), - weekNewUsers: Number(result.week_new_users), - lastWeekNewUsers: Number(result.last_week_new_users), - monthNewUsers: Number(result.month_new_users), - dailyRegistrations, - }; - } catch (error) { - this.logger.error('Failed to fetch user stats:', error); - return null; - } - } -} diff --git a/services/telegram-stats-bot/tsconfig.json b/services/telegram-stats-bot/tsconfig.json deleted file mode 100644 index 1e5880c81..000000000 --- a/services/telegram-stats-bot/tsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2022", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "skipLibCheck": true, - "strictNullChecks": true, - "noImplicitAny": true, - "strictBindCallApply": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/services/telegram-todo-bot/.env.example b/services/telegram-todo-bot/.env.example deleted file mode 100644 index 7f791224c..000000000 --- a/services/telegram-todo-bot/.env.example +++ /dev/null @@ -1,14 +0,0 @@ -# Server -PORT=3304 - -# Telegram -TELEGRAM_BOT_TOKEN=xxx - -# Database (Bot's own database for user mappings) -DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/todo_bot - -# Todo Backend API -TODO_API_URL=http://localhost:3018 - -# Mana Core Auth -MANA_CORE_AUTH_URL=http://localhost:3001 diff --git a/services/telegram-todo-bot/CLAUDE.md b/services/telegram-todo-bot/CLAUDE.md deleted file mode 100644 index 2e9a51f32..000000000 --- a/services/telegram-todo-bot/CLAUDE.md +++ /dev/null @@ -1,209 +0,0 @@ -# Telegram Todo Bot - -Telegram Bot fuer Todo - Aufgabenverwaltung via Telegram. - -## Tech Stack - -- **Framework**: NestJS 10 -- **Telegram**: nestjs-telegraf + Telegraf -- **Database**: PostgreSQL + Drizzle ORM -- **Scheduler**: @nestjs/schedule -- **API Client**: Calls Todo Backend (localhost:3018) - -## Commands - -```bash -# Development -pnpm start:dev # Start with hot reload - -# Build -pnpm build # Production build - -# Type check -pnpm type-check # Check TypeScript types - -# Database -pnpm db:generate # Generate migrations -pnpm db:push # Push schema to database -pnpm db:studio # Open Drizzle Studio -``` - -## Telegram Commands - -| Command | Beschreibung | -|---------|--------------| -| `/start` | Willkommensnachricht | -| `/help` | Hilfe anzeigen | -| `/login` | Account verknuepfen | -| `/logout` | Account trennen | -| `/add [Text]` | Neue Aufgabe erstellen | -| `/inbox` | Inbox-Aufgaben anzeigen | -| `/today` | Heutige Aufgaben | -| `/list` | Alle offenen Aufgaben | -| `/done [Nr]` | Aufgabe als erledigt markieren | -| `/projects` | Projekte anzeigen | -| `/remind` | Taegliche Erinnerung an/aus | - -## User Flow - -``` -1. /start → Willkommen -2. /login → Email eingeben -3. [Email eingeben] → Passwort eingeben -4. [Passwort eingeben] → Account verknuepft -5. /today → Heutige Aufgaben -6. /add Einkaufen → Aufgabe erstellt -7. /done 1 → Aufgabe erledigt -8. /remind → Taegliche Erinnerung aktivieren -``` - -## Architecture - -Der Bot verwendet einen **API-Client Ansatz**: -- Bot hat eigene DB fuer Telegram User ↔ Todo User Mapping -- Ruft Todo Backend REST API auf fuer Task-Operationen -- Kein direkter DB-Zugriff auf Todo-Datenbank - -``` -┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Telegram │────>│ Todo Bot │────>│ Todo Backend │ -│ User │ │ (port 3304) │ │ (port 3018) │ -└─────────────┘ └─────────────────┘ └─────────────────┘ - │ │ - │ Bot DB (user mapping) │ Todo DB (tasks) - ▼ ▼ - ┌─────────────┐ ┌─────────────┐ - │ todo_bot │ │ todo │ - │ (PG DB) │ │ (PG DB) │ - └─────────────┘ └─────────────┘ -``` - -## Environment Variables - -```env -# Server -PORT=3304 - -# Telegram -TELEGRAM_BOT_TOKEN=xxx # Bot Token von @BotFather - -# Database (Bot's own database) -DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/todo_bot - -# Todo Backend API -TODO_API_URL=http://localhost:3018 - -# Mana Core Auth -MANA_CORE_AUTH_URL=http://localhost:3001 -``` - -## Projekt-Struktur - -``` -services/telegram-todo-bot/ -├── src/ -│ ├── main.ts # Entry point -│ ├── app.module.ts # Root module -│ ├── health.controller.ts # Health endpoint -│ ├── config/ -│ │ └── configuration.ts # Config -│ ├── database/ -│ │ ├── database.module.ts # Drizzle connection -│ │ └── schema.ts # DB schema (user mapping) -│ ├── bot/ -│ │ ├── bot.module.ts -│ │ └── bot.update.ts # Command handlers -│ ├── todo-client/ -│ │ ├── todo-client.module.ts -│ │ ├── todo-client.service.ts # Todo API wrapper -│ │ └── types.ts # TypeScript interfaces -│ ├── user/ -│ │ ├── user.module.ts -│ │ └── user.service.ts # Account linking, settings -│ └── scheduler/ -│ ├── scheduler.module.ts -│ └── reminder.scheduler.ts # Cron fuer 08:00 Uhr -├── drizzle/ # Migrations -├── drizzle.config.ts -├── package.json -└── .env.example -``` - -## Lokale Entwicklung - -### 1. Bot bei Telegram erstellen - -1. Oeffne @BotFather in Telegram -2. Sende `/newbot` -3. Waehle einen Namen (z.B. "Todo Bot") -4. Waehle einen Username (z.B. "mana_todo_bot") -5. Kopiere den Token - -### 2. Umgebung vorbereiten - -```bash -# Docker Services starten (PostgreSQL) -pnpm docker:up - -# Datenbank erstellen und Schema pushen -pnpm dev:todo-bot:full -``` - -### 3. Bot starten - -```bash -# Nur Bot starten (DB muss existieren) -pnpm dev:todo-bot -``` - -## Features - -- **Account-Verknuepfung**: Login via Email/Passwort -- **Aufgaben erstellen**: Schnell neue Aufgaben anlegen -- **Aufgaben anzeigen**: Inbox, Today, alle offenen -- **Aufgaben erledigen**: Per Nummer abhaken -- **Projekte**: Projektliste anzeigen -- **Taegliche Erinnerung**: Automatisch um 08:00 Uhr - -## Datenbank-Schema - -``` -telegram_users -├── id (UUID) -├── telegram_user_id (BIGINT, unique) -├── telegram_username (TEXT) -├── mana_user_id (TEXT) # Verknuepfter Todo-User -├── access_token (TEXT) # JWT fuer API-Calls -├── refresh_token (TEXT) -├── token_expires_at (TIMESTAMP) -├── daily_reminder_enabled (BOOLEAN) -├── daily_reminder_time (TEXT, default '08:00') -├── timezone (TEXT, default 'Europe/Berlin') -├── created_at, updated_at -``` - -## Health Check - -```bash -curl http://localhost:3304/health -``` - -## MVP Features (Phase 1) - -- `/start`, `/help` -- `/login`, `/logout` - Account-Verknuepfung -- `/add [text]` - Aufgabe in Inbox erstellen -- `/today` - Heutige Aufgaben -- `/inbox` - Inbox-Aufgaben -- `/list` - Alle offenen Aufgaben -- `/done [Nr]` - Abhaken -- `/projects` - Projektliste -- `/remind` - Taegliche Erinnerung - -## Spaetere Features (Phase 2) - -- `/add @projekt [text]` - Aufgabe in Projekt -- `/due [Nr] [Datum]` - Faelligkeitsdatum setzen -- `/priority [Nr] [hoch/mittel/niedrig]` -- Inline-Buttons fuer schnelle Aktionen -- OAuth-basiertes Login (statt Email/Passwort) diff --git a/services/telegram-todo-bot/drizzle.config.ts b/services/telegram-todo-bot/drizzle.config.ts deleted file mode 100644 index c102bbfc1..000000000 --- a/services/telegram-todo-bot/drizzle.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createDrizzleConfig } from '@manacore/shared-drizzle-config'; - -export default createDrizzleConfig({ - dbName: 'todo_bot', - schemaPath: './src/database/schema.ts', - outDir: './drizzle', - verbose: false, - strict: false, -}); diff --git a/services/telegram-todo-bot/nest-cli.json b/services/telegram-todo-bot/nest-cli.json deleted file mode 100644 index 95538fb90..000000000 --- a/services/telegram-todo-bot/nest-cli.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true - } -} diff --git a/services/telegram-todo-bot/package.json b/services/telegram-todo-bot/package.json deleted file mode 100644 index c35742d68..000000000 --- a/services/telegram-todo-bot/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@manacore/telegram-todo-bot", - "version": "1.0.0", - "description": "Telegram bot for Todo - Task management via Telegram", - "private": true, - "license": "MIT", - "scripts": { - "prebuild": "rimraf dist", - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit", - "db:generate": "drizzle-kit generate", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" - }, - "dependencies": { - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "@nestjs/schedule": "^4.1.2", - "drizzle-orm": "^0.38.3", - "nestjs-telegraf": "^2.8.0", - "postgres": "^3.4.5", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "telegraf": "^4.16.3" - }, - "devDependencies": { - "@manacore/shared-drizzle-config": "workspace:*", - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@types/node": "^22.10.5", - "drizzle-kit": "^0.30.1", - "rimraf": "^6.0.1", - "typescript": "^5.7.3" - } -} diff --git a/services/telegram-todo-bot/src/app.module.ts b/services/telegram-todo-bot/src/app.module.ts deleted file mode 100644 index c85d9f80f..000000000 --- a/services/telegram-todo-bot/src/app.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { TelegrafModule } from 'nestjs-telegraf'; -import configuration from './config/configuration'; -import { DatabaseModule } from './database/database.module'; -import { BotModule } from './bot/bot.module'; -import { SchedulerModule } from './scheduler/scheduler.module'; -import { HealthController } from './health.controller'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [configuration], - }), - TelegrafModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - token: configService.get('telegram.token') || '', - }), - inject: [ConfigService], - }), - DatabaseModule, - BotModule, - SchedulerModule, - ], - controllers: [HealthController], -}) -export class AppModule {} diff --git a/services/telegram-todo-bot/src/bot/bot.module.ts b/services/telegram-todo-bot/src/bot/bot.module.ts deleted file mode 100644 index f7e0b4ce7..000000000 --- a/services/telegram-todo-bot/src/bot/bot.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BotUpdate } from './bot.update'; -import { TodoClientModule } from '../todo-client/todo-client.module'; -import { UserModule } from '../user/user.module'; - -@Module({ - imports: [TodoClientModule, UserModule], - providers: [BotUpdate], -}) -export class BotModule {} diff --git a/services/telegram-todo-bot/src/bot/bot.update.ts b/services/telegram-todo-bot/src/bot/bot.update.ts deleted file mode 100644 index dad03215f..000000000 --- a/services/telegram-todo-bot/src/bot/bot.update.ts +++ /dev/null @@ -1,460 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { Update, Ctx, Start, Help, Command, Message, On } from 'nestjs-telegraf'; -import { Context } from 'telegraf'; -import { TodoClientService } from '../todo-client/todo-client.service'; -import { UserService } from '../user/user.service'; -import { Task } from '../todo-client/types'; - -// State for users currently in the login flow -interface LoginState { - step: 'email' | 'password'; - email?: string; -} - -@Update() -export class BotUpdate { - private readonly logger = new Logger(BotUpdate.name); - - // Track last shown tasks per user for /done command - private lastTaskList: Map = new Map(); - - // Track users in login flow - private loginFlow: Map = new Map(); - - constructor( - private readonly todoClient: TodoClientService, - private readonly userService: UserService - ) {} - - private formatHelp(): string { - return `Todo Bot - -Verwalte deine Aufgaben direkt in Telegram. - -Aufgaben: -/add [Text] - Neue Aufgabe erstellen -/inbox - Inbox-Aufgaben anzeigen -/today - Heutige Aufgaben -/list - Alle offenen Aufgaben -/done [Nr] - Aufgabe als erledigt markieren - -Projekte: -/projects - Projekte anzeigen - -Einstellungen: -/remind - Taegliche Erinnerung an/aus -/login - Account verknuepfen -/logout - Account trennen - -Tipp: Starte mit /today fuer deine heutigen Aufgaben!`; - } - - @Start() - async start(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - const username = ctx.from?.username; - - if (!userId) return; - - // Ensure user exists in database - await this.userService.ensureUser(userId, username); - const linkedUser = await this.userService.getLinkedUser(userId); - - this.logger.log(`/start from user ${userId} (@${username})`); - - if (linkedUser) { - await ctx.replyWithHTML( - `Willkommen zurueck!\n\n` + - `Dein Account ist verknuepft. Du kannst sofort loslegen.\n\n` + - this.formatHelp() - ); - } else { - await ctx.replyWithHTML( - `Willkommen beim Todo Bot!\n\n` + - `Um Aufgaben zu verwalten, verknuepfe deinen Account:\n` + - `/login - Mit Email/Passwort anmelden\n\n` + - `Oder sieh dir die Hilfe an:\n` + - `/help - Alle Befehle anzeigen` - ); - } - } - - @Help() - async help(@Ctx() ctx: Context) { - await ctx.replyWithHTML(this.formatHelp()); - } - - @Command('login') - async login(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - await this.userService.ensureUser(userId, ctx.from?.username); - - // Check if already linked - const linkedUser = await this.userService.getLinkedUser(userId); - if (linkedUser) { - await ctx.reply( - 'Dein Account ist bereits verknuepft.\n\n' + - 'Mit /logout kannst du die Verknuepfung aufheben.' - ); - return; - } - - // Start login flow - this.loginFlow.set(userId, { step: 'email' }); - await ctx.reply('Bitte gib deine E-Mail-Adresse ein:'); - } - - @Command('logout') - async logout(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - const linkedUser = await this.userService.getLinkedUser(userId); - if (!linkedUser) { - await ctx.reply('Kein Account verknuepft.'); - return; - } - - await this.userService.unlinkAccount(userId); - await ctx.reply( - 'Account-Verknuepfung wurde aufgehoben.\n\nMit /login kannst du dich erneut anmelden.' - ); - } - - @On('text') - async onText(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId) return; - - // Check if user is in login flow - const loginState = this.loginFlow.get(userId); - if (!loginState) return; // Not in login flow, ignore - - // Ignore commands - if (text.startsWith('/')) return; - - if (loginState.step === 'email') { - // Validate email format - if (!text.includes('@')) { - await ctx.reply('Bitte gib eine gueltige E-Mail-Adresse ein:'); - return; - } - - this.loginFlow.set(userId, { step: 'password', email: text.trim() }); - await ctx.reply('Bitte gib dein Passwort ein:'); - } else if (loginState.step === 'password') { - const email = loginState.email!; - const password = text.trim(); - - // Clear login flow - this.loginFlow.delete(userId); - - // Attempt login - const result = await this.userService.linkAccount(userId, email, password); - - if (result.success) { - await ctx.replyWithHTML( - 'Account erfolgreich verknuepft!\n\n' + - 'Du kannst jetzt Aufgaben verwalten.\n\n' + - 'Probiere /today fuer deine heutigen Aufgaben.' - ); - } else { - await ctx.reply(result.error || 'Anmeldung fehlgeschlagen.'); - } - } - } - - @Command('add') - async addTask(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId) return; - - const user = await this.userService.getLinkedUser(userId); - if (!user) { - await ctx.reply('Bitte verknuepfe erst deinen Account mit /login'); - return; - } - - const title = text.replace('/add', '').trim(); - if (!title) { - await ctx.reply('Verwendung: /add Aufgabentext\n\nBeispiel: /add Einkaufen gehen'); - return; - } - - try { - const task = await this.todoClient.createTask(user.accessToken!, title); - await ctx.reply(`Aufgabe erstellt: "${task.title}"`); - } catch (error) { - this.logger.error(`Failed to create task: ${error}`); - await ctx.reply('Fehler beim Erstellen der Aufgabe. Bitte versuche es erneut.'); - } - } - - @Command('inbox') - async inboxTasks(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - const user = await this.userService.getLinkedUser(userId); - if (!user) { - await ctx.reply('Bitte verknuepfe erst deinen Account mit /login'); - return; - } - - try { - const tasks = await this.todoClient.getInboxTasks(user.accessToken!); - this.lastTaskList.set(userId, tasks); - - if (tasks.length === 0) { - await ctx.reply('Keine Aufgaben in der Inbox.\n\nErstelle eine mit /add [Text]'); - return; - } - - let response = `Inbox (${tasks.length}):\n\n`; - tasks.slice(0, 20).forEach((task, i) => { - const status = task.isCompleted ? '' : ''; - const priority = this.formatPriority(task.priority); - response += `${i + 1}. ${status} ${task.title}${priority}\n`; - }); - - if (tasks.length > 20) { - response += `\n... und ${tasks.length - 20} weitere`; - } - - response += '\n\nAbhaken mit /done [Nr]'; - await ctx.replyWithHTML(response); - } catch (error) { - this.logger.error(`Failed to get inbox: ${error}`); - await ctx.reply('Fehler beim Laden der Inbox.'); - } - } - - @Command('today') - async todayTasks(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - const user = await this.userService.getLinkedUser(userId); - if (!user) { - await ctx.reply('Bitte verknuepfe erst deinen Account mit /login'); - return; - } - - try { - const tasks = await this.todoClient.getTodayTasks(user.accessToken!); - this.lastTaskList.set(userId, tasks); - - if (tasks.length === 0) { - await ctx.reply('Keine Aufgaben fuer heute!\n\nErstelle eine mit /add [Text]'); - return; - } - - let response = `Heute (${tasks.length}):\n\n`; - tasks.slice(0, 20).forEach((task, i) => { - const status = task.isCompleted ? '' : ''; - const priority = this.formatPriority(task.priority); - const overdue = this.isOverdue(task.dueDate) ? ' (ueberfaellig)' : ''; - response += `${i + 1}. ${status} ${task.title}${priority}${overdue}\n`; - }); - - if (tasks.length > 20) { - response += `\n... und ${tasks.length - 20} weitere`; - } - - response += '\n\nAbhaken mit /done [Nr]'; - await ctx.replyWithHTML(response); - } catch (error) { - this.logger.error(`Failed to get today tasks: ${error}`); - await ctx.reply('Fehler beim Laden der heutigen Aufgaben.'); - } - } - - @Command('list') - async listTasks(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - const user = await this.userService.getLinkedUser(userId); - if (!user) { - await ctx.reply('Bitte verknuepfe erst deinen Account mit /login'); - return; - } - - try { - const tasks = await this.todoClient.getAllTasks(user.accessToken!, false); - this.lastTaskList.set(userId, tasks); - - if (tasks.length === 0) { - await ctx.reply('Keine offenen Aufgaben.\n\nErstelle eine mit /add [Text]'); - return; - } - - let response = `Alle Aufgaben (${tasks.length}):\n\n`; - tasks.slice(0, 20).forEach((task, i) => { - const priority = this.formatPriority(task.priority); - const dueInfo = this.formatDueDate(task.dueDate); - response += `${i + 1}. ${task.title}${priority}${dueInfo}\n`; - }); - - if (tasks.length > 20) { - response += `\n... und ${tasks.length - 20} weitere`; - } - - response += '\n\nAbhaken mit /done [Nr]'; - await ctx.replyWithHTML(response); - } catch (error) { - this.logger.error(`Failed to get tasks: ${error}`); - await ctx.reply('Fehler beim Laden der Aufgaben.'); - } - } - - @Command('done') - async completeTask(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId) return; - - const user = await this.userService.getLinkedUser(userId); - if (!user) { - await ctx.reply('Bitte verknuepfe erst deinen Account mit /login'); - return; - } - - const nrStr = text.replace('/done', '').trim(); - const nr = parseInt(nrStr, 10); - - if (!nrStr || isNaN(nr) || nr < 1) { - await ctx.reply( - 'Verwendung: /done [Nr]\n\n' + - 'Zeige erst deine Aufgaben mit /today, /inbox oder /list um die Nummer zu sehen.' - ); - return; - } - - const tasks = this.lastTaskList.get(userId); - if (!tasks || tasks.length === 0) { - await ctx.reply( - 'Keine Aufgabenliste im Cache. Bitte erst /today, /inbox oder /list ausfuehren.' - ); - return; - } - - if (nr > tasks.length) { - await ctx.reply(`Ungueltige Nummer. Du hast ${tasks.length} Aufgaben in der Liste.`); - return; - } - - const task = tasks[nr - 1]; - - try { - await this.todoClient.completeTask(user.accessToken!, task.id); - await ctx.reply(`"${task.title}" erledigt!`); - - // Remove from cache - tasks.splice(nr - 1, 1); - this.lastTaskList.set(userId, tasks); - } catch (error) { - this.logger.error(`Failed to complete task: ${error}`); - await ctx.reply('Fehler beim Abschliessen der Aufgabe.'); - } - } - - @Command('projects') - async showProjects(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - const user = await this.userService.getLinkedUser(userId); - if (!user) { - await ctx.reply('Bitte verknuepfe erst deinen Account mit /login'); - return; - } - - try { - const projects = await this.todoClient.getProjects(user.accessToken!); - - if (projects.length === 0) { - await ctx.reply('Keine Projekte vorhanden.'); - return; - } - - let response = `Projekte (${projects.length}):\n\n`; - projects.forEach((project, i) => { - const icon = project.icon || ''; - const archived = project.isArchived ? ' (archiviert)' : ''; - const isDefault = project.isDefault ? ' (Inbox)' : ''; - response += `${i + 1}. ${icon} ${project.name}${isDefault}${archived}\n`; - }); - - await ctx.replyWithHTML(response); - } catch (error) { - this.logger.error(`Failed to get projects: ${error}`); - await ctx.reply('Fehler beim Laden der Projekte.'); - } - } - - @Command('remind') - async toggleReminder(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - await this.userService.ensureUser(userId, ctx.from?.username); - - const newState = await this.userService.toggleDailyReminder(userId); - const settings = await this.userService.getDailyReminderSettings(userId); - - if (newState) { - await ctx.replyWithHTML( - `Taegliche Erinnerung aktiviert!\n\n` + - `Du erhaeltst jeden Tag um ${settings?.time || '08:00'} Uhr eine Uebersicht deiner Aufgaben.\n\n` + - `Mit /remind wieder deaktivieren.` - ); - } else { - await ctx.reply('Taegliche Erinnerung deaktiviert.'); - } - } - - private formatPriority(priority: string): string { - switch (priority) { - case 'urgent': - return ' !!!'; - case 'high': - return ' !!'; - case 'low': - return ''; - default: - return ''; - } - } - - private formatDueDate(dueDate: string | null): string { - if (!dueDate) return ''; - - const date = new Date(dueDate); - const today = new Date(); - today.setHours(0, 0, 0, 0); - - const tomorrow = new Date(today); - tomorrow.setDate(tomorrow.getDate() + 1); - - if (date < today) { - return ' (ueberfaellig)'; - } else if (date < tomorrow) { - return ' (heute)'; - } else { - const options: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit' }; - return ` (${date.toLocaleDateString('de-DE', options)})`; - } - } - - private isOverdue(dueDate: string | null): boolean { - if (!dueDate) return false; - - const date = new Date(dueDate); - const today = new Date(); - today.setHours(0, 0, 0, 0); - - return date < today; - } -} diff --git a/services/telegram-todo-bot/src/config/configuration.ts b/services/telegram-todo-bot/src/config/configuration.ts deleted file mode 100644 index 8c0a58297..000000000 --- a/services/telegram-todo-bot/src/config/configuration.ts +++ /dev/null @@ -1,15 +0,0 @@ -export default () => ({ - port: parseInt(process.env.PORT || '3304', 10), - telegram: { - token: process.env.TELEGRAM_BOT_TOKEN, - }, - database: { - url: process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/todo_bot', - }, - todoApi: { - url: process.env.TODO_API_URL || 'http://localhost:3018', - }, - manaCore: { - authUrl: process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001', - }, -}); diff --git a/services/telegram-todo-bot/src/database/database.module.ts b/services/telegram-todo-bot/src/database/database.module.ts deleted file mode 100644 index 905674bf6..000000000 --- a/services/telegram-todo-bot/src/database/database.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from './schema'; - -export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_CONNECTION, - useFactory: (configService: ConfigService) => { - const connectionString = configService.get('database.url'); - const client = postgres(connectionString!); - return drizzle(client, { schema }); - }, - inject: [ConfigService], - }, - ], - exports: [DATABASE_CONNECTION], -}) -export class DatabaseModule {} diff --git a/services/telegram-todo-bot/src/database/schema.ts b/services/telegram-todo-bot/src/database/schema.ts deleted file mode 100644 index a3b7ec26f..000000000 --- a/services/telegram-todo-bot/src/database/schema.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { pgTable, uuid, text, timestamp, bigint, boolean } from 'drizzle-orm/pg-core'; - -// Telegram users - Mapping Telegram User <-> Todo User -export const telegramUsers = pgTable('telegram_users', { - id: uuid('id').primaryKey().defaultRandom(), - telegramUserId: bigint('telegram_user_id', { mode: 'number' }).notNull().unique(), - telegramUsername: text('telegram_username'), - // Linking with mana-core-auth - manaUserId: text('mana_user_id'), - accessToken: text('access_token'), - refreshToken: text('refresh_token'), - tokenExpiresAt: timestamp('token_expires_at'), - // Settings - dailyReminderEnabled: boolean('daily_reminder_enabled').default(false).notNull(), - dailyReminderTime: text('daily_reminder_time').default('08:00').notNull(), - timezone: text('timezone').default('Europe/Berlin').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}); - -// Types -export type TelegramUser = typeof telegramUsers.$inferSelect; -export type NewTelegramUser = typeof telegramUsers.$inferInsert; diff --git a/services/telegram-todo-bot/src/health.controller.ts b/services/telegram-todo-bot/src/health.controller.ts deleted file mode 100644 index ea78b59a6..000000000 --- a/services/telegram-todo-bot/src/health.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller('health') -export class HealthController { - @Get() - check() { - return { - status: 'ok', - service: 'telegram-todo-bot', - timestamp: new Date().toISOString(), - }; - } -} diff --git a/services/telegram-todo-bot/src/main.ts b/services/telegram-todo-bot/src/main.ts deleted file mode 100644 index 69c14555d..000000000 --- a/services/telegram-todo-bot/src/main.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const logger = new Logger('Bootstrap'); - const app = await NestFactory.create(AppModule); - - const configService = app.get(ConfigService); - const port = configService.get('port') || 3304; - - await app.listen(port); - logger.log(`Telegram Todo Bot running on port ${port}`); - logger.log(`Health check: http://localhost:${port}/health`); -} - -bootstrap(); diff --git a/services/telegram-todo-bot/src/scheduler/reminder.scheduler.ts b/services/telegram-todo-bot/src/scheduler/reminder.scheduler.ts deleted file mode 100644 index a684aafd8..000000000 --- a/services/telegram-todo-bot/src/scheduler/reminder.scheduler.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron } from '@nestjs/schedule'; -import { InjectBot } from 'nestjs-telegraf'; -import { Telegraf, Context } from 'telegraf'; -import { TodoClientService } from '../todo-client/todo-client.service'; -import { UserService } from '../user/user.service'; - -@Injectable() -export class ReminderScheduler { - private readonly logger = new Logger(ReminderScheduler.name); - - constructor( - @InjectBot() private readonly bot: Telegraf, - private readonly todoClient: TodoClientService, - private readonly userService: UserService - ) {} - - // Run every day at 8:00 AM Europe/Berlin - @Cron('0 8 * * *', { - timeZone: 'Europe/Berlin', - }) - async sendDailyReminders() { - this.logger.log('Starting daily reminder distribution...'); - - try { - const users = await this.userService.getUsersWithDailyReminderEnabled(); - this.logger.log(`Found ${users.length} users with daily reminder enabled`); - - let sent = 0; - let failed = 0; - - for (const user of users) { - // Skip users without linked account - if (!user.accessToken) { - this.logger.debug(`Skipping user ${user.telegramUserId}: no linked account`); - continue; - } - - try { - // Get today's tasks - const tasks = await this.todoClient.getTodayTasks(user.accessToken); - - let message: string; - if (tasks.length === 0) { - message = `Guten Morgen!\n\nDu hast keine Aufgaben fuer heute. Genieße den Tag!\n\nMit /add kannst du neue Aufgaben erstellen.`; - } else { - message = `Guten Morgen!\n\nDeine Aufgaben fuer heute (${tasks.length}):\n\n`; - - tasks.slice(0, 10).forEach((task, i) => { - const priority = this.formatPriority(task.priority); - const overdue = this.isOverdue(task.dueDate) ? ' (ueberfaellig)' : ''; - message += `${i + 1}. ${task.title}${priority}${overdue}\n`; - }); - - if (tasks.length > 10) { - message += `\n... und ${tasks.length - 10} weitere\n`; - } - - message += '\nAbhaken mit /done [Nr]'; - } - - await this.bot.telegram.sendMessage(user.telegramUserId, message, { - parse_mode: 'HTML', - }); - - sent++; - this.logger.debug(`Sent daily reminder to user ${user.telegramUserId}`); - } catch (error) { - failed++; - this.logger.warn( - `Failed to send daily reminder to user ${user.telegramUserId}: ${error}` - ); - - // If user blocked the bot, disable reminder - if ((error as { response?: { error_code?: number } }).response?.error_code === 403) { - this.logger.log(`User ${user.telegramUserId} blocked bot, disabling daily reminder`); - await this.userService.toggleDailyReminder(user.telegramUserId); - } - } - } - - this.logger.log(`Daily reminder distribution complete: ${sent} sent, ${failed} failed`); - } catch (error) { - this.logger.error('Daily reminder distribution failed:', error); - } - } - - private formatPriority(priority: string): string { - switch (priority) { - case 'urgent': - return ' !!!'; - case 'high': - return ' !!'; - default: - return ''; - } - } - - private isOverdue(dueDate: string | null): boolean { - if (!dueDate) return false; - - const date = new Date(dueDate); - const today = new Date(); - today.setHours(0, 0, 0, 0); - - return date < today; - } -} diff --git a/services/telegram-todo-bot/src/scheduler/scheduler.module.ts b/services/telegram-todo-bot/src/scheduler/scheduler.module.ts deleted file mode 100644 index 78d8cd23e..000000000 --- a/services/telegram-todo-bot/src/scheduler/scheduler.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { ReminderScheduler } from './reminder.scheduler'; -import { TodoClientModule } from '../todo-client/todo-client.module'; -import { UserModule } from '../user/user.module'; - -@Module({ - imports: [ScheduleModule.forRoot(), TodoClientModule, UserModule], - providers: [ReminderScheduler], -}) -export class SchedulerModule {} diff --git a/services/telegram-todo-bot/src/todo-client/todo-client.module.ts b/services/telegram-todo-bot/src/todo-client/todo-client.module.ts deleted file mode 100644 index fa5ad0446..000000000 --- a/services/telegram-todo-bot/src/todo-client/todo-client.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TodoClientService } from './todo-client.service'; - -@Module({ - providers: [TodoClientService], - exports: [TodoClientService], -}) -export class TodoClientModule {} diff --git a/services/telegram-todo-bot/src/todo-client/todo-client.service.ts b/services/telegram-todo-bot/src/todo-client/todo-client.service.ts deleted file mode 100644 index 6d1d3f22d..000000000 --- a/services/telegram-todo-bot/src/todo-client/todo-client.service.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { - Task, - Project, - CreateTaskDto, - TasksResponse, - TaskResponse, - ProjectsResponse, -} from './types'; - -@Injectable() -export class TodoClientService { - private readonly logger = new Logger(TodoClientService.name); - private readonly baseUrl: string; - - constructor(private configService: ConfigService) { - this.baseUrl = this.configService.get('todoApi.url') || 'http://localhost:3018'; - } - - private async request( - token: string, - method: string, - path: string, - body?: unknown - ): Promise { - const url = `${this.baseUrl}${path}`; - this.logger.debug(`${method} ${url}`); - - const response = await fetch(url, { - method, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: body ? JSON.stringify(body) : undefined, - }); - - if (!response.ok) { - const errorText = await response.text(); - this.logger.error(`API Error: ${response.status} - ${errorText}`); - throw new Error(`Todo API error: ${response.status}`); - } - - return response.json() as Promise; - } - - // Task Operations - - async createTask(token: string, title: string, projectId?: string): Promise { - const dto: CreateTaskDto = { title }; - if (projectId) { - dto.projectId = projectId; - } - - const response = await this.request(token, 'POST', '/tasks', dto); - return response.task; - } - - async getInboxTasks(token: string): Promise { - const response = await this.request(token, 'GET', '/tasks/inbox'); - return response.tasks; - } - - async getTodayTasks(token: string): Promise { - const response = await this.request(token, 'GET', '/tasks/today'); - return response.tasks; - } - - async getAllTasks(token: string, isCompleted = false): Promise { - const response = await this.request( - token, - 'GET', - `/tasks?isCompleted=${isCompleted}` - ); - return response.tasks; - } - - async getUpcomingTasks(token: string, days = 7): Promise { - const response = await this.request( - token, - 'GET', - `/tasks/upcoming?days=${days}` - ); - return response.tasks; - } - - async completeTask(token: string, taskId: string): Promise { - const response = await this.request(token, 'POST', `/tasks/${taskId}/complete`); - return response.task; - } - - async uncompleteTask(token: string, taskId: string): Promise { - const response = await this.request(token, 'POST', `/tasks/${taskId}/uncomplete`); - return response.task; - } - - async deleteTask(token: string, taskId: string): Promise { - await this.request<{ success: boolean }>(token, 'DELETE', `/tasks/${taskId}`); - } - - // Project Operations - - async getProjects(token: string): Promise { - const response = await this.request(token, 'GET', '/projects'); - return response.projects; - } - - async getProjectById(token: string, projectId: string): Promise { - try { - const response = await this.request<{ project: Project }>( - token, - 'GET', - `/projects/${projectId}` - ); - return response.project; - } catch { - return null; - } - } -} diff --git a/services/telegram-todo-bot/src/todo-client/types.ts b/services/telegram-todo-bot/src/todo-client/types.ts deleted file mode 100644 index 8ff94c325..000000000 --- a/services/telegram-todo-bot/src/todo-client/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent'; -export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; - -export interface Subtask { - id: string; - title: string; - isCompleted: boolean; - order: number; -} - -export interface Task { - id: string; - projectId: string | null; - userId: string; - title: string; - description: string | null; - dueDate: string | null; - dueTime: string | null; - priority: TaskPriority; - status: TaskStatus; - isCompleted: boolean; - completedAt: string | null; - order: number; - subtasks: Subtask[] | null; - createdAt: string; - updatedAt: string; -} - -export interface Project { - id: string; - userId: string; - name: string; - color: string | null; - icon: string | null; - order: number; - isArchived: boolean; - isDefault: boolean; - createdAt: string; - updatedAt: string; -} - -export interface CreateTaskDto { - title: string; - description?: string; - projectId?: string; - dueDate?: string; - priority?: TaskPriority; -} - -export interface TasksResponse { - tasks: Task[]; -} - -export interface TaskResponse { - task: Task; -} - -export interface ProjectsResponse { - projects: Project[]; -} diff --git a/services/telegram-todo-bot/src/user/user.module.ts b/services/telegram-todo-bot/src/user/user.module.ts deleted file mode 100644 index ab6ead25c..000000000 --- a/services/telegram-todo-bot/src/user/user.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UserService } from './user.service'; - -@Module({ - providers: [UserService], - exports: [UserService], -}) -export class UserModule {} diff --git a/services/telegram-todo-bot/src/user/user.service.ts b/services/telegram-todo-bot/src/user/user.service.ts deleted file mode 100644 index d3f82f5ee..000000000 --- a/services/telegram-todo-bot/src/user/user.service.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { eq } from 'drizzle-orm'; -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import * as schema from '../database/schema'; -import { TelegramUser } from '../database/schema'; - -interface AuthResponse { - accessToken: string; - refreshToken: string; - user: { - id: string; - email: string; - }; -} - -@Injectable() -export class UserService { - private readonly logger = new Logger(UserService.name); - private readonly authUrl: string; - - constructor( - @Inject(DATABASE_CONNECTION) - private readonly db: PostgresJsDatabase, - private readonly configService: ConfigService - ) { - this.authUrl = this.configService.get('manaCore.authUrl') || 'http://localhost:3001'; - } - - async ensureUser(telegramUserId: number, username?: string): Promise { - // Try to find existing user - const existing = await this.db.query.telegramUsers.findFirst({ - where: eq(schema.telegramUsers.telegramUserId, telegramUserId), - }); - - if (existing) { - // Update username if changed - if (username && existing.telegramUsername !== username) { - await this.db - .update(schema.telegramUsers) - .set({ telegramUsername: username, updatedAt: new Date() }) - .where(eq(schema.telegramUsers.id, existing.id)); - } - return existing; - } - - // Create new user - const [newUser] = await this.db - .insert(schema.telegramUsers) - .values({ - telegramUserId, - telegramUsername: username, - }) - .returning(); - - this.logger.log(`Created new user: ${telegramUserId} (@${username})`); - return newUser; - } - - async getLinkedUser(telegramUserId: number): Promise { - const user = await this.db.query.telegramUsers.findFirst({ - where: eq(schema.telegramUsers.telegramUserId, telegramUserId), - }); - - if (!user || !user.accessToken) { - return null; - } - - // Check if token is expired - if (user.tokenExpiresAt && user.tokenExpiresAt < new Date()) { - // Try to refresh the token - if (user.refreshToken) { - const refreshed = await this.refreshAccessToken(user); - if (refreshed) { - return refreshed; - } - } - return null; - } - - return user; - } - - async linkAccount( - telegramUserId: number, - email: string, - password: string - ): Promise<{ success: boolean; error?: string }> { - try { - // Authenticate with mana-core-auth - 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 error = await response.text(); - this.logger.warn(`Login failed for telegram user ${telegramUserId}: ${error}`); - return { success: false, error: 'Anmeldung fehlgeschlagen. Bitte Zugangsdaten pruefen.' }; - } - - const data = (await response.json()) as AuthResponse; - - // Calculate token expiry (15 minutes from now, or parse from JWT) - const tokenExpiresAt = new Date(); - tokenExpiresAt.setMinutes(tokenExpiresAt.getMinutes() + 14); // 14 min to be safe - - // Update the user with tokens - await this.db - .update(schema.telegramUsers) - .set({ - manaUserId: data.user.id, - accessToken: data.accessToken, - refreshToken: data.refreshToken, - tokenExpiresAt, - updatedAt: new Date(), - }) - .where(eq(schema.telegramUsers.telegramUserId, telegramUserId)); - - this.logger.log(`Linked telegram user ${telegramUserId} to mana user ${data.user.id}`); - return { success: true }; - } catch (error) { - this.logger.error(`Failed to link account: ${error}`); - return { success: false, error: 'Verbindungsfehler. Bitte spaeter erneut versuchen.' }; - } - } - - async unlinkAccount(telegramUserId: number): Promise { - await this.db - .update(schema.telegramUsers) - .set({ - manaUserId: null, - accessToken: null, - refreshToken: null, - tokenExpiresAt: null, - updatedAt: new Date(), - }) - .where(eq(schema.telegramUsers.telegramUserId, telegramUserId)); - } - - private async refreshAccessToken(user: TelegramUser): Promise { - try { - const response = await fetch(`${this.authUrl}/api/v1/auth/refresh`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken: user.refreshToken }), - }); - - if (!response.ok) { - this.logger.warn(`Token refresh failed for user ${user.telegramUserId}`); - return null; - } - - const data = (await response.json()) as AuthResponse; - - const tokenExpiresAt = new Date(); - tokenExpiresAt.setMinutes(tokenExpiresAt.getMinutes() + 14); - - const [updated] = await this.db - .update(schema.telegramUsers) - .set({ - accessToken: data.accessToken, - refreshToken: data.refreshToken, - tokenExpiresAt, - updatedAt: new Date(), - }) - .where(eq(schema.telegramUsers.id, user.id)) - .returning(); - - return updated; - } catch (error) { - this.logger.error(`Token refresh error: ${error}`); - return null; - } - } - - async toggleDailyReminder(telegramUserId: number): Promise { - const user = await this.db.query.telegramUsers.findFirst({ - where: eq(schema.telegramUsers.telegramUserId, telegramUserId), - }); - - if (!user) { - return false; - } - - const newValue = !user.dailyReminderEnabled; - await this.db - .update(schema.telegramUsers) - .set({ dailyReminderEnabled: newValue, updatedAt: new Date() }) - .where(eq(schema.telegramUsers.id, user.id)); - - return newValue; - } - - async setDailyReminderTime(telegramUserId: number, time: string): Promise { - await this.db - .update(schema.telegramUsers) - .set({ dailyReminderTime: time, updatedAt: new Date() }) - .where(eq(schema.telegramUsers.telegramUserId, telegramUserId)); - } - - async getUsersWithDailyReminderEnabled(): Promise { - return this.db.query.telegramUsers.findMany({ - where: eq(schema.telegramUsers.dailyReminderEnabled, true), - }); - } - - async getDailyReminderSettings( - telegramUserId: number - ): Promise<{ enabled: boolean; time: string } | null> { - const user = await this.db.query.telegramUsers.findFirst({ - where: eq(schema.telegramUsers.telegramUserId, telegramUserId), - }); - - if (!user) { - return null; - } - - return { - enabled: user.dailyReminderEnabled, - time: user.dailyReminderTime, - }; - } -} diff --git a/services/telegram-todo-bot/tsconfig.json b/services/telegram-todo-bot/tsconfig.json deleted file mode 100644 index c705ffcb3..000000000 --- a/services/telegram-todo-bot/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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, - "resolveJsonModule": true - } -} diff --git a/services/telegram-zitare-bot/.env.example b/services/telegram-zitare-bot/.env.example deleted file mode 100644 index db1d222a5..000000000 --- a/services/telegram-zitare-bot/.env.example +++ /dev/null @@ -1,8 +0,0 @@ -# Server -PORT=3303 - -# Telegram -TELEGRAM_BOT_TOKEN=xxx # Bot Token von @BotFather - -# Database -DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/zitare_bot diff --git a/services/telegram-zitare-bot/CLAUDE.md b/services/telegram-zitare-bot/CLAUDE.md deleted file mode 100644 index 08caefdcd..000000000 --- a/services/telegram-zitare-bot/CLAUDE.md +++ /dev/null @@ -1,161 +0,0 @@ -# Telegram Zitare Bot - -Telegram Bot fuer Zitare - deutsche Inspirationszitate. - -## Tech Stack - -- **Framework**: NestJS 10 -- **Telegram**: nestjs-telegraf + Telegraf -- **Database**: PostgreSQL + Drizzle ORM -- **Scheduler**: @nestjs/schedule - -## Commands - -```bash -# Development -pnpm start:dev # Start with hot reload - -# Build -pnpm build # Production build - -# Type check -pnpm type-check # Check TypeScript types - -# Database -pnpm db:generate # Generate migrations -pnpm db:push # Push schema to database -pnpm db:studio # Open Drizzle Studio -``` - -## Telegram Commands - -| Command | Beschreibung | -|---------|--------------| -| `/start` | Willkommensnachricht | -| `/help` | Hilfe anzeigen | -| `/quote` | Zufaelliges Zitat | -| `/zitat` | Alias fuer /quote | -| `/search [Begriff]` | Zitate suchen | -| `/author [Name]` | Zitate eines Autors | -| `/favorite` | Aktuelles Zitat speichern | -| `/favorites` | Favoriten anzeigen | -| `/removefav [Nr]` | Favorit entfernen | -| `/daily` | Taegliches Zitat an/aus | - -## User Flow - -``` -1. /start → Willkommen -2. /quote → Zufaelliges Zitat -3. /favorite → Zitat zu Favoriten -4. /favorites → Liste der Favoriten -5. /daily → Taegliches Zitat aktivieren -``` - -## Environment Variables - -```env -# Server -PORT=3303 - -# Telegram -TELEGRAM_BOT_TOKEN=xxx # Bot Token von @BotFather - -# Database -DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/zitare_bot -``` - -## Projekt-Struktur - -``` -services/telegram-zitare-bot/ -├── src/ -│ ├── main.ts # Entry point -│ ├── app.module.ts # Root module -│ ├── health.controller.ts # Health endpoint -│ ├── config/ -│ │ └── configuration.ts # Config -│ ├── database/ -│ │ ├── database.module.ts # Drizzle connection -│ │ └── schema.ts # DB schema -│ ├── bot/ -│ │ ├── bot.module.ts -│ │ └── bot.update.ts # Command handlers -│ ├── quotes/ -│ │ ├── quotes.module.ts -│ │ ├── quotes.service.ts # Zitat-Logik -│ │ ├── types.ts # TypeScript Interfaces -│ │ └── data/ -│ │ ├── quotes.json # Deutsche Zitate -│ │ └── authors.json # Autoren -│ ├── user/ -│ │ ├── user.module.ts -│ │ └── user.service.ts # Favoriten, Daily -│ └── scheduler/ -│ ├── scheduler.module.ts -│ └── daily.scheduler.ts # Cron fuer 08:00 Uhr -├── drizzle/ # Migrations -├── drizzle.config.ts -├── package.json -└── .env.example -``` - -## Lokale Entwicklung - -### 1. Bot bei Telegram erstellen - -1. Oeffne @BotFather in Telegram -2. Sende `/newbot` -3. Waehle einen Namen (z.B. "Zitare Bot") -4. Waehle einen Username (z.B. "zitare_inspiration_bot") -5. Kopiere den Token - -### 2. Umgebung vorbereiten - -```bash -# Docker Services starten (PostgreSQL) -pnpm docker:up - -# Datenbank erstellen und Schema pushen -pnpm dev:zitare-bot:full -``` - -### 3. Bot starten - -```bash -# Nur Bot starten (DB muss existieren) -pnpm dev:zitare-bot -``` - -## Features - -- **Zitat-Suche**: Nach Begriff oder Autor suchen -- **Favoriten**: Lieblingszitate speichern -- **Taegliches Zitat**: Automatisch um 08:00 Uhr -- **40+ deutsche Zitate**: Von Einstein bis Goethe - -## Datenbank-Schema - -``` -telegram_users -├── id (UUID) -├── telegram_user_id (BIGINT, unique) -├── telegram_username (TEXT) -├── daily_enabled (BOOLEAN) -├── daily_time (TEXT, default '08:00') -├── timezone (TEXT, default 'Europe/Berlin') -├── created_at, updated_at - -user_favorites -├── id (UUID) -├── telegram_user_id (BIGINT) -├── quote_id (TEXT) -├── created_at -├── UNIQUE(telegram_user_id, quote_id) -``` - -## Health Check - -```bash -curl http://localhost:3303/health -``` diff --git a/services/telegram-zitare-bot/drizzle.config.ts b/services/telegram-zitare-bot/drizzle.config.ts deleted file mode 100644 index e012c250a..000000000 --- a/services/telegram-zitare-bot/drizzle.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createDrizzleConfig } from '@manacore/shared-drizzle-config'; - -export default createDrizzleConfig({ - dbName: 'zitare_bot', - schemaPath: './src/database/schema.ts', - outDir: './drizzle', - verbose: false, - strict: false, -}); diff --git a/services/telegram-zitare-bot/nest-cli.json b/services/telegram-zitare-bot/nest-cli.json deleted file mode 100644 index 07c816dc4..000000000 --- a/services/telegram-zitare-bot/nest-cli.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/nest-cli", - "collection": "@nestjs/schematics", - "sourceRoot": "src", - "compilerOptions": { - "deleteOutDir": true, - "assets": ["quotes/data/**/*.json"], - "watchAssets": true - } -} diff --git a/services/telegram-zitare-bot/package.json b/services/telegram-zitare-bot/package.json deleted file mode 100644 index 065f74314..000000000 --- a/services/telegram-zitare-bot/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@manacore/telegram-zitare-bot", - "version": "1.0.0", - "description": "Telegram bot for Zitare - German inspiration quotes", - "private": true, - "license": "MIT", - "scripts": { - "prebuild": "rimraf dist", - "build": "nest build", - "format": "prettier --write \"src/**/*.ts\"", - "start": "nest start", - "start:dev": "nest start --watch", - "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", - "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "type-check": "tsc --noEmit", - "db:generate": "drizzle-kit generate", - "db:push": "drizzle-kit push", - "db:studio": "drizzle-kit studio" - }, - "dependencies": { - "@nestjs/common": "^10.4.15", - "@nestjs/config": "^3.3.0", - "@nestjs/core": "^10.4.15", - "@nestjs/platform-express": "^10.4.15", - "@nestjs/schedule": "^4.1.2", - "drizzle-orm": "^0.38.3", - "nestjs-telegraf": "^2.8.0", - "postgres": "^3.4.5", - "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1", - "telegraf": "^4.16.3" - }, - "devDependencies": { - "@manacore/shared-drizzle-config": "workspace:*", - "@nestjs/cli": "^10.4.9", - "@nestjs/schematics": "^10.2.3", - "@types/node": "^22.10.5", - "drizzle-kit": "^0.30.1", - "rimraf": "^6.0.1", - "typescript": "^5.7.3" - } -} diff --git a/services/telegram-zitare-bot/src/app.module.ts b/services/telegram-zitare-bot/src/app.module.ts deleted file mode 100644 index c85d9f80f..000000000 --- a/services/telegram-zitare-bot/src/app.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule, ConfigService } from '@nestjs/config'; -import { TelegrafModule } from 'nestjs-telegraf'; -import configuration from './config/configuration'; -import { DatabaseModule } from './database/database.module'; -import { BotModule } from './bot/bot.module'; -import { SchedulerModule } from './scheduler/scheduler.module'; -import { HealthController } from './health.controller'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - load: [configuration], - }), - TelegrafModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (configService: ConfigService) => ({ - token: configService.get('telegram.token') || '', - }), - inject: [ConfigService], - }), - DatabaseModule, - BotModule, - SchedulerModule, - ], - controllers: [HealthController], -}) -export class AppModule {} diff --git a/services/telegram-zitare-bot/src/bot/bot.module.ts b/services/telegram-zitare-bot/src/bot/bot.module.ts deleted file mode 100644 index bc407adee..000000000 --- a/services/telegram-zitare-bot/src/bot/bot.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { BotUpdate } from './bot.update'; -import { QuotesModule } from '../quotes/quotes.module'; -import { UserModule } from '../user/user.module'; - -@Module({ - imports: [QuotesModule, UserModule], - providers: [BotUpdate], -}) -export class BotModule {} diff --git a/services/telegram-zitare-bot/src/bot/bot.update.ts b/services/telegram-zitare-bot/src/bot/bot.update.ts deleted file mode 100644 index ad9a46059..000000000 --- a/services/telegram-zitare-bot/src/bot/bot.update.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { Update, Ctx, Start, Help, Command, Message } from 'nestjs-telegraf'; -import { Context } from 'telegraf'; -import { QuotesService } from '../quotes/quotes.service'; -import { UserService } from '../user/user.service'; - -@Update() -export class BotUpdate { - private readonly logger = new Logger(BotUpdate.name); - - // Track last shown quote per user for /favorite command - private lastQuote: Map = new Map(); - - constructor( - private readonly quotesService: QuotesService, - private readonly userService: UserService - ) {} - - private formatHelp(): string { - return `✨ Zitare Bot - -Deine tägliche Dosis Inspiration mit deutschen Zitaten. - -Zitate: -/quote oder /zitat - Zufälliges Zitat -/search [Begriff] - Zitate suchen -/author [Name] - Zitate eines Autors - -Favoriten: -/favorite - Aktuelles Zitat speichern -/favorites - Deine Favoriten anzeigen -/removefav [Nr] - Favorit entfernen - -Täglich: -/daily - Tägliches Zitat an/aus - -Tipp: Starte mit /quote für ein erstes Zitat!`; - } - - @Start() - async start(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - const username = ctx.from?.username; - - if (!userId) return; - - // Ensure user exists in database - await this.userService.ensureUser(userId, username); - - this.logger.log(`/start from user ${userId} (@${username})`); - await ctx.replyWithHTML(this.formatHelp()); - } - - @Help() - async help(@Ctx() ctx: Context) { - await ctx.replyWithHTML(this.formatHelp()); - } - - @Command('quote') - async quote(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - await this.userService.ensureUser(userId, ctx.from?.username); - - const quote = this.quotesService.getRandomQuote(); - this.lastQuote.set(userId, quote.id); - - const formatted = this.quotesService.formatQuote(quote); - await ctx.reply(formatted); - } - - @Command('zitat') - async zitat(@Ctx() ctx: Context) { - await this.quote(ctx); - } - - @Command('search') - async search(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId) return; - - const term = text.replace('/search', '').trim(); - if (!term) { - await ctx.reply('Verwendung: /search [Begriff]\n\nBeispiel: /search Leben'); - return; - } - - const results = this.quotesService.search(term); - - if (results.length === 0) { - await ctx.reply(`Keine Zitate gefunden für "${term}".`); - return; - } - - let response = `🔍 Suchergebnisse für "${term}":\n\n`; - results.forEach((quote, index) => { - response += `${index + 1}. „${quote.text}"\n— ${quote.author.name}\n\n`; - }); - - // Store last quote for /favorite - if (results.length > 0) { - this.lastQuote.set(userId, results[0].id); - } - - await ctx.replyWithHTML(response); - } - - @Command('author') - async author(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId) return; - - const authorName = text.replace('/author', '').trim(); - if (!authorName) { - await ctx.reply('Verwendung: /author [Name]\n\nBeispiel: /author Einstein'); - return; - } - - const results = this.quotesService.getByAuthor(authorName); - - if (results.length === 0) { - await ctx.reply(`Keine Zitate gefunden von "${authorName}".`); - return; - } - - let response = `📚 Zitate von ${results[0].author.name}:\n\n`; - results.forEach((quote, index) => { - response += `${index + 1}. „${quote.text}"\n\n`; - }); - - // Store last quote for /favorite - if (results.length > 0) { - this.lastQuote.set(userId, results[0].id); - } - - await ctx.replyWithHTML(response); - } - - @Command('favorite') - async favorite(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - await this.userService.ensureUser(userId, ctx.from?.username); - - const lastQuoteId = this.lastQuote.get(userId); - if (!lastQuoteId) { - await ctx.reply('Kein aktuelles Zitat zum Speichern.\n\nHole dir erst ein Zitat mit /quote'); - return; - } - - const added = await this.userService.addFavorite(userId, lastQuoteId); - - if (added) { - await ctx.reply('⭐ Zitat zu Favoriten hinzugefügt!'); - } else { - await ctx.reply('ℹ️ Dieses Zitat ist bereits in deinen Favoriten.'); - } - } - - @Command('favorites') - async favorites(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - await this.userService.ensureUser(userId, ctx.from?.username); - - const favoriteIds = await this.userService.getFavoriteQuoteIds(userId); - - if (favoriteIds.length === 0) { - await ctx.reply( - 'Du hast noch keine Favoriten.\n\nSpeichere Zitate mit /favorite nach /quote' - ); - return; - } - - const quotes = this.quotesService.getQuotesByIds(favoriteIds); - - let response = `⭐ Deine Favoriten (${quotes.length}):\n\n`; - quotes.forEach((quote, index) => { - response += `${index + 1}. „${quote.text}"\n— ${quote.author.name}\n\n`; - }); - - response += `\nEntfernen mit /removefav [Nr]`; - - await ctx.replyWithHTML(response); - } - - @Command('removefav') - async removeFavorite(@Ctx() ctx: Context, @Message('text') text: string) { - const userId = ctx.from?.id; - if (!userId) return; - - const indexStr = text.replace('/removefav', '').trim(); - const index = parseInt(indexStr, 10); - - if (!indexStr || isNaN(index) || index < 1) { - await ctx.reply( - 'Verwendung: /removefav [Nr]\n\nZeige deine Favoriten mit /favorites um die Nummer zu sehen.' - ); - return; - } - - const favoriteIds = await this.userService.getFavoriteQuoteIds(userId); - - if (index > favoriteIds.length) { - await ctx.reply(`Ungültige Nummer. Du hast ${favoriteIds.length} Favoriten.`); - return; - } - - const quoteId = favoriteIds[index - 1]; - const removed = await this.userService.removeFavorite(userId, quoteId); - - if (removed) { - await ctx.reply(`✅ Favorit #${index} entfernt.`); - } else { - await ctx.reply('Fehler beim Entfernen.'); - } - } - - @Command('daily') - async daily(@Ctx() ctx: Context) { - const userId = ctx.from?.id; - if (!userId) return; - - await this.userService.ensureUser(userId, ctx.from?.username); - - const newState = await this.userService.toggleDaily(userId); - const settings = await this.userService.getDailySettings(userId); - - if (newState) { - await ctx.replyWithHTML( - `✅ Tägliches Zitat aktiviert!\n\n` + - `Du erhältst jeden Tag um ${settings?.time || '08:00'} Uhr ein inspirierendes Zitat.\n\n` + - `Mit /daily wieder deaktivieren.` - ); - } else { - await ctx.reply('❌ Tägliches Zitat deaktiviert.'); - } - } -} diff --git a/services/telegram-zitare-bot/src/config/configuration.ts b/services/telegram-zitare-bot/src/config/configuration.ts deleted file mode 100644 index 15b3918b3..000000000 --- a/services/telegram-zitare-bot/src/config/configuration.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default () => ({ - port: parseInt(process.env.PORT || '3303', 10), - telegram: { - token: process.env.TELEGRAM_BOT_TOKEN, - }, - database: { - url: process.env.DATABASE_URL || 'postgresql://manacore:devpassword@localhost:5432/zitare_bot', - }, -}); diff --git a/services/telegram-zitare-bot/src/database/database.module.ts b/services/telegram-zitare-bot/src/database/database.module.ts deleted file mode 100644 index 905674bf6..000000000 --- a/services/telegram-zitare-bot/src/database/database.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Module, Global } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from './schema'; - -export const DATABASE_CONNECTION = 'DATABASE_CONNECTION'; - -@Global() -@Module({ - providers: [ - { - provide: DATABASE_CONNECTION, - useFactory: (configService: ConfigService) => { - const connectionString = configService.get('database.url'); - const client = postgres(connectionString!); - return drizzle(client, { schema }); - }, - inject: [ConfigService], - }, - ], - exports: [DATABASE_CONNECTION], -}) -export class DatabaseModule {} diff --git a/services/telegram-zitare-bot/src/database/schema.ts b/services/telegram-zitare-bot/src/database/schema.ts deleted file mode 100644 index afdef2a54..000000000 --- a/services/telegram-zitare-bot/src/database/schema.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { pgTable, uuid, text, timestamp, bigint, boolean, unique } from 'drizzle-orm/pg-core'; -import { relations } from 'drizzle-orm'; - -// Telegram users -export const telegramUsers = pgTable('telegram_users', { - id: uuid('id').primaryKey().defaultRandom(), - telegramUserId: bigint('telegram_user_id', { mode: 'number' }).notNull().unique(), - telegramUsername: text('telegram_username'), - dailyEnabled: boolean('daily_enabled').default(false).notNull(), - dailyTime: text('daily_time').default('08:00').notNull(), - timezone: text('timezone').default('Europe/Berlin').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow().notNull(), -}); - -// User favorites -export const userFavorites = pgTable( - 'user_favorites', - { - id: uuid('id').primaryKey().defaultRandom(), - telegramUserId: bigint('telegram_user_id', { mode: 'number' }).notNull(), - quoteId: text('quote_id').notNull(), - createdAt: timestamp('created_at').defaultNow().notNull(), - }, - (table) => [unique().on(table.telegramUserId, table.quoteId)] -); - -// Relations -export const telegramUsersRelations = relations(telegramUsers, ({ many }) => ({ - favorites: many(userFavorites), -})); - -export const userFavoritesRelations = relations(userFavorites, ({ one }) => ({ - user: one(telegramUsers, { - fields: [userFavorites.telegramUserId], - references: [telegramUsers.telegramUserId], - }), -})); - -// Types -export type TelegramUser = typeof telegramUsers.$inferSelect; -export type NewTelegramUser = typeof telegramUsers.$inferInsert; -export type UserFavorite = typeof userFavorites.$inferSelect; -export type NewUserFavorite = typeof userFavorites.$inferInsert; diff --git a/services/telegram-zitare-bot/src/health.controller.ts b/services/telegram-zitare-bot/src/health.controller.ts deleted file mode 100644 index 74499387e..000000000 --- a/services/telegram-zitare-bot/src/health.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; - -@Controller('health') -export class HealthController { - @Get() - check() { - return { - status: 'ok', - service: 'telegram-zitare-bot', - timestamp: new Date().toISOString(), - }; - } -} diff --git a/services/telegram-zitare-bot/src/main.ts b/services/telegram-zitare-bot/src/main.ts deleted file mode 100644 index d40da01a8..000000000 --- a/services/telegram-zitare-bot/src/main.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { AppModule } from './app.module'; - -async function bootstrap() { - const logger = new Logger('Bootstrap'); - const app = await NestFactory.create(AppModule); - - const configService = app.get(ConfigService); - const port = configService.get('port') || 3303; - - await app.listen(port); - logger.log(`Telegram Zitare Bot running on port ${port}`); - logger.log(`Health check: http://localhost:${port}/health`); -} - -bootstrap(); diff --git a/services/telegram-zitare-bot/src/quotes/data/authors.json b/services/telegram-zitare-bot/src/quotes/data/authors.json deleted file mode 100644 index e7bcc762b..000000000 --- a/services/telegram-zitare-bot/src/quotes/data/authors.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { "id": "a001", "name": "John Lennon", "profession": ["Musiker", "Sänger"] }, - { "id": "a002", "name": "Steve Jobs", "profession": ["Unternehmer", "Visionär"] }, - { "id": "a003", "name": "Albert Einstein", "profession": ["Physiker", "Wissenschaftler"] }, - { "id": "a004", "name": "Mahatma Gandhi", "profession": ["Politiker", "Friedensaktivist"] }, - { - "id": "a005", - "name": "Johann Wolfgang von Goethe", - "profession": ["Dichter", "Schriftsteller"] - }, - { "id": "a006", "name": "Eleanor Roosevelt", "profession": ["Diplomatin", "Aktivistin"] }, - { "id": "a007", "name": "Seneca", "profession": ["Philosoph", "Schriftsteller"] }, - { "id": "a008", "name": "Bertolt Brecht", "profession": ["Dramatiker", "Dichter"] }, - { "id": "a009", "name": "Marc Aurel", "profession": ["Kaiser", "Philosoph"] }, - { "id": "a010", "name": "Pablo Picasso", "profession": ["Maler", "Künstler"] }, - { - "id": "a011", - "name": "Dalai Lama", - "profession": ["Geistlicher Führer", "Friedensnobelpreisträger"] - }, - { "id": "a012", "name": "Konfuzius", "profession": ["Philosoph", "Lehrer"] }, - { "id": "a013", "name": "Winston Churchill", "profession": ["Politiker", "Staatsmann"] }, - { "id": "a014", "name": "Chinesisches Sprichwort" }, - { "id": "a015", "name": "Hermann Hesse", "profession": ["Schriftsteller", "Dichter"] }, - { "id": "a016", "name": "Nelson Mandela", "profession": ["Politiker", "Aktivist"] }, - { "id": "a017", "name": "Henry Ford", "profession": ["Unternehmer", "Industrieller"] }, - { "id": "a018", "name": "Blaise Pascal", "profession": ["Mathematiker", "Philosoph"] }, - { "id": "a019", "name": "Aristoteles", "profession": ["Philosoph"] }, - { "id": "a020", "name": "Muhammad Ali", "profession": ["Boxer", "Aktivist"] }, - { "id": "a021", "name": "T.S. Eliot", "profession": ["Dichter", "Schriftsteller"] }, - { "id": "a022", "name": "André Gide", "profession": ["Schriftsteller"] }, - { "id": "a023", "name": "David Ben-Gurion", "profession": ["Politiker", "Staatsmann"] }, - { "id": "a024", "name": "Abraham Lincoln", "profession": ["Politiker", "Präsident"] }, - { "id": "a025", "name": "Arthur Schopenhauer", "profession": ["Philosoph"] }, - { "id": "a026", "name": "Laozi", "profession": ["Philosoph"] }, - { "id": "a027", "name": "Unbekannt" }, - { "id": "a028", "name": "Oscar Wilde", "profession": ["Schriftsteller", "Dichter"] }, - { "id": "a029", "name": "E. Joseph Cossman", "profession": ["Unternehmer"] }, - { "id": "a030", "name": "Ingmar Bergman", "profession": ["Regisseur", "Autor"] }, - { "id": "a031", "name": "Demokrit", "profession": ["Philosoph"] }, - { "id": "a032", "name": "Indisches Sprichwort" }, - { "id": "a033", "name": "Mark Twain", "profession": ["Schriftsteller"] }, - { "id": "a034", "name": "Deutsches Sprichwort" } -] diff --git a/services/telegram-zitare-bot/src/quotes/data/quotes.json b/services/telegram-zitare-bot/src/quotes/data/quotes.json deleted file mode 100644 index 7db065ea3..000000000 --- a/services/telegram-zitare-bot/src/quotes/data/quotes.json +++ /dev/null @@ -1,166 +0,0 @@ -[ - { - "id": "q001", - "text": "Das Leben ist das, was passiert, während du andere Pläne machst.", - "authorId": "a001" - }, - { - "id": "q002", - "text": "Der einzige Weg, großartige Arbeit zu leisten, ist zu lieben, was man tut.", - "authorId": "a002" - }, - { - "id": "q003", - "text": "In der Mitte der Schwierigkeit liegt die Möglichkeit.", - "authorId": "a003" - }, - { - "id": "q004", - "text": "Sei du selbst die Veränderung, die du dir wünschst für diese Welt.", - "authorId": "a004" - }, - { - "id": "q005", - "text": "Es ist nicht genug zu wissen – man muss auch anwenden. Es ist nicht genug zu wollen – man muss auch tun.", - "authorId": "a005" - }, - { - "id": "q006", - "text": "Die Zukunft gehört denen, die an die Schönheit ihrer Träume glauben.", - "authorId": "a006" - }, - { - "id": "q007", - "text": "Nicht weil es schwer ist, wagen wir es nicht, sondern weil wir es nicht wagen, ist es schwer.", - "authorId": "a007" - }, - { - "id": "q008", - "text": "Wer kämpft, kann verlieren. Wer nicht kämpft, hat schon verloren.", - "authorId": "a008" - }, - { - "id": "q009", - "text": "Das Glück deines Lebens hängt von der Beschaffenheit deiner Gedanken ab.", - "authorId": "a009" - }, - { "id": "q010", "text": "Alles, was du dir vorstellen kannst, ist real.", "authorId": "a010" }, - { - "id": "q011", - "text": "Es gibt nur zwei Tage im Jahr, an denen man nichts tun kann. Der eine ist Gestern, der andere Morgen.", - "authorId": "a011" - }, - { "id": "q012", "text": "Der Weg ist das Ziel.", "authorId": "a012" }, - { - "id": "q013", - "text": "Lerne aus der Vergangenheit, lebe in der Gegenwart, hoffe für die Zukunft.", - "authorId": "a003" - }, - { - "id": "q014", - "text": "Erfolg ist nicht endgültig, Misserfolg ist nicht fatal: Es ist der Mut weiterzumachen, der zählt.", - "authorId": "a013" - }, - { - "id": "q015", - "text": "Die beste Zeit, einen Baum zu pflanzen, war vor zwanzig Jahren. Die zweitbeste Zeit ist jetzt.", - "authorId": "a014" - }, - { - "id": "q016", - "text": "Hab keine Angst, langsam zu gehen. Hab nur Angst, stehen zu bleiben.", - "authorId": "a014" - }, - { - "id": "q017", - "text": "Man muss das Unmögliche versuchen, um das Mögliche zu erreichen.", - "authorId": "a015" - }, - { - "id": "q018", - "text": "Die größte Ehre im Leben liegt nicht darin, niemals zu fallen, sondern jedes Mal wieder aufzustehen.", - "authorId": "a016" - }, - { - "id": "q019", - "text": "Wer immer tut, was er schon kann, bleibt immer das, was er schon ist.", - "authorId": "a017" - }, - { - "id": "q020", - "text": "Phantasie ist wichtiger als Wissen, denn Wissen ist begrenzt.", - "authorId": "a003" - }, - { - "id": "q021", - "text": "Es ist nicht wichtig, wie langsam du gehst, solange du nicht stehen bleibst.", - "authorId": "a012" - }, - { - "id": "q022", - "text": "Der Mensch, der den Berg versetzte, war derselbe, der anfing, kleine Steine wegzutragen.", - "authorId": "a012" - }, - { - "id": "q023", - "text": "Wenn der Wind der Veränderung weht, bauen die einen Mauern und die anderen Windmühlen.", - "authorId": "a014" - }, - { - "id": "q024", - "text": "Ein Tropfen Liebe ist mehr als ein Ozean Verstand.", - "authorId": "a018" - }, - { "id": "q025", "text": "Glück ist kein Zufall, sondern eine Entscheidung.", "authorId": "a019" }, - { - "id": "q026", - "text": "Die Kraft liegt nicht im Körper, sondern im Willen.", - "authorId": "a020" - }, - { "id": "q027", "text": "Jeder Tag ist ein neuer Anfang.", "authorId": "a021" }, - { - "id": "q028", - "text": "Das Geheimnis des Glücks liegt nicht im Besitz, sondern im Geben.", - "authorId": "a022" - }, - { "id": "q029", "text": "Wer nicht an Wunder glaubt, ist kein Realist.", "authorId": "a023" }, - { - "id": "q030", - "text": "Es sind nicht die Jahre in deinem Leben, die zählen. Es ist das Leben in deinen Jahren.", - "authorId": "a024" - }, - { - "id": "q031", - "text": "Das Schicksal mischt die Karten, aber wir spielen.", - "authorId": "a025" - }, - { "id": "q032", "text": "Nur wer sein Ziel kennt, findet den Weg.", "authorId": "a026" }, - { "id": "q033", "text": "Das Leben ist zu kurz für später.", "authorId": "a027" }, - { - "id": "q034", - "text": "Wer nach den Sternen greift, wird nicht im Schlamm stecken bleiben.", - "authorId": "a028" - }, - { - "id": "q035", - "text": "Die beste Brücke zwischen Verzweiflung und Hoffnung ist eine gute Nachtruhe.", - "authorId": "a029" - }, - { - "id": "q036", - "text": "Es gibt keine Grenzen. Weder für Gedanken noch für Gefühle. Es ist die Angst, die immer Grenzen setzt.", - "authorId": "a030" - }, - { "id": "q037", "text": "Mut steht am Anfang des Handelns, Glück am Ende.", "authorId": "a031" }, - { - "id": "q038", - "text": "Das Lächeln, das du aussendest, kehrt zu dir zurück.", - "authorId": "a032" - }, - { - "id": "q039", - "text": "Gib jedem Tag die Chance, der schönste deines Lebens zu werden.", - "authorId": "a033" - }, - { "id": "q040", "text": "Wer wagt, gewinnt.", "authorId": "a034" } -] diff --git a/services/telegram-zitare-bot/src/quotes/quotes.module.ts b/services/telegram-zitare-bot/src/quotes/quotes.module.ts deleted file mode 100644 index 8174627bd..000000000 --- a/services/telegram-zitare-bot/src/quotes/quotes.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { QuotesService } from './quotes.service'; - -@Module({ - providers: [QuotesService], - exports: [QuotesService], -}) -export class QuotesModule {} diff --git a/services/telegram-zitare-bot/src/quotes/quotes.service.ts b/services/telegram-zitare-bot/src/quotes/quotes.service.ts deleted file mode 100644 index f6663f62d..000000000 --- a/services/telegram-zitare-bot/src/quotes/quotes.service.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Quote, Author, QuoteWithAuthor } from './types'; -import quotesJson from './data/quotes.json'; -import authorsJson from './data/authors.json'; - -@Injectable() -export class QuotesService { - private readonly logger = new Logger(QuotesService.name); - private readonly quotes: Quote[]; - private readonly authors: Map; - - constructor() { - this.quotes = quotesJson as Quote[]; - this.authors = new Map((authorsJson as Author[]).map((a) => [a.id, a])); - this.logger.log(`Loaded ${this.quotes.length} quotes and ${this.authors.size} authors`); - } - - private getAuthor(authorId: string): Author { - return this.authors.get(authorId) || { id: authorId, name: 'Unbekannt' }; - } - - private toQuoteWithAuthor(quote: Quote): QuoteWithAuthor { - return { - id: quote.id, - text: quote.text, - author: this.getAuthor(quote.authorId), - }; - } - - getRandomQuote(): QuoteWithAuthor { - const index = Math.floor(Math.random() * this.quotes.length); - return this.toQuoteWithAuthor(this.quotes[index]); - } - - getQuoteById(id: string): QuoteWithAuthor | null { - const quote = this.quotes.find((q) => q.id === id); - return quote ? this.toQuoteWithAuthor(quote) : null; - } - - getQuotesByIds(ids: string[]): QuoteWithAuthor[] { - return ids.map((id) => this.getQuoteById(id)).filter((q): q is QuoteWithAuthor => q !== null); - } - - search(term: string, limit = 5): QuoteWithAuthor[] { - const lowerTerm = term.toLowerCase(); - const results: QuoteWithAuthor[] = []; - - for (const quote of this.quotes) { - if (results.length >= limit) break; - - const author = this.getAuthor(quote.authorId); - if ( - quote.text.toLowerCase().includes(lowerTerm) || - author.name.toLowerCase().includes(lowerTerm) - ) { - results.push(this.toQuoteWithAuthor(quote)); - } - } - - return results; - } - - getByAuthor(authorName: string, limit = 5): QuoteWithAuthor[] { - const lowerName = authorName.toLowerCase(); - const results: QuoteWithAuthor[] = []; - - // Find matching author(s) - const matchingAuthorIds: string[] = []; - for (const author of this.authors.values()) { - if (author.name.toLowerCase().includes(lowerName)) { - matchingAuthorIds.push(author.id); - } - } - - if (matchingAuthorIds.length === 0) { - return []; - } - - // Get quotes from matching authors - for (const quote of this.quotes) { - if (results.length >= limit) break; - - if (matchingAuthorIds.includes(quote.authorId)) { - results.push(this.toQuoteWithAuthor(quote)); - } - } - - return results; - } - - getAllAuthors(): Author[] { - return Array.from(this.authors.values()); - } - - getTotalCount(): number { - return this.quotes.length; - } - - formatQuote(quote: QuoteWithAuthor): string { - const profession = - quote.author.profession && quote.author.profession.length > 0 - ? ` (${quote.author.profession.join(', ')})` - : ''; - return `„${quote.text}"\n\n— ${quote.author.name}${profession}`; - } -} diff --git a/services/telegram-zitare-bot/src/quotes/types.ts b/services/telegram-zitare-bot/src/quotes/types.ts deleted file mode 100644 index fbdb1a455..000000000 --- a/services/telegram-zitare-bot/src/quotes/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface Quote { - id: string; - text: string; - authorId: string; -} - -export interface Author { - id: string; - name: string; - profession?: string[]; -} - -export interface QuoteWithAuthor { - id: string; - text: string; - author: Author; -} diff --git a/services/telegram-zitare-bot/src/scheduler/daily.scheduler.ts b/services/telegram-zitare-bot/src/scheduler/daily.scheduler.ts deleted file mode 100644 index 5a6a0439a..000000000 --- a/services/telegram-zitare-bot/src/scheduler/daily.scheduler.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import { InjectBot } from 'nestjs-telegraf'; -import { Telegraf, Context } from 'telegraf'; -import { QuotesService } from '../quotes/quotes.service'; -import { UserService } from '../user/user.service'; - -@Injectable() -export class DailyScheduler { - private readonly logger = new Logger(DailyScheduler.name); - - constructor( - @InjectBot() private readonly bot: Telegraf, - private readonly quotesService: QuotesService, - private readonly userService: UserService - ) {} - - // Run every day at 8:00 AM Europe/Berlin - @Cron('0 8 * * *', { - timeZone: 'Europe/Berlin', - }) - async sendDailyQuotes() { - this.logger.log('Starting daily quote distribution...'); - - try { - const users = await this.userService.getUsersWithDailyEnabled(); - this.logger.log(`Found ${users.length} users with daily enabled`); - - let sent = 0; - let failed = 0; - - for (const user of users) { - try { - const quote = this.quotesService.getRandomQuote(); - const message = - `☀️ Dein tägliches Zitat:\n\n` + this.quotesService.formatQuote(quote); - - await this.bot.telegram.sendMessage(user.telegramUserId, message, { - parse_mode: 'HTML', - }); - - sent++; - this.logger.debug(`Sent daily quote to user ${user.telegramUserId}`); - } catch (error) { - failed++; - this.logger.warn(`Failed to send daily quote to user ${user.telegramUserId}: ${error}`); - - // If user blocked the bot, disable daily - if ((error as { response?: { error_code?: number } }).response?.error_code === 403) { - this.logger.log(`User ${user.telegramUserId} blocked bot, disabling daily`); - await this.userService.toggleDaily(user.telegramUserId); - } - } - } - - this.logger.log(`Daily quote distribution complete: ${sent} sent, ${failed} failed`); - } catch (error) { - this.logger.error('Daily quote distribution failed:', error); - } - } -} diff --git a/services/telegram-zitare-bot/src/scheduler/scheduler.module.ts b/services/telegram-zitare-bot/src/scheduler/scheduler.module.ts deleted file mode 100644 index 28241407b..000000000 --- a/services/telegram-zitare-bot/src/scheduler/scheduler.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ScheduleModule } from '@nestjs/schedule'; -import { DailyScheduler } from './daily.scheduler'; -import { QuotesModule } from '../quotes/quotes.module'; -import { UserModule } from '../user/user.module'; - -@Module({ - imports: [ScheduleModule.forRoot(), QuotesModule, UserModule], - providers: [DailyScheduler], -}) -export class SchedulerModule {} diff --git a/services/telegram-zitare-bot/src/user/user.module.ts b/services/telegram-zitare-bot/src/user/user.module.ts deleted file mode 100644 index ab6ead25c..000000000 --- a/services/telegram-zitare-bot/src/user/user.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UserService } from './user.service'; - -@Module({ - providers: [UserService], - exports: [UserService], -}) -export class UserModule {} diff --git a/services/telegram-zitare-bot/src/user/user.service.ts b/services/telegram-zitare-bot/src/user/user.service.ts deleted file mode 100644 index 29efe8157..000000000 --- a/services/telegram-zitare-bot/src/user/user.service.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { eq, and } from 'drizzle-orm'; -import { PostgresJsDatabase } from 'drizzle-orm/postgres-js'; -import { DATABASE_CONNECTION } from '../database/database.module'; -import * as schema from '../database/schema'; -import { TelegramUser, UserFavorite } from '../database/schema'; - -@Injectable() -export class UserService { - private readonly logger = new Logger(UserService.name); - - constructor( - @Inject(DATABASE_CONNECTION) - private readonly db: PostgresJsDatabase - ) {} - - async ensureUser(telegramUserId: number, username?: string): Promise { - // Try to find existing user - const existing = await this.db.query.telegramUsers.findFirst({ - where: eq(schema.telegramUsers.telegramUserId, telegramUserId), - }); - - if (existing) { - // Update username if changed - if (username && existing.telegramUsername !== username) { - await this.db - .update(schema.telegramUsers) - .set({ telegramUsername: username, updatedAt: new Date() }) - .where(eq(schema.telegramUsers.id, existing.id)); - } - return existing; - } - - // Create new user - const [newUser] = await this.db - .insert(schema.telegramUsers) - .values({ - telegramUserId, - telegramUsername: username, - }) - .returning(); - - this.logger.log(`Created new user: ${telegramUserId} (@${username})`); - return newUser; - } - - async addFavorite(telegramUserId: number, quoteId: string): Promise { - try { - await this.db.insert(schema.userFavorites).values({ - telegramUserId, - quoteId, - }); - return true; - } catch (error) { - // Unique constraint violation = already favorited - if ((error as { code?: string }).code === '23505') { - return false; - } - throw error; - } - } - - async removeFavorite(telegramUserId: number, quoteId: string): Promise { - const result = await this.db - .delete(schema.userFavorites) - .where( - and( - eq(schema.userFavorites.telegramUserId, telegramUserId), - eq(schema.userFavorites.quoteId, quoteId) - ) - ) - .returning(); - - return result.length > 0; - } - - async getFavorites(telegramUserId: number): Promise { - return this.db.query.userFavorites.findMany({ - where: eq(schema.userFavorites.telegramUserId, telegramUserId), - orderBy: (favorites, { desc }) => [desc(favorites.createdAt)], - }); - } - - async getFavoriteQuoteIds(telegramUserId: number): Promise { - const favorites = await this.getFavorites(telegramUserId); - return favorites.map((f) => f.quoteId); - } - - async isFavorite(telegramUserId: number, quoteId: string): Promise { - const favorite = await this.db.query.userFavorites.findFirst({ - where: and( - eq(schema.userFavorites.telegramUserId, telegramUserId), - eq(schema.userFavorites.quoteId, quoteId) - ), - }); - return !!favorite; - } - - async toggleDaily(telegramUserId: number): Promise { - const user = await this.db.query.telegramUsers.findFirst({ - where: eq(schema.telegramUsers.telegramUserId, telegramUserId), - }); - - if (!user) { - return false; - } - - const newValue = !user.dailyEnabled; - await this.db - .update(schema.telegramUsers) - .set({ dailyEnabled: newValue, updatedAt: new Date() }) - .where(eq(schema.telegramUsers.id, user.id)); - - return newValue; - } - - async setDailyTime(telegramUserId: number, time: string): Promise { - await this.db - .update(schema.telegramUsers) - .set({ dailyTime: time, updatedAt: new Date() }) - .where(eq(schema.telegramUsers.telegramUserId, telegramUserId)); - } - - async getUsersWithDailyEnabled(): Promise { - return this.db.query.telegramUsers.findMany({ - where: eq(schema.telegramUsers.dailyEnabled, true), - }); - } - - async getDailySettings( - telegramUserId: number - ): Promise<{ enabled: boolean; time: string } | null> { - const user = await this.db.query.telegramUsers.findFirst({ - where: eq(schema.telegramUsers.telegramUserId, telegramUserId), - }); - - if (!user) { - return null; - } - - return { - enabled: user.dailyEnabled, - time: user.dailyTime, - }; - } -} diff --git a/services/telegram-zitare-bot/tsconfig.json b/services/telegram-zitare-bot/tsconfig.json deleted file mode 100644 index c705ffcb3..000000000 --- a/services/telegram-zitare-bot/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "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, - "resolveJsonModule": true - } -}