From 7c20d886494325cbda01943becb79e67623449b4 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Tue, 27 Jan 2026 03:29:08 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(services):=20add=20telegram-pr?= =?UTF-8?q?oject-doc-bot=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add new NestJS-based Telegram bot for project documentation with: - Drizzle ORM for database access - OpenAI integration for AI features - S3 storage support via AWS SDK - Monorepo integration (dev scripts, database setup, MinIO bucket) Co-Authored-By: Claude Opus 4.5 --- docker-compose.dev.yml | 1 + package.json | 4 + pnpm-lock.yaml | 803 ++++++++++++++---- scripts/setup-databases.sh | 5 + .../telegram-project-doc-bot/.env.example | 24 + 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 | 53 ++ .../src/database/database.module.ts | 24 + .../src/database/schema.ts | 85 ++ .../src/generation/generation.module.ts | 8 + .../src/generation/generation.service.ts | 202 +++++ .../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 | 49 ++ .../telegram-project-doc-bot/tsconfig.json | 22 + 28 files changed, 2365 insertions(+), 180 deletions(-) create mode 100644 services/telegram-project-doc-bot/.env.example create mode 100644 services/telegram-project-doc-bot/CLAUDE.md create mode 100644 services/telegram-project-doc-bot/Dockerfile create mode 100644 services/telegram-project-doc-bot/drizzle.config.ts create mode 100644 services/telegram-project-doc-bot/nest-cli.json create mode 100644 services/telegram-project-doc-bot/package.json create mode 100644 services/telegram-project-doc-bot/src/app.module.ts create mode 100644 services/telegram-project-doc-bot/src/bot/bot.module.ts create mode 100644 services/telegram-project-doc-bot/src/bot/bot.update.ts create mode 100644 services/telegram-project-doc-bot/src/config/configuration.ts create mode 100644 services/telegram-project-doc-bot/src/database/database.module.ts create mode 100644 services/telegram-project-doc-bot/src/database/schema.ts create mode 100644 services/telegram-project-doc-bot/src/generation/generation.module.ts create mode 100644 services/telegram-project-doc-bot/src/generation/generation.service.ts create mode 100644 services/telegram-project-doc-bot/src/health.controller.ts create mode 100644 services/telegram-project-doc-bot/src/main.ts create mode 100644 services/telegram-project-doc-bot/src/media/media.module.ts create mode 100644 services/telegram-project-doc-bot/src/media/media.service.ts create mode 100644 services/telegram-project-doc-bot/src/media/storage.service.ts create mode 100644 services/telegram-project-doc-bot/src/project/project.module.ts create mode 100644 services/telegram-project-doc-bot/src/project/project.service.ts create mode 100644 services/telegram-project-doc-bot/src/transcription/transcription.module.ts create mode 100644 services/telegram-project-doc-bot/src/transcription/transcription.service.ts create mode 100644 services/telegram-project-doc-bot/tsconfig.json diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c9753311a..ab76cc4d9 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -86,6 +86,7 @@ services: mc mb --ignore-existing myminio/storage-storage; mc mb --ignore-existing myminio/inventory-storage; mc mb --ignore-existing myminio/planta-storage; + mc mb --ignore-existing myminio/projectdoc-storage; mc anonymous set download myminio/picture-storage; mc anonymous set download myminio/planta-storage; mc anonymous set download myminio/inventory-storage; diff --git a/package.json b/package.json index c7b97cace..34794df17 100644 --- a/package.json +++ b/package.json @@ -208,6 +208,10 @@ "cf:login": "npx wrangler login", "cf:projects:list": "npx wrangler pages project list", "cf:projects:create": "echo 'Creating Cloudflare Pages projects...' && npx wrangler pages project create chat-landing --production-branch=main && npx wrangler pages project create picture-landing --production-branch=main && npx wrangler pages project create manacore-landing --production-branch=main && npx wrangler pages project create manadeck-landing --production-branch=main && npx wrangler pages project create zitare-landing --production-branch=main", + "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", "prepare": "husky" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 533487e02..f36166cb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,7 +119,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) + 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) @@ -152,7 +152,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) + 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) @@ -176,14 +176,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.8.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.8.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.8.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.8.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)) @@ -192,13 +192,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 @@ -558,19 +558,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 @@ -4539,7 +4539,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^11.0.0 - version: 11.0.12(@types/node@22.19.1)(esbuild@0.19.12) + version: 11.0.12(@types/node@22.19.1) '@nestjs/schematics': specifier: ^11.0.0 version: 11.0.9(chokidar@4.0.3)(typescript@5.9.3) @@ -4596,10 +4596,10 @@ 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))(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) + 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) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -4656,6 +4656,67 @@ importers: 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)(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)(typescript@5.9.3) + openai: + specifier: ^4.77.0 + version: 4.104.0(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 + 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': @@ -7018,7 +7079,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==} @@ -20312,6 +20373,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.8.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.8.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.8.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.8.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) @@ -23357,7 +23428,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(dux2nvtiztnejw7mxzfaajqvh4) + expo-router: 6.0.15(nttrd3tw67nnyhowcwgdzipb5e) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -24696,6 +24767,43 @@ snapshots: - supports-color - ts-node + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.2.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-resolve-dependencies: 30.2.0 + jest-runner: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + jest-watcher: 30.2.0 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': dependencies: '@jest/console': 30.2.0 @@ -25120,6 +25228,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) @@ -25146,7 +25280,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@11.0.12(@types/node@22.19.1)(esbuild@0.19.12)': + '@nestjs/cli@11.0.12(@types/node@22.19.1)': dependencies: '@angular-devkit/core': 19.2.19(chokidar@4.0.3) '@angular-devkit/schematics': 19.2.19(chokidar@4.0.3) @@ -25157,14 +25291,14 @@ snapshots: chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.100.2) glob: 12.0.0 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.100.2(esbuild@0.19.12) + webpack: 5.100.2 webpack-node-externals: 3.0.0 transitivePeerDependencies: - '@types/node' @@ -28528,6 +28662,19 @@ snapshots: jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) optional: true + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(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: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(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(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 @@ -29044,16 +29191,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 @@ -29102,15 +29249,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 @@ -29202,14 +29349,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: @@ -29241,14 +29388,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: @@ -29374,12 +29521,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 @@ -29410,12 +29557,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 @@ -29597,15 +29744,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 @@ -29636,13 +29783,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 @@ -30433,6 +30580,108 @@ snapshots: transitivePeerDependencies: - supports-color + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.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.8.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.8.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 @@ -32685,6 +32934,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) @@ -32695,9 +32949,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 @@ -32712,9 +32966,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 @@ -32732,14 +32986,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) @@ -32764,17 +33018,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: @@ -32812,7 +33066,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 @@ -32823,7 +33077,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 @@ -32837,12 +33106,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 @@ -32857,25 +33126,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 @@ -32899,12 +33182,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) @@ -32958,7 +33235,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 @@ -32967,9 +33244,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 @@ -32981,7 +33258,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 @@ -33016,7 +33293,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 @@ -33027,7 +33304,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 @@ -33045,7 +33322,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 @@ -33056,7 +33333,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 @@ -33084,16 +33361,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) @@ -33124,16 +33391,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) @@ -33158,10 +33415,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) @@ -33192,28 +33445,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 @@ -34467,6 +34698,53 @@ snapshots: - supports-color optional: true + expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e): + 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@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(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(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(qjp3usx4acoq47dkosl6pmu254): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -35395,6 +35673,23 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + 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 @@ -35429,23 +35724,6 @@ snapshots: typescript: 5.7.2 webpack: 5.97.1 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 4.0.3 - cosmiconfig: 8.3.6(typescript@5.9.3) - 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.9.3 - webpack: 5.100.2(esbuild@0.19.12) - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -35463,6 +35741,23 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.100.2): + dependencies: + '@babel/code-frame': 7.27.1 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + 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.9.3 + webpack: 5.100.2 + form-data-encoder@1.7.2: {} form-data@3.0.4: @@ -36665,6 +36960,26 @@ snapshots: - ts-node optional: true + jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-util: 30.2.0 + jest-validate: 30.2.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) @@ -36850,6 +37165,41 @@ snapshots: - supports-color optional: true + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + esbuild-register: 3.6.0(esbuild@0.19.12) + ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@babel/core': 7.28.5 @@ -37424,6 +37774,20 @@ snapshots: - ts-node optional: true + jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/types': 30.2.0 + import-local: 3.2.0 + jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) @@ -41376,6 +41740,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 @@ -42653,14 +43027,14 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)): + 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.100.2(esbuild@0.19.12) + webpack: 5.97.1(esbuild@0.19.12) optionalDependencies: esbuild: 0.19.12 @@ -42686,6 +43060,15 @@ snapshots: optionalDependencies: esbuild: 0.27.0 + terser-webpack-plugin@5.3.14(webpack@5.100.2): + 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.100.2 + terser-webpack-plugin@5.3.14(webpack@5.97.1): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -42832,27 +43215,6 @@ 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 @@ -42895,15 +43257,25 @@ snapshots: esbuild: 0.27.0 jest-util: 30.2.0 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.19.12)): + 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: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 + 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 - source-map: 0.7.6 + type-fest: 4.41.0 typescript: 5.9.3 - webpack: 5.100.2(esbuild@0.19.12) + 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: @@ -42915,6 +43287,26 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2(esbuild@0.27.0) + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.100.2): + 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.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 @@ -43512,6 +43904,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 @@ -43614,6 +44023,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) @@ -43864,7 +44277,7 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.100.2(esbuild@0.19.12): + webpack@5.100.2: dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.8 @@ -43888,7 +44301,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.100.2(esbuild@0.19.12)) + terser-webpack-plugin: 5.3.14(webpack@5.100.2) watchpack: 2.4.4 webpack-sources: 3.3.3 transitivePeerDependencies: @@ -43958,6 +44371,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/scripts/setup-databases.sh b/scripts/setup-databases.sh index 01f31f6b9..6afe08cb8 100755 --- a/scripts/setup-databases.sh +++ b/scripts/setup-databases.sh @@ -74,6 +74,7 @@ ALL_DATABASES=( "figgos" "planta" "nutriphi" + "projectdoc" ) # Check if specific service requested @@ -155,6 +156,10 @@ setup_service() { create_db_if_not_exists "storage" push_schema "@storage/backend" "storage" ;; + projectdoc) + create_db_if_not_exists "projectdoc" + push_schema "@manacore/telegram-project-doc-bot" "projectdoc" + ;; *) echo -e "${RED}Unknown service: $service${NC}" echo "Available services: auth, chat, zitare, contacts, calendar, clock, todo, manadeck, mail, moodlit, finance, voxel-lava, figgos, planta, nutriphi, presi, storage" diff --git a/services/telegram-project-doc-bot/.env.example b/services/telegram-project-doc-bot/.env.example new file mode 100644 index 000000000..6a57b14a2 --- /dev/null +++ b/services/telegram-project-doc-bot/.env.example @@ -0,0 +1,24 @@ +# 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 (OpenAI Whisper) +OPENAI_API_KEY=sk-your-openai-key + +# AI - Generation +LLM_PROVIDER=ollama # ollama | openai +OLLAMA_URL=http://localhost:11434 +OLLAMA_MODEL=gemma3:4b diff --git a/services/telegram-project-doc-bot/CLAUDE.md b/services/telegram-project-doc-bot/CLAUDE.md new file mode 100644 index 000000000..60c57331c --- /dev/null +++ b/services/telegram-project-doc-bot/CLAUDE.md @@ -0,0 +1,245 @@ +# 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**: Ollama (lokal) 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=ollama # ollama oder openai +OLLAMA_URL=http://localhost:11434 +OLLAMA_MODEL=gemma3:4b +``` + +## 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 new file mode 100644 index 000000000..fade07891 --- /dev/null +++ b/services/telegram-project-doc-bot/Dockerfile @@ -0,0 +1,41 @@ +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 new file mode 100644 index 000000000..194347802 --- /dev/null +++ b/services/telegram-project-doc-bot/drizzle.config.ts @@ -0,0 +1,10 @@ +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 new file mode 100644 index 000000000..95538fb90 --- /dev/null +++ b/services/telegram-project-doc-bot/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$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 new file mode 100644 index 000000000..5022fb3d1 --- /dev/null +++ b/services/telegram-project-doc-bot/package.json @@ -0,0 +1,44 @@ +{ + "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 new file mode 100644 index 000000000..58c1dc80e --- /dev/null +++ b/services/telegram-project-doc-bot/src/app.module.ts @@ -0,0 +1,27 @@ +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 new file mode 100644 index 000000000..1541be516 --- /dev/null +++ b/services/telegram-project-doc-bot/src/bot/bot.module.ts @@ -0,0 +1,11 @@ +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 new file mode 100644 index 000000000..b8abf4c97 --- /dev/null +++ b/services/telegram-project-doc-bot/src/bot/bot.update.ts @@ -0,0 +1,490 @@ +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 new file mode 100644 index 000000000..2ee99c866 --- /dev/null +++ b/services/telegram-project-doc-bot/src/config/configuration.ts @@ -0,0 +1,53 @@ +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, + }, + llm: { + provider: process.env.LLM_PROVIDER || 'ollama', + ollama: { + url: process.env.OLLAMA_URL || 'http://localhost:11434', + model: process.env.OLLAMA_MODEL || '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 new file mode 100644 index 000000000..905674bf6 --- /dev/null +++ b/services/telegram-project-doc-bot/src/database/database.module.ts @@ -0,0 +1,24 @@ +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 new file mode 100644 index 000000000..10c2fb2db --- /dev/null +++ b/services/telegram-project-doc-bot/src/database/schema.ts @@ -0,0 +1,85 @@ +import { pgTable, uuid, text, timestamp, integer, 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: integer('telegram_user_id').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 new file mode 100644 index 000000000..fccb8adfd --- /dev/null +++ b/services/telegram-project-doc-bot/src/generation/generation.module.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000..3cd293b0e --- /dev/null +++ b/services/telegram-project-doc-bot/src/generation/generation.service.ts @@ -0,0 +1,202 @@ +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 ollamaUrl: string; + private readonly ollamaModel: string; + private readonly openai: OpenAI | null; + + constructor( + @Inject(DATABASE_CONNECTION) + private db: PostgresJsDatabase, + private configService: ConfigService + ) { + this.llmProvider = this.configService.get('llm.provider') || 'ollama'; + this.ollamaUrl = this.configService.get('llm.ollama.url') || 'http://localhost:11434'; + this.ollamaModel = this.configService.get('llm.ollama.model') || '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.callOllama(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 callOllama(prompt: string): Promise { + const response = await fetch(`${this.ollamaUrl}/api/generate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: this.ollamaModel, + prompt, + stream: false, + }), + signal: AbortSignal.timeout(180000), // 3 minutes timeout + }); + + if (!response.ok) { + throw new Error(`Ollama API error: ${response.status}`); + } + + const data = await response.json(); + return data.response || ''; + } + + 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 new file mode 100644 index 000000000..59bc025e7 --- /dev/null +++ b/services/telegram-project-doc-bot/src/health.controller.ts @@ -0,0 +1,13 @@ +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 new file mode 100644 index 000000000..559bf11d4 --- /dev/null +++ b/services/telegram-project-doc-bot/src/main.ts @@ -0,0 +1,18 @@ +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 new file mode 100644 index 000000000..7d62a4e77 --- /dev/null +++ b/services/telegram-project-doc-bot/src/media/media.module.ts @@ -0,0 +1,11 @@ +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 new file mode 100644 index 000000000..1b4c964c1 --- /dev/null +++ b/services/telegram-project-doc-bot/src/media/media.service.ts @@ -0,0 +1,164 @@ +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 new file mode 100644 index 000000000..5280d6fd1 --- /dev/null +++ b/services/telegram-project-doc-bot/src/media/storage.service.ts @@ -0,0 +1,77 @@ +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 new file mode 100644 index 000000000..c1b3f70d8 --- /dev/null +++ b/services/telegram-project-doc-bot/src/project/project.module.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000..e82ec9ccf --- /dev/null +++ b/services/telegram-project-doc-bot/src/project/project.service.ts @@ -0,0 +1,90 @@ +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 new file mode 100644 index 000000000..fb5aeeaf1 --- /dev/null +++ b/services/telegram-project-doc-bot/src/transcription/transcription.module.ts @@ -0,0 +1,8 @@ +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 new file mode 100644 index 000000000..631a7b84a --- /dev/null +++ b/services/telegram-project-doc-bot/src/transcription/transcription.service.ts @@ -0,0 +1,49 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import OpenAI from 'openai'; + +@Injectable() +export class TranscriptionService { + private readonly logger = new Logger(TranscriptionService.name); + private readonly openai: OpenAI | null; + + constructor(private configService: ConfigService) { + const apiKey = this.configService.get('openai.apiKey'); + + if (apiKey) { + this.openai = new OpenAI({ apiKey }); + this.logger.log('OpenAI Whisper initialized'); + } else { + this.openai = null; + this.logger.warn('OpenAI API key not configured - transcription disabled'); + } + } + + async transcribe(audioBuffer: Buffer, filename = 'audio.ogg'): Promise { + if (!this.openai) { + throw new Error('Transcription not available - OpenAI API key not configured'); + } + + try { + // Create a File object from the buffer using Uint8Array + 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', // Default to German, could be made configurable + }); + + this.logger.debug(`Transcribed ${audioBuffer.length} bytes -> ${response.text.length} chars`); + return response.text; + } catch (error) { + this.logger.error('Transcription failed:', error); + throw new Error('Transkription fehlgeschlagen'); + } + } + + isAvailable(): boolean { + return this.openai !== null; + } +} diff --git a/services/telegram-project-doc-bot/tsconfig.json b/services/telegram-project-doc-bot/tsconfig.json new file mode 100644 index 000000000..94f1e9493 --- /dev/null +++ b/services/telegram-project-doc-bot/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true + } +}