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 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-01-30 16:15:08 +01:00
parent 3b745cf068
commit 8950692cfd
18 changed files with 1712 additions and 126 deletions

411
pnpm-lock.yaml generated
View file

@ -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)

View file

@ -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

29
services/matrix-picture-bot/.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
# Dependencies
node_modules/
# Build output
dist/
# Environment
.env
.env.local
# Data
data/
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
# TypeScript
*.tsbuildinfo

View file

@ -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

View file

@ -0,0 +1,41 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Copy package files and install production dependencies only
COPY package.json ./
RUN npm install --omit=dev
# Copy built application from builder
COPY --from=builder /app/dist ./dist
# Create data directory
RUN mkdir -p /app/data
# Expose port
EXPOSE 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"]

View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

View file

@ -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"
}
}

View file

@ -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 {}

View file

@ -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 {}

View file

@ -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<string, string> = new Map();
// Track selected model per user
private userModels: Map<string, string> = new Map();
constructor(
private configService: ConfigService,
private pictureService: PictureService,
private sessionService: SessionService
) {
this.allowedRooms = this.configService.get<string[]>('matrix.allowedRooms') || [];
}
async onModuleInit() {
const homeserverUrl = this.configService.get<string>('matrix.homeserverUrl');
const accessToken = this.configService.get<string>('matrix.accessToken');
const storagePath = this.configService.get<string>('matrix.storagePath');
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, '<pre><code>$2</code></pre>')
// Inline code
.replace(/`([^`]+)`/g, '<code>$1</code>')
// Bold
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
// Italic
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
// Underscore italic
.replace(/_([^_]+)_/g, '<em>$1</em>')
// Line breaks
.replace(/\n/g, '<br/>')
);
}
}

View file

@ -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<Style, string> = {
photorealistic: 'Fotorealistisch',
anime: 'Anime',
'digital-art': 'Digital Art',
'oil-painting': 'Olmalerei',
watercolor: 'Aquarell',
sketch: 'Skizze',
'3d-render': '3D Render',
'pixel-art': 'Pixel Art',
};

View file

@ -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(),
};
}
}

View file

@ -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();

View file

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { PictureService } from './picture.service';
@Module({
providers: [PictureService],
exports: [PictureService],
})
export class PictureModule {}

View file

@ -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<string>('picture.backendUrl') || 'http://localhost:3006';
this.apiPrefix = this.configService.get<string>('picture.apiPrefix') || '/api/v1';
}
private getApiUrl(path: string): string {
return `${this.backendUrl}${this.apiPrefix}${path}`;
}
async checkHealth(): Promise<boolean> {
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<Model[]> {
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<Model> {
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<GenerateResult> {
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<ImageInfo[]> {
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<void> {
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<number> {
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;
}
}
}

View file

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { SessionService } from './session.service';
@Module({
providers: [SessionService],
exports: [SessionService],
})
export class SessionModule {}

View file

@ -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<string, UserSession> = new Map();
private authUrl: string;
constructor(private configService: ConfigService) {
this.authUrl = this.configService.get<string>('auth.url') || 'http://localhost:3001';
}
async login(
matrixUserId: string,
email: string,
password: string
): Promise<{ success: boolean; error?: string }> {
try {
const response = await fetch(`${this.authUrl}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
return {
success: false,
error: errorData.message || 'Authentifizierung fehlgeschlagen',
};
}
const data = await response.json();
const token = data.accessToken || data.token;
if (!token) {
return { success: false, error: 'Kein Token erhalten' };
}
// Store session (7 days expiry)
this.sessions.set(matrixUserId, {
token,
email,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
});
this.logger.log(`User ${matrixUserId} logged in as ${email}`);
return { success: true };
} catch (error) {
this.logger.error(`Login failed for ${matrixUserId}:`, error);
return {
success: false,
error: 'Verbindung zum Auth-Server fehlgeschlagen',
};
}
}
logout(matrixUserId: string): void {
this.sessions.delete(matrixUserId);
this.logger.log(`User ${matrixUserId} logged out`);
}
getToken(matrixUserId: string): string | null {
const session = this.sessions.get(matrixUserId);
if (!session) return null;
// 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;
}
}

View file

@ -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
}
}