From 8950692cfd5ce3b81f0521f7c72bf20d606a5570 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:15:08 +0100 Subject: [PATCH] feat(matrix-picture-bot): add Matrix bot for AI image generation - Generate images via Picture backend with `!generate` command - Support prompt options (--width, --height, --steps, --negative) - Model selection with `!models` and `!model [id]` - Image history and deletion - Login/logout via mana-core-auth - Credit balance display Co-Authored-By: Claude Opus 4.5 --- pnpm-lock.yaml | 411 +++++++---- services/matrix-picture-bot/.env.example | 15 + services/matrix-picture-bot/.gitignore | 29 + services/matrix-picture-bot/CLAUDE.md | 175 +++++ services/matrix-picture-bot/Dockerfile | 41 ++ services/matrix-picture-bot/nest-cli.json | 8 + services/matrix-picture-bot/package.json | 39 ++ services/matrix-picture-bot/src/app.module.ts | 17 + .../matrix-picture-bot/src/bot/bot.module.ts | 11 + .../src/bot/matrix.service.ts | 648 ++++++++++++++++++ .../src/config/configuration.ts | 69 ++ .../src/health.controller.ts | 13 + services/matrix-picture-bot/src/main.ts | 17 + .../src/picture/picture.module.ts | 8 + .../src/picture/picture.service.ts | 206 ++++++ .../src/session/session.module.ts | 8 + .../src/session/session.service.ts | 100 +++ services/matrix-picture-bot/tsconfig.json | 23 + 18 files changed, 1712 insertions(+), 126 deletions(-) create mode 100644 services/matrix-picture-bot/.env.example create mode 100644 services/matrix-picture-bot/.gitignore create mode 100644 services/matrix-picture-bot/CLAUDE.md create mode 100644 services/matrix-picture-bot/Dockerfile create mode 100644 services/matrix-picture-bot/nest-cli.json create mode 100644 services/matrix-picture-bot/package.json create mode 100644 services/matrix-picture-bot/src/app.module.ts create mode 100644 services/matrix-picture-bot/src/bot/bot.module.ts create mode 100644 services/matrix-picture-bot/src/bot/matrix.service.ts create mode 100644 services/matrix-picture-bot/src/config/configuration.ts create mode 100644 services/matrix-picture-bot/src/health.controller.ts create mode 100644 services/matrix-picture-bot/src/main.ts create mode 100644 services/matrix-picture-bot/src/picture/picture.module.ts create mode 100644 services/matrix-picture-bot/src/picture/picture.service.ts create mode 100644 services/matrix-picture-bot/src/session/session.module.ts create mode 100644 services/matrix-picture-bot/src/session/session.service.ts create mode 100644 services/matrix-picture-bot/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99761d7a8..eb59997cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,7 +137,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9 + version: 10.4.9(esbuild@0.19.12) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -188,7 +188,7 @@ importers: version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2) + version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -212,14 +212,14 @@ importers: version: link:../../../../packages/shared-landing-ui astro: specifier: ^5.16.0 - version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.9.2 version: 5.9.3 devDependencies: '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.18 version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) @@ -228,13 +228,13 @@ importers: version: 20.19.25 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) + version: 9.1.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-astro: specifier: ^1.0.0 - version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) + version: 1.5.0(eslint@9.39.1(jiti@1.21.7)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -606,19 +606,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 @@ -5621,7 +5621,7 @@ importers: version: link:../../packages/shared-drizzle-config '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9 '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -5911,6 +5911,40 @@ importers: specifier: ^5.7.3 version: 5.9.3 + services/matrix-picture-bot: + dependencies: + '@nestjs/common': + specifier: ^10.4.15 + version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^3.3.0 + version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^10.4.15 + version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^10.4.15 + version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + matrix-bot-sdk: + specifier: ^0.7.1 + version: 0.7.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + devDependencies: + '@nestjs/cli': + specifier: ^10.4.9 + version: 10.4.9 + '@types/node': + specifier: ^22.10.2 + version: 22.19.1 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + services/matrix-project-doc-bot: dependencies: '@aws-sdk/client-s3': @@ -8809,7 +8843,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==} @@ -23287,6 +23321,16 @@ snapshots: transitivePeerDependencies: - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': + dependencies: + astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + autoprefixer: 10.4.22(postcss@8.5.6) + postcss: 8.5.6 + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -32458,16 +32502,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 @@ -32516,15 +32560,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 @@ -32616,14 +32660,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: @@ -32655,14 +32699,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: @@ -32788,12 +32832,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 @@ -32824,12 +32868,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 @@ -33011,15 +33055,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 @@ -33050,13 +33094,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 @@ -33955,6 +33999,108 @@ snapshots: transitivePeerDependencies: - supports-color + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + '@astrojs/compiler': 2.13.0 + '@astrojs/internal-helpers': 0.7.5 + '@astrojs/markdown-remark': 6.3.9 + '@astrojs/telemetry': 3.3.0 + '@capsizecss/unpack': 3.0.1 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.3.1 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 1.1.0 + cssesc: 3.0.0 + debug: 4.4.3 + deterministic-object-hash: 2.0.2 + devalue: 5.5.0 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.25.12 + estree-walker: 3.0.3 + flattie: 1.1.1 + fontace: 0.3.1 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + import-meta-resolve: 4.2.0 + js-yaml: 4.1.1 + magic-string: 0.30.21 + magicast: 0.5.1 + mrmime: 2.0.1 + neotraverse: 0.6.18 + p-limit: 6.2.0 + p-queue: 8.1.1 + package-manager-detector: 1.5.0 + piccolore: 0.1.3 + picomatch: 4.0.3 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.7.3 + shiki: 3.15.0 + smol-toml: 1.5.2 + svgo: 4.0.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tsconfck: 3.1.6(typescript@5.9.3) + ultrahtml: 1.6.0 + unifont: 0.6.0 + unist-util-visit: 5.0.0 + unstorage: 1.17.3(@netlify/blobs@10.4.1)(ioredis@5.9.2) + vfile: 6.0.3 + vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.3 + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 @@ -36351,6 +36497,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) @@ -36361,9 +36512,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 @@ -36378,9 +36529,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 @@ -36398,14 +36549,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) @@ -36430,17 +36581,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: @@ -36478,7 +36629,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 @@ -36489,7 +36640,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 @@ -36503,12 +36669,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 @@ -36523,25 +36689,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 @@ -36565,12 +36745,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) @@ -36624,7 +36798,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 @@ -36633,9 +36807,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 @@ -36647,7 +36821,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 @@ -36682,7 +36856,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 @@ -36693,7 +36867,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 @@ -36711,7 +36885,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 @@ -36722,7 +36896,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 @@ -36750,16 +36924,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) @@ -36790,16 +36954,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) @@ -36824,10 +36978,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) @@ -36858,28 +37008,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 @@ -47608,6 +47736,16 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 + semver: 7.7.3 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.97.1(esbuild@0.19.12) + ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -48288,6 +48426,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 @@ -48391,6 +48546,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) diff --git a/services/matrix-picture-bot/.env.example b/services/matrix-picture-bot/.env.example new file mode 100644 index 000000000..3b96e8301 --- /dev/null +++ b/services/matrix-picture-bot/.env.example @@ -0,0 +1,15 @@ +# Server +PORT=3319 + +# Matrix +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN=syt_xxx +MATRIX_ALLOWED_ROOMS=#picture:matrix.mana.how +MATRIX_STORAGE_PATH=./data/bot-storage.json + +# Picture Backend +PICTURE_BACKEND_URL=http://localhost:3006 +PICTURE_API_PREFIX=/api/v1 + +# Mana Core Auth +MANA_CORE_AUTH_URL=http://localhost:3001 diff --git a/services/matrix-picture-bot/.gitignore b/services/matrix-picture-bot/.gitignore new file mode 100644 index 000000000..2d508e5ec --- /dev/null +++ b/services/matrix-picture-bot/.gitignore @@ -0,0 +1,29 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment +.env +.env.local + +# Data +data/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# TypeScript +*.tsbuildinfo diff --git a/services/matrix-picture-bot/CLAUDE.md b/services/matrix-picture-bot/CLAUDE.md new file mode 100644 index 000000000..27772902e --- /dev/null +++ b/services/matrix-picture-bot/CLAUDE.md @@ -0,0 +1,175 @@ +# Matrix Picture Bot - Claude Code Guidelines + +## Overview + +Matrix Picture Bot provides AI image generation via Matrix chat. It integrates with the Picture backend to generate images using various AI models (Replicate). + +## Tech Stack + +- **Framework**: NestJS 10 +- **Matrix**: matrix-bot-sdk +- **Backend**: Picture API (port 3006) +- **Auth**: Mana Core Auth (JWT) + +## Commands + +```bash +# Development +pnpm install +pnpm start:dev # Start with hot reload + +# Build +pnpm build # Production build + +# Type check +pnpm type-check # Check TypeScript types +``` + +## Project Structure + +``` +services/matrix-picture-bot/ +├── src/ +│ ├── main.ts # Application entry point (port 3319) +│ ├── app.module.ts # Root module +│ ├── health.controller.ts # Health check endpoint +│ ├── config/ +│ │ └── configuration.ts # Configuration & help messages +│ ├── bot/ +│ │ ├── bot.module.ts +│ │ └── matrix.service.ts # Matrix client & command handlers +│ ├── picture/ +│ │ ├── picture.module.ts +│ │ └── picture.service.ts # Picture Backend API client +│ └── session/ +│ ├── session.module.ts +│ └── session.service.ts # User session & auth management +├── Dockerfile +└── package.json +``` + +## Bot Commands + +| Command | Aliases | Description | +|---------|---------|-------------| +| `!help` | hilfe | Show help message | +| `!generate [prompt]` | bild, gen | Generate an image | +| `!models` | modelle | List available models | +| `!model [id]` | modell | Switch model | +| `!history` | verlauf | Show recent images | +| `!delete [nr]` | loeschen | Delete an image | +| `!credits` | guthaben | Show credit balance | +| `!login email pass` | - | Login to Picture | +| `!logout` | - | Logout | +| `!cancel` | abbrechen | Cancel active generation | +| `!status` | - | Bot status | + +## Prompt Options + +Options can be added to the generate command: + +``` +!generate A beautiful sunset --width 1280 --height 720 --steps 40 +!bild Ein Hund --negative blurry, low quality --style photorealistic +``` + +| Option | Description | Default | +|--------|-------------|---------| +| `--width N` | Image width | 1024 | +| `--height N` | Image height | 1024 | +| `--steps N` | Generation steps | 25 | +| `--negative [text]` | Negative prompt | - | +| `--style [name]` | Style preset | - | + +## Environment Variables + +```env +# Server +PORT=3319 + +# Matrix +MATRIX_HOMESERVER_URL=http://localhost:8008 +MATRIX_ACCESS_TOKEN=syt_xxx +MATRIX_ALLOWED_ROOMS=#picture:matrix.mana.how +MATRIX_STORAGE_PATH=./data/bot-storage.json + +# Picture Backend +PICTURE_BACKEND_URL=http://localhost:3006 +PICTURE_API_PREFIX=/api/v1 + +# Mana Core Auth +MANA_CORE_AUTH_URL=http://localhost:3001 +``` + +## Docker + +```bash +# Build locally +docker build -f services/matrix-picture-bot/Dockerfile -t matrix-picture-bot services/matrix-picture-bot + +# Run +docker run -p 3319:3319 \ + -e MATRIX_HOMESERVER_URL=http://synapse:8008 \ + -e MATRIX_ACCESS_TOKEN=syt_xxx \ + -e PICTURE_BACKEND_URL=http://picture-backend:3006 \ + -e MANA_CORE_AUTH_URL=http://mana-core-auth:3001 \ + -v matrix-picture-bot-data:/app/data \ + matrix-picture-bot +``` + +## Health Check + +```bash +curl http://localhost:3319/health +``` + +## Getting a Matrix Access Token + +```bash +# Create bot user first, then login +curl -X POST "https://matrix.mana.how/_matrix/client/v3/login" \ + -H "Content-Type: application/json" \ + -d '{ + "type": "m.login.password", + "user": "picture-bot", + "password": "your-password" + }' + +# Response contains: {"access_token": "syt_xxx", ...} +``` + +## Authentication Flow + +1. User sends `!login email password` +2. Bot calls mana-core-auth `/api/v1/auth/login` +3. JWT token stored in session (in-memory) +4. Token used for all Picture API calls +5. Token expires after 7 days (re-login required) + +## Picture Backend API Endpoints Used + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check | +| `/api/v1/models` | GET | List available models | +| `/api/v1/models/:id` | GET | Get model details | +| `/api/v1/generate` | POST | Generate image | +| `/api/v1/images` | GET | List user's images | +| `/api/v1/images/:id` | DELETE | Delete image | +| `/api/v1/credits/balance` | GET | Get credit balance | + +## Credit System + +- **Cost**: 10 credits per image generation +- **Free tier**: 3 free generations for new users +- **Enforcement**: Only in production environment +- **Development**: Fail-open (no credit enforcement) + +## Image Upload Flow + +1. User sends `!generate [prompt]` +2. Bot calls Picture Backend with `waitForResult: true` +3. Backend generates image via Replicate +4. Bot downloads image from storage URL +5. Bot uploads image to Matrix media server +6. Bot sends image message to room diff --git a/services/matrix-picture-bot/Dockerfile b/services/matrix-picture-bot/Dockerfile new file mode 100644 index 000000000..4211d1c71 --- /dev/null +++ b/services/matrix-picture-bot/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package.json ./ + +# Install dependencies +RUN npm install + +# Copy source code +COPY . . + +# Build the application +RUN npm run build + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy package files and install production dependencies only +COPY package.json ./ +RUN npm install --omit=dev + +# Copy built application from builder +COPY --from=builder /app/dist ./dist + +# Create data directory +RUN mkdir -p /app/data + +# Expose port +EXPOSE 3319 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3319/health || exit 1 + +# Start the application +CMD ["node", "dist/main.js"] diff --git a/services/matrix-picture-bot/nest-cli.json b/services/matrix-picture-bot/nest-cli.json new file mode 100644 index 000000000..95538fb90 --- /dev/null +++ b/services/matrix-picture-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/matrix-picture-bot/package.json b/services/matrix-picture-bot/package.json new file mode 100644 index 000000000..f3fe55418 --- /dev/null +++ b/services/matrix-picture-bot/package.json @@ -0,0 +1,39 @@ +{ + "name": "@manacore/matrix-picture-bot", + "version": "1.0.0", + "description": "Matrix bot for AI image generation via Picture backend", + "private": true, + "pnpm": { + "neverBuiltDependencies": [ + "@matrix-org/matrix-sdk-crypto-nodejs" + ], + "overrides": { + "@matrix-org/matrix-sdk-crypto-nodejs": "npm:empty-npm-package@1.0.0" + } + }, + "overrides": { + "@matrix-org/matrix-sdk-crypto-nodejs": "npm:empty-npm-package@1.0.0" + }, + "scripts": { + "prebuild": "rm -rf dist || true", + "build": "nest build", + "start": "nest start", + "start:dev": "nest start --watch", + "start:prod": "node dist/main", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@nestjs/common": "^10.4.15", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.15", + "@nestjs/platform-express": "^10.4.15", + "matrix-bot-sdk": "^0.7.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.9", + "@types/node": "^22.10.2", + "typescript": "^5.7.2" + } +} diff --git a/services/matrix-picture-bot/src/app.module.ts b/services/matrix-picture-bot/src/app.module.ts new file mode 100644 index 000000000..09bbe3a8c --- /dev/null +++ b/services/matrix-picture-bot/src/app.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import configuration from './config/configuration'; +import { BotModule } from './bot/bot.module'; +import { HealthController } from './health.controller'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [configuration], + }), + BotModule, + ], + controllers: [HealthController], +}) +export class AppModule {} diff --git a/services/matrix-picture-bot/src/bot/bot.module.ts b/services/matrix-picture-bot/src/bot/bot.module.ts new file mode 100644 index 000000000..c72f5b4e0 --- /dev/null +++ b/services/matrix-picture-bot/src/bot/bot.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { MatrixService } from './matrix.service'; +import { PictureModule } from '../picture/picture.module'; +import { SessionModule } from '../session/session.module'; + +@Module({ + imports: [PictureModule, SessionModule], + providers: [MatrixService], + exports: [MatrixService], +}) +export class BotModule {} diff --git a/services/matrix-picture-bot/src/bot/matrix.service.ts b/services/matrix-picture-bot/src/bot/matrix.service.ts new file mode 100644 index 000000000..73e2871e4 --- /dev/null +++ b/services/matrix-picture-bot/src/bot/matrix.service.ts @@ -0,0 +1,648 @@ +import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { + MatrixClient, + SimpleFsStorageProvider, + RichConsoleLogger, + LogService, + LogLevel, +} from 'matrix-bot-sdk'; +import { PictureService } from '../picture/picture.service'; +import { SessionService } from '../session/session.service'; +import { HELP_MESSAGE } from '../config/configuration'; + +// Natural language keywords that trigger commands +const KEYWORD_COMMANDS: { keywords: string[]; command: string }[] = [ + { keywords: ['hilfe', 'help', 'befehle', 'commands'], command: 'help' }, + { keywords: ['modelle', 'models'], command: 'models' }, + { keywords: ['verlauf', 'history', 'bilder'], command: 'history' }, + { keywords: ['credits', 'guthaben'], command: 'credits' }, + { keywords: ['status', 'info'], command: 'status' }, +]; + +interface ParsedPrompt { + prompt: string; + negativePrompt?: string; + width?: number; + height?: number; + steps?: number; + style?: string; +} + +@Injectable() +export class MatrixService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(MatrixService.name); + private client!: MatrixClient; + private readonly allowedRooms: string[]; + private botUserId: string = ''; + + // Track active generations per user + private activeGenerations: Map = new Map(); + // Track selected model per user + private userModels: Map = new Map(); + + constructor( + private configService: ConfigService, + private pictureService: PictureService, + private sessionService: SessionService + ) { + this.allowedRooms = this.configService.get('matrix.allowedRooms') || []; + } + + async onModuleInit() { + const homeserverUrl = this.configService.get('matrix.homeserverUrl'); + const accessToken = this.configService.get('matrix.accessToken'); + const storagePath = this.configService.get('matrix.storagePath'); + + if (!accessToken) { + this.logger.error('MATRIX_ACCESS_TOKEN is required'); + return; + } + + // Setup logging + LogService.setLogger(new RichConsoleLogger()); + LogService.setLevel(LogLevel.INFO); + + // Storage for sync token persistence + const storage = new SimpleFsStorageProvider(storagePath || './data/bot-storage.json'); + + // Create Matrix client + this.client = new MatrixClient(homeserverUrl!, accessToken, storage); + + // Auto-join rooms when invited + this.client.on('room.invite', async (roomId: string) => { + this.logger.log(`Invited to room ${roomId}, joining...`); + await this.client.joinRoom(roomId); + + setTimeout(async () => { + try { + await this.sendBotIntroduction(roomId); + } catch (error) { + this.logger.error(`Failed to send introduction to ${roomId}:`, error); + } + }, 2000); + }); + + // Get bot's user ID + this.botUserId = await this.client.getUserId(); + this.logger.log(`Bot user ID: ${this.botUserId}`); + + // Setup message handler + this.client.on('room.message', this.handleRoomMessage.bind(this)); + + // Start the client + await this.client.start(); + this.logger.log('Matrix Picture Bot started successfully'); + } + + async onModuleDestroy() { + if (this.client) { + await this.client.stop(); + this.logger.log('Matrix bot stopped'); + } + } + + private async sendBotIntroduction(roomId: string) { + const introText = `**Picture Bot - AI-Bildgenerierung** + +Ich generiere Bilder mit AI fur dich! + +**Schnellstart:** +\`!generate A beautiful landscape\` +\`!bild Ein niedlicher Hund\` + +Sag "hilfe" fur alle Befehle!`; + + await this.sendMessage(roomId, introText); + } + + private isRoomAllowed(roomId: string): boolean { + if (this.allowedRooms.length === 0) return true; + return this.allowedRooms.some((allowed) => roomId === allowed || roomId.includes(allowed)); + } + + private async handleRoomMessage(roomId: string, event: any) { + // Ignore messages from self + if (event.sender === this.botUserId) return; + + // Check if room is allowed + if (!this.isRoomAllowed(roomId)) { + this.logger.debug(`Ignoring message from non-allowed room: ${roomId}`); + return; + } + + const content = event.content as { msgtype?: string; body?: string }; + + // Only handle text messages + if (content.msgtype !== 'm.text') return; + + const body = content.body; + if (!body) return; + + this.logger.log(`Message from ${event.sender} in ${roomId}: ${body.substring(0, 50)}...`); + + // Handle commands with ! prefix + if (body.startsWith('!')) { + await this.handleCommand(roomId, event.sender, body); + return; + } + + // Check for natural language keywords + const keywordCommand = this.detectKeywordCommand(body); + if (keywordCommand) { + await this.handleCommand(roomId, event.sender, `!${keywordCommand}`); + return; + } + + // Don't respond to random messages + } + + private detectKeywordCommand(message: string): string | null { + const lowerMessage = message.toLowerCase().trim(); + + // Only match if the message is short + if (lowerMessage.length > 30) return null; + + for (const { keywords, command } of KEYWORD_COMMANDS) { + for (const keyword of keywords) { + if (lowerMessage === keyword || lowerMessage.startsWith(keyword + ' ')) { + this.logger.log(`Detected keyword "${keyword}" -> command "${command}"`); + return command; + } + } + } + return null; + } + + private async handleCommand(roomId: string, sender: string, body: string) { + const [command, ...args] = body.slice(1).split(' '); + const argString = args.join(' '); + + switch (command.toLowerCase()) { + case 'help': + case 'hilfe': + case 'start': + await this.sendHelp(roomId); + break; + + case 'generate': + case 'bild': + case 'gen': + await this.handleGenerate(roomId, sender, argString); + break; + + case 'models': + case 'modelle': + await this.handleModels(roomId); + break; + + case 'model': + case 'modell': + await this.handleSelectModel(roomId, sender, argString); + break; + + case 'history': + case 'verlauf': + await this.handleHistory(roomId, sender); + break; + + case 'delete': + case 'loeschen': + await this.handleDelete(roomId, sender, args); + break; + + case 'credits': + case 'guthaben': + await this.handleCredits(roomId, sender); + break; + + case 'login': + await this.handleLogin(roomId, sender, args); + break; + + case 'logout': + this.sessionService.logout(sender); + await this.sendMessage(roomId, 'Du wurdest abgemeldet.'); + break; + + case 'status': + await this.handleStatus(roomId, sender); + break; + + case 'cancel': + case 'abbrechen': + await this.handleCancel(roomId, sender); + break; + + case 'pin': + await this.pinHelpMessage(roomId); + break; + + default: + await this.sendMessage( + roomId, + `Unbekannter Befehl: !${command}\n\nSag "hilfe" fur alle Befehle.` + ); + } + } + + private parsePrompt(input: string): ParsedPrompt { + const result: ParsedPrompt = { prompt: '' }; + + // Extract options + const widthMatch = input.match(/--width\s+(\d+)/i); + if (widthMatch) { + result.width = parseInt(widthMatch[1], 10); + input = input.replace(widthMatch[0], ''); + } + + const heightMatch = input.match(/--height\s+(\d+)/i); + if (heightMatch) { + result.height = parseInt(heightMatch[1], 10); + input = input.replace(heightMatch[0], ''); + } + + const stepsMatch = input.match(/--steps\s+(\d+)/i); + if (stepsMatch) { + result.steps = parseInt(stepsMatch[1], 10); + input = input.replace(stepsMatch[0], ''); + } + + const negativeMatch = input.match(/--negative\s+(.+?)(?=--|$)/i); + if (negativeMatch) { + result.negativePrompt = negativeMatch[1].trim(); + input = input.replace(negativeMatch[0], ''); + } + + const styleMatch = input.match(/--style\s+(\S+)/i); + if (styleMatch) { + result.style = styleMatch[1]; + input = input.replace(styleMatch[0], ''); + } + + result.prompt = input.trim(); + return result; + } + + private async handleGenerate(roomId: string, sender: string, promptInput: string) { + if (!promptInput.trim()) { + await this.sendMessage( + roomId, + `**Verwendung:** \`!generate [prompt]\`\n\nBeispiel: \`!generate A beautiful sunset over mountains\`` + ); + return; + } + + // Check if user is logged in + const token = this.sessionService.getToken(sender); + if (!token) { + await this.sendMessage( + roomId, + `Du musst angemeldet sein, um Bilder zu generieren.\n\nNutze \`!login email passwort\` zum Anmelden.` + ); + return; + } + + // Check if user already has an active generation + if (this.activeGenerations.has(sender)) { + await this.sendMessage( + roomId, + `Du hast bereits eine laufende Generierung. Warte bis sie fertig ist oder nutze \`!cancel\`.` + ); + return; + } + + // Parse the prompt + const parsed = this.parsePrompt(promptInput); + + await this.sendMessage(roomId, `Generiere Bild...\n\n**Prompt:** "${parsed.prompt}"`); + + try { + // Get user's selected model or use default + const modelId = this.userModels.get(sender); + + // Mark generation as active + this.activeGenerations.set(sender, 'generating'); + + const result = await this.pictureService.generateImage(token, { + prompt: parsed.prompt, + negativePrompt: parsed.negativePrompt, + modelId, + width: parsed.width, + height: parsed.height, + steps: parsed.steps, + style: parsed.style, + }); + + // Clear active generation + this.activeGenerations.delete(sender); + + if (result.status === 'completed' && result.image) { + // Upload image to Matrix + const imageUrl = result.image.publicUrl; + if (imageUrl) { + try { + // Download and upload to Matrix + const response = await fetch(imageUrl); + const buffer = Buffer.from(await response.arrayBuffer()); + const mxcUrl = await this.client.uploadContent(buffer, 'image/png'); + + // Send image message + await this.client.sendMessage(roomId, { + msgtype: 'm.image', + body: parsed.prompt.substring(0, 50), + url: mxcUrl, + info: { + mimetype: 'image/png', + w: result.image.width || 1024, + h: result.image.height || 1024, + }, + }); + + let infoText = `**Bild generiert!**\n\n`; + infoText += `**Prompt:** ${parsed.prompt}\n`; + if (result.creditsUsed) { + infoText += `**Credits verwendet:** ${result.creditsUsed}`; + } + + await this.sendMessage(roomId, infoText); + } catch (uploadError) { + this.logger.error('Failed to upload image to Matrix:', uploadError); + await this.sendMessage(roomId, `Bild generiert! Direkter Link: ${imageUrl}`); + } + } else { + await this.sendMessage(roomId, `Bild generiert, aber keine URL verfugbar.`); + } + } else if (result.status === 'processing') { + await this.sendMessage( + roomId, + `Generierung gestartet (ID: ${result.generationId}). Das Bild wird bald fertig sein.` + ); + } else { + await this.sendMessage(roomId, `Generierung fehlgeschlagen. Bitte versuche es erneut.`); + } + } catch (error) { + this.activeGenerations.delete(sender); + const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler'; + this.logger.error('Generation error:', error); + await this.sendMessage(roomId, `Fehler bei der Generierung: ${errorMsg}`); + } + } + + private async handleModels(roomId: string) { + try { + const models = await this.pictureService.getModels(); + + if (models.length === 0) { + await this.sendMessage(roomId, 'Keine Modelle verfugbar.'); + return; + } + + let text = `**Verfugbare Modelle:**\n\n`; + for (const model of models) { + const defaultTag = model.isDefault ? ' **(Standard)**' : ''; + text += `**${model.name}**${defaultTag}\n`; + text += `ID: \`${model.id}\`\n`; + if (model.description) { + text += `${model.description}\n`; + } + text += `\n`; + } + + text += `\nNutze \`!model [id]\` um ein Modell auszuwahlen.`; + + await this.sendMessage(roomId, text); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler'; + await this.sendMessage(roomId, `Fehler beim Laden der Modelle: ${errorMsg}`); + } + } + + private async handleSelectModel(roomId: string, sender: string, modelId: string) { + if (!modelId.trim()) { + const currentModel = this.userModels.get(sender); + if (currentModel) { + await this.sendMessage( + roomId, + `Aktuelles Modell: \`${currentModel}\`\n\nNutze \`!models\` um alle Modelle zu sehen.` + ); + } else { + await this.sendMessage( + roomId, + `Kein Modell ausgewahlt (Standard wird verwendet).\n\nNutze \`!models\` um alle Modelle zu sehen.` + ); + } + return; + } + + try { + const model = await this.pictureService.getModel(modelId.trim()); + this.userModels.set(sender, model.id); + await this.sendMessage(roomId, `Modell gewechselt zu: **${model.name}**`); + } catch (error) { + await this.sendMessage( + roomId, + `Modell "${modelId}" nicht gefunden. Nutze \`!models\` fur verfugbare Modelle.` + ); + } + } + + private async handleHistory(roomId: string, sender: string) { + const token = this.sessionService.getToken(sender); + if (!token) { + await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); + return; + } + + try { + const images = await this.pictureService.getImages(token, 10); + + if (images.length === 0) { + await this.sendMessage(roomId, `Du hast noch keine Bilder generiert.`); + return; + } + + let text = `**Deine letzten Bilder (${images.length}):**\n\n`; + + for (let i = 0; i < images.length; i++) { + const img = images[i]; + const promptPreview = img.prompt?.substring(0, 40) || 'Kein Prompt'; + const date = new Date(img.createdAt).toLocaleDateString('de-DE'); + text += `**${i + 1}.** "${promptPreview}${img.prompt && img.prompt.length > 40 ? '...' : ''}"\n`; + text += ` ${date} | ${img.width}x${img.height}\n\n`; + } + + await this.sendMessage(roomId, text); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler'; + await this.sendMessage(roomId, `Fehler: ${errorMsg}`); + } + } + + private async handleDelete(roomId: string, sender: string, args: string[]) { + const token = this.sessionService.getToken(sender); + if (!token) { + await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); + return; + } + + if (args.length < 1) { + await this.sendMessage( + roomId, + `**Verwendung:** \`!delete [bild-nr]\`\n\nNutze \`!history\` um Bildnummern zu sehen.` + ); + return; + } + + const imageIndex = parseInt(args[0], 10); + if (isNaN(imageIndex) || imageIndex < 1) { + await this.sendMessage(roomId, `Ungultige Bildnummer.`); + return; + } + + try { + const images = await this.pictureService.getImages(token, 10); + if (imageIndex > images.length) { + await this.sendMessage(roomId, `Bild ${imageIndex} existiert nicht.`); + return; + } + + const image = images[imageIndex - 1]; + await this.pictureService.deleteImage(token, image.id); + + await this.sendMessage(roomId, `Bild ${imageIndex} geloscht.`); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler'; + await this.sendMessage(roomId, `Fehler: ${errorMsg}`); + } + } + + private async handleCredits(roomId: string, sender: string) { + const token = this.sessionService.getToken(sender); + if (!token) { + await this.sendMessage(roomId, `Du bist nicht angemeldet. Nutze \`!login\` zuerst.`); + return; + } + + try { + const balance = await this.pictureService.getCredits(token); + await this.sendMessage( + roomId, + `**Dein Credit-Guthaben:** ${balance} Credits\n\nEine Bildgenerierung kostet 10 Credits.` + ); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unbekannter Fehler'; + await this.sendMessage(roomId, `Fehler: ${errorMsg}`); + } + } + + private async handleCancel(roomId: string, sender: string) { + if (!this.activeGenerations.has(sender)) { + await this.sendMessage(roomId, `Du hast keine laufende Generierung.`); + return; + } + + this.activeGenerations.delete(sender); + await this.sendMessage(roomId, `Generierung abgebrochen.`); + } + + private async sendHelp(roomId: string) { + await this.sendMessage(roomId, HELP_MESSAGE); + } + + private async handleLogin(roomId: string, sender: string, args: string[]) { + if (args.length < 2) { + await this.sendMessage( + roomId, + `**Verwendung:** \`!login email passwort\`\n\nBeispiel: \`!login nutzer@example.com meinpasswort\`` + ); + return; + } + + const [email, password] = args; + + await this.sendMessage(roomId, 'Anmeldung lauft...'); + + const result = await this.sessionService.login(sender, email, password); + + if (result.success) { + await this.sendMessage( + roomId, + `Erfolgreich angemeldet!\n\nDu kannst jetzt Bilder generieren mit \`!generate [prompt]\`` + ); + } else { + await this.sendMessage(roomId, `Anmeldung fehlgeschlagen: ${result.error}`); + } + } + + private async handleStatus(roomId: string, sender: string) { + const backendHealthy = await this.pictureService.checkHealth(); + const isLoggedIn = this.sessionService.isLoggedIn(sender); + const sessionCount = this.sessionService.getSessionCount(); + const currentModel = this.userModels.get(sender); + const hasActiveGeneration = this.activeGenerations.has(sender); + + const statusText = `**Picture Bot Status** + +**Backend:** ${backendHealthy ? 'Online' : 'Offline'} +**Dein Status:** ${isLoggedIn ? 'Angemeldet' : 'Nicht angemeldet'} +**Ausgewahltes Modell:** ${currentModel || 'Standard'} +**Aktive Generierung:** ${hasActiveGeneration ? 'Ja' : 'Nein'} +**Aktive Sessions:** ${sessionCount} + +${!isLoggedIn ? 'Nutze `!login email passwort` um dich anzumelden.' : ''}`; + + await this.sendMessage(roomId, statusText); + } + + private async pinHelpMessage(roomId: string) { + try { + const htmlBody = this.markdownToHtml(HELP_MESSAGE); + + const eventId = await this.client.sendMessage(roomId, { + msgtype: 'm.text', + body: HELP_MESSAGE, + format: 'org.matrix.custom.html', + formatted_body: htmlBody, + }); + + await this.client.sendStateEvent(roomId, 'm.room.pinned_events', '', { + pinned: [eventId], + }); + + this.logger.log(`Pinned help message in room ${roomId}`); + } catch (error) { + this.logger.error(`Failed to pin help message:`, error); + await this.sendMessage(roomId, 'Fehler beim Pinnen der Hilfe.'); + } + } + + private async sendMessage(roomId: string, message: string) { + const htmlBody = this.markdownToHtml(message); + + await this.client.sendMessage(roomId, { + msgtype: 'm.text', + body: message, + format: 'org.matrix.custom.html', + formatted_body: htmlBody, + }); + } + + private markdownToHtml(markdown: string): string { + return ( + markdown + // Code blocks + .replace(/```(\w+)?\n([\s\S]*?)```/g, '
$2
') + // Inline code + .replace(/`([^`]+)`/g, '$1') + // Bold + .replace(/\*\*([^*]+)\*\*/g, '$1') + // Italic + .replace(/\*([^*]+)\*/g, '$1') + // Underscore italic + .replace(/_([^_]+)_/g, '$1') + // Line breaks + .replace(/\n/g, '
') + ); + } +} diff --git a/services/matrix-picture-bot/src/config/configuration.ts b/services/matrix-picture-bot/src/config/configuration.ts new file mode 100644 index 000000000..d1ad99a13 --- /dev/null +++ b/services/matrix-picture-bot/src/config/configuration.ts @@ -0,0 +1,69 @@ +export default () => ({ + port: parseInt(process.env.PORT || '3319', 10), + matrix: { + homeserverUrl: process.env.MATRIX_HOMESERVER_URL || 'http://localhost:8008', + accessToken: process.env.MATRIX_ACCESS_TOKEN || '', + allowedRooms: (process.env.MATRIX_ALLOWED_ROOMS || '').split(',').filter(Boolean), + storagePath: process.env.MATRIX_STORAGE_PATH || './data/bot-storage.json', + }, + picture: { + backendUrl: process.env.PICTURE_BACKEND_URL || 'http://localhost:3006', + apiPrefix: process.env.PICTURE_API_PREFIX || '/api/v1', + }, + auth: { + url: process.env.MANA_CORE_AUTH_URL || 'http://localhost:3001', + }, +}); + +export const HELP_MESSAGE = `**Picture Bot - AI-Bildgenerierung** + +**Bilder generieren:** +- \`!generate [prompt]\` - Bild generieren +- \`!bild [prompt]\` - Bild generieren (deutsch) +- \`!model [id]\` - Modell wechseln +- \`!models\` - Verfugbare Modelle anzeigen + +**Optionen (im Prompt):** +- \`--width 1024\` - Breite setzen +- \`--height 768\` - Hohe setzen +- \`--steps 30\` - Mehr Schritte = mehr Detail +- \`--negative [text]\` - Negative Prompts + +**Beispiele:** +\`!generate A beautiful sunset over mountains --width 1280 --height 720\` +\`!bild Ein niedlicher Hund im Park --steps 40\` + +**Bilder verwalten:** (Login erforderlich) +- \`!login email passwort\` - Anmelden +- \`!logout\` - Abmelden +- \`!history\` - Letzte Bilder anzeigen +- \`!delete [nr]\` - Bild loschen + +**Sonstiges:** +- \`!status\` - Bot-Status +- \`!credits\` - Credits anzeigen +- \`!help\` - Diese Hilfe`; + +export const STYLES = [ + 'photorealistic', + 'anime', + 'digital-art', + 'oil-painting', + 'watercolor', + 'sketch', + '3d-render', + 'pixel-art', +] as const; + +export type Style = (typeof STYLES)[number]; + +export const STYLE_LABELS: Record = { + photorealistic: 'Fotorealistisch', + anime: 'Anime', + 'digital-art': 'Digital Art', + 'oil-painting': 'Olmalerei', + watercolor: 'Aquarell', + sketch: 'Skizze', + '3d-render': '3D Render', + 'pixel-art': 'Pixel Art', +}; diff --git a/services/matrix-picture-bot/src/health.controller.ts b/services/matrix-picture-bot/src/health.controller.ts new file mode 100644 index 000000000..42e55a0ba --- /dev/null +++ b/services/matrix-picture-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: 'matrix-picture-bot', + timestamp: new Date().toISOString(), + }; + } +} diff --git a/services/matrix-picture-bot/src/main.ts b/services/matrix-picture-bot/src/main.ts new file mode 100644 index 000000000..4b95c383d --- /dev/null +++ b/services/matrix-picture-bot/src/main.ts @@ -0,0 +1,17 @@ +import { NestFactory } from '@nestjs/core'; +import { Logger } from '@nestjs/common'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const logger = new Logger('Bootstrap'); + + const app = await NestFactory.create(AppModule); + + const port = process.env.PORT || 3319; + await app.listen(port); + + logger.log(`Matrix Picture Bot running on port ${port}`); + logger.log(`Health check: http://localhost:${port}/health`); +} + +bootstrap(); diff --git a/services/matrix-picture-bot/src/picture/picture.module.ts b/services/matrix-picture-bot/src/picture/picture.module.ts new file mode 100644 index 000000000..6b5abb7a9 --- /dev/null +++ b/services/matrix-picture-bot/src/picture/picture.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { PictureService } from './picture.service'; + +@Module({ + providers: [PictureService], + exports: [PictureService], +}) +export class PictureModule {} diff --git a/services/matrix-picture-bot/src/picture/picture.service.ts b/services/matrix-picture-bot/src/picture/picture.service.ts new file mode 100644 index 000000000..5a75f6c67 --- /dev/null +++ b/services/matrix-picture-bot/src/picture/picture.service.ts @@ -0,0 +1,206 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +export interface Model { + id: string; + name: string; + description?: string; + isDefault?: boolean; + defaultWidth?: number; + defaultHeight?: number; +} + +export interface GenerateOptions { + prompt: string; + negativePrompt?: string; + modelId?: string; + width?: number; + height?: number; + steps?: number; + style?: string; +} + +export interface GenerateResult { + generationId: string; + status: string; + image?: { + id: string; + publicUrl?: string; + width?: number; + height?: number; + }; + creditsUsed?: number; +} + +export interface ImageInfo { + id: string; + prompt?: string; + width: number; + height: number; + publicUrl?: string; + createdAt: string; +} + +@Injectable() +export class PictureService { + private readonly logger = new Logger(PictureService.name); + private readonly backendUrl: string; + private readonly apiPrefix: string; + + constructor(private configService: ConfigService) { + this.backendUrl = + this.configService.get('picture.backendUrl') || 'http://localhost:3006'; + this.apiPrefix = this.configService.get('picture.apiPrefix') || '/api/v1'; + } + + private getApiUrl(path: string): string { + return `${this.backendUrl}${this.apiPrefix}${path}`; + } + + async checkHealth(): Promise { + try { + const response = await fetch(`${this.backendUrl}/health`); + return response.ok; + } catch (error) { + this.logger.error('Health check failed:', error); + return false; + } + } + + async getModels(): Promise { + try { + const response = await fetch(this.getApiUrl('/models')); + + if (!response.ok) { + throw new Error(`Failed to fetch models: ${response.status}`); + } + + const data = await response.json(); + return data; + } catch (error) { + this.logger.error('Failed to fetch models:', error); + throw error; + } + } + + async getModel(modelId: string): Promise { + try { + const response = await fetch(this.getApiUrl(`/models/${modelId}`)); + + if (!response.ok) { + if (response.status === 404) { + throw new Error('Model not found'); + } + throw new Error(`Failed to fetch model: ${response.status}`); + } + + return await response.json(); + } catch (error) { + this.logger.error(`Failed to fetch model ${modelId}:`, error); + throw error; + } + } + + async generateImage(token: string, options: GenerateOptions): Promise { + try { + // First, get default model if none specified + let modelId = options.modelId; + if (!modelId) { + const models = await this.getModels(); + const defaultModel = models.find((m) => m.isDefault) || models[0]; + if (!defaultModel) { + throw new Error('No models available'); + } + modelId = defaultModel.id; + } + + const response = await fetch(this.getApiUrl('/generate'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + prompt: options.prompt, + negativePrompt: options.negativePrompt, + modelId, + width: options.width, + height: options.height, + steps: options.steps, + style: options.style, + waitForResult: true, // Sync mode for Matrix bot + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `Generation failed: ${response.status}`); + } + + return await response.json(); + } catch (error) { + this.logger.error('Generation error:', error); + throw error; + } + } + + async getImages(token: string, limit: number = 10): Promise { + try { + const response = await fetch(this.getApiUrl(`/images?limit=${limit}`), { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch images: ${response.status}`); + } + + return await response.json(); + } catch (error) { + this.logger.error('Failed to fetch images:', error); + throw error; + } + } + + async deleteImage(token: string, imageId: string): Promise { + try { + const response = await fetch(this.getApiUrl(`/images/${imageId}`), { + method: 'DELETE', + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to delete image: ${response.status}`); + } + } catch (error) { + this.logger.error(`Failed to delete image ${imageId}:`, error); + throw error; + } + } + + async getCredits(token: string): Promise { + try { + // Credits are managed by mana-core, but we can try to get them via the backend + // If the backend doesn't expose this endpoint, return a placeholder + const response = await fetch(this.getApiUrl('/credits/balance'), { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + // Credits endpoint might not exist, return placeholder + return -1; + } + + const data = await response.json(); + return data.balance ?? data.credits ?? -1; + } catch (error) { + this.logger.warn('Failed to fetch credits:', error); + return -1; + } + } +} diff --git a/services/matrix-picture-bot/src/session/session.module.ts b/services/matrix-picture-bot/src/session/session.module.ts new file mode 100644 index 000000000..834b715eb --- /dev/null +++ b/services/matrix-picture-bot/src/session/session.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { SessionService } from './session.service'; + +@Module({ + providers: [SessionService], + exports: [SessionService], +}) +export class SessionModule {} diff --git a/services/matrix-picture-bot/src/session/session.service.ts b/services/matrix-picture-bot/src/session/session.service.ts new file mode 100644 index 000000000..7c2064522 --- /dev/null +++ b/services/matrix-picture-bot/src/session/session.service.ts @@ -0,0 +1,100 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +interface UserSession { + token: string; + email: string; + expiresAt: Date; +} + +@Injectable() +export class SessionService { + private readonly logger = new Logger(SessionService.name); + private sessions: Map = new Map(); + private authUrl: string; + + constructor(private configService: ConfigService) { + this.authUrl = this.configService.get('auth.url') || 'http://localhost:3001'; + } + + async login( + matrixUserId: string, + email: string, + password: string + ): Promise<{ success: boolean; error?: string }> { + try { + const response = await fetch(`${this.authUrl}/api/v1/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + return { + success: false, + error: errorData.message || 'Authentifizierung fehlgeschlagen', + }; + } + + const data = await response.json(); + const token = data.accessToken || data.token; + + if (!token) { + return { success: false, error: 'Kein Token erhalten' }; + } + + // Store session (7 days expiry) + this.sessions.set(matrixUserId, { + token, + email, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }); + + this.logger.log(`User ${matrixUserId} logged in as ${email}`); + return { success: true }; + } catch (error) { + this.logger.error(`Login failed for ${matrixUserId}:`, error); + return { + success: false, + error: 'Verbindung zum Auth-Server fehlgeschlagen', + }; + } + } + + logout(matrixUserId: string): void { + this.sessions.delete(matrixUserId); + this.logger.log(`User ${matrixUserId} logged out`); + } + + getToken(matrixUserId: string): string | null { + const session = this.sessions.get(matrixUserId); + + if (!session) return null; + + // Check if token expired + if (session.expiresAt < new Date()) { + this.sessions.delete(matrixUserId); + return null; + } + + return session.token; + } + + isLoggedIn(matrixUserId: string): boolean { + return this.getToken(matrixUserId) !== null; + } + + getSessionCount(): number { + return this.sessions.size; + } + + getLoggedInCount(): number { + const now = new Date(); + let count = 0; + for (const session of this.sessions.values()) { + if (session.expiresAt > now) count++; + } + return count; + } +} diff --git a/services/matrix-picture-bot/tsconfig.json b/services/matrix-picture-bot/tsconfig.json new file mode 100644 index 000000000..38c2b55d7 --- /dev/null +++ b/services/matrix-picture-bot/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "resolveJsonModule": true + } +}