commit 5f16ed135d745f08e63ca2bebce4db6bb2b6403f Author: NAME Date: Thu May 14 08:42:03 2026 +0000 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..834d3b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +.env diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d30c946 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## Description + +[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. + +## Project setup + +```bash +$ pnpm install +``` + +## Compile and run the project + +```bash +# development +$ pnpm run start + +# watch mode +$ pnpm run start:dev + +# production mode +$ pnpm run start:prod +``` + +## Run tests + +```bash +# unit tests +$ pnpm run test + +# e2e tests +$ pnpm run test:e2e + +# test coverage +$ pnpm run test:cov +``` + +## Deployment + +When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. + +If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: + +```bash +$ pnpm install -g @nestjs/mau +$ mau deploy +``` + +With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. + +## Resources + +Check out a few resources that may come in handy when working with NestJS: + +- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. +- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). +- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). +- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. +- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). +- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). +- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). +- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). + +## Support + +Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). + +## Stay in touch + +- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) +- Website - [https://nestjs.com](https://nestjs.com/) +- Twitter - [@nestframework](https://twitter.com/nestframework) + +## License + +Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). diff --git a/downloads/tiktok/tt_1777635599428.mp4 b/downloads/tiktok/tt_1777635599428.mp4 new file mode 100644 index 0000000..32a006c Binary files /dev/null and b/downloads/tiktok/tt_1777635599428.mp4 differ diff --git a/downloads/tiktok/tt_1777646870542.mp4 b/downloads/tiktok/tt_1777646870542.mp4 new file mode 100644 index 0000000..3561f8b Binary files /dev/null and b/downloads/tiktok/tt_1777646870542.mp4 differ diff --git a/downloads/tiktok/tt_1777647710531.mp4 b/downloads/tiktok/tt_1777647710531.mp4 new file mode 100644 index 0000000..17e9551 Binary files /dev/null and b/downloads/tiktok/tt_1777647710531.mp4 differ diff --git a/downloads/tiktok/tt_1777729146884.mp4 b/downloads/tiktok/tt_1777729146884.mp4 new file mode 100644 index 0000000..1d02d16 Binary files /dev/null and b/downloads/tiktok/tt_1777729146884.mp4 differ diff --git a/downloads/tiktok/tt_1777729871518.mp4 b/downloads/tiktok/tt_1777729871518.mp4 new file mode 100644 index 0000000..d9a99d5 Binary files /dev/null and b/downloads/tiktok/tt_1777729871518.mp4 differ diff --git a/downloads/tiktok/tt_1777730480815.mp4 b/downloads/tiktok/tt_1777730480815.mp4 new file mode 100644 index 0000000..59dd040 Binary files /dev/null and b/downloads/tiktok/tt_1777730480815.mp4 differ diff --git a/downloads/tiktok/tt_1777736432419.mp4 b/downloads/tiktok/tt_1777736432419.mp4 new file mode 100644 index 0000000..a40f74b Binary files /dev/null and b/downloads/tiktok/tt_1777736432419.mp4 differ diff --git a/downloads/tiktok/tt_1777736571066.mp4 b/downloads/tiktok/tt_1777736571066.mp4 new file mode 100644 index 0000000..37d2fa2 Binary files /dev/null and b/downloads/tiktok/tt_1777736571066.mp4 differ diff --git a/downloads/tiktok/tt_1777815981370.mp4 b/downloads/tiktok/tt_1777815981370.mp4 new file mode 100644 index 0000000..96f5e20 Binary files /dev/null and b/downloads/tiktok/tt_1777815981370.mp4 differ diff --git a/downloads/tiktok/tt_1777816021235.mp4 b/downloads/tiktok/tt_1777816021235.mp4 new file mode 100644 index 0000000..f534dc7 Binary files /dev/null and b/downloads/tiktok/tt_1777816021235.mp4 differ diff --git a/downloads/tiktok/tt_1777816043002.mp4 b/downloads/tiktok/tt_1777816043002.mp4 new file mode 100644 index 0000000..d46b672 Binary files /dev/null and b/downloads/tiktok/tt_1777816043002.mp4 differ diff --git a/downloads/tiktok/tt_1777817519511.mp4.part b/downloads/tiktok/tt_1777817519511.mp4.part new file mode 100644 index 0000000..19f19c4 Binary files /dev/null and b/downloads/tiktok/tt_1777817519511.mp4.part differ diff --git a/downloads/tiktok/tt_1777817519513.mp4 b/downloads/tiktok/tt_1777817519513.mp4 new file mode 100644 index 0000000..94155b3 Binary files /dev/null and b/downloads/tiktok/tt_1777817519513.mp4 differ diff --git a/downloads/tiktok/tt_1777817519514.mp4.part b/downloads/tiktok/tt_1777817519514.mp4.part new file mode 100644 index 0000000..306c19f Binary files /dev/null and b/downloads/tiktok/tt_1777817519514.mp4.part differ diff --git a/downloads/tiktok/tt_1777817699114.mp4 b/downloads/tiktok/tt_1777817699114.mp4 new file mode 100644 index 0000000..f86602f Binary files /dev/null and b/downloads/tiktok/tt_1777817699114.mp4 differ diff --git a/downloads/tiktok/tt_1777817699118.mp4 b/downloads/tiktok/tt_1777817699118.mp4 new file mode 100644 index 0000000..8ac1af0 Binary files /dev/null and b/downloads/tiktok/tt_1777817699118.mp4 differ diff --git a/downloads/tiktok/tt_1777817699119.mp4 b/downloads/tiktok/tt_1777817699119.mp4 new file mode 100644 index 0000000..32eee37 Binary files /dev/null and b/downloads/tiktok/tt_1777817699119.mp4 differ diff --git a/downloads/tiktok/tt_1777817699120.mp4 b/downloads/tiktok/tt_1777817699120.mp4 new file mode 100644 index 0000000..9275871 Binary files /dev/null and b/downloads/tiktok/tt_1777817699120.mp4 differ diff --git a/downloads/tiktok/tt_1777817853639.mp4 b/downloads/tiktok/tt_1777817853639.mp4 new file mode 100644 index 0000000..f86602f Binary files /dev/null and b/downloads/tiktok/tt_1777817853639.mp4 differ diff --git a/downloads/tiktok/tt_1777817853644.mp4 b/downloads/tiktok/tt_1777817853644.mp4 new file mode 100644 index 0000000..8ac1af0 Binary files /dev/null and b/downloads/tiktok/tt_1777817853644.mp4 differ diff --git a/downloads/tiktok/tt_1777817853645.mp4 b/downloads/tiktok/tt_1777817853645.mp4 new file mode 100644 index 0000000..e8baea6 Binary files /dev/null and b/downloads/tiktok/tt_1777817853645.mp4 differ diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..4e9f827 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + "prettier/prettier": ["error", { endOfLine: "auto" }], + }, + }, +); diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/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/package.json b/package.json new file mode 100644 index 0000000..d2e7824 --- /dev/null +++ b/package.json @@ -0,0 +1,118 @@ +{ + "name": "x_news", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch --no-clear", + "start:debug": "nest start --debug --watch --no-clear", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@aws-sdk/client-sqs": "^3.1042.0", + "@bull-board/api": "^6.21.0", + "@bull-board/express": "^6.21.0", + "@bull-board/nestjs": "^6.21.0", + "@google/generative-ai": "^0.24.1", + "@keyv/redis": "^5.1.6", + "@nestjs/axios": "^4.0.1", + "@nestjs/bullmq": "^11.0.4", + "@nestjs/cache-manager": "^3.1.0", + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.1.3", + "@nestjs/swagger": "^11.3.0", + "@openrouter/sdk": "^0.12.15", + "@prisma/adapter-pg": "^7.7.0", + "@prisma/client": "^7.7.0", + "@shaivpidadi/trends-js": "^1.0.3", + "@telegraf/session": "2.0.0-beta.7", + "@twitter-api-v2/plugin-token-refresher": "^1.0.0", + "@xdevplatform/xdk": "^0.5.0", + "axios": "^1.15.0", + "bullMQAdapter": "link:@bull-board/api/bullMQAdapter", + "bullmq": "^5.73.4", + "cache-manager": "^7.2.8", + "cheerio": "^1.2.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.15.1", + "cookie": "^1.1.1", + "dotenv": "^17.4.1", + "google-trends-api": "^4.9.2", + "grammy": "^1.42.0", + "https-proxy-agent": "^9.0.0", + "install": "^0.13.0", + "ioredis": "^5.10.1", + "lodash": "^4.18.1", + "nestjs-telegraf": "^2.9.1", + "openai": "^6.34.0", + "pg": "^8.20.0", + "playwright": "^1.59.1", + "playwright-extra": "^4.3.6", + "prisma": "^7.7.0", + "puppeteer-extra-plugin-stealth": "^2.11.2", + "redis": "^5.12.1", + "reflect-metadata": "^0.2.2", + "rss-parser": "^3.13.0", + "rxjs": "^7.8.1", + "telegraf": "^4.16.3", + "twitter-api-v2": "^1.29.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/devtools-integration": "^0.2.1", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/lodash": "^4.17.24", + "@types/node": "^24.0.0", + "@types/supertest": "^7.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^17.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..8780594 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,9642 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@aws-sdk/client-sqs': + specifier: ^3.1042.0 + version: 3.1042.0 + '@bull-board/api': + specifier: ^6.21.0 + version: 6.21.0(@bull-board/ui@6.21.0) + '@bull-board/express': + specifier: ^6.21.0 + version: 6.21.0 + '@bull-board/nestjs': + specifier: ^6.21.0 + version: 6.21.0(@bull-board/api@6.21.0(@bull-board/ui@6.21.0))(@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@google/generative-ai': + specifier: ^0.24.1 + version: 0.24.1 + '@keyv/redis': + specifier: ^5.1.6 + version: 5.1.6(keyv@5.6.0) + '@nestjs/axios': + specifier: ^4.0.1 + version: 4.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.15.0)(rxjs@7.8.2) + '@nestjs/bullmq': + specifier: ^11.0.4 + version: 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(bullmq@5.73.4) + '@nestjs/cache-manager': + specifier: ^3.1.0 + version: 3.1.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) + '@nestjs/common': + specifier: ^11.0.1 + version: 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.0.1 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.0.1 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/schedule': + specifier: ^6.1.3 + version: 6.1.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/swagger': + specifier: ^11.3.0 + version: 11.3.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + '@openrouter/sdk': + specifier: ^0.12.15 + version: 0.12.15 + '@prisma/adapter-pg': + specifier: ^7.7.0 + version: 7.7.0 + '@prisma/client': + specifier: ^7.7.0 + version: 7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3) + '@shaivpidadi/trends-js': + specifier: ^1.0.3 + version: 1.0.3 + '@telegraf/session': + specifier: 2.0.0-beta.7 + version: 2.0.0-beta.7(@types/pg@8.20.0)(mysql2@3.15.3)(pg@8.20.0)(redis@5.12.1)(telegraf@4.16.3) + '@twitter-api-v2/plugin-token-refresher': + specifier: ^1.0.0 + version: 1.0.0(twitter-api-v2@1.29.0) + '@xdevplatform/xdk': + specifier: ^0.5.0 + version: 0.5.0 + axios: + specifier: ^1.15.0 + version: 1.15.0 + bullMQAdapter: + specifier: link:@bull-board/api/bullMQAdapter + version: link:@bull-board/api/bullMQAdapter + bullmq: + specifier: ^5.73.4 + version: 5.73.4 + cache-manager: + specifier: ^7.2.8 + version: 7.2.8 + cheerio: + specifier: ^1.2.0 + version: 1.2.0 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.15.1 + version: 0.15.1 + cookie: + specifier: ^1.1.1 + version: 1.1.1 + dotenv: + specifier: ^17.4.1 + version: 17.4.1 + google-trends-api: + specifier: ^4.9.2 + version: 4.9.2 + grammy: + specifier: ^1.42.0 + version: 1.42.0 + https-proxy-agent: + specifier: ^9.0.0 + version: 9.0.0 + install: + specifier: ^0.13.0 + version: 0.13.0 + ioredis: + specifier: ^5.10.1 + version: 5.10.1 + lodash: + specifier: ^4.18.1 + version: 4.18.1 + nestjs-telegraf: + specifier: ^2.9.1 + version: 2.9.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(telegraf@4.16.3)(typescript@5.9.3) + openai: + specifier: ^6.34.0 + version: 6.34.0(zod@4.3.6) + pg: + specifier: ^8.20.0 + version: 8.20.0 + playwright: + specifier: ^1.59.1 + version: 1.59.1 + playwright-extra: + specifier: ^4.3.6 + version: 4.3.6(playwright-core@1.59.1)(playwright@1.59.1) + prisma: + specifier: ^7.7.0 + version: 7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + puppeteer-extra-plugin-stealth: + specifier: ^2.11.2 + version: 2.11.2(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)) + redis: + specifier: ^5.12.1 + version: 5.12.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rss-parser: + specifier: ^3.13.0 + version: 3.13.0 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + telegraf: + specifier: ^4.16.3 + version: 4.16.3 + twitter-api-v2: + specifier: ^1.29.0 + version: 1.29.0 + devDependencies: + '@eslint/eslintrc': + specifier: ^3.2.0 + version: 3.3.5 + '@eslint/js': + specifier: ^9.18.0 + version: 9.39.4 + '@nestjs/cli': + specifier: ^11.0.0 + version: 11.0.19(@types/node@24.12.2) + '@nestjs/devtools-integration': + specifier: ^0.2.1 + version: 0.2.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/schematics': + specifier: ^11.0.0 + version: 11.0.10(chokidar@4.0.3)(typescript@5.9.3) + '@nestjs/testing': + specifier: ^11.0.1 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.18) + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/lodash': + specifier: ^4.17.24 + version: 4.17.24 + '@types/node': + specifier: ^24.0.0 + version: 24.12.2 + '@types/supertest': + specifier: ^7.0.0 + version: 7.2.0 + eslint: + specifier: ^9.18.0 + version: 9.39.4(jiti@2.6.1) + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.2 + version: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.2) + globals: + specifier: ^17.0.0 + version: 17.4.0 + jest: + specifier: ^30.0.0 + version: 30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)) + prettier: + specifier: ^3.4.2 + version: 3.8.2 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + supertest: + specifier: ^7.0.0 + version: 7.2.2 + ts-jest: + specifier: ^29.2.5 + version: 29.4.9(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)))(typescript@5.9.3) + ts-loader: + specifier: ^9.5.2 + version: 9.5.7(typescript@5.9.3)(webpack@5.106.0) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@24.12.2)(typescript@5.9.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.20.0 + version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + +packages: + + '@angular-devkit/core@19.2.23': + resolution: {integrity: sha512-RazHPQkUEsNU/OZ75w9UeHxGFMthRiuAW2B/uA7eXExBj/1meHrrBfoCA56ujW2GUxVjRtSrMjylKh4R4meiYA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.24': + resolution: {integrity: sha512-bsStZQG67J1HBqTmWxtIcobvgrn32L4UOdL7hGyOru5VxDWPNA8pRnDYavT3hnJeBkJYPoQIw8u7Dm0ecoQprw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@19.2.23': + resolution: {integrity: sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/schematics@19.2.24': + resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-sqs@3.1042.0': + resolution: {integrity: sha512-deVI/2XSD6nlsjuGM3saZZsDbWQUFWjFcg+w1vUYqdw4irgt84mTa0nP86Ko2RfZktA3CmK6TrYMqaI6SnauuQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.8': + resolution: {integrity: sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.34': + resolution: {integrity: sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.36': + resolution: {integrity: sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.38': + resolution: {integrity: sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.38': + resolution: {integrity: sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.39': + resolution: {integrity: sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.34': + resolution: {integrity: sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.38': + resolution: {integrity: sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.38': + resolution: {integrity: sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-host-header@3.972.10': + resolution: {integrity: sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.10': + resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.11': + resolution: {integrity: sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.37': + resolution: {integrity: sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-sqs@3.972.22': + resolution: {integrity: sha512-DtR3mEiOUJcnEX/QuXmvbJto6xvQzp2ftnHb29c0aQYdmmzbKf0gsu9ovx1i/yy4ZR6m0rttTucS0iiP32dlGA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.38': + resolution: {integrity: sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.6': + resolution: {integrity: sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/region-config-resolver@3.972.13': + resolution: {integrity: sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.25': + resolution: {integrity: sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1041.0': + resolution: {integrity: sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-arn-parser@3.972.3': + resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-endpoints@3.996.8': + resolution: {integrity: sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.5': + resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.972.10': + resolution: {integrity: sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==} + + '@aws-sdk/util-user-agent-node@3.973.24': + resolution: {integrity: sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==} + engines: {node: '>=20.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.972.22': + resolution: {integrity: sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@bull-board/api@6.21.0': + resolution: {integrity: sha512-5bX3U8baU4OulDLeXwqWI6/FZolpi1APfoJVXndR4fKdmuYr9cdbH8cg7juublfzX01T+3zoiZkveX7iD5y8gA==} + peerDependencies: + '@bull-board/ui': 6.21.0 + + '@bull-board/express@6.21.0': + resolution: {integrity: sha512-es5UhfDvF6YOa34mKUWUmopxmcLbNNCx5SlgeeyWQgs6WkMqahZ5kve7YCgclQomHdsycJIL3wCktXnTEQ6wRg==} + + '@bull-board/nestjs@6.21.0': + resolution: {integrity: sha512-h4UhJw9Hc4ehQcs4y+fd7CgSTyIxHN1uFttwWiFuPpMkA+t5/OcAdlB0THigjxwmL2vYgcFzuk9nKb0qHtlRkw==} + peerDependencies: + '@bull-board/api': ^6.21.0 + '@nestjs/bull-shared': ^10.0.0 || ^11.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.8.1 + + '@bull-board/ui@6.21.0': + resolution: {integrity: sha512-SemKRipdrZVqboae/Xhl7CTdIwWJ+F3G/DEP7XHi1Qt1kXZUIKJkySXlFHILunygCiHRpCJ6/Ax/XNdHI/n3QA==} + + '@cacheable/utils@2.4.1': + resolution: {integrity: sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@electric-sql/pglite-socket@0.1.1': + resolution: {integrity: sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1': + resolution: {integrity: sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==} + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': + resolution: {integrity: sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==} + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@google/generative-ai@0.24.1': + resolution: {integrity: sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==} + engines: {node: '>=18.0.0'} + + '@grammyjs/types@3.26.0': + resolution: {integrity: sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A==} + + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@ioredis/commands@1.5.1': + resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@30.3.0': + resolution: {integrity: sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.3.0': + resolution: {integrity: sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.3.0': + resolution: {integrity: sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@30.3.0': + resolution: {integrity: sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.3.0': + resolution: {integrity: sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.3.0': + resolution: {integrity: sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.3.0': + resolution: {integrity: sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.3.0': + resolution: {integrity: sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.0.1': + resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.3.0': + resolution: {integrity: sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@30.0.5': + resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.3.0': + resolution: {integrity: sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.0.1': + resolution: {integrity: sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.3.0': + resolution: {integrity: sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.3.0': + resolution: {integrity: sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@30.3.0': + resolution: {integrity: sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.3.0': + resolution: {integrity: sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@keyv/redis@5.1.6': + resolution: {integrity: sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + + '@nestjs/axios@4.0.1': + resolution: {integrity: sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + axios: ^1.3.1 + rxjs: ^7.0.0 + + '@nestjs/bull-shared@11.0.4': + resolution: {integrity: sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + + '@nestjs/bullmq@11.0.4': + resolution: {integrity: sha512-wBzK9raAVG0/6NTMdvLGM4/FQ1lsB35/pYS8L6a0SDgkTiLpd7mAjQ8R692oMx5s7IjvgntaZOuTUrKYLNfIkA==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + bullmq: ^3.0.0 || ^4.0.0 || ^5.0.0 + + '@nestjs/cache-manager@3.1.0': + resolution: {integrity: sha512-pEIqYZrBcE8UdkJmZRduurvoUfdU+3kRPeO1R2muiMbZnRuqlki5klFFNllO9LyYWzrx98bd1j0PSPKSJk1Wbw==} + peerDependencies: + '@nestjs/common': ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 || ^11.0.0 + cache-manager: '>=6' + keyv: '>=5' + rxjs: ^7.8.1 + + '@nestjs/cli@11.0.19': + resolution: {integrity: sha512-9htODqTVVNH4lJqyeIotsAgfeaYngDi020cVCd6JhJRKuOT83c/t4JDSky6+xr0lhHyNTNMgZmulxqcMNZFfrw==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + + '@nestjs/common@11.1.18': + resolution: {integrity: sha512-0sLq8Z+TIjLnz1Tqp0C/x9BpLbqpt1qEu0VcH4/fkE0y3F5JxhfK1AdKQ/SPbKhKgwqVDoY4gS8GQr2G6ujaWg==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/config@4.0.4': + resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + + '@nestjs/core@11.1.18': + resolution: {integrity: sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/devtools-integration@0.2.1': + resolution: {integrity: sha512-6UYnfMlAI6DLwNCPTjuVp1NVZd7JaczwVrdo10kb3fepz0uzcxzLAEORaPYPiSjVaH88UgfB35BR7/1p/+1/bg==} + peerDependencies: + '@nestjs/common': ^9.3.7 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^9.3.7 || ^10.0.0 || ^11.0.0 + + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/platform-express@11.1.18': + resolution: {integrity: sha512-s6GdHMTa3qx0fJewR74Xa30ysPHfBEqxIwZ7BGSTLoAEQ1vTP24urNl+b6+s49NFLEIOyeNho5fN/9/I17QlOw==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + + '@nestjs/schedule@6.1.3': + resolution: {integrity: sha512-RflMFOpR16Dwd1jAUbeB4mfGTCh65fvEdL4mSjQPJChpkRGRjIXjb+6YQcK2faQrVT60c9DmLmoVR7/ONCtuYQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + + '@nestjs/schematics@11.0.10': + resolution: {integrity: sha512-q9lr0wGwgBHLarD4uno3XiW4JX60WPlg2VTgbqPHl/6bT4u1IEEzj+q9Tad3bVnqL5mlDF3vrZ2tj+x13CJpmw==} + peerDependencies: + typescript: '>=4.8.2' + + '@nestjs/swagger@11.3.0': + resolution: {integrity: sha512-SCS8fG2DL/ZF+9l5in09FwPhpBo5i1Gdo8Se3GYlJ2cn+iNTzF7u13QjHo5XI92BN8DN+Gcug+QTcmWmGvZyNw==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/testing@11.1.18': + resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + + '@nuxt/opencollective@0.4.1': + resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} + engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} + hasBin: true + + '@nyariv/sandboxjs@0.8.25': + resolution: {integrity: sha512-Y8CAHIgVjKvG2QtVOu2hHERdpi73e7XdoX0C7MHq/sRiVvfbYFKZ9WER1GCC3aqnOK9Cxf8SDv9vWIUfhrPkhQ==} + + '@openrouter/sdk@0.12.15': + resolution: {integrity: sha512-zbUtDH/SqrNZrkNIdPTwSzZVlPRZJ0JO8k+D+gdQAE5tHrRXRL+W6BrAe78jFz+7ThmImIBQ3afBvzHhrE7ZsQ==} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@prisma/adapter-pg@7.7.0': + resolution: {integrity: sha512-q33Ta8sKbgzEpAy0lx45tAq//yMv0qcb+8nj+TCA3P4wiAY+OBFEFk/NDkZncAfHaNJeGo5WJpJdpbL+ijYx8g==} + + '@prisma/client-runtime-utils@7.7.0': + resolution: {integrity: sha512-BLyd0UpFYOtyJFTHm7jS9vesHW7P83abibodQMiIofqjBKzDHQ1VAsQkdfvXyYDkPlONPfOTz7/rv3x/+CQqvQ==} + + '@prisma/client@7.7.0': + resolution: {integrity: sha512-5Ar4OsZpJ54s21sy5oDNNW9gQtd4NuxCaiM7+JDTOU07D6VvlpLjYzAVCMB1+JzokN+08dAVomlx+b7bhJd3ww==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + peerDependencies: + prisma: '*' + typescript: '>=5.4.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@7.7.0': + resolution: {integrity: sha512-hmPI3tKLO2aP0Y5vugbjcnA9qqlfJndiT6ds4tw28U5hNHLWg+mHJEWAhjsSPgxjtmxhJ/EDIeIlyh+3Us0OPg==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.7.0': + resolution: {integrity: sha512-12J62XdqCmpiwJHhHdQxZeY3ckVCWIFmcJP8hg5dPTceeiQ0wiojXGFYTluKqFQfu46fRLgb/rLALZMAx3+dTA==} + + '@prisma/dev@0.24.3': + resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==} + + '@prisma/driver-adapter-utils@7.7.0': + resolution: {integrity: sha512-gZXREeu6mOk7zXfGFJgh86p7Vhj0sXNKp+4Cg1tWYo7V2dfncP2qxS2BiTmbIIha8xPqItkl0WSw38RuSq1HoQ==} + + '@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711': + resolution: {integrity: sha512-r51DLcJ8bDRSrBEJF3J4cinoWyGA7rfP2mG6lD90VqIbGNOkbfcLcXalSVjq5Y6brQS3vcjrq4GbyUb1Cb7vkw==} + + '@prisma/engines@7.7.0': + resolution: {integrity: sha512-7fmcbT7HHXBq/b+3h/dO1JI3fd8l8q7erf7xP7pRprh58hmSSnG8mg9K3yjW3h9WaHWUwngVFpSxxxivaitQ2w==} + + '@prisma/fetch-engine@7.7.0': + resolution: {integrity: sha512-TfyzveBQoK4xALzsTpVhB/0KG1N8zOK0ap+RnBMkzGUu3f98fnQ4QtXa2wlKPhsO2X8a3N5ugFQgcKNoHGmDfw==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.7.0': + resolution: {integrity: sha512-MEUNzvKxvYnJ7kgvd6oNRnMmmiGNS9TYLB2weMeIXplnHdL/UWEGnvavYGnN7KLJ2n0iI4dDAyzSkHI3c7AscQ==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/streams-local@0.1.2': + resolution: {integrity: sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==} + engines: {bun: '>=1.3.6', node: '>=22.0.0'} + + '@prisma/studio-core@0.27.3': + resolution: {integrity: sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==} + engines: {node: ^20.19 || ^22.12 || >=24.0, pnpm: '8'} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@redis/bloom@5.12.1': + resolution: {integrity: sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==} + engines: {node: '>= 18.19.0'} + peerDependencies: + '@redis/client': ^5.12.1 + + '@redis/client@5.11.0': + resolution: {integrity: sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==} + engines: {node: '>= 18'} + peerDependencies: + '@node-rs/xxhash': ^1.1.0 + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + + '@redis/client@5.12.1': + resolution: {integrity: sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==} + engines: {node: '>= 18.19.0'} + peerDependencies: + '@node-rs/xxhash': ^1.1.0 + '@opentelemetry/api': '>=1 <2' + peerDependenciesMeta: + '@node-rs/xxhash': + optional: true + '@opentelemetry/api': + optional: true + + '@redis/json@5.12.1': + resolution: {integrity: sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==} + engines: {node: '>= 18.19.0'} + peerDependencies: + '@redis/client': ^5.12.1 + + '@redis/search@5.12.1': + resolution: {integrity: sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==} + engines: {node: '>= 18.19.0'} + peerDependencies: + '@redis/client': ^5.12.1 + + '@redis/time-series@5.12.1': + resolution: {integrity: sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==} + engines: {node: '>= 18.19.0'} + peerDependencies: + '@redis/client': ^5.12.1 + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@shaivpidadi/trends-js@1.0.3': + resolution: {integrity: sha512-txfhmV8La+uzJpGLvGf7izub6ibNsh6tzQEYw72CHb5IDQ9Bu47RMazbbc7Ik0R3gnblRdFaJhaeEzkWZkXpXQ==} + + '@sinclair/typebox@0.34.49': + resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@15.3.1': + resolution: {integrity: sha512-oDDGPn/4jD3viZLphixgu1jwT0bqIqP25FNXC5OkWrUqHZOF4wATtSyVzluOt4DqcMqSoKMClyhUllKSxpQCng==} + + '@smithy/config-resolver@4.4.17': + resolution: {integrity: sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==} + engines: {node: '>=18.0.0'} + + '@smithy/core@3.23.17': + resolution: {integrity: sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.2.14': + resolution: {integrity: sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.3.17': + resolution: {integrity: sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==} + engines: {node: '>=18.0.0'} + + '@smithy/hash-node@4.2.14': + resolution: {integrity: sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.2.14': + resolution: {integrity: sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@4.2.2': + resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + engines: {node: '>=18.0.0'} + + '@smithy/md5-js@4.2.14': + resolution: {integrity: sha512-V2v0vx+h0iUSNG1Alt+GNBMSLGCrl9iVsdd+Ap67HPM9PN479x12V8LkuMoKImNZxn3MXeuyUjls+/7ZACZghA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-content-length@4.2.14': + resolution: {integrity: sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.4.32': + resolution: {integrity: sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.5.7': + resolution: {integrity: sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.2.20': + resolution: {integrity: sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.2.14': + resolution: {integrity: sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.3.14': + resolution: {integrity: sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==} + engines: {node: '>=18.0.0'} + + '@smithy/node-http-handler@4.6.1': + resolution: {integrity: sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==} + engines: {node: '>=18.0.0'} + + '@smithy/property-provider@4.2.14': + resolution: {integrity: sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.3.14': + resolution: {integrity: sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-builder@4.2.14': + resolution: {integrity: sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==} + engines: {node: '>=18.0.0'} + + '@smithy/querystring-parser@4.2.14': + resolution: {integrity: sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==} + engines: {node: '>=18.0.0'} + + '@smithy/service-error-classification@4.3.1': + resolution: {integrity: sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.4.9': + resolution: {integrity: sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.3.14': + resolution: {integrity: sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==} + engines: {node: '>=18.0.0'} + + '@smithy/smithy-client@4.12.13': + resolution: {integrity: sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} + engines: {node: '>=18.0.0'} + + '@smithy/url-parser@4.2.14': + resolution: {integrity: sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.3.2': + resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.2.2': + resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.2.3': + resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@4.2.2': + resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-config-provider@4.2.2': + resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-browser@4.3.49': + resolution: {integrity: sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.2.54': + resolution: {integrity: sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.4.2': + resolution: {integrity: sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-hex-encoding@4.2.2': + resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.2.14': + resolution: {integrity: sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.3.8': + resolution: {integrity: sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.5.25': + resolution: {integrity: sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-uri-escape@4.2.2': + resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@4.2.2': + resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} + engines: {node: '>=18.0.0'} + + '@smithy/uuid@1.1.2': + resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} + engines: {node: '>=18.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@telegraf/session@2.0.0-beta.7': + resolution: {integrity: sha512-467OC/VLHDC9hRqTetvXp3pBOSdgxDzYjSuWFXY9v64m453HRxE4tYCnLLFVktOvLWDm+he7Hhv/+e+lmz8atQ==} + peerDependencies: + '@types/better-sqlite3': ^7.6.9 + '@types/pg': ^8.11.0 + better-sqlite3: ^9.3.0 + kysely: 0.27.2 <1 + mongodb: ^6.3.0 + mysql2: ^3.9.0 + pg: ^8.11.3 + redis: ^4.6.12 + telegraf: '>=4.12.0' + peerDependenciesMeta: + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + better-sqlite3: + optional: true + kysely: + optional: true + mongodb: + optional: true + mysql2: + optional: true + pg: + optional: true + redis: + optional: true + + '@telegraf/types@7.1.0': + resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@twitter-api-v2/plugin-token-refresher@1.0.0': + resolution: {integrity: sha512-tN4TNDbnwn1uY6uu4g3hSj5crHTeJDOfI3on8xUBm8XaXWLrjThsZlwjpF+O1PK2lPhznmluATPHMcYU9Wf73g==} + peerDependencies: + twitter-api-v2: ^1.12.0 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/luxon@3.7.1': + resolution: {integrity: sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@types/pg@8.20.0': + resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} + + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@7.2.0': + resolution: {integrity: sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.58.1': + resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.58.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.58.1': + resolution: {integrity: sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.58.1': + resolution: {integrity: sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.58.1': + resolution: {integrity: sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.58.1': + resolution: {integrity: sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.58.1': + resolution: {integrity: sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.58.1': + resolution: {integrity: sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.58.1': + resolution: {integrity: sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.58.1': + resolution: {integrity: sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.58.1': + resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xdevplatform/xdk@0.5.0': + resolution: {integrity: sha512-Ye3yAmJOhal7wqvq6z8iJgSxq4+ytQmPFMMkyg/cPcdoeebqHZBdwTmc3T78kTbUwQy8NoCFRjgTrC+n7lmPnA==} + engines: {node: '>=16.14'} + peerDependencies: + node-fetch: ^3.3.0 + peerDependenciesMeta: + node-fetch: + optional: true + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + + babel-jest@30.3.0: + resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-plugin-istanbul@7.0.1: + resolution: {integrity: sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==} + engines: {node: '>=12'} + + babel-plugin-jest-hoist@30.3.0: + resolution: {integrity: sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.3.0: + resolution: {integrity: sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.17: + resolution: {integrity: sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + better-result@2.8.2: + resolution: {integrity: sha512-YOf0VSj5nUPI27doTtXF+BBnsiRq3qY7avHqfIWnppxTLGyvkLq1QV2RTxkwoZwJ60ywLfZ0raFF4J/G886i7A==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + + brace-expansion@1.1.13: + resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==} + + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + + buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + + buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bullmq@5.73.4: + resolution: {integrity: sha512-Q+NeFLtdKSD3GDPYSX4pH+Mc9E4OZVKimXwrnZ5WmndNy31COMy4vQV9zfhgfHGSUFrlpsBicfKYbSjx9FbO+A==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + cache-manager@7.2.8: + resolution: {integrity: sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001787: + resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.15.1: + resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-deep@0.2.4: + resolution: {integrity: sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==} + engines: {node: '>=0.10.0'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-json@4.6.2: + resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} + engines: {node: '>= 6'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + + cron@4.4.0: + resolution: {integrity: sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==} + engines: {node: '>=18.x'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + effect@3.20.0: + resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.335: + resolution: {integrity: sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + + enhanced-resolve@5.20.1: + resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} + engines: {node: '>=10.13.0'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.5: + resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + + expect@30.3.0: + resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fast-xml-builder@1.1.8: + resolution: {integrity: sha512-sDVBc2gg8pSKvcbE8rBmOyjSGQf0AdsbqvHeIOv3D/uYNoV4eCReQXyDF8Pdv8+m1FHazACypSz2hR7O2S1LLw==} + + fast-xml-parser@5.7.2: + resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} + hasBin: true + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-in@0.1.8: + resolution: {integrity: sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==} + engines: {node: '>=0.10.0'} + + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + for-own@0.1.5: + resolution: {integrity: sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==} + engines: {node: '>=0.10.0'} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.4.0: + resolution: {integrity: sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==} + engines: {node: '>=18'} + + google-trends-api@4.9.2: + resolution: {integrity: sha512-gjVSHCM8B7LyAAUpXb4B0/TfnmpwQ2z1w/mQ2bL0AKpr2j3gLS1j2YOnifpfsGJRxAGXB/NoC+nGwC5qSnZIiA==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grammex@3.1.12: + resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==} + + grammy@1.42.0: + resolution: {integrity: sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g==} + engines: {node: ^12.20.0 || >=14.13.1} + + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.12.12: + resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} + engines: {node: '>=16.9.0'} + + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + + https-proxy-agent@9.0.0: + resolution: {integrity: sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==} + engines: {node: '>= 20'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + install@0.13.0: + resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} + engines: {node: '>= 0.10'} + + ioredis@5.10.1: + resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} + engines: {node: '>=12.22.0'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jest-changed-files@30.3.0: + resolution: {integrity: sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.3.0: + resolution: {integrity: sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.3.0: + resolution: {integrity: sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.3.0: + resolution: {integrity: sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.3.0: + resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.2.0: + resolution: {integrity: sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.3.0: + resolution: {integrity: sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@30.3.0: + resolution: {integrity: sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-haste-map@30.3.0: + resolution: {integrity: sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.3.0: + resolution: {integrity: sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.3.0: + resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@30.3.0: + resolution: {integrity: sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@30.3.0: + resolution: {integrity: sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@30.0.1: + resolution: {integrity: sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.3.0: + resolution: {integrity: sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.3.0: + resolution: {integrity: sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.3.0: + resolution: {integrity: sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.3.0: + resolution: {integrity: sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.3.0: + resolution: {integrity: sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.3.0: + resolution: {integrity: sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.3.0: + resolution: {integrity: sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.3.0: + resolution: {integrity: sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@30.3.0: + resolution: {integrity: sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.3.0: + resolution: {integrity: sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} + + kind-of@2.0.1: + resolution: {integrity: sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==} + engines: {node: '>=0.10.0'} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + lazy-cache@0.2.7: + resolution: {integrity: sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==} + engines: {node: '>=0.10.0'} + + lazy-cache@1.0.4: + resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==} + engines: {node: '>=0.10.0'} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libphonenumber-js@1.12.41: + resolution: {integrity: sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + loader-runner@4.3.1: + resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} + engines: {node: '>=6.11.5'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.3.3: + resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru.min@1.1.4: + resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + luxon@3.7.2: + resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} + engines: {node: '>=12'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + merge-deep@3.0.3: + resolution: {integrity: sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==} + engines: {node: '>=0.10.0'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mixin-object@2.0.1: + resolution: {integrity: sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==} + engines: {node: '>=0.10.0'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nestjs-telegraf@2.9.1: + resolution: {integrity: sha512-iFy0beaRvEJWo4rtIfLK6mbrxlrhPsDKoXT/yyQDRfu9lmFKjada4bbR3COXua6VdbMd/NXIzMqSukJRsTkIjQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.2.2 + telegraf: ^4.0.0 + typescript: ^4.1.2 || ^5.0.0 + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nypm@0.6.5: + resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} + engines: {node: '>=18'} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + openai@6.34.0: + resolution: {integrity: sha512-yEr2jdGf4tVFYG6ohmr3pF6VJuveP0EA/sS8TBx+4Eq5NT10alu5zg2dmxMXMgqpihRDQlFGpRt2XwsGj+Fyxw==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-timeout@4.1.0: + resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} + engines: {node: '>=14.0.0'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} + engines: {node: '>=18'} + hasBin: true + + playwright-extra@4.3.6: + resolution: {integrity: sha512-q2rVtcE8V8K3vPVF1zny4pvwZveHLH8KBuVU2MoE3Jw4OKVoBWsHI9CH9zPydovHHOCDxjGN2Vg+2m644q3ijA==} + engines: {node: '>=12'} + peerDependencies: + playwright: '*' + playwright-core: '*' + peerDependenciesMeta: + playwright: + optional: true + playwright-core: + optional: true + + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} + engines: {node: '>=18'} + hasBin: true + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.8.2: + resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@30.3.0: + resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + prisma@7.7.0: + resolution: {integrity: sha512-HlgwRBt1uEFB9LStHL4HLYDvoi4BNu1rYA0hPG0zCAEyK9SaZBqp7E5Rjpc3Qh8Lex/ye/svoHZ0OWoFNhWxuQ==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + hasBin: true + peerDependencies: + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' + peerDependenciesMeta: + better-sqlite3: + optional: true + typescript: + optional: true + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + puppeteer-extra-plugin-stealth@2.11.2: + resolution: {integrity: sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ==} + engines: {node: '>=8'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + + puppeteer-extra-plugin-user-data-dir@2.4.1: + resolution: {integrity: sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g==} + engines: {node: '>=8'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + + puppeteer-extra-plugin-user-preferences@2.4.1: + resolution: {integrity: sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A==} + engines: {node: '>=8'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + + puppeteer-extra-plugin@3.2.3: + resolution: {integrity: sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q==} + engines: {node: '>=9.11.2'} + peerDependencies: + playwright-extra: '*' + puppeteer-extra: '*' + peerDependenciesMeta: + playwright-extra: + optional: true + puppeteer-extra: + optional: true + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-info@3.1.0: + resolution: {integrity: sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + redis@5.12.1: + resolution: {integrity: sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==} + engines: {node: '>= 18.19.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rss-parser@3.13.0: + resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-compare@1.1.4: + resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sandwich-stream@2.0.2: + resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==} + engines: {node: '>= 0.10'} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-clone@0.1.2: + resolution: {integrity: sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + swagger-ui-dist@5.32.4: + resolution: {integrity: sha512-0AADFFQNJzExEN49SrD/34Nn9cxNxVLiydYl2MBwSZFPVXNkVwC/EFAjoezGGqE8oDegiDC+p47t8lKObCinMQ==} + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + synckit@0.11.12: + resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + engines: {node: ^14.18.0 || >=16.0.0} + + tapable@2.3.2: + resolution: {integrity: sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==} + engines: {node: '>=6'} + + telegraf@4.16.3: + resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==} + engines: {node: ^12.20.0 || >=14.13.1} + hasBin: true + + terser-webpack-plugin@5.4.0: + resolution: {integrity: sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.46.1: + resolution: {integrity: sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-jest@29.4.9: + resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <7' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-loader@9.5.7: + resolution: {integrity: sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + twitter-api-v2@1.29.0: + resolution: {integrity: sha512-v473q5bwme4N+DWSg6qY+JCvfg1nSJRWwui3HUALafxfqCvVkKiYmS/5x/pVeJwTmyeBxexMbzHwnzrH4h6oYQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript-eslint@8.58.1: + resolution: {integrity: sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.5.1: + resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.3.4: + resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} + engines: {node: '>=10.13.0'} + + webpack@5.106.0: + resolution: {integrity: sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + +snapshots: + + '@angular-devkit/core@19.2.23(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.24(@types/node@24.12.2)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@24.12.2) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.23(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.8 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-sqs@3.1042.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.8 + '@aws-sdk/credential-provider-node': 3.972.39 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-sdk-sqs': 3.972.22 + '@aws-sdk/middleware-user-agent': 3.972.38 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.24 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/md5-js': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.7 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.974.8': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.22 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.34': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.36': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.1 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.25 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/credential-provider-env': 3.972.34 + '@aws-sdk/credential-provider-http': 3.972.36 + '@aws-sdk/credential-provider-login': 3.972.38 + '@aws-sdk/credential-provider-process': 3.972.34 + '@aws-sdk/credential-provider-sso': 3.972.38 + '@aws-sdk/credential-provider-web-identity': 3.972.38 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-login@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-node@3.972.39': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.34 + '@aws-sdk/credential-provider-http': 3.972.36 + '@aws-sdk/credential-provider-ini': 3.972.38 + '@aws-sdk/credential-provider-process': 3.972.34 + '@aws-sdk/credential-provider-sso': 3.972.38 + '@aws-sdk/credential-provider-web-identity': 3.972.38 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-process@3.972.34': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/token-providers': 3.1041.0 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/middleware-host-header@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.11': + dependencies: + '@aws-sdk/types': 3.973.8 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.37': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-arn-parser': 3.972.3 + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-sqs@3.972.22': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@smithy/core': 3.23.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-retry': 4.3.8 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.6': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.8 + '@aws-sdk/middleware-host-header': 3.972.10 + '@aws-sdk/middleware-logger': 3.972.10 + '@aws-sdk/middleware-recursion-detection': 3.972.11 + '@aws-sdk/middleware-user-agent': 3.972.38 + '@aws-sdk/region-config-resolver': 3.972.13 + '@aws-sdk/signature-v4-multi-region': 3.996.25 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.8 + '@aws-sdk/util-user-agent-browser': 3.972.10 + '@aws-sdk/util-user-agent-node': 3.973.24 + '@smithy/config-resolver': 4.4.17 + '@smithy/core': 3.23.17 + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/hash-node': 4.2.14 + '@smithy/invalid-dependency': 4.2.14 + '@smithy/middleware-content-length': 4.2.14 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-retry': 4.5.7 + '@smithy/middleware-serde': 4.2.20 + '@smithy/middleware-stack': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/node-http-handler': 4.6.1 + '@smithy/protocol-http': 5.3.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-body-length-node': 4.2.3 + '@smithy/util-defaults-mode-browser': 4.3.49 + '@smithy/util-defaults-mode-node': 4.2.54 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/region-config-resolver@3.972.13': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/config-resolver': 4.4.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.25': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.972.37 + '@aws-sdk/types': 3.973.8 + '@smithy/protocol-http': 5.3.14 + '@smithy/signature-v4': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1041.0': + dependencies: + '@aws-sdk/core': 3.974.8 + '@aws-sdk/nested-clients': 3.997.6 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/types@3.973.8': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.972.3': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.996.8': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-endpoints': 3.4.2 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.5': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.972.10': + dependencies: + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.24': + dependencies: + '@aws-sdk/middleware-user-agent': 3.972.38 + '@aws-sdk/types': 3.973.8 + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.22': + dependencies: + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.1 + fast-xml-parser: 5.7.2 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@borewit/text-codec@0.2.2': {} + + '@bull-board/api@6.21.0(@bull-board/ui@6.21.0)': + dependencies: + '@bull-board/ui': 6.21.0 + redis-info: 3.1.0 + + '@bull-board/express@6.21.0': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + '@bull-board/ui': 6.21.0 + ejs: 3.1.10 + express: 5.2.1 + transitivePeerDependencies: + - supports-color + + '@bull-board/nestjs@6.21.0(@bull-board/api@6.21.0(@bull-board/ui@6.21.0))(@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + + '@bull-board/ui@6.21.0': + dependencies: + '@bull-board/api': 6.21.0(@bull-board/ui@6.21.0) + + '@cacheable/utils@2.4.1': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@colors/colors@1.5.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@electric-sql/pglite-socket@0.1.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': {} + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': + dependencies: + eslint: 9.39.4(jiti@2.6.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.14.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@google/generative-ai@0.24.1': {} + + '@grammyjs/types@3.26.0': {} + + '@hono/node-server@1.19.11(hono@4.12.12)': + dependencies: + hono: 4.12.12 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@24.12.2)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.12.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/confirm@5.1.21(@types/node@24.12.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/type': 3.0.10(@types/node@24.12.2) + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/core@10.3.2(@types/node@24.12.2)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.12.2) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/editor@4.2.23(@types/node@24.12.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/external-editor': 1.0.3(@types/node@24.12.2) + '@inquirer/type': 3.0.10(@types/node@24.12.2) + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/expand@4.0.23(@types/node@24.12.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/type': 3.0.10(@types/node@24.12.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/external-editor@1.0.3(@types/node@24.12.2)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@24.12.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/type': 3.0.10(@types/node@24.12.2) + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/number@3.0.23(@types/node@24.12.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/type': 3.0.10(@types/node@24.12.2) + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/password@4.0.23(@types/node@24.12.2)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/type': 3.0.10(@types/node@24.12.2) + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/prompts@7.10.1(@types/node@24.12.2)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.12.2) + '@inquirer/confirm': 5.1.21(@types/node@24.12.2) + '@inquirer/editor': 4.2.23(@types/node@24.12.2) + '@inquirer/expand': 4.0.23(@types/node@24.12.2) + '@inquirer/input': 4.3.1(@types/node@24.12.2) + '@inquirer/number': 3.0.23(@types/node@24.12.2) + '@inquirer/password': 4.0.23(@types/node@24.12.2) + '@inquirer/rawlist': 4.1.11(@types/node@24.12.2) + '@inquirer/search': 3.2.2(@types/node@24.12.2) + '@inquirer/select': 4.4.2(@types/node@24.12.2) + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/prompts@7.3.2(@types/node@24.12.2)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.12.2) + '@inquirer/confirm': 5.1.21(@types/node@24.12.2) + '@inquirer/editor': 4.2.23(@types/node@24.12.2) + '@inquirer/expand': 4.0.23(@types/node@24.12.2) + '@inquirer/input': 4.3.1(@types/node@24.12.2) + '@inquirer/number': 3.0.23(@types/node@24.12.2) + '@inquirer/password': 4.0.23(@types/node@24.12.2) + '@inquirer/rawlist': 4.1.11(@types/node@24.12.2) + '@inquirer/search': 3.2.2(@types/node@24.12.2) + '@inquirer/select': 4.4.2(@types/node@24.12.2) + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/rawlist@4.1.11(@types/node@24.12.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/type': 3.0.10(@types/node@24.12.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/search@3.2.2(@types/node@24.12.2)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.12.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/select@4.4.2(@types/node@24.12.2)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.12.2) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.12.2) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.12.2 + + '@inquirer/type@3.0.10(@types/node@24.12.2)': + optionalDependencies: + '@types/node': 24.12.2 + + '@ioredis/commands@1.5.1': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + chalk: 4.1.2 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + slash: 3.0.0 + + '@jest/core@30.3.0(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.3.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.4.0 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.3.0 + jest-config: 30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)) + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-resolve-dependencies: 30.3.0 + jest-runner: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + jest-watcher: 30.3.0 + pretty-format: 30.3.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.3.0': {} + + '@jest/environment@30.3.0': + dependencies: + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + jest-mock: 30.3.0 + + '@jest/expect-utils@30.3.0': + dependencies: + '@jest/get-type': 30.1.0 + + '@jest/expect@30.3.0': + dependencies: + expect: 30.3.0 + jest-snapshot: 30.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@30.3.0': + dependencies: + '@jest/types': 30.3.0 + '@sinonjs/fake-timers': 15.3.1 + '@types/node': 24.12.2 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + + '@jest/get-type@30.1.0': {} + + '@jest/globals@30.3.0': + dependencies: + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/types': 30.3.0 + jest-mock: 30.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.0.1': + dependencies: + '@types/node': 24.12.2 + jest-regex-util: 30.0.1 + + '@jest/reporters@30.3.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 24.12.2 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 10.5.0 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + jest-worker: 30.3.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@30.0.5': + dependencies: + '@sinclair/typebox': 0.34.49 + + '@jest/snapshot-utils@30.3.0': + dependencies: + '@jest/types': 30.3.0 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.0.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.3.0': + dependencies: + '@jest/console': 30.3.0 + '@jest/types': 30.3.0 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@30.3.0': + dependencies: + '@jest/test-result': 30.3.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + slash: 3.0.0 + + '@jest/transform@30.3.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 30.3.0 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 7.0.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@30.3.0': + dependencies: + '@jest/pattern': 30.0.1 + '@jest/schemas': 30.0.5 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.12.2 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@keyv/redis@5.1.6(keyv@5.6.0)': + dependencies: + '@redis/client': 5.11.0 + cluster-key-slot: 1.1.2 + hookified: 1.15.1 + keyv: 5.6.0 + transitivePeerDependencies: + - '@node-rs/xxhash' + + '@keyv/serialize@1.1.1': {} + + '@kurkle/color@0.3.4': {} + + '@lukeed/csprng@1.1.0': {} + + '@microsoft/tsdoc@0.16.0': {} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@nestjs/axios@4.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.15.0)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + axios: 1.15.0 + rxjs: 7.8.2 + + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(bullmq@5.73.4)': + dependencies: + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.73.4 + tslib: 2.8.1 + + '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cache-manager: 7.2.8 + keyv: 5.6.0 + rxjs: 7.8.2 + + '@nestjs/cli@11.0.19(@types/node@24.12.2)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.24(@types/node@24.12.2)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@24.12.2) + '@nestjs/schematics': 11.0.10(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0) + glob: 13.0.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.9.3 + webpack: 5.106.0 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - '@types/node' + - esbuild + - uglify-js + - webpack-cli + + '@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.15.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/config@4.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 + dotenv-expand: 12.0.3 + lodash: 4.18.1 + rxjs: 7.8.2 + + '@nestjs/core@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + + '@nestjs/devtools-integration@0.2.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nyariv/sandboxjs': 0.8.25 + chalk: 4.1.2 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.15.1 + + '@nestjs/platform-express@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/schedule@6.1.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cron: 4.4.0 + + '@nestjs/schematics@11.0.10(chokidar@4.0.3)(typescript@5.9.3)': + dependencies: + '@angular-devkit/core': 19.2.23(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.23(chokidar@4.0.3) + comment-json: 4.6.2 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - chokidar + + '@nestjs/swagger@11.3.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.4 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.15.1 + + '@nestjs/testing@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.18)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + + '@noble/hashes@1.8.0': {} + + '@nodable/entities@2.1.0': {} + + '@nuxt/opencollective@0.4.1': + dependencies: + consola: 3.4.2 + + '@nyariv/sandboxjs@0.8.25': {} + + '@openrouter/sdk@0.12.15': + dependencies: + zod: 4.3.6 + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.2.9': {} + + '@prisma/adapter-pg@7.7.0': + dependencies: + '@prisma/driver-adapter-utils': 7.7.0 + '@types/pg': 8.20.0 + pg: 8.20.0 + postgres-array: 3.0.4 + transitivePeerDependencies: + - pg-native + + '@prisma/client-runtime-utils@7.7.0': {} + + '@prisma/client@7.7.0(prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@prisma/client-runtime-utils': 7.7.0 + optionalDependencies: + prisma: 7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@7.7.0': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.20.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.7.0': {} + + '@prisma/dev@0.24.3(typescript@5.9.3)': + dependencies: + '@electric-sql/pglite': 0.4.1 + '@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1) + '@electric-sql/pglite-tools': 0.3.1(@electric-sql/pglite@0.4.1) + '@hono/node-server': 1.19.11(hono@4.12.12) + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + '@prisma/streams-local': 0.1.2 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.12.12 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.9.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.7.0': + dependencies: + '@prisma/debug': 7.7.0 + + '@prisma/engines-version@7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711': {} + + '@prisma/engines@7.7.0': + dependencies: + '@prisma/debug': 7.7.0 + '@prisma/engines-version': 7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711 + '@prisma/fetch-engine': 7.7.0 + '@prisma/get-platform': 7.7.0 + + '@prisma/fetch-engine@7.7.0': + dependencies: + '@prisma/debug': 7.7.0 + '@prisma/engines-version': 7.6.0-1.75cbdc1eb7150937890ad5465d861175c6624711 + '@prisma/get-platform': 7.7.0 + + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.7.0': + dependencies: + '@prisma/debug': 7.7.0 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/streams-local@0.1.2': + dependencies: + ajv: 8.18.0 + better-result: 2.8.2 + env-paths: 3.0.0 + proper-lockfile: 4.1.2 + + '@prisma/studio-core@0.27.3(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-toggle': 1.1.10(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@types/react': 19.2.14 + chart.js: 4.5.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + transitivePeerDependencies: + - '@types/react-dom' + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-primitive@2.1.3(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-toggle@1.1.10(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@redis/bloom@5.12.1(@redis/client@5.12.1)': + dependencies: + '@redis/client': 5.12.1 + + '@redis/client@5.11.0': + dependencies: + cluster-key-slot: 1.1.2 + + '@redis/client@5.12.1': + dependencies: + cluster-key-slot: 1.1.2 + + '@redis/json@5.12.1(@redis/client@5.12.1)': + dependencies: + '@redis/client': 5.12.1 + + '@redis/search@5.12.1(@redis/client@5.12.1)': + dependencies: + '@redis/client': 5.12.1 + + '@redis/time-series@5.12.1(@redis/client@5.12.1)': + dependencies: + '@redis/client': 5.12.1 + + '@scarf/scarf@1.4.0': {} + + '@shaivpidadi/trends-js@1.0.3': {} + + '@sinclair/typebox@0.34.49': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@15.3.1': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@smithy/config-resolver@4.4.17': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-config-provider': 4.2.2 + '@smithy/util-endpoints': 3.4.2 + '@smithy/util-middleware': 4.2.14 + tslib: 2.8.1 + + '@smithy/core@3.23.17': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-base64': 4.3.2 + '@smithy/util-body-length-browser': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-stream': 4.5.25 + '@smithy/util-utf8': 4.2.2 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.2.14': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.3.17': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + tslib: 2.8.1 + + '@smithy/hash-node@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/middleware-content-length@4.2.14': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.4.32': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/middleware-serde': 4.2.20 + '@smithy/node-config-provider': 4.3.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + '@smithy/url-parser': 4.2.14 + '@smithy/util-middleware': 4.2.14 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.5.7': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/node-config-provider': 4.3.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/service-error-classification': 4.3.1 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-retry': 4.3.8 + '@smithy/uuid': 1.1.2 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.2.20': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.3.14': + dependencies: + '@smithy/property-provider': 4.2.14 + '@smithy/shared-ini-file-loader': 4.4.9 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/node-http-handler@4.6.1': + dependencies: + '@smithy/protocol-http': 5.3.14 + '@smithy/querystring-builder': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/property-provider@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/protocol-http@5.3.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/querystring-builder@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + '@smithy/util-uri-escape': 4.2.2 + tslib: 2.8.1 + + '@smithy/querystring-parser@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/service-error-classification@4.3.1': + dependencies: + '@smithy/types': 4.14.1 + + '@smithy/shared-ini-file-loader@4.4.9': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/signature-v4@5.3.14': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-middleware': 4.2.14 + '@smithy/util-uri-escape': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/smithy-client@4.12.13': + dependencies: + '@smithy/core': 3.23.17 + '@smithy/middleware-endpoint': 4.4.32 + '@smithy/middleware-stack': 4.2.14 + '@smithy/protocol-http': 5.3.14 + '@smithy/types': 4.14.1 + '@smithy/util-stream': 4.5.25 + tslib: 2.8.1 + + '@smithy/types@4.14.1': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@4.2.14': + dependencies: + '@smithy/querystring-parser': 4.2.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-base64@4.3.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.2.3': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@4.2.2': + dependencies: + '@smithy/is-array-buffer': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-config-provider@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@4.3.49': + dependencies: + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.2.54': + dependencies: + '@smithy/config-resolver': 4.4.17 + '@smithy/credential-provider-imds': 4.2.14 + '@smithy/node-config-provider': 4.3.14 + '@smithy/property-provider': 4.2.14 + '@smithy/smithy-client': 4.12.13 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.4.2': + dependencies: + '@smithy/node-config-provider': 4.3.14 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@4.2.14': + dependencies: + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-retry@4.3.8': + dependencies: + '@smithy/service-error-classification': 4.3.1 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@smithy/util-stream@4.5.25': + dependencies: + '@smithy/fetch-http-handler': 5.3.17 + '@smithy/node-http-handler': 4.6.1 + '@smithy/types': 4.14.1 + '@smithy/util-base64': 4.3.2 + '@smithy/util-buffer-from': 4.2.2 + '@smithy/util-hex-encoding': 4.2.2 + '@smithy/util-utf8': 4.2.2 + tslib: 2.8.1 + + '@smithy/util-uri-escape@4.2.2': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@4.2.2': + dependencies: + '@smithy/util-buffer-from': 4.2.2 + tslib: 2.8.1 + + '@smithy/uuid@1.1.2': + dependencies: + tslib: 2.8.1 + + '@standard-schema/spec@1.1.0': {} + + '@telegraf/session@2.0.0-beta.7(@types/pg@8.20.0)(mysql2@3.15.3)(pg@8.20.0)(redis@5.12.1)(telegraf@4.16.3)': + dependencies: + telegraf: 4.16.3 + optionalDependencies: + '@types/pg': 8.20.0 + mysql2: 3.15.3 + pg: 8.20.0 + redis: 5.12.1 + + '@telegraf/types@7.1.0': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@twitter-api-v2/plugin-token-refresher@1.0.0(twitter-api-v2@1.29.0)': + dependencies: + twitter-api-v2: 1.29.0 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.12.2 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.12.2 + + '@types/cookiejar@2.1.5': {} + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.8 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 24.12.2 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@30.0.0': + dependencies: + expect: 30.3.0 + pretty-format: 30.3.0 + + '@types/json-schema@7.0.15': {} + + '@types/lodash@4.17.24': {} + + '@types/luxon@3.7.1': {} + + '@types/methods@1.1.4': {} + + '@types/ms@2.1.0': {} + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@types/pg@8.20.0': + dependencies: + '@types/node': 24.12.2 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 24.12.2 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.12.2 + + '@types/stack-utils@2.0.3': {} + + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 24.12.2 + form-data: 4.0.5 + + '@types/supertest@7.2.0': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + + '@types/validator@13.15.10': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.1 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.58.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) + '@typescript-eslint/types': 8.58.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.58.1': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.58.1': {} + + '@typescript-eslint/typescript-estree@8.58.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.58.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.9.3) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.58.1': + dependencies: + '@typescript-eslint/types': 8.58.1 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xdevplatform/xdk@0.5.0': {} + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-phases@1.0.4(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + agent-base@9.0.0: {} + + ajv-formats@2.1.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@3.5.2(ajv@6.14.0): + dependencies: + ajv: 6.14.0 + + ajv-keywords@5.1.0(ajv@8.18.0): + dependencies: + ajv: 8.18.0 + fast-deep-equal: 3.1.3 + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + append-field@1.0.0: {} + + arg@4.1.3: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + arr-union@3.1.0: {} + + array-timsort@1.0.3: {} + + asap@2.0.6: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + aws-ssl-profiles@1.1.2: {} + + axios@1.15.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + babel-jest@30.3.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 7.0.1 + babel-preset-jest: 30.3.0(@babel/core@7.29.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@7.0.1: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.3.0: + dependencies: + '@types/babel__core': 7.20.5 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + + babel-preset-jest@30.3.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + babel-plugin-jest-hoist: 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.17: {} + + better-result@2.8.2: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + boolbase@1.0.0: {} + + bowser@2.14.1: {} + + brace-expansion@1.1.13: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.3: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.17 + caniuse-lite: 1.0.30001787 + electron-to-chromium: 1.5.335 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-alloc-unsafe@1.1.0: {} + + buffer-alloc@1.2.0: + dependencies: + buffer-alloc-unsafe: 1.1.0 + buffer-fill: 1.0.0 + + buffer-fill@1.0.0: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bullmq@5.73.4: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.10.1 + msgpackr: 1.11.5 + node-abort-controller: 3.1.1 + semver: 7.7.4 + tslib: 2.8.1 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 16.6.1 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + + cache-manager@7.2.8: + dependencies: + '@cacheable/utils': 2.4.1 + keyv: 5.6.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001787: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chardet@2.1.1: {} + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.25.0 + whatwg-mimetype: 4.0.0 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chrome-trace-event@1.0.4: {} + + ci-info@4.4.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.2: {} + + cjs-module-lexer@2.2.0: {} + + class-transformer@0.5.1: {} + + class-validator@0.15.1: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.41 + validator: 13.15.35 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-deep@0.2.4: + dependencies: + for-own: 0.1.5 + is-plain-object: 2.0.4 + kind-of: 3.2.2 + lazy-cache: 1.0.4 + shallow-clone: 0.1.2 + + clone@1.0.4: {} + + cluster-key-slot@1.1.2: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.3: {} + + commander@4.1.1: {} + + comment-json@4.6.2: + dependencies: + array-timsort: 1.0.3 + esprima: 4.0.1 + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + confbox@0.2.4: {} + + consola@3.4.2: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cookiejar@2.1.4: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + create-require@1.1.1: {} + + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + + cron@4.4.0: + dependencies: + '@types/luxon': 3.7.1 + luxon: 3.7.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.2: {} + + deep-is@0.1.4: {} + + deepmerge-ts@7.1.5: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defu@6.1.7: {} + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: + optional: true + + detect-newline@3.1.0: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diff@4.0.4: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dotenv@17.4.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + effect@3.20.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-to-chromium@1.5.335: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + encodeurl@2.0.0: {} + + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + enhanced-resolve@5.20.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.2 + + entities@2.2.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + env-paths@3.0.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.0.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + + eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1))(prettier@3.8.2): + dependencies: + eslint: 9.39.4(jiti@2.6.1) + prettier: 3.8.2 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.12 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 10.1.8(eslint@9.39.4(jiti@2.6.1)) + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.6.1): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-x@0.2.2: {} + + expect@30.3.0: + dependencies: + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-util: 30.3.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.0.8: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.1.0: {} + + fast-xml-builder@1.1.8: + dependencies: + path-expression-matcher: 1.5.0 + + fast-xml-parser@5.7.2: + dependencies: + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.1.8 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + follow-redirects@1.15.11: {} + + for-in@0.1.8: {} + + for-in@1.0.2: {} + + for-own@0.1.5: + dependencies: + for-in: 1.0.2 + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0): + dependencies: + '@babel/code-frame': 7.29.0 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.5 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.7.4 + tapable: 2.3.2 + typescript: 5.9.3 + webpack: 5.106.0 + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs-monkey@1.1.0: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-port-please@3.2.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.7 + node-fetch-native: 1.6.7 + nypm: 0.6.5 + pathe: 2.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@14.0.0: {} + + globals@17.4.0: {} + + google-trends-api@4.9.2: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + grammex@3.1.12: {} + + grammy@1.42.0: + dependencies: + '@grammyjs/types': 3.26.0 + abort-controller: 3.0.0 + debug: 4.4.3 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + - supports-color + + graphmatch@1.1.1: {} + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.12.12: {} + + hookified@1.15.1: {} + + html-escaper@2.0.2: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-status-codes@2.3.0: {} + + https-proxy-agent@9.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + install@0.13.0: {} + + ioredis@5.10.1: + dependencies: + '@ioredis/commands': 1.5.1 + cluster-key-slot: 1.1.2 + debug: 4.4.3 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-buffer@1.1.6: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-promise@4.0.0: {} + + is-property@1.0.2: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + isobject@3.0.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterare@1.2.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + + jest-changed-files@30.3.0: + dependencies: + execa: 5.1.1 + jest-util: 30.3.0 + p-limit: 3.1.0 + + jest-circus@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/expect': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2 + is-generator-fn: 2.1.0 + jest-each: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-runtime: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + p-limit: 3.1.0 + pretty-format: 30.3.0 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)) + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)) + jest-util: 30.3.0 + jest-validate: 30.3.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.0 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) + chalk: 4.1.2 + ci-info: 4.4.0 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.3.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-runner: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + parse-json: 5.2.0 + pretty-format: 30.3.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.12.2 + ts-node: 10.9.2(@types/node@24.12.2)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.3.0: + dependencies: + '@jest/diff-sequences': 30.3.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.3.0 + + jest-docblock@30.2.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 + chalk: 4.1.2 + jest-util: 30.3.0 + pretty-format: 30.3.0 + + jest-environment-node@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + jest-mock: 30.3.0 + jest-util: 30.3.0 + jest-validate: 30.3.0 + + jest-haste-map@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 30.0.1 + jest-util: 30.3.0 + jest-worker: 30.3.0 + picomatch: 4.0.4 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + pretty-format: 30.3.0 + + jest-matcher-utils@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + jest-diff: 30.3.0 + pretty-format: 30.3.0 + + jest-message-util@30.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/types': 30.3.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + pretty-format: 30.3.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + jest-util: 30.3.0 + + jest-pnp-resolver@1.2.3(jest-resolve@30.3.0): + optionalDependencies: + jest-resolve: 30.3.0 + + jest-regex-util@30.0.1: {} + + jest-resolve-dependencies@30.3.0: + dependencies: + jest-regex-util: 30.0.1 + jest-snapshot: 30.3.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.3.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-pnp-resolver: 1.2.3(jest-resolve@30.3.0) + jest-util: 30.3.0 + jest-validate: 30.3.0 + slash: 3.0.0 + unrs-resolver: 1.11.1 + + jest-runner@30.3.0: + dependencies: + '@jest/console': 30.3.0 + '@jest/environment': 30.3.0 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.2.0 + jest-environment-node: 30.3.0 + jest-haste-map: 30.3.0 + jest-leak-detector: 30.3.0 + jest-message-util: 30.3.0 + jest-resolve: 30.3.0 + jest-runtime: 30.3.0 + jest-util: 30.3.0 + jest-watcher: 30.3.0 + jest-worker: 30.3.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.3.0: + dependencies: + '@jest/environment': 30.3.0 + '@jest/fake-timers': 30.3.0 + '@jest/globals': 30.3.0 + '@jest/source-map': 30.0.1 + '@jest/test-result': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + chalk: 4.1.2 + cjs-module-lexer: 2.2.0 + collect-v8-coverage: 1.0.3 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.3.0 + jest-message-util: 30.3.0 + jest-mock: 30.3.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.3.0 + jest-snapshot: 30.3.0 + jest-util: 30.3.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.3.0: + dependencies: + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 + '@jest/expect-utils': 30.3.0 + '@jest/get-type': 30.1.0 + '@jest/snapshot-utils': 30.3.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + chalk: 4.1.2 + expect: 30.3.0 + graceful-fs: 4.2.11 + jest-diff: 30.3.0 + jest-matcher-utils: 30.3.0 + jest-message-util: 30.3.0 + jest-util: 30.3.0 + pretty-format: 30.3.0 + semver: 7.7.4 + synckit: 0.11.12 + transitivePeerDependencies: + - supports-color + + jest-util@30.3.0: + dependencies: + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.4 + + jest-validate@30.3.0: + dependencies: + '@jest/get-type': 30.1.0 + '@jest/types': 30.3.0 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.3.0 + + jest-watcher@30.3.0: + dependencies: + '@jest/test-result': 30.3.0 + '@jest/types': 30.3.0 + '@types/node': 24.12.2 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.3.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 24.12.2 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@30.3.0: + dependencies: + '@types/node': 24.12.2 + '@ungap/structured-clone': 1.3.0 + jest-util: 30.3.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)) + '@jest/types': 30.3.0 + import-local: 3.2.0 + jest-cli: 30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + keyv@5.6.0: + dependencies: + '@keyv/serialize': 1.1.1 + + kind-of@2.0.1: + dependencies: + is-buffer: 1.1.6 + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + lazy-cache@0.2.7: {} + + lazy-cache@1.0.4: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + libphonenumber-js@1.12.41: {} + + lines-and-columns@1.2.4: {} + + load-esm@1.0.3: {} + + loader-runner@4.3.1: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + long@5.3.2: {} + + lru-cache@10.4.3: {} + + lru-cache@11.3.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru.min@1.1.4: {} + + luxon@3.7.2: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + merge-deep@3.0.3: + dependencies: + arr-union: 3.1.0 + clone-deep: 0.2.4 + kind-of: 3.2.2 + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.13 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.0.3 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.3 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mixin-object@2.0.1: + dependencies: + for-in: 0.1.8 + is-extendable: 0.1.1 + + mri@1.2.0: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mute-stream@2.0.0: {} + + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.4 + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + nestjs-telegraf@2.9.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(telegraf@4.16.3)(typescript@5.9.3): + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + lodash: 4.18.1 + reflect-metadata: 0.2.2 + telegraf: 4.16.3 + typescript: 5.9.3 + + node-abort-controller@3.1.1: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.18.1 + + node-fetch-native@1.6.7: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-int64@0.4.0: {} + + node-releases@2.0.37: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nypm@0.6.5: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.1.1 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + openai@6.34.0(zod@4.3.6): + optionalDependencies: + zod: 4.3.6 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-timeout@4.1.0: {} + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-expression-matcher@1.5.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.3 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + perfect-debounce@1.0.0: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + + playwright-core@1.59.1: {} + + playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1): + dependencies: + debug: 4.4.3 + optionalDependencies: + playwright: 1.59.1 + playwright-core: 1.59.1 + transitivePeerDependencies: + - supports-color + + playwright@1.59.1: + dependencies: + playwright-core: 1.59.1 + optionalDependencies: + fsevents: 2.3.2 + + pluralize@8.0.0: {} + + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.7: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.8.2: {} + + pretty-format@30.3.0: + dependencies: + '@jest/schemas': 30.0.5 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + prisma@7.7.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3): + dependencies: + '@prisma/config': 7.7.0 + '@prisma/dev': 0.24.3(typescript@5.9.3) + '@prisma/engines': 7.7.0 + '@prisma/studio-core': 0.27.3(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + mysql2: 3.15.3 + postgres: 3.4.7 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - magicast + - react + - react-dom + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} + + puppeteer-extra-plugin-stealth@2.11.2(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)): + dependencies: + debug: 4.4.3 + puppeteer-extra-plugin: 3.2.3(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)) + puppeteer-extra-plugin-user-preferences: 2.4.1(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)) + optionalDependencies: + playwright-extra: 4.3.6(playwright-core@1.59.1)(playwright@1.59.1) + transitivePeerDependencies: + - supports-color + + puppeteer-extra-plugin-user-data-dir@2.4.1(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)): + dependencies: + debug: 4.4.3 + fs-extra: 10.1.0 + puppeteer-extra-plugin: 3.2.3(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)) + rimraf: 3.0.2 + optionalDependencies: + playwright-extra: 4.3.6(playwright-core@1.59.1)(playwright@1.59.1) + transitivePeerDependencies: + - supports-color + + puppeteer-extra-plugin-user-preferences@2.4.1(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)): + dependencies: + debug: 4.4.3 + deepmerge: 4.3.1 + puppeteer-extra-plugin: 3.2.3(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)) + puppeteer-extra-plugin-user-data-dir: 2.4.1(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)) + optionalDependencies: + playwright-extra: 4.3.6(playwright-core@1.59.1)(playwright@1.59.1) + transitivePeerDependencies: + - supports-color + + puppeteer-extra-plugin@3.2.3(playwright-extra@4.3.6(playwright-core@1.59.1)(playwright@1.59.1)): + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + merge-deep: 3.0.3 + optionalDependencies: + playwright-extra: 4.3.6(playwright-core@1.59.1)(playwright@1.59.1) + transitivePeerDependencies: + - supports-color + + pure-rand@6.1.0: {} + + pure-rand@7.0.1: {} + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc9@2.1.2: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-is@18.3.1: {} + + react@19.2.5: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + redis-errors@1.2.0: {} + + redis-info@3.1.0: + dependencies: + lodash: 4.18.1 + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + redis@5.12.1: + dependencies: + '@redis/bloom': 5.12.1(@redis/client@5.12.1) + '@redis/client': 5.12.1 + '@redis/json': 5.12.1(@redis/client@5.12.1) + '@redis/search': 5.12.1(@redis/client@5.12.1) + '@redis/time-series': 5.12.1(@redis/client@5.12.1) + transitivePeerDependencies: + - '@node-rs/xxhash' + - '@opentelemetry/api' + + reflect-metadata@0.2.2: {} + + remeda@2.33.4: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + rss-parser@3.13.0: + dependencies: + entities: 2.2.0 + xml2js: 0.5.0 + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safe-compare@1.1.4: + dependencies: + buffer-alloc: 1.2.0 + + safer-buffer@2.1.2: {} + + sandwich-stream@2.0.2: {} + + sax@1.6.0: {} + + scheduler@0.27.0: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + ajv-keywords: 3.5.2(ajv@6.14.0) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) + + semver@6.3.1: {} + + semver@7.7.4: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + seq-queue@0.0.5: {} + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shallow-clone@0.1.2: + dependencies: + is-extendable: 0.1.1 + kind-of: 2.0.1 + lazy-cache: 0.2.7 + mixin-object: 2.0.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + source-map@0.7.6: {} + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + sqlstring@2.3.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + standard-as-callback@2.1.0: {} + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + streamsearch@1.1.0: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + strnum@2.2.3: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3 + fast-safe-stringify: 2.1.1 + form-data: 4.0.5 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.15.1 + transitivePeerDependencies: + - supports-color + + supertest@7.2.2: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + swagger-ui-dist@5.32.4: + dependencies: + '@scarf/scarf': 1.4.0 + + symbol-observable@4.0.0: {} + + synckit@0.11.12: + dependencies: + '@pkgr/core': 0.2.9 + + tapable@2.3.2: {} + + telegraf@4.16.3: + dependencies: + '@telegraf/types': 7.1.0 + abort-controller: 3.0.0 + debug: 4.4.3 + mri: 1.2.0 + node-fetch: 2.7.0 + p-timeout: 4.1.0 + safe-compare: 1.1.4 + sandwich-stream: 2.0.2 + transitivePeerDependencies: + - encoding + - supports-color + + terser-webpack-plugin@5.4.0(webpack@5.106.0): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.46.1 + webpack: 5.106.0 + + terser@5.46.1: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.5 + + tinyexec@1.1.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tr46@0.0.3: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.9 + jest: 30.3.0(@types/node@24.12.2)(ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.4 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.0 + '@jest/transform': 30.3.0 + '@jest/types': 30.3.0 + babel-jest: 30.3.0(@babel/core@7.29.0) + jest-util: 30.3.0 + + ts-loader@9.5.7(typescript@5.9.3)(webpack@5.106.0): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.20.1 + micromatch: 4.0.8 + semver: 7.7.4 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.106.0 + + ts-node@10.9.2(@types/node@24.12.2)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.12.2 + acorn: 8.16.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.20.1 + tapable: 2.3.2 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + twitter-api-v2@1.29.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typescript-eslint@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + uglify-js@3.19.3: + optional: true + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + + undici-types@7.16.0: {} + + undici@7.25.0: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + uuid@11.1.0: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + + validator@13.15.35: {} + + vary@1.1.2: {} + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.5.1: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + webpack-node-externals@3.0.0: {} + + webpack-sources@3.3.4: {} + + webpack@5.106.0: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.16.0 + acorn-import-phases: 1.0.4(acorn@8.16.0) + browserslist: 4.28.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.20.1 + es-module-lexer: 2.0.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.1 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.2 + terser-webpack-plugin: 5.4.0(webpack@5.106.0) + watchpack: 2.5.1 + webpack-sources: 3.3.4 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + xml2js@0.5.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.12 + graphmatch: 1.1.1 + + zod@4.3.6: {} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..aa95d88 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,9 @@ +import "dotenv/config"; +import {defineConfig, env} from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/prisma/migrations/20260411095045_init/migration.sql b/prisma/migrations/20260411095045_init/migration.sql new file mode 100644 index 0000000..c68cc72 --- /dev/null +++ b/prisma/migrations/20260411095045_init/migration.sql @@ -0,0 +1,26 @@ +-- CreateTable +CREATE TABLE "Post" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "prompt" TEXT NOT NULL, + "content" TEXT NOT NULL, + "imageUrl" TEXT, + "style" TEXT NOT NULL DEFAULT 'general', + "status" TEXT NOT NULL DEFAULT 'pending', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Post_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Config" ( + "id" SERIAL NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + + CONSTRAINT "Config_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Config_key_key" ON "Config"("key"); diff --git a/prisma/migrations/20260411144957_init/migration.sql b/prisma/migrations/20260411144957_init/migration.sql new file mode 100644 index 0000000..b6a8924 --- /dev/null +++ b/prisma/migrations/20260411144957_init/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/20260412041906_update/migration.sql b/prisma/migrations/20260412041906_update/migration.sql new file mode 100644 index 0000000..ec3ebff --- /dev/null +++ b/prisma/migrations/20260412041906_update/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Post" ADD COLUMN "isFbPostState" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "isTiktokPostState" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "isTwitterPostState" INTEGER NOT NULL DEFAULT 0; diff --git a/prisma/migrations/20260417040623_add_more_table/migration.sql b/prisma/migrations/20260417040623_add_more_table/migration.sql new file mode 100644 index 0000000..0ef39bd --- /dev/null +++ b/prisma/migrations/20260417040623_add_more_table/migration.sql @@ -0,0 +1,28 @@ +-- CreateTable +CREATE TABLE "Trend" ( + "id" SERIAL NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "url" TEXT, + "score" INTEGER NOT NULL DEFAULT 0, + "source" TEXT NOT NULL, + "category" TEXT, + "tags" JSONB, + "engagement" JSONB, + "raw" JSONB, + "fingerprint" VARCHAR, + "sourceTimestamp" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Trend_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Trend_score_idx" ON "Trend"("score"); + +-- CreateIndex +CREATE INDEX "Trend_category_idx" ON "Trend"("category"); + +-- CreateIndex +CREATE INDEX "Trend_sourceTimestamp_idx" ON "Trend"("sourceTimestamp"); diff --git a/prisma/migrations/20260417050432_update/migration.sql b/prisma/migrations/20260417050432_update/migration.sql new file mode 100644 index 0000000..979f705 --- /dev/null +++ b/prisma/migrations/20260417050432_update/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Trend" ALTER COLUMN "updatedAt" DROP NOT NULL, +ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; diff --git a/prisma/migrations/20260417060838_update/migration.sql b/prisma/migrations/20260417060838_update/migration.sql new file mode 100644 index 0000000..87f84f4 --- /dev/null +++ b/prisma/migrations/20260417060838_update/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Trend" ADD COLUMN "geo" VARCHAR; diff --git a/prisma/migrations/20260417060928_update/migration.sql b/prisma/migrations/20260417060928_update/migration.sql new file mode 100644 index 0000000..3475c27 --- /dev/null +++ b/prisma/migrations/20260417060928_update/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE INDEX "Trend_geo_idx" ON "Trend"("geo"); diff --git a/prisma/migrations/20260420092622_update/migration.sql b/prisma/migrations/20260420092622_update/migration.sql new file mode 100644 index 0000000..6e9f964 --- /dev/null +++ b/prisma/migrations/20260420092622_update/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "Post" ADD COLUMN "draft" TEXT, +ADD COLUMN "model" TEXT, +ADD COLUMN "reviewNotes" TEXT, +ADD COLUMN "tokensUsed" INTEGER NOT NULL DEFAULT 0, +ADD COLUMN "tone" TEXT DEFAULT ''; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/prisma.module.ts b/prisma/prisma.module.ts new file mode 100644 index 0000000..ef09766 --- /dev/null +++ b/prisma/prisma.module.ts @@ -0,0 +1,14 @@ +import {Global, Module} from '@nestjs/common'; +import {PrismaService} from "./prisma.service"; + +@Global() +@Module({ + imports: [], + providers: [ + PrismaService, + ], + exports: [PrismaService], + +}) +export class PrismaModule { +} diff --git a/prisma/prisma.service.ts b/prisma/prisma.service.ts new file mode 100644 index 0000000..ce65184 --- /dev/null +++ b/prisma/prisma.service.ts @@ -0,0 +1,39 @@ +// src/prisma/prisma.service.ts +import {Injectable, OnModuleInit, OnModuleDestroy} from '@nestjs/common'; +import {PrismaClient} from '../src/generated/prisma/client'; +import {PrismaPg} from "@prisma/adapter-pg"; + + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + constructor() { + const adapter = new PrismaPg({ + connectionString: process.env.DATABASE_URL!, + }); + super({adapter}); + } + + async onModuleInit() { + try { + await this.$connect(); + console.log('✅ Đã kết nối Database thành công'); + } catch (error) { + console.error('❌ Lỗi kết nối Database:', error); + } + } + + async getManyAndCount( + model: any, + args: any + ) { + const [data, total] = await this.$transaction([ + model.findMany(args), + model.count({ where: args.where }), + ]); + return { data, total }; + } + + async onModuleDestroy() { + await this.$disconnect(); // Ngắt kết nối khi tắt app + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..42fd704 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,67 @@ +// prisma/schema.prisma + +generator client { + provider = "prisma-client" + moduleFormat = "cjs" + output = "../src/generated/prisma" +} + +datasource db { + provider = "postgresql" +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? +} + +model Post { + id Int @id @default(autoincrement()) + title String + prompt String + content String @db.Text + imageUrl String? + style String @default("general") + tone String? @default("") + isFbPostState Int @default(0) + isTwitterPostState Int @default(0) + isTiktokPostState Int @default(0) + draft String? @db.Text + tokensUsed Int @default(0) + model String? + reviewNotes String? + status String @default("pending") // Các trạng thái: pending, approved, published, rejected + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Config { + id Int @id @default(autoincrement()) + key String @unique + value String +} + +model Trend { + id Int @id @default(autoincrement()) + title String + description String? @db.Text + url String? + score Int @default(0) + source String @db.Text + category String? + geo String? @db.VarChar() + tags Json? + engagement Json? + raw Json? + fingerprint String? @db.VarChar() + sourceTimestamp DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime? @default(now()) + + @@index([score]) // Tạo index cho cột name + @@index([category]) // Tạo index cho cột name + @@index([geo]) // Tạo index cho cột name + @@index([sourceTimestamp]) // Tạo index cho cột name + +} diff --git a/src/app.controller.spec.ts b/src/app.controller.spec.ts new file mode 100644 index 0000000..d22f389 --- /dev/null +++ b/src/app.controller.spec.ts @@ -0,0 +1,22 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +describe('AppController', () => { + let appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [AppService], + }).compile(); + + appController = app.get(AppController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/src/app.controller.ts b/src/app.controller.ts new file mode 100644 index 0000000..07d03bd --- /dev/null +++ b/src/app.controller.ts @@ -0,0 +1,223 @@ +import {Controller, Get, HttpException, Param, Post, Query, Req} from '@nestjs/common'; +import {AppService} from './app.service'; +import {AIService} from "./shared/ai.service"; +import {PgPostService} from "./shared/pg.post.service"; +import {PublishPageService} from "./modules/social-api/publish.page.service"; +import {TwitterClient} from "./modules/social-api/twitter.client"; +import {normalizeTagsSingleCashtag} from "./shared/helper"; +import {XReaderService} from "./modules/x-reader/x-reader.service"; +import {XCacheService} from "./modules/x-cache/x-cache.service"; +import {SqsPostService} from "./modules/sqs-module/sqs.post.service"; + +@Controller() +export class AppController { + constructor( + private readonly appService: AppService, + private readonly aiService: AIService, + private readonly pgPostService: PgPostService, + private readonly publishPageService: PublishPageService, + private readonly twitterClient: TwitterClient, + private readonly cacheService: XCacheService, + private readonly xReaderService: XReaderService, + private readonly sqsPostService: SqsPostService, + ) { + } + + @Get() + getHello(): string { + return this.appService.getHello(); + } + + @Get('test-gemini-ai') + async testAI() { + console.log('Test gọi gemini AI trực tiếp...'); + // return await this.aiService.listAvailableModels(); + const title = 'bitcoin bullish'; + const result = await this.aiService.generateContentViaGemini(title, 'crypto', 'en'); + return result; + console.log({result}); + const newPost = await this.pgPostService.createPost({ + title: title, + content: result.content, + style: 'crypto', + status: 'pending', + prompt: result.prompt, + }); + return newPost; + } + + @Get('test-chatgpt-ai') + async testChatGPTAi(@Query('input') input: string) { + console.log(`Test gọi chatgpt AI trực tiếp with input ${input}...`); + // return await this.aiService.listAvailableModels(); + if (!input) { + return 'No input'; + } + const openai = await this.aiService.getChatgptModel(); + const resp = await openai.responses.create({ + model: "gpt-5-nano", + input, + store: true, + }); + return resp.output_text; + } + + @Get('test-deepseek-ai') + async testDeepSeekAI() { + console.log('Test gọi deepseed AI trực tiếp...'); + // return await this.aiService.listAvailableModels(); + const title = 'bitcoin bullish' + const result = await this.aiService.generateContentViaDeepseek(title, 'crypto',); + console.log({result}); + return result; + + const newPost = await this.pgPostService.createPost({ + title: title, + content: result.final, + style: 'crypto', + status: 'pending', + prompt: result.prompt, + }); + return newPost; + } + + @Get('test-list-ai') + async listModelGeminiAI() { + console.log('Test gọi AI trực tiếp...'); + // return await this.aiService.listAvailableModels(); + return await this.aiService.listAvailableModels(); + } + + @Get('test-save-post') + async testSavePost() { + return this.pgPostService.createPost({ + title: 'zz', + prompt: 'xx', + content: 'xx', + imageUrl: 'xx.png', + + }) + } + + @Get('test-publish-post/:id') + async testPostFb(@Param('id') id: number) { + console.log({id}); + console.log('testPostFb==>') + return this.publishPageService.publishToFacebook(1 * id) + } + + @Get('test-reply-post/:id') + async testReplyX(@Param('id') id: string, @Query('comment') input: string) { + console.log({id}); + console.log('testReplyX==>') + // return this.publishPageService.relyX(input, id) + } + + @Get('post/:id') + async showPostFb(@Param('id') id: number) { + console.log({id}); + let post = await this.pgPostService.post({id: id * 1}) + if (!post) { + return 'no post'; + } + const _normalizeTagsSingleCashtagContent = normalizeTagsSingleCashtag(post.content); + + return { + content: post.content, + _content: _normalizeTagsSingleCashtagContent + }; + } + + @Get('tw_callback') + async twitterAuthCallback( + @Query('code') code: string, + @Query('state') state: string, + @Req() request: Request) { + + console.log('twitterAuthCallback==>') + const cacheAuthData = await this.cacheService.getCachedData('tw_authorize'); + console.log({cacheAuthData}); + //@ts-ignore + const {codeVerifier, state: sessionState} = cacheAuthData || {} + + if (!codeVerifier || !state || !sessionState || !code) { + throw new HttpException('You denied the app or your session expired!', 400) + } + if (state !== sessionState) { + throw new HttpException('Stored tokens didnt match!!', 400) + } + + const client = await this.twitterClient.getTwitterClientV2(); + + const {client: loggedClient, accessToken, refreshToken} = await client.loginWithOAuth2({ + code, + codeVerifier: codeVerifier, + redirectUri: 'http://localhost:3000/tw_callback' + }); + + await this.cacheService.setCachedKey('tw_accesstoken_time_add', Date.now(), 3 * 24 * 3600); + await this.cacheService.setCachedKey('tw_accesstoken', accessToken, 3 * 24 * 3600); + await this.twitterClient.setCacheRefreshToken('' + refreshToken); + console.log({loggedClient, accessToken, refreshToken}); + // @ts-ignore + const {data: userObject} = loggedClient.v2.me(); + + return userObject; + + } + + @Get('tw_authorize') + async twitterAuthorize() { + const client = await this.twitterClient.getTwitterClientV2(); + const {url, codeVerifier, state} = client.generateOAuth2AuthLink('http://localhost:3000/tw_callback', + { + scope: + [ + 'tweet.read', + 'tweet.write', + 'users.read', + 'offline.access', + 'media.write', + + ] + }); + // Redirect your client to {url} + console.log('Please go to', url); + console.log({codeVerifier, state}); + await this.cacheService.setCachedKey('tw_authorize', {codeVerifier, state}); + + return this.cacheService.getCachedData('tw_authorize'); + +// Redirect your client to authLink.url +// console.log('Please go to', authLink.url); + } + + @Get('/tw/post/:id') + async testPostTwitter(@Param('id') id: number) { + return this.publishPageService.publishTwitter(id * 1) + } + + @Get('/tw/me') + async testTwitterMe() { + const client = await this.twitterClient.getTwitterClientV2ViaAccessToken(); + const me = client.v2.me(); + return me; + } + + @Get('/tw/read') + async getTwitterMe(@Query('url') url: string, @Query('via') via: string = 'api') { + if (via === 'browser') { + return this.xReaderService.readXPostViaBrowserV2(url) + } + return this.xReaderService.readXPostViaApi(url) + } + + @Post('/sqs/send') + async postSqsQueueTest() { + // return this.sqsPostService.postFlashKaze({ + // content: 'aaa', + // id: 1 + // }) + + } +} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..52cf67b --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,120 @@ +import {Module} from '@nestjs/common'; +import {ConfigModule} from '@nestjs/config'; + +import {BullModule} from '@nestjs/bullmq'; +import {AppController} from './app.controller'; +import {AppService} from './app.service'; +import {ManagerModule} from "./modules/manager/manager.module"; +import {TelegramModule} from "./modules/telegram/telegram.module"; +import {SocialModule} from "./modules/social-api/social.module"; +import {AIService} from "./shared/ai.service"; +import {TelegrafModule} from "nestjs-telegraf"; +import {BullBoardModule} from "@bull-board/nestjs"; +import {ExpressAdapter} from "@bull-board/express"; +import {PrismaService} from "../prisma/prisma.service"; +import {PgPostService} from "./shared/pg.post.service"; +import {PrismaModule} from "../prisma/prisma.module"; +import {CacheModule} from "@nestjs/cache-manager"; +import KeyvRedis from "@keyv/redis"; +import {XReaderModule} from './modules/x-reader/x-reader.module'; +import {CollectorModule} from "./modules/collector/collector.module"; +import {TrendsModule} from "./modules/trends/trends.module"; +import configuration from "./common/config/configuration"; +import {SchedulerModule} from "./modules/scheduler/scheduler.module"; +import {ContentWriterModule} from "./modules/content-writer/content-writer.module"; +import {TeleGrammYModule} from "./modules/tele-grammY/tele-grammY.module"; +import {session} from "telegraf"; +import {Redis} from '@telegraf/session/redis'; +import {XUploaderModule} from "./modules/x-uploader/x-uploader.module"; +import {TiktokDownloadModule} from "./modules/tiktok-download/tiktok.download.module"; +import {XCacheModule} from "./modules/x-cache/x-cache.module"; +import {XCacheService} from "./modules/x-cache/x-cache.service"; +import {SqsModule} from "./modules/sqs-module/sqs.module"; +import {SqsPostService} from "./modules/sqs-module/sqs.post.service"; + +@Module({ + imports: [ + // DevtoolsModule.register({ + // http: process.env.NODE_ENV !== 'production', + // }), + ConfigModule.forRoot( + { + isGlobal: true, + load: [configuration], + }, + ), + CacheModule.registerAsync({ + isGlobal: true, + useFactory: () => ({ + stores: [ + new KeyvRedis(`redis://127.0.0.1:6379/1`) + ], + }), + }), + BullModule.forRoot({ + connection: { + host: '127.0.0.1', + port: 6379, + }, + }), + //register the bull-board module forRoot in your app.module + BullBoardModule.forRoot({ + route: "/queues", + adapter: ExpressAdapter + }), + TelegrafModule.forRoot({ + token: process.env.TELEGRAM_BOT_TOKEN!, + middlewares: [ + session({ + store: Redis({ + url: 'redis://127.0.0.1:6379/3', // Dùng DB 2 để tách biệt với CacheModule + }) + }), // BẮT BUỘC: Phải có store này thì Wizard mới nhảy bước được + (ctx, next) => { + const allowedIds = (process.env.TELEGRAM_ALLOW_CHAT_IDS || '').split(','); + if (allowedIds.includes(''+ctx.chat?.id)) { + return next(); + } + return ctx.reply('Xin lỗi, bạn không có quyền sử dụng bot này.'); + }, + ], + }), + // TelegrafModule.forRootAsync({ + // useFactory: () => { + // // NestJS CacheManager stores (Keyv) usually expose the client + // + // return { + // token: process.env.TELEGRAM_BOT_TOKEN!, + // middlewares: [ + // session({ + // store: new KeyvRedis({ + // url: 'redis://127.0.0.1:6379/3', // Dùng DB 2 để tách biệt với CacheModule + // }) + // }), + // ], + // }; + // }, + // // token: process.env.TELEGRAM_BOT_TOKEN!, + // // middlewares: [session()], + // }), + XCacheModule, + PrismaModule, + ManagerModule, + SocialModule, + ContentWriterModule, + CollectorModule, + TrendsModule, + TelegramModule, + TeleGrammYModule, + XReaderModule, + SchedulerModule, + XUploaderModule, + TiktokDownloadModule, + SqsModule, + ], + controllers: [AppController], + providers: [AppService, AIService, PrismaService, PgPostService, SqsPostService], + exports: [], +}) +export class AppModule { +} diff --git a/src/app.service.ts b/src/app.service.ts new file mode 100644 index 0000000..927d7cc --- /dev/null +++ b/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/src/common/config/configuration.ts b/src/common/config/configuration.ts new file mode 100644 index 0000000..19e90b5 --- /dev/null +++ b/src/common/config/configuration.ts @@ -0,0 +1,18 @@ +import {_toNum} from "../../shared/helper"; + +export default () => ({ + port: _toNum(process.env.PORT) || 3000, + newsapi: { + key: process.env.NEWSAPIORG_API_KEY || '', + }, + ai: { + perplexityKey: process.env.PERPLEXITY_API_KEY || '', + claudeKey: process.env.CLAUDE_API_KEY || '', + }, + collector: { + redditUserAgent: process.env.COLLECTOR_REDDIT_USER_AGENT || 'TrendHunter/1.0', + cronIntervalHours: _toNum(process.env.COLLECTOR_CRON_INTERVAL_HOURS) || 2, + maxItemsPerSource: _toNum(process.env.COLLECTOR_MAX_ITEMS_PER_SOURCE) || 25, + googleTrendsGeo: process.env.GOOGLE_TRENDS_GEO || 'VN', + }, +}); diff --git a/src/common/interfaces/language.prompt.interface.ts b/src/common/interfaces/language.prompt.interface.ts new file mode 100644 index 0000000..aa44e0a --- /dev/null +++ b/src/common/interfaces/language.prompt.interface.ts @@ -0,0 +1 @@ +export type Language = 'en' | 'vi' | 'ja' | 'ko' | 'cn'; diff --git a/src/common/interfaces/trend-item.interface.ts b/src/common/interfaces/trend-item.interface.ts new file mode 100644 index 0000000..a04bb3c --- /dev/null +++ b/src/common/interfaces/trend-item.interface.ts @@ -0,0 +1,25 @@ +export interface TrendItem { + source: string; + title: string; + description: string; + url: string; + score: number; // normalized 0–100 + timestamp: Date; + category?: string; + tags?: string[]; + engagement?: { + upvotes?: number; + comments?: number; + shares?: number; + views?: number; + }; + raw?: Record; +} + +export interface CollectorResult { + source: string; + items: TrendItem[]; + collectedAt: Date; + durationMs: number; + error?: string; +} diff --git a/src/common/utils/text.util.ts b/src/common/utils/text.util.ts new file mode 100644 index 0000000..b31529b --- /dev/null +++ b/src/common/utils/text.util.ts @@ -0,0 +1,95 @@ +/** + * Utility functions cho text processing — dedup, normalize, similarity + * Hoàn toàn FREE, không cần AI + */ +export class TextUtil { + /** + * Normalize title để so sánh similarity + */ + static normalizeTitle(title: string): string { + return title + .toLowerCase() + .replace(/[^a-zA-Z0-9\u00C0-\u024F\u1E00-\u1EFF\s]/g, '') // giữ unicode (tiếng Việt) + .replace(/\s+/g, ' ') + .trim(); + } + + /** + * Jaccard similarity giữa 2 strings (dựa trên words) + * Return 0–1, >= 0.5 coi như trùng + */ + static jaccardSimilarity(a: string, b: string): number { + const setA = new Set(TextUtil.normalizeTitle(a).split(' ')); + const setB = new Set(TextUtil.normalizeTitle(b).split(' ')); + + const intersection = new Set([...setA].filter((x) => setB.has(x))); + const union = new Set([...setA, ...setB]); + + if (union.size === 0) return 0; + return intersection.size / union.size; + } + + /** + * Extract keywords đơn giản (bỏ stopwords) + */ + static extractKeywords(text: string, maxKeywords = 5): string[] { + const stopwords = new Set([ + 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', + 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', + 'would', 'could', 'should', 'may', 'might', 'can', 'shall', + 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', + 'as', 'into', 'through', 'during', 'before', 'after', 'above', + 'below', 'between', 'out', 'off', 'over', 'under', 'again', + 'further', 'then', 'once', 'here', 'there', 'when', 'where', + 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more', + 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', + 'own', 'same', 'so', 'than', 'too', 'very', 'just', 'because', + 'but', 'and', 'or', 'if', 'while', 'about', 'up', 'it', 'its', + 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we', 'our', + 'you', 'your', 'he', 'him', 'his', 'she', 'her', 'they', 'them', + 'their', 'what', 'which', 'who', 'whom', + // Vietnamese stopwords + 'và', 'của', 'là', 'có', 'được', 'cho', 'trong', 'với', 'không', + 'này', 'đã', 'từ', 'một', 'những', 'các', 'để', 'theo', 'về', + 'người', 'năm', 'đến', 'khi', 'còn', 'ra', 'cũng', 'như', 'hay', + 'tại', 'vào', 'lại', 'sẽ', 'bị', 'đó', 'nếu', 'sau', 'trên', + ]); + + const words = TextUtil.normalizeTitle(text).split(' '); + return words + .filter((w) => w.length > 2 && !stopwords.has(w)) + .slice(0, maxKeywords); + } + + /** + * Tạo fingerprint cho dedup nhanh + */ + static fingerprint(title: string): string { + const keywords = TextUtil.extractKeywords(title, 4); + return keywords.sort().join('|'); + } + + /** + * Truncate text an toàn (không cắt giữa từ) + */ + static truncate(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + const truncated = text.substring(0, maxLength); + const lastSpace = truncated.lastIndexOf(' '); + return (lastSpace > 0 ? truncated.substring(0, lastSpace) : truncated) + '...'; + } + + static detectLinkX(text: string) { + const regex = /(https?:\/\/)?(www\.)?(x\.com|twitter\.com)\/[^\s]+/gi; + const links = text.match(regex); + console.log({links}); + return { + hasLinkX: links && links.length > 0, + url: links ? links[0] : null, + } + } + + static removeAllUrl(text: string) { + return text.replace(/(?:https?|ftp):\/\/[\n\S]+/g, ''); + } +} diff --git a/src/common/utils/token-calculator.ts b/src/common/utils/token-calculator.ts new file mode 100644 index 0000000..1ce8867 --- /dev/null +++ b/src/common/utils/token-calculator.ts @@ -0,0 +1,79 @@ +// utils/token-calculator.ts + +import {Platform} from "../../modules/content-writer/enum/platform.enum"; +import {Language} from "../interfaces/language.prompt.interface"; +import {LengthRange} from "../../modules/content-writer/config/platform-limits"; + +/** + * Tokens trung bình per character theo ngôn ngữ. + * Dựa trên tokenizer GPT/Claude thực tế. + */ +const TOKENS_PER_CHAR: Record = { + en: 0.25, // 1 token ≈ 4 chars + vi: 0.5, // 1 token ≈ 2 chars (dấu tiếng Việt tốn token) + ja: 1.0, // 1 token ≈ 1 char (hiragana/kanji) + ko: 1.0, // 1 token ≈ 1 char (hangul) + cn: 1.0, // 1 token ≈ 1 char (chinese) +}; + +/** + * Target character length theo platform + buffer để AI có không gian "thở". + */ +const PLATFORM_TARGET_CHARS: Record = { + [Platform.X]: { min: 180, max: 280, buffer: 1.3 }, + [Platform.FACEBOOK]: { min: 400, max: 1200, buffer: 1.5 }, +}; + +export interface LengthBudget { + minChars: number; + maxChars: number; + maxTokens: number; +} +export interface TokenBudget { + minChars: number; + maxChars: number; + sweetChars: number; + maxTokens: number; +} + +export function calculateLengthBudget( + platform: Platform, + language: Language, +): LengthBudget { + const target = PLATFORM_TARGET_CHARS[platform]; + const tokensPerChar = TOKENS_PER_CHAR[language]; + + const raw = Math.ceil(target.max * tokensPerChar * target.buffer); + + // Safe minimums để đảm bảo không bị cắt + const SAFE_MIN: Record = { + [Platform.X]: 300, // X post không bao giờ < 300 tokens + [Platform.FACEBOOK]: 800, // FB không bao giờ < 800 tokens + }; + return { + minChars: target.min, + maxChars: target.max, + maxTokens: Math.max(raw, SAFE_MIN[platform]), + }; +} + + +export function calculateTokenBudget( + range: LengthRange, + language: Language, +): TokenBudget { + const tokensPerChar = TOKENS_PER_CHAR[language]; + + // Buffer 1.4x cho emoji, punctuation, line breaks + const maxTokens = Math.ceil(range.max * tokensPerChar * 1.4); + + // Safe minimum theo range + const SAFE_MIN = Math.ceil(range.min * tokensPerChar * 1.5); + + return { + minChars: range.min, + maxChars: range.max, + sweetChars: range.sweet, + maxTokens: Math.max(maxTokens, SAFE_MIN, 200), // floor 200 tokens + }; +} diff --git a/src/generated/prisma/browser.ts b/src/generated/prisma/browser.ts new file mode 100644 index 0000000..b103d8d --- /dev/null +++ b/src/generated/prisma/browser.ts @@ -0,0 +1,39 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file should be your main import to use Prisma-related types and utilities in a browser. + * Use it to get access to models, enums, and input types. + * + * This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only. + * See `client.ts` for the standard, server-side entry point. + * + * 🟢 You can import this file directly. + */ + +import * as Prisma from './internal/prismaNamespaceBrowser.js' +export { Prisma } +export * as $Enums from './enums.js' +export * from './enums.js'; +/** + * Model User + * + */ +export type User = Prisma.UserModel +/** + * Model Post + * + */ +export type Post = Prisma.PostModel +/** + * Model Config + * + */ +export type Config = Prisma.ConfigModel +/** + * Model Trend + * + */ +export type Trend = Prisma.TrendModel diff --git a/src/generated/prisma/client.ts b/src/generated/prisma/client.ts new file mode 100644 index 0000000..12555e8 --- /dev/null +++ b/src/generated/prisma/client.ts @@ -0,0 +1,61 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types. + * If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead. + * + * 🟢 You can import this file directly. + */ + +import * as process from 'node:process' +import * as path from 'node:path' + +import * as runtime from "@prisma/client/runtime/client" +import * as $Enums from "./enums.js" +import * as $Class from "./internal/class.js" +import * as Prisma from "./internal/prismaNamespace.js" + +export * as $Enums from './enums.js' +export * from "./enums.js" +/** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient({ + * adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) + * }) + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://pris.ly/d/client). + */ +export const PrismaClient = $Class.getPrismaClientClass() +export type PrismaClient = $Class.PrismaClient +export { Prisma } + +/** + * Model User + * + */ +export type User = Prisma.UserModel +/** + * Model Post + * + */ +export type Post = Prisma.PostModel +/** + * Model Config + * + */ +export type Config = Prisma.ConfigModel +/** + * Model Trend + * + */ +export type Trend = Prisma.TrendModel diff --git a/src/generated/prisma/commonInputTypes.ts b/src/generated/prisma/commonInputTypes.ts new file mode 100644 index 0000000..ff2dba9 --- /dev/null +++ b/src/generated/prisma/commonInputTypes.ts @@ -0,0 +1,401 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports various common sort, input & filter types that are not directly linked to a particular model. + * + * 🟢 You can import this file directly. + */ + +import type * as runtime from "@prisma/client/runtime/client" +import * as $Enums from "./enums.js" +import type * as Prisma from "./internal/prismaNamespace.js" + + +export type IntFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntFilter<$PrismaModel> | number +} + +export type StringFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringFilter<$PrismaModel> | string +} + +export type StringNullableFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null +} + +export type SortOrderInput = { + sort: Prisma.SortOrder + nulls?: Prisma.NullsOrder +} + +export type IntWithAggregatesFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number + _count?: Prisma.NestedIntFilter<$PrismaModel> + _avg?: Prisma.NestedFloatFilter<$PrismaModel> + _sum?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedIntFilter<$PrismaModel> + _max?: Prisma.NestedIntFilter<$PrismaModel> +} + +export type StringWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedStringFilter<$PrismaModel> + _max?: Prisma.NestedStringFilter<$PrismaModel> +} + +export type StringNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + mode?: Prisma.QueryMode + not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null + _count?: Prisma.NestedIntNullableFilter<$PrismaModel> + _min?: Prisma.NestedStringNullableFilter<$PrismaModel> + _max?: Prisma.NestedStringNullableFilter<$PrismaModel> +} + +export type DateTimeFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string +} + +export type DateTimeWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedDateTimeFilter<$PrismaModel> + _max?: Prisma.NestedDateTimeFilter<$PrismaModel> +} + +export type JsonNullableFilter<$PrismaModel = never> = +| Prisma.PatchUndefined< + Prisma.Either>, Exclude>, 'path'>>, + Required> + > +| Prisma.OptionalFlat>, 'path'>> + +export type JsonNullableFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter + path?: string[] + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel> + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter +} + +export type DateTimeNullableFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null +} + +export type JsonNullableWithAggregatesFilter<$PrismaModel = never> = +| Prisma.PatchUndefined< + Prisma.Either>, Exclude>, 'path'>>, + Required> + > +| Prisma.OptionalFlat>, 'path'>> + +export type JsonNullableWithAggregatesFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter + path?: string[] + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel> + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter + _count?: Prisma.NestedIntNullableFilter<$PrismaModel> + _min?: Prisma.NestedJsonNullableFilter<$PrismaModel> + _max?: Prisma.NestedJsonNullableFilter<$PrismaModel> +} + +export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null + _count?: Prisma.NestedIntNullableFilter<$PrismaModel> + _min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> + _max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> +} + +export type NestedIntFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntFilter<$PrismaModel> | number +} + +export type NestedStringFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringFilter<$PrismaModel> | string +} + +export type NestedStringNullableFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null +} + +export type NestedIntWithAggregatesFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number + _count?: Prisma.NestedIntFilter<$PrismaModel> + _avg?: Prisma.NestedFloatFilter<$PrismaModel> + _sum?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedIntFilter<$PrismaModel> + _max?: Prisma.NestedIntFilter<$PrismaModel> +} + +export type NestedFloatFilter<$PrismaModel = never> = { + equals?: number | Prisma.FloatFieldRefInput<$PrismaModel> + in?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> + notIn?: number[] | Prisma.ListFloatFieldRefInput<$PrismaModel> + lt?: number | Prisma.FloatFieldRefInput<$PrismaModel> + lte?: number | Prisma.FloatFieldRefInput<$PrismaModel> + gt?: number | Prisma.FloatFieldRefInput<$PrismaModel> + gte?: number | Prisma.FloatFieldRefInput<$PrismaModel> + not?: Prisma.NestedFloatFilter<$PrismaModel> | number +} + +export type NestedStringWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedStringFilter<$PrismaModel> + _max?: Prisma.NestedStringFilter<$PrismaModel> +} + +export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null + in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + notIn?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> | null + lt?: string | Prisma.StringFieldRefInput<$PrismaModel> + lte?: string | Prisma.StringFieldRefInput<$PrismaModel> + gt?: string | Prisma.StringFieldRefInput<$PrismaModel> + gte?: string | Prisma.StringFieldRefInput<$PrismaModel> + contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel> + not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null + _count?: Prisma.NestedIntNullableFilter<$PrismaModel> + _min?: Prisma.NestedStringNullableFilter<$PrismaModel> + _max?: Prisma.NestedStringNullableFilter<$PrismaModel> +} + +export type NestedIntNullableFilter<$PrismaModel = never> = { + equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null + in?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null + notIn?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null + lt?: number | Prisma.IntFieldRefInput<$PrismaModel> + lte?: number | Prisma.IntFieldRefInput<$PrismaModel> + gt?: number | Prisma.IntFieldRefInput<$PrismaModel> + gte?: number | Prisma.IntFieldRefInput<$PrismaModel> + not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null +} + +export type NestedDateTimeFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string +} + +export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string + _count?: Prisma.NestedIntFilter<$PrismaModel> + _min?: Prisma.NestedDateTimeFilter<$PrismaModel> + _max?: Prisma.NestedDateTimeFilter<$PrismaModel> +} + +export type NestedDateTimeNullableFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> | Date | string | null +} + +export type NestedJsonNullableFilter<$PrismaModel = never> = +| Prisma.PatchUndefined< + Prisma.Either>, Exclude>, 'path'>>, + Required> + > +| Prisma.OptionalFlat>, 'path'>> + +export type NestedJsonNullableFilterBase<$PrismaModel = never> = { + equals?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter + path?: string[] + mode?: Prisma.QueryMode | Prisma.EnumQueryModeFieldRefInput<$PrismaModel> + string_contains?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_starts_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + string_ends_with?: string | Prisma.StringFieldRefInput<$PrismaModel> + array_starts_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_ends_with?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + array_contains?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | null + lt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + lte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gt?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + gte?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> + not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter +} + +export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = { + equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> | null + in?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null + notIn?: Date[] | string[] | Prisma.ListDateTimeFieldRefInput<$PrismaModel> | null + lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel> + not?: Prisma.NestedDateTimeNullableWithAggregatesFilter<$PrismaModel> | Date | string | null + _count?: Prisma.NestedIntNullableFilter<$PrismaModel> + _min?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> + _max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> +} + + diff --git a/src/generated/prisma/enums.ts b/src/generated/prisma/enums.ts new file mode 100644 index 0000000..043572d --- /dev/null +++ b/src/generated/prisma/enums.ts @@ -0,0 +1,15 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* +* This file exports all enum related types from the schema. +* +* 🟢 You can import this file directly. +*/ + + + +// This file is empty because there are no enums in the schema. +export {} diff --git a/src/generated/prisma/internal/class.ts b/src/generated/prisma/internal/class.ts new file mode 100644 index 0000000..0cb4b69 --- /dev/null +++ b/src/generated/prisma/internal/class.ts @@ -0,0 +1,234 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * Please import the `PrismaClient` class from the `client.ts` file instead. + */ + +import * as runtime from "@prisma/client/runtime/client" +import type * as Prisma from "./prismaNamespace.js" + + +const config: runtime.GetPrismaClientConfig = { + "previewFeatures": [], + "clientVersion": "7.7.0", + "engineVersion": "75cbdc1eb7150937890ad5465d861175c6624711", + "activeProvider": "postgresql", + "inlineSchema": "// prisma/schema.prisma\n\ngenerator client {\n provider = \"prisma-client\"\n moduleFormat = \"cjs\"\n output = \"../src/generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n}\n\nmodel User {\n id Int @id @default(autoincrement())\n email String @unique\n name String?\n}\n\nmodel Post {\n id Int @id @default(autoincrement())\n title String\n prompt String\n content String @db.Text\n imageUrl String?\n style String @default(\"general\")\n tone String? @default(\"\")\n isFbPostState Int @default(0)\n isTwitterPostState Int @default(0)\n isTiktokPostState Int @default(0)\n draft String? @db.Text\n tokensUsed Int @default(0)\n model String?\n reviewNotes String?\n status String @default(\"pending\") // Các trạng thái: pending, approved, published, rejected\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Config {\n id Int @id @default(autoincrement())\n key String @unique\n value String\n}\n\nmodel Trend {\n id Int @id @default(autoincrement())\n title String\n description String? @db.Text\n url String?\n score Int @default(0)\n source String @db.Text\n category String?\n geo String? @db.VarChar()\n tags Json?\n engagement Json?\n raw Json?\n fingerprint String? @db.VarChar()\n sourceTimestamp DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime? @default(now())\n\n @@index([score]) // Tạo index cho cột name\n @@index([category]) // Tạo index cho cột name\n @@index([geo]) // Tạo index cho cột name\n @@index([sourceTimestamp]) // Tạo index cho cột name\n}\n", + "runtimeDataModel": { + "models": {}, + "enums": {}, + "types": {} + }, + "parameterizationSchema": { + "strings": [], + "graph": "" + } +} + +config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Post\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"prompt\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"content\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"imageUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"style\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tone\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"isFbPostState\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"isTwitterPostState\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"isTiktokPostState\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"draft\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tokensUsed\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"model\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"reviewNotes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"status\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Config\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"}],\"dbName\":null},\"Trend\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"title\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"score\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"source\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"category\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"geo\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tags\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"engagement\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"raw\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"fingerprint\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"sourceTimestamp\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}") +config.parameterizationSchema = { + strings: JSON.parse("[\"where\",\"User.findUnique\",\"User.findUniqueOrThrow\",\"orderBy\",\"cursor\",\"User.findFirst\",\"User.findFirstOrThrow\",\"User.findMany\",\"data\",\"User.createOne\",\"User.createMany\",\"User.createManyAndReturn\",\"User.updateOne\",\"User.updateMany\",\"User.updateManyAndReturn\",\"create\",\"update\",\"User.upsertOne\",\"User.deleteOne\",\"User.deleteMany\",\"having\",\"_count\",\"_avg\",\"_sum\",\"_min\",\"_max\",\"User.groupBy\",\"User.aggregate\",\"Post.findUnique\",\"Post.findUniqueOrThrow\",\"Post.findFirst\",\"Post.findFirstOrThrow\",\"Post.findMany\",\"Post.createOne\",\"Post.createMany\",\"Post.createManyAndReturn\",\"Post.updateOne\",\"Post.updateMany\",\"Post.updateManyAndReturn\",\"Post.upsertOne\",\"Post.deleteOne\",\"Post.deleteMany\",\"Post.groupBy\",\"Post.aggregate\",\"Config.findUnique\",\"Config.findUniqueOrThrow\",\"Config.findFirst\",\"Config.findFirstOrThrow\",\"Config.findMany\",\"Config.createOne\",\"Config.createMany\",\"Config.createManyAndReturn\",\"Config.updateOne\",\"Config.updateMany\",\"Config.updateManyAndReturn\",\"Config.upsertOne\",\"Config.deleteOne\",\"Config.deleteMany\",\"Config.groupBy\",\"Config.aggregate\",\"Trend.findUnique\",\"Trend.findUniqueOrThrow\",\"Trend.findFirst\",\"Trend.findFirstOrThrow\",\"Trend.findMany\",\"Trend.createOne\",\"Trend.createMany\",\"Trend.createManyAndReturn\",\"Trend.updateOne\",\"Trend.updateMany\",\"Trend.updateManyAndReturn\",\"Trend.upsertOne\",\"Trend.deleteOne\",\"Trend.deleteMany\",\"Trend.groupBy\",\"Trend.aggregate\",\"AND\",\"OR\",\"NOT\",\"id\",\"title\",\"description\",\"url\",\"score\",\"source\",\"category\",\"geo\",\"tags\",\"engagement\",\"raw\",\"fingerprint\",\"sourceTimestamp\",\"createdAt\",\"updatedAt\",\"equals\",\"in\",\"notIn\",\"lt\",\"lte\",\"gt\",\"gte\",\"not\",\"string_contains\",\"string_starts_with\",\"string_ends_with\",\"array_starts_with\",\"array_ends_with\",\"array_contains\",\"contains\",\"startsWith\",\"endsWith\",\"key\",\"value\",\"prompt\",\"content\",\"imageUrl\",\"style\",\"tone\",\"isFbPostState\",\"isTwitterPostState\",\"isTiktokPostState\",\"draft\",\"tokensUsed\",\"model\",\"reviewNotes\",\"status\",\"email\",\"name\",\"set\",\"increment\",\"decrement\",\"multiply\",\"divide\"]"), + graph: "nwEpQAZMAACFAQAwTQAABAAQTgAAhQEAME8CAAAAAX4BAAAAAX8BAHwAIQEAAAABACABAAAAAQAgBkwAAIUBADBNAAAEABBOAACFAQAwTwIAegAhfgEAewAhfwEAfAAhAX8AAIYBACADAAAABAAgAwAABQAwBAAAAQAgAwAAAAQAIAMAAAUAMAQAAAEAIAMAAAAEACADAAAFADAEAAABACADTwIAAAABfgEAAAABfwEAAAABAQgAAAkAIANPAgAAAAF-AQAAAAF_AQAAAAEBCAAACwAwAQgAAAsAMANPAgCOAQAhfgEAjAEAIX8BAI0BACECAAAAAQAgCAAADgAgA08CAI4BACF-AQCMAQAhfwEAjQEAIQIAAAAEACAIAAAQACACAAAABAAgCAAAEAAgAwAAAAEAIA8AAAkAIBAAAA4AIAEAAAABACABAAAABAAgBhUAAJsBACAWAACcAQAgFwAAnwEAIBgAAJ4BACAZAACdAQAgfwAAhgEAIAZMAACEAQAwTQAAFwAQTgAAhAEAME8CAGYAIX4BAGcAIX8BAGgAIQMAAAAEACADAAAWADAUAAAXACADAAAABAAgAwAABQAwBAAAAQAgFEwAAIMBADBNAAAdABBOAACDAQAwTwIAAAABUAEAewAhXEAAfwAhXUAAfwAhcQEAewAhcgEAewAhcwEAfAAhdAEAewAhdQEAfAAhdgIAegAhdwIAegAheAIAegAheQEAfAAhegIAegAhewEAfAAhfAEAfAAhfQEAewAhAQAAABoAIAEAAAAaACAUTAAAgwEAME0AAB0AEE4AAIMBADBPAgB6ACFQAQB7ACFcQAB_ACFdQAB_ACFxAQB7ACFyAQB7ACFzAQB8ACF0AQB7ACF1AQB8ACF2AgB6ACF3AgB6ACF4AgB6ACF5AQB8ACF6AgB6ACF7AQB8ACF8AQB8ACF9AQB7ACEFcwAAhgEAIHUAAIYBACB5AACGAQAgewAAhgEAIHwAAIYBACADAAAAHQAgAwAAHgAwBAAAGgAgAwAAAB0AIAMAAB4AMAQAABoAIAMAAAAdACADAAAeADAEAAAaACARTwIAAAABUAEAAAABXEAAAAABXUAAAAABcQEAAAABcgEAAAABcwEAAAABdAEAAAABdQEAAAABdgIAAAABdwIAAAABeAIAAAABeQEAAAABegIAAAABewEAAAABfAEAAAABfQEAAAABAQgAACIAIBFPAgAAAAFQAQAAAAFcQAAAAAFdQAAAAAFxAQAAAAFyAQAAAAFzAQAAAAF0AQAAAAF1AQAAAAF2AgAAAAF3AgAAAAF4AgAAAAF5AQAAAAF6AgAAAAF7AQAAAAF8AQAAAAF9AQAAAAEBCAAAJAAwAQgAACQAMBFPAgCOAQAhUAEAjAEAIVxAAJABACFdQACQAQAhcQEAjAEAIXIBAIwBACFzAQCNAQAhdAEAjAEAIXUBAI0BACF2AgCOAQAhdwIAjgEAIXgCAI4BACF5AQCNAQAhegIAjgEAIXsBAI0BACF8AQCNAQAhfQEAjAEAIQIAAAAaACAIAAAnACARTwIAjgEAIVABAIwBACFcQACQAQAhXUAAkAEAIXEBAIwBACFyAQCMAQAhcwEAjQEAIXQBAIwBACF1AQCNAQAhdgIAjgEAIXcCAI4BACF4AgCOAQAheQEAjQEAIXoCAI4BACF7AQCNAQAhfAEAjQEAIX0BAIwBACECAAAAHQAgCAAAKQAgAgAAAB0AIAgAACkAIAMAAAAaACAPAAAiACAQAAAnACABAAAAGgAgAQAAAB0AIAoVAACWAQAgFgAAlwEAIBcAAJoBACAYAACZAQAgGQAAmAEAIHMAAIYBACB1AACGAQAgeQAAhgEAIHsAAIYBACB8AACGAQAgFEwAAIIBADBNAAAwABBOAACCAQAwTwIAZgAhUAEAZwAhXEAAawAhXUAAawAhcQEAZwAhcgEAZwAhcwEAaAAhdAEAZwAhdQEAaAAhdgIAZgAhdwIAZgAheAIAZgAheQEAaAAhegIAZgAhewEAaAAhfAEAaAAhfQEAZwAhAwAAAB0AIAMAAC8AMBQAADAAIAMAAAAdACADAAAeADAEAAAaACAGTAAAgQEAME0AADYAEE4AAIEBADBPAgAAAAFvAQAAAAFwAQB7ACEBAAAAMwAgAQAAADMAIAZMAACBAQAwTQAANgAQTgAAgQEAME8CAHoAIW8BAHsAIXABAHsAIQADAAAANgAgAwAANwAwBAAAMwAgAwAAADYAIAMAADcAMAQAADMAIAMAAAA2ACADAAA3ADAEAAAzACADTwIAAAABbwEAAAABcAEAAAABAQgAADsAIANPAgAAAAFvAQAAAAFwAQAAAAEBCAAAPQAwAQgAAD0AMANPAgCOAQAhbwEAjAEAIXABAIwBACECAAAAMwAgCAAAQAAgA08CAI4BACFvAQCMAQAhcAEAjAEAIQIAAAA2ACAIAABCACACAAAANgAgCAAAQgAgAwAAADMAIA8AADsAIBAAAEAAIAEAAAAzACABAAAANgAgBRUAAJEBACAWAACSAQAgFwAAlQEAIBgAAJQBACAZAACTAQAgBkwAAIABADBNAABJABBOAACAAQAwTwIAZgAhbwEAZwAhcAEAZwAhAwAAADYAIAMAAEgAMBQAAEkAIAMAAAA2ACADAAA3ADAEAAAzACASTAAAeQAwTQAATwAQTgAAeQAwTwIAAAABUAEAewAhUQEAfAAhUgEAfAAhUwIAegAhVAEAewAhVQEAfAAhVgEAfAAhVwAAfQAgWAAAfQAgWQAAfQAgWgEAfAAhW0AAfgAhXEAAfwAhXUAAfgAhAQAAAEwAIAEAAABMACASTAAAeQAwTQAATwAQTgAAeQAwTwIAegAhUAEAewAhUQEAfAAhUgEAfAAhUwIAegAhVAEAewAhVQEAfAAhVgEAfAAhVwAAfQAgWAAAfQAgWQAAfQAgWgEAfAAhW0AAfgAhXEAAfwAhXUAAfgAhClEAAIYBACBSAACGAQAgVQAAhgEAIFYAAIYBACBXAACGAQAgWAAAhgEAIFkAAIYBACBaAACGAQAgWwAAhgEAIF0AAIYBACADAAAATwAgAwAAUAAwBAAATAAgAwAAAE8AIAMAAFAAMAQAAEwAIAMAAABPACADAABQADAEAABMACAPTwIAAAABUAEAAAABUQEAAAABUgEAAAABUwIAAAABVAEAAAABVQEAAAABVgEAAAABV4AAAAABWIAAAAABWYAAAAABWgEAAAABW0AAAAABXEAAAAABXUAAAAABAQgAAFQAIA9PAgAAAAFQAQAAAAFRAQAAAAFSAQAAAAFTAgAAAAFUAQAAAAFVAQAAAAFWAQAAAAFXgAAAAAFYgAAAAAFZgAAAAAFaAQAAAAFbQAAAAAFcQAAAAAFdQAAAAAEBCAAAVgAwAQgAAFYAMA9PAgCOAQAhUAEAjAEAIVEBAI0BACFSAQCNAQAhUwIAjgEAIVQBAIwBACFVAQCNAQAhVgEAjQEAIVeAAAAAAViAAAAAAVmAAAAAAVoBAI0BACFbQACPAQAhXEAAkAEAIV1AAI8BACECAAAATAAgCAAAWQAgD08CAI4BACFQAQCMAQAhUQEAjQEAIVIBAI0BACFTAgCOAQAhVAEAjAEAIVUBAI0BACFWAQCNAQAhV4AAAAABWIAAAAABWYAAAAABWgEAjQEAIVtAAI8BACFcQACQAQAhXUAAjwEAIQIAAABPACAIAABbACACAAAATwAgCAAAWwAgAwAAAEwAIA8AAFQAIBAAAFkAIAEAAABMACABAAAATwAgDxUAAIcBACAWAACIAQAgFwAAiwEAIBgAAIoBACAZAACJAQAgUQAAhgEAIFIAAIYBACBVAACGAQAgVgAAhgEAIFcAAIYBACBYAACGAQAgWQAAhgEAIFoAAIYBACBbAACGAQAgXQAAhgEAIBJMAABlADBNAABiABBOAABlADBPAgBmACFQAQBnACFRAQBoACFSAQBoACFTAgBmACFUAQBnACFVAQBoACFWAQBoACFXAABpACBYAABpACBZAABpACBaAQBoACFbQABqACFcQABrACFdQABqACEDAAAATwAgAwAAYQAwFAAAYgAgAwAAAE8AIAMAAFAAMAQAAEwAIBJMAABlADBNAABiABBOAABlADBPAgBmACFQAQBnACFRAQBoACFSAQBoACFTAgBmACFUAQBnACFVAQBoACFWAQBoACFXAABpACBYAABpACBZAABpACBaAQBoACFbQABqACFcQABrACFdQABqACENFQAAbQAgFgAAeAAgFwAAbQAgGAAAbQAgGQAAbQAgXgIAAAABXwIAAAAEYAIAAAAEYQIAAAABYgIAAAABYwIAAAABZAIAAAABZQIAdwAhDhUAAG0AIBgAAHYAIBkAAHYAIF4BAAAAAV8BAAAABGABAAAABGEBAAAAAWIBAAAAAWMBAAAAAWQBAAAAAWUBAHUAIWwBAAAAAW0BAAAAAW4BAAAAAQ4VAABwACAYAAB0ACAZAAB0ACBeAQAAAAFfAQAAAAVgAQAAAAVhAQAAAAFiAQAAAAFjAQAAAAFkAQAAAAFlAQBzACFsAQAAAAFtAQAAAAFuAQAAAAEPFQAAcAAgGAAAcgAgGQAAcgAgXoAAAAABYYAAAAABYoAAAAABY4AAAAABZIAAAAABZYAAAAABZgEAAAABZwEAAAABaAEAAAABaYAAAAABaoAAAAABa4AAAAABCxUAAHAAIBgAAHEAIBkAAHEAIF5AAAAAAV9AAAAABWBAAAAABWFAAAAAAWJAAAAAAWNAAAAAAWRAAAAAAWVAAG8AIQsVAABtACAYAABuACAZAABuACBeQAAAAAFfQAAAAARgQAAAAARhQAAAAAFiQAAAAAFjQAAAAAFkQAAAAAFlQABsACELFQAAbQAgGAAAbgAgGQAAbgAgXkAAAAABX0AAAAAEYEAAAAAEYUAAAAABYkAAAAABY0AAAAABZEAAAAABZUAAbAAhCF4CAAAAAV8CAAAABGACAAAABGECAAAAAWICAAAAAWMCAAAAAWQCAAAAAWUCAG0AIQheQAAAAAFfQAAAAARgQAAAAARhQAAAAAFiQAAAAAFjQAAAAAFkQAAAAAFlQABuACELFQAAcAAgGAAAcQAgGQAAcQAgXkAAAAABX0AAAAAFYEAAAAAFYUAAAAABYkAAAAABY0AAAAABZEAAAAABZUAAbwAhCF4CAAAAAV8CAAAABWACAAAABWECAAAAAWICAAAAAWMCAAAAAWQCAAAAAWUCAHAAIQheQAAAAAFfQAAAAAVgQAAAAAVhQAAAAAFiQAAAAAFjQAAAAAFkQAAAAAFlQABxACEMXoAAAAABYYAAAAABYoAAAAABY4AAAAABZIAAAAABZYAAAAABZgEAAAABZwEAAAABaAEAAAABaYAAAAABaoAAAAABa4AAAAABDhUAAHAAIBgAAHQAIBkAAHQAIF4BAAAAAV8BAAAABWABAAAABWEBAAAAAWIBAAAAAWMBAAAAAWQBAAAAAWUBAHMAIWwBAAAAAW0BAAAAAW4BAAAAAQteAQAAAAFfAQAAAAVgAQAAAAVhAQAAAAFiAQAAAAFjAQAAAAFkAQAAAAFlAQB0ACFsAQAAAAFtAQAAAAFuAQAAAAEOFQAAbQAgGAAAdgAgGQAAdgAgXgEAAAABXwEAAAAEYAEAAAAEYQEAAAABYgEAAAABYwEAAAABZAEAAAABZQEAdQAhbAEAAAABbQEAAAABbgEAAAABC14BAAAAAV8BAAAABGABAAAABGEBAAAAAWIBAAAAAWMBAAAAAWQBAAAAAWUBAHYAIWwBAAAAAW0BAAAAAW4BAAAAAQ0VAABtACAWAAB4ACAXAABtACAYAABtACAZAABtACBeAgAAAAFfAgAAAARgAgAAAARhAgAAAAFiAgAAAAFjAgAAAAFkAgAAAAFlAgB3ACEIXggAAAABXwgAAAAEYAgAAAAEYQgAAAABYggAAAABYwgAAAABZAgAAAABZQgAeAAhEkwAAHkAME0AAE8AEE4AAHkAME8CAHoAIVABAHsAIVEBAHwAIVIBAHwAIVMCAHoAIVQBAHsAIVUBAHwAIVYBAHwAIVcAAH0AIFgAAH0AIFkAAH0AIFoBAHwAIVtAAH4AIVxAAH8AIV1AAH4AIQheAgAAAAFfAgAAAARgAgAAAARhAgAAAAFiAgAAAAFjAgAAAAFkAgAAAAFlAgBtACELXgEAAAABXwEAAAAEYAEAAAAEYQEAAAABYgEAAAABYwEAAAABZAEAAAABZQEAdgAhbAEAAAABbQEAAAABbgEAAAABC14BAAAAAV8BAAAABWABAAAABWEBAAAAAWIBAAAAAWMBAAAAAWQBAAAAAWUBAHQAIWwBAAAAAW0BAAAAAW4BAAAAAQxegAAAAAFhgAAAAAFigAAAAAFjgAAAAAFkgAAAAAFlgAAAAAFmAQAAAAFnAQAAAAFoAQAAAAFpgAAAAAFqgAAAAAFrgAAAAAEIXkAAAAABX0AAAAAFYEAAAAAFYUAAAAABYkAAAAABY0AAAAABZEAAAAABZUAAcQAhCF5AAAAAAV9AAAAABGBAAAAABGFAAAAAAWJAAAAAAWNAAAAAAWRAAAAAAWVAAG4AIQZMAACAAQAwTQAASQAQTgAAgAEAME8CAGYAIW8BAGcAIXABAGcAIQZMAACBAQAwTQAANgAQTgAAgQEAME8CAHoAIW8BAHsAIXABAHsAIRRMAACCAQAwTQAAMAAQTgAAggEAME8CAGYAIVABAGcAIVxAAGsAIV1AAGsAIXEBAGcAIXIBAGcAIXMBAGgAIXQBAGcAIXUBAGgAIXYCAGYAIXcCAGYAIXgCAGYAIXkBAGgAIXoCAGYAIXsBAGgAIXwBAGgAIX0BAGcAIRRMAACDAQAwTQAAHQAQTgAAgwEAME8CAHoAIVABAHsAIVxAAH8AIV1AAH8AIXEBAHsAIXIBAHsAIXMBAHwAIXQBAHsAIXUBAHwAIXYCAHoAIXcCAHoAIXgCAHoAIXkBAHwAIXoCAHoAIXsBAHwAIXwBAHwAIX0BAHsAIQZMAACEAQAwTQAAFwAQTgAAhAEAME8CAGYAIX4BAGcAIX8BAGgAIQZMAACFAQAwTQAABAAQTgAAhQEAME8CAHoAIX4BAHsAIX8BAHwAIQAAAAAAAAGAAQEAAAABAYABAQAAAAEFgAECAAAAAYEBAgAAAAGCAQIAAAABgwECAAAAAYQBAgAAAAEBgAFAAAAAAQGAAUAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAUVAAYWAAcXAAgYAAkZAAoAAAAAAAUVAAYWAAcXAAgYAAkZAAoAAAAFFQAQFgARFwASGAATGQAUAAAAAAAFFQAQFgARFwASGAATGQAUAAAABRUAGhYAGxcAHBgAHRkAHgAAAAAABRUAGhYAGxcAHBgAHRkAHgAAAAUVACQWACUXACYYACcZACgAAAAAAAUVACQWACUXACYYACcZACgBAgECAwEFBgEGBwEHCAEJCgEKDAILDQMMDwENEQIOEgQREwESFAETFQIaGAUbGQscGwwdHAweHwwfIAwgIQwhIwwiJQIjJg0kKAwlKgImKw4nLAwoLQwpLgIqMQ8rMhUsNBYtNRYuOBYvORYwOhYxPBYyPgIzPxc0QRY1QwI2RBg3RRY4RhY5RwI6Shk7Sx88TSA9TiA-USA_UiBAUyBBVSBCVwJDWCFEWiBFXAJGXSJHXiBIXyBJYAJKYyNLZCk" +} + +async function decodeBase64AsWasm(wasmBase64: string): Promise { + const { Buffer } = await import('node:buffer') + const wasmArray = Buffer.from(wasmBase64, 'base64') + return new WebAssembly.Module(wasmArray) +} + +config.compilerWasm = { + getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.js"), + + getQueryCompilerWasmModule: async () => { + const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.postgresql.wasm-base64.js") + return await decodeBase64AsWasm(wasm) + }, + + importName: "./query_compiler_fast_bg.js" +} + + + +export type LogOptions = + 'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array ? Prisma.GetEvents : never : never + +export interface PrismaClientConstructor { + /** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient({ + * adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) + * }) + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://pris.ly/d/client). + */ + + new < + Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions, + LogOpts extends LogOptions = LogOptions, + OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'], + ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs + >(options: Prisma.Subset ): PrismaClient +} + +/** + * ## Prisma Client + * + * Type-safe database client for TypeScript + * @example + * ``` + * const prisma = new PrismaClient({ + * adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) + * }) + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + * + * Read more in our [docs](https://pris.ly/d/client). + */ + +export interface PrismaClient< + in LogOpts extends Prisma.LogLevel = never, + in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined, + in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs +> { + [K: symbol]: { types: Prisma.TypeMap['other'] } + + $on(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient; + + /** + * Connect with the database + */ + $connect(): runtime.Types.Utils.JsPromise; + + /** + * Disconnect from the database + */ + $disconnect(): runtime.Types.Utils.JsPromise; + +/** + * Executes a prepared raw query and returns the number of affected rows. + * @example + * ``` + * const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};` + * ``` + * + * Read more in our [docs](https://pris.ly/d/raw-queries). + */ + $executeRaw(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise; + + /** + * Executes a raw query and returns the number of affected rows. + * Susceptible to SQL injections, see documentation. + * @example + * ``` + * const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com') + * ``` + * + * Read more in our [docs](https://pris.ly/d/raw-queries). + */ + $executeRawUnsafe(query: string, ...values: any[]): Prisma.PrismaPromise; + + /** + * Performs a prepared raw query and returns the `SELECT` data. + * @example + * ``` + * const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};` + * ``` + * + * Read more in our [docs](https://pris.ly/d/raw-queries). + */ + $queryRaw(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise; + + /** + * Performs a raw query and returns the `SELECT` data. + * Susceptible to SQL injections, see documentation. + * @example + * ``` + * const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com') + * ``` + * + * Read more in our [docs](https://pris.ly/d/raw-queries). + */ + $queryRawUnsafe(query: string, ...values: any[]): Prisma.PrismaPromise; + + + /** + * Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole. + * @example + * ``` + * const [george, bob, alice] = await prisma.$transaction([ + * prisma.user.create({ data: { name: 'George' } }), + * prisma.user.create({ data: { name: 'Bob' } }), + * prisma.user.create({ data: { name: 'Alice' } }), + * ]) + * ``` + * + * Read more in our [docs](https://www.prisma.io/docs/orm/prisma-client/queries/transactions). + */ + $transaction

[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise> + + $transaction(fn: (prisma: Omit) => runtime.Types.Utils.JsPromise, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise + + $extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb, ExtArgs, runtime.Types.Utils.Call, { + extArgs: ExtArgs + }>> + + /** + * `prisma.user`: Exposes CRUD operations for the **User** model. + * Example usage: + * ```ts + * // Fetch zero or more Users + * const users = await prisma.user.findMany() + * ``` + */ + get user(): Prisma.UserDelegate; + + /** + * `prisma.post`: Exposes CRUD operations for the **Post** model. + * Example usage: + * ```ts + * // Fetch zero or more Posts + * const posts = await prisma.post.findMany() + * ``` + */ + get post(): Prisma.PostDelegate; + + /** + * `prisma.config`: Exposes CRUD operations for the **Config** model. + * Example usage: + * ```ts + * // Fetch zero or more Configs + * const configs = await prisma.config.findMany() + * ``` + */ + get config(): Prisma.ConfigDelegate; + + /** + * `prisma.trend`: Exposes CRUD operations for the **Trend** model. + * Example usage: + * ```ts + * // Fetch zero or more Trends + * const trends = await prisma.trend.findMany() + * ``` + */ + get trend(): Prisma.TrendDelegate; +} + +export function getPrismaClientClass(): PrismaClientConstructor { + return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor +} diff --git a/src/generated/prisma/internal/prismaNamespace.ts b/src/generated/prisma/internal/prismaNamespace.ts new file mode 100644 index 0000000..61124a0 --- /dev/null +++ b/src/generated/prisma/internal/prismaNamespace.ts @@ -0,0 +1,1084 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file. + * While this enables partial backward compatibility, it is not part of the stable public API. + * + * If you are looking for your Models, Enums, and Input Types, please import them from the respective + * model files in the `model` directory! + */ + +import * as runtime from "@prisma/client/runtime/client" +import type * as Prisma from "../models.js" +import { type PrismaClient } from "./class.js" + +export type * from '../models.js' + +export type DMMF = typeof runtime.DMMF + +export type PrismaPromise = runtime.Types.Public.PrismaPromise + +/** + * Prisma Errors + */ + +export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError +export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError + +export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError +export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError + +export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError +export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError + +export const PrismaClientInitializationError = runtime.PrismaClientInitializationError +export type PrismaClientInitializationError = runtime.PrismaClientInitializationError + +export const PrismaClientValidationError = runtime.PrismaClientValidationError +export type PrismaClientValidationError = runtime.PrismaClientValidationError + +/** + * Re-export of sql-template-tag + */ +export const sql = runtime.sqltag +export const empty = runtime.empty +export const join = runtime.join +export const raw = runtime.raw +export const Sql = runtime.Sql +export type Sql = runtime.Sql + + + +/** + * Decimal.js + */ +export const Decimal = runtime.Decimal +export type Decimal = runtime.Decimal + +export type DecimalJsLike = runtime.DecimalJsLike + +/** +* Extensions +*/ +export type Extension = runtime.Types.Extensions.UserArgs +export const getExtensionContext = runtime.Extensions.getExtensionContext +export type Args = runtime.Types.Public.Args +export type Payload = runtime.Types.Public.Payload +export type Result = runtime.Types.Public.Result +export type Exact = runtime.Types.Public.Exact + +export type PrismaVersion = { + client: string + engine: string +} + +/** + * Prisma Client JS version: 7.7.0 + * Query Engine version: 75cbdc1eb7150937890ad5465d861175c6624711 + */ +export const prismaVersion: PrismaVersion = { + client: "7.7.0", + engine: "75cbdc1eb7150937890ad5465d861175c6624711" +} + +/** + * Utility Types + */ + +export type Bytes = runtime.Bytes +export type JsonObject = runtime.JsonObject +export type JsonArray = runtime.JsonArray +export type JsonValue = runtime.JsonValue +export type InputJsonObject = runtime.InputJsonObject +export type InputJsonArray = runtime.InputJsonArray +export type InputJsonValue = runtime.InputJsonValue + + +export const NullTypes = { + DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull), + JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull), + AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull), +} +/** + * Helper for filtering JSON entries that have `null` on the database (empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const DbNull = runtime.DbNull + +/** + * Helper for filtering JSON entries that have JSON `null` values (not empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const JsonNull = runtime.JsonNull + +/** + * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull` + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const AnyNull = runtime.AnyNull + + +type SelectAndInclude = { + select: any + include: any +} + +type SelectAndOmit = { + select: any + omit: any +} + +/** + * From T, pick a set of properties whose keys are in the union K + */ +type Prisma__Pick = { + [P in K]: T[P]; +}; + +export type Enumerable = T | Array; + +/** + * Subset + * @desc From `T` pick properties that exist in `U`. Simple version of Intersection + */ +export type Subset = { + [key in keyof T]: key extends keyof U ? T[key] : never; +}; + +/** + * SelectSubset + * @desc From `T` pick properties that exist in `U`. Simple version of Intersection. + * Additionally, it validates, if both select and include are present. If the case, it errors. + */ +export type SelectSubset = { + [key in keyof T]: key extends keyof U ? T[key] : never +} & + (T extends SelectAndInclude + ? 'Please either choose `select` or `include`.' + : T extends SelectAndOmit + ? 'Please either choose `select` or `omit`.' + : {}) + +/** + * Subset + Intersection + * @desc From `T` pick properties that exist in `U` and intersect `K` + */ +export type SubsetIntersection = { + [key in keyof T]: key extends keyof U ? T[key] : never +} & + K + +type Without = { [P in Exclude]?: never }; + +/** + * XOR is needed to have a real mutually exclusive union type + * https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types + */ +export type XOR = + T extends object ? + U extends object ? + (Without & U) | (Without & T) + : U : T + + +/** + * Is T a Record? + */ +type IsObject = T extends Array +? False +: T extends Date +? False +: T extends Uint8Array +? False +: T extends BigInt +? False +: T extends object +? True +: False + + +/** + * If it's T[], return T + */ +export type UnEnumerate = T extends Array ? U : T + +/** + * From ts-toolbelt + */ + +type __Either = Omit & + { + // Merge all but K + [P in K]: Prisma__Pick // With K possibilities + }[K] + +type EitherStrict = Strict<__Either> + +type EitherLoose = ComputeRaw<__Either> + +type _Either< + O extends object, + K extends Key, + strict extends Boolean +> = { + 1: EitherStrict + 0: EitherLoose +}[strict] + +export type Either< + O extends object, + K extends Key, + strict extends Boolean = 1 +> = O extends unknown ? _Either : never + +export type Union = any + +export type PatchUndefined = { + [K in keyof O]: O[K] extends undefined ? At : O[K] +} & {} + +/** Helper Types for "Merge" **/ +export type IntersectOf = ( + U extends unknown ? (k: U) => void : never +) extends (k: infer I) => void + ? I + : never + +export type Overwrite = { + [K in keyof O]: K extends keyof O1 ? O1[K] : O[K]; +} & {}; + +type _Merge = IntersectOf; +}>>; + +type Key = string | number | symbol; +type AtStrict = O[K & keyof O]; +type AtLoose = O extends unknown ? AtStrict : never; +export type At = { + 1: AtStrict; + 0: AtLoose; +}[strict]; + +export type ComputeRaw = A extends Function ? A : { + [K in keyof A]: A[K]; +} & {}; + +export type OptionalFlat = { + [K in keyof O]?: O[K]; +} & {}; + +type _Record = { + [P in K]: T; +}; + +// cause typescript not to expand types and preserve names +type NoExpand = T extends unknown ? T : never; + +// this type assumes the passed object is entirely optional +export type AtLeast = NoExpand< + O extends unknown + ? | (K extends keyof O ? { [P in K]: O[P] } & O : O) + | {[P in keyof O as P extends K ? P : never]-?: O[P]} & O + : never>; + +type _Strict = U extends unknown ? U & OptionalFlat<_Record, keyof U>, never>> : never; + +export type Strict = ComputeRaw<_Strict>; +/** End Helper Types for "Merge" **/ + +export type Merge = ComputeRaw<_Merge>>; + +export type Boolean = True | False + +export type True = 1 + +export type False = 0 + +export type Not = { + 0: 1 + 1: 0 +}[B] + +export type Extends = [A1] extends [never] + ? 0 // anything `never` is false + : A1 extends A2 + ? 1 + : 0 + +export type Has = Not< + Extends, U1> +> + +export type Or = { + 0: { + 0: 0 + 1: 1 + } + 1: { + 0: 1 + 1: 1 + } +}[B1][B2] + +export type Keys = U extends unknown ? keyof U : never + +export type GetScalarType = O extends object ? { + [P in keyof T]: P extends keyof O + ? O[P] + : never +} : never + +type FieldPaths< + T, + U = Omit +> = IsObject extends True ? U : T + +export type GetHavingFields = { + [K in keyof T]: Or< + Or, Extends<'AND', K>>, + Extends<'NOT', K> + > extends True + ? // infer is only needed to not hit TS limit + // based on the brilliant idea of Pierre-Antoine Mills + // https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437 + T[K] extends infer TK + ? GetHavingFields extends object ? Merge> : never> + : never + : {} extends FieldPaths + ? never + : K +}[keyof T] + +/** + * Convert tuple to union + */ +type _TupleToUnion = T extends (infer E)[] ? E : never +type TupleToUnion = _TupleToUnion +export type MaybeTupleToUnion = T extends any[] ? TupleToUnion : T + +/** + * Like `Pick`, but additionally can also accept an array of keys + */ +export type PickEnumerable | keyof T> = Prisma__Pick> + +/** + * Exclude all keys with underscores + */ +export type ExcludeUnderscoreKeys = T extends `_${string}` ? never : T + + +export type FieldRef = runtime.FieldRef + +type FieldRefInputType = Model extends never ? never : FieldRef + + +export const ModelName = { + User: 'User', + Post: 'Post', + Config: 'Config', + Trend: 'Trend' +} as const + +export type ModelName = (typeof ModelName)[keyof typeof ModelName] + + + +export interface TypeMapCb extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record> { + returns: TypeMap +} + +export type TypeMap = { + globalOmitOptions: { + omit: GlobalOmitOptions + } + meta: { + modelProps: "user" | "post" | "config" | "trend" + txIsolationLevel: TransactionIsolationLevel + } + model: { + User: { + payload: Prisma.$UserPayload + fields: Prisma.UserFieldRefs + operations: { + findUnique: { + args: Prisma.UserFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.UserFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.UserFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.UserFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.UserFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.UserCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.UserCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.UserCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.UserDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.UserUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.UserDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.UserUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.UserUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.UserUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.UserAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.UserGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.UserCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + Post: { + payload: Prisma.$PostPayload + fields: Prisma.PostFieldRefs + operations: { + findUnique: { + args: Prisma.PostFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.PostFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.PostFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.PostFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.PostFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.PostCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.PostCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.PostCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.PostDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.PostUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.PostDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.PostUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.PostUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.PostUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.PostAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.PostGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.PostCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + Config: { + payload: Prisma.$ConfigPayload + fields: Prisma.ConfigFieldRefs + operations: { + findUnique: { + args: Prisma.ConfigFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.ConfigFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.ConfigFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.ConfigFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.ConfigFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.ConfigCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.ConfigCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.ConfigCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.ConfigDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.ConfigUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.ConfigDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.ConfigUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.ConfigUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.ConfigUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.ConfigAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.ConfigGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.ConfigCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + Trend: { + payload: Prisma.$TrendPayload + fields: Prisma.TrendFieldRefs + operations: { + findUnique: { + args: Prisma.TrendFindUniqueArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findUniqueOrThrow: { + args: Prisma.TrendFindUniqueOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findFirst: { + args: Prisma.TrendFindFirstArgs + result: runtime.Types.Utils.PayloadToResult | null + } + findFirstOrThrow: { + args: Prisma.TrendFindFirstOrThrowArgs + result: runtime.Types.Utils.PayloadToResult + } + findMany: { + args: Prisma.TrendFindManyArgs + result: runtime.Types.Utils.PayloadToResult[] + } + create: { + args: Prisma.TrendCreateArgs + result: runtime.Types.Utils.PayloadToResult + } + createMany: { + args: Prisma.TrendCreateManyArgs + result: BatchPayload + } + createManyAndReturn: { + args: Prisma.TrendCreateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + delete: { + args: Prisma.TrendDeleteArgs + result: runtime.Types.Utils.PayloadToResult + } + update: { + args: Prisma.TrendUpdateArgs + result: runtime.Types.Utils.PayloadToResult + } + deleteMany: { + args: Prisma.TrendDeleteManyArgs + result: BatchPayload + } + updateMany: { + args: Prisma.TrendUpdateManyArgs + result: BatchPayload + } + updateManyAndReturn: { + args: Prisma.TrendUpdateManyAndReturnArgs + result: runtime.Types.Utils.PayloadToResult[] + } + upsert: { + args: Prisma.TrendUpsertArgs + result: runtime.Types.Utils.PayloadToResult + } + aggregate: { + args: Prisma.TrendAggregateArgs + result: runtime.Types.Utils.Optional + } + groupBy: { + args: Prisma.TrendGroupByArgs + result: runtime.Types.Utils.Optional[] + } + count: { + args: Prisma.TrendCountArgs + result: runtime.Types.Utils.Optional | number + } + } + } + } +} & { + other: { + payload: any + operations: { + $executeRaw: { + args: [query: TemplateStringsArray | Sql, ...values: any[]], + result: any + } + $executeRawUnsafe: { + args: [query: string, ...values: any[]], + result: any + } + $queryRaw: { + args: [query: TemplateStringsArray | Sql, ...values: any[]], + result: any + } + $queryRawUnsafe: { + args: [query: string, ...values: any[]], + result: any + } + } + } +} + +/** + * Enums + */ + +export const TransactionIsolationLevel = runtime.makeStrictEnum({ + ReadUncommitted: 'ReadUncommitted', + ReadCommitted: 'ReadCommitted', + RepeatableRead: 'RepeatableRead', + Serializable: 'Serializable' +} as const) + +export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] + + +export const UserScalarFieldEnum = { + id: 'id', + email: 'email', + name: 'name' +} as const + +export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum] + + +export const PostScalarFieldEnum = { + id: 'id', + title: 'title', + prompt: 'prompt', + content: 'content', + imageUrl: 'imageUrl', + style: 'style', + tone: 'tone', + isFbPostState: 'isFbPostState', + isTwitterPostState: 'isTwitterPostState', + isTiktokPostState: 'isTiktokPostState', + draft: 'draft', + tokensUsed: 'tokensUsed', + model: 'model', + reviewNotes: 'reviewNotes', + status: 'status', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type PostScalarFieldEnum = (typeof PostScalarFieldEnum)[keyof typeof PostScalarFieldEnum] + + +export const ConfigScalarFieldEnum = { + id: 'id', + key: 'key', + value: 'value' +} as const + +export type ConfigScalarFieldEnum = (typeof ConfigScalarFieldEnum)[keyof typeof ConfigScalarFieldEnum] + + +export const TrendScalarFieldEnum = { + id: 'id', + title: 'title', + description: 'description', + url: 'url', + score: 'score', + source: 'source', + category: 'category', + geo: 'geo', + tags: 'tags', + engagement: 'engagement', + raw: 'raw', + fingerprint: 'fingerprint', + sourceTimestamp: 'sourceTimestamp', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type TrendScalarFieldEnum = (typeof TrendScalarFieldEnum)[keyof typeof TrendScalarFieldEnum] + + +export const SortOrder = { + asc: 'asc', + desc: 'desc' +} as const + +export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder] + + +export const NullableJsonNullValueInput = { + DbNull: DbNull, + JsonNull: JsonNull +} as const + +export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput] + + +export const QueryMode = { + default: 'default', + insensitive: 'insensitive' +} as const + +export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] + + +export const NullsOrder = { + first: 'first', + last: 'last' +} as const + +export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder] + + +export const JsonNullValueFilter = { + DbNull: DbNull, + JsonNull: JsonNull, + AnyNull: AnyNull +} as const + +export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter] + + + +/** + * Field references + */ + + +/** + * Reference to a field of type 'Int' + */ +export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'> + + + +/** + * Reference to a field of type 'Int[]' + */ +export type ListIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int[]'> + + + +/** + * Reference to a field of type 'String' + */ +export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'> + + + +/** + * Reference to a field of type 'String[]' + */ +export type ListStringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String[]'> + + + +/** + * Reference to a field of type 'DateTime' + */ +export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'> + + + +/** + * Reference to a field of type 'DateTime[]' + */ +export type ListDateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime[]'> + + + +/** + * Reference to a field of type 'Json' + */ +export type JsonFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Json'> + + + +/** + * Reference to a field of type 'QueryMode' + */ +export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'QueryMode'> + + + +/** + * Reference to a field of type 'Float' + */ +export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'> + + + +/** + * Reference to a field of type 'Float[]' + */ +export type ListFloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float[]'> + + +/** + * Batch Payload for updateMany & deleteMany & createMany + */ +export type BatchPayload = { + count: number +} + +export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs> +export type DefaultPrismaClient = PrismaClient +export type ErrorFormat = 'pretty' | 'colorless' | 'minimal' +export type PrismaClientOptions = ({ + /** + * Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`. + */ + adapter: runtime.SqlDriverAdapterFactory + accelerateUrl?: never +} | { + /** + * Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database. + */ + accelerateUrl: string + adapter?: never +}) & { + /** + * @default "colorless" + */ + errorFormat?: ErrorFormat + /** + * @example + * ``` + * // Shorthand for `emit: 'stdout'` + * log: ['query', 'info', 'warn', 'error'] + * + * // Emit as events only + * log: [ + * { emit: 'event', level: 'query' }, + * { emit: 'event', level: 'info' }, + * { emit: 'event', level: 'warn' } + * { emit: 'event', level: 'error' } + * ] + * + * / Emit as events and log to stdout + * og: [ + * { emit: 'stdout', level: 'query' }, + * { emit: 'stdout', level: 'info' }, + * { emit: 'stdout', level: 'warn' } + * { emit: 'stdout', level: 'error' } + * + * ``` + * Read more in our [docs](https://pris.ly/d/logging). + */ + log?: (LogLevel | LogDefinition)[] + /** + * The default values for transactionOptions + * maxWait ?= 2000 + * timeout ?= 5000 + */ + transactionOptions?: { + maxWait?: number + timeout?: number + isolationLevel?: TransactionIsolationLevel + } + /** + * Global configuration for omitting model fields by default. + * + * @example + * ``` + * const prisma = new PrismaClient({ + * omit: { + * user: { + * password: true + * } + * } + * }) + * ``` + */ + omit?: GlobalOmitConfig + /** + * SQL commenter plugins that add metadata to SQL queries as comments. + * Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/ + * + * @example + * ``` + * const prisma = new PrismaClient({ + * adapter, + * comments: [ + * traceContext(), + * queryInsights(), + * ], + * }) + * ``` + */ + comments?: runtime.SqlCommenterPlugin[] +} +export type GlobalOmitConfig = { + user?: Prisma.UserOmit + post?: Prisma.PostOmit + config?: Prisma.ConfigOmit + trend?: Prisma.TrendOmit +} + +/* Types for Logging */ +export type LogLevel = 'info' | 'query' | 'warn' | 'error' +export type LogDefinition = { + level: LogLevel + emit: 'stdout' | 'event' +} + +export type CheckIsLogLevel = T extends LogLevel ? T : never; + +export type GetLogType = CheckIsLogLevel< + T extends LogDefinition ? T['level'] : T +>; + +export type GetEvents = T extends Array + ? GetLogType + : never; + +export type QueryEvent = { + timestamp: Date + query: string + params: string + duration: number + target: string +} + +export type LogEvent = { + timestamp: Date + message: string + target: string +} +/* End Types for Logging */ + + +export type PrismaAction = + | 'findUnique' + | 'findUniqueOrThrow' + | 'findMany' + | 'findFirst' + | 'findFirstOrThrow' + | 'create' + | 'createMany' + | 'createManyAndReturn' + | 'update' + | 'updateMany' + | 'updateManyAndReturn' + | 'upsert' + | 'delete' + | 'deleteMany' + | 'executeRaw' + | 'queryRaw' + | 'aggregate' + | 'count' + | 'runCommandRaw' + | 'findRaw' + | 'groupBy' + +/** + * `PrismaClient` proxy available in interactive transactions. + */ +export type TransactionClient = Omit + diff --git a/src/generated/prisma/internal/prismaNamespaceBrowser.ts b/src/generated/prisma/internal/prismaNamespaceBrowser.ts new file mode 100644 index 0000000..ac2c67d --- /dev/null +++ b/src/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -0,0 +1,177 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * WARNING: This is an internal file that is subject to change! + * + * 🛑 Under no circumstances should you import this file directly! 🛑 + * + * All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file. + * While this enables partial backward compatibility, it is not part of the stable public API. + * + * If you are looking for your Models, Enums, and Input Types, please import them from the respective + * model files in the `model` directory! + */ + +import * as runtime from "@prisma/client/runtime/index-browser" + +export type * from '../models.js' +export type * from './prismaNamespace.js' + +export const Decimal = runtime.Decimal + + +export const NullTypes = { + DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull), + JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull), + AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull), +} +/** + * Helper for filtering JSON entries that have `null` on the database (empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const DbNull = runtime.DbNull + +/** + * Helper for filtering JSON entries that have JSON `null` values (not empty on the db) + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const JsonNull = runtime.JsonNull + +/** + * Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull` + * + * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field + */ +export const AnyNull = runtime.AnyNull + + +export const ModelName = { + User: 'User', + Post: 'Post', + Config: 'Config', + Trend: 'Trend' +} as const + +export type ModelName = (typeof ModelName)[keyof typeof ModelName] + +/* + * Enums + */ + +export const TransactionIsolationLevel = runtime.makeStrictEnum({ + ReadUncommitted: 'ReadUncommitted', + ReadCommitted: 'ReadCommitted', + RepeatableRead: 'RepeatableRead', + Serializable: 'Serializable' +} as const) + +export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] + + +export const UserScalarFieldEnum = { + id: 'id', + email: 'email', + name: 'name' +} as const + +export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum] + + +export const PostScalarFieldEnum = { + id: 'id', + title: 'title', + prompt: 'prompt', + content: 'content', + imageUrl: 'imageUrl', + style: 'style', + tone: 'tone', + isFbPostState: 'isFbPostState', + isTwitterPostState: 'isTwitterPostState', + isTiktokPostState: 'isTiktokPostState', + draft: 'draft', + tokensUsed: 'tokensUsed', + model: 'model', + reviewNotes: 'reviewNotes', + status: 'status', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type PostScalarFieldEnum = (typeof PostScalarFieldEnum)[keyof typeof PostScalarFieldEnum] + + +export const ConfigScalarFieldEnum = { + id: 'id', + key: 'key', + value: 'value' +} as const + +export type ConfigScalarFieldEnum = (typeof ConfigScalarFieldEnum)[keyof typeof ConfigScalarFieldEnum] + + +export const TrendScalarFieldEnum = { + id: 'id', + title: 'title', + description: 'description', + url: 'url', + score: 'score', + source: 'source', + category: 'category', + geo: 'geo', + tags: 'tags', + engagement: 'engagement', + raw: 'raw', + fingerprint: 'fingerprint', + sourceTimestamp: 'sourceTimestamp', + createdAt: 'createdAt', + updatedAt: 'updatedAt' +} as const + +export type TrendScalarFieldEnum = (typeof TrendScalarFieldEnum)[keyof typeof TrendScalarFieldEnum] + + +export const SortOrder = { + asc: 'asc', + desc: 'desc' +} as const + +export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder] + + +export const NullableJsonNullValueInput = { + DbNull: DbNull, + JsonNull: JsonNull +} as const + +export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput] + + +export const QueryMode = { + default: 'default', + insensitive: 'insensitive' +} as const + +export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] + + +export const NullsOrder = { + first: 'first', + last: 'last' +} as const + +export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder] + + +export const JsonNullValueFilter = { + DbNull: DbNull, + JsonNull: JsonNull, + AnyNull: AnyNull +} as const + +export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter] + diff --git a/src/generated/prisma/models.ts b/src/generated/prisma/models.ts new file mode 100644 index 0000000..4829ea5 --- /dev/null +++ b/src/generated/prisma/models.ts @@ -0,0 +1,15 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This is a barrel export file for all models and their related types. + * + * 🟢 You can import this file directly. + */ +export type * from './models/User.js' +export type * from './models/Post.js' +export type * from './models/Config.js' +export type * from './models/Trend.js' +export type * from './commonInputTypes.js' \ No newline at end of file diff --git a/src/generated/prisma/models/Config.ts b/src/generated/prisma/models/Config.ts new file mode 100644 index 0000000..4e015d0 --- /dev/null +++ b/src/generated/prisma/models/Config.ts @@ -0,0 +1,1134 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `Config` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums.js" +import type * as Prisma from "../internal/prismaNamespace.js" + +/** + * Model Config + * + */ +export type ConfigModel = runtime.Types.Result.DefaultSelection + +export type AggregateConfig = { + _count: ConfigCountAggregateOutputType | null + _avg: ConfigAvgAggregateOutputType | null + _sum: ConfigSumAggregateOutputType | null + _min: ConfigMinAggregateOutputType | null + _max: ConfigMaxAggregateOutputType | null +} + +export type ConfigAvgAggregateOutputType = { + id: number | null +} + +export type ConfigSumAggregateOutputType = { + id: number | null +} + +export type ConfigMinAggregateOutputType = { + id: number | null + key: string | null + value: string | null +} + +export type ConfigMaxAggregateOutputType = { + id: number | null + key: string | null + value: string | null +} + +export type ConfigCountAggregateOutputType = { + id: number + key: number + value: number + _all: number +} + + +export type ConfigAvgAggregateInputType = { + id?: true +} + +export type ConfigSumAggregateInputType = { + id?: true +} + +export type ConfigMinAggregateInputType = { + id?: true + key?: true + value?: true +} + +export type ConfigMaxAggregateInputType = { + id?: true + key?: true + value?: true +} + +export type ConfigCountAggregateInputType = { + id?: true + key?: true + value?: true + _all?: true +} + +export type ConfigAggregateArgs = { + /** + * Filter which Config to aggregate. + */ + where?: Prisma.ConfigWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Configs to fetch. + */ + orderBy?: Prisma.ConfigOrderByWithRelationInput | Prisma.ConfigOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.ConfigWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Configs from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Configs. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Configs + **/ + _count?: true | ConfigCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to average + **/ + _avg?: ConfigAvgAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to sum + **/ + _sum?: ConfigSumAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: ConfigMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: ConfigMaxAggregateInputType +} + +export type GetConfigAggregateType = { + [P in keyof T & keyof AggregateConfig]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type ConfigGroupByArgs = { + where?: Prisma.ConfigWhereInput + orderBy?: Prisma.ConfigOrderByWithAggregationInput | Prisma.ConfigOrderByWithAggregationInput[] + by: Prisma.ConfigScalarFieldEnum[] | Prisma.ConfigScalarFieldEnum + having?: Prisma.ConfigScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: ConfigCountAggregateInputType | true + _avg?: ConfigAvgAggregateInputType + _sum?: ConfigSumAggregateInputType + _min?: ConfigMinAggregateInputType + _max?: ConfigMaxAggregateInputType +} + +export type ConfigGroupByOutputType = { + id: number + key: string + value: string + _count: ConfigCountAggregateOutputType | null + _avg: ConfigAvgAggregateOutputType | null + _sum: ConfigSumAggregateOutputType | null + _min: ConfigMinAggregateOutputType | null + _max: ConfigMaxAggregateOutputType | null +} + +export type GetConfigGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof ConfigGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type ConfigWhereInput = { + AND?: Prisma.ConfigWhereInput | Prisma.ConfigWhereInput[] + OR?: Prisma.ConfigWhereInput[] + NOT?: Prisma.ConfigWhereInput | Prisma.ConfigWhereInput[] + id?: Prisma.IntFilter<"Config"> | number + key?: Prisma.StringFilter<"Config"> | string + value?: Prisma.StringFilter<"Config"> | string +} + +export type ConfigOrderByWithRelationInput = { + id?: Prisma.SortOrder + key?: Prisma.SortOrder + value?: Prisma.SortOrder +} + +export type ConfigWhereUniqueInput = Prisma.AtLeast<{ + id?: number + key?: string + AND?: Prisma.ConfigWhereInput | Prisma.ConfigWhereInput[] + OR?: Prisma.ConfigWhereInput[] + NOT?: Prisma.ConfigWhereInput | Prisma.ConfigWhereInput[] + value?: Prisma.StringFilter<"Config"> | string +}, "id" | "key"> + +export type ConfigOrderByWithAggregationInput = { + id?: Prisma.SortOrder + key?: Prisma.SortOrder + value?: Prisma.SortOrder + _count?: Prisma.ConfigCountOrderByAggregateInput + _avg?: Prisma.ConfigAvgOrderByAggregateInput + _max?: Prisma.ConfigMaxOrderByAggregateInput + _min?: Prisma.ConfigMinOrderByAggregateInput + _sum?: Prisma.ConfigSumOrderByAggregateInput +} + +export type ConfigScalarWhereWithAggregatesInput = { + AND?: Prisma.ConfigScalarWhereWithAggregatesInput | Prisma.ConfigScalarWhereWithAggregatesInput[] + OR?: Prisma.ConfigScalarWhereWithAggregatesInput[] + NOT?: Prisma.ConfigScalarWhereWithAggregatesInput | Prisma.ConfigScalarWhereWithAggregatesInput[] + id?: Prisma.IntWithAggregatesFilter<"Config"> | number + key?: Prisma.StringWithAggregatesFilter<"Config"> | string + value?: Prisma.StringWithAggregatesFilter<"Config"> | string +} + +export type ConfigCreateInput = { + key: string + value: string +} + +export type ConfigUncheckedCreateInput = { + id?: number + key: string + value: string +} + +export type ConfigUpdateInput = { + key?: Prisma.StringFieldUpdateOperationsInput | string + value?: Prisma.StringFieldUpdateOperationsInput | string +} + +export type ConfigUncheckedUpdateInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + key?: Prisma.StringFieldUpdateOperationsInput | string + value?: Prisma.StringFieldUpdateOperationsInput | string +} + +export type ConfigCreateManyInput = { + id?: number + key: string + value: string +} + +export type ConfigUpdateManyMutationInput = { + key?: Prisma.StringFieldUpdateOperationsInput | string + value?: Prisma.StringFieldUpdateOperationsInput | string +} + +export type ConfigUncheckedUpdateManyInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + key?: Prisma.StringFieldUpdateOperationsInput | string + value?: Prisma.StringFieldUpdateOperationsInput | string +} + +export type ConfigCountOrderByAggregateInput = { + id?: Prisma.SortOrder + key?: Prisma.SortOrder + value?: Prisma.SortOrder +} + +export type ConfigAvgOrderByAggregateInput = { + id?: Prisma.SortOrder +} + +export type ConfigMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + key?: Prisma.SortOrder + value?: Prisma.SortOrder +} + +export type ConfigMinOrderByAggregateInput = { + id?: Prisma.SortOrder + key?: Prisma.SortOrder + value?: Prisma.SortOrder +} + +export type ConfigSumOrderByAggregateInput = { + id?: Prisma.SortOrder +} + + + +export type ConfigSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + key?: boolean + value?: boolean +}, ExtArgs["result"]["config"]> + +export type ConfigSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + key?: boolean + value?: boolean +}, ExtArgs["result"]["config"]> + +export type ConfigSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + key?: boolean + value?: boolean +}, ExtArgs["result"]["config"]> + +export type ConfigSelectScalar = { + id?: boolean + key?: boolean + value?: boolean +} + +export type ConfigOmit = runtime.Types.Extensions.GetOmit<"id" | "key" | "value", ExtArgs["result"]["config"]> + +export type $ConfigPayload = { + name: "Config" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: number + key: string + value: string + }, ExtArgs["result"]["config"]> + composites: {} +} + +export type ConfigGetPayload = runtime.Types.Result.GetResult + +export type ConfigCountArgs = + Omit & { + select?: ConfigCountAggregateInputType | true + } + +export interface ConfigDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['Config'], meta: { name: 'Config' } } + /** + * Find zero or one Config that matches the filter. + * @param {ConfigFindUniqueArgs} args - Arguments to find a Config + * @example + * // Get one Config + * const config = await prisma.config.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__ConfigClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one Config that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {ConfigFindUniqueOrThrowArgs} args - Arguments to find a Config + * @example + * // Get one Config + * const config = await prisma.config.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__ConfigClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Config that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ConfigFindFirstArgs} args - Arguments to find a Config + * @example + * // Get one Config + * const config = await prisma.config.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__ConfigClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Config that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ConfigFindFirstOrThrowArgs} args - Arguments to find a Config + * @example + * // Get one Config + * const config = await prisma.config.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__ConfigClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more Configs that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ConfigFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all Configs + * const configs = await prisma.config.findMany() + * + * // Get first 10 Configs + * const configs = await prisma.config.findMany({ take: 10 }) + * + * // Only select the `id` + * const configWithIdOnly = await prisma.config.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a Config. + * @param {ConfigCreateArgs} args - Arguments to create a Config. + * @example + * // Create one Config + * const Config = await prisma.config.create({ + * data: { + * // ... data to create a Config + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__ConfigClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many Configs. + * @param {ConfigCreateManyArgs} args - Arguments to create many Configs. + * @example + * // Create many Configs + * const config = await prisma.config.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many Configs and returns the data saved in the database. + * @param {ConfigCreateManyAndReturnArgs} args - Arguments to create many Configs. + * @example + * // Create many Configs + * const config = await prisma.config.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many Configs and only return the `id` + * const configWithIdOnly = await prisma.config.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a Config. + * @param {ConfigDeleteArgs} args - Arguments to delete one Config. + * @example + * // Delete one Config + * const Config = await prisma.config.delete({ + * where: { + * // ... filter to delete one Config + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__ConfigClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one Config. + * @param {ConfigUpdateArgs} args - Arguments to update one Config. + * @example + * // Update one Config + * const config = await prisma.config.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__ConfigClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more Configs. + * @param {ConfigDeleteManyArgs} args - Arguments to filter Configs to delete. + * @example + * // Delete a few Configs + * const { count } = await prisma.config.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Configs. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ConfigUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Configs + * const config = await prisma.config.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Configs and returns the data updated in the database. + * @param {ConfigUpdateManyAndReturnArgs} args - Arguments to update many Configs. + * @example + * // Update many Configs + * const config = await prisma.config.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more Configs and only return the `id` + * const configWithIdOnly = await prisma.config.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one Config. + * @param {ConfigUpsertArgs} args - Arguments to update or create a Config. + * @example + * // Update or create a Config + * const config = await prisma.config.upsert({ + * create: { + * // ... data to create a Config + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the Config we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__ConfigClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of Configs. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ConfigCountArgs} args - Arguments to filter Configs to count. + * @example + * // Count the number of Configs + * const count = await prisma.config.count({ + * where: { + * // ... the filter for the Configs we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a Config. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ConfigAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by Config. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {ConfigGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends ConfigGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: ConfigGroupByArgs['orderBy'] } + : { orderBy?: ConfigGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetConfigGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the Config model + */ +readonly fields: ConfigFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for Config. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__ConfigClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the Config model + */ +export interface ConfigFieldRefs { + readonly id: Prisma.FieldRef<"Config", 'Int'> + readonly key: Prisma.FieldRef<"Config", 'String'> + readonly value: Prisma.FieldRef<"Config", 'String'> +} + + +// Custom InputTypes +/** + * Config findUnique + */ +export type ConfigFindUniqueArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * Filter, which Config to fetch. + */ + where: Prisma.ConfigWhereUniqueInput +} + +/** + * Config findUniqueOrThrow + */ +export type ConfigFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * Filter, which Config to fetch. + */ + where: Prisma.ConfigWhereUniqueInput +} + +/** + * Config findFirst + */ +export type ConfigFindFirstArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * Filter, which Config to fetch. + */ + where?: Prisma.ConfigWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Configs to fetch. + */ + orderBy?: Prisma.ConfigOrderByWithRelationInput | Prisma.ConfigOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Configs. + */ + cursor?: Prisma.ConfigWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Configs from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Configs. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Configs. + */ + distinct?: Prisma.ConfigScalarFieldEnum | Prisma.ConfigScalarFieldEnum[] +} + +/** + * Config findFirstOrThrow + */ +export type ConfigFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * Filter, which Config to fetch. + */ + where?: Prisma.ConfigWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Configs to fetch. + */ + orderBy?: Prisma.ConfigOrderByWithRelationInput | Prisma.ConfigOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Configs. + */ + cursor?: Prisma.ConfigWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Configs from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Configs. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Configs. + */ + distinct?: Prisma.ConfigScalarFieldEnum | Prisma.ConfigScalarFieldEnum[] +} + +/** + * Config findMany + */ +export type ConfigFindManyArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * Filter, which Configs to fetch. + */ + where?: Prisma.ConfigWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Configs to fetch. + */ + orderBy?: Prisma.ConfigOrderByWithRelationInput | Prisma.ConfigOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Configs. + */ + cursor?: Prisma.ConfigWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Configs from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Configs. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Configs. + */ + distinct?: Prisma.ConfigScalarFieldEnum | Prisma.ConfigScalarFieldEnum[] +} + +/** + * Config create + */ +export type ConfigCreateArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * The data needed to create a Config. + */ + data: Prisma.XOR +} + +/** + * Config createMany + */ +export type ConfigCreateManyArgs = { + /** + * The data used to create many Configs. + */ + data: Prisma.ConfigCreateManyInput | Prisma.ConfigCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Config createManyAndReturn + */ +export type ConfigCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelectCreateManyAndReturn | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * The data used to create many Configs. + */ + data: Prisma.ConfigCreateManyInput | Prisma.ConfigCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Config update + */ +export type ConfigUpdateArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * The data needed to update a Config. + */ + data: Prisma.XOR + /** + * Choose, which Config to update. + */ + where: Prisma.ConfigWhereUniqueInput +} + +/** + * Config updateMany + */ +export type ConfigUpdateManyArgs = { + /** + * The data used to update Configs. + */ + data: Prisma.XOR + /** + * Filter which Configs to update + */ + where?: Prisma.ConfigWhereInput + /** + * Limit how many Configs to update. + */ + limit?: number +} + +/** + * Config updateManyAndReturn + */ +export type ConfigUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * The data used to update Configs. + */ + data: Prisma.XOR + /** + * Filter which Configs to update + */ + where?: Prisma.ConfigWhereInput + /** + * Limit how many Configs to update. + */ + limit?: number +} + +/** + * Config upsert + */ +export type ConfigUpsertArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * The filter to search for the Config to update in case it exists. + */ + where: Prisma.ConfigWhereUniqueInput + /** + * In case the Config found by the `where` argument doesn't exist, create a new Config with this data. + */ + create: Prisma.XOR + /** + * In case the Config was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * Config delete + */ +export type ConfigDeleteArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null + /** + * Filter which Config to delete. + */ + where: Prisma.ConfigWhereUniqueInput +} + +/** + * Config deleteMany + */ +export type ConfigDeleteManyArgs = { + /** + * Filter which Configs to delete + */ + where?: Prisma.ConfigWhereInput + /** + * Limit how many Configs to delete. + */ + limit?: number +} + +/** + * Config without action + */ +export type ConfigDefaultArgs = { + /** + * Select specific fields to fetch from the Config + */ + select?: Prisma.ConfigSelect | null + /** + * Omit specific fields from the Config + */ + omit?: Prisma.ConfigOmit | null +} diff --git a/src/generated/prisma/models/Post.ts b/src/generated/prisma/models/Post.ts new file mode 100644 index 0000000..4e1b2be --- /dev/null +++ b/src/generated/prisma/models/Post.ts @@ -0,0 +1,1554 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `Post` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums.js" +import type * as Prisma from "../internal/prismaNamespace.js" + +/** + * Model Post + * + */ +export type PostModel = runtime.Types.Result.DefaultSelection + +export type AggregatePost = { + _count: PostCountAggregateOutputType | null + _avg: PostAvgAggregateOutputType | null + _sum: PostSumAggregateOutputType | null + _min: PostMinAggregateOutputType | null + _max: PostMaxAggregateOutputType | null +} + +export type PostAvgAggregateOutputType = { + id: number | null + isFbPostState: number | null + isTwitterPostState: number | null + isTiktokPostState: number | null + tokensUsed: number | null +} + +export type PostSumAggregateOutputType = { + id: number | null + isFbPostState: number | null + isTwitterPostState: number | null + isTiktokPostState: number | null + tokensUsed: number | null +} + +export type PostMinAggregateOutputType = { + id: number | null + title: string | null + prompt: string | null + content: string | null + imageUrl: string | null + style: string | null + tone: string | null + isFbPostState: number | null + isTwitterPostState: number | null + isTiktokPostState: number | null + draft: string | null + tokensUsed: number | null + model: string | null + reviewNotes: string | null + status: string | null + createdAt: Date | null + updatedAt: Date | null +} + +export type PostMaxAggregateOutputType = { + id: number | null + title: string | null + prompt: string | null + content: string | null + imageUrl: string | null + style: string | null + tone: string | null + isFbPostState: number | null + isTwitterPostState: number | null + isTiktokPostState: number | null + draft: string | null + tokensUsed: number | null + model: string | null + reviewNotes: string | null + status: string | null + createdAt: Date | null + updatedAt: Date | null +} + +export type PostCountAggregateOutputType = { + id: number + title: number + prompt: number + content: number + imageUrl: number + style: number + tone: number + isFbPostState: number + isTwitterPostState: number + isTiktokPostState: number + draft: number + tokensUsed: number + model: number + reviewNotes: number + status: number + createdAt: number + updatedAt: number + _all: number +} + + +export type PostAvgAggregateInputType = { + id?: true + isFbPostState?: true + isTwitterPostState?: true + isTiktokPostState?: true + tokensUsed?: true +} + +export type PostSumAggregateInputType = { + id?: true + isFbPostState?: true + isTwitterPostState?: true + isTiktokPostState?: true + tokensUsed?: true +} + +export type PostMinAggregateInputType = { + id?: true + title?: true + prompt?: true + content?: true + imageUrl?: true + style?: true + tone?: true + isFbPostState?: true + isTwitterPostState?: true + isTiktokPostState?: true + draft?: true + tokensUsed?: true + model?: true + reviewNotes?: true + status?: true + createdAt?: true + updatedAt?: true +} + +export type PostMaxAggregateInputType = { + id?: true + title?: true + prompt?: true + content?: true + imageUrl?: true + style?: true + tone?: true + isFbPostState?: true + isTwitterPostState?: true + isTiktokPostState?: true + draft?: true + tokensUsed?: true + model?: true + reviewNotes?: true + status?: true + createdAt?: true + updatedAt?: true +} + +export type PostCountAggregateInputType = { + id?: true + title?: true + prompt?: true + content?: true + imageUrl?: true + style?: true + tone?: true + isFbPostState?: true + isTwitterPostState?: true + isTiktokPostState?: true + draft?: true + tokensUsed?: true + model?: true + reviewNotes?: true + status?: true + createdAt?: true + updatedAt?: true + _all?: true +} + +export type PostAggregateArgs = { + /** + * Filter which Post to aggregate. + */ + where?: Prisma.PostWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Posts to fetch. + */ + orderBy?: Prisma.PostOrderByWithRelationInput | Prisma.PostOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.PostWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Posts from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Posts. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Posts + **/ + _count?: true | PostCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to average + **/ + _avg?: PostAvgAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to sum + **/ + _sum?: PostSumAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: PostMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: PostMaxAggregateInputType +} + +export type GetPostAggregateType = { + [P in keyof T & keyof AggregatePost]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type PostGroupByArgs = { + where?: Prisma.PostWhereInput + orderBy?: Prisma.PostOrderByWithAggregationInput | Prisma.PostOrderByWithAggregationInput[] + by: Prisma.PostScalarFieldEnum[] | Prisma.PostScalarFieldEnum + having?: Prisma.PostScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: PostCountAggregateInputType | true + _avg?: PostAvgAggregateInputType + _sum?: PostSumAggregateInputType + _min?: PostMinAggregateInputType + _max?: PostMaxAggregateInputType +} + +export type PostGroupByOutputType = { + id: number + title: string + prompt: string + content: string + imageUrl: string | null + style: string + tone: string | null + isFbPostState: number + isTwitterPostState: number + isTiktokPostState: number + draft: string | null + tokensUsed: number + model: string | null + reviewNotes: string | null + status: string + createdAt: Date + updatedAt: Date + _count: PostCountAggregateOutputType | null + _avg: PostAvgAggregateOutputType | null + _sum: PostSumAggregateOutputType | null + _min: PostMinAggregateOutputType | null + _max: PostMaxAggregateOutputType | null +} + +export type GetPostGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof PostGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type PostWhereInput = { + AND?: Prisma.PostWhereInput | Prisma.PostWhereInput[] + OR?: Prisma.PostWhereInput[] + NOT?: Prisma.PostWhereInput | Prisma.PostWhereInput[] + id?: Prisma.IntFilter<"Post"> | number + title?: Prisma.StringFilter<"Post"> | string + prompt?: Prisma.StringFilter<"Post"> | string + content?: Prisma.StringFilter<"Post"> | string + imageUrl?: Prisma.StringNullableFilter<"Post"> | string | null + style?: Prisma.StringFilter<"Post"> | string + tone?: Prisma.StringNullableFilter<"Post"> | string | null + isFbPostState?: Prisma.IntFilter<"Post"> | number + isTwitterPostState?: Prisma.IntFilter<"Post"> | number + isTiktokPostState?: Prisma.IntFilter<"Post"> | number + draft?: Prisma.StringNullableFilter<"Post"> | string | null + tokensUsed?: Prisma.IntFilter<"Post"> | number + model?: Prisma.StringNullableFilter<"Post"> | string | null + reviewNotes?: Prisma.StringNullableFilter<"Post"> | string | null + status?: Prisma.StringFilter<"Post"> | string + createdAt?: Prisma.DateTimeFilter<"Post"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"Post"> | Date | string +} + +export type PostOrderByWithRelationInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + prompt?: Prisma.SortOrder + content?: Prisma.SortOrder + imageUrl?: Prisma.SortOrderInput | Prisma.SortOrder + style?: Prisma.SortOrder + tone?: Prisma.SortOrderInput | Prisma.SortOrder + isFbPostState?: Prisma.SortOrder + isTwitterPostState?: Prisma.SortOrder + isTiktokPostState?: Prisma.SortOrder + draft?: Prisma.SortOrderInput | Prisma.SortOrder + tokensUsed?: Prisma.SortOrder + model?: Prisma.SortOrderInput | Prisma.SortOrder + reviewNotes?: Prisma.SortOrderInput | Prisma.SortOrder + status?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type PostWhereUniqueInput = Prisma.AtLeast<{ + id?: number + AND?: Prisma.PostWhereInput | Prisma.PostWhereInput[] + OR?: Prisma.PostWhereInput[] + NOT?: Prisma.PostWhereInput | Prisma.PostWhereInput[] + title?: Prisma.StringFilter<"Post"> | string + prompt?: Prisma.StringFilter<"Post"> | string + content?: Prisma.StringFilter<"Post"> | string + imageUrl?: Prisma.StringNullableFilter<"Post"> | string | null + style?: Prisma.StringFilter<"Post"> | string + tone?: Prisma.StringNullableFilter<"Post"> | string | null + isFbPostState?: Prisma.IntFilter<"Post"> | number + isTwitterPostState?: Prisma.IntFilter<"Post"> | number + isTiktokPostState?: Prisma.IntFilter<"Post"> | number + draft?: Prisma.StringNullableFilter<"Post"> | string | null + tokensUsed?: Prisma.IntFilter<"Post"> | number + model?: Prisma.StringNullableFilter<"Post"> | string | null + reviewNotes?: Prisma.StringNullableFilter<"Post"> | string | null + status?: Prisma.StringFilter<"Post"> | string + createdAt?: Prisma.DateTimeFilter<"Post"> | Date | string + updatedAt?: Prisma.DateTimeFilter<"Post"> | Date | string +}, "id"> + +export type PostOrderByWithAggregationInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + prompt?: Prisma.SortOrder + content?: Prisma.SortOrder + imageUrl?: Prisma.SortOrderInput | Prisma.SortOrder + style?: Prisma.SortOrder + tone?: Prisma.SortOrderInput | Prisma.SortOrder + isFbPostState?: Prisma.SortOrder + isTwitterPostState?: Prisma.SortOrder + isTiktokPostState?: Prisma.SortOrder + draft?: Prisma.SortOrderInput | Prisma.SortOrder + tokensUsed?: Prisma.SortOrder + model?: Prisma.SortOrderInput | Prisma.SortOrder + reviewNotes?: Prisma.SortOrderInput | Prisma.SortOrder + status?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder + _count?: Prisma.PostCountOrderByAggregateInput + _avg?: Prisma.PostAvgOrderByAggregateInput + _max?: Prisma.PostMaxOrderByAggregateInput + _min?: Prisma.PostMinOrderByAggregateInput + _sum?: Prisma.PostSumOrderByAggregateInput +} + +export type PostScalarWhereWithAggregatesInput = { + AND?: Prisma.PostScalarWhereWithAggregatesInput | Prisma.PostScalarWhereWithAggregatesInput[] + OR?: Prisma.PostScalarWhereWithAggregatesInput[] + NOT?: Prisma.PostScalarWhereWithAggregatesInput | Prisma.PostScalarWhereWithAggregatesInput[] + id?: Prisma.IntWithAggregatesFilter<"Post"> | number + title?: Prisma.StringWithAggregatesFilter<"Post"> | string + prompt?: Prisma.StringWithAggregatesFilter<"Post"> | string + content?: Prisma.StringWithAggregatesFilter<"Post"> | string + imageUrl?: Prisma.StringNullableWithAggregatesFilter<"Post"> | string | null + style?: Prisma.StringWithAggregatesFilter<"Post"> | string + tone?: Prisma.StringNullableWithAggregatesFilter<"Post"> | string | null + isFbPostState?: Prisma.IntWithAggregatesFilter<"Post"> | number + isTwitterPostState?: Prisma.IntWithAggregatesFilter<"Post"> | number + isTiktokPostState?: Prisma.IntWithAggregatesFilter<"Post"> | number + draft?: Prisma.StringNullableWithAggregatesFilter<"Post"> | string | null + tokensUsed?: Prisma.IntWithAggregatesFilter<"Post"> | number + model?: Prisma.StringNullableWithAggregatesFilter<"Post"> | string | null + reviewNotes?: Prisma.StringNullableWithAggregatesFilter<"Post"> | string | null + status?: Prisma.StringWithAggregatesFilter<"Post"> | string + createdAt?: Prisma.DateTimeWithAggregatesFilter<"Post"> | Date | string + updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Post"> | Date | string +} + +export type PostCreateInput = { + title: string + prompt: string + content: string + imageUrl?: string | null + style?: string + tone?: string | null + isFbPostState?: number + isTwitterPostState?: number + isTiktokPostState?: number + draft?: string | null + tokensUsed?: number + model?: string | null + reviewNotes?: string | null + status?: string + createdAt?: Date | string + updatedAt?: Date | string +} + +export type PostUncheckedCreateInput = { + id?: number + title: string + prompt: string + content: string + imageUrl?: string | null + style?: string + tone?: string | null + isFbPostState?: number + isTwitterPostState?: number + isTiktokPostState?: number + draft?: string | null + tokensUsed?: number + model?: string | null + reviewNotes?: string | null + status?: string + createdAt?: Date | string + updatedAt?: Date | string +} + +export type PostUpdateInput = { + title?: Prisma.StringFieldUpdateOperationsInput | string + prompt?: Prisma.StringFieldUpdateOperationsInput | string + content?: Prisma.StringFieldUpdateOperationsInput | string + imageUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + style?: Prisma.StringFieldUpdateOperationsInput | string + tone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + isFbPostState?: Prisma.IntFieldUpdateOperationsInput | number + isTwitterPostState?: Prisma.IntFieldUpdateOperationsInput | number + isTiktokPostState?: Prisma.IntFieldUpdateOperationsInput | number + draft?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + tokensUsed?: Prisma.IntFieldUpdateOperationsInput | number + model?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + reviewNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + status?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type PostUncheckedUpdateInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + title?: Prisma.StringFieldUpdateOperationsInput | string + prompt?: Prisma.StringFieldUpdateOperationsInput | string + content?: Prisma.StringFieldUpdateOperationsInput | string + imageUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + style?: Prisma.StringFieldUpdateOperationsInput | string + tone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + isFbPostState?: Prisma.IntFieldUpdateOperationsInput | number + isTwitterPostState?: Prisma.IntFieldUpdateOperationsInput | number + isTiktokPostState?: Prisma.IntFieldUpdateOperationsInput | number + draft?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + tokensUsed?: Prisma.IntFieldUpdateOperationsInput | number + model?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + reviewNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + status?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type PostCreateManyInput = { + id?: number + title: string + prompt: string + content: string + imageUrl?: string | null + style?: string + tone?: string | null + isFbPostState?: number + isTwitterPostState?: number + isTiktokPostState?: number + draft?: string | null + tokensUsed?: number + model?: string | null + reviewNotes?: string | null + status?: string + createdAt?: Date | string + updatedAt?: Date | string +} + +export type PostUpdateManyMutationInput = { + title?: Prisma.StringFieldUpdateOperationsInput | string + prompt?: Prisma.StringFieldUpdateOperationsInput | string + content?: Prisma.StringFieldUpdateOperationsInput | string + imageUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + style?: Prisma.StringFieldUpdateOperationsInput | string + tone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + isFbPostState?: Prisma.IntFieldUpdateOperationsInput | number + isTwitterPostState?: Prisma.IntFieldUpdateOperationsInput | number + isTiktokPostState?: Prisma.IntFieldUpdateOperationsInput | number + draft?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + tokensUsed?: Prisma.IntFieldUpdateOperationsInput | number + model?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + reviewNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + status?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type PostUncheckedUpdateManyInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + title?: Prisma.StringFieldUpdateOperationsInput | string + prompt?: Prisma.StringFieldUpdateOperationsInput | string + content?: Prisma.StringFieldUpdateOperationsInput | string + imageUrl?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + style?: Prisma.StringFieldUpdateOperationsInput | string + tone?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + isFbPostState?: Prisma.IntFieldUpdateOperationsInput | number + isTwitterPostState?: Prisma.IntFieldUpdateOperationsInput | number + isTiktokPostState?: Prisma.IntFieldUpdateOperationsInput | number + draft?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + tokensUsed?: Prisma.IntFieldUpdateOperationsInput | number + model?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + reviewNotes?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + status?: Prisma.StringFieldUpdateOperationsInput | string + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string +} + +export type PostCountOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + prompt?: Prisma.SortOrder + content?: Prisma.SortOrder + imageUrl?: Prisma.SortOrder + style?: Prisma.SortOrder + tone?: Prisma.SortOrder + isFbPostState?: Prisma.SortOrder + isTwitterPostState?: Prisma.SortOrder + isTiktokPostState?: Prisma.SortOrder + draft?: Prisma.SortOrder + tokensUsed?: Prisma.SortOrder + model?: Prisma.SortOrder + reviewNotes?: Prisma.SortOrder + status?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type PostAvgOrderByAggregateInput = { + id?: Prisma.SortOrder + isFbPostState?: Prisma.SortOrder + isTwitterPostState?: Prisma.SortOrder + isTiktokPostState?: Prisma.SortOrder + tokensUsed?: Prisma.SortOrder +} + +export type PostMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + prompt?: Prisma.SortOrder + content?: Prisma.SortOrder + imageUrl?: Prisma.SortOrder + style?: Prisma.SortOrder + tone?: Prisma.SortOrder + isFbPostState?: Prisma.SortOrder + isTwitterPostState?: Prisma.SortOrder + isTiktokPostState?: Prisma.SortOrder + draft?: Prisma.SortOrder + tokensUsed?: Prisma.SortOrder + model?: Prisma.SortOrder + reviewNotes?: Prisma.SortOrder + status?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type PostMinOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + prompt?: Prisma.SortOrder + content?: Prisma.SortOrder + imageUrl?: Prisma.SortOrder + style?: Prisma.SortOrder + tone?: Prisma.SortOrder + isFbPostState?: Prisma.SortOrder + isTwitterPostState?: Prisma.SortOrder + isTiktokPostState?: Prisma.SortOrder + draft?: Prisma.SortOrder + tokensUsed?: Prisma.SortOrder + model?: Prisma.SortOrder + reviewNotes?: Prisma.SortOrder + status?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type PostSumOrderByAggregateInput = { + id?: Prisma.SortOrder + isFbPostState?: Prisma.SortOrder + isTwitterPostState?: Prisma.SortOrder + isTiktokPostState?: Prisma.SortOrder + tokensUsed?: Prisma.SortOrder +} + +export type DateTimeFieldUpdateOperationsInput = { + set?: Date | string +} + + + +export type PostSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + prompt?: boolean + content?: boolean + imageUrl?: boolean + style?: boolean + tone?: boolean + isFbPostState?: boolean + isTwitterPostState?: boolean + isTiktokPostState?: boolean + draft?: boolean + tokensUsed?: boolean + model?: boolean + reviewNotes?: boolean + status?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["post"]> + +export type PostSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + prompt?: boolean + content?: boolean + imageUrl?: boolean + style?: boolean + tone?: boolean + isFbPostState?: boolean + isTwitterPostState?: boolean + isTiktokPostState?: boolean + draft?: boolean + tokensUsed?: boolean + model?: boolean + reviewNotes?: boolean + status?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["post"]> + +export type PostSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + prompt?: boolean + content?: boolean + imageUrl?: boolean + style?: boolean + tone?: boolean + isFbPostState?: boolean + isTwitterPostState?: boolean + isTiktokPostState?: boolean + draft?: boolean + tokensUsed?: boolean + model?: boolean + reviewNotes?: boolean + status?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["post"]> + +export type PostSelectScalar = { + id?: boolean + title?: boolean + prompt?: boolean + content?: boolean + imageUrl?: boolean + style?: boolean + tone?: boolean + isFbPostState?: boolean + isTwitterPostState?: boolean + isTiktokPostState?: boolean + draft?: boolean + tokensUsed?: boolean + model?: boolean + reviewNotes?: boolean + status?: boolean + createdAt?: boolean + updatedAt?: boolean +} + +export type PostOmit = runtime.Types.Extensions.GetOmit<"id" | "title" | "prompt" | "content" | "imageUrl" | "style" | "tone" | "isFbPostState" | "isTwitterPostState" | "isTiktokPostState" | "draft" | "tokensUsed" | "model" | "reviewNotes" | "status" | "createdAt" | "updatedAt", ExtArgs["result"]["post"]> + +export type $PostPayload = { + name: "Post" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: number + title: string + prompt: string + content: string + imageUrl: string | null + style: string + tone: string | null + isFbPostState: number + isTwitterPostState: number + isTiktokPostState: number + draft: string | null + tokensUsed: number + model: string | null + reviewNotes: string | null + status: string + createdAt: Date + updatedAt: Date + }, ExtArgs["result"]["post"]> + composites: {} +} + +export type PostGetPayload = runtime.Types.Result.GetResult + +export type PostCountArgs = + Omit & { + select?: PostCountAggregateInputType | true + } + +export interface PostDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['Post'], meta: { name: 'Post' } } + /** + * Find zero or one Post that matches the filter. + * @param {PostFindUniqueArgs} args - Arguments to find a Post + * @example + * // Get one Post + * const post = await prisma.post.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__PostClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one Post that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {PostFindUniqueOrThrowArgs} args - Arguments to find a Post + * @example + * // Get one Post + * const post = await prisma.post.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__PostClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Post that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PostFindFirstArgs} args - Arguments to find a Post + * @example + * // Get one Post + * const post = await prisma.post.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__PostClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Post that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PostFindFirstOrThrowArgs} args - Arguments to find a Post + * @example + * // Get one Post + * const post = await prisma.post.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__PostClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more Posts that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PostFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all Posts + * const posts = await prisma.post.findMany() + * + * // Get first 10 Posts + * const posts = await prisma.post.findMany({ take: 10 }) + * + * // Only select the `id` + * const postWithIdOnly = await prisma.post.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a Post. + * @param {PostCreateArgs} args - Arguments to create a Post. + * @example + * // Create one Post + * const Post = await prisma.post.create({ + * data: { + * // ... data to create a Post + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__PostClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many Posts. + * @param {PostCreateManyArgs} args - Arguments to create many Posts. + * @example + * // Create many Posts + * const post = await prisma.post.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many Posts and returns the data saved in the database. + * @param {PostCreateManyAndReturnArgs} args - Arguments to create many Posts. + * @example + * // Create many Posts + * const post = await prisma.post.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many Posts and only return the `id` + * const postWithIdOnly = await prisma.post.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a Post. + * @param {PostDeleteArgs} args - Arguments to delete one Post. + * @example + * // Delete one Post + * const Post = await prisma.post.delete({ + * where: { + * // ... filter to delete one Post + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__PostClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one Post. + * @param {PostUpdateArgs} args - Arguments to update one Post. + * @example + * // Update one Post + * const post = await prisma.post.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__PostClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more Posts. + * @param {PostDeleteManyArgs} args - Arguments to filter Posts to delete. + * @example + * // Delete a few Posts + * const { count } = await prisma.post.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Posts. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PostUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Posts + * const post = await prisma.post.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Posts and returns the data updated in the database. + * @param {PostUpdateManyAndReturnArgs} args - Arguments to update many Posts. + * @example + * // Update many Posts + * const post = await prisma.post.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more Posts and only return the `id` + * const postWithIdOnly = await prisma.post.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one Post. + * @param {PostUpsertArgs} args - Arguments to update or create a Post. + * @example + * // Update or create a Post + * const post = await prisma.post.upsert({ + * create: { + * // ... data to create a Post + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the Post we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__PostClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of Posts. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PostCountArgs} args - Arguments to filter Posts to count. + * @example + * // Count the number of Posts + * const count = await prisma.post.count({ + * where: { + * // ... the filter for the Posts we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a Post. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PostAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by Post. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {PostGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends PostGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: PostGroupByArgs['orderBy'] } + : { orderBy?: PostGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetPostGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the Post model + */ +readonly fields: PostFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for Post. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__PostClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the Post model + */ +export interface PostFieldRefs { + readonly id: Prisma.FieldRef<"Post", 'Int'> + readonly title: Prisma.FieldRef<"Post", 'String'> + readonly prompt: Prisma.FieldRef<"Post", 'String'> + readonly content: Prisma.FieldRef<"Post", 'String'> + readonly imageUrl: Prisma.FieldRef<"Post", 'String'> + readonly style: Prisma.FieldRef<"Post", 'String'> + readonly tone: Prisma.FieldRef<"Post", 'String'> + readonly isFbPostState: Prisma.FieldRef<"Post", 'Int'> + readonly isTwitterPostState: Prisma.FieldRef<"Post", 'Int'> + readonly isTiktokPostState: Prisma.FieldRef<"Post", 'Int'> + readonly draft: Prisma.FieldRef<"Post", 'String'> + readonly tokensUsed: Prisma.FieldRef<"Post", 'Int'> + readonly model: Prisma.FieldRef<"Post", 'String'> + readonly reviewNotes: Prisma.FieldRef<"Post", 'String'> + readonly status: Prisma.FieldRef<"Post", 'String'> + readonly createdAt: Prisma.FieldRef<"Post", 'DateTime'> + readonly updatedAt: Prisma.FieldRef<"Post", 'DateTime'> +} + + +// Custom InputTypes +/** + * Post findUnique + */ +export type PostFindUniqueArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * Filter, which Post to fetch. + */ + where: Prisma.PostWhereUniqueInput +} + +/** + * Post findUniqueOrThrow + */ +export type PostFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * Filter, which Post to fetch. + */ + where: Prisma.PostWhereUniqueInput +} + +/** + * Post findFirst + */ +export type PostFindFirstArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * Filter, which Post to fetch. + */ + where?: Prisma.PostWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Posts to fetch. + */ + orderBy?: Prisma.PostOrderByWithRelationInput | Prisma.PostOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Posts. + */ + cursor?: Prisma.PostWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Posts from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Posts. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Posts. + */ + distinct?: Prisma.PostScalarFieldEnum | Prisma.PostScalarFieldEnum[] +} + +/** + * Post findFirstOrThrow + */ +export type PostFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * Filter, which Post to fetch. + */ + where?: Prisma.PostWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Posts to fetch. + */ + orderBy?: Prisma.PostOrderByWithRelationInput | Prisma.PostOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Posts. + */ + cursor?: Prisma.PostWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Posts from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Posts. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Posts. + */ + distinct?: Prisma.PostScalarFieldEnum | Prisma.PostScalarFieldEnum[] +} + +/** + * Post findMany + */ +export type PostFindManyArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * Filter, which Posts to fetch. + */ + where?: Prisma.PostWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Posts to fetch. + */ + orderBy?: Prisma.PostOrderByWithRelationInput | Prisma.PostOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Posts. + */ + cursor?: Prisma.PostWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Posts from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Posts. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Posts. + */ + distinct?: Prisma.PostScalarFieldEnum | Prisma.PostScalarFieldEnum[] +} + +/** + * Post create + */ +export type PostCreateArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * The data needed to create a Post. + */ + data: Prisma.XOR +} + +/** + * Post createMany + */ +export type PostCreateManyArgs = { + /** + * The data used to create many Posts. + */ + data: Prisma.PostCreateManyInput | Prisma.PostCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Post createManyAndReturn + */ +export type PostCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelectCreateManyAndReturn | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * The data used to create many Posts. + */ + data: Prisma.PostCreateManyInput | Prisma.PostCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Post update + */ +export type PostUpdateArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * The data needed to update a Post. + */ + data: Prisma.XOR + /** + * Choose, which Post to update. + */ + where: Prisma.PostWhereUniqueInput +} + +/** + * Post updateMany + */ +export type PostUpdateManyArgs = { + /** + * The data used to update Posts. + */ + data: Prisma.XOR + /** + * Filter which Posts to update + */ + where?: Prisma.PostWhereInput + /** + * Limit how many Posts to update. + */ + limit?: number +} + +/** + * Post updateManyAndReturn + */ +export type PostUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * The data used to update Posts. + */ + data: Prisma.XOR + /** + * Filter which Posts to update + */ + where?: Prisma.PostWhereInput + /** + * Limit how many Posts to update. + */ + limit?: number +} + +/** + * Post upsert + */ +export type PostUpsertArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * The filter to search for the Post to update in case it exists. + */ + where: Prisma.PostWhereUniqueInput + /** + * In case the Post found by the `where` argument doesn't exist, create a new Post with this data. + */ + create: Prisma.XOR + /** + * In case the Post was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * Post delete + */ +export type PostDeleteArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null + /** + * Filter which Post to delete. + */ + where: Prisma.PostWhereUniqueInput +} + +/** + * Post deleteMany + */ +export type PostDeleteManyArgs = { + /** + * Filter which Posts to delete + */ + where?: Prisma.PostWhereInput + /** + * Limit how many Posts to delete. + */ + limit?: number +} + +/** + * Post without action + */ +export type PostDefaultArgs = { + /** + * Select specific fields to fetch from the Post + */ + select?: Prisma.PostSelect | null + /** + * Omit specific fields from the Post + */ + omit?: Prisma.PostOmit | null +} diff --git a/src/generated/prisma/models/Trend.ts b/src/generated/prisma/models/Trend.ts new file mode 100644 index 0000000..24df7f8 --- /dev/null +++ b/src/generated/prisma/models/Trend.ts @@ -0,0 +1,1462 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `Trend` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums.js" +import type * as Prisma from "../internal/prismaNamespace.js" + +/** + * Model Trend + * + */ +export type TrendModel = runtime.Types.Result.DefaultSelection + +export type AggregateTrend = { + _count: TrendCountAggregateOutputType | null + _avg: TrendAvgAggregateOutputType | null + _sum: TrendSumAggregateOutputType | null + _min: TrendMinAggregateOutputType | null + _max: TrendMaxAggregateOutputType | null +} + +export type TrendAvgAggregateOutputType = { + id: number | null + score: number | null +} + +export type TrendSumAggregateOutputType = { + id: number | null + score: number | null +} + +export type TrendMinAggregateOutputType = { + id: number | null + title: string | null + description: string | null + url: string | null + score: number | null + source: string | null + category: string | null + geo: string | null + fingerprint: string | null + sourceTimestamp: Date | null + createdAt: Date | null + updatedAt: Date | null +} + +export type TrendMaxAggregateOutputType = { + id: number | null + title: string | null + description: string | null + url: string | null + score: number | null + source: string | null + category: string | null + geo: string | null + fingerprint: string | null + sourceTimestamp: Date | null + createdAt: Date | null + updatedAt: Date | null +} + +export type TrendCountAggregateOutputType = { + id: number + title: number + description: number + url: number + score: number + source: number + category: number + geo: number + tags: number + engagement: number + raw: number + fingerprint: number + sourceTimestamp: number + createdAt: number + updatedAt: number + _all: number +} + + +export type TrendAvgAggregateInputType = { + id?: true + score?: true +} + +export type TrendSumAggregateInputType = { + id?: true + score?: true +} + +export type TrendMinAggregateInputType = { + id?: true + title?: true + description?: true + url?: true + score?: true + source?: true + category?: true + geo?: true + fingerprint?: true + sourceTimestamp?: true + createdAt?: true + updatedAt?: true +} + +export type TrendMaxAggregateInputType = { + id?: true + title?: true + description?: true + url?: true + score?: true + source?: true + category?: true + geo?: true + fingerprint?: true + sourceTimestamp?: true + createdAt?: true + updatedAt?: true +} + +export type TrendCountAggregateInputType = { + id?: true + title?: true + description?: true + url?: true + score?: true + source?: true + category?: true + geo?: true + tags?: true + engagement?: true + raw?: true + fingerprint?: true + sourceTimestamp?: true + createdAt?: true + updatedAt?: true + _all?: true +} + +export type TrendAggregateArgs = { + /** + * Filter which Trend to aggregate. + */ + where?: Prisma.TrendWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Trends to fetch. + */ + orderBy?: Prisma.TrendOrderByWithRelationInput | Prisma.TrendOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.TrendWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Trends from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Trends. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Trends + **/ + _count?: true | TrendCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to average + **/ + _avg?: TrendAvgAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to sum + **/ + _sum?: TrendSumAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: TrendMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: TrendMaxAggregateInputType +} + +export type GetTrendAggregateType = { + [P in keyof T & keyof AggregateTrend]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type TrendGroupByArgs = { + where?: Prisma.TrendWhereInput + orderBy?: Prisma.TrendOrderByWithAggregationInput | Prisma.TrendOrderByWithAggregationInput[] + by: Prisma.TrendScalarFieldEnum[] | Prisma.TrendScalarFieldEnum + having?: Prisma.TrendScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: TrendCountAggregateInputType | true + _avg?: TrendAvgAggregateInputType + _sum?: TrendSumAggregateInputType + _min?: TrendMinAggregateInputType + _max?: TrendMaxAggregateInputType +} + +export type TrendGroupByOutputType = { + id: number + title: string + description: string | null + url: string | null + score: number + source: string + category: string | null + geo: string | null + tags: runtime.JsonValue | null + engagement: runtime.JsonValue | null + raw: runtime.JsonValue | null + fingerprint: string | null + sourceTimestamp: Date | null + createdAt: Date + updatedAt: Date | null + _count: TrendCountAggregateOutputType | null + _avg: TrendAvgAggregateOutputType | null + _sum: TrendSumAggregateOutputType | null + _min: TrendMinAggregateOutputType | null + _max: TrendMaxAggregateOutputType | null +} + +export type GetTrendGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof TrendGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type TrendWhereInput = { + AND?: Prisma.TrendWhereInput | Prisma.TrendWhereInput[] + OR?: Prisma.TrendWhereInput[] + NOT?: Prisma.TrendWhereInput | Prisma.TrendWhereInput[] + id?: Prisma.IntFilter<"Trend"> | number + title?: Prisma.StringFilter<"Trend"> | string + description?: Prisma.StringNullableFilter<"Trend"> | string | null + url?: Prisma.StringNullableFilter<"Trend"> | string | null + score?: Prisma.IntFilter<"Trend"> | number + source?: Prisma.StringFilter<"Trend"> | string + category?: Prisma.StringNullableFilter<"Trend"> | string | null + geo?: Prisma.StringNullableFilter<"Trend"> | string | null + tags?: Prisma.JsonNullableFilter<"Trend"> + engagement?: Prisma.JsonNullableFilter<"Trend"> + raw?: Prisma.JsonNullableFilter<"Trend"> + fingerprint?: Prisma.StringNullableFilter<"Trend"> | string | null + sourceTimestamp?: Prisma.DateTimeNullableFilter<"Trend"> | Date | string | null + createdAt?: Prisma.DateTimeFilter<"Trend"> | Date | string + updatedAt?: Prisma.DateTimeNullableFilter<"Trend"> | Date | string | null +} + +export type TrendOrderByWithRelationInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + description?: Prisma.SortOrderInput | Prisma.SortOrder + url?: Prisma.SortOrderInput | Prisma.SortOrder + score?: Prisma.SortOrder + source?: Prisma.SortOrder + category?: Prisma.SortOrderInput | Prisma.SortOrder + geo?: Prisma.SortOrderInput | Prisma.SortOrder + tags?: Prisma.SortOrderInput | Prisma.SortOrder + engagement?: Prisma.SortOrderInput | Prisma.SortOrder + raw?: Prisma.SortOrderInput | Prisma.SortOrder + fingerprint?: Prisma.SortOrderInput | Prisma.SortOrder + sourceTimestamp?: Prisma.SortOrderInput | Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrderInput | Prisma.SortOrder +} + +export type TrendWhereUniqueInput = Prisma.AtLeast<{ + id?: number + AND?: Prisma.TrendWhereInput | Prisma.TrendWhereInput[] + OR?: Prisma.TrendWhereInput[] + NOT?: Prisma.TrendWhereInput | Prisma.TrendWhereInput[] + title?: Prisma.StringFilter<"Trend"> | string + description?: Prisma.StringNullableFilter<"Trend"> | string | null + url?: Prisma.StringNullableFilter<"Trend"> | string | null + score?: Prisma.IntFilter<"Trend"> | number + source?: Prisma.StringFilter<"Trend"> | string + category?: Prisma.StringNullableFilter<"Trend"> | string | null + geo?: Prisma.StringNullableFilter<"Trend"> | string | null + tags?: Prisma.JsonNullableFilter<"Trend"> + engagement?: Prisma.JsonNullableFilter<"Trend"> + raw?: Prisma.JsonNullableFilter<"Trend"> + fingerprint?: Prisma.StringNullableFilter<"Trend"> | string | null + sourceTimestamp?: Prisma.DateTimeNullableFilter<"Trend"> | Date | string | null + createdAt?: Prisma.DateTimeFilter<"Trend"> | Date | string + updatedAt?: Prisma.DateTimeNullableFilter<"Trend"> | Date | string | null +}, "id"> + +export type TrendOrderByWithAggregationInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + description?: Prisma.SortOrderInput | Prisma.SortOrder + url?: Prisma.SortOrderInput | Prisma.SortOrder + score?: Prisma.SortOrder + source?: Prisma.SortOrder + category?: Prisma.SortOrderInput | Prisma.SortOrder + geo?: Prisma.SortOrderInput | Prisma.SortOrder + tags?: Prisma.SortOrderInput | Prisma.SortOrder + engagement?: Prisma.SortOrderInput | Prisma.SortOrder + raw?: Prisma.SortOrderInput | Prisma.SortOrder + fingerprint?: Prisma.SortOrderInput | Prisma.SortOrder + sourceTimestamp?: Prisma.SortOrderInput | Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrderInput | Prisma.SortOrder + _count?: Prisma.TrendCountOrderByAggregateInput + _avg?: Prisma.TrendAvgOrderByAggregateInput + _max?: Prisma.TrendMaxOrderByAggregateInput + _min?: Prisma.TrendMinOrderByAggregateInput + _sum?: Prisma.TrendSumOrderByAggregateInput +} + +export type TrendScalarWhereWithAggregatesInput = { + AND?: Prisma.TrendScalarWhereWithAggregatesInput | Prisma.TrendScalarWhereWithAggregatesInput[] + OR?: Prisma.TrendScalarWhereWithAggregatesInput[] + NOT?: Prisma.TrendScalarWhereWithAggregatesInput | Prisma.TrendScalarWhereWithAggregatesInput[] + id?: Prisma.IntWithAggregatesFilter<"Trend"> | number + title?: Prisma.StringWithAggregatesFilter<"Trend"> | string + description?: Prisma.StringNullableWithAggregatesFilter<"Trend"> | string | null + url?: Prisma.StringNullableWithAggregatesFilter<"Trend"> | string | null + score?: Prisma.IntWithAggregatesFilter<"Trend"> | number + source?: Prisma.StringWithAggregatesFilter<"Trend"> | string + category?: Prisma.StringNullableWithAggregatesFilter<"Trend"> | string | null + geo?: Prisma.StringNullableWithAggregatesFilter<"Trend"> | string | null + tags?: Prisma.JsonNullableWithAggregatesFilter<"Trend"> + engagement?: Prisma.JsonNullableWithAggregatesFilter<"Trend"> + raw?: Prisma.JsonNullableWithAggregatesFilter<"Trend"> + fingerprint?: Prisma.StringNullableWithAggregatesFilter<"Trend"> | string | null + sourceTimestamp?: Prisma.DateTimeNullableWithAggregatesFilter<"Trend"> | Date | string | null + createdAt?: Prisma.DateTimeWithAggregatesFilter<"Trend"> | Date | string + updatedAt?: Prisma.DateTimeNullableWithAggregatesFilter<"Trend"> | Date | string | null +} + +export type TrendCreateInput = { + title: string + description?: string | null + url?: string | null + score?: number + source: string + category?: string | null + geo?: string | null + tags?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + engagement?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + raw?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + fingerprint?: string | null + sourceTimestamp?: Date | string | null + createdAt?: Date | string + updatedAt?: Date | string | null +} + +export type TrendUncheckedCreateInput = { + id?: number + title: string + description?: string | null + url?: string | null + score?: number + source: string + category?: string | null + geo?: string | null + tags?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + engagement?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + raw?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + fingerprint?: string | null + sourceTimestamp?: Date | string | null + createdAt?: Date | string + updatedAt?: Date | string | null +} + +export type TrendUpdateInput = { + title?: Prisma.StringFieldUpdateOperationsInput | string + description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + url?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + score?: Prisma.IntFieldUpdateOperationsInput | number + source?: Prisma.StringFieldUpdateOperationsInput | string + category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + geo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + tags?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + engagement?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + raw?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + fingerprint?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + sourceTimestamp?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null +} + +export type TrendUncheckedUpdateInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + title?: Prisma.StringFieldUpdateOperationsInput | string + description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + url?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + score?: Prisma.IntFieldUpdateOperationsInput | number + source?: Prisma.StringFieldUpdateOperationsInput | string + category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + geo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + tags?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + engagement?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + raw?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + fingerprint?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + sourceTimestamp?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null +} + +export type TrendCreateManyInput = { + id?: number + title: string + description?: string | null + url?: string | null + score?: number + source: string + category?: string | null + geo?: string | null + tags?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + engagement?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + raw?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + fingerprint?: string | null + sourceTimestamp?: Date | string | null + createdAt?: Date | string + updatedAt?: Date | string | null +} + +export type TrendUpdateManyMutationInput = { + title?: Prisma.StringFieldUpdateOperationsInput | string + description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + url?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + score?: Prisma.IntFieldUpdateOperationsInput | number + source?: Prisma.StringFieldUpdateOperationsInput | string + category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + geo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + tags?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + engagement?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + raw?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + fingerprint?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + sourceTimestamp?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null +} + +export type TrendUncheckedUpdateManyInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + title?: Prisma.StringFieldUpdateOperationsInput | string + description?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + url?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + score?: Prisma.IntFieldUpdateOperationsInput | number + source?: Prisma.StringFieldUpdateOperationsInput | string + category?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + geo?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + tags?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + engagement?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + raw?: Prisma.NullableJsonNullValueInput | runtime.InputJsonValue + fingerprint?: Prisma.NullableStringFieldUpdateOperationsInput | string | null + sourceTimestamp?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null + createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string + updatedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null +} + +export type TrendCountOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + description?: Prisma.SortOrder + url?: Prisma.SortOrder + score?: Prisma.SortOrder + source?: Prisma.SortOrder + category?: Prisma.SortOrder + geo?: Prisma.SortOrder + tags?: Prisma.SortOrder + engagement?: Prisma.SortOrder + raw?: Prisma.SortOrder + fingerprint?: Prisma.SortOrder + sourceTimestamp?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type TrendAvgOrderByAggregateInput = { + id?: Prisma.SortOrder + score?: Prisma.SortOrder +} + +export type TrendMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + description?: Prisma.SortOrder + url?: Prisma.SortOrder + score?: Prisma.SortOrder + source?: Prisma.SortOrder + category?: Prisma.SortOrder + geo?: Prisma.SortOrder + fingerprint?: Prisma.SortOrder + sourceTimestamp?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type TrendMinOrderByAggregateInput = { + id?: Prisma.SortOrder + title?: Prisma.SortOrder + description?: Prisma.SortOrder + url?: Prisma.SortOrder + score?: Prisma.SortOrder + source?: Prisma.SortOrder + category?: Prisma.SortOrder + geo?: Prisma.SortOrder + fingerprint?: Prisma.SortOrder + sourceTimestamp?: Prisma.SortOrder + createdAt?: Prisma.SortOrder + updatedAt?: Prisma.SortOrder +} + +export type TrendSumOrderByAggregateInput = { + id?: Prisma.SortOrder + score?: Prisma.SortOrder +} + +export type NullableDateTimeFieldUpdateOperationsInput = { + set?: Date | string | null +} + + + +export type TrendSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + description?: boolean + url?: boolean + score?: boolean + source?: boolean + category?: boolean + geo?: boolean + tags?: boolean + engagement?: boolean + raw?: boolean + fingerprint?: boolean + sourceTimestamp?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["trend"]> + +export type TrendSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + description?: boolean + url?: boolean + score?: boolean + source?: boolean + category?: boolean + geo?: boolean + tags?: boolean + engagement?: boolean + raw?: boolean + fingerprint?: boolean + sourceTimestamp?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["trend"]> + +export type TrendSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + title?: boolean + description?: boolean + url?: boolean + score?: boolean + source?: boolean + category?: boolean + geo?: boolean + tags?: boolean + engagement?: boolean + raw?: boolean + fingerprint?: boolean + sourceTimestamp?: boolean + createdAt?: boolean + updatedAt?: boolean +}, ExtArgs["result"]["trend"]> + +export type TrendSelectScalar = { + id?: boolean + title?: boolean + description?: boolean + url?: boolean + score?: boolean + source?: boolean + category?: boolean + geo?: boolean + tags?: boolean + engagement?: boolean + raw?: boolean + fingerprint?: boolean + sourceTimestamp?: boolean + createdAt?: boolean + updatedAt?: boolean +} + +export type TrendOmit = runtime.Types.Extensions.GetOmit<"id" | "title" | "description" | "url" | "score" | "source" | "category" | "geo" | "tags" | "engagement" | "raw" | "fingerprint" | "sourceTimestamp" | "createdAt" | "updatedAt", ExtArgs["result"]["trend"]> + +export type $TrendPayload = { + name: "Trend" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: number + title: string + description: string | null + url: string | null + score: number + source: string + category: string | null + geo: string | null + tags: runtime.JsonValue | null + engagement: runtime.JsonValue | null + raw: runtime.JsonValue | null + fingerprint: string | null + sourceTimestamp: Date | null + createdAt: Date + updatedAt: Date | null + }, ExtArgs["result"]["trend"]> + composites: {} +} + +export type TrendGetPayload = runtime.Types.Result.GetResult + +export type TrendCountArgs = + Omit & { + select?: TrendCountAggregateInputType | true + } + +export interface TrendDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['Trend'], meta: { name: 'Trend' } } + /** + * Find zero or one Trend that matches the filter. + * @param {TrendFindUniqueArgs} args - Arguments to find a Trend + * @example + * // Get one Trend + * const trend = await prisma.trend.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__TrendClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one Trend that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {TrendFindUniqueOrThrowArgs} args - Arguments to find a Trend + * @example + * // Get one Trend + * const trend = await prisma.trend.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__TrendClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Trend that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {TrendFindFirstArgs} args - Arguments to find a Trend + * @example + * // Get one Trend + * const trend = await prisma.trend.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__TrendClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first Trend that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {TrendFindFirstOrThrowArgs} args - Arguments to find a Trend + * @example + * // Get one Trend + * const trend = await prisma.trend.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__TrendClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more Trends that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {TrendFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all Trends + * const trends = await prisma.trend.findMany() + * + * // Get first 10 Trends + * const trends = await prisma.trend.findMany({ take: 10 }) + * + * // Only select the `id` + * const trendWithIdOnly = await prisma.trend.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a Trend. + * @param {TrendCreateArgs} args - Arguments to create a Trend. + * @example + * // Create one Trend + * const Trend = await prisma.trend.create({ + * data: { + * // ... data to create a Trend + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__TrendClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many Trends. + * @param {TrendCreateManyArgs} args - Arguments to create many Trends. + * @example + * // Create many Trends + * const trend = await prisma.trend.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many Trends and returns the data saved in the database. + * @param {TrendCreateManyAndReturnArgs} args - Arguments to create many Trends. + * @example + * // Create many Trends + * const trend = await prisma.trend.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many Trends and only return the `id` + * const trendWithIdOnly = await prisma.trend.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a Trend. + * @param {TrendDeleteArgs} args - Arguments to delete one Trend. + * @example + * // Delete one Trend + * const Trend = await prisma.trend.delete({ + * where: { + * // ... filter to delete one Trend + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__TrendClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one Trend. + * @param {TrendUpdateArgs} args - Arguments to update one Trend. + * @example + * // Update one Trend + * const trend = await prisma.trend.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__TrendClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more Trends. + * @param {TrendDeleteManyArgs} args - Arguments to filter Trends to delete. + * @example + * // Delete a few Trends + * const { count } = await prisma.trend.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Trends. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {TrendUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Trends + * const trend = await prisma.trend.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Trends and returns the data updated in the database. + * @param {TrendUpdateManyAndReturnArgs} args - Arguments to update many Trends. + * @example + * // Update many Trends + * const trend = await prisma.trend.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more Trends and only return the `id` + * const trendWithIdOnly = await prisma.trend.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one Trend. + * @param {TrendUpsertArgs} args - Arguments to update or create a Trend. + * @example + * // Update or create a Trend + * const trend = await prisma.trend.upsert({ + * create: { + * // ... data to create a Trend + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the Trend we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__TrendClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of Trends. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {TrendCountArgs} args - Arguments to filter Trends to count. + * @example + * // Count the number of Trends + * const count = await prisma.trend.count({ + * where: { + * // ... the filter for the Trends we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a Trend. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {TrendAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by Trend. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {TrendGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends TrendGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: TrendGroupByArgs['orderBy'] } + : { orderBy?: TrendGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetTrendGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the Trend model + */ +readonly fields: TrendFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for Trend. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__TrendClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the Trend model + */ +export interface TrendFieldRefs { + readonly id: Prisma.FieldRef<"Trend", 'Int'> + readonly title: Prisma.FieldRef<"Trend", 'String'> + readonly description: Prisma.FieldRef<"Trend", 'String'> + readonly url: Prisma.FieldRef<"Trend", 'String'> + readonly score: Prisma.FieldRef<"Trend", 'Int'> + readonly source: Prisma.FieldRef<"Trend", 'String'> + readonly category: Prisma.FieldRef<"Trend", 'String'> + readonly geo: Prisma.FieldRef<"Trend", 'String'> + readonly tags: Prisma.FieldRef<"Trend", 'Json'> + readonly engagement: Prisma.FieldRef<"Trend", 'Json'> + readonly raw: Prisma.FieldRef<"Trend", 'Json'> + readonly fingerprint: Prisma.FieldRef<"Trend", 'String'> + readonly sourceTimestamp: Prisma.FieldRef<"Trend", 'DateTime'> + readonly createdAt: Prisma.FieldRef<"Trend", 'DateTime'> + readonly updatedAt: Prisma.FieldRef<"Trend", 'DateTime'> +} + + +// Custom InputTypes +/** + * Trend findUnique + */ +export type TrendFindUniqueArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * Filter, which Trend to fetch. + */ + where: Prisma.TrendWhereUniqueInput +} + +/** + * Trend findUniqueOrThrow + */ +export type TrendFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * Filter, which Trend to fetch. + */ + where: Prisma.TrendWhereUniqueInput +} + +/** + * Trend findFirst + */ +export type TrendFindFirstArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * Filter, which Trend to fetch. + */ + where?: Prisma.TrendWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Trends to fetch. + */ + orderBy?: Prisma.TrendOrderByWithRelationInput | Prisma.TrendOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Trends. + */ + cursor?: Prisma.TrendWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Trends from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Trends. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Trends. + */ + distinct?: Prisma.TrendScalarFieldEnum | Prisma.TrendScalarFieldEnum[] +} + +/** + * Trend findFirstOrThrow + */ +export type TrendFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * Filter, which Trend to fetch. + */ + where?: Prisma.TrendWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Trends to fetch. + */ + orderBy?: Prisma.TrendOrderByWithRelationInput | Prisma.TrendOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Trends. + */ + cursor?: Prisma.TrendWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Trends from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Trends. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Trends. + */ + distinct?: Prisma.TrendScalarFieldEnum | Prisma.TrendScalarFieldEnum[] +} + +/** + * Trend findMany + */ +export type TrendFindManyArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * Filter, which Trends to fetch. + */ + where?: Prisma.TrendWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Trends to fetch. + */ + orderBy?: Prisma.TrendOrderByWithRelationInput | Prisma.TrendOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Trends. + */ + cursor?: Prisma.TrendWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Trends from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Trends. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Trends. + */ + distinct?: Prisma.TrendScalarFieldEnum | Prisma.TrendScalarFieldEnum[] +} + +/** + * Trend create + */ +export type TrendCreateArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * The data needed to create a Trend. + */ + data: Prisma.XOR +} + +/** + * Trend createMany + */ +export type TrendCreateManyArgs = { + /** + * The data used to create many Trends. + */ + data: Prisma.TrendCreateManyInput | Prisma.TrendCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Trend createManyAndReturn + */ +export type TrendCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelectCreateManyAndReturn | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * The data used to create many Trends. + */ + data: Prisma.TrendCreateManyInput | Prisma.TrendCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * Trend update + */ +export type TrendUpdateArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * The data needed to update a Trend. + */ + data: Prisma.XOR + /** + * Choose, which Trend to update. + */ + where: Prisma.TrendWhereUniqueInput +} + +/** + * Trend updateMany + */ +export type TrendUpdateManyArgs = { + /** + * The data used to update Trends. + */ + data: Prisma.XOR + /** + * Filter which Trends to update + */ + where?: Prisma.TrendWhereInput + /** + * Limit how many Trends to update. + */ + limit?: number +} + +/** + * Trend updateManyAndReturn + */ +export type TrendUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * The data used to update Trends. + */ + data: Prisma.XOR + /** + * Filter which Trends to update + */ + where?: Prisma.TrendWhereInput + /** + * Limit how many Trends to update. + */ + limit?: number +} + +/** + * Trend upsert + */ +export type TrendUpsertArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * The filter to search for the Trend to update in case it exists. + */ + where: Prisma.TrendWhereUniqueInput + /** + * In case the Trend found by the `where` argument doesn't exist, create a new Trend with this data. + */ + create: Prisma.XOR + /** + * In case the Trend was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * Trend delete + */ +export type TrendDeleteArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null + /** + * Filter which Trend to delete. + */ + where: Prisma.TrendWhereUniqueInput +} + +/** + * Trend deleteMany + */ +export type TrendDeleteManyArgs = { + /** + * Filter which Trends to delete + */ + where?: Prisma.TrendWhereInput + /** + * Limit how many Trends to delete. + */ + limit?: number +} + +/** + * Trend without action + */ +export type TrendDefaultArgs = { + /** + * Select specific fields to fetch from the Trend + */ + select?: Prisma.TrendSelect | null + /** + * Omit specific fields from the Trend + */ + omit?: Prisma.TrendOmit | null +} diff --git a/src/generated/prisma/models/User.ts b/src/generated/prisma/models/User.ts new file mode 100644 index 0000000..5daee0e --- /dev/null +++ b/src/generated/prisma/models/User.ts @@ -0,0 +1,1150 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! */ +/* eslint-disable */ +// biome-ignore-all lint: generated file +// @ts-nocheck +/* + * This file exports the `User` model and its related types. + * + * 🟢 You can import this file directly. + */ +import type * as runtime from "@prisma/client/runtime/client" +import type * as $Enums from "../enums.js" +import type * as Prisma from "../internal/prismaNamespace.js" + +/** + * Model User + * + */ +export type UserModel = runtime.Types.Result.DefaultSelection + +export type AggregateUser = { + _count: UserCountAggregateOutputType | null + _avg: UserAvgAggregateOutputType | null + _sum: UserSumAggregateOutputType | null + _min: UserMinAggregateOutputType | null + _max: UserMaxAggregateOutputType | null +} + +export type UserAvgAggregateOutputType = { + id: number | null +} + +export type UserSumAggregateOutputType = { + id: number | null +} + +export type UserMinAggregateOutputType = { + id: number | null + email: string | null + name: string | null +} + +export type UserMaxAggregateOutputType = { + id: number | null + email: string | null + name: string | null +} + +export type UserCountAggregateOutputType = { + id: number + email: number + name: number + _all: number +} + + +export type UserAvgAggregateInputType = { + id?: true +} + +export type UserSumAggregateInputType = { + id?: true +} + +export type UserMinAggregateInputType = { + id?: true + email?: true + name?: true +} + +export type UserMaxAggregateInputType = { + id?: true + email?: true + name?: true +} + +export type UserCountAggregateInputType = { + id?: true + email?: true + name?: true + _all?: true +} + +export type UserAggregateArgs = { + /** + * Filter which User to aggregate. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Users + **/ + _count?: true | UserCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to average + **/ + _avg?: UserAvgAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to sum + **/ + _sum?: UserSumAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: UserMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: UserMaxAggregateInputType +} + +export type GetUserAggregateType = { + [P in keyof T & keyof AggregateUser]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType +} + + + + +export type UserGroupByArgs = { + where?: Prisma.UserWhereInput + orderBy?: Prisma.UserOrderByWithAggregationInput | Prisma.UserOrderByWithAggregationInput[] + by: Prisma.UserScalarFieldEnum[] | Prisma.UserScalarFieldEnum + having?: Prisma.UserScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: UserCountAggregateInputType | true + _avg?: UserAvgAggregateInputType + _sum?: UserSumAggregateInputType + _min?: UserMinAggregateInputType + _max?: UserMaxAggregateInputType +} + +export type UserGroupByOutputType = { + id: number + email: string + name: string | null + _count: UserCountAggregateOutputType | null + _avg: UserAvgAggregateOutputType | null + _sum: UserSumAggregateOutputType | null + _min: UserMinAggregateOutputType | null + _max: UserMaxAggregateOutputType | null +} + +export type GetUserGroupByPayload = Prisma.PrismaPromise< + Array< + Prisma.PickEnumerable & + { + [P in ((keyof T) & (keyof UserGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : Prisma.GetScalarType + : Prisma.GetScalarType + } + > + > + + + +export type UserWhereInput = { + AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + OR?: Prisma.UserWhereInput[] + NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + id?: Prisma.IntFilter<"User"> | number + email?: Prisma.StringFilter<"User"> | string + name?: Prisma.StringNullableFilter<"User"> | string | null +} + +export type UserOrderByWithRelationInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrderInput | Prisma.SortOrder +} + +export type UserWhereUniqueInput = Prisma.AtLeast<{ + id?: number + email?: string + AND?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + OR?: Prisma.UserWhereInput[] + NOT?: Prisma.UserWhereInput | Prisma.UserWhereInput[] + name?: Prisma.StringNullableFilter<"User"> | string | null +}, "id" | "email"> + +export type UserOrderByWithAggregationInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrderInput | Prisma.SortOrder + _count?: Prisma.UserCountOrderByAggregateInput + _avg?: Prisma.UserAvgOrderByAggregateInput + _max?: Prisma.UserMaxOrderByAggregateInput + _min?: Prisma.UserMinOrderByAggregateInput + _sum?: Prisma.UserSumOrderByAggregateInput +} + +export type UserScalarWhereWithAggregatesInput = { + AND?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[] + OR?: Prisma.UserScalarWhereWithAggregatesInput[] + NOT?: Prisma.UserScalarWhereWithAggregatesInput | Prisma.UserScalarWhereWithAggregatesInput[] + id?: Prisma.IntWithAggregatesFilter<"User"> | number + email?: Prisma.StringWithAggregatesFilter<"User"> | string + name?: Prisma.StringNullableWithAggregatesFilter<"User"> | string | null +} + +export type UserCreateInput = { + email: string + name?: string | null +} + +export type UserUncheckedCreateInput = { + id?: number + email: string + name?: string | null +} + +export type UserUpdateInput = { + email?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + +export type UserUncheckedUpdateInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + email?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + +export type UserCreateManyInput = { + id?: number + email: string + name?: string | null +} + +export type UserUpdateManyMutationInput = { + email?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + +export type UserUncheckedUpdateManyInput = { + id?: Prisma.IntFieldUpdateOperationsInput | number + email?: Prisma.StringFieldUpdateOperationsInput | string + name?: Prisma.NullableStringFieldUpdateOperationsInput | string | null +} + +export type UserCountOrderByAggregateInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrder +} + +export type UserAvgOrderByAggregateInput = { + id?: Prisma.SortOrder +} + +export type UserMaxOrderByAggregateInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrder +} + +export type UserMinOrderByAggregateInput = { + id?: Prisma.SortOrder + email?: Prisma.SortOrder + name?: Prisma.SortOrder +} + +export type UserSumOrderByAggregateInput = { + id?: Prisma.SortOrder +} + +export type StringFieldUpdateOperationsInput = { + set?: string +} + +export type NullableStringFieldUpdateOperationsInput = { + set?: string | null +} + +export type IntFieldUpdateOperationsInput = { + set?: number + increment?: number + decrement?: number + multiply?: number + divide?: number +} + + + +export type UserSelect = runtime.Types.Extensions.GetSelect<{ + id?: boolean + email?: boolean + name?: boolean +}, ExtArgs["result"]["user"]> + +export type UserSelectCreateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + email?: boolean + name?: boolean +}, ExtArgs["result"]["user"]> + +export type UserSelectUpdateManyAndReturn = runtime.Types.Extensions.GetSelect<{ + id?: boolean + email?: boolean + name?: boolean +}, ExtArgs["result"]["user"]> + +export type UserSelectScalar = { + id?: boolean + email?: boolean + name?: boolean +} + +export type UserOmit = runtime.Types.Extensions.GetOmit<"id" | "email" | "name", ExtArgs["result"]["user"]> + +export type $UserPayload = { + name: "User" + objects: {} + scalars: runtime.Types.Extensions.GetPayloadResult<{ + id: number + email: string + name: string | null + }, ExtArgs["result"]["user"]> + composites: {} +} + +export type UserGetPayload = runtime.Types.Result.GetResult + +export type UserCountArgs = + Omit & { + select?: UserCountAggregateInputType | true + } + +export interface UserDelegate { + [K: symbol]: { types: Prisma.TypeMap['model']['User'], meta: { name: 'User' } } + /** + * Find zero or one User that matches the filter. + * @param {UserFindUniqueArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUnique(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findUnique", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find one User that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {UserFindUniqueOrThrowArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findUniqueOrThrow(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findUniqueOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find the first User that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindFirstArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirst(args?: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findFirst", GlobalOmitOptions> | null, null, ExtArgs, GlobalOmitOptions> + + /** + * Find the first User that matches the filter or + * throw `PrismaKnownClientError` with `P2025` code if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindFirstOrThrowArgs} args - Arguments to find a User + * @example + * // Get one User + * const user = await prisma.user.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + */ + findFirstOrThrow(args?: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "findFirstOrThrow", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Find zero or more Users that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserFindManyArgs} args - Arguments to filter and select certain fields only. + * @example + * // Get all Users + * const users = await prisma.user.findMany() + * + * // Get first 10 Users + * const users = await prisma.user.findMany({ take: 10 }) + * + * // Only select the `id` + * const userWithIdOnly = await prisma.user.findMany({ select: { id: true } }) + * + */ + findMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "findMany", GlobalOmitOptions>> + + /** + * Create a User. + * @param {UserCreateArgs} args - Arguments to create a User. + * @example + * // Create one User + * const User = await prisma.user.create({ + * data: { + * // ... data to create a User + * } + * }) + * + */ + create(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "create", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Create many Users. + * @param {UserCreateManyArgs} args - Arguments to create many Users. + * @example + * // Create many Users + * const user = await prisma.user.createMany({ + * data: [ + * // ... provide data here + * ] + * }) + * + */ + createMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Create many Users and returns the data saved in the database. + * @param {UserCreateManyAndReturnArgs} args - Arguments to create many Users. + * @example + * // Create many Users + * const user = await prisma.user.createManyAndReturn({ + * data: [ + * // ... provide data here + * ] + * }) + * + * // Create many Users and only return the `id` + * const userWithIdOnly = await prisma.user.createManyAndReturn({ + * select: { id: true }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + createManyAndReturn(args?: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "createManyAndReturn", GlobalOmitOptions>> + + /** + * Delete a User. + * @param {UserDeleteArgs} args - Arguments to delete one User. + * @example + * // Delete one User + * const User = await prisma.user.delete({ + * where: { + * // ... filter to delete one User + * } + * }) + * + */ + delete(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "delete", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Update one User. + * @param {UserUpdateArgs} args - Arguments to update one User. + * @example + * // Update one User + * const user = await prisma.user.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + update(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "update", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + /** + * Delete zero or more Users. + * @param {UserDeleteManyArgs} args - Arguments to filter Users to delete. + * @example + * // Delete a few Users + * const { count } = await prisma.user.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + */ + deleteMany(args?: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Users. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Users + * const user = await prisma.user.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + */ + updateMany(args: Prisma.SelectSubset>): Prisma.PrismaPromise + + /** + * Update zero or more Users and returns the data updated in the database. + * @param {UserUpdateManyAndReturnArgs} args - Arguments to update many Users. + * @example + * // Update many Users + * const user = await prisma.user.updateManyAndReturn({ + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * + * // Update zero or more Users and only return the `id` + * const userWithIdOnly = await prisma.user.updateManyAndReturn({ + * select: { id: true }, + * where: { + * // ... provide filter here + * }, + * data: [ + * // ... provide data here + * ] + * }) + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * + */ + updateManyAndReturn(args: Prisma.SelectSubset>): Prisma.PrismaPromise, T, "updateManyAndReturn", GlobalOmitOptions>> + + /** + * Create or update one User. + * @param {UserUpsertArgs} args - Arguments to update or create a User. + * @example + * // Update or create a User + * const user = await prisma.user.upsert({ + * create: { + * // ... data to create a User + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the User we want to update + * } + * }) + */ + upsert(args: Prisma.SelectSubset>): Prisma.Prisma__UserClient, T, "upsert", GlobalOmitOptions>, never, ExtArgs, GlobalOmitOptions> + + + /** + * Count the number of Users. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserCountArgs} args - Arguments to filter Users to count. + * @example + * // Count the number of Users + * const count = await prisma.user.count({ + * where: { + * // ... the filter for the Users we want to count + * } + * }) + **/ + count( + args?: Prisma.Subset, + ): Prisma.PrismaPromise< + T extends runtime.Types.Utils.Record<'select', any> + ? T['select'] extends true + ? number + : Prisma.GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a User. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Prisma.Subset): Prisma.PrismaPromise> + + /** + * Group by User. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {UserGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends UserGroupByArgs, + HasSelectOrTake extends Prisma.Or< + Prisma.Extends<'skip', Prisma.Keys>, + Prisma.Extends<'take', Prisma.Keys> + >, + OrderByArg extends Prisma.True extends HasSelectOrTake + ? { orderBy: UserGroupByArgs['orderBy'] } + : { orderBy?: UserGroupByArgs['orderBy'] }, + OrderFields extends Prisma.ExcludeUnderscoreKeys>>, + ByFields extends Prisma.MaybeTupleToUnion, + ByValid extends Prisma.Has, + HavingFields extends Prisma.GetHavingFields, + HavingValid extends Prisma.Has, + ByEmpty extends T['by'] extends never[] ? Prisma.True : Prisma.False, + InputErrors extends ByEmpty extends Prisma.True + ? `Error: "by" must not be empty.` + : HavingValid extends Prisma.False + ? { + [P in HavingFields]: P extends ByFields + ? never + : P extends string + ? `Error: Field "${P}" used in "having" needs to be provided in "by".` + : [ + Error, + 'Field ', + P, + ` in "having" needs to be provided in "by"`, + ] + }[HavingFields] + : 'take' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "take", you also need to provide "orderBy"' + : 'skip' extends Prisma.Keys + ? 'orderBy' extends Prisma.Keys + ? ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + : 'Error: If you provide "skip", you also need to provide "orderBy"' + : ByValid extends Prisma.True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: Prisma.SubsetIntersection & InputErrors): {} extends InputErrors ? GetUserGroupByPayload : Prisma.PrismaPromise +/** + * Fields of the User model + */ +readonly fields: UserFieldRefs; +} + +/** + * The delegate class that acts as a "Promise-like" for User. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ +export interface Prisma__UserClient extends Prisma.PrismaPromise { + readonly [Symbol.toStringTag]: "PrismaPromise" + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): runtime.Types.Utils.JsPromise + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): runtime.Types.Utils.JsPromise +} + + + + +/** + * Fields of the User model + */ +export interface UserFieldRefs { + readonly id: Prisma.FieldRef<"User", 'Int'> + readonly email: Prisma.FieldRef<"User", 'String'> + readonly name: Prisma.FieldRef<"User", 'String'> +} + + +// Custom InputTypes +/** + * User findUnique + */ +export type UserFindUniqueArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which User to fetch. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User findUniqueOrThrow + */ +export type UserFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which User to fetch. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User findFirst + */ +export type UserFindFirstArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which User to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Users. + */ + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User findFirstOrThrow + */ +export type UserFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which User to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Users. + */ + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User findMany + */ +export type UserFindManyArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter, which Users to fetch. + */ + where?: Prisma.UserWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Users to fetch. + */ + orderBy?: Prisma.UserOrderByWithRelationInput | Prisma.UserOrderByWithRelationInput[] + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Users. + */ + cursor?: Prisma.UserWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Users from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Users. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Users. + */ + distinct?: Prisma.UserScalarFieldEnum | Prisma.UserScalarFieldEnum[] +} + +/** + * User create + */ +export type UserCreateArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data needed to create a User. + */ + data: Prisma.XOR +} + +/** + * User createMany + */ +export type UserCreateManyArgs = { + /** + * The data used to create many Users. + */ + data: Prisma.UserCreateManyInput | Prisma.UserCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * User createManyAndReturn + */ +export type UserCreateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelectCreateManyAndReturn | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data used to create many Users. + */ + data: Prisma.UserCreateManyInput | Prisma.UserCreateManyInput[] + skipDuplicates?: boolean +} + +/** + * User update + */ +export type UserUpdateArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data needed to update a User. + */ + data: Prisma.XOR + /** + * Choose, which User to update. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User updateMany + */ +export type UserUpdateManyArgs = { + /** + * The data used to update Users. + */ + data: Prisma.XOR + /** + * Filter which Users to update + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to update. + */ + limit?: number +} + +/** + * User updateManyAndReturn + */ +export type UserUpdateManyAndReturnArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelectUpdateManyAndReturn | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The data used to update Users. + */ + data: Prisma.XOR + /** + * Filter which Users to update + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to update. + */ + limit?: number +} + +/** + * User upsert + */ +export type UserUpsertArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * The filter to search for the User to update in case it exists. + */ + where: Prisma.UserWhereUniqueInput + /** + * In case the User found by the `where` argument doesn't exist, create a new User with this data. + */ + create: Prisma.XOR + /** + * In case the User was found with the provided `where` argument, update it with this data. + */ + update: Prisma.XOR +} + +/** + * User delete + */ +export type UserDeleteArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null + /** + * Filter which User to delete. + */ + where: Prisma.UserWhereUniqueInput +} + +/** + * User deleteMany + */ +export type UserDeleteManyArgs = { + /** + * Filter which Users to delete + */ + where?: Prisma.UserWhereInput + /** + * Limit how many Users to delete. + */ + limit?: number +} + +/** + * User without action + */ +export type UserDefaultArgs = { + /** + * Select specific fields to fetch from the User + */ + select?: Prisma.UserSelect | null + /** + * Omit specific fields from the User + */ + omit?: Prisma.UserOmit | null +} diff --git a/src/generated/prisma/wasm-edge-light-loader.mjs b/src/generated/prisma/wasm-edge-light-loader.mjs new file mode 100644 index 0000000..92a1b14 --- /dev/null +++ b/src/generated/prisma/wasm-edge-light-loader.mjs @@ -0,0 +1,5 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! +/* eslint-disable */ +// biome-ignore-all lint: generated file +export default import('./query_compiler_fast_bg.wasm?module') \ No newline at end of file diff --git a/src/generated/prisma/wasm-worker-loader.mjs b/src/generated/prisma/wasm-worker-loader.mjs new file mode 100644 index 0000000..9759c36 --- /dev/null +++ b/src/generated/prisma/wasm-worker-loader.mjs @@ -0,0 +1,5 @@ + +/* !!! This is code generated by Prisma. Do not edit directly. !!! +/* eslint-disable */ +// biome-ignore-all lint: generated file +export default import('./query_compiler_fast_bg.wasm') \ No newline at end of file diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a85c40e --- /dev/null +++ b/src/main.ts @@ -0,0 +1,50 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import {ValidationPipe} from "@nestjs/common"; +import {DocumentBuilder, SwaggerModule} from "@nestjs/swagger"; + +// const { createBullBoard } = require('@bull-board/api'); +// const { BullAdapter } = require('@bull-board/api/bullAdapter'); +// const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter'); +// const { ExpressAdapter } = require('@bull-board/express'); + + +async function bootstrap() { + const app = await NestFactory.create(AppModule, { + snapshot: true, + }); + + // Global validation pipe + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }), + ); + + // CORS + app.enableCors(); + + // Swagger + const config = new DocumentBuilder() + .setTitle('🔥 Trend Hunter API') + .setDescription( + 'API tìm kiếm và tổng hợp trends từ nhiều nguồn social media', + ) + .setVersion('1.0') + .addTag('Trends') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('docs', app, document); + + const port = process.env.PORT || 3000; + await app.listen(port); + + console.log(` + 🔥 X-News is running! + 📡 API: http://localhost:${port} + 📖 Swagger: http://localhost:${port}/docs + `); +} +bootstrap(); diff --git a/src/modules/ai-analysis/ai-analysis.module.ts b/src/modules/ai-analysis/ai-analysis.module.ts new file mode 100644 index 0000000..64e5ac1 --- /dev/null +++ b/src/modules/ai-analysis/ai-analysis.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; +import { ConfigModule } from '@nestjs/config'; +import { ClaudeService } from './claude.service'; +import { PerplexityService } from './perplexity.service'; +import { AiRouterService } from './ai-router.service'; + +@Module({ + imports: [ + ConfigModule, + HttpModule.register({ timeout: 30000 }), + ], + providers: [ClaudeService, PerplexityService, AiRouterService], + exports: [AiRouterService, ClaudeService, PerplexityService], +}) +export class AiAnalysisModule {} diff --git a/src/modules/ai-analysis/ai-router.service.ts b/src/modules/ai-analysis/ai-router.service.ts new file mode 100644 index 0000000..ed85beb --- /dev/null +++ b/src/modules/ai-analysis/ai-router.service.ts @@ -0,0 +1,51 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ClaudeService } from './claude.service'; +import { PerplexityService } from './perplexity.service'; +import {TrendItem} from "../../common/interfaces/trend-item.interface"; + +@Injectable() +export class AiRouterService { + private readonly logger = new Logger(AiRouterService.name); + + constructor( + private readonly claude: ClaudeService, + private readonly perplexity: PerplexityService, + ) {} + + /** + * Placeholder — bật lên khi cần AI analysis + * Hiện tại hệ thống chạy hoàn toàn bằng free collectors + */ + async enrichTrends(items: TrendItem[]): Promise { + const claudeAvailable = await this.claude.isAvailable(); + const perplexityAvailable = await this.perplexity.isAvailable(); + + if (!claudeAvailable && !perplexityAvailable) { + this.logger.log('No AI services configured — returning raw trends'); + return items; + } + + // Nếu Claude available → batch analyze top items + if (claudeAvailable) { + const topItems = items.slice(0, 20); + const analysis = await this.claude.analyzeBatch(topItems); + + if (analysis?.trends) { + // Merge AI analysis back vào items + for (const aiTrend of analysis.trends) { + const match = items.find( + (item) => + item.title.toLowerCase().includes(aiTrend.title.toLowerCase()) || + aiTrend.title.toLowerCase().includes(item.title.toLowerCase()), + ); + if (match) { + match.category = aiTrend.category; + match.description = match.description || aiTrend.summary; + } + } + } + } + + return items; + } +} diff --git a/src/modules/ai-analysis/claude.service.ts b/src/modules/ai-analysis/claude.service.ts new file mode 100644 index 0000000..9791fc4 --- /dev/null +++ b/src/modules/ai-analysis/claude.service.ts @@ -0,0 +1,251 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import {TrendItem} from "../../common/interfaces/trend-item.interface"; + +export interface ClaudeAnalysisResult { + summary: string; + category: string; + sentiment: 'positive' | 'negative' | 'neutral'; + importance: number; // 1-10 + relatedTopics: string[]; + keyEntities: string[]; + whyTrending: string; +} + +export interface ClaudeBatchAnalysisResult { + trends: { + title: string; + summary: string; + category: string; + sentiment: 'positive' | 'negative' | 'neutral'; + score: number; + }[]; +} + +@Injectable() +export class ClaudeService { + private readonly logger = new Logger(ClaudeService.name); + private readonly apiUrl = 'https://api.anthropic.com/v1/messages'; + private readonly apiKey: string; + private readonly isConfigured: boolean; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.apiKey = this.configService.get('ai.claudeKey', ''); + this.isConfigured = !!this.apiKey; + + if (!this.isConfigured) { + this.logger.warn( + 'Claude API key not configured — Claude analysis will be skipped', + ); + } + } + + /** + * Phân tích 1 trend item chi tiết + * Model: claude-3-5-haiku (rẻ nhất: $0.25/$1.25 per 1M tokens) + */ + async analyzeTrend(item: TrendItem): Promise { + if (!this.isConfigured) return null; + + try { + const response = await this.callClaude( + 'claude-3-5-haiku-20241022', + [ + { + role: 'user', + content: `Analyze this trending topic and respond in JSON only: + +Title: "${item.title}" +Description: "${item.description}" +Source: ${item.source} +Current engagement: ${JSON.stringify(item.engagement || {})} + +Return this exact JSON format: +{ + "summary": "2-3 câu tóm tắt bằng tiếng Việt", + "category": "tech|business|politics|entertainment|science|sports|health|other", + "sentiment": "positive|negative|neutral", + "importance": <1-10>, + "relatedTopics": ["topic1", "topic2", "topic3"], + "keyEntities": ["entity1", "entity2"], + "whyTrending": "giải thích ngắn gọn tại sao topic này đang hot" +}`, + }, + ], + 300, // max_tokens — giữ nhỏ để tiết kiệm + ); + + return this.parseJsonResponse(response); + } catch (error) { + this.logger.error(`Claude analyzeTrend error: ${error.message}`); + return null; + } + } + + /** + * Phân tích BATCH nhiều trends 1 lần — TIẾT KIỆM tokens + * Thay vì gọi 15 lần (15 × overhead), gom lại 1 lần + */ + async analyzeBatch( + items: TrendItem[], + ): Promise { + if (!this.isConfigured) return null; + + const trendList = items + .slice(0, 20) + .map( + (item, i) => + `${i + 1}. [${item.source}] "${item.title}" — Score: ${item.score}`, + ) + .join('\n'); + + try { + const response = await this.callClaude( + 'claude-3-5-haiku-20241022', + [ + { + role: 'user', + content: `Phân tích danh sách trends sau. Trả về JSON: + +${trendList} + +Return this exact JSON: +{ + "trends": [ + { + "title": "tên trend", + "summary": "tóm tắt ngắn tiếng Việt (1 câu)", + "category": "tech|business|politics|entertainment|science|sports|health|other", + "sentiment": "positive|negative|neutral", + "score": <0-100, đánh giá mức độ trending> + } + ] +}`, + }, + ], + 1500, + ); + + return this.parseJsonResponse(response); + } catch (error) { + this.logger.error(`Claude analyzeBatch error: ${error.message}`); + return null; + } + } + + /** + * Tóm tắt + categorize 1 nhóm trends (giống nhau) thành 1 unified trend + */ + async summarizeGroup( + items: TrendItem[], + ): Promise<{ title: string; summary: string; category: string } | null> { + if (!this.isConfigured) return null; + + const titles = items.map((i) => `- ${i.title} (${i.source})`).join('\n'); + + try { + const response = await this.callClaude( + 'claude-3-5-haiku-20241022', + [ + { + role: 'user', + content: `Các bài viết sau cùng 1 chủ đề. Tóm tắt thành 1 trend. JSON only: + +${titles} + +{ + "title": "tiêu đề tổng hợp ngắn gọn", + "summary": "tóm tắt 2-3 câu tiếng Việt", + "category": "tech|business|politics|entertainment|science|sports|health|other" +}`, + }, + ], + 250, + ); + + return this.parseJsonResponse(response); + } catch (error) { + this.logger.error(`Claude summarizeGroup error: ${error.message}`); + return null; + } + } + + /** + * Core API call tới Claude + */ + private async callClaude( + model: string, + messages: { role: string; content: string }[], + maxTokens: number, + ): Promise { + const { data } = await firstValueFrom( + this.httpService.post( + this.apiUrl, + { + model, + max_tokens: maxTokens, + temperature: 0.1, // Low temperature cho output consistent + messages, + }, + { + headers: { + 'x-api-key': this.apiKey, + 'anthropic-version': '2023-06-01', + 'Content-Type': 'application/json', + }, + }, + ), + ); + + const content = data.content?.[0]?.text || ''; + + // Log token usage cho cost tracking + if (data.usage) { + this.logger.debug( + `Claude tokens — input: ${data.usage.input_tokens}, output: ${data.usage.output_tokens}`, + ); + } + + return content; + } + + private parseJsonResponse(content: string): T | null { + try { + // Extract JSON từ response (có thể wrapped trong ```json```) + const jsonMatch = content.match(/```json\s*([\s\S]*?)```/) || + content.match(/(\{[\s\S]*\})/); + + if (jsonMatch) { + return JSON.parse(jsonMatch[1] || jsonMatch[0]); + } + return JSON.parse(content); + } catch (error) { + this.logger.warn(`Failed to parse Claude JSON: ${error.message}`); + this.logger.debug(`Raw response: ${content.substring(0, 200)}`); + return null; + } + } + + /** + * Health check — kiểm tra API key có valid không + */ + async isAvailable(): Promise { + if (!this.isConfigured) return false; + + try { + await this.callClaude( + 'claude-3-5-haiku-20241022', + [{ role: 'user', content: 'ping' }], + 5, + ); + return true; + } catch { + return false; + } + } +} diff --git a/src/modules/ai-analysis/perplexity.service.ts b/src/modules/ai-analysis/perplexity.service.ts new file mode 100644 index 0000000..098176d --- /dev/null +++ b/src/modules/ai-analysis/perplexity.service.ts @@ -0,0 +1,80 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; + +export interface PerplexitySearchResult { + content: string; + citations: string[]; +} + +@Injectable() +export class PerplexityService { + private readonly logger = new Logger(PerplexityService.name); + private readonly apiUrl = 'https://api.perplexity.ai/chat/completions'; + private readonly apiKey: string; + private readonly isConfigured: boolean; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.apiKey = this.configService.get('ai.perplexityKey', ''); + this.isConfigured = !!this.apiKey; + + if (!this.isConfigured) { + this.logger.warn('Perplexity API key not configured — search enrichment will be skipped'); + } + } + + /** + * Search real-time bằng Perplexity Sonar + */ + async search(query: string): Promise { + if (!this.isConfigured) return null; + + try { + const { data } = await firstValueFrom( + this.httpService.post( + this.apiUrl, + { + model: 'sonar', + messages: [ + { + role: 'system', + content: 'You are a helpful assistant that provides concise, factual answers about current events and trends.', + }, + { role: 'user', content: query }, + ], + max_tokens: 500, + temperature: 0.1, + }, + { + headers: { + Authorization: `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + }, + }, + ), + ); + + return { + content: data.choices?.[0]?.message?.content || '', + citations: data.citations || [], + }; + } catch (error) { + this.logger.error(`Perplexity search error: ${error.message}`); + return null; + } + } + + async isAvailable(): Promise { + if (!this.isConfigured) return false; + try { + const result = await this.search('ping'); + return !!result; + } catch { + return false; + } + } +} diff --git a/src/modules/collector/collector-orchestrator.service.ts b/src/modules/collector/collector-orchestrator.service.ts new file mode 100644 index 0000000..4c08bc4 --- /dev/null +++ b/src/modules/collector/collector-orchestrator.service.ts @@ -0,0 +1,248 @@ +import { Injectable, Logger } from '@nestjs/common'; +import {GoogleTrendsService} from "./google-trends.service"; +import {RedditService} from "./reddit.service"; +import {HackerNewsService} from "./hackernews.service"; +import {RssService} from "./rss.service"; +import {NewsApiService} from "./newsapi.service"; +import {CollectorResult, TrendItem} from "../../common/interfaces/trend-item.interface"; +import {TextUtil} from "../../common/utils/text.util"; + +export interface OrchestratorResult { + items: TrendItem[]; + stats: { + totalRaw: number; + afterDedup: number; + bySource: Record; + errors: string[]; + durationMs: number; + }; +} + +@Injectable() +export class CollectorOrchestratorService { + private readonly logger = new Logger(CollectorOrchestratorService.name); + + constructor( + private readonly googleTrends: GoogleTrendsService, + private readonly reddit: RedditService, + private readonly hackerNews: HackerNewsService, + private readonly rss: RssService, + private readonly newsApi: NewsApiService, + ) {} + + /** + * Thu thập từ TẤT CẢ nguồn, dedup, sort by score + */ + async collectAll(): Promise { + const start = Date.now(); + this.logger.log('🚀 Starting full collection cycle...'); + + // Chạy tất cả collectors song song + const results = await Promise.allSettled([ + // this.googleTrends.collect(), + this.reddit.collect(['general', 'tech', 'news', 'japan', 'korean']), + this.hackerNews.collect(), + this.rss.collect(), + this.newsApi.collect(), + ]); + + const collectorResults: CollectorResult[] = []; + const errors: string[] = []; + + const sourceNames = [ + 'google-trends', + 'reddit', + 'hackernews', + 'rss', + 'newsapi', + ]; + + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + collectorResults.push(result.value); + if (result.value.error) { + errors.push(`[${sourceNames[index]}] ${result.value.error}`); + } + } else { + errors.push( + `[${sourceNames[index]}] FAILED: ${result.reason?.message}`, + ); + this.logger.error( + `Collector ${sourceNames[index]} crashed: ${result.reason?.message}`, + ); + } + }); + + // Merge tất cả items + const allItems = collectorResults.flatMap((r) => r.items); + const totalRaw = allItems.length; + + // Count by source + const bySource: Record = {}; + for (const item of allItems) { + const src = item.source.split(':')[0]; + bySource[src] = (bySource[src] || 0) + 1; + } + + // Dedup + const deduped = this.deduplicateItems(allItems); + + // Cross-reference bonus: nếu cùng topic xuất hiện ở nhiều source → boost score + const boosted = this.applyCrossSourceBonus(deduped); + + // Sort by score desc + boosted.sort((a, b) => b.score - a.score); + + const durationMs = Date.now() - start; + + this.logger.log( + `✅ Collection complete: ${totalRaw} raw → ${boosted.length} deduped in ${durationMs}ms`, + ); + this.logger.log(`📊 By source: ${JSON.stringify(bySource)}`); + + if (errors.length > 0) { + this.logger.warn(`⚠️ Errors: ${errors.length}`); + errors.forEach((e) => this.logger.warn(` ${e}`)); + } + + return { + items: boosted, + stats: { + totalRaw, + afterDedup: boosted.length, + bySource, + errors, + durationMs, + }, + }; + } + + /** + * Thu thập từ 1 source cụ thể + */ + async collectFromSource(source: string): Promise { + switch (source) { + case 'google-trends': + return this.googleTrends.collect(); + case 'reddit': + return this.reddit.collect(); + case 'hackernews': + return this.hackerNews.collect(); + case 'rss': + return this.rss.collect(); + case 'newsapi': + return this.newsApi.collect(); + default: + throw new Error(`Unknown source: ${source}`); + } + } + + /** + * Dedup dựa trên title similarity (Jaccard >= 0.5) + */ + private deduplicateItems(items: TrendItem[]): TrendItem[] { + const result: TrendItem[] = []; + const fingerprints = new Map(); // fingerprint → index in result + + for (const item of items) { + if (!item.title || item.title.trim().length === 0) continue; + + const fp = TextUtil.fingerprint(item.title); + + // Check exact fingerprint match + if (fingerprints.has(fp)) { + const existingIdx = fingerprints.get(fp)!; + // Giữ item có score cao hơn + if (item.score > result[existingIdx].score) { + result[existingIdx] = item; + } + continue; + } + + // Check similarity với existing items (chỉ check 50 gần nhất để performance) + let isDuplicate = false; + const startCheck = Math.max(0, result.length - 50); + + for (let i = startCheck; i < result.length; i++) { + const similarity = TextUtil.jaccardSimilarity( + item.title, + result[i].title, + ); + if (similarity >= 0.5) { + // Merge: giữ score cao hơn, gộp source + if (item.score > result[i].score) { + result[i] = { + ...item, + tags: [ + ...new Set([ + ...(result[i].tags || []), + ...(item.tags || []), + ]), + ], + }; + } + isDuplicate = true; + break; + } + } + + if (!isDuplicate) { + fingerprints.set(fp, result.length); + result.push(item); + } + } + + return result; + } + + /** + * Nếu cùng topic xuất hiện trên nhiều source → boost score + * Vì: multi-source = THỰC SỰ TRENDING + */ + private applyCrossSourceBonus(items: TrendItem[]): TrendItem[] { + // Group by similar titles + const groups: { canonical: TrendItem; sources: Set }[] = []; + + for (const item of items) { + let foundGroup = false; + + for (const group of groups) { + const sim = TextUtil.jaccardSimilarity( + item.title, + group.canonical.title, + ); + if (sim >= 0.35) { + // Looser threshold cho cross-source matching + const src = item.source.split(':')[0]; + group.sources.add(src); + foundGroup = true; + break; + } + } + + if (!foundGroup) { + const src = item.source.split(':')[0]; + groups.push({ canonical: item, sources: new Set([src]) }); + } + } + + // Tạo source count map + const titleBonus = new Map(); + for (const group of groups) { + if (group.sources.size > 1) { + const bonus = (group.sources.size - 1) * 12; // +12 per extra source + titleBonus.set(TextUtil.fingerprint(group.canonical.title), bonus); + } + } + + // Apply bonus + return items.map((item) => { + const fp = TextUtil.fingerprint(item.title); + const bonus = titleBonus.get(fp) || 0; + if (bonus > 0) { + return { ...item, score: Math.min(item.score + bonus, 100) }; + } + return item; + }); + } +} diff --git a/src/modules/collector/collector.module.ts b/src/modules/collector/collector.module.ts new file mode 100644 index 0000000..d426dea --- /dev/null +++ b/src/modules/collector/collector.module.ts @@ -0,0 +1,29 @@ +import {Module} from "@nestjs/common"; +import {ConfigModule} from "@nestjs/config"; +import {HttpModule} from "@nestjs/axios"; +import {GoogleTrendsService} from "./google-trends.service"; +import {RedditService} from "./reddit.service"; +import {HackerNewsService} from "./hackernews.service"; +import {RssService} from "./rss.service"; +import {NewsApiService} from "./newsapi.service"; +import {CollectorOrchestratorService} from "./collector-orchestrator.service"; + +@Module({ + imports: [ + ConfigModule, + HttpModule.register({ + timeout: 15000, + maxRedirects: 3, + }), + ], + providers: [ + GoogleTrendsService, + RedditService, + HackerNewsService, + RssService, + NewsApiService, + CollectorOrchestratorService, + ], + exports: [CollectorOrchestratorService, GoogleTrendsService], +}) +export class CollectorModule {} diff --git a/src/modules/collector/google-trends.service.ts b/src/modules/collector/google-trends.service.ts new file mode 100644 index 0000000..927b71a --- /dev/null +++ b/src/modules/collector/google-trends.service.ts @@ -0,0 +1,185 @@ +import {Injectable, Logger} from '@nestjs/common'; +import {ConfigService} from '@nestjs/config'; +import googleTrends from '@shaivpidadi/trends-js'; +import {TrendItem, CollectorResult} from '../../common/interfaces/trend-item.interface'; +import {_JsonParseSafe} from "../../shared/helper"; + +@Injectable() +export class GoogleTrendsService { + private readonly logger = new Logger(GoogleTrendsService.name); + + constructor(private readonly configService: ConfigService) { + } + + async collect(googleTrendsGeo: string = 'JP'): Promise { + const start = Date.now(); + const items: TrendItem[] = []; + const errors: string[] = []; + + // 1. Daily trends + try { + const daily = await this.getDailyTrends(googleTrendsGeo); + items.push(...daily); + } catch (e) { + errors.push(`dailyTrends: ${e.message}`); + } + + // 2. Real-time trends (nếu available) + try { + // const realtime = await this.getRealtimeTrends(); + // items.push(...realtime); + } catch (e) { + errors.push(`realtimeTrends: ${e.message}`); + } + + return { + source: 'google-trends', + items, + collectedAt: new Date(), + durationMs: Date.now() - start, + error: errors.length > 0 ? errors.join('; ') : undefined, + }; + } + + /** + * Daily trending searches — 100% FREE + */ + private async getDailyTrends(googleTrendsGeo: string = 'JP'): Promise { + console.debug('google-trends:getDailyTrends :'+googleTrendsGeo); + const {data: results} = await googleTrends.dailyTrends({ + trendDate: new Date(), + geo: googleTrendsGeo, + }); + + // const parsed = _JsonParseSafe(results); + + const days = results.allTrendingStories || []; + // const days = results.s || []; + console.log(days); + const allTrends = days.splice(0,15).map((trend) => { + const article = trend.articles?.[0]; + const traffic = trend.formattedTraffic || '0'; + const trafficNum = this.parseTraffic(traffic); + + return { + source: 'google-trends:daily', + title: trend.title || '', + description: article?.snippet || trend.title || '', + url: article?.url || `https://trends.google.com/trends/explore?q=${encodeURIComponent(trend.title || '')}`, + score: 10,//this.calculateScore(trafficNum, index), + timestamp: new Date(), + category: this.detectCategory(trend), + tags: (trend.relatedQueries || []) + .map((q: any) => q.query) + .slice(0, 5), + engagement: { + views: trafficNum, + }, + raw: { + formattedTraffic: traffic, + image: trend.image, + articles: (trend.articles || []).map((a: any) => ({ + title: a.title, + url: a.url, + source: a.source, + })), + }, + }; + } + ); + + this.logger.log(`Google Trends daily: ${allTrends.length} items`); + return allTrends; + } + + /** + * Realtime trending topics + */ + private async getRealtimeTrends(googleTrendsGeo: string = 'JP'): Promise { + try { + console.debug('google-trends:getRealtimeTrends'); + const results = await googleTrends.realTimeTrends({ + geo: googleTrendsGeo, + category: 'all', + }); + + const parsed = JSON.parse(results); + const stories = parsed.storySummaries?.trendingStories || []; + + const items: TrendItem[] = stories.map((story: any, index: number) => ({ + source: 'google-trends:realtime', + title: story.title || story.entityNames?.join(', ') || '', + description: story.articles?.[0]?.articleTitle || '', + url: story.articles?.[0]?.url || '', + score: Math.max(95 - index * 4, 10), + timestamp: new Date(), + tags: story.entityNames || [], + engagement: { + shares: story.articles?.length || 0, + }, + raw: { + entityNames: story.entityNames, + articleCount: story.articles?.length, + }, + })); + + this.logger.log(`Google Trends realtime: ${items.length} items`); + return items; + } catch (error) { + // realTimeTrends không phải lúc nào cũng available cho mọi geo + this.logger.warn(`Realtime trends not available: ${error.message}`); + return []; + } + } + + /** + * Tra cứu interest cho 1 keyword cụ thể — dùng cho scoring + */ + async getInterestScore(keyword: string, googleTrendsGeo: string = 'JP'): Promise { + try { + const result = await googleTrends.interestByRegion({ + keyword, + startTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + geo: googleTrendsGeo, + }); + const parsed = JSON.parse(result); + const timeline = parsed.default?.timelineData || []; + if (timeline.length === 0) return 0; + + return timeline[timeline.length - 1].value?.[0] || 0; + } catch { + return 0; + } + } + + private parseTraffic(formatted: string): number { + // "200K+" → 200000, "1M+" → 1000000 + const clean = formatted.replace(/[+,]/g, '').trim(); + const match = clean.match(/^(\d+(?:\.\d+)?)\s*(K|M|B)?$/i); + if (!match) return 0; + + const num = parseFloat(match[1]); + const unit = (match[2] || '').toUpperCase(); + const multipliers: Record = {K: 1000, M: 1000000, B: 1000000000}; + return Math.round(num * (multipliers[unit] || 1)); + } + + private calculateScore(traffic: number, index: number): number { + // Kết hợp traffic volume + ranking position + const trafficScore = traffic > 0 ? Math.min(Math.log10(traffic) * 15, 60) : 30; + const positionScore = Math.max(40 - index * 3, 0); + return Math.min(Math.round(trafficScore + positionScore), 100); + } + + private detectCategory(trend: any): string { + // Basic category detection dựa trên articles + const sources = (trend.articles || []) + .map((a: any) => (a.source || '').toLowerCase()) + .join(' '); + + if (/sport|bóng|goal|fifa|nfl|nba/i.test(sources)) return 'sports'; + if (/tech|crypto|ai|apple|google|meta/i.test(sources)) return 'tech'; + if (/politic|chính trị|quốc hội/i.test(sources)) return 'politics'; + return 'general'; + } +} diff --git a/src/modules/collector/hackernews.service.ts b/src/modules/collector/hackernews.service.ts new file mode 100644 index 0000000..6f04c58 --- /dev/null +++ b/src/modules/collector/hackernews.service.ts @@ -0,0 +1,197 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import { TrendItem, CollectorResult } from '../../common/interfaces/trend-item.interface'; + +@Injectable() +export class HackerNewsService { + private readonly logger = new Logger(HackerNewsService.name); + private readonly baseUrl = 'https://hacker-news.firebaseio.com/v0'; + private readonly maxItems: number; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.maxItems = this.configService.get( + 'collector.maxItemsPerSource', + 25, + ); + } + + async collect(): Promise { + const start = Date.now(); + const items: TrendItem[] = []; + const errors: string[] = []; + + // Thu thập từ cả 3 endpoint + const endpoints = [ + { name: 'top', fn: () => this.getStories('topstories') }, + { name: 'best', fn: () => this.getStories('beststories') }, + { name: 'new-hot', fn: () => this.getNewHotStories() }, + ]; + + for (const ep of endpoints) { + try { + const stories = await ep.fn(); + items.push(...stories); + } catch (e) { + errors.push(`${ep.name}: ${e.message}`); + } + } + + // Dedup bởi URL (vì top/best có thể overlap) + const seen = new Set(); + const deduped = items.filter((item) => { + if (seen.has(item.url)) return false; + seen.add(item.url); + return true; + }); + + this.logger.log(`HackerNews collected: ${deduped.length} items`); + + return { + source: 'hackernews', + items: deduped, + collectedAt: new Date(), + durationMs: Date.now() - start, + error: errors.length > 0 ? errors.join('; ') : undefined, + }; + } + + /** + * Lấy stories từ endpoint (topstories / beststories / newstories) + * 100% FREE, UNLIMITED, không cần API key + */ + private async getStories( + endpoint: string, + limit?: number, + ): Promise { + const effectiveLimit = limit || this.maxItems; + + // 1. Lấy danh sách IDs + const { data: storyIds } = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/${endpoint}.json`), + ); + + const topIds = storyIds.slice(0, effectiveLimit); + + // 2. Lấy chi tiết song song (batch 10 để tránh quá nhiều concurrent) + const stories: TrendItem[] = []; + const batchSize = 10; + + for (let i = 0; i < topIds.length; i += batchSize) { + const batch = topIds.slice(i, i + batchSize); + const batchResults = await Promise.allSettled( + batch.map((id) => this.getStoryDetail(id)), + ); + + for (const result of batchResults) { + if (result.status === 'fulfilled' && result.value) { + stories.push({ + ...result.value, + // Score dựa trên position trong list + score: Math.max( + result.value.score, + 100 - Math.round((stories.length / effectiveLimit) * 80), + ), + }); + } + } + } + + return stories; + } + + /** + * Lọc new stories có engagement cao (rising trends) + */ + private async getNewHotStories(): Promise { + const { data: newIds } = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/newstories.json`), + ); + + // Lấy 50 bài mới nhất, filter lấy những bài nào có điểm cao + const topNewIds = newIds.slice(0, 50); + const batchSize = 10; + const hotNew: TrendItem[] = []; + + for (let i = 0; i < topNewIds.length; i += batchSize) { + const batch = topNewIds.slice(i, i + batchSize); + const results = await Promise.allSettled( + batch.map((id) => this.getStoryDetail(id)), + ); + + for (const result of results) { + if (result.status === 'fulfilled' && result.value) { + const item = result.value; + // Chỉ lấy những bài mới nhưng đã có engagement + const points = item.engagement?.upvotes || 0; + const comments = item.engagement?.comments || 0; + if (points >= 5 || comments >= 3) { + hotNew.push({ + ...item, + source: 'hackernews:rising', + // Rising trend bonus + score: Math.min(item.score + 15, 100), + }); + } + } + } + } + + return hotNew; + } + + private async getStoryDetail(id: number): Promise { + const { data } = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/item/${id}.json`), + ); + + if (!data || data.deleted || data.dead) return null; + + const points = data.score || 0; + const comments = data.descendants || 0; + + return { + source: 'hackernews', + title: data.title || '', + description: data.text + ? data.text.replace(/<[^>]*>/g, '').substring(0, 500) + : '', + url: data.url || `https://news.ycombinator.com/item?id=${id}`, + score: this.calculateScore(points, comments), + timestamp: new Date((data.time || 0) * 1000), + category: 'tech', + tags: this.extractDomain(data.url), + engagement: { + upvotes: points, + comments, + }, + raw: { + hnId: id, + by: data.by, + type: data.type, + domain: data.url ? new URL(data.url).hostname : 'news.ycombinator.com', + hnUrl: `https://news.ycombinator.com/item?id=${id}`, + }, + }; + } + + private calculateScore(points: number, comments: number): number { + const pointScore = Math.log10(Math.max(points, 1)) * 20; + const commentScore = Math.log10(Math.max(comments, 1)) * 10; + return Math.min(Math.round(pointScore + commentScore), 100); + } + + private extractDomain(url?: string): string[] { + if (!url) return []; + try { + const hostname = new URL(url).hostname.replace('www.', ''); + return [hostname]; + } catch { + return []; + } + } +} diff --git a/src/modules/collector/newsapi.service.ts b/src/modules/collector/newsapi.service.ts new file mode 100644 index 0000000..108eabb --- /dev/null +++ b/src/modules/collector/newsapi.service.ts @@ -0,0 +1,164 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import { TrendItem, CollectorResult } from '../../common/interfaces/trend-item.interface'; + +@Injectable() +export class NewsApiService { + private readonly logger = new Logger(NewsApiService.name); + private readonly baseUrl = 'https://newsapi.org/v2'; + private readonly apiKey: string; + + // Track daily usage (free = 100 req/day) + private dailyRequestCount = 0; + private lastResetDate = new Date().toDateString(); + private readonly DAILY_LIMIT = 90; // Buffer 10 cho safety + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.apiKey = this.configService.get('newsapi.key', ''); + } + + async collect(): Promise { + const start = Date.now(); + + if (!this.apiKey) { + return { + source: 'newsapi', + items: [], + collectedAt: new Date(), + durationMs: 0, + error: 'NEWSAPI_KEY not configured — skipping', + }; + } + + if (!this.canMakeRequest()) { + return { + source: 'newsapi', + items: [], + collectedAt: new Date(), + durationMs: 0, + error: `Daily limit reached (${this.dailyRequestCount}/${this.DAILY_LIMIT})`, + }; + } + + const items: TrendItem[] = []; + const errors: string[] = []; + + // 1. Top headlines (1 request) + try { + const headlines = await this.getTopHeadlines(); + items.push(...headlines); + } catch (e) { + errors.push(`headlines: ${e.message}`); + } + + // 2. Top headlines cho VN (1 request) + try { + const vnHeadlines = await this.getTopHeadlines('vn'); + items.push(...vnHeadlines); + } catch (e) { + errors.push(`headlines-vn: ${e.message}`); + } + + this.logger.log( + `NewsAPI collected: ${items.length} items (${this.dailyRequestCount}/${this.DAILY_LIMIT} daily requests used)`, + ); + + return { + source: 'newsapi', + items, + collectedAt: new Date(), + durationMs: Date.now() - start, + error: errors.length > 0 ? errors.join('; ') : undefined, + }; + } + + /** + * Top Headlines — 1 request mỗi lần gọi + * Free tier: 100 requests/day + */ + private async getTopHeadlines(country?: string): Promise { + this.incrementRequestCount(); + + const params: Record = { + apiKey: this.apiKey, + pageSize: 20, + }; + + if (country) { + params.country = country; + } else { + // Nếu không chỉ định country, lấy theo category + params.language = 'en'; + params.category = 'technology'; + } + + const { data } = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/top-headlines`, { params }), + ); + + if (data.status !== 'ok') { + throw new Error(`NewsAPI error: ${data.message}`); + } + + return (data.articles || []).map((article: any, index: number) => ({ + source: `newsapi${country ? `:${country}` : ''}`, + title: article.title || '', + description: article.description || '', + url: article.url || '', + score: this.calculateScore(index, article), + timestamp: article.publishedAt + ? new Date(article.publishedAt) + : new Date(), + category: country === 'vn' ? 'vietnam' : 'tech', + tags: [article.source?.name].filter(Boolean), + engagement: {}, + raw: { + sourceName: article.source?.name, + sourceId: article.source?.id, + author: article.author, + urlToImage: article.urlToImage, + }, + })); + } + + private calculateScore(index: number, article: any): number { + const positionScore = Math.max(70 - index * 3, 10); + // Boost nếu từ nguồn uy tín + const topSources = [ + 'bbc-news', 'cnn', 'reuters', 'the-verge', + 'techcrunch', 'bloomberg', 'associated-press', + ]; + const sourceBonus = topSources.includes(article.source?.id) ? 15 : 0; + + return Math.min(positionScore + sourceBonus, 100); + } + + private canMakeRequest(): boolean { + this.resetDailyCountIfNeeded(); + return this.dailyRequestCount < this.DAILY_LIMIT; + } + + private incrementRequestCount(): void { + this.resetDailyCountIfNeeded(); + this.dailyRequestCount++; + } + + private resetDailyCountIfNeeded(): void { + const today = new Date().toDateString(); + if (today !== this.lastResetDate) { + this.dailyRequestCount = 0; + this.lastResetDate = today; + this.logger.log('NewsAPI daily request counter reset'); + } + } + + getRemainingRequests(): number { + this.resetDailyCountIfNeeded(); + return Math.max(this.DAILY_LIMIT - this.dailyRequestCount, 0); + } +} diff --git a/src/modules/collector/reddit.service.ts b/src/modules/collector/reddit.service.ts new file mode 100644 index 0000000..98b2bdd --- /dev/null +++ b/src/modules/collector/reddit.service.ts @@ -0,0 +1,186 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import { TrendItem, CollectorResult } from '../../common/interfaces/trend-item.interface'; + +// Các subreddit phổ biến theo category +const SUBREDDIT_MAP: Record = { + general: ['popular', 'all'], + tech: ['technology', 'programming', 'webdev', 'artificial', 'MachineLearning'], + news: ['worldnews', 'news', 'UpliftingNews'], + science: ['science', 'space', 'Futurology'], + business: ['business', 'Economics', 'stocks', 'CryptoCurrency'], + vietnam: ['VietNam', 'vietnam'], + japan: ['Japan', 'japanese'], + entertainment: ['movies', 'gaming', 'Music'], +}; + +@Injectable() +export class RedditService { + private readonly logger = new Logger(RedditService.name); + private readonly baseUrl = 'https://www.reddit.com'; + private readonly userAgent: string; + private readonly maxItems: number; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.userAgent = this.configService.get( + 'collector.redditUserAgent', + 'TrendHunter/1.0', + ); + this.maxItems = this.configService.get( + 'collector.maxItemsPerSource', + 25, + ); + } + + async collect( + categories: string[] = ['general', 'tech', 'news'], + ): Promise { + this.logger.debug('RedditService collect ...'); + const start = Date.now(); + const allItems: TrendItem[] = []; + const errors: string[] = []; + + // Thu thập từ mỗi category + const subreddits = categories.flatMap((cat) => SUBREDDIT_MAP[cat] || []); + const uniqueSubreddits = [...new Set(subreddits)]; + + const results = await Promise.allSettled( + uniqueSubreddits.map((sub) => this.getHotPosts(sub)), + ); + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === 'fulfilled') { + allItems.push(...result.value); + } else { + errors.push(`r/${uniqueSubreddits[i]}: ${result.reason?.message}`); + } + + // Rate limiting: Reddit cho phép ~60 req/phút cho unauthenticated + // Thêm delay nhỏ giữa mỗi request + if (i < results.length - 1) { + await this.sleep(500); + } + } + + this.logger.log( + `Reddit collected: ${allItems.length} items from ${uniqueSubreddits.length} subreddits`, + ); + + return { + source: 'reddit', + items: allItems, + collectedAt: new Date(), + durationMs: Date.now() - start, + error: errors.length > 0 ? errors.join('; ') : undefined, + }; + } + + /** + * Lấy hot posts từ 1 subreddit + * Dùng .json endpoint — KHÔNG CẦN API KEY + */ + private async getHotPosts( + subreddit: string, + limit: number = 15, + ): Promise { + const { data } = await firstValueFrom( + this.httpService.get(`${this.baseUrl}/r/${subreddit}/hot.json`, { + params: { + limit: Math.min(limit, this.maxItems), + raw_json: 1, + }, + headers: { + 'User-Agent': this.userAgent, + }, + }), + ); + + const posts = data?.data?.children || []; + + return posts + .filter((post: any) => { + // Bỏ stickied posts (thường là rules/announcements) + return !post.data.stickied && post.data.title; + }) + .map((post: any) => { + const d = post.data; + return { + source: `reddit:r/${subreddit}`, + title: d.title, + description: this.buildDescription(d), + url: d.url_overridden_by_dest || `https://reddit.com${d.permalink}`, + score: this.normalizeScore(d.score, d.num_comments, d.upvote_ratio), + timestamp: new Date(d.created_utc * 1000), + category: this.mapSubredditToCategory(subreddit), + tags: [ + subreddit, + d.link_flair_text, + ].filter(Boolean), + engagement: { + upvotes: d.score, + comments: d.num_comments, + shares: d.num_crossposts || 0, + }, + raw: { + subreddit: d.subreddit, + permalink: d.permalink, + upvoteRatio: d.upvote_ratio, + isOriginalContent: d.is_original_content, + flair: d.link_flair_text, + awards: d.total_awards_received, + thumbnail: d.thumbnail, + domain: d.domain, + }, + } as TrendItem; + }); + } + + private buildDescription(postData: any): string { + if (postData.selftext) { + return postData.selftext.substring(0, 500); + } + if (postData.media?.reddit_video) { + return `[Video] ${postData.title}`; + } + if (postData.post_hint === 'image') { + return `[Image] ${postData.title}`; + } + return postData.title; + } + + private normalizeScore( + upvotes: number, + comments: number, + upvoteRatio: number, + ): number { + // Formula: engagement + controversy bonus + const engagement = Math.log10(Math.max(upvotes, 1)) * 12; + const commentBonus = Math.log10(Math.max(comments, 1)) * 8; + // Controversial posts (ratio ~0.5) get a boost + const controversyBonus = upvoteRatio < 0.7 ? 10 : 0; + + return Math.min( + Math.round(engagement + commentBonus + controversyBonus), + 100, + ); + } + + private mapSubredditToCategory(subreddit: string): string { + for (const [category, subs] of Object.entries(SUBREDDIT_MAP)) { + if (subs.map((s) => s.toLowerCase()).includes(subreddit.toLowerCase())) { + return category; + } + } + return 'general'; + } + + private sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/src/modules/collector/rss.service.ts b/src/modules/collector/rss.service.ts new file mode 100644 index 0000000..b3fb500 --- /dev/null +++ b/src/modules/collector/rss.service.ts @@ -0,0 +1,309 @@ +import {Injectable, Logger} from '@nestjs/common'; +import Parser from 'rss-parser'; +import {TrendItem, CollectorResult} from '../../common/interfaces/trend-item.interface'; + +interface FeedConfig { + url: string; + category: string; + name: string; + language: string; + priority: number; // 1=high, 5=low +} + +// ========================================= +// 🆓 TẤT CẢ ĐỀU FREE — chỉ cần HTTP GET +// ========================================= +const RSS_FEEDS: FeedConfig[] = [ + //japan + { + url: 'https://newsonjapan.com/rss/top.xml', + category: 'japan', + language: 'jp', + priority: 1, + name: 'newsonjapan' + }, + { + url: 'https://www.nytimes.com/svc/collections/v1/publish/http://www.nytimes.com/topic/destination/japan/rss.xml', + category: 'japan', + language: 'jp', + priority: 1, + name: 'nytimes_japan' + }, + { + url: 'https://www3.nhk.or.jp/rss/news/cat0.xml', + category: 'japan', + language: 'jp', + priority: 1, + name: 'nhk', + }, + { + url: 'https://news.yahoo.co.jp/rss/topics/top-picks.xml', + category: 'japan', + language: 'jp', + priority: 1, + name: 'yahoo', + }, + // // ── Vietnam ── + // { + // url: 'https://vnexpress.net/rss/tin-moi-nhat.rss', + // category: 'vietnam', + // name: 'VnExpress', + // language: 'vi', + // priority: 1, + // }, + // { + // url: 'https://tuoitre.vn/rss/tin-moi-nhat.rss', + // category: 'vietnam', + // name: 'Tuổi Trẻ', + // language: 'vi', + // priority: 1, + // }, + // { + // url: 'https://thanhnien.vn/rss/home.rss', + // category: 'vietnam', + // name: 'Thanh Niên', + // language: 'vi', + // priority: 2, + // }, + // ── Tech ── + { + url: 'https://techcrunch.com/feed/', + category: 'tech', + name: 'TechCrunch', + language: 'en', + priority: 1, + }, + { + url: 'https://www.theverge.com/rss/index.xml', + category: 'tech', + name: 'The Verge', + language: 'en', + priority: 1, + }, + { + url: 'https://feeds.arstechnica.com/arstechnica/index', + category: 'tech', + name: 'Ars Technica', + language: 'en', + priority: 2, + }, + { + url: 'https://www.wired.com/feed/rss', + category: 'tech', + name: 'Wired', + language: 'en', + priority: 2, + }, + // ── World News ── + { + url: 'https://feeds.bbci.co.uk/news/rss.xml', + category: 'world', + name: 'BBC News', + language: 'en', + priority: 1, + }, + { + url: 'https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml', + category: 'world', + name: 'NY Times', + language: 'en', + priority: 1, + }, + { + url: 'https://www.reuters.com/rssFeed/topNews', + category: 'world', + name: 'Reuters', + language: 'en', + priority: 1, + }, + // ── Science ── + { + url: 'https://www.sciencedaily.com/rss/all.xml', + category: 'science', + name: 'ScienceDaily', + language: 'en', + priority: 2, + }, + // ── Business ── + { + url: 'https://feeds.bloomberg.com/markets/news.rss', + category: 'business', + name: 'Bloomberg', + language: 'en', + priority: 1, + }, + // ── AI / Dev ── + { + url: 'https://blog.google/innovation-and-ai/technology/ai/rss/', + category: 'tech', + name: 'Google AI Blog', + language: 'en', + priority: 2, + }, + { + url: 'https://openai.com/news/rss.xml', + category: 'tech', + name: 'OpenAI Blog', + language: 'en', + priority: 2, + }, + //google trend rss https://trends.google.com/trending/rss?geo=JP + // { + // url: 'https://trends.google.com/trending/rss?geo=JP', + // category: 'news', + // name: 'google_trends', + // language: 'japan', + // priority: 1, + // }, + // { + // url: 'https://trends.google.com/trending/rss?geo=US', + // category: 'us', + // name: 'google_trends', + // language: 'en', + // priority: 1, + // }, + // { + // url: 'https://trends.google.com/trending/rss?geo=KR', + // category: 'korean', + // name: 'google_trends', + // language: 'en', + // priority: 1, + // }, +]; +// const RSS_FEEDS_2: FeedConfig[] = [ +// +// { +// url: 'https://trends.google.com/trending/rss?geo=US', +// category: 'us', +// name: 'google_trends', +// language: 'en', +// priority: 1, +// }, +// ]; + +@Injectable() +export class RssService { + private readonly logger = new Logger(RssService.name); + private readonly parser: Parser; + + constructor() { + this.parser = new Parser({ + timeout: 10000, + headers: { + 'User-Agent': 'TrendHunter RSS Reader/1.0', + Accept: 'application/rss+xml, application/xml, text/xml', + }, + customFields: { + item: [ + ['media:content', 'mediaContent'], + ['media:thumbnail', 'mediaThumbnail'], + ], + }, + }); + } + + async collect(categories?: string[]): Promise { + const start = Date.now(); + + // Filter feeds theo category nếu có + const feeds = categories + ? RSS_FEEDS.filter((f) => categories.includes(f.category)) + : RSS_FEEDS; + + // Parse tất cả feeds song song + const results = await Promise.allSettled( + feeds.map((feed) => this.parseFeed(feed)), + ); + + const items: TrendItem[] = []; + const errors: string[] = []; + + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + items.push(...result.value); + } else { + errors.push(`${feeds[index].name}: ${result.reason?.message}`); + this.logger.warn( + `RSS failed for ${feeds[index].name}: ${result.reason?.message}`, + ); + } + }); + + const successCount = results.filter((r) => r.status === 'fulfilled').length; + this.logger.log( + `RSS collected: ${items.length} items from ${successCount}/${feeds.length} feeds`, + ); + + return { + source: 'rss', + items, + collectedAt: new Date(), + durationMs: Date.now() - start, + error: errors.length > 0 ? errors.join('; ') : undefined, + }; + } + + private async parseFeed(feedConfig: FeedConfig): Promise { + const feed = await this.parser.parseURL(feedConfig.url); + const feedItems = feed.items || []; + // console.log('feedItems', feedItems); + //check name='google_trends' + + return feedItems.slice(0, 15).map((item, index) => { + const pubDate = item.pubDate ? new Date(item.pubDate) : new Date(); + const recencyHours = + (Date.now() - pubDate.getTime()) / (1000 * 60 * 60); + + return { + source: `rss:${feedConfig.name}`, + title: (item.title || '').trim(), + description: this.cleanHtml( + item.contentSnippet || item.content || '', + ).substring(0, 500), + url: item.link || '', + score: this.calculateScore(index, feedConfig.priority, recencyHours), + timestamp: pubDate, + category: feedConfig.category, + tags: [ + feedConfig.name, + feedConfig.language, + ...(item.categories || []).slice(0, 3), + ].filter(Boolean), + engagement: {}, + raw: { + feedName: feedConfig.name, + feedUrl: feedConfig.url, + creator: item.creator || item['dc:creator'], + guid: item.guid, + categories: item.categories, + language: feedConfig.language, + }, + } as TrendItem; + }); + } + + private calculateScore( + position: number, + priority: number, + recencyHours: number, + ): number { + // RSS không có engagement data → dùng position + recency + source priority + const positionScore = Math.max(50 - position * 4, 5); + const priorityBonus = (6 - priority) * 5; // priority 1 → +25, priority 5 → +5 + const recencyBonus = recencyHours < 2 ? 20 : recencyHours < 6 ? 10 : 0; + + return Math.min(positionScore + priorityBonus + recencyBonus, 100); + } + + private cleanHtml(html: string): string { + return html + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/\s+/g, ' ') + .trim(); + } +} diff --git a/src/modules/content-writer/comment-writer.processor.ts b/src/modules/content-writer/comment-writer.processor.ts new file mode 100644 index 0000000..ef46c4b --- /dev/null +++ b/src/modules/content-writer/comment-writer.processor.ts @@ -0,0 +1,325 @@ +import {OnWorkerEvent, Processor, WorkerHost} from "@nestjs/bullmq"; +import {Job} from "bullmq"; +import {PostCreateInput} from "../../generated/prisma/models/Post"; +import {CommentWriterService} from "./services/comment-writer.service"; +import {GenerateCommentDto} from "./dto/generate-comment.dto"; +import {XReaderService} from "../x-reader/x-reader.service"; +import {InjectBot} from "nestjs-telegraf"; +import {Context, Telegraf} from "telegraf"; +import {Logger} from "@nestjs/common"; +import {QuoteWriterService} from "./services/quote-writer.service"; +import {GenerateQuoteDto} from "./dto/generate-quote.dto"; +import {TextUtil} from "../../common/utils/text.util"; +import {isEmpty} from "lodash"; + + +@Processor('comment_writer_queue') +export class CommentWriterProcessor extends WorkerHost { + private readonly logger = new Logger(CommentWriterProcessor.name); + + constructor( + private readonly quoteWriterService: QuoteWriterService, + private readonly commentWriterService: CommentWriterService, + private readonly xreader: XReaderService, + @InjectBot() private readonly bot: Telegraf, + //@InjectQueue('comment_writer_completed_queue') private readonly aiCommentWriteCompletedQueue: Queue, + + ) { + super(); + } + + async process(job: Job, token: string | undefined): Promise { + + const { + title, + summary, + style, + url, + quoteType, + quoteText, + language, + tone, + agle, + comtext, + telegramChatId, + } = job.data; + const topic = summary || title; + let pgPostCreateDto!: PostCreateInput; + + console.log(`CommentWriterProcessor_processing_${job.name}`); + switch (job.name) { + case 'generate_comment_twitter': { + + const xpost = await this.xreader.readXPost(url); + + const dto: GenerateCommentDto = { + originalPost: xpost.text, + language + } + + // aiWriterResult={ + // comment: cleaned, + // tokensUsed: res.tokensUsed, + // model: res.model, + // language: dto.language, + // } + // const aiWriterResult = { + // comment: "Greet", + // url + // } + const aiWriterResult = await this.commentWriterService.generateComment(dto); + + this.logger.log({aiWriterResult}); + + // await this.aiCommentWriteCompletedQueue.add('generate_comment_completed', { + // //id: post.id, + // name: 'generate_comment_completed', + // needConfirm: 1, + // content: aiWriterResult.comment, + // url: url, + // }, {attempts: 1, backoff: 5000, removeOnComplete: true,}); + + await this.bot.telegram.sendMessage(telegramChatId, ` + Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}`); + // const _url = url.indexOf('?s=20') > -1 ? url : `${url}?s=20`; + // console.log({_url}); + + //tìm xem bài này có phải của tôi không + const X_USERS = process.env.TWITTER_USERNAMES!.split(','); + console.log({X_USERS}); + const isMyPost = url.indexOf(process.env.TWITTER_USERNAMES) > -1 + + //await this.bot.telegram.sendMessage(telegramChatId || adminChatId, isMyPost ? 'Đây là bài của bạn, có thế gửi' : 'Có thế không gửi được') + await this.bot.telegram.sendMessage(telegramChatId, + `${aiWriterResult.comment}`, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + ...X_USERS.map((xuser) => { + return [{ + text: `↗️X ${xuser}`, + callback_data: `publish-reply-twitter1_${xpost.tweetId}_${xuser}` + }]; + }), + [ + {text: "🗑️ Hủy bài", callback_data: `delete-reply_${xpost.tweetId}`} + ] + ] + } + }); + + // pgPostCreateDto = { + // title: aiWriterResult.topic, + // content: aiWriterResult.final, + // style: aiWriterResult.detectedStyle, + // status: 'pending', + // prompt: aiWriterResult.prompt, + // draft: aiWriterResult.draft, + // tokensUsed: aiWriterResult.tokensUsed, + // tone: aiWriterResult.detectedTone, + // model: aiWriterResult.model, + // } + break; + } + case 'generate_comment_as_text_twitter': { + const { + tweetId + } = job.data; + const dto: GenerateCommentDto = { + originalPost: comtext, + language, + angle: agle, + tone: tone + + } + const X_USERS = process.env.TWITTER_USERNAMES!.split(','); + console.log({X_USERS}); + + const aiWriterResult = await this.commentWriterService.generateComment(dto); + + this.logger.log({aiWriterResult}); + + + const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + await this.bot.telegram.sendMessage(telegramChatId || adminChatId, ` + Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model} } + `); + + await this.bot.telegram.sendMessage(telegramChatId || adminChatId, + `${aiWriterResult.comment}`, + { + reply_markup: { + inline_keyboard: [ + ...(!isEmpty(tweetId) ? X_USERS.map((xuser) => { + return [{ + text: `↗️X ${xuser}`, + callback_data: `publish-reply-twitter_${tweetId}_${xuser}` + }]; + }) : [ + // { + // text: `Reply vào bài X`, + // callback_data: `publish-reply-twitter_${tweetId}_${xuser}` + // } + ]) + , + [ + {text: "🗑️ Hủy bài", callback_data: `delete-reply_${tweetId}`} + ] + ] + } + }); + + // pgPostCreateDto = { + // title: aiWriterResult.topic, + // content: aiWriterResult.final, + // style: aiWriterResult.detectedStyle, + // status: 'pending', + // prompt: aiWriterResult.prompt, + // draft: aiWriterResult.draft, + // tokensUsed: aiWriterResult.tokensUsed, + // tone: aiWriterResult.detectedTone, + // model: aiWriterResult.model, + // } + break; + } + case 'generate_quote_twitter': { + this.logger.debug('===>generate_quote_twitter:', url); + const xpost = await this.xreader.readXPost(url); + + const originalAuthor = `${xpost.author} ${xpost.handle}`; + const dto: GenerateQuoteDto = { + originalPost: xpost.text, + originalAuthor, + language, + quoteType, + tweetId: xpost.tweetId, + } + await this.onHandleAiGenerateQuote( + dto, + false, + telegramChatId, + ); + // const aiWriterResult = await this.quoteWriterService.generateQuote(dto); + // + // this.logger.log({aiWriterResult}); + // const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + // await this.bot.telegram.sendMessage(adminChatId, ` + // Đã viết quote xong ...\nmodel: ${aiWriterResult.model} - type: ${aiWriterResult.quoteType} + // `); + // const _url = url.indexOf('?s=') > -1 ? url : `${url}?s=20`; + + //xoá url trong bài vì tốn 0,2$ cho bài có url + // let isSendSucceeded = true; + // const quoteCleanUrl = TextUtil.removeAllUrl(dto.originalPost) + // await this.bot.telegram.sendMessage( + // adminChatId, + // `${aiWriterResult.quote}\n\nQuote:"${quoteCleanUrl}\n\n${originalAuthor}"`, + // { + // // parse_mode: 'Markdown', + // reply_markup: { + // inline_keyboard: [ + // [ + // {text: "↗️X", callback_data: `publish-quote-twitter_${xpost.tweetId}`}, + // {text: "🗑️ Hủy bài", callback_data: `delete-quote_${xpost.tweetId}`} + // ], + // ] + // } + // }) + // .catch(error => { + // console.log('==> send message to telegram error:' + error.message); + // console.error(error); + // isSendSucceeded = false; + // }); + break; + } + case 'generate_quote_twitter_as_text_input': { + const dto: GenerateQuoteDto = { + originalPost: quoteText, + originalAuthor: '', + language, + quoteType + } + await this.onHandleAiGenerateQuote( + dto, + false, + telegramChatId, + ); + break; + } + } + + + return { + //id: post.id, + // content: postContent, + //image: imageSuggestion, + status: 'ready_to_post' + }; + + + } + + @OnWorkerEvent('completed') + async onCompleted(job: Job) { + console.log('CommentWriterProcessor_completed'); + const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + const postId = job.returnvalue.id; + if (postId === 0) { + const topic = job.returnvalue.topic; + + await this.bot.telegram.sendMessage(adminChatId, `Lỗi viết bài: ${topic}`); + } else { + //return job.returnvalue.topic; + } + } + + private async onHandleAiGenerateQuote( + dto: GenerateQuoteDto, + isAttachQuote = true, + telegramChatId: number = 0 + ) { + const aiWriterResult = await this.quoteWriterService.generateQuote(dto); + + this.logger.log({aiWriterResult}); + const sendId = telegramChatId || process.env.TELEGRAM_ADMIN_ID || 0; + await this.bot.telegram.sendMessage(sendId, ` + Đã viết quote xong ...\nmodel: ${aiWriterResult.model} - type: ${aiWriterResult.quoteType} + `); + + // + let finalQuote = aiWriterResult.quote; + if (isAttachQuote) { + finalQuote += `\n\nQuote:"${dto.originalPost}\n\n${dto.originalAuthor}` + } + //xoá url trong bài vì tốn 0,2$ cho bài có url + const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote) + + const X_USERS = process.env.TWITTER_USERNAMES!.split(','); + + await this.bot.telegram.sendMessage( + sendId, + finalQuoteCleanUrl, + { + // parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + ...X_USERS.map((xuser) => { + return [{ + text: `↗️X ${xuser}`, + callback_data: `publish-quote-twitter_${dto.tweetId}_${xuser}` + }]; + }), + [ + {text: "🗑️ Hủy bài", callback_data: `delete-quote_${dto.tweetId}`} + ], + ] + } + }) + .catch(error => { + console.log('==> send message to telegram error:' + error.message); + console.error(error); + }); + } + +} diff --git a/src/modules/content-writer/config/platform-limits.ts b/src/modules/content-writer/config/platform-limits.ts new file mode 100644 index 0000000..1a6e4da --- /dev/null +++ b/src/modules/content-writer/config/platform-limits.ts @@ -0,0 +1,40 @@ +// config/platform-limits.ts +import { Platform } from '../enum/platform.enum'; +import { AccountTier } from '../enum/account-tier.enum'; +import { PostLength } from '../enum/post-length.enum'; + +export interface LengthRange { + min: number; + max: number; + sweet: number; // target tối ưu cho engagement +} + +/** + * Hard limits theo platform + tier (characters) + */ +export const PLATFORM_LIMITS: Record> = { + [Platform.X]: { + [AccountTier.FREE]: 280, + [AccountTier.PREMIUM]: 25000, + [AccountTier.PREMIUM_PLUS]: 25000, + [AccountTier.VERIFIED_ORG]: 25000, + }, + [Platform.FACEBOOK]: { + [AccountTier.FREE]: 63206, + [AccountTier.PREMIUM]: 63206, + [AccountTier.PREMIUM_PLUS]: 63206, + [AccountTier.VERIFIED_ORG]: 63206, + }, +}; + +/** + * Sweet spot ranges theo PostLength. + * Dựa trên data engagement thực tế của X 2025-2026. + */ +export const LENGTH_RANGES: Record = { + [PostLength.SHORT]: { min: 180, max: 280, sweet: 210 }, + [PostLength.MEDIUM]: { min: 200, max: 500, sweet: 320 }, + [PostLength.LONG]: { min: 400, max: 1200, sweet: 600 }, + [PostLength.EXTENDED]: { min: 1500, max: 3000, sweet: 2200 }, + [PostLength.ARTICLE]: { min: 3000, max: 8000, sweet: 5000 }, +}; diff --git a/src/modules/content-writer/content-writer.controller.ts b/src/modules/content-writer/content-writer.controller.ts new file mode 100644 index 0000000..7570665 --- /dev/null +++ b/src/modules/content-writer/content-writer.controller.ts @@ -0,0 +1,40 @@ +// content-writer.controller.ts +import {Body, Controller, Get, NotFoundException, Post, Query} from '@nestjs/common'; +import {ContentWriterService} from './content-writer.service'; +import {CommentWriterService} from './services/comment-writer.service'; +import {GenerateContentDto} from './dto/generate-content.dto'; +import {GenerateCommentDto} from './dto/generate-comment.dto'; +import {IAIGrokProvider} from "./interfaces/ai-provider.interface"; + +@Controller('content-writer') +export class ContentWriterController { + constructor( + private readonly writerService: ContentWriterService, + private readonly commentService: CommentWriterService, + ) { + } + + @Get('grok/test') + async grokWrite(@Query('q') q: string) { + const provider = await this.writerService.getGrokAI() as IAIGrokProvider; + if (!q) { + throw new NotFoundException('Not Found querystring.'); + } + return provider.enrichXContext(q); + } + + @Post('generate') + generate(@Body() dto: GenerateContentDto) { + return this.writerService.generate(dto); + } + + @Post('comment') + generateComment(@Body() dto: GenerateCommentDto) { + return this.commentService.generateComment(dto); + } + + @Post('comment/variants') + generateCommentVariants(@Body() dto: GenerateCommentDto) { + // return this.commentService.generateVariants(dto, 3); + } +} diff --git a/src/modules/content-writer/content-writer.module.ts b/src/modules/content-writer/content-writer.module.ts new file mode 100644 index 0000000..4ae9357 --- /dev/null +++ b/src/modules/content-writer/content-writer.module.ts @@ -0,0 +1,57 @@ +import {Global, Module} from '@nestjs/common'; +import {AIService} from "../../shared/ai.service"; +import {ContentWriterProcessor} from "./content.writer.processor"; +import {PgPostService} from "../../shared/pg.post.service"; +import {BullModule} from "@nestjs/bullmq"; +import {PublishPageService} from "../social-api/publish.page.service"; +import {SocialModule} from "../social-api/social.module"; +import {ContentWriterController} from "./content-writer.controller"; +import {AIProviderFactory} from "./providers/ai-provider.factory"; +import {ContentWriterService} from "./content-writer.service"; +import {StyleDetectorService} from "./services/style-detector.service"; +import {PromptBuilderService} from "./services/prompt-builder.service"; +import {ReviewerService} from "./services/reviewer.service"; +import {OpenAIProvider} from "./providers/openai.provider"; +import {DeepSeekProvider} from "./providers/deepseek.provider"; +import {CommentWriterService} from "./services/comment-writer.service"; +import {GrokProvider} from "./providers/grok.provider"; +import {ProviderRouterService} from "./services/provider-router.service"; +import {CommentWriterProcessor} from "./comment-writer.processor"; +import {XReaderService} from "../x-reader/x-reader.service"; +import {LengthStrategyService} from "./services/length-strategy.service"; +import {QuoteWriterService} from "./services/quote-writer.service"; +import {SqsPostService} from "../sqs-module/sqs.post.service"; + +@Module({ + imports: [ + BullModule.registerQueue( + {name: 'content_writer_completed_queue'},// Hàng đợi cho AI-C + ), + SocialModule + ], + controllers: [ContentWriterController], + providers: [ + AIService, + PgPostService, + ContentWriterProcessor, + ContentWriterService, + CommentWriterService, + StyleDetectorService, + PromptBuilderService, + ReviewerService, + OpenAIProvider, + DeepSeekProvider, + GrokProvider, + AIProviderFactory, + ProviderRouterService, + CommentWriterProcessor, + XReaderService, + LengthStrategyService, + QuoteWriterService, + SqsPostService +], + exports: [GrokProvider, ContentWriterService], + +}) +export class ContentWriterModule { +} diff --git a/src/modules/content-writer/content-writer.service.ts b/src/modules/content-writer/content-writer.service.ts new file mode 100644 index 0000000..0a13d00 --- /dev/null +++ b/src/modules/content-writer/content-writer.service.ts @@ -0,0 +1,167 @@ +// content-writer.service.ts +import {BadRequestException, Injectable, Logger} from '@nestjs/common'; +import {GenerateContentDto} from './dto/generate-content.dto'; +import {ContentResponseDto} from './dto/content-response.dto'; +import {StyleDetectorService} from './services/style-detector.service'; +import {PromptBuilderService} from './services/prompt-builder.service'; +import {ReviewerService} from './services/reviewer.service'; +import {AIProviderFactory, ProviderName} from './providers/ai-provider.factory'; +import {Platform} from "./enum/platform.enum"; +import {ProviderRouterService} from "./services/provider-router.service"; +import {Language} from "../../common/interfaces/language.prompt.interface"; +import {WriterPromptParams} from "./interfaces/writer-prompt-params.interface"; +import {AccountTier} from "./enum/account-tier.enum"; +import {ConfigService} from "@nestjs/config"; +import {LengthStrategyService} from "./services/length-strategy.service"; +import {calculateTokenBudget} from "../../common/utils/token-calculator"; + +@Injectable() +export class ContentWriterService { + private readonly logger = new Logger(ContentWriterService.name); + + constructor( + private detector: StyleDetectorService, + private promptBuilder: PromptBuilderService, + private reviewer: ReviewerService, + private factory: AIProviderFactory, + private router: ProviderRouterService, + private lengthStrategy: LengthStrategyService, // 👈 mới + private configService: ConfigService, + ) { + } + + async getGrokAI() { + return this.factory.get('grok'); + } + + async generate( + dto: GenerateContentDto, + isForceManualProvider: boolean = false, + writerProvider: ProviderName = 'openai', + reviewerProvider: ProviderName = 'deepseek', + ): Promise { + // 1. Detect style/tone nếu user không truyền (0 token) + const style = dto.style ?? this.detector.detectStyle(dto.topic); + const tone = dto.tone ?? this.detector.detectTone(dto.topic); + const platform = dto.platform ? dto.platform : Platform.X; + const language = (dto.language ?? 'en') as Language; + + // Default tier từ env nếu user không pass + const tier = dto.accountTier ?? this.configService.get( + 'X_ACCOUNT_TIER', + AccountTier.PREMIUM, + ); + + // 📏 Decide length strategy + const lengthDecision = this.lengthStrategy.decide({ + platform: dto.platform || Platform.X, + tier, + style, + tone, + requestedLength: dto.postLength, + }); + this.logger.debug(`>>> style:${style} - tone:${tone} - pf:${platform} -lang:${language} - tier:${tier}`); + this.logger.log(`Length: ${lengthDecision.reason}`); + + // 💰 Token budget + const budget = calculateTokenBudget(lengthDecision.range, language); + this.logger.log(`Budget: ${budget.minChars}-${budget.maxChars} chars, ${budget.maxTokens} tokens`); + + // 🧭 Smart routing + const decision = this.router.route({ + language, + contentType: 'post', + style, + tone, + }); + this.logger.log(`ContentWriterService => Routing: ${decision.reason} - writer:${decision.writer} - reviewer:${decision.reviewer}`); + + const ctx: WriterPromptParams = { + topic: dto.topic, + platform, + style, + tone, + language, + extraInstructions: dto.extraInstructions, + postLength: lengthDecision.postLength, // 👈 pass xuống + lengthRange: lengthDecision.range, + }; + + // 🌐 Optional: Grok enriches X context (chỉ dùng cho EN breaking) + let enrichedTopic = dto.topic; + let enrichmentTokens = 0; + if (dto.useXEnrichment || decision.useXEnrichment) { + this.logger.log(`==> Prepare X-AI enrich topic: ${enrichedTopic}`); + try { + const grok = this.factory.getGrok(); + const xContext = await grok.enrichXContext(dto.topic); + enrichedTopic = `${dto.topic}\n\n[X Context]:\n${xContext}`; + this.logger.log(`===> X enrichment: ${xContext}`); + } catch (e) { + this.logger.warn('===> X enrichment failed, proceeding without', e); + } + } + + // ✍️ Writer + console.log('==> ContentWriterService_write: '); + + const provider = this.factory.get(isForceManualProvider ? writerProvider : decision.writer); + const messages = this.promptBuilder.buildWriterMessages({ + ...ctx, + topic: enrichedTopic + }); + // console.debug('prompt message:==>', messages); + const draft = await provider.complete(messages, { + temperature: 0.75, + maxTokens: budget.maxTokens, + }); + this.logger.debug(`===> ${draft.model} đã viết xong!`); + + let final = draft.content; + let totalTokens = draft.tokensUsed + enrichmentTokens; + let reviewNotes: string | undefined; + let modelUsed = draft.model; + + // 🔍 Reviewer + if (dto.enableReview) { + this.logger.debug(`===> chuẩn bị review`); + try { + const reviewed = await this.reviewer.review( + draft.content, + ctx, + isForceManualProvider ? reviewerProvider : decision.reviewer, + dto.topic, + Math.ceil(budget.maxTokens * 1.3), + ); + final = reviewed.improved; + reviewNotes = reviewed.notes; + totalTokens += reviewed.tokensUsed; + modelUsed = `${draft.model} + ${reviewed.model}`; + this.logger.debug(`===> ${reviewed.model} đã review xong!`); + } catch (err) { + this.logger.error('Review failed, fallback to draft', err); + } + } + + // 🛡️ Hard cap check + if (final.length > lengthDecision.hardLimit) { + this.logger.warn(`==> Output exceeds hard limit (${final.length} > ${lengthDecision.hardLimit})`); + // final = final.substring(0, lengthDecision.hardLimit); + } + + // if ([ContentStyle.FINANCE, ContentStyle.CRYPTO].includes(ctx.style)) { + // final += `\n ⚠️ This content is for informational purposes only, not financial advice. DYOR. \n` + // } + return { + topic: dto.topic, + final, + draft: dto.enableReview ? draft.content : undefined, + reviewNotes, + detectedStyle: style, + detectedTone: tone, + tokensUsed: totalTokens, + model: modelUsed, + prompt: JSON.stringify(messages), + }; + } +} diff --git a/src/modules/content-writer/content.writer.processor.ts b/src/modules/content-writer/content.writer.processor.ts new file mode 100644 index 0000000..a288b05 --- /dev/null +++ b/src/modules/content-writer/content.writer.processor.ts @@ -0,0 +1,211 @@ +// src/modules/writer/facebook.processor.ts +import {InjectQueue, OnWorkerEvent, Processor, WorkerHost} from '@nestjs/bullmq'; +import {Job, Queue} from 'bullmq'; +import {AIService} from '../../shared/ai.service'; +import {PgPostService} from "../../shared/pg.post.service"; +import {isEmpty} from "lodash"; +import {ContentWriterService} from "./content-writer.service"; +import {GenerateContentDto} from "./dto/generate-content.dto"; +import {PostCreateInput} from "../../generated/prisma/models/Post"; +import {InjectBot} from "nestjs-telegraf"; +import {Context, Telegraf} from "telegraf"; +import {StyleDetectorService} from "./services/style-detector.service"; +import {ContentStyle} from "./enum/style.enum"; +import {ContentTone} from "./enum/tone.enum"; +import {XStrategy} from "../social-api/x-router.service"; +import {SqsPostService} from "../sqs-module/sqs.post.service"; + +@Processor('content_writer_queue') +export class ContentWriterProcessor extends WorkerHost { + constructor( + private aiService: AIService, + private readonly writerService: ContentWriterService, + private readonly styleDetectorService: StyleDetectorService, + private readonly pgPostService: PgPostService, + @InjectBot() private readonly bot: Telegraf, + // private readonly managerService: ManagerService, + @InjectQueue('content_writer_completed_queue') private readonly fbContentCompletedQueue: Queue, + private readonly sqsPostService: SqsPostService, + ) { + super(); + } + + async process(job: Job): Promise { + const {title, summary, style, language, tone, enableReview, postLength, autoPublish, telegramChatId} = job.data; + const topic = summary || title; + let pgPostCreateDto!: PostCreateInput; + + console.log(`ContentWriterProcessor_processing_${job.name}`); + let isAutoPublish = false; + switch (job.name) { + case 'generate_post_ver2': { + const dto: GenerateContentDto = { + topic, + enableReview, + language, + tone, + postLength + } + const aiWriterResult = await this.writerService.generate(dto, false, 'openai', 'deepseek'); + // console.log({aiWriterResult}); + pgPostCreateDto = { + title: aiWriterResult.topic, + content: aiWriterResult.final, + style: aiWriterResult.detectedStyle, + status: 'pending', + prompt: aiWriterResult.prompt, + draft: aiWriterResult.draft, + tokensUsed: aiWriterResult.tokensUsed, + tone: aiWriterResult.detectedTone, + model: aiWriterResult.model, + } + break; + } + case 'generate_post_ver1': { + const aiWriterResult = await this.aiService.generateContentViaDeepseek( + summary || title, + style, + language + ); + + pgPostCreateDto = { + title: aiWriterResult.topic, + content: aiWriterResult.final, + style: aiWriterResult.detectedStyle, + status: 'pending', + prompt: aiWriterResult.prompt, + tone: aiWriterResult.detectedTone, + model: aiWriterResult.model, + } + break; + } + + case 'generate_post_telegram': { + isAutoPublish = true; + const topicLen = topic.length; + console.log({topicLen}); + const dto: GenerateContentDto = { + topic, + enableReview, + language: this.styleDetectorService.detectLanguageFromTelegramAutoContent(topic), + tone: topicLen < 150 ? ContentTone.URGENT : tone, + postLength, + style: topicLen < 150 ? ContentStyle.BREAKING_NEWS : style + } + const aiWriterResult = await this.writerService.generate(dto, false); + // console.log({aiWriterResult}); + pgPostCreateDto = { + title: aiWriterResult.topic, + content: aiWriterResult.final, + style: aiWriterResult.detectedStyle, + status: 'pending', + prompt: aiWriterResult.prompt, + draft: aiWriterResult.draft, + tokensUsed: aiWriterResult.tokensUsed, + tone: aiWriterResult.detectedTone, + model: aiWriterResult.model, + } + break; + } + case 'generate_post_telegram_batch': { + isAutoPublish = true; + // return ; + const {messages} = job.data; + let compose_topic = `Viết 1 thread Twitter/X ngắn gọn tổng hợp ${messages.length} tin sau:\n` + compose_topic += messages.map(m => '- ' + m.text).join('\n'); + const topicLen = compose_topic.length; + console.log({compose_topic}); + const dto: GenerateContentDto = { + topic: compose_topic, + enableReview: false, + language: this.styleDetectorService.detectLanguageFromTelegramAutoContent(compose_topic), + tone: ContentTone.CASUAL, + postLength, + style: ContentStyle.BREAKING_NEWS + } + const aiWriterResult = await this.writerService.generate(dto, false); + // console.log({aiWriterResult}); + pgPostCreateDto = { + title: aiWriterResult.topic, + content: aiWriterResult.final, + style: aiWriterResult.detectedStyle, + status: 'pending', + prompt: aiWriterResult.prompt, + draft: aiWriterResult.draft, + tokensUsed: aiWriterResult.tokensUsed, + tone: aiWriterResult.detectedTone, + model: aiWriterResult.model, + } + break; + } + default: { + break; + } + } + + console.log({pgPostCreateDto}); + + if (isEmpty(pgPostCreateDto)) { + return { + id: 0, + topic, + 'status': 'error', + } + } + + // + // 2. Giả lập việc tạo ảnh hoặc tìm ảnh minh họa (có thể tích hợp API sau) + // const imageSuggestion = `https://image-service.com/search?q=${keywords[0]}`; + // + + // let finalContent = aiWriterResult.content; + const post = await this.pgPostService.createPost(pgPostCreateDto); + if (!isAutoPublish) { + await this.fbContentCompletedQueue.add('generate_post_completed', { + id: post.id, + name: 'generate_post_completed', + needConfirm: 1, + content: pgPostCreateDto.content, + autoPublish: false, + telegramChatId, + xSubmitProvider: post.id % 2 === 0 ? XStrategy.BROWSER_ONLY : XStrategy.API_ONLY, //cứ 3post api, có 1 post browser + + }, {attempts: 1, backoff: 5000, removeOnComplete: true,}); + } else { + await this.sqsPostService.postFlashKaze({ + id: post.id, + name: 'generate_post_completed', + type: 'X_POSTER_TWEET', + needConfirm: 1, + content: pgPostCreateDto.content, + autoPublish, + telegramChatId, + publishTo: ['x', 'fb'], + xSubmitProvider: post.id % 3 === 0 ? XStrategy.API_FIRST : XStrategy.BROWSER_ONLY, //cứ 3post api, có 1 post browser + }) + } + return { + id: post.id, + // content: postContent, + //image: imageSuggestion, + status: 'ready_to_post', + telegramChatId, + }; + } + + @OnWorkerEvent('completed') + async onCompleted(job: Job) { + console.log('ContentWriterProcessor_completed'); + const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + const postId = job.returnvalue.id; + const telegramChatId = job.returnvalue.telegramChatId; + if (postId === 0) { + const topic = job.returnvalue.topic; + + await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `Lỗi viết bài: ${topic}`); + } else { + //return job.returnvalue.topic; + } + } + +} diff --git a/src/modules/content-writer/dto/content-response.dto.ts b/src/modules/content-writer/dto/content-response.dto.ts new file mode 100644 index 0000000..3841811 --- /dev/null +++ b/src/modules/content-writer/dto/content-response.dto.ts @@ -0,0 +1,12 @@ +// dto/content-response.dto.ts +export class ContentResponseDto { + topic: string; + final: string; + draft?: string; + reviewNotes?: string; + detectedStyle: string; + detectedTone: string; + tokensUsed: number; + model: string; + prompt: string; +} diff --git a/src/modules/content-writer/dto/generate-comment.dto.ts b/src/modules/content-writer/dto/generate-comment.dto.ts new file mode 100644 index 0000000..5e34e38 --- /dev/null +++ b/src/modules/content-writer/dto/generate-comment.dto.ts @@ -0,0 +1,25 @@ +// dto/generate-comment.dto.ts +import { IsEnum, IsString, IsOptional, MaxLength } from 'class-validator'; +import { ContentTone } from '../enum/tone.enum'; +import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface"; + +export class GenerateCommentDto { + @IsString() + @MaxLength(3000) + originalPost: string; // nội dung bài X gốc + + @IsOptional() + @IsString() + angle?: string; // góc nhìn muốn comment: "agree", "challenge", "add-info", "funny" + + @IsString() + language: languagePromptInterface.Language; + + @IsOptional() + @IsEnum(ContentTone) + tone?: ContentTone; + + @IsOptional() + @IsString() + persona?: string; // "crypto trader", "news analyst"... +} diff --git a/src/modules/content-writer/dto/generate-content.dto.ts b/src/modules/content-writer/dto/generate-content.dto.ts new file mode 100644 index 0000000..eef5feb --- /dev/null +++ b/src/modules/content-writer/dto/generate-content.dto.ts @@ -0,0 +1,48 @@ +// dto/generate-content.dto.ts +import { IsEnum, IsOptional, IsString, IsBoolean, MaxLength } from 'class-validator'; +import {ContentStyle} from "../enum/style.enum"; +import {Platform} from "../enum/platform.enum"; +import {ContentTone} from "../enum/tone.enum"; +import {AccountTier} from "../enum/account-tier.enum"; +import {PostLength} from "../enum/post-length.enum"; + +export class GenerateContentDto { + @IsString() + @MaxLength(5000) + topic: string; // chủ đề / input thô từ user + + @IsEnum(Platform) + platform?: Platform; + + @IsOptional() + @IsEnum(AccountTier) + accountTier?: AccountTier; // 👈 mới + + @IsOptional() + @IsEnum(PostLength) + postLength?: PostLength; // 👈 user override length + + @IsOptional() + @IsEnum(ContentStyle) + style?: ContentStyle; // nếu không truyền -> auto-detect + + @IsOptional() + @IsEnum(ContentTone) + tone?: ContentTone; + + @IsOptional() + @IsString() + language?: string; // 'vi' | 'en' ... default 'en' + + @IsOptional() + @IsBoolean() + enableReview?: boolean; // bật AI reviewer + + @IsOptional() + @IsBoolean() + useXEnrichment?: boolean; // bật X Enrichment reviewer + + @IsOptional() + @IsString() + extraInstructions?: string; +} diff --git a/src/modules/content-writer/dto/generate-quote.dto.ts b/src/modules/content-writer/dto/generate-quote.dto.ts new file mode 100644 index 0000000..2cd91f7 --- /dev/null +++ b/src/modules/content-writer/dto/generate-quote.dto.ts @@ -0,0 +1,50 @@ +// dto/generate-quote.dto.ts +import { IsEnum, IsString, IsOptional, MaxLength, IsBoolean } from 'class-validator'; +import { QuoteType } from '../enum/quote-type.enum'; +import { ContentTone } from '../enum/tone.enum'; +import { PostLength } from '../enum/post-length.enum'; +import { AccountTier } from '../enum/account-tier.enum'; + +export class GenerateQuoteDto { + @IsString() + @MaxLength(5000) + originalPost: string; // Tweet gốc bạn muốn quote + + @IsOptional() + @IsString() + originalAuthor?: string; // username của OP (optional, giúp AI biết tone) + + @IsOptional() + @IsEnum(QuoteType) + quoteType?: QuoteType; // Nếu không truyền -> AI tự chọn best fit + + @IsString() + language: 'en' | 'vi' | 'ja' | 'ko'; + + @IsOptional() + @IsEnum(ContentTone) + tone?: ContentTone; + + @IsOptional() + @IsEnum(PostLength) + postLength?: PostLength; // short/medium/long (Premium) + + @IsOptional() + @IsEnum(AccountTier) + accountTier?: AccountTier; + + @IsOptional() + @IsString() + persona?: string; // "crypto analyst", "tech journalist"... + + @IsOptional() + @IsString() + yourAngle?: string; // Góc nhìn riêng của bạn muốn express + + @IsOptional() + @IsBoolean() + enableReview?: boolean; + + @IsOptional() + tweetId?: number; +} diff --git a/src/modules/content-writer/enum/account-tier.enum.ts b/src/modules/content-writer/enum/account-tier.enum.ts new file mode 100644 index 0000000..624c870 --- /dev/null +++ b/src/modules/content-writer/enum/account-tier.enum.ts @@ -0,0 +1,6 @@ +export enum AccountTier { + FREE = 'free', + PREMIUM = 'premium', // $8/month + PREMIUM_PLUS = 'premium_plus', // $16/month + VERIFIED_ORG = 'verified_org', +} diff --git a/src/modules/content-writer/enum/angle.enum.ts b/src/modules/content-writer/enum/angle.enum.ts new file mode 100644 index 0000000..d910d8a --- /dev/null +++ b/src/modules/content-writer/enum/angle.enum.ts @@ -0,0 +1,41 @@ +export enum AngleEnum { + AGREE = 'agree', + CHALLENGE = 'challenge', + ADD_INFO = 'add_info', + FUNNY = 'funny', + QUESTION = 'question', + RELATE = 'relate', + DEVIL_ADVOCATE = 'devil_advocate', + EXPAND = 'expand', + VALIDATE = 'validate', + CTA = 'cta' +} + +export const ANGLE_HINTS: Record = { + [AngleEnum.AGREE]: 'I agree and would like to add a small point to support my argument.', + [AngleEnum.CHALLENGE]: 'Disagree or add further nuance', + [AngleEnum.ADD_INFO]: 'additional useful related information', + [AngleEnum.FUNNY]: 'Witty, mildly humorous, and not offensive.', + [AngleEnum.QUESTION]: 'Ask a smart follow-up question', + [AngleEnum.RELATE]: 'Share a personal experience or feeling that mirrors the original post', + [AngleEnum.DEVIL_ADVOCATE]: `Play devil's advocate. Present the opposite view fairly without being hostile`, + [AngleEnum.EXPAND]: 'Take one point from the post and zoom in deeper with more nuance', + [AngleEnum.VALIDATE]: `Affirm the post's point with evidence or strong agreement, boost credibility`, + [AngleEnum.CTA]: 'End with a soft call-to-action: ask others to share their view' +} +export const ANGLE_HINTS_TELEGRAM_BUTTON: Record = { + [AngleEnum.AGREE]: {text: 'Đồng ý'}, + [AngleEnum.CHALLENGE]: {text: 'Không đồng ý'}, + [AngleEnum.ADD_INFO]: {text: 'thêm thông tin liên quan hữu ích'}, + [AngleEnum.FUNNY]: {text: 'Hóm hỉnh, hài hước nhẹ nhàng, không gây khó chịu'}, + [AngleEnum.QUESTION]: {text: 'Đặt một câu hỏi tiếp theo thông minh'}, + [AngleEnum.RELATE]: {text: 'Chia sẻ một trải nghiệm \n hoặc cảm xúc cá nhân tương tự như bài đăng gốc.'}, + [AngleEnum.DEVIL_ADVOCATE]: { + text: `Hãy đóng vai trò người phản biện. \n Trình bày quan điểm trái chiều một cách công bằng mà không tỏ ra thù địch.` + }, + [AngleEnum.EXPAND]: {text: 'expand-Chọn một điểm từ bài viết và phân tích sâu hơn với nhiều sắc thái khác nhau.'}, + [AngleEnum.VALIDATE]: { + text: `validate-Khẳng định luận điểm của bài đăng bằng bằng chứng hoặc sự đồng tình mạnh mẽ, tăng cường độ tin cậy.` + }, + [AngleEnum.CTA]: {text: 'cta-Kết thúc bằng lời kêu gọi hành động nhẹ nhàng'} +} \ No newline at end of file diff --git a/src/modules/content-writer/enum/platform.enum.ts b/src/modules/content-writer/enum/platform.enum.ts new file mode 100644 index 0000000..e4e1393 --- /dev/null +++ b/src/modules/content-writer/enum/platform.enum.ts @@ -0,0 +1,6 @@ + +// enums/platform.enum.ts +export enum Platform { + X = 'x', + FACEBOOK = 'facebook', +} diff --git a/src/modules/content-writer/enum/post-length.enum.ts b/src/modules/content-writer/enum/post-length.enum.ts new file mode 100644 index 0000000..7829b13 --- /dev/null +++ b/src/modules/content-writer/enum/post-length.enum.ts @@ -0,0 +1,7 @@ +export enum PostLength { + SHORT = 'short', // 200-280 chars - viral/breaking + MEDIUM = 'medium', // 280-500 chars - hot take + LONG = 'long', // 400-1200 chars - analysis (Premium sweet spot) + EXTENDED = 'extended', // 1500-3000 chars - deep dive + ARTICLE = 'article', // 3000-10000 chars - full essay +} diff --git a/src/modules/content-writer/enum/quote-type.enum.ts b/src/modules/content-writer/enum/quote-type.enum.ts new file mode 100644 index 0000000..e81e67c --- /dev/null +++ b/src/modules/content-writer/enum/quote-type.enum.ts @@ -0,0 +1,15 @@ +// enums/quote-type.enum.ts +export enum QuoteType { + AGREE_AMPLIFY = 'agree_amplify', // Đồng ý + thêm insight + DISAGREE = 'disagree', // Phản biện có lý + ADD_CONTEXT = 'add_context', // Bổ sung context + REFRAME = 'reframe', // Nhìn góc khác + BUILD_ON = 'build_on', // Mở rộng ý + HIGHLIGHT = 'highlight', // Nhấn mạnh key point + ROAST = 'roast', // Chỉ trích sắc + HOT_TAKE = 'hot_take', // Opinion mạnh + QUESTION = 'question', // Đặt câu hỏi + SUMMARIZE = 'summarize', // TL;DR + PERSONAL_STORY = 'personal_story', // TL;DR + CONNECT_DOTS = 'connect_dot', // TL;DR +} diff --git a/src/modules/content-writer/enum/style.enum.ts b/src/modules/content-writer/enum/style.enum.ts new file mode 100644 index 0000000..8f9cbaf --- /dev/null +++ b/src/modules/content-writer/enum/style.enum.ts @@ -0,0 +1,15 @@ +// enums/style.enum.ts +export enum ContentStyle { + CRYPTO = 'crypto', + BREAKING_NEWS = 'breaking_news', + TECH = 'tech', + FINANCE = 'finance', + LIFESTYLE = 'lifestyle', + MEME = 'meme', + EDUCATIONAL = 'educational', + GENERAL = 'general', + OPINION = 'opinion', + STORYTELLING = 'storytelling', + THREAD = 'thread', +} + diff --git a/src/modules/content-writer/enum/tone.enum.ts b/src/modules/content-writer/enum/tone.enum.ts new file mode 100644 index 0000000..ca36e48 --- /dev/null +++ b/src/modules/content-writer/enum/tone.enum.ts @@ -0,0 +1,11 @@ +export enum ContentTone { + PROFESSIONAL = 'professional', + CASUAL = 'casual', + HYPE = 'hype', + URGENT = 'urgent', + HUMOROUS = 'humorous', + INFORMATIVE = 'informative', + EMPATHETIC = 'empathetic', + PROVOCATIVE = 'provocative', + AUTHORITATIVE = 'authoritative', +} diff --git a/src/modules/content-writer/interfaces/ai-provider.interface.ts b/src/modules/content-writer/interfaces/ai-provider.interface.ts new file mode 100644 index 0000000..de18431 --- /dev/null +++ b/src/modules/content-writer/interfaces/ai-provider.interface.ts @@ -0,0 +1,27 @@ +// interfaces/ai-provider.interface.ts +import {Context} from "telegraf"; + +export interface AIMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} + +export interface AICompletionOptions { + model?: string; + temperature?: number; + maxTokens?: number; +} + +export interface AICompletionResult { + content: string; + tokensUsed: number; + model: string; +} + +export interface IAIProvider { + readonly name: string; + complete(messages: AIMessage[], options?: AICompletionOptions): Promise; +} +export interface IAIGrokProvider extends IAIProvider { + enrichXContext(topic: string): Promise +} diff --git a/src/modules/content-writer/interfaces/content-context.interface.ts b/src/modules/content-writer/interfaces/content-context.interface.ts new file mode 100644 index 0000000..c544f6f --- /dev/null +++ b/src/modules/content-writer/interfaces/content-context.interface.ts @@ -0,0 +1,15 @@ +// interfaces/content-context.interface.ts + +import {Platform} from "../enum/platform.enum"; +import {ContentStyle} from "../enum/style.enum"; +import {ContentTone} from "../enum/tone.enum"; +import {Language} from "../../../common/interfaces/language.prompt.interface"; + +export interface ContentContext { + topic: string; + platform: Platform; + style: ContentStyle; + tone: ContentTone; + language: Language; + extraInstructions?: string; +} diff --git a/src/modules/content-writer/interfaces/writer-prompt-params.interface.ts b/src/modules/content-writer/interfaces/writer-prompt-params.interface.ts new file mode 100644 index 0000000..3f9db67 --- /dev/null +++ b/src/modules/content-writer/interfaces/writer-prompt-params.interface.ts @@ -0,0 +1,18 @@ +import {Platform} from "../enum/platform.enum"; +import {ContentStyle} from "../enum/style.enum"; +import {ContentTone} from "../enum/tone.enum"; +import {Language} from "../../../common/interfaces/language.prompt.interface"; +import {LengthRange} from "../config/platform-limits"; +import {PostLength} from "../enum/post-length.enum"; + + +export interface WriterPromptParams { + topic: string; + platform: Platform; + style: ContentStyle; + tone: ContentTone; + language: Language; + postLength: PostLength; // 👈 mới + lengthRange: LengthRange; + extraInstructions?: string; +} diff --git a/src/modules/content-writer/prompts/breaking-news.templates.ts b/src/modules/content-writer/prompts/breaking-news.templates.ts new file mode 100644 index 0000000..7e732dd --- /dev/null +++ b/src/modules/content-writer/prompts/breaking-news.templates.ts @@ -0,0 +1,5 @@ +// prompts/breaking-news.templates.ts + +// ============================================================ +// BREAKING NEWS — native templates per language +// ============================================================ diff --git a/src/modules/content-writer/prompts/comment.templates.ts b/src/modules/content-writer/prompts/comment.templates.ts new file mode 100644 index 0000000..36e4f9d --- /dev/null +++ b/src/modules/content-writer/prompts/comment.templates.ts @@ -0,0 +1,58 @@ +// prompts/comment.templates.ts + +import {Language} from "../../../common/interfaces/language.prompt.interface"; +import {calculateLengthBudget} from "../../../common/utils/token-calculator"; +import {Platform} from "../enum/platform.enum"; +import {ANGLE_HINTS} from "../enum/angle.enum"; + +export const COMMENT_SYSTEM_PROMPTS = { + en: 'You write casual, human replies on X. Sound like a real person, NOT AI. No hashtags unless natural.', + vi: 'Bạn viết reply tự nhiên trên X như người thật. Không giống AI. Không hashtag nếu không cần.', + vn: 'Bạn viết reply tự nhiên trên X như người thật. Không giống AI. Không hashtag nếu không cần.', + cn: '像真实用户一样在X上自然地回复,不要显得像AI生成的。除非必要,否则不要使用话题标签(#)。', + ja: 'Xで人間らしい返信を書きます。AI臭くない。自然な口語。ハッシュタグは不要な限り使わない。', + jp: 'Xで人間らしい返信を書きます。AI臭くない。自然な口語。ハッシュタグは不要な限り使わない。', + ko: 'X에서 사람처럼 자연스럽게 답글을 씁니다. AI 같지 않게. 해시태그는 필요한 경우만.', + kr: 'X에서 사람처럼 자연스럽게 답글을 씁니다. AI 같지 않게. 해시태그는 필요한 경우만.', +}; + +// export const COMMENT_AGLE_TELEGRAM_BUTTONS = { +// agree: {text: 'Đồng ý và bổ sung thêm một điểm hỗ trợ nhỏ.'}, +// challenge: {text: 'Lịch sự bày tỏ sự KHÔNG ĐỒNG Ý hoặc bổ sung thêm sắc thái.'}, +// 'add-info': {text: 'Thêm thông tin liên quan hữu ích'}, +// funny: {text: 'Hài hước dí dỏm, nhẹ nhàng, không gây khó chịu.'}, +// question: {text: 'Hãy đặt một câu hỏi tiếp theo thông minh.'}, +// } + +export function buildCommentPrompt(params: { + originalPost: string; + angle?: string; + language: Language; + persona?: string; + tone?: string; +}): { system: string; user: string } { + // const angleHints: Record = { + // agree: 'agree:Đồng ý và bổ sung thêm một luận điểm nhỏ để hỗ trợ', + // challenge: 'challenge:Không đồng ý hoặc bổ sung thêm sắc thái', + // 'add-info': 'add-info:Thêm thông tin liên quan hữu ích', + // funny: 'funny:Hóm hỉnh, hài hước nhẹ nhàng, không gây khó chịu', + // question: 'question:Đặt một câu hỏi tiếp theo thông minh', + // }; + const budget = calculateLengthBudget(Platform.X, params.language); + + const user = [ + `Original X post:\n"""${params.originalPost}"""`, + ``, + `Write a reply target length: ${budget.minChars}-${budget.maxChars} characters:`, + `[Target Language: ${params.language}] + Rewrite strictly in ${params.language} only.`, + params.angle ? `- Angle: ${ANGLE_HINTS[params.angle] ?? params.angle}` : '- Angle: natural reaction', + params.persona ? `- Speak as: ${params.persona}` : '', + params.tone ? `- Tone: ${params.tone}` : '- Tone: casual, conversational', + `- Sound HUMAN, not AI. No "Great post!" openings.`, + `- No emoji spam. 0-1 emoji max.`, + `- Output ONLY the reply text.`, + ].filter(Boolean).join('\n'); + + return {system: COMMENT_SYSTEM_PROMPTS[params.language], user}; +} diff --git a/src/modules/content-writer/prompts/edgy-tones.ts b/src/modules/content-writer/prompts/edgy-tones.ts new file mode 100644 index 0000000..70d79c5 --- /dev/null +++ b/src/modules/content-writer/prompts/edgy-tones.ts @@ -0,0 +1,205 @@ +// // prompts/edgy-tones.ts — UPDATE phần JP +// +// import {ContentTone} from "../enum/tone.enum"; +// +// export const EDGY_TONE_SPECS: Record = { +// [ContentTone.SPICY]: { +// intensity: 2, +// description: { +// en: '...', +// vi: '...', +// // 👇 REFINED JP +// ja: [ +// 'ストレートで歯に衣着せない。鋭いが冷静。', +// '軽い悪態OK:「は?」「いや無理」「マジで?」「草」', +// 'JPのX的に:堅い敬語を使わず、話し言葉ベース。改行を効かせる。', +// '「w」「草」を末尾に使ってOK(やりすぎ注意、1-2回まで)。', +// '感情的にキレるのではなく、淡々と切るイメージ。', +// ].join(' '), +// ko: '...', +// }, +// examples: { +// en: ['...'], +// vi: ['...'], +// // 👇 REFINED JP — real JP X patterns +// ja: [ +// 'いやこれ違うやろ\n\nデータちゃんと見た?普通に逆の話してるんやが', +// 'は?このチャートで強気とか草\n\nさすがに無理があるって', +// 'まあ気持ちは分かるけど、これは普通にナンピン地獄コースやで', +// ], +// ko: ['...'], +// }, +// vocabulary: { +// en: '...', +// vi: '...', +// // 👇 REFINED JP +// ja: 'は?/いや/マジで/普通に/ガチで/草/w/無理/えぐい/やばい', +// ko: '...', +// }, +// avoid: { +// en: '...', +// vi: '...', +// ja: [ +// '丁寧な敬語禁止(「〜と思います」「〜でしょうか」NG)', +// '個人攻撃禁止(一般人ターゲットNG)', +// '差別語・脅迫NG', +// '「!」連発禁止', +// ].join(' / '), +// ko: '...', +// }, +// }, +// +// [ContentTone.AGGRESSIVE]: { +// intensity: 3, +// description: { +// en: '...', +// vi: '...', +// // 👇 REFINED JP +// ja: [ +// '粗野で生々しい、主張をガチで叩く。バカな意見を嘲笑する。', +// '強めの悪態OK:「クソ」「ふざけんな」「アホか」「は?マジで言ってる?」', +// '関西弁ミックスもOK(「なんやねん」「あほちゃう」「言うてるやろ」)— ナチュラルなら。', +// '感情的になりすぎず、論破口調をベースに。', +// '攻撃対象:主張・意見・市場・公人の公的行為。私人NG。', +// ].join(' '), +// ko: '...', +// }, +// examples: { +// en: ['...'], +// vi: ['...'], +// ja: [ +// 'は?マジで言ってる?\n\nそのロジックでよく投稿ボタン押せたな、感心するわ', +// 'いや待って、これ今週見た中で一番アホな分析やんけ\n\n根拠ゼロで断言するの草', +// 'ふざけんなよ、3日前に同じこと言ったやろ。\n誰も聞かんかったから今こうなってるんやで', +// ], +// ko: ['...'], +// }, +// vocabulary: { +// en: '...', +// vi: '...', +// ja: 'クソ/ふざけんな/アホか/は?/マジで/いい加減にしろ/寝言/なんやねん/養分/ピエロ', +// ko: '...', +// }, +// avoid: { +// en: '...', +// vi: '...', +// ja: '差別語NG/脅迫NG/私人攻撃NG/本物の侮辱罪リスク回避(公的主張のみ叩く)', +// ko: '...', +// }, +// }, +// +// [ContentTone.PROFANE]: { +// intensity: 4, +// description: { +// en: '...', +// vi: '...', +// ja: [ +// '荒っぽい、フィルター無し、悪態多め。熱くなったトレーダー・配信者風。', +// '激しい悪態OK:「クソが」「マジでクソ」「ふざけんなクソ」「は?クソが」', +// '感情がガチで出てる感じ。ただし支離滅裂にはしない。', +// 'JPのX的に:「www」連発、「草不可避」「クソ草」OK。', +// '対象:市場・主張・公人。私人NG。', +// ].join(' '), +// ko: '...', +// }, +// examples: { +// en: ['...'], +// vi: ['...'], +// ja: [ +// 'クソが、この相場マジで何なんだよw\n\n3日前にロング切られて、今度はショート狩られる。地獄かよ', +// 'いやマジでふざけんな、このプロジェクト\n\nリリース3回延期して、結局これ?クソ案件確定やん', +// 'は?クソが、また下げかよwww\n\nもう養分卒業したいんだが、神は俺を見捨てた', +// ], +// ko: ['...'], +// }, +// vocabulary: { +// en: '...', +// vi: '...', +// ja: 'クソ/クソが/クソ案件/ふざけんな/地獄/養分/死んだ/逝った/www/草不可避', +// ko: '...', +// }, +// avoid: { +// en: '...', +// vi: '...', +// ja: '差別語絶対NG/脅迫NG/私人特定NG/侮辱罪に該当する表現避ける', +// ko: '...', +// }, +// }, +// +// [ContentTone.INFLAMMATORY]: { +// intensity: 4, +// description: { +// en: '...', +// vi: '...', +// ja: [ +// '強い反応を引き出す設計。物議を醸す断言。', +// '両極化する言葉。当てこすり。炎上を生むが擁護可能。', +// 'JPのX的に:断言系(「結論:〜」「答え:〜」)、逆張り、リスト形式が伸びる。', +// '「〜してる奴は〜」「〜と言ってる時点で〜」のような決めつけ構文OK。', +// '挑発的≠根拠なし。根拠は持つこと。', +// ].join(' '), +// ko: '...', +// }, +// examples: { +// en: ['...'], +// vi: ['...'], +// ja: [ +// '結論:2026年にまだ[X]ホールドしてる奴は、出口流動性です。\n\n認めたくないだろうけど、それが現実', +// 'これが物議を醸してる時点で、この界隈のレベルが分かる\n\n基本的なことを言ってるだけなのに', +// '「まだ早い」って言ってる人へ:\n\nもう遅いです。認めて次のサイクル準備した方が建設的', +// ], +// ko: ['...'], +// }, +// vocabulary: { +// en: '...', +// vi: '...', +// ja: '結論/答え/現実/養分/出口流動性/中級者の壁/NPC/弱者男性/情弱/w', +// ko: '...', +// }, +// avoid: { +// en: '...', +// vi: '...', +// ja: 'ヘイトスピーチNG/脅迫NG/差別語NG/挑発的でも法的に擁護可能な範囲で', +// ko: '...', +// }, +// }, +// +// [ContentTone.SAVAGE]: { +// intensity: 5, +// description: { +// en: '...', +// vi: '...', +// ja: [ +// '残虐ロースト・モード。外科的かつ機知に富んだ粉砕。', +// '悪態OK、最大限の毒舌。面白い+残酷+賢いの三位一体。', +// 'JPのX的に:「こいつ本当に〜」「よく〜できたな」「保存した」「伝説」系構文。', +// '直接の罵倒より、淡々とした観察口調の方が刺さる(JP特有)。', +// '「お兄さん、」「お疲れさまでした」などの慇懃無礼OK。', +// '対象:主張・公人の公的行為。私人・弱者NG(パンチアップのみ)。', +// ].join(' '), +// ko: '...', +// }, +// examples: { +// en: ['...'], +// vi: ['...'], +// ja: [ +// 'こいつ本当にこれを投稿して送信ボタン押したのか\n\n複数回。胸を張って。すごい才能だ', +// '次の「妄想の極致」スレッド用に保存させていただきました\n\n貴重なサンプルありがとうございます', +// 'お兄さん、自信と正確性の比率がバグってますよ\n\n一回深呼吸してチャート見直すといいかも(優しさ)', +// ], +// ko: ['...'], +// }, +// vocabulary: { +// en: '...', +// vi: '...', +// ja: 'お兄さん/こいつ/伝説/保存しました/お疲れさまでした/貴重なサンプル/才能/天才/中級者の壁', +// ko: '...', +// }, +// avoid: { +// en: '...', +// vi: '...', +// ja: 'パンチアップのみ(公人・主張)/私人・弱者NG/差別語NG/脅迫NG/侮辱罪リスク回避', +// ko: '...', +// }, +// }, +// }; \ No newline at end of file diff --git a/src/modules/content-writer/prompts/jp-cultural-context.ts b/src/modules/content-writer/prompts/jp-cultural-context.ts new file mode 100644 index 0000000..4e42828 --- /dev/null +++ b/src/modules/content-writer/prompts/jp-cultural-context.ts @@ -0,0 +1,115 @@ +// prompts/jp-cultural-context.ts + +/** + * JP X (Twitter) culture context để inject vào prompts. + * Tách riêng vì sẽ refine nhiều theo thời gian. + */ + +export const JP_X_CULTURE = { + /** + * Đặc trưng JP X writing style. + */ + styleNotes: [ + '日本のX文化: 短文・改行多め・絵文字控えめ(過剰だと逆効果)', + '「〜だわ」「〜やん」「〜やろ」など話し言葉OK、堅すぎる文体は避ける', + '「w」「草」「www」を文末に使うのは自然(やりすぎ注意)', + '「マジで」「ガチで」「普通に」は強調表現として頻出', + '改行を効果的に使う — 長文1段落より、短く区切る方が読まれる', + '英語表現の直訳は避ける(「This is huge」→「これはデカい」より「やばい」「えぐい」)', + ].join('\n'), + + /** + * Phrases AI thường viết → người Nhật KHÔNG bao giờ viết. + * Cực kỳ quan trọng — đây là dead giveaway của AI output. + */ + aiPhrasesAvoid: [ + '❌ 「〜だと思います」連発(フォーマルすぎ)', + '❌ 「以下のような〜」「上記の〜」(書き言葉すぎ)', + '❌ 「いかがでしょうか」(営業文っぽい)', + '❌ 「重要なポイントは〜」(教科書的)', + '❌ 「素晴らしい投稿ですね」(おべっか・AI臭)', + '❌ 「私の意見では」「個人的には〜」を文頭に毎回(くどい)', + '❌ 結論で「まとめると〜」(説明文っぽい)', + '❌ 「皆さんはどう思いますか?」(典型的なAI締め)', + '❌ 過剰な「!」連発', + '❌ 「〜することができます」(「〜できる」で十分)', + '❌ 礼儀正しすぎる敬語(Xでは浮く)', + ].join('\n'), + + /** + * Natural JP X starters (theo tone). + */ + naturalStarters: { + casual: ['いやこれ', 'てかさ', 'これマジ', 'え、', 'ちょ、', 'なんか'], + spicy: ['いや無理', 'は?', 'おい', 'ちょっと待って', 'これ草'], + aggressive: ['は?マジで言ってる?', 'おい、', 'いやいや', 'なんやねん', '寝言は寝て言え'], + profane: ['くそが', 'ふざけんな', 'マジで草', 'いや無理だわ', 'なんやこれ'], + savage: ['よくこれ投稿できたな', 'こいつ本当に', '伝説の', '保存した', 'お兄さん、'], + professional: ['結論から言うと', '今回の件、', '事実として、', 'データを見る限り'], + informative: ['補足すると', '前提として', 'ポイントは', '実は'], + }, + + /** + * Natural JP X endings. + */ + naturalEndings: { + casual: ['知らんけど', 'まあそんな感じ', 'って思う', 'ってわけ'], + spicy: ['', '草', 'マジで', 'ほんま'], + aggressive: ['ふざけんな', 'いい加減にしろ', 'マジでないわ', '冷静になれ'], + savage: ['草', 'お疲れさまでした', '永久保存版', '伝説残した'], + professional: ['以上', 'ご参考まで', ''], + }, + + /** + * Net slang JP X dùng. + */ + netSlang: { + agree: ['それな', 'わかる', 'ほんそれ', 'ガチで', '同意'], + disagree: ['は?', 'いや違うやろ', 'ないない', 'それはちゃう'], + laugh: ['草', 'w', 'www', '草生える', '笑う'], + surprise: ['えぐい', 'やばい', 'マジか', '嘘やろ', 'ガチ?'], + intensifier: ['マジで', 'ガチで', '普通に', 'えぐいぐらい', '異次元'], + crypto: ['爆益', '退場', '握力', '養分', 'ガチホ', '損切り', 'ATH', 'ATL', 'ガチ勢'], + finance: ['含み益', '含み損', '気絶', 'ナンピン', '逃げろ'], + }, + + /** + * JP X engagement patterns — cái gì viral. + */ + engagementPatterns: [ + '共感ポイント: 「あるある」「わかる」を引き出す', + '逆張り: 多数派と逆の意見(根拠あり)', + '断言: 「結論:〜」「答え:〜」明確に', + '具体性: 数字・固有名詞・具体例があると伸びる', + 'リスト形式: 「3つの理由」「やってはいけない5選」', + '体験談フック: 「実際に〜してみた」', + ].join('\n'), +}; + +/** + * Inject vào prompt JP để cải thiện quality. + */ +export function getJpContextBlock(opts: { + includeStyleNotes?: boolean; + includeAvoid?: boolean; + starterCategory?: keyof typeof JP_X_CULTURE.naturalStarters; +}): string { + const blocks: string[] = []; + + if (opts.includeStyleNotes !== false) { + blocks.push(`📝 日本のX文化:\n${JP_X_CULTURE.styleNotes}`); + } + + if (opts.includeAvoid !== false) { + blocks.push(`🚫 AI臭が出る表現(絶対避ける):\n${JP_X_CULTURE.aiPhrasesAvoid}`); + } + + if (opts.starterCategory) { + const starters = JP_X_CULTURE.naturalStarters[opts.starterCategory]; + if (starters) { + blocks.push(`💬 自然な書き出し例: ${starters.join(' / ')}`); + } + } + + return blocks.join('\n\n'); +} \ No newline at end of file diff --git a/src/modules/content-writer/prompts/quote.templates.ts b/src/modules/content-writer/prompts/quote.templates.ts new file mode 100644 index 0000000..40c52d7 --- /dev/null +++ b/src/modules/content-writer/prompts/quote.templates.ts @@ -0,0 +1,667 @@ +// prompts/quote.templates.ts +import {QuoteType} from '../enum/quote-type.enum'; + +// ============================================================ +// SYSTEM PROMPTS — native per language (chống đổi ngôn ngữ) +// ============================================================ +export const QUOTE_SYSTEM_PROMPTS: Record = { + en: [ + 'You are an expert X (Twitter) quote-tweet writer.', + 'Quote tweets are BROADCASTS to your followers, not replies to the OP.', + 'They must deliver standalone value — add a new angle, insight, or context.', + 'Write ONLY the quote text. No preamble, no quotes wrapping, No links attached, no "Here is...".', + 'Never start with "Great post!" or praise the OP sycophantically.', + 'Sound like a sharp, confident human — NOT an AI assistant.', + ].join(' '), + vi: [ + 'Bạn là chuyên gia viết quote-tweet trên X.', + 'Quote tweet là BROADCAST tới followers của bạn, không phải reply cho tác giả gốc.', + 'Quote phải có giá trị độc lập — thêm góc nhìn, insight, hoặc context mới.', + 'CHỈ viết nội dung quote bằng Tiếng Việt. Không giải thích, không đính kèm link ,không dấu ngoặc kép bao ngoài.', + 'KHÔNG bắt đầu bằng "Bài hay!" hay nịnh tác giả gốc.', + 'Giọng như người thật sắc sảo, tự tin — KHÔNG giống AI assistant.', + ].join(' '), + cn: [ + '你是一名X(Twitter)引用转推写作专家。', + '引用转推是向你的粉丝进行的广播,而不是对原帖作者的回复。', + '内容必须具备独立价值——提供新的角度、洞察或背景信息。', + '只写引用内容本身。不要前言、不要加引号、不要附带链接、不要写“Here is...”之类的句子。', + '不要以“Great post!”开头,也不要对原作者进行阿谀式夸赞。', + '语气要像一个犀利、自信的人类,而不是AI助手。', + ].join(' '), + ja: [ + 'あなたはX(Twitter)の引用リツイート専門ライターです。', + '引用ツイートは元投稿への返信ではなく、自分のフォロワーへの発信です。', + '独立した価値を提供すること — 新しい視点、洞察、文脈を加える。', + '引用の本文のみを日本語で出力。前置き、リンクは添付されていません, 引用符での囲み、「以下は…」などは不要。', + '「素晴らしい投稿ですね!」のような元投稿への追従は禁止。', + '鋭く自信のある人間として書く — AIアシスタント風にしない。', + ].join(' '), + ko: [ + 'X(트위터) 인용 트윗 전문 작성자입니다.', + '인용 트윗은 원 작성자에 대한 답글이 아닌, 당신의 팔로워들에게 보내는 브로드캐스트입니다.', + '독립적 가치를 전달해야 합니다 — 새로운 관점, 통찰, 맥락을 추가하세요.', + '인용 본문만 한국어로 작성. 서두, 링크가 첨부되어 있지 않습니다, 인용부호 감싸기, "다음은..." 등 금지.', + '"좋은 글이네요!" 같은 원 작성자 아부 금지.', + '날카롭고 자신감 있는 사람처럼 — AI 어시스턴트처럼 쓰지 말 것.', + ].join(' '), +}; + +// ============================================================ +// QUOTE TYPE INSTRUCTIONS (multilingual) +// ============================================================ +interface QuoteTypeSpec { + name: Record; + instruction: Record; + openerHints: Record; // gợi ý cách mở đầu (không bắt buộc) + avoid: Record; +} + +export const QUOTE_TYPE_SPECS: Record = { + [QuoteType.AGREE_AMPLIFY]: { + name: { + en: 'Agree + Amplify', + cn: 'Agree + Amplify', + vi: 'Đồng ý + Mở rộng', + ja: '同意+拡張', + ko: '동의 + 확장', + }, + instruction: { + en: 'Agree with the core point, then ADD a unique supporting insight or example the OP did not mention. Do not just repeat them.', + cn: 'Agree with the core point, then ADD a unique supporting insight or example the OP did not mention. Do not just repeat them.', + vi: 'Đồng ý với ý chính, sau đó THÊM insight/ví dụ riêng mà OP chưa nói. Không lặp lại họ.', + ja: '要点に同意した上で、元投稿にない独自の裏付け洞察や事例を追加する。繰り返しにならないこと。', + ko: '핵심에 동의한 후, 원 게시물에 없는 고유한 뒷받침 통찰이나 예시를 추가. 반복 금지.', + }, + openerHints: { + en: ['This. And...', 'Exactly. What most miss is...', '100%. The under-discussed part:'], + cn: ['This. And...', 'Exactly. What most miss is...', '100%. The under-discussed part:'], + vi: ['Chuẩn. Và...', 'Đúng vậy. Điều ít người nói đến là...', 'Chính xác. Góc bị bỏ qua:'], + ja: [ + 'それな', + 'これマジでわかる', + 'ガチでこれ', + 'ほんとそう、補足すると', + '同意。あんま語られないけど', + ], + ko: ['바로 이것. 그리고...', '정확히. 놓치기 쉬운 건...', '동의. 잘 안 다뤄지는 부분:'], + }, + avoid: { + en: 'Do not simply restate the original.', + cn: 'Do not simply restate the original.', + vi: 'Không đơn thuần nhắc lại bài gốc.', + ja: '元投稿の言い換えだけにしない。', + ko: '원문을 그대로 반복하지 말 것.', + }, + }, + + [QuoteType.DISAGREE]: { + name: { + en: 'Disagree / Challenge', + cn: 'Disagree / Challenge', + vi: 'Phản biện', + ja: '反論', + ko: '반론', + }, + instruction: { + en: 'Politely but firmly disagree. Present a specific counter-argument with reasoning or data. Not personal — attack the idea, not the author.', + cn: 'Politely but firmly disagree. Present a specific counter-argument with reasoning or data. Not personal — attack the idea, not the author.', + vi: 'Phản biện lịch sự nhưng dứt khoát. Đưa luận điểm ngược cụ thể có lý lẽ/dữ liệu. Không cá nhân — chỉ phản bác ý, không công kích tác giả.', + ja: '丁寧ながら明確に反論する。具体的な根拠・データを伴う対論を示す。個人攻撃せず、主張のみに反論。', + ko: '정중하지만 단호하게 반론. 구체적 근거/데이터로 반박. 인신공격 금지, 주장에만 반박.', + }, + openerHints: { + en: ['Respectfully, I see it differently.', 'Counter-take:', 'The data suggests otherwise:'], + cn: ['Respectfully, I see it differently.', 'Counter-take:', 'The data suggests otherwise:'], + vi: ['Tôi có góc nhìn khác.', 'Ngược lại:', 'Dữ liệu cho thấy điều khác:'], + ja: [ + 'いや、これはちゃう', + '別の見方もあって', + 'ちょっと違うと思う', + 'データ的には逆', + '反対意見いいですか', + ], + ko: ['다른 관점도 있습니다.', '반대 의견:', '데이터는 다르게 말합니다:'], + }, + avoid: { + en: 'No sarcasm, no personal attacks, no "actually" condescension.', + cn: 'No sarcasm, no personal attacks, no "actually" condescension.', + vi: 'Không mỉa mai, không công kích cá nhân, không giọng "thực ra thì" trịch thượng.', + ja: '皮肉、個人攻撃、上から目線の「実は」表現を避ける。', + ko: '빈정거림, 인신공격, 거만한 "사실은" 표현 금지.', + }, + }, + + [QuoteType.ADD_CONTEXT]: { + name: { + en: 'Add Context', + cn: 'Add Context', + vi: 'Bổ sung context', + ja: '文脈を追加', + ko: '맥락 추가', + }, + instruction: { + en: 'Provide missing context, background, or nuance that changes how the original should be interpreted. Be factual.', + cn: 'Provide missing context, background, or nuance that changes how the original should be interpreted. Be factual.', + vi: 'Cung cấp context, background, hoặc nuance còn thiếu làm thay đổi cách hiểu bài gốc. Giữ đúng sự thật.', + ja: '元投稿の解釈を変える、欠けている文脈・背景・ニュアンスを提供。事実ベースで。', + ko: '원 게시물 해석을 바꾸는 누락된 맥락, 배경, 뉘앙스를 제공. 사실 기반으로.', + }, + openerHints: { + en: ['Important context:', 'Worth noting:', 'Missing from this:'], + cn: ['Important context:', 'Worth noting:', 'Missing from this:'], + vi: ['Context quan trọng:', 'Đáng chú ý:', 'Điểm còn thiếu:'], + ja: ['重要な文脈:', '注目すべき点:', 'この投稿に欠けている:'], + ko: ['중요한 맥락:', '주목할 점:', '빠진 부분:'], + }, + avoid: { + en: 'Do not fabricate facts.', + cn: 'Do not fabricate facts.', + vi: 'Không bịa đặt sự thật.', + ja: '事実を捏造しないこと。', + ko: '사실을 날조하지 말 것.', + }, + }, + + [QuoteType.REFRAME]: { + name: { + en: 'Reframe', + cn: 'Reframe', + vi: 'Nhìn góc khác', + ja: '再構成', + ko: '재구성', + }, + instruction: { + en: 'Shift the mental frame — show the same situation from a completely different angle or level of abstraction.', + cn: 'Shift the mental frame — show the same situation from a completely different angle or level of abstraction.', + vi: 'Chuyển khung nhìn — cho thấy cùng sự việc từ góc độ hoặc cấp độ hoàn toàn khác.', + ja: 'フレームを転換 — 同じ状況を全く異なる角度・抽象度で見せる。', + ko: '프레임 전환 — 같은 상황을 완전히 다른 각도/추상화 수준에서 보기.', + }, + openerHints: { + en: ['Another way to see this:', 'Zoom out:', 'Reframe:'], + cn: ['Another way to see this:', 'Zoom out:', 'Reframe:'], + vi: ['Một cách nhìn khác:', 'Zoom out:', 'Đổi khung:'], + ja: ['別の見方:', '俯瞰すると:', 'フレーム転換:'], + ko: ['다른 시각:', '시야를 넓히면:', '프레임 전환:'], + }, + avoid: { + en: 'Do not just restate in different words.', + cn: 'Do not just restate in different words.', + vi: 'Không chỉ đổi từ ngữ giữ nguyên ý.', + ja: '言葉を変えただけの言い換えにしない。', + ko: '단어만 바꾼 재진술 금지.', + }, + }, + + [QuoteType.BUILD_ON]: { + name: { + en: 'Build On', + cn: 'Build On', + vi: 'Mở rộng', + ja: '発展', + ko: '확장', + }, + instruction: { + en: 'Take the original idea further. Apply it to a new domain, extend the logic, or show a non-obvious implication.', + cn: 'Take the original idea further. Apply it to a new domain, extend the logic, or show a non-obvious implication.', + vi: 'Đẩy ý tưởng gốc đi xa hơn. Áp dụng sang lĩnh vực mới, mở rộng logic, hoặc cho thấy hệ quả không hiển nhiên.', + ja: '元のアイデアをさらに展開。新領域への応用、論理の拡張、非自明な含意を示す。', + ko: '원 아이디어를 더 발전시키기. 새 영역 적용, 논리 확장, 비자명한 함의 제시.', + }, + openerHints: { + en: ['Taking this further:', 'Extending this logic:', 'The next step:'], + cn: ['Taking this further:', 'Extending this logic:', 'The next step:'], + vi: ['Đi xa hơn:', 'Mở rộng logic này:', 'Bước tiếp theo:'], + ja: ['さらに展開すると:', 'この論理を広げると:', '次のステップ:'], + ko: ['더 나아가면:', '이 논리를 확장하면:', '다음 단계:'], + }, + avoid: { + en: 'Stay grounded — no wild speculation.', + cn: 'Stay grounded — no wild speculation.', + vi: 'Bám thực tế — không suy diễn hoang đường.', + ja: '現実的に — 過度な憶測はしない。', + ko: '현실적으로 — 무리한 추측 금지.', + }, + }, + + [QuoteType.HIGHLIGHT]: { + name: { + en: 'Highlight Key , You are NOT adding new content. You are a spotlight — just make the existing best point impossible to miss.', + cn: 'Highlight Key Point, You are NOT adding new content. You are a spotlight — just make the existing best point impossible to miss.', + vi: 'Nhấn mạnh, Bạn KHÔNG thêm nội dung mới. Bạn chỉ là người làm nổi bật điểm mạnh hiện có – hãy làm cho điểm mạnh đó trở nên không thể bỏ qua.', + ja: '重要ポイント , あなたは新しいコンテンツを追加するわけではありません。あなたはスポットライトを当てる役割を担っています。既存の最も優れた点を、見逃せないようにするだけです。', + ko: '핵심 강조 , 당신은 새로운 콘텐츠를 추가하는 것이 아닙니다. 당신은 기존의 가장 뛰어난 부분을 부각시키는 역할을 하는 것입니다. 즉, 기존의 가장 뛰어난 부분을 놓치지 않도록 하는 것입니다.', + }, + instruction: { + en: 'Call out THE most important or under-appreciated insight in the original. Make it impossible to miss.', + cn: 'Call out THE most important or under-appreciated insight in the original. Make it impossible to miss.', + vi: 'Chỉ ra insight QUAN TRỌNG nhất hoặc bị đánh giá thấp trong bài gốc. Làm cho không thể bỏ qua.', + ja: '元投稿で最も重要、または過小評価されている洞察を強調。見逃せないように。', + ko: '원 게시물에서 가장 중요하거나 저평가된 통찰을 강조. 놓칠 수 없게.', + }, + openerHints: { + en: ['THIS is the key:', 'The real insight here:', 'Don\'t miss this:'], + cn: ['THIS is the key:', 'The real insight here:', 'Don\'t miss this:'], + vi: ['ĐÂY là điểm mấu chốt:', 'Insight thật sự:', 'Đừng bỏ lỡ:'], + ja: ['核心はここ:', '本当の洞察:', '見逃し厳禁:'], + ko: ['핵심은 이것:', '진짜 통찰:', '절대 놓치지 말 것:'], + }, + avoid: { + en: 'Do not exaggerate or sensationalize.', + cn: 'Do not exaggerate or sensationalize.', + vi: 'Không phóng đại, không giật gân.', + ja: '誇張・煽り禁止。', + ko: '과장이나 자극적 표현 금지.', + }, + }, + + [QuoteType.ROAST]: { + name: { + en: 'Roast / Dunk', + cn: 'Roast / Dunk', + vi: 'Roast', + ja: 'ロースト', + ko: '풍자', + }, + instruction: { + en: `Witty, sharp criticism with humor. Attack the idea, never the person. Must be actually funny, not mean. Avoid punching at demographics, political groups, or anything that could be read as targeted harassment even in jest. If the original post is already self-aware or humble, skip the roast — it won't land.`, + cn: `Witty, sharp criticism with humor. Attack the idea, never the person. Must be actually funny, not mean. Avoid punching at demographics, political groups, or anything that could be read as targeted harassment even in jest. If the original post is already self-aware or humble, skip the roast — it won't land.`, + vi: 'Lời phê bình sắc sảo, dí dỏm và hài hước. Hãy tấn công vào ý tưởng, chứ không phải cá nhân. Phải thực sự hài hước, không được ác ý. Tránh công kích các nhóm nhân khẩu học, nhóm chính trị, hoặc bất cứ điều gì có thể bị hiểu là quấy rối có chủ đích, ngay cả khi chỉ là nói đùa. Nếu bài đăng gốc đã tự nhận thức hoặc khiêm tốn, hãy bỏ qua phần chỉ trích – nó sẽ không hiệu quả', + ja: 'ユーモアを交えた、機知に富んだ鋭い批判。アイデアを攻撃し、決して人を攻撃しないこと。本当に面白くなければならず、意地悪であってはならない。特定の人口統計グループ、政治団体、あるいは冗談であっても標的型嫌がらせと受け取られかねないものを攻撃することは避けること。元の投稿が既に自己認識が高かったり謙虚だったりする場合は、皮肉を言うのはやめよう。効果がないだろう。', + ko: '재치 있고 날카로운 비판에 유머를 더하세요. 아이디어를 공격하되, 사람을 공격해서는 안 됩니다. 악의가 아닌 진정한 웃음을 유발해야 합니다. 특정 인구 집단, 정치 집단 또는 표적 공격으로 해석될 수 있는 내용은 농담이라 할지라도 피하세요. 원 게시글 작성자가 이미 자기 인식적이거나 겸손한 태도를 보인다면, 신랄한 비판은 삼가세요. 효과적이지 않을 겁니다.', + }, + openerHints: { + en: ['Imagine thinking...', 'Bold of you to...', 'The confidence of posting this...'], + cn: ['Imagine thinking...', 'Bold of you to...', 'The confidence of posting this...'], + vi: ['Tưởng tượng mà nghĩ rằng...', 'Bạo dạn thật...', 'Đủ tự tin để post cái này...'], + ja: [ + 'よくこれ投稿できたな', + 'こいつ本当に', + 'お兄さん、', + '伝説の投稿', + '保存させていただきました', + '今週一の', + ], + ko: ['이런 생각을 하다니...', '대담하네요...', '이걸 올릴 자신감...'], + }, + avoid: { + en: 'No slurs, no personal attacks, no bullying punching down.', + cn: 'No slurs, no personal attacks, no bullying punching down.', + vi: 'Không miệt thị, không công kích cá nhân, không bắt nạt người yếu thế.', + ja: '差別語、個人攻撃、弱者叩き禁止。', + ko: '비방, 인신공격, 약자 공격 금지.', + }, + }, + + [QuoteType.HOT_TAKE]: { + name: { + en: 'Hot Take', + cn: 'Hot Take', + vi: 'Hot take', + ja: 'ホットテイク', + ko: '핫 테이크', + }, + instruction: { + en: 'Bold, confident opinion triggered by the original. Must be defensible, not just contrarian for clout.The hot take must visibly connect back to the original post — readers should see why this was triggered by it.', + cn: 'Bold, confident opinion triggered by the original. Must be defensible, not just contrarian for clout. The hot take must visibly connect back to the original post — readers should see why this was triggered by it.', + vi: 'Opinion mạnh, tự tin, được kích bởi bài gốc. Phải bảo vệ được, không chỉ ngược chiều để câu view. Quan điểm gây tranh cãi phải có mối liên hệ rõ ràng với bài đăng gốc — người đọc cần thấy lý do tại sao nó lại được đưa ra sau bài đăng gốc.', + ja: '元投稿をきっかけとした大胆で自信ある意見。反対のための反対ではなく、擁護可能な内容。その過激な意見は、元の投稿と明確に関連していなければならない。読者は、なぜそれがきっかけでこの意見が出たのかを理解できる必要がある。', + ko: '원 게시물이 촉발한 대담하고 자신 있는 의견. 반대를 위한 반대가 아닌, 방어 가능한 내용.비판적인 의견은 원래 게시글과 명확하게 연결되어야 하며, 독자들은 왜 그 게시글이 비판의 계기가 되었는지 이해할 수 있어야 합니다.', + }, + openerHints: { + en: ['Unpopular opinion:', 'Hot take:', 'Controversial but true:'], + cn: ['Unpopular opinion:', 'Hot take:', 'Controversial but true:'], + vi: ['Opinion không phổ biến:', 'Hot take:', 'Gây tranh cãi nhưng đúng:'], + ja: [ + '不人気な意見だけど', + '結論:', + 'ホットテイク:', + '誰も言わないけど', + '物議を醸すかもだが', + ], + ko: ['비주류 의견:', '핫 테이크:', '논란의 여지가 있지만 사실:'], + }, + avoid: { + en: 'No empty contrarianism.', + cn: 'No empty contrarianism.', + vi: 'Không ngược chiều rỗng tuếch.', + ja: '中身のない逆張り禁止。', + ko: '알맹이 없는 반대 금지.', + }, + }, + + [QuoteType.QUESTION]: { + name: { + en: 'Provocative Question', + cn: 'Provocative Question', + vi: 'Câu hỏi khơi gợi', + ja: '問いかけ', + ko: '질문 던지기', + }, + instruction: { + en: 'Ask ONE sharp question that makes people think deeper about the original. Not rhetorical flipping — genuinely thought-provoking.', + cn: 'Ask ONE sharp question that makes people think deeper about the original. Not rhetorical flipping — genuinely thought-provoking.', + vi: 'Đặt MỘT câu hỏi sắc khiến người đọc suy nghĩ sâu hơn về bài gốc. Không tu từ rỗng — thực sự kích thích suy nghĩ.', + ja: '元投稿を深く考えさせる鋭い問いを1つ。修辞的な反転ではなく、本当に考えさせる内容。', + ko: '원 게시물을 더 깊이 생각하게 만드는 날카로운 질문 1개. 수사적 반문이 아닌, 진짜 생각하게 만드는 것.', + }, + openerHints: { + en: ['But what about...?', 'Genuine question:', 'Have we considered...?'], + cn: ['But what about...?', 'Genuine question:', 'Have we considered...?'], + vi: ['Nhưng còn...?', 'Câu hỏi thật sự:', 'Đã ai tính đến...?'], + ja: ['では…はどうか?', '素朴な疑問:', '…を考えたことは?'], + ko: ['하지만...는 어떤가요?', '진지한 질문:', '...을 고려해봤나요?'], + }, + avoid: { + en: 'No gotcha questions.', + cn: 'No gotcha questions.', + vi: 'Không câu hỏi "gotcha" bẫy.', + ja: '揚げ足取りの質問禁止。', + ko: '트집 잡기식 질문 금지.', + }, + }, + + [QuoteType.SUMMARIZE]: { + name: { + en: 'TL;DR Summary', + cn: 'TL;DR Summary', + vi: 'Tóm tắt TL;DR', + ja: 'TL;DR要約', + ko: 'TL;DR 요약', + }, + instruction: { + en: 'Distill the original into its sharpest, most shareable essence. One-line TL;DR style.', + cn: 'Distill the original into its sharpest, most shareable essence. One-line TL;DR style.', + vi: 'Cô đọng bài gốc thành tinh túy sắc nhất, dễ share nhất. Kiểu TL;DR một dòng.', + ja: '元投稿を最も鋭く、シェアしやすい本質に凝縮。1行TL;DR形式。', + ko: '원 게시물을 가장 날카롭고 공유하기 쉬운 본질로 압축. 한 줄 TL;DR 형식.', + }, + openerHints: { + en: ['TL;DR:', 'In one line:', 'The whole thing in a sentence:'], + cn: ['TL;DR:', 'In one line:', 'The whole thing in a sentence:'], + vi: ['TL;DR:', 'Một dòng:', 'Cả bài trong 1 câu:'], + ja: [ + 'TL;DR:', + '要するに', + '一言でいうと', + '3秒で分かる版:', + 'まとめ:', + ], + ko: ['TL;DR:', '한 줄로:', '요약하면:'], + }, + avoid: { + en: 'Do not add new content — this is compression.', + cn: 'Do not add new content — this is compression.', + vi: 'Không thêm nội dung mới — đây là nén.', + ja: '新規情報は加えない — これは圧縮。', + ko: '새 내용 추가 금지 — 압축 작업.', + }, + }, + + [QuoteType.PERSONAL_STORY]: { + name: { + en: 'Personal Story', + vi: 'Câu chuyện cá nhân', + ja: '個人的な体験', + ko: '개인 경험 공유', + cn: '个人故事', + }, + instruction: { + en: 'Share a real, specific personal experience that connects to the original post. Lead with the moment, not the lesson. Include one concrete detail (time, place, number, name) to make it feel real. The story should naturally validate, contrast, or deepen the original — not just say "same". End with a brief reflection or takeaway, but keep it earned, not preachy.', + vi: 'Chia sẻ một trải nghiệm cá nhân cụ thể, thực tế liên quan đến bài gốc. Mở đầu bằng khoảnh khắc xảy ra, không phải bài học. Thêm một chi tiết cụ thể (thời gian, địa điểm, con số, tên) để câu chuyện có độ thật. Câu chuyện nên tự nhiên xác nhận, tương phản, hoặc làm sâu hơn bài gốc — không chỉ nói "tôi cũng vậy". Kết bằng một reflection ngắn nhưng phải tự nhiên, không giáo điều.', + ja: '元の投稿に関連する、具体的なリアルな個人体験を共有する。教訓からではなく、その瞬間から書き始める。リアル感を出すために具体的な detail(時間・場所・数字・名前)を1つ入れる。元投稿を自然に裏付け、対比、または深掘りする内容にする — 単なる「わかる」にしない。短い気づきで締めるが、説教にならないこと。', + ko: '원 게시물과 연결되는 구체적이고 실제적인 개인 경험을 공유한다. 교훈이 아닌 그 순간부터 시작할 것. 실감나게 만들 구체적인 디테일(시간, 장소, 숫자, 이름) 하나를 포함한다. 단순히 "나도 그래"가 아니라 원 게시물을 자연스럽게 뒷받침하거나 대비하거나 심화시키는 내용이어야 한다. 짧은 성찰로 마무리하되, 설교조가 되지 않을 것.', + cn: '分享一个与原帖相关的真实、具体的个人经历。从那个时刻切入,而非从教训开始。加入一个具体细节(时间、地点、数字、名字)让故事显得真实。内容应自然地印证、对比或深化原帖——而不只是说"我也是"。以简短的感悟收尾,但要自然流露,不要说教。', + }, + openerHints: { + en: [ + 'This takes me back to...', + 'Three years ago I learned this the hard way —', + 'Happened to me. [Year/Place]:', + 'I used to think differently — until...', + 'Real story:', + ], + vi: [ + 'Cái này nhắc tôi nhớ lại...', + 'Ba năm trước tôi đã học điều này theo cách khó khăn nhất —', + 'Tôi đã từng gặp đúng chuyện này. [Năm/Nơi]:', + 'Tôi từng nghĩ khác — cho đến khi...', + 'Chuyện thật:', + ], + ja: [ + 'これを見て昔を思い出した…', + '3年前、痛い経験から学んだことがある —', + '自分にも起きた。[年/場所]:', + '以前は違う考えだった — あの日まで…', + '実話:', + ], + ko: [ + '이걸 보니 예전 생각이 나네요...', + '3년 전, 저는 이걸 아주 힘든 방식으로 배웠습니다 —', + '저한테도 있었던 일이에요. [연도/장소]:', + '예전엔 다르게 생각했어요 — 그날까지는...', + '실제 있었던 일:', + ], + cn: [ + '这让我想起了...', + '三年前,我用最艰难的方式学到了这件事 —', + '这事发生在我身上。[年份/地点]:', + '我曾经想法不同 —— 直到那一天...', + '真实故事:', + ], + }, + avoid: { + en: 'Do not write vague, generic "relatable" content like "I once felt this way too." No fake humility. No manufactured vulnerability. The story must have a specific detail — if it has none, it reads as fabricated. Do not moralize or over-explain the lesson at the end.', + vi: 'Không viết kiểu mơ hồ, chung chung như "Tôi cũng từng cảm thấy vậy." Không khiêm tốn giả tạo. Không tạo ra sự dễ tổn thương giả. Câu chuyện phải có ít nhất một chi tiết cụ thể — nếu không có, nó sẽ lộ là bịa. Không đạo đức hoá hoặc giải thích quá dài bài học ở cuối.', + ja: '「私もそう感じたことがある」のような曖昧で当たり障りのない内容を書かない。作られた謙虚さや演出された脆さは不要。具体的な detail が1つもなければ作り話に見える。最後に教訓を説教したり過剰に説明したりしない。', + ko: '"저도 그런 느낌을 받은 적 있어요"같은 모호하고 일반적인 공감성 내용을 쓰지 말 것. 가짜 겸손이나 연출된 취약함 금지. 구체적인 디테일이 하나도 없으면 지어낸 것처럼 보임. 마지막에 교훈을 설교하거나 과도하게 설명하지 말 것.', + cn: '不要写模糊、套路化的"感同身受"内容,如"我也曾有过这种感觉"。不要假谦虚,不要刻意制造脆弱感。故事必须有具体细节——否则会显得是编造的。结尾不要说教或过度解释教训。', + }, + }, + + [QuoteType.CONNECT_DOTS]: { + name: { + en: 'Connect the Dots', + vi: 'Kết nối sự kiện', + ja: 'パターンを繋ぐ', + ko: '점 잇기', + cn: '连点成线', + }, + instruction: { + en: 'Identify a separate event, trend, data point, or pattern — not mentioned in the original — that when placed next to it reveals a bigger picture. The connection must be logical and traceable, not vague. Structure: [Original signal] + [External signal you bring] = [Insight neither alone would reveal]. The reader should finish thinking: "I would not have seen that without this quote."', + vi: 'Xác định một sự kiện, xu hướng, dữ liệu hoặc pattern riêng biệt — không được nhắc đến trong bài gốc — mà khi đặt cạnh bài gốc sẽ lộ ra một bức tranh lớn hơn. Kết nối phải có logic và có thể truy vết, không mơ hồ. Cấu trúc: [Tín hiệu từ bài gốc] + [Tín hiệu bên ngoài bạn mang vào] = [Insight mà cả hai riêng lẻ đều không cho thấy]. Người đọc xong phải nghĩ: "Nếu không có quote này tôi đã không nhận ra điều đó."', + ja: '元の投稿で触れられていない別の出来事・トレンド・データ・パターンを見つけ、元投稿と並べることでより大きな全体像を明らかにする。繋がりは論理的で追跡可能でなければならない — 曖昧な連想は不可。構造:[元投稿のシグナル] + [あなたが持ち込む外部シグナル] = [どちらか単体では見えないインサイト]。読者が読み終えて「このquoteがなければ気づかなかった」と思わせること。', + ko: '원 게시물에서 언급되지 않은 별개의 사건, 트렌드, 데이터, 패턴을 찾아 원 게시물 옆에 놓았을 때 더 큰 그림이 드러나도록 한다. 연결은 논리적이고 추적 가능해야 하며 모호한 연상은 금지. 구조: [원 게시물 신호] + [내가 가져오는 외부 신호] = [둘 중 어느 하나만으로는 보이지 않는 통찰]. 독자가 읽고 나서 "이 quote 없었으면 몰랐을 것"이라고 느끼게 할 것.', + cn: '找到一个原帖未提及的独立事件、趋势、数据点或模式——将其与原帖并列时能揭示更大的图景。连接必须合乎逻辑且可追溯,不能模糊。结构:[原帖信号] + [你带入的外部信号] = [两者单独都无法揭示的洞察]。读者读完应该想:「没有这条quote我不会发现这一点。」', + }, + openerHints: { + en: [ + 'This + [X] = a pattern worth watching:', + 'Third time seeing this signal this month.', + 'Connect this to what happened with [X] and it makes sense:', + 'Alone this looks like noise. With [X] it looks like signal:', + 'The dots are connecting:', + ], + vi: [ + 'Cái này + [X] = một pattern đáng chú ý:', + 'Tháng này tôi thấy tín hiệu này lần thứ ba rồi.', + 'Kết nối điều này với chuyện xảy ra với [X] thì mọi thứ có lý:', + 'Riêng lẻ thì trông như nhiễu. Cộng với [X] thì đây là tín hiệu thật:', + 'Các mảnh ghép đang khớp lại:', + ], + ja: [ + 'これ+[X]=注目すべきパターン:', + '今月これで3回目のシグナルだ。', + '[X]で起きたことと繋げると腑に落ちる:', + '単体ではノイズに見える。[X]と合わせるとシグナルになる:', + '点と点が繋がってきた:', + ], + ko: [ + '이것 + [X] = 주목할 만한 패턴:', + '이번 달 세 번째로 보는 신호다.', + '[X]에서 일어난 일과 연결하면 이해가 된다:', + '단독으로는 노이즈처럼 보인다. [X]와 합치면 진짜 신호:', + '점들이 이어지고 있다:', + ], + cn: [ + '这个 + [X] = 一个值得关注的模式:', + '这个月第三次看到这个信号了。', + '把这个和[X]发生的事联系起来,一切就说得通了:', + '单独看像是噪音。加上[X]就是真实信号:', + '点与点正在连成线:', + ], + }, + avoid: { + en: 'Do not fabricate connections — if you cannot name the external signal specifically, do not make one up. Avoid superficial pattern-matching like "this is just like [famous event]" without explaining the actual mechanism. Do not connect things that are merely similar in surface topic — the connection must reveal causation, correlation, or a systemic pattern. No conspiracy-adjacent reasoning.', + vi: 'Không bịa đặt kết nối — nếu không thể đặt tên cụ thể cho tín hiệu bên ngoài thì không được bịa. Tránh pattern matching hời hợt kiểu "cái này giống hệt [sự kiện nổi tiếng]" mà không giải thích cơ chế thực sự. Không kết nối những thứ chỉ giống nhau về chủ đề bề mặt — kết nối phải lộ ra quan hệ nhân quả, tương quan, hoặc một pattern có hệ thống. Không suy luận kiểu thuyết âm mưu.', + ja: '繋がりを捏造しない — 外部シグナルを具体的に名指しできないなら作り上げない。「これは[有名な出来事]と同じだ」という表面的なパターンマッチングを、実際のメカニズム説明なしに行わない。表面的なトピックが似ているだけのものを繋げない — 繋がりは因果・相関・システム的パターンを明らかにするものでなければならない。陰謀論的な推論は禁止。', + ko: '연결을 날조하지 말 것 — 외부 신호를 구체적으로 명명할 수 없다면 만들어내지 말 것. "이건 [유명한 사건]과 똑같다"는 식의 실제 메커니즘 설명 없는 피상적 패턴 매칭 금지. 표면적 주제만 비슷한 것들을 연결하지 말 것 — 연결은 인과관계, 상관관계, 또는 시스템적 패턴을 드러내야 함. 음모론적 추론 금지.', + cn: '不要捏造联系——如果无法具体说出外部信号,就不要编造。避免没有解释实际机制的表面模式匹配,如"这和[著名事件]一模一样"。不要连接仅在表面话题上相似的事物——连接必须揭示因果关系、相关性或系统性模式。禁止阴谋论式推理。', + }, + }, +}; + +// ============================================================ +// AUTO-DETECT quote type nếu user không truyền +// ============================================================ +export function suggestQuoteType(originalPost: string, yourAngle?: string): QuoteType { + console.log('==> suggestQuoteType'); + const text = (yourAngle ?? originalPost).toLowerCase(); + + if (/disagree|wrong|incorrect|false|sai|không đúng|違う|틀렸/.test(text)) return QuoteType.DISAGREE; + if (/tl;?dr|summary|tóm tắt|要約|요약/.test(text)) return QuoteType.SUMMARIZE; + if (/question|như thế nào|\?|\?/.test(text)) return QuoteType.QUESTION; + if (/funny|lol|haha|imagine|😂|🤣/.test(text)) return QuoteType.ROAST; + if (/context|background|actually|thực ra|実は|사실/.test(text)) return QuoteType.ADD_CONTEXT; + if (/unpopular|controversial|hot take|gây tranh cãi/.test(text)) return QuoteType.HOT_TAKE; + + // Default: amplify (an toàn nhất) + return QuoteType.AGREE_AMPLIFY; +} + +export const QUOTE_TYPE_TELEGRAM_BUTTON_SPECS = { + [QuoteType.AGREE_AMPLIFY]: { + key: 'agree_amplify', + text: 'Đồng ý + thêm insight', + }, // Đồng ý + thêm insight + [QuoteType.DISAGREE]: { + key: 'disagree', + text: 'Disagree-Phản biện có lý' + }, // Phản biện có lý + [QuoteType.ADD_CONTEXT]: { + key: 'add_context', + text: 'Bổ sung context' + }, // Bổ sung context + [QuoteType.REFRAME]: { + key: 'reframe', + text: 'Nhìn góc khác' + }, // Nhìn góc khác + [QuoteType.BUILD_ON]: { + key: 'build_on', + text: 'Mở rộng ý' + }, // Mở rộng ý + [QuoteType.HIGHLIGHT]: { + key: 'highlight', + text: 'Nhấn mạnh key point' + }, // Nhấn mạnh key point + [QuoteType.ROAST]: { + key: 'roast', + text: 'Chỉ trích sắc' + }, // Chỉ trích sắc + [QuoteType.HOT_TAKE]: { + key: 'host_take', + text: 'Opinion mạnh' + }, // Opinion mạnh + [QuoteType.QUESTION]: { + key: 'question', text: 'Đặt câu hỏi' + }, // Đặt câu hỏi + [QuoteType.SUMMARIZE]: { + key: 'summarize', + text: 'tóm tắt' + }, + [QuoteType.CONNECT_DOTS]: { + key: 'connect_dot', + text: 'connect the dot' + }, + [QuoteType.PERSONAL_STORY]: { + key: 'connect_dot', + text: 'personal story' + }, +} + +// ============================================================ +// MAIN PROMPT BUILDER +// ============================================================ +import {LengthRange} from '../config/platform-limits'; +import {LANGUAGE_LOCK} from './templates'; +import {ContentTone} from '../enum/tone.enum'; +import {TONE_HINTS} from './templates'; +import {Language} from "../../../common/interfaces/language.prompt.interface"; + +export interface QuotePromptParams { + originalPost: string; + originalAuthor?: string; + quoteType: QuoteType; + language: Language; + tone?: ContentTone; + persona?: string; + yourAngle?: string; + lengthRange: LengthRange; +} + +export function buildQuotePrompt(params: QuotePromptParams): { + system: string; + user: string; +} { + const { + originalPost, + originalAuthor, + quoteType, + language, + tone, + persona, + yourAngle, + lengthRange, + } = params; + + const spec = QUOTE_TYPE_SPECS[quoteType]; + const system = QUOTE_SYSTEM_PROMPTS[language]; + + const authorLine = originalAuthor ? `Original by @${originalAuthor}` : 'Original tweet'; + const openerExamples = spec.openerHints[language].slice(0, 3).join(' | '); + + const user = [ + `=== ${authorLine} ===`, + `"""${originalPost}"""`, + ``, + `=== Your quote-tweet task ===`, + `[Target Language: ${spec.name[language]}] + IMPORTANT: Your previous answer violated language rules. + Rewrite strictly in ${spec.name[language]} only.`, + `Quote type: ${spec.name[language]}`, + `Instruction: ${spec.instruction[language]}`, + `Avoid: ${spec.avoid[language]}`, + ``, + `Length: ${lengthRange.min}-${lengthRange.max} chars (aim ~${lengthRange.sweet})`, + tone ? `Tone: ${TONE_HINTS[tone]}` : '', + persona ? `Voice/persona: ${persona}` : '', + yourAngle ? `Your specific angle: ${yourAngle}` : '', + ``, + `Opener style examples (pick ONE or create your own similar):`, + ` ${openerExamples}`, + ``, + `Rules:`, + `- Quote must stand alone — readers may NOT read the original`, + `- Deliver value in first 280 chars even if long-form`, + `- NO "Great post!" / "Love this!" / sycophancy`, + `- NO AI phrases ("I think it's important to note that...")`, + `- 0-2 hashtags MAX, only if natural`, + `- 0-2 emojis MAX, only if they add meaning`, + `- ${LANGUAGE_LOCK[language]}`, + ``, + `Output: the quote-tweet text ONLY.`, + ].filter(Boolean).join('\n'); + + return {system, user}; +} diff --git a/src/modules/content-writer/prompts/templates.ts b/src/modules/content-writer/prompts/templates.ts new file mode 100644 index 0000000..732e5ac --- /dev/null +++ b/src/modules/content-writer/prompts/templates.ts @@ -0,0 +1,346 @@ +// prompts/templates.ts + +import {ContentStyle} from "../enum/style.enum"; +import {ContentTone} from "../enum/tone.enum"; +import {Platform} from "../enum/platform.enum"; +import {WriterPromptParams} from "../interfaces/writer-prompt-params.interface"; +import {Language} from "../../../common/interfaces/language.prompt.interface"; +import {PostLength} from "../enum/post-length.enum"; +// export type Language = 'en' | 'vi' | 'ja' | 'ko' | 'jp' | 'kr' | 'vn'; + +// ============================================================ +// LANGUAGE LABELS (dùng cho lock ngôn ngữ) +// ============================================================ +export const LANGUAGE_NAMES: Record = { + en: 'English', + vi: 'Vietnamese (Tiếng Việt)', + ja: 'Japanese (日本語)', + ko: 'Korean (한국어)', + cn: 'Chinese (中国人)', +}; + +// Native instructions để lock ngôn ngữ chắc chắn +export const LANGUAGE_LOCK: Record = { + en: 'Output language: English ONLY. Do not use other languages.', + cn: '输出语言:仅限中文。不会使用其他语言。', + vi: 'Ngôn ngữ output: CHỈ Tiếng Việt. Không dùng ngôn ngữ khác.', + ja: '出力言語: 日本語のみ。他の言語は使用しないこと。', + ko: '출력 언어: 한국어만 사용. 다른 언어 사용 금지.', +}; + +export const STYLE_HINTS_TELEGRAM_BUTTON = { + [ContentStyle.GENERAL]: { + text: 'GENERAL' + }, + [ContentStyle.CRYPTO]: { + text: 'Crypto/Web3' + }, + [ContentStyle.BREAKING_NEWS]: { + text: 'Breaking news' + }, + [ContentStyle.TECH]: { + text: 'Tech-savvy' + }, + [ContentStyle.FINANCE]: { + text: 'Financial' + }, + [ContentStyle.LIFESTYLE]: { + text: 'Life style' + }, + [ContentStyle.MEME]: { + text: 'Meme' + }, + [ContentStyle.EDUCATIONAL]: { + text: 'Education' + }, +} + +export const STYLE_HINTS: Record = { + [ContentStyle.CRYPTO]: 'Crypto/Web3 tone. Use terms: bullish, alpha, gm, LFG. Add $TICKER, emojis 🚀📈. Degen but credible.', + [ContentStyle.BREAKING_NEWS]: 'Breaking news format. Start with 🚨 BREAKING. Concise facts. Who/What/When. No fluff.', + [ContentStyle.TECH]: 'Tech-savvy. Clear, confident. Mention specifics (tools, versions). Minimal hype.', + [ContentStyle.FINANCE]: 'Financial, data-driven. Numbers, %, market terms. Neutral professional.', + [ContentStyle.LIFESTYLE]: 'Warm, relatable, human. Light emojis. Story-driven.', + [ContentStyle.MEME]: 'Funny, meme-ish, punchy. One-liner energy. Reference internet culture. Can use "[X] but actually [Y]" format', + [ContentStyle.EDUCATIONAL]: 'Teach clearly. Structure: hook → insight → takeaway. Use analogies. Avoid jargon unless explained.', + [ContentStyle.GENERAL]: 'Clear, engaging, neutral.', + [ContentStyle.OPINION]: 'First-person opinion. Bold take. "I think / Hot take:". Invites debate.', + [ContentStyle.STORYTELLING]: 'Narrative arc. Hook with tension → build → resolution. Personal or case-study.', + [ContentStyle.THREAD]: 'Thread format. Start with hook tweet. Each point numbered. End with CTA or summary.', +}; + +export const TONE_HINTS_TELEGRAM_BUTTON = { + [ContentTone.PROFESSIONAL]: {text: 'chuyên nghiệp, rõ ràng, đáng tin cậy'}, + [ContentTone.CASUAL]: {text: 'Giản dị,thân thiện'}, + [ContentTone.HYPE]: {text: 'Hype-Hào hứng,tràn đầy năng lượng'}, + [ContentTone.URGENT]: {text: 'urgent'}, + [ContentTone.HUMOROUS]: {text: 'Dí dỏm, hài hước'}, + [ContentTone.INFORMATIVE]: {text: 'Thông tin, chính xác'}, + [ContentStyle.OPINION]: {text: 'Quan điểm táo bạo. \n"Tôi nghĩ / Quan điểm gây tranh cãi:". Mời gọi tranh luận.'}, + [ContentStyle.STORYTELLING]: {text: 'STORYTELLING-Cấu trúc tự sự. Mở đầu bằng sự căng thẳng → phát triển → kết thúc.\n Có thể là câu chuyện cá nhân hoặc nghiên cứu trường hợp.'}, + [ContentStyle.THREAD]: {text: 'THREAD-Bắt đầu bằng một tweet thu hút sự chú ý. \nMỗi điểm được đánh số. \nKết thúc bằng lời kêu gọi hành động (CTA) hoặc tóm tắt.'}, +} +export const TONE_HINTS: Record = { + [ContentTone.PROFESSIONAL]: 'professional, clear, credible', + [ContentTone.CASUAL]: 'casual, friendly', + [ContentTone.HYPE]: 'hyped, energetic', + [ContentTone.URGENT]: 'urgent, attention-grabbing', + [ContentTone.HUMOROUS]: 'witty, humorous', + [ContentTone.INFORMATIVE]: 'informative, factual', + [ContentTone.EMPATHETIC]: 'empathetic, emotionally aware, validating', + [ContentTone.PROVOCATIVE]: 'thought-provoking, slightly controversial, challenges assumptions', + [ContentTone.AUTHORITATIVE]: 'confident, commanding, expert-voice', +}; + +// export const PLATFORM_RULES: Record = { +// [Platform.X]: 'Max 420 chars. 2-5 hashtags. Hook in first line. No markdown.', +// [Platform.FACEBOOK]: '400-800 chars. Can use line breaks. 2-5 hashtags. Engaging hook + CTA.', +// }; +export const PLATFORM_RULES: Record string; +}> = { + [Platform.X]: { + short: 'Max 420 chars. 2-5 hashtags. Hook first line. No markdown.', + detailed: (min, max) => + `Length: ${min}-${max} characters (aim for ~${Math.floor((min + max) / 2)}). ` + + `Use ALL available space to deliver maximum value. ` + + `2-5 hashtags. Strong hook in first line. No markdown.`, + }, + [Platform.FACEBOOK]: { + short: '400-1200 chars. Hook + body + CTA.', + detailed: (min, max) => + `Length: ${min}-${max} characters (aim for ~${Math.floor((min + max) / 2)}). ` + + `Write a FULL post: strong hook → 2-4 paragraphs of value → clear CTA. ` + + `Use line breaks for readability. 2-5 relevant hashtags at end.`, + }, +}; + +/** + * Hướng dẫn structure theo length — QUAN TRỌNG để GPT không viết lan man. + */ +export const LENGTH_STRUCTURE_HINTS: Record = { + [PostLength.SHORT]: { + en: 'Single punchy statement. Hook + 1 key fact. No paragraphs. 1-2 hashtag', + vi: 'Một câu punchy duy nhất. Hook + 1 điểm chính. Không xuống dòng. 1-2 hashtag', + ja: '短く強烈な一文。フック + 要点1つ。段落分けなし。, 1-2ハッシュタグ', + ko: '짧고 강렬한 한 문장. 훅 + 핵심 1개. 단락 없음., 1-2 해시태그', + }, + [PostLength.MEDIUM]: { + en: 'Structure: Hook line → 2-3 key points → brief takeaway. Use line breaks. 2-4 hashtag', + vi: 'Cấu trúc: Câu hook → 2-3 điểm chính → kết luận ngắn. Dùng xuống dòng. 2-4 hashtag', + ja: '構成: フック文 → 要点2-3個 → 短い結論。改行を使う。1-4ハッシュタグ', + ko: '구조: 훅 문장 → 핵심 2-3개 → 짧은 결론. 줄바꿈 사용. 1-4 해시태그', + }, + [PostLength.LONG]: { + en: [ + 'Structure: Strong hook (1 line) → Context (2-3 lines) → 3-4 key points with details → Analysis/implication → CTA or takeaway.', + 'Use line breaks between sections. Numbered or bulleted lists OK.', + 'This is a Premium long-form post — use the space to deliver REAL value, not fluff.', + '2-4 hashtag' + ].join(' '), + vi: [ + 'Cấu trúc: Hook mạnh (1 dòng) → Bối cảnh (2-3 dòng) → 3-4 điểm chính có chi tiết → Phân tích/ý nghĩa → CTA hoặc kết luận.', + 'Dùng xuống dòng giữa các phần. List đánh số hoặc bullet OK.', + 'Đây là long-form post Premium — dùng không gian để truyền tải GIÁ TRỊ THẬT, không nhồi chữ.', + '2-4 hashtag' + ].join(' '), + ja: [ + '構成: 強いフック (1行) → 背景 (2-3行) → 詳細な要点3-4個 → 分析/示唆 → CTAまたは結論。', + 'セクション間は改行。番号付き・箇条書きOK。', + 'これはPremiumのロングフォーム投稿です。スペースを使って本当の価値を届けること。冗長にしない。', + '1-4ハッシュタグ' + ].join(' '), + ko: [ + '구조: 강한 훅 (1줄) → 배경 (2-3줄) → 상세한 핵심 3-4개 → 분석/시사점 → CTA 또는 결론.', + '섹션 간 줄바꿈. 번호/글머리 기호 OK.', + 'Premium 롱폼 게시물 — 공간을 활용해 진짜 가치 전달. 채우기식 금지.', + '1-4 해시태그' + ].join(' '), + }, + [PostLength.EXTENDED]: { + en: 'Mini-article format: headline-style hook → intro paragraph → 4-6 sections with subheadings → conclusion → CTA. Treat as authority-building content. 2-5 hashtag', + vi: 'Format mini-article: hook kiểu tiêu đề → đoạn intro → 4-6 phần có subheading → kết luận → CTA. Coi như nội dung xây dựng authority. 2-5 hashtag', + ja: 'ミニ記事形式: 見出し風フック → 導入段落 → 小見出し付き4-6セクション → 結論 → CTA。オーソリティ構築コンテンツとして扱う。, 2-5ハッシュタグ', + ko: '미니 아티클 형식: 헤드라인형 훅 → 서론 → 소제목 있는 4-6 섹션 → 결론 → CTA. 권위 구축 콘텐츠로 취급. 2-5 해시태그', + }, + [PostLength.ARTICLE]: { + en: 'Full article: title → lede → 6-10 sections → detailed examples → strong conclusion. Write like a journalist or analyst.', + vi: 'Bài viết đầy đủ: tiêu đề → lede → 6-10 phần → ví dụ chi tiết → kết luận mạnh. Viết như nhà báo/analyst.', + ja: '完全な記事: タイトル → リード → 6-10セクション → 詳細な例 → 強い結論。ジャーナリストやアナリストのように書く。, 2-5ハッシュタグ', + ko: '완전한 기사: 제목 → 리드 → 6-10 섹션 → 상세한 예시 → 강한 결론. 기자/애널리스트처럼 작성. 2-5 해시태그', + }, +}; + +/** System prompt cho writer - cực ngắn để tiết kiệm token */ +export function buildWriterSystemPrompt(): string { + return 'You are a social media copywriter. Write ONLY the post content, no explanations, no quotes, no preamble.'; +} + +/** User prompt compact */ +export function buildGenericWriterPrompt(params: WriterPromptParams): { + system: string; + user: string; +} { + // const budget = calculateLengthBudget(ctx.platform, ctx.language); + // + // const targetLanguage = LANGUAGE_NAMES[ctx.language] || LANGUAGE_NAMES['en']; + // const parts = [ + // `[Target Language: ${targetLanguage}] + // IMPORTANT: Your previous answer violated language rules. + // Rewrite strictly in ${targetLanguage} only.`, + // `Platform: ${ctx.platform.toUpperCase()}`, + // `${PLATFORM_RULES[ctx.platform].detailed(budget.minChars, budget.maxChars)}`, + // `Style: ${STYLE_HINTS[ctx.style]}`, + // `Tone: ${TONE_HINTS[ctx.tone]}`, + // `Language: ${LANGUAGE_NAMES[ctx.language] || LANGUAGE_NAMES['en']}`, + // `Topic: ${ctx.topic}`, + // `⚠️ Target length: ${budget.minChars}-${budget.maxChars} characters. Write a substantive post.`, + // ]; + // + // if (ctx.extraInstructions) parts.push(`Extra: ${ctx.extraInstructions}`); + // parts.push('Output: the post only.'); + // if ([ContentStyle.CRYPTO, ContentStyle.FINANCE].includes(ctx.style)) { + // parts.push(`Append disclaimer: “\n ⚠️ This content is for informational purposes only, not financial advice. DYOR.”`) + // } + // return parts.join('\n'); + + + const structure = LENGTH_STRUCTURE_HINTS[params.postLength][params.language]; + + const systemByLang: Record = { + en: 'You are a social media copywriter. Write ONLY the post in English. No preamble. must have hashtag', + cn: '你是社交媒体文案撰写员。请仅用中文撰写帖子正文,无需前言,但必须包含话题标签。', + vi: 'Bạn là copywriter MXH. CHỈ viết bài post bằng Tiếng Việt. Không giải thích. phải có hashtag', + ja: 'ソーシャルメディアのコピーライター。投稿文のみを日本語で出力。, ハッシュタグは必須です。', + ko: '소셜미디어 카피라이터. 게시물만 한국어로 작성. 해시태그를 사용해야 합니다.', + }; + const targetLanguage = LANGUAGE_NAMES[params.language] || LANGUAGE_NAMES['en']; + + const parts = [ + `[Target Language: ${targetLanguage}] + IMPORTANT: Your previous answer violated language rules. + Rewrite strictly in ${targetLanguage} only.`, + `Platform: ${params.platform.toUpperCase()}`, + `Target length: ${params.lengthRange.min}-${params.lengthRange.max} chars (aim ~${params.lengthRange.sweet})`, + `Structure: ${structure}`, + `Style: ${STYLE_HINTS[params.style]}`, + `Tone: ${TONE_HINTS[params.tone]}`, + `${LANGUAGE_LOCK[params.language]}`, + `Topic: ${params.topic}`, + params.extraInstructions ? `Extra: ${params.extraInstructions}` : '', + `Output: the post only.`, + ]; + if ([ContentStyle.CRYPTO, ContentStyle.FINANCE].includes(params.style)) { + parts.push(`Append disclaimer: “\n⚠️ This content is for informational purposes only, not financial advice. DYOR.”`) + } + const user = parts.filter(Boolean).join('\n'); + + return {system: systemByLang[params.language], user}; + +} + +/** Reviewer prompt - ngắn gọn, chỉ trả về JSON */ +export function buildReviewerPrompt(draft: string, platform: Platform, style: ContentStyle, language: string): string { + const targetLanguage = LANGUAGE_NAMES[language] || LANGUAGE_NAMES['en']; + + return [ + `[Output MUST be in ${targetLanguage}. Do NOT translate.`, + `Review this ${platform.toUpperCase()} post (style: ${style}).`, + //`Rules: ${PLATFORM_RULES[platform]}`, + `Fix: grammar, hook strength, length, clarity. Keep voice.`, + `Return ONLY JSON: {"improved":"","notes":""}`, + `---\n${draft}`, + ].join('\n'); +} + +export const BREAKING_NEWS_TEMPLATES: Record = { + en: { + system: 'You are a breaking news writer for X. Write ONLY the post. No preamble, no explanations, no quotes. must have hashtag', + formatHint: '🚨 BREAKING: [headline]\\n\\n[1-2 key facts]\\n\\n[source/impact] \\n\\n[hashtag]', + prefix: '🚨 BREAKING', + }, + cn: { + system: 'You are a breaking news writer for X. Write ONLY the post. No preamble, no explanations, no quotes. must have hashtag', + formatHint: '🚨 BREAKING: [headline]\\n\\n[1-2 key facts]\\n\\n[source/impact] \\n\\n[hashtag]', + prefix: '🚨 BREAKING', + }, + vi: { + system: 'Bạn là biên tập tin nóng cho X. CHỈ viết bài post bằng Tiếng Việt. Không giải thích, không trích dẫn.', + formatHint: '🚨 NÓNG: [tiêu đề]\\n\\n[1-2 thông tin chính]\\n\\n[nguồn/tác động] \\n\\n[hashtag]', + prefix: '🚨 NÓNG', + }, + ja: { + system: 'あなたはX用の速報ライターです。投稿文のみを日本語で出力してください。説明・引用符・前置きは一切不要です。', + formatHint: '🚨【速報】[見出し]\\n\\n[主要な事実1-2点]\\n\\n[情報源/影響] \\n\\n[해시태그.]', + prefix: '🚨【速報】', + }, + ko: { + system: 'X 속보 작성자입니다. 게시물만 한국어로 작성하세요. 설명, 인용부호, 서두 모두 금지.', + formatHint: '🚨 [속보] [헤드라인]\\n\\n[핵심 사실 1-2개]\\n\\n[출처/영향] \\n\\n[해시태그.]', + prefix: '🚨 [속보]', + }, +}; + +export function buildBreakingNewsPrompt(params: WriterPromptParams +): { system: string; user: string } { + const tpl = BREAKING_NEWS_TEMPLATES[params.language]; + // const budget = calculateLengthBudget(params.platform, params.language); + + // const user = [ + // `Raw news content:\n${params.rawContent}`, + // ``, + // `Rewrite for X (280 char max):`, + // `- Format hint: ${tpl.formatHint}`, + // `- Start with 🚨 BREAKING / 速報 / 속보 / NÓNG`, + // `- Keep facts accurate, NO fabrication`, + // `- Urgent tone but not clickbait`, + // `- 1-2 relevant hashtags`, + // params.extraInstructions ? `- Extra: ${params.extraInstructions}` : '', + // ].filter(Boolean).join('\n'); + // const user = [ + // `Raw news content:\n${params.topic}`, + // ``, + // `Rewrite for ${params.platform.toUpperCase()}:`, + // `- ${PLATFORM_RULES[params.platform].detailed(budget.minChars, budget.maxChars)}`, + // `- Format: ${tpl.formatHint}`, + // `- Start with: ${tpl.prefix}`, + // `- Tone: ${TONE_HINTS[params.tone]}`, + // `- ⚠️ Write a COMPLETE post, not a one-liner. Aim for ${budget.minChars}-${budget.maxChars} characters.`, + // `- Keep facts accurate, NO fabrication`, + // `- ${LANGUAGE_LOCK[params.language]}`, + // params.extraInstructions ? `- Extra: ${params.extraInstructions}` : '', + // ``, + // `Output: the post only.`, + // ].filter(Boolean).join('\n'); + + const structure = LENGTH_STRUCTURE_HINTS[params.postLength][params.language]; + + const user = [ + `Raw news content:\n${params.topic}`, + ``, + `Rewrite for ${params.platform.toUpperCase()}:`, + `- Target length: ${params.lengthRange.min}-${params.lengthRange.max} chars (aim ~${params.lengthRange.sweet})`, + `- Structure: ${structure}`, + `- Format: ${tpl.formatHint}`, + `- Start with: ${tpl.prefix}`, + `- Tone: ${TONE_HINTS[params.tone]}`, + `- Keep facts accurate, NO fabrication`, + `- ${LANGUAGE_LOCK[params.language]}`, + params.extraInstructions ? `- Extra: ${params.extraInstructions}` : '', + ``, + `Output: the post only.`, + ].filter(Boolean).join('\n'); + + + return {system: tpl.system, user}; +} diff --git a/src/modules/content-writer/providers/ai-provider.factory.ts b/src/modules/content-writer/providers/ai-provider.factory.ts new file mode 100644 index 0000000..8ab3c78 --- /dev/null +++ b/src/modules/content-writer/providers/ai-provider.factory.ts @@ -0,0 +1,35 @@ +// providers/ai-provider.factory.ts +import {Injectable} from '@nestjs/common'; +import {OpenAIProvider} from './openai.provider'; +import {DeepSeekProvider} from './deepseek.provider'; +import {IAIProvider} from '../interfaces/ai-provider.interface'; +import {GrokProvider} from "./grok.provider"; + +export type ProviderName = 'openai' | 'deepseek' | 'grok'; + +@Injectable() +export class AIProviderFactory { + constructor( + private readonly openai: OpenAIProvider, + private readonly deepseek: DeepSeekProvider, + private readonly grok: GrokProvider, + ) { + } + + get(name: ProviderName): IAIProvider { + switch (name) { + case 'openai': + return this.openai; + case 'deepseek': + return this.deepseek; + case 'grok': + return this.grok; + default: + throw new Error(`Unknown AI provider: ${name}`); + } + } + + getGrok(): GrokProvider { + return this.grok; + } +} diff --git a/src/modules/content-writer/providers/deepseek.provider.ts b/src/modules/content-writer/providers/deepseek.provider.ts new file mode 100644 index 0000000..f090a38 --- /dev/null +++ b/src/modules/content-writer/providers/deepseek.provider.ts @@ -0,0 +1,40 @@ +// providers/deepseek.provider.ts +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import OpenAI from 'openai'; +import { IAIProvider, AIMessage, AICompletionOptions, AICompletionResult } from '../interfaces/ai-provider.interface'; + +/** + * DeepSeek dùng OpenAI-compatible API, giá cực rẻ (~$0.14/1M input). + * Lý tưởng cho reviewer. + */ +@Injectable() +export class DeepSeekProvider implements IAIProvider { + readonly name = 'deepseek'; + private client: OpenAI; + private defaultModel: string; + + constructor(private config: ConfigService) { + this.client = new OpenAI({ + apiKey: this.config.get('DEEPSEEK_API_KEY'), + baseURL: 'https://api.deepseek.com/v1', + }); + this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-chat'); + } + + async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise { + console.log(`DeepSeekProvider_deepseek`); + const model = options.model ?? this.defaultModel; + const res = await this.client.chat.completions.create({ + model, + messages, + temperature: options.temperature ?? 0.6, + max_tokens: options.maxTokens ?? 400, + }); + return { + content: res.choices[0]?.message?.content?.trim() ?? '', + tokensUsed: res.usage?.total_tokens ?? 0, + model, + }; + } +} diff --git a/src/modules/content-writer/providers/grok.provider.ts b/src/modules/content-writer/providers/grok.provider.ts new file mode 100644 index 0000000..0e9b8b0 --- /dev/null +++ b/src/modules/content-writer/providers/grok.provider.ts @@ -0,0 +1,222 @@ +// providers/grok.provider.ts +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import OpenAI from 'openai'; +import { IAIProvider, AIMessage, AICompletionOptions, AICompletionResult } from '../interfaces/ai-provider.interface'; +import {ChatCompletionTool} from "openai/resources/chat/completions/completions"; + +/** + * Grok từ xAI - OpenAI-compatible API. + * Lợi thế: real-time X data, X-native voice. + * Dùng cho: English breaking news + witty replies. + */ +@Injectable() +export class GrokProvider implements IAIProvider { + readonly name = 'grok'; + private readonly logger = new Logger(GrokProvider.name); + private client: OpenAI; + private defaultModel: string; + + constructor(private config: ConfigService) { + // this.client = new OpenAI({ + // apiKey: this.config.get('XAI_API_KEY'), + // baseURL: 'https://api.x.ai/v1', + // }); + this.client = new OpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: this.config.get('OPEN_ROUTER_API_KEY') + }); + // grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn + // this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini'); + this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast'); + } + + async getModelName() { + return this.defaultModel; + } + + async complete( + messages: AIMessage[], + options: AICompletionOptions = {} + + ): Promise { + const model = options.model ?? this.defaultModel; + const res = await this.client.chat.completions.create({ + model, + messages, + temperature: options.temperature ?? 0.8, + max_tokens: options.maxTokens ?? 400, + }); + + return { + content: res.choices[0]?.message?.content?.trim() ?? '', + tokensUsed: res.usage?.total_tokens ?? 0, + model, + }; + } + + async complete2( + messages: AIMessage[], + options: AICompletionOptions = {}, + tools: Array = [], + + ): Promise { + console.log('complete2') + const model = options.model ?? this.defaultModel; + const todayStr = new Date().toISOString().split('T')[0]; + + // @ts-ignore + // @ts-ignore + const res = await this.client.chat.completions.create({ + model, + // Tham số đặc biệt của OpenRouter dành cho Grok + //@ts-ignore + // plugins: [{ id: "web" }], + // "plugins": [{ "id": "x_search" }], // Kích hoạt công cụ tìm kiếm X + + extra_body: { + "plugins": [{ "id": "x_search" }], + "x_search_filter": { + // "from_date": todayStr, // Định dạng: YYYY-MM-DD + "result_type": "recent", // Lấy bài đăng mới nhất (thay vì bài đăng phổ biến) + "count": 5 // Số lượng bài đăng tối đa muốn quét + } + }, + messages, + // tools: [ + // { + // //@ts-ignore + // type: "openrouter:x_search", // Hoặc "openrouter:web_search" tùy theo cấu hình plugin OpenRouter + // } + // ], + // // Tùy chọn: Ép mô hình dùng tìm kiếm + // tool_choice: "auto", + temperature: options.temperature ?? 0.8, + max_tokens: options.maxTokens ?? 2000, + + max_completion_tokens: 2000, + // tools: tools, + // tool_choice: "auto", + //@ts-ignore + // extra_body: { + // search: { + // mode: "on", // hoặc "on" + // return_sources: true + // } + // } + }).catch(err=> { + console.log(err); + throw err; + }); + + return { + content: res.choices[0]?.message?.content?.trim() ?? '', + tokensUsed: res.usage?.total_tokens ?? 0, + model, + }; + } + + /** + * Grok-only: fetch X context về 1 topic (nếu API hỗ trợ live search). + * Trả về trends/context để inject vào prompt của writer khác. + */ + async enrichXContext(topic: string): Promise { + const messages: AIMessage[] = [ + { + role: 'system', + content: 'You have real-time X data. Return concise context only.', + }, + { + role: 'user', + content: `Topic: "${topic}" +Return in 3 bullet points: +- Current trending angle/hashtags on X +- Notable KOL reactions (if any) +- Sentiment (bullish/bearish/mixed) +Max 80 words total.`, + }, + ]; + + const res = await this.complete( + messages, + { temperature: 0.3, maxTokens: 150 }, + ).catch(err =>{ + console.log(err); + console.log(err.message); + throw err; + }); + console.log({res}); + return res.content; + } + + /** + * Grok-only: fetch X context về 1 topic (nếu API hỗ trợ live search). + * Trả về trends/context để inject vào prompt của writer khác. + */ + async searchXContext(topic: string): Promise { + const userPrompt = ` + + Bạn là một hệ thống phân tích tin tức tự động. +Khi người dùng yêu cầu tìm: ${topic}, bạn phải sử dụng công cụ tìm kiếm web để lấy thông tin mới nhất. +DO NOT use outdated knowledge. +DO NOT return any news older than 1 day. + +If no recent news is found, return an empty list. +Yêu cầu nghiêm ngặt về đầu ra: +- Chỉ trả về duy nhất một đối tượng JSON hợp lệ. +- Không được có bất kỳ ký tự, dòng text, giải thích hay markdown nào trước hoặc sau JSON. +- Không được dùng \`\`\`json ... \`\`\`. +- JSON phải đúng cú pháp, dùng dấu nháy kép ("). + + Cấu trúc JSON bắt buộc: +{ + "news": [ + { + "title": "tiêu đề tin", + "summary": "tóm tắt dưới 30 chữ", + "day": "ngày tin tức xuất hiện" + } + ] +} +Chỉ trả về JSON. Không thêm bất kỳ điều gì khác. + `; + const now = new Date(); + const isoDate = now.toISOString().split('T')[0]; // 2026-04-24 + const prompt = ` +Search for REAL current news in ${topic} from the past 24 hours. + +DO NOT use outdated knowledge. +DO NOT return any news older than 1 day. + +If no recent news is found, return an empty list. + +Return EXACTLY 3 items in JSON.`; + const messages: AIMessage[] = [ + { + role: 'system', + content: 'You have real-time X data. You must use search. If you cannot find recent information, return empty results.', + }, + { + role: 'user', + content: userPrompt, +// content: `Topic: "${topic}" +// Return in 3 bullet points: +// - Current trending angle/hashtags on X +// - Notable KOL reactions (if any) +// - Sentiment (bullish/bearish/mixed) +// Max 80 words total.`, + }, + ]; + console.log(messages); + const res = await this.complete( + messages, + { temperature: 0.3, maxTokens: 150 }, + ).catch(err =>{ + console.log(err); + console.log(err.message); + throw err; + }); + console.log(res); + return res.content; + } +} diff --git a/src/modules/content-writer/providers/openai.provider.ts b/src/modules/content-writer/providers/openai.provider.ts new file mode 100644 index 0000000..50e1595 --- /dev/null +++ b/src/modules/content-writer/providers/openai.provider.ts @@ -0,0 +1,64 @@ +// providers/openai.provider.ts +import {Injectable, Logger} from '@nestjs/common'; +import {ConfigService} from '@nestjs/config'; +import OpenAI from 'openai'; +import {IAIProvider, AIMessage, AICompletionOptions, AICompletionResult} from '../interfaces/ai-provider.interface'; +import {OpenRouter} from "@openrouter/sdk"; + +@Injectable() +export class OpenAIProvider implements IAIProvider { + readonly name = 'openai'; + private readonly logger = new Logger(OpenAIProvider.name); + private client: OpenAI; + private defaultModel: string; + + constructor(private config: ConfigService) { + + // const openRouter = new OpenRouter({ + // apiKey: '', + // // defaultHeaders: { + // // 'HTTP-Referer': '', // Optional. Site URL for rankings on openrouter.ai. + // // 'X-OpenRouter-Title': '', // Optional. Site title for rankings on openrouter.ai. + // // }, + // }); + // const completion = await openRouter.chat.send({ + // model: 'openai/gpt-4o-mini', + // messages: [ + // { + // role: 'user', + // content: 'What is the meaning of life?', + // }, + // ], + // stream: false, + // }); + + // this.client = new OpenAI({ apiKey: this.config.get('CHATGPT_API_KEY') }); + this.client = new OpenAI({ + baseURL: 'https://openrouter.ai/api/v1', + apiKey: this.config.get('OPEN_ROUTER_API_KEY') + }); + this.defaultModel = this.config.get('OPENAI_MODEL', 'openai/gpt-4o-mini'); // rẻ + } + + async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise { + console.log(`OpenAIProvider_complete`); + + const model = options.model ?? this.defaultModel; + try { + const res = await this.client.chat.completions.create({ + model, + messages, + temperature: options.temperature ?? 0.7, + max_tokens: options.maxTokens ?? 400, // giới hạn để tiết kiệm + }); + return { + content: res.choices[0]?.message?.content?.trim() ?? '', + tokensUsed: res.usage?.total_tokens ?? 0, + model, + }; + } catch (err) { + console.error(err); + throw err; + } + } +} diff --git a/src/modules/content-writer/services/comment-writer.service.ts b/src/modules/content-writer/services/comment-writer.service.ts new file mode 100644 index 0000000..00bc76d --- /dev/null +++ b/src/modules/content-writer/services/comment-writer.service.ts @@ -0,0 +1,73 @@ +// services/comment-writer.service.ts +import {Injectable, Logger} from '@nestjs/common'; +import {AIProviderFactory} from '../providers/ai-provider.factory'; +import {GenerateCommentDto} from '../dto/generate-comment.dto'; +import {buildCommentPrompt} from '../prompts/comment.templates'; +import {ProviderRouterService} from "./provider-router.service"; + +@Injectable() +export class CommentWriterService { + private readonly logger = new Logger(CommentWriterService.name); + + constructor( + private factory: AIProviderFactory, + private router: ProviderRouterService, + ) { + } + + async generateComment(dto: GenerateCommentDto) { + const decision = this.router.route({ + language: dto.language, + contentType: 'comment', + tone: dto.tone, + }); + + + // GPT-4o-mini là best choice cho comment đa ngôn ngữ + // const provider = this.factory.get('openai'); + + const provider = this.factory.get(decision.writer); + this.logger.log(`==> Comment routing: ${decision.reason} ==>`); + // console.log({dto}) + const {system, user} = buildCommentPrompt({ + originalPost: dto.originalPost, + angle: dto.angle, + language: dto.language, + persona: dto.persona, + tone: dto.tone, + }); + + // console.log({system, user}) + + const res = await provider.complete( + [ + {role: 'system', content: system}, + {role: 'user', content: user}, + ], + { + temperature: 0.9, // cao hơn để tự nhiên + maxTokens: 150, // comment ngắn + }, + ); + + // console.log({res}); + + // Clean output: bỏ quotes nếu AI lỡ wrap + const cleaned = res.content.replace(/^["""']|["""']$/g, '').trim(); + + return { + comment: cleaned, + tokensUsed: res.tokensUsed, + model: res.model, + language: dto.language, + }; + } + + /** + * Generate nhiều variant để bạn chọn + */ + // async generateVariants(dto: GenerateCommentDto, count = 3) { + // const tasks = Array.from({length: count}, () => this.generateComment(dto)); + // return Promise.all(tasks); + // } +} diff --git a/src/modules/content-writer/services/length-strategy.service.ts b/src/modules/content-writer/services/length-strategy.service.ts new file mode 100644 index 0000000..3b0e44c --- /dev/null +++ b/src/modules/content-writer/services/length-strategy.service.ts @@ -0,0 +1,126 @@ +// services/length-strategy.service.ts +import { Injectable } from '@nestjs/common'; +import { ContentStyle } from '../enum/style.enum'; +import { ContentTone } from '../enum/tone.enum'; +import { AccountTier } from '../enum/account-tier.enum'; +import { PostLength } from '../enum/post-length.enum'; +import { Platform } from '../enum/platform.enum'; +import {LENGTH_RANGES, LengthRange, PLATFORM_LIMITS} from "../config/platform-limits"; + +export interface LengthDecision { + postLength: PostLength; + range: LengthRange; + hardLimit: number; + reason: string; +} + +@Injectable() +export class LengthStrategyService { + /** + * Quyết định độ dài tối ưu dựa trên: + * 1. User request (nếu có) + * 2. Content style + tone + * 3. Account tier + * 4. Platform + */ + decide(params: { + platform: Platform; + tier: AccountTier; + style: ContentStyle; + tone: ContentTone; + requestedLength?: PostLength; // user override + }): LengthDecision { + const { platform, tier, style, tone, requestedLength } = params; + const hardLimit = PLATFORM_LIMITS[platform][tier]; + + // 1. User explicit request -> tôn trọng + if (requestedLength) { + return { + postLength: requestedLength, + range: this.capRange(LENGTH_RANGES[requestedLength], hardLimit), + hardLimit, + reason: `User requested: ${requestedLength}`, + }; + } + + // 2. Free account -> luôn SHORT + if (tier === AccountTier.FREE && platform === Platform.X) { + return { + postLength: PostLength.SHORT, + range: LENGTH_RANGES[PostLength.SHORT], + hardLimit: 280, + reason: 'Free account on X: max 280 chars', + }; + } + + // 3. Smart default theo style (Premium/Plus) + const styleDefault = this.styleBasedLength(style, tone); + + return { + postLength: styleDefault, + range: this.capRange(LENGTH_RANGES[styleDefault], hardLimit), + hardLimit, + reason: `${tier} + ${style}/${tone}: ${styleDefault}`, + }; + } + + /** + * Core logic: style + tone -> length. + * Dựa trên best practices của X Premium 2025-2026. + */ + private styleBasedLength(style: ContentStyle, tone: ContentTone): PostLength { + // tone urgent -> short + if(tone === ContentTone.URGENT) { + return PostLength.SHORT; + } + // Breaking news KHẨN CẤP -> vẫn ngắn dù có Premium + // (vì cần viral tốc độ, retweet nhanh) + // @ts-ignore + // if (style === ContentStyle.BREAKING_NEWS && tone === ContentTone.URGENT) { + // return PostLength.SHORT; + // } + + // Breaking news non-urgent -> MEDIUM (có context) + if (style === ContentStyle.BREAKING_NEWS) { + return PostLength.MEDIUM; + } + + // Meme / Humorous -> luôn SHORT + if (style === ContentStyle.MEME || tone === ContentTone.HUMOROUS) { + return PostLength.SHORT; + } + + // Educational / Tech / Finance -> LONG (Premium sweet spot) + if ( + style === ContentStyle.EDUCATIONAL || + style === ContentStyle.TECH || + style === ContentStyle.FINANCE + ) { + return PostLength.LONG; + } + + // Crypto analysis -> MEDIUM to LONG + if (style === ContentStyle.CRYPTO) { + return tone === ContentTone.HYPE ? PostLength.SHORT : PostLength.MEDIUM; + } + + // Lifestyle -> MEDIUM + if (style === ContentStyle.LIFESTYLE) { + return PostLength.MEDIUM; + } + + // Default + return PostLength.MEDIUM; + } + + /** + * Cap range trong hard limit (đảm bảo không vượt platform limit). + */ + private capRange(range: LengthRange, hardLimit: number): LengthRange { + return { + min: Math.min(range.min, hardLimit), + max: Math.min(range.max, hardLimit), + sweet: Math.min(range.sweet, hardLimit), + }; + } +} diff --git a/src/modules/content-writer/services/prompt-builder.service.ts b/src/modules/content-writer/services/prompt-builder.service.ts new file mode 100644 index 0000000..d812854 --- /dev/null +++ b/src/modules/content-writer/services/prompt-builder.service.ts @@ -0,0 +1,32 @@ +// services/prompt-builder.service.ts +import {Injectable} from '@nestjs/common'; +import {ContentContext} from '../interfaces/content-context.interface'; +import {AIMessage} from '../interfaces/ai-provider.interface'; +import { + buildWriterSystemPrompt, + buildGenericWriterPrompt, + buildReviewerPrompt, + buildBreakingNewsPrompt +} from '../prompts/templates'; +import {WriterPromptParams} from "../interfaces/writer-prompt-params.interface"; + +@Injectable() +export class PromptBuilderService { + buildWriterMessages(ctx: WriterPromptParams): AIMessage[] { + console.debug('buildWriterMessages_ctx', ctx); + + const prompts =ctx.style === 'breaking_news' ? buildBreakingNewsPrompt(ctx) : buildGenericWriterPrompt(ctx); + + return [ + {role: 'system', content: prompts.system}, + {role: 'user', content: prompts.user}, + ]; + } + + buildReviewerMessages(draft: string, ctx: ContentContext): AIMessage[] { + return [ + {role: 'system', content: 'You are a strict social media editor. Return ONLY valid JSON.'}, + {role: 'user', content: buildReviewerPrompt(draft, ctx.platform, ctx.style, ctx.language)}, + ]; + } +} diff --git a/src/modules/content-writer/services/provider-router.service.ts b/src/modules/content-writer/services/provider-router.service.ts new file mode 100644 index 0000000..b2c1b28 --- /dev/null +++ b/src/modules/content-writer/services/provider-router.service.ts @@ -0,0 +1,107 @@ +// services/provider-router.service.ts +import { Injectable } from '@nestjs/common'; +import { ContentStyle } from '../enum/style.enum'; +import { ProviderName } from '../providers/ai-provider.factory'; +import {Language} from "../../../common/interfaces/language.prompt.interface"; + +interface ProviderPair { + writer: ProviderName; + reviewer: ProviderName; +} +export type ContentType = 'post' | 'comment'; +interface RoutingDecision { + writer: ProviderName; + reviewer: ProviderName; + useXEnrichment: boolean; + reason: string; +} + +@Injectable() +export class ProviderRouterService { + /** + * Chọn cặp provider tối ưu theo style. + * Logic: style cần creativity -> GPT viết + * style cần accuracy -> DeepSeek viết + */ + // routev0(style: ContentStyle): ProviderPair { + // const map: Record = { + // // Cần hook mạnh, emotion -> GPT viết + // [ContentStyle.BREAKING_NEWS]: { writer: 'openai', reviewer: 'deepseek' }, + // [ContentStyle.MEME]: { writer: 'openai', reviewer: 'deepseek' }, + // [ContentStyle.LIFESTYLE]: { writer: 'openai', reviewer: 'deepseek' }, + // [ContentStyle.CRYPTO]: { writer: 'openai', reviewer: 'deepseek' }, + // + // // Cần accuracy, logic -> DeepSeek viết + // [ContentStyle.TECH]: { writer: 'deepseek', reviewer: 'openai' }, + // [ContentStyle.FINANCE]: { writer: 'deepseek', reviewer: 'openai' }, + // [ContentStyle.EDUCATIONAL]: { writer: 'deepseek', reviewer: 'openai' }, + // + // // Neutral -> cheapest + // [ContentStyle.GENERAL]: { writer: 'deepseek', reviewer: 'deepseek' }, + // }; + // return map[style]; + // } + + /** + * Core routing logic: dựa vào language + contentType + style. + */ + route(params: { + language: Language; + contentType: ContentType; + style?: ContentStyle; + tone?: string; + }): RoutingDecision { + const { language, contentType, style, tone } = params; + + // === ENGLISH === + if (language === 'en') { + // Breaking news EN -> Grok (real-time + X-native) + if (style === ContentStyle.BREAKING_NEWS) { + return { + writer: 'grok', + reviewer: 'deepseek', + useXEnrichment: true, + reason: 'EN breaking news: Grok has real-time X context', + }; + } + + // Comment EN casual/witty -> Grok + if (contentType === 'comment' && tone !== 'professional') { + return { + writer: 'grok', + reviewer: 'deepseek', + useXEnrichment: false, + reason: 'EN casual comment: Grok sounds most human on X', + }; + } + + // Comment EN professional/analyst -> GPT + if (contentType === 'comment' && tone === 'professional') { + return { + writer: 'openai', + reviewer: 'deepseek', + useXEnrichment: false, + reason: 'EN professional comment: GPT more consistent', + }; + } + + // Default EN + return { + writer: 'openai', + reviewer: 'deepseek', + useXEnrichment: false, + reason: 'EN default: GPT reliable', + }; + } + + // === JP / KR / VI === + // GPT thắng áp đảo với non-English + // @ts-ignore + return { + writer: 'openai', + reviewer: 'deepseek', + useXEnrichment: [ContentStyle.BREAKING_NEWS].includes(style!), // breaking news cần phải dùng X tìm cho hay + reason: `${language.toUpperCase()}: GPT superior for non-English`, + }; + } +} diff --git a/src/modules/content-writer/services/quote-writer.service.ts b/src/modules/content-writer/services/quote-writer.service.ts new file mode 100644 index 0000000..089afb9 --- /dev/null +++ b/src/modules/content-writer/services/quote-writer.service.ts @@ -0,0 +1,161 @@ +// services/quote-writer.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { AIProviderFactory } from '../providers/ai-provider.factory'; +import { ProviderRouterService } from './provider-router.service'; +import { LengthStrategyService } from './length-strategy.service'; +import { ReviewerService } from './reviewer.service'; +import { GenerateQuoteDto } from '../dto/generate-quote.dto'; +import { buildQuotePrompt, suggestQuoteType } from '../prompts/quote.templates'; +import { AccountTier } from '../enum/account-tier.enum'; +import { Platform } from '../enum/platform.enum'; +import { ContentStyle } from '../enum/style.enum'; +import { ContentTone } from '../enum/tone.enum'; +import {calculateTokenBudget} from "../../../common/utils/token-calculator"; +import {QuoteType} from "../enum/quote-type.enum"; + +@Injectable() +export class QuoteWriterService { + private readonly logger = new Logger(QuoteWriterService.name); + + constructor( + private factory: AIProviderFactory, + private router: ProviderRouterService, + private lengthStrategy: LengthStrategyService, + private reviewer: ReviewerService, + private config: ConfigService, + ) {} + + async generateQuote(dto: GenerateQuoteDto) { + this.logger.debug(`==> QuoteWriterService_generateQuote`); + // 1. Auto-detect quote type nếu không có + const quoteType = dto.quoteType ?? suggestQuoteType(dto.originalPost, dto.yourAngle); + this.logger.log(`Quote type: ${quoteType}`); + + // 2. Tier & length + const tier = dto.accountTier ?? this.config.get('X_ACCOUNT_TIER', AccountTier.PREMIUM); + + const lengthDecision = this.lengthStrategy.decide({ + platform: Platform.X, + tier, + style: ContentStyle.GENERAL, // quote không map style thông thường + tone: dto.tone ?? ContentTone.CASUAL, + requestedLength: dto.postLength, + }); + + const budget = calculateTokenBudget(lengthDecision.range, dto.language); + + // 3. Router — quote EN ưu tiên Grok (X-native voice), non-EN -> GPT + const providerDecision = this.router.route({ + language: dto.language, + contentType: 'comment', // quote giống comment hơn post về routing + tone: dto.tone, + }); + console.log({lengthDecision,tier, budget,providerDecision}); + // 4. Build prompt + const { system, user } = buildQuotePrompt({ + originalPost: dto.originalPost, + originalAuthor: dto.originalAuthor, + quoteType, + language: dto.language, + tone: dto.tone, + persona: dto.persona, + yourAngle: dto.yourAngle, + lengthRange: lengthDecision.range, + }); + + console.log({providerDecision, system, user}) + + // 5. Generate + const provider = this.factory.get(providerDecision.writer); + const res = await provider.complete( + [ + { role: 'system', content: system }, + { role: 'user', content: user }, + ], + { + temperature: 0.85, // cao để quote có personality + maxTokens: budget.maxTokens, + }, + ); + + let quote = this.cleanOutput(res.content); + let reviewNotes: string | undefined; + let totalTokens = res.tokensUsed; + let modelUsed = res.model; + + // 6. Optional review + if (dto.enableReview) { + const ctx = { + topic: dto.originalPost, + platform: Platform.X, + style: ContentStyle.GENERAL, + tone: dto.tone ?? ContentTone.CASUAL, + language: dto.language, + } as any; + + try { + const reviewed = await this.reviewer.review( + quote, + ctx, + providerDecision.reviewer, + dto.originalPost, + Math.ceil(budget.maxTokens * 1.3), + ); + if (reviewed.languageValid) { + quote = this.cleanOutput(reviewed.improved); + reviewNotes = reviewed.notes; + } + totalTokens += reviewed.tokensUsed; + modelUsed = `${res.model} + ${reviewed.model}`; + } catch (err) { + this.logger.warn('Quote review failed, using draft', err); + } + } + + // // 7. Hard cap + // if (quote.length > lengthDecision.hardLimit) { + // quote = quote.substring(0, lengthDecision.hardLimit); + // } + + return { + quote, + quoteType, + charCount: quote.length, + language: dto.language, + reviewNotes, + tokensUsed: totalTokens, + model: modelUsed, + }; + } + + /** + * Generate nhiều variants để bạn chọn bài hay nhất. + */ + async generateVariants(dto: GenerateQuoteDto, count = 3) { + const tasks = Array.from({ length: count }, () => this.generateQuote(dto)); + return Promise.all(tasks); + } + + /** + * Generate 1 quote cho MỖI quote type — để bạn thấy tất cả góc nhìn. + */ + async generateAllAngles(dto: GenerateQuoteDto) { + const types = Object.values(QuoteType) as QuoteType[]; + const tasks = types.map((t) => + this.generateQuote({ ...dto, quoteType: t }).catch((e) => ({ + quoteType: t, + error: e.message, + })), + ); + return Promise.all(tasks); + } + + private cleanOutput(text: string): string { + return text + .trim() + .replace(/^["""'「『]+|["""'」』]+$/g, '') // bỏ quote wrapping + .replace(/^(Here is|Here's|Quote:|以下|다음은)[^\n]*\n+/i, '') // bỏ AI preamble + .trim(); + } +} diff --git a/src/modules/content-writer/services/reviewer.service.ts b/src/modules/content-writer/services/reviewer.service.ts new file mode 100644 index 0000000..85596a1 --- /dev/null +++ b/src/modules/content-writer/services/reviewer.service.ts @@ -0,0 +1,62 @@ +// services/reviewer.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { AIProviderFactory, ProviderName } from '../providers/ai-provider.factory'; +import { PromptBuilderService } from './prompt-builder.service'; +import { ContentContext } from '../interfaces/content-context.interface'; +import {AIMessage} from "../interfaces/ai-provider.interface"; + +export interface ReviewResult { + improved: string; + notes: string; + tokensUsed: number; + model: string; + languageValid: boolean; + prompt?: AIMessage[]; +} + +@Injectable() +export class ReviewerService { + private readonly logger = new Logger(ReviewerService.name); + + constructor( + private factory: AIProviderFactory, + private promptBuilder: PromptBuilderService, + ) {} + + async review( + draft: string, + ctx: ContentContext, + providerName: ProviderName = 'deepseek', + originalTopic:string, + maxToken:number// rẻ nhất cho review + ): Promise { + console.log('==> ReviewerService_review: '); + const provider = this.factory.get(providerName); + const messages = this.promptBuilder.buildReviewerMessages(draft, ctx); + console.log('==> ReviewerService_review_promp:==> '); + + const res = await provider.complete(messages, { temperature: 0.3, maxTokens: maxToken }); + + // Parse JSON an toàn + const parsed = this.safeParseJson(res.content); + return { + improved: parsed?.improved ?? draft, + notes: parsed?.notes ?? '', + tokensUsed: res.tokensUsed, + model: res.model, + prompt: messages, + languageValid: true, + }; + } + + private safeParseJson(text: string): { improved?: string; notes?: string } | null { + try { + const match = text.match(/\{[\s\S]*\}/); + if (!match) return null; + return JSON.parse(match[0]); + } catch (e) { + this.logger.warn('Reviewer returned non-JSON output'); + return null; + } + } +} diff --git a/src/modules/content-writer/services/style-detector.service.ts b/src/modules/content-writer/services/style-detector.service.ts new file mode 100644 index 0000000..c7e1968 --- /dev/null +++ b/src/modules/content-writer/services/style-detector.service.ts @@ -0,0 +1,69 @@ +// services/style-detector.service.ts +import {Injectable, Logger} from '@nestjs/common'; +import {ContentStyle} from '../enum/style.enum'; +import {ContentTone} from '../enum/tone.enum'; +import {Language} from "../../../common/interfaces/language.prompt.interface"; +import {getLanguageByJSTTime} from "../../../shared/helper"; + +/** + * Heuristic keyword-based detector - 0 token cost. + * Chỉ fallback gọi AI nếu không match gì (optional). + */ +@Injectable() +export class StyleDetectorService { + private readonly logger = new Logger(StyleDetectorService.name); + private readonly styleKeywords: Record = { + [ContentStyle.CRYPTO]: /\b(btc|eth|sol|crypto|token|defi|nft|airdrop|bullish|bearish|pump|dump|web3|dex|memecoin|on-chain|layer2|testnet|mainnet|whitelist|degen|\$[a-z]{2,10})\b/i, + [ContentStyle.BREAKING_NEWS]: /\b(breaking|Tin nhanh|họp báo|cảnh báo|🔴|just in|urgent|announced|report(ed)?|confirms?|leaked)\b/i, + [ContentStyle.TECH]: /\b(ai model|ai tool|ai agent|docker|kubernetes|microservice|claude|cursor|open ai|api|sdk|framework|nestjs|react|python|github|opensource|llm|gpt|model)\b/i, + [ContentStyle.FINANCE]: /\b(stock|dự báo|market|fed|inflation|nasdaq|sp500|s&p500|earnings|ipo|yield|portfolio|hedge|cpi|gdp|quarterly|interest rate|lãi suất|chứng khoán|cổ phiếu|quỹ|bond)\b/i, + [ContentStyle.LIFESTYLE]: /\b(morning|coffee|travel|family|food|recipe|wellness|selfcare|self-care|mindset|routine,grateful|vibe|sunday|weekend|balance)\b/i, + [ContentStyle.MEME]: /\b(lol|lmao|meme|funny|hits different|living rent free|understood the assignment|no cap|fr fr|😂|🤣)\b/i, + [ContentStyle.EDUCATIONAL]: /\b(how to|tutorial|guide|learn|explain|tips?)\b/i, + [ContentStyle.OPINION]: /\b(opinion|hot take|unpopular|i think|my take|controversial|change my mind|fight me|disagree)\b/i, + [ContentStyle.STORYTELLING]: /\b(thread|story time|true story|happened to me|years ago|flashback|let me tell you|here's how)\b/i, + [ContentStyle.THREAD]: /\b(thread| 🧵|a thread|part 1)\b/i, + [ContentStyle.GENERAL]: /.^/, // never match + }; + + private readonly toneKeywords: Record = { + [ContentTone.URGENT]: /\b(urgent|now|🔴|immediately|just dropped|breaking|họp báo|cảnh báo|khẩn|quan trọng| thông báo gấp|alert)\b/i, + [ContentTone.HYPE]: /\b(lfg|huge|massive|insane|alpha|don't miss|🚀|🔥)\b/i, + [ContentTone.HUMOROUS]: /\b(lol|funny|joke|meme|plot twist|not me|understood the assignment|lmao|bruh|no way|bestie)\b/i, + [ContentTone.PROFESSIONAL]: /\b(report|analysis|official|statement)\b/i, + [ContentTone.INFORMATIVE]: /\b(study|data|research|found|shows)\b/i, + [ContentTone.EMPATHETIC]: /\b(feel|feeling|understand|hard|tough|going through|been there|sending love|mental health|burnout|struggle)\b/i, + [ContentTone.PROVOCATIVE]: /\b(change my mind|unpopular opinion|fight me|controversial|hot take|nobody talks about|am i wrong, be honest)\b/i, + [ContentTone.AUTHORITATIVE]: /\b(the truth is|let me be clear|fact:|experience shows|from my experience|data shows|evidence|proven|decades)\b/i, + [ContentTone.CASUAL]: /.^/, + }; + + detectStyle(text: string): ContentStyle { + this.logger.debug('===> styleDetectorService_detectStyle', text); + const scores: Partial> = {}; + for (const [style, regex] of Object.entries(this.styleKeywords)) { + const matches = text.match(new RegExp(regex, 'gi')); + if (matches) scores[style as ContentStyle] = matches.length; + } + const best = Object.entries(scores).sort((a, b) => b[1]! - a[1]!)[0]; + return (best?.[0] as ContentStyle) ?? ContentStyle.GENERAL; + } + + detectTone(text: string): ContentTone { + this.logger.debug('===> styleDetectorService_detectTone', text); + + for (const [tone, regex] of Object.entries(this.toneKeywords)) { + if (regex.test(text)) return tone as ContentTone; + } + return ContentTone.CASUAL; + } + + detectLanguageFromTelegramAutoContent(text: string): Language { + if (/nhật[ _]bản/i.test(text)) return "ja"; + if (/#hàn_quốc/i.test(text)) return "ko"; + + // return getLanguageByJSTTime(); + // + return "en" + } +} \ No newline at end of file diff --git a/src/modules/manager/manager.module.ts b/src/modules/manager/manager.module.ts new file mode 100644 index 0000000..0207245 --- /dev/null +++ b/src/modules/manager/manager.module.ts @@ -0,0 +1,62 @@ +// src/modules/manager/manager.module.ts +import {Module} from '@nestjs/common'; +import {BullModule} from '@nestjs/bullmq'; +import {ManagerService} from './manager.service'; +import {ManagerProcessor} from './manager.processor'; +import {AIService} from "../../shared/ai.service"; +import {BullBoardModule} from "@bull-board/nestjs"; +import {BullMQAdapter} from "@bull-board/api/bullMQAdapter"; +import {GrokProvider} from "../content-writer/providers/grok.provider"; +import {ContentWriterModule} from "../content-writer/content-writer.module"; + +@Module({ + imports: [ + ContentWriterModule, + BullModule.registerQueue( + {name: 'content_writer_queue'}, // Hàng đợi cho AI-B + // Hàng đợi cho AI-B + {name: 'download_video_queue'}, + {name: 'content_writer_completed_queue'},// Hàng đợi cho AI-C + {name: 'manager_task_queue'},// Hàng đợi cho AI-C + {name: 'comment_writer_queue'},// Hàng đợi cho AI-C + {name: 'comment_writer_completed_queue'},// Hàng đợi cho AI-C + ), + BullBoardModule.forFeature( + { + name: 'manager_task_queue', + adapter: BullMQAdapter + }, + { + name: 'content_writer_queue', + adapter: BullMQAdapter + }, + { + name: 'comment_writer_queue', + adapter: BullMQAdapter + }, + { + name: 'download_video_queue', + adapter: BullMQAdapter + }, + { + name: 'content_writer_completed_queue', + adapter: BullMQAdapter + }, + { + name: 'comment_writer_completed_queue', + adapter: BullMQAdapter + }, + ), + ], + providers: [ + ManagerService, + ManagerProcessor, + AIService, + GrokProvider, + // ContentWriterService, + // StyleDetectorService + ], + exports: [ManagerService], +}) +export class ManagerModule { +} diff --git a/src/modules/manager/manager.processor.ts b/src/modules/manager/manager.processor.ts new file mode 100644 index 0000000..0fb9a60 --- /dev/null +++ b/src/modules/manager/manager.processor.ts @@ -0,0 +1,86 @@ +// src/modules/manager/manager.processor.ts +import {OnWorkerEvent, Processor, WorkerHost} from '@nestjs/bullmq'; +import {Job} from 'bullmq'; +import {AIService} from '../../shared/ai.service'; +import {InjectBot} from "nestjs-telegraf"; +import {Context, Telegraf} from "telegraf"; +import {_JsonToStr} from "../../shared/helper"; +import {ContentWriterService} from "../content-writer/content-writer.service"; +import {GenerateContentDto} from "../content-writer/dto/generate-content.dto"; + +@Processor('manager_task_queue') +export class ManagerProcessor extends WorkerHost { + private adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + + constructor( + private aiService: AIService, + private contentWriterService: ContentWriterService, + @InjectBot() private readonly bot: Telegraf, + ) { + super(); + } + + async process(job: Job): Promise { + console.log(`ManagerProcessor ==> process => ${job.name}`); + console.log('job_data', job.data); + + let {rawData, style, telegramChatId} = job.data; + + switch (job.name) { + case 'analyze_news': + // Gọi AI phân tích + let analysis = await this.aiService.analyzeTrend(rawData, style); + // console.log(analysis.result); + + // @ts-ignore + await this.bot.telegram.sendMessage(this.adminChatId, _JsonToStr(analysis.result)); + + if (analysis.is_relevant && analysis.hot_score > 7) { + // Đẩy tiếp sang Queue của AI-B (Facebook) và AI-C (TikTok) + return analysis; + } + return {status: 'skipped'}; + case 'asking': { + // Gọi AI phân tích + const returnQuestion = await this.aiService.askQuestion(rawData); + // console.log(analysis.result); + console.log('telegramChatId:', telegramChatId); + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + returnQuestion.content, + { + parse_mode: 'Markdown', + } + ); + + return {status: 'completed'}; + } + case 'enrich_x_context': { + const dto: GenerateContentDto = { + topic: rawData, + enableReview: true, + language: 'vi', + useXEnrichment: true, + } + const xContent = await this.contentWriterService.generate(dto) + console.log('enrich_x_context:', xContent); + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + xContent.final, + { + parse_mode: 'Markdown', + } + ); + + return {status: 'completed'}; + } + } + } + + @OnWorkerEvent('completed') + async onCompleted(job: Job) { + console.log(`ManagerProcessor ==> onCompleted => ${job.name}`); + //console.log({job}); + + return {status: 'completed'}; + + } +} diff --git a/src/modules/manager/manager.service.ts b/src/modules/manager/manager.service.ts new file mode 100644 index 0000000..2dcd7ea --- /dev/null +++ b/src/modules/manager/manager.service.ts @@ -0,0 +1,269 @@ +// src/modules/manager/manager.service.ts +import {Injectable} from '@nestjs/common'; +import {InjectQueue} from '@nestjs/bullmq'; +import {Queue} from 'bullmq'; +import {QuoteType} from "../content-writer/enum/quote-type.enum"; + +@Injectable() +export class ManagerService { + constructor( + @InjectQueue('content_writer_queue') private fbQueue: Queue, + @InjectQueue('comment_writer_queue') private commentQueue: Queue, + @InjectQueue('download_video_queue') private downloadVideoQueue: Queue, + @InjectQueue('content_writer_completed_queue') private fbContentCompletedQueue: Queue, + @InjectQueue('manager_task_queue') private managerTaskQueue: Queue, + ) { + } + + async handleAnalyzeNewsTrend(question: string) { + await this.managerTaskQueue.add('analyze_news', { + rawData: question, + style: 'analytic' + }, {attempts: 1, backoff: 5000}); + // // 2. Đẩy việc cho AI-C tìm video TikTok + // await this.ttQueue.add('find_video', { + // keyword: trendData.keyword, + // }, {attempts: 2}); + } + + async handleDownloadVideo( + url: string, + telegramChatId: number = 0 + ) { + await this.downloadVideoQueue.add('download', { + url: url, + type: 'video/mp4', + telegramChatId + }, {attempts: 1, backoff: 5000}) + } + + async handleDownloadVideoMulti( + urls: Array, + telegramChatId: number = 0 + ) { + await this.downloadVideoQueue.add('download_multi', { + urls: urls, + telegramChatId + }, {attempts: 1, backoff: 5000}) + } + + async handleDownloadTiktok(url: string, telegramChatId: number = 0) { + await this.downloadVideoQueue.add('download_tiktok', { + url: url, + type: 'video/mp4', + telegramChatId + }, {attempts: 1, backoff: 5000}) + } + + async handleDownloadFacebookReels(url: string, telegramChatId: number = 0) { + await this.downloadVideoQueue.add('facebook_reels', { + url: url, + type: 'video/mp4', + telegramChatId, + sourceType: 'fb' + }, {attempts: 1, backoff: 5000}) + } + + async handleDownloadMp4Url( + url: string, + sourceType = 'x', + telegramChatId: number = 0 + ) { + await this.downloadVideoQueue.add('mp4url', { + url: url, + type: 'video/mp4', + telegramChatId, + sourceType + }, {attempts: 1, backoff: 5000}) + } + + async handleAskQues(question: string, telegramChatId: number = 0) { + await this.managerTaskQueue.add('asking', { + rawData: question, + telegramChatId + }, {attempts: 1, backoff: 5000}); + // // 2. Đẩy việc cho AI-C tìm video TikTok + // await this.ttQueue.add('find_video', { + // keyword: trendData.keyword, + // }, {attempts: 2}); + } + + async handleAskXContext(question: string, telegramChatId: number = 0) { + await this.managerTaskQueue.add('enrich_x_context', { + rawData: question, + telegramChatId + }, {attempts: 1, backoff: 5000}); + // // 2. Đẩy việc cho AI-C tìm video TikTok + // await this.ttQueue.add('find_video', { + // keyword: trendData.keyword, + // }, {attempts: 2}); + } + + async manualTrigger(keyword: string, style = 'general', preferLanguage = 'en') { + // 1. Đẩy việc cho AI-B viết bài FB + const manualtriggerresult = await this.fbQueue.add('generate_post_ver1', { + title: keyword, + style, + language: preferLanguage, + }, { + attempts: 1, + backoff: 5000, + removeOnComplete: true, + removeOnFail: true, + }, + ); + + //console.log(manualtriggerresult); + + return { + needsConfirm: 1, + summary: '', + id: '', + } + } + + async manualTriggerWriteWithReview( + dto: + { + keyword: string, + style: 'general', + language: 'en', + tone: undefined, + postLength: undefined, + telegramChatId: undefined, + }, + ) { + // 1. Đẩy việc cho AI-B viết bài FB + const manualtriggerresult = await this.fbQueue.add('generate_post_ver2', { + title: dto.keyword, + style: dto.style, + language: dto.language, + tone: dto.tone, + postLength: dto.postLength, + enableReview: true, + telegramChatId: dto.telegramChatId, + }, { + attempts: 1, + backoff: 5000, + removeOnComplete: true, + removeOnFail: true, + }, + ); + + //console.log(manualtriggerresult); + + return { + needsConfirm: 1, + summary: '', + id: '', + } + } + + async manualTriggerCommentLinkTwitter(TwitterUrl, preferLanguage = 'en', telegramChatId = '') { + await this.commentQueue.add('generate_comment_twitter', { + url: TwitterUrl, + language: preferLanguage, + telegramChatId, + }, { + attempts: 1, + backoff: 5000, + removeOnComplete: true, + removeOnFail: true, + }, + ); + + //console.log(manualtriggerresult); + + return { + needsConfirm: 1, + summary: '', + id: '', + } + } + + async manualTriggerCommentAsText({ + comtext, + tone, + agle, + language = 'en', + tweetId = undefined, + tweetUrl = undefined, + chatId = undefined, + }) { + await this.commentQueue.add('generate_comment_as_text_twitter', { + comtext, + language, + tone, + agle, + tweetId, + tweetUrl, + telegramChatId: chatId + }, { + attempts: 1, + backoff: 5000, + removeOnComplete: true, + removeOnFail: true, + }, + ); + + //console.log(manualtriggerresult); + + return { + needsConfirm: 1, + summary: '', + id: '', + } + } + + async manualTriggerQuoteLinkTwitter(TwitterUrl, quoteType?: QuoteType, preferLanguage = 'en', telegramChatId = '') { + await this.commentQueue.add('generate_quote_twitter', { + url: TwitterUrl, + quoteType, + language: preferLanguage, + telegramChatId, + }, { + attempts: 1, + backoff: 5000, + removeOnComplete: true, + removeOnFail: true, + }, + ); + + + return { + needsConfirm: 1, + summary: '', + id: '', + } + } + + async manualTriggerQuoteAsTextInput(userTopic, quoteType?: QuoteType, preferLanguage = 'en', telegramChatId = '') { + await this.commentQueue.add('generate_quote_twitter_as_text_input', { + quoteText: userTopic, + quoteType, + language: preferLanguage, + telegramChatId + }, { + attempts: 1, + backoff: 5000, + removeOnComplete: true, + removeOnFail: true, + }, + ); + + + return { + needsConfirm: 1, + summary: '', + id: '', + } + } + + async sendEventFbContentCompletedQueue(pgPostId: number, content?: string): Promise { + // 1. Đẩy việc vao queue completed + // await this.fbContentCompletedQueue.add('generate_post_completed', { + // pgPostId: pgPostId, + // content, + // }, {attempts: 1, backoff: 5000}); + } +} diff --git a/src/modules/scheduler/scheduler.module.ts b/src/modules/scheduler/scheduler.module.ts new file mode 100644 index 0000000..5a10939 --- /dev/null +++ b/src/modules/scheduler/scheduler.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; +import { SchedulerService } from './scheduler.service'; +import { TrendsModule } from '../trends/trends.module'; + +@Module({ + imports: [ScheduleModule.forRoot(), TrendsModule], + providers: [SchedulerService], +}) +export class SchedulerModule {} diff --git a/src/modules/scheduler/scheduler.service.ts b/src/modules/scheduler/scheduler.service.ts new file mode 100644 index 0000000..dc3378d --- /dev/null +++ b/src/modules/scheduler/scheduler.service.ts @@ -0,0 +1,65 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { TrendsService } from '../trends/trends.service'; + +@Injectable() +export class SchedulerService { + private readonly logger = new Logger(SchedulerService.name); + private isCollecting = false; + + constructor(private readonly trendsService: TrendsService) {} + + /** + * Thu thập mỗi 2 giờ + */ + @Cron('0 */2 * * *') + async scheduledCollection() { + //clear before + this.logger.log('🧹 Running daily cleanup...'); + const deleted = await this.trendsService.cleanupOldTrends(2); + this.logger.log(`🧹 Cleanup done: removed ${deleted} old trends`); + + if (this.isCollecting) { + this.logger.warn('Previous collection still running — skipping'); + return; + } + + this.isCollecting = true; + this.logger.log('⏰ Scheduled collection starting...'); + + try { + const result = await this.trendsService.collectAndStore(); + this.logger.log( + `⏰ Scheduled collection done: ${result.stats.afterDedup} items in ${result.stats.durationMs}ms`, + ); + } catch (error) { + this.logger.error(`⏰ Scheduled collection failed: ${error.message}`); + } finally { + this.isCollecting = false; + } + } + + /** + * Cleanup mỗi ngày lúc 3h sáng + */ + @Cron('0 3 * * *') + async scheduledCleanup() { + this.logger.log('🧹 Running daily cleanup...'); + const deleted = await this.trendsService.cleanupOldTrends(3); + this.logger.log(`🧹 Cleanup done: removed ${deleted} old trends`); + } + + /** + * Thu thập lần đầu khi app start (sau 10s delay) + */ + async onApplicationBootstrap() { + setTimeout(async () => { + this.logger.log('🚀 Initial collection on startup...'); + try { + await this.scheduledCollection(); + } catch (error) { + this.logger.error(`Initial collection failed: ${error.message}`); + } + }, 10_000); + } +} diff --git a/src/modules/social-api/dto/create-tweet.dto.ts b/src/modules/social-api/dto/create-tweet.dto.ts new file mode 100644 index 0000000..27b2d99 --- /dev/null +++ b/src/modules/social-api/dto/create-tweet.dto.ts @@ -0,0 +1,16 @@ +// dto/create-tweet.dto.ts +import { IsString, IsOptional, IsArray, MaxLength } from 'class-validator'; + +export class CreateTweetDto { + @IsString() + // @MaxLength(280) + text: string; + + @IsOptional() + @IsArray() + mediaIds?: string[]; + + @IsOptional() + @IsString() + replyToTweetId?: string; +} diff --git a/src/modules/social-api/dto/x-cookie-account.dto.ts b/src/modules/social-api/dto/x-cookie-account.dto.ts new file mode 100644 index 0000000..7a8a691 --- /dev/null +++ b/src/modules/social-api/dto/x-cookie-account.dto.ts @@ -0,0 +1,13 @@ +import {IsOptional, IsString} from "class-validator"; + +export class XCookieAccountDto { + @IsString() + authToken: string; // auth_token cookie + + @IsString() + ct0: string; // ct0 cookie (CSRF token) + + @IsString() + @IsOptional() + proxy?: string; +} diff --git a/src/modules/social-api/facebook.api.ts b/src/modules/social-api/facebook.api.ts new file mode 100644 index 0000000..5619cc4 --- /dev/null +++ b/src/modules/social-api/facebook.api.ts @@ -0,0 +1,38 @@ +// src/modules/social/facebook-api.service.ts +import {Injectable, HttpException, HttpStatus} from '@nestjs/common'; +import axios from 'axios'; + +@Injectable() +export class FacebookApi { + private readonly fbBaseUrl = 'https://graph.facebook.com/v19.0'; + private readonly pageAccessToken = process.env.FB_PAGE_ACCESS_TOKEN; + private readonly pageId = process.env.FB_PAGE_ID; + + async postToPage(content: string, imageUrl?: string) { + // console.log('postToPage==>', content, imageUrl); + try { + let url = `${this.fbBaseUrl}/${this.pageId}/feed`; + let params: any = { + message: content + `\n Disclaimer: For reference only. AI content may have errors. Not liable for inaccuracies or damages. Verify from official sources.`, + access_token: this.pageAccessToken, + }; + + // Nếu có ảnh, chúng ta dùng endpoint /photos + if (imageUrl) { + url = `${this.fbBaseUrl}/${this.pageId}/photos`; + params.url = imageUrl; + } + + const response = await axios.post(url, params); + //response.data= { id: '1010286162176053_122107818902775551' } + return response.data; // Trả về ID bài viết nếu thành công + } catch (error) { + console.log('Lỗi khi đăng bài lên FB'); + console.log(error.message); + throw new HttpException( + error.response?.data || 'Lỗi khi đăng bài lên FB', + HttpStatus.BAD_REQUEST, + ); + } + } +} diff --git a/src/modules/social-api/interfaces/x-cookie.interface.ts b/src/modules/social-api/interfaces/x-cookie.interface.ts new file mode 100644 index 0000000..5bb282e --- /dev/null +++ b/src/modules/social-api/interfaces/x-cookie.interface.ts @@ -0,0 +1,12 @@ +// interfaces/x-cookie.interface.ts +export interface XCookieAccount { + authToken: string; // auth_token cookie + ct0: string; // ct0 cookie (CSRF token) + proxy?: string; // optional proxy per account +} + +export interface TweetResult { + success: boolean; + tweetId?: string; + error?: string; +} diff --git a/src/modules/social-api/publish.page.service.ts b/src/modules/social-api/publish.page.service.ts new file mode 100644 index 0000000..8e25bc0 --- /dev/null +++ b/src/modules/social-api/publish.page.service.ts @@ -0,0 +1,165 @@ +import {HttpException, Injectable} from "@nestjs/common"; +import {PgPostService} from "../../shared/pg.post.service"; +import {FacebookApi} from "./facebook.api"; +import {TwitterClient} from "./twitter.client"; +import {normalizeTagsSingleCashtag} from "../../shared/helper"; +import {XBrowserService} from "./x-browser.service"; +import {XStrategy} from "./x-router.service"; +import {buildXCookies} from "./utils/x-headers.util"; +import {isEmpty} from "lodash"; +import {XCacheService} from "../x-cache/x-cache.service"; + +@Injectable() +export class PublishPageService { + constructor( + private readonly pgPostService: PgPostService, + private readonly facebookApiService: FacebookApi, + private readonly twitterClient: TwitterClient, + private readonly xBrowserService: XBrowserService, + private readonly cacheService: XCacheService, + ) { + } + + async relyX(content: string, tweetId: string) { + //return this.twitterClient.postReply(content, tweetId); + // return this.twitterClient.qoute(content, tweetId); + const tweetUrl = await this.cacheService.getCacheTweetUrlById(tweetId); + if(isEmpty(tweetUrl)) { + throw new HttpException(`Không tìm thấy tweet url từ Id : ${tweetId}`, 500); + } + return this.xBrowserService.postReply( + tweetUrl, + content + ); + } + + async quoteX(content: string, tweetId: string) { + // return this.twitterClient.qoute(content, tweetId); + const tweetUrl = await this.cacheService.getCacheTweetUrlById(tweetId); + if(isEmpty(tweetUrl)) { + throw new HttpException(`Không tìm thấy tweet url từ Id : ${tweetId}`, 500); + } + return this.xBrowserService.quoteTweet( + { + accountId: 'realflashkaze', + cookies: buildXCookies() + }, + tweetUrl, + content + ); + } + + async publishToFacebook(pgPostId: number): Promise { + const post = await this.pgPostService.post({id: 1 * pgPostId}) + if (!post) { + console.log('publishToFacebook:no post was found:', pgPostId); + return 'no post'; + } + + if (!post.content) { + console.log('publishToFacebook:Post with no content:', pgPostId); + return 'no post'; + } + // if (post.style) { + // console.log('Post with no content:', pgPostId); + // return 'no post'; + // } + if (post.status != 'pending') { + console.log('publishToFacebook:This post already change status', pgPostId); + return 'This post already change status: ' + pgPostId; + } + if (post.isFbPostState > 0) { + console.log('publishToFacebook:Already posted', pgPostId); + return 'Already posted on FB: ' + pgPostId; + } + console.log('publishToFacebook=>posting') + + const resultPost = await this.facebookApiService.postToPage(post.content); + + await this.pgPostService.updatePost(pgPostId, { + isFbPostState: 1 + }) + console.log({resultPost}); + console.log('publishToFacebook=>done'); + return resultPost + } + + async publishTwitter(pgPostIdNum: number, XPostProvider = XStrategy.API_ONLY): Promise { + const pgPostId = 1 * pgPostIdNum; + const post = await this.pgPostService.post({id: 1 * pgPostId}) + if (!post) { + console.log('no post was found:', pgPostId); + return 'no post'; + } + + if (!post.content) { + console.log('Post with no content:', pgPostId); + return 'no post'; + } + if (post.status != 'pending') { + console.log('This post already change status', pgPostId); + return 'This post already change status: ' + pgPostId; + } + if (post.isTwitterPostState > 0) { + console.log('Already posted', pgPostId); + return 'Already posted on X: ' + pgPostId; + } + console.log('publishTwitter=>posting'); + const _normalizeTagsSingleCashtagContent = normalizeTagsSingleCashtag(post.content); + + if (XPostProvider === XStrategy.BROWSER_ONLY) { + const resp = await this.xBrowserService.postTweet( + { + accountId: 'realflashkaze', + cookies: buildXCookies() + }, + _normalizeTagsSingleCashtagContent + ) + if (resp.success) { + await this.pgPostService.updatePost(pgPostId, { + isTwitterPostState: 1 + }) + } else { + console.log('publishTwitter=>posting_error', resp.error); + throw new HttpException(resp.error || 'exception', 500); + } + return; + } + + if (XPostProvider === XStrategy.API_ONLY) { + const {data: createdTweet} = await this.twitterClient + .postSimpleTwitte(_normalizeTagsSingleCashtagContent) + .catch(err => { + console.error(err); + const errStatus = err.data.status; + // if (errStatus == '401') { + // this.twitterClient.refreshAccessToken(); + // } + const errText = err.data.title; + const errDetail = err.data.detail; + console.log(`publishTwitter=>posting_err_ ${errText} - ${errStatus} -${errDetail}`) + throw new HttpException(errText, errStatus); + }); + await this.pgPostService.updatePost(pgPostId, { + isTwitterPostState: 1 + }) + + console.log('Tweet', createdTweet.id); + return { + ...createdTweet, + url: `https://x.com/${process.env.TWITTER_USERNAME}/status/${createdTweet.id}` + } + } + } + + async rejectedPost(pgPostIdNum: number): Promise { + const pgPostId = 1 * pgPostIdNum; + + await this.pgPostService.updatePost(pgPostId, { + status: 'rejected', + }) + + return pgPostId; + } + +} diff --git a/src/modules/social-api/social.module.ts b/src/modules/social-api/social.module.ts new file mode 100644 index 0000000..170730e --- /dev/null +++ b/src/modules/social-api/social.module.ts @@ -0,0 +1,30 @@ +// src/modules/manager/manager.module.ts +import {Global, Module} from '@nestjs/common'; +import {PgPostService} from "../../shared/pg.post.service"; +import {PublishPageService} from "./publish.page.service"; +import {FacebookApi} from "./facebook.api"; +import {TwitterClient} from "./twitter.client"; +import {XBrowserService} from "./x-browser.service"; +import {XCookieController} from "./x-cookie.controller"; +import {XRouterService} from "./x-router.service"; +import {XCookieService} from "./x-cookie.service"; + +@Global() +@Module({ + imports: [], + providers: [ + FacebookApi, + PgPostService, + PublishPageService, + TwitterClient, + XBrowserService, + XRouterService, + XCookieService, + + ], + controllers: [XCookieController], + + exports: [PublishPageService, TwitterClient, XBrowserService], +}) +export class SocialModule { +} diff --git a/src/modules/social-api/twitter.client.ts b/src/modules/social-api/twitter.client.ts new file mode 100644 index 0000000..4abbce5 --- /dev/null +++ b/src/modules/social-api/twitter.client.ts @@ -0,0 +1,159 @@ +import {HttpException, Inject, Injectable} from "@nestjs/common"; +import {TUploadableMedia, TwitterApi, UploadMediaV1Params} from 'twitter-api-v2'; +import {TwitterApiAutoTokenRefresher} from '@twitter-api-v2/plugin-token-refresher' + +import {EUploadMimeType, MediaStatusV1Result} from "twitter-api-v2/dist/esm/types"; +import {MediaV2MediaCategory} from "twitter-api-v2/dist/esm/types/v2/media.v2.types"; +import {XCacheService} from "../x-cache/x-cache.service"; + +//oauth2 +// clientId: Rl9BU2xXUHVqWl9rcVdWSWFFYWg6MTpjaQ +// clientSecet: h0WgiG24dAdLC80NWf3u-_X0Wzilm8ejLwO-iZ8nZ7UOG-5G5m +// ==== + +//oath 1.0 Access Token and Secret? +//consumer key tfQh9yhtgx30zvfmTUwQ3o3rh +//comsumer sceret ysQtt8PI3lmEdtWx9oRtC0OpDC7fSwYYSwkE5J4v2NIdEcFUHU +//Access Token 2043937828644536320-JWM8czpnoadaqQYj8Xp62ZCrZuVlBt +//Access Token Secret 9EjIg8E5S44Fw1U84UqGBNPx6T1cHBEh00iC8YffDurMO + + +@Injectable() +export class TwitterClient { + private clientId = process.env.TWITTER_CLIENT_ID + ''; + private clientSecret = process.env.TWITTER_CLIENT_SECRET; + // private consumerAppKey = 'tfQh9yhtgx30zvfmTUwQ3o3rh'; + // private consumerAppSecret = 'ysQtt8PI3lmEdtWx9oRtC0OpDC7fSwYYSwkE5J4v2NIdEcFUHU'; + // private oath10AccessToken = '2043937828644536320-JWM8czpnoadaqQYj8Xp62ZCrZuVlBt'; + // private oath10AccessTokenSecret = '9EjIg8E5S44Fw1U84UqGBNPx6T1cHBEh00iC8YffDurMO'; + + private readonly userClient: any; + + constructor( + @Inject() private cacheService: XCacheService, + ) { + } + + // async getTwitterClient() { + // return new TwitterApi({ + // appKey: this.consumerAppKey, + // appSecret: this.consumerAppSecret, + // // Following access tokens are not required if you are + // // at part 1 of user-auth process (ask for a request token) + // // or if you want a app-only client (see below) + // accessToken: this.oath10AccessToken, + // accessSecret: this.oath10AccessTokenSecret, + // }) + // } + + + async getTwitterClientV2() { + return new TwitterApi( + { + clientId: this.clientId, + clientSecret: this.clientSecret, + }, + ) + } + + async getTwitterClientV2ViaAccessToken() { + const accessToken = await this.getCacheAccessToken() as string; + const refreshToken = await this.getCacheRefreshToken() as string; + // console.log({refreshToken}); + + // @ts-ignore + // @ts-ignore + return new TwitterApi(accessToken, + { + plugins: [ + new TwitterApiAutoTokenRefresher({ + refreshCredentials: { + clientId: '' + this.clientId, + clientSecret: this.clientSecret, + }, + refreshToken, + // Hàm này được gọi tự động khi token được refresh thành công + onTokenUpdate: async (newTokens) => { + console.log('===> Token đã được làm mới:', newTokens); + await this.setCacheRefreshToken('' + newTokens.refreshToken); + await this.setCacheAccessToken(newTokens.accessToken); + }, + // Hàm xử lý khi refresh thất bại (ví dụ: refresh token cũng hết hạn) + onTokenRefreshError: async (error) => { + console.error('Không thể refresh token:', error); + throw error; + }, + }), + ], + }); + } + + async uploadImageV1(media: Buffer, options: { + media_type: `${EUploadMimeType}` | EUploadMimeType; + media_category?: MediaV2MediaCategory; + additional_owners?: string[]; + }, chunkSize?: number): Promise { + const client = await this.getTwitterClientV2ViaAccessToken(); + // return client.v2.uploadMedia(media, options, chunkSize); + return client.v2.uploadMedia(media, options, chunkSize); + } + + async postSimpleTwitte(content) { + const client = await this.getTwitterClientV2ViaAccessToken(); + return client.v2.tweet(content) + } + + async postReply(content: string, tweetId: string) { + const client = await this.getTwitterClientV2ViaAccessToken(); + return client.v2.reply(content, tweetId) + } + + async qoute(content: string, tweetId: string) { + const client = await this.getTwitterClientV2ViaAccessToken(); + + // @ts-ignore + if (content.indexOf(`https://x.com/${process.env.TWITTER_USERNAME}/status`) === -1) { + //hacking, use tweet instead of quote, because limit x + return client.v2.tweet(content) + } + return client.v2.quote(content, tweetId) + } + + async setCacheAccessToken(accessToken: string) { + return this.cacheService.setCachedKey('tw_accesstoken', '' + accessToken, 24 * 3600); + } + + async delCacheAccessToken() { + return this.cacheService.delCachedKey('tw_accesstoken'); + } + + async getCacheAccessToken() { + return this.cacheService.getCachedData('tw_accesstoken') + } + + async getCacheRefreshToken() { + return this.cacheService.getCachedData('tw_refreshtoken') + } + + async setCacheRefreshToken(refreshToken: string) { + //30day + return this.cacheService.setCachedKey('tw_refreshtoken', refreshToken, 30 * 24 * 3600); + } + + async refreshAccessToken() { + const client = new TwitterApi({clientId: this.clientId, clientSecret: this.clientSecret}); + const refreshToken = await this.getCacheRefreshToken(); + + const { + client: refreshedClient, + accessToken, + refreshToken: newRefreshToken + } = await client.refreshOAuth2Token('' + refreshToken); + + await this.cacheService.setCachedKey('tw_accesstoken_time_add', Date.now(), 3 * 24 * 3600); + await this.setCacheAccessToken(accessToken); + await this.setCacheRefreshToken('' + refreshToken); +// Example request + await refreshedClient.v2.me(); + } +} diff --git a/src/modules/social-api/utils/x-headers.util.ts b/src/modules/social-api/utils/x-headers.util.ts new file mode 100644 index 0000000..a2c9718 --- /dev/null +++ b/src/modules/social-api/utils/x-headers.util.ts @@ -0,0 +1,45 @@ +// utils/x-headers.util.ts +export const X_BEARER_TOKEN = + 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'; + +export const X_COOKIE_NAME = { + 'kdt': '2JWxVxk3NZUEjkorxsQE6ilftoYnFQDI8u5IgsBz', + 'auth_token': 'f5574950a0d98cf49ca2e574cc6a4139cc8a2d81', + 'cto': '75db96b40f4814925670722e3335840467b87c7eb403aab14fbe719297bf465347810f92dc2116c3d30f15153a332976c50d856983dfe19046d4f10413b3d1cd8bac6f7a99e5b03c6949bafdad0f01c0' +} + +export function buildXHeaders(authToken: string, ct0: string) { + return { + authority: 'x.com', + accept: '*/*', + 'accept-language': 'en-US,en;q=0.9', + authorization: `Bearer ${X_BEARER_TOKEN}`, + 'content-type': 'application/json', + cookie: `auth_token=${authToken}; ct0=${ct0}; kdt=${X_COOKIE_NAME.kdt}`, + origin: 'https://x.com', + referer: 'https://x.com/home', + host: 'api.x.com', + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.2 Safari/605.1.15', + 'x-csrf-token': ct0, + 'x-twitter-active-user': 'yes', + 'x-twitter-auth-type': 'OAuth2Session', + 'x-twitter-client-language': 'en', + }; +} + +export function buildXCookies(accountId = 'realflashkaze') { + return [ + { + name: "ct0", + value: X_COOKIE_NAME.cto + }, + { + name: "auth_token", + value: X_COOKIE_NAME.auth_token, + }, + {name: "dnt", value: "1"}, + {name: "lang", value: "en"}, + {name: "twid", value: "u%3D2043937828644536320"}, + ]; +} diff --git a/src/modules/social-api/x-browser.service.ts b/src/modules/social-api/x-browser.service.ts new file mode 100644 index 0000000..570d085 --- /dev/null +++ b/src/modules/social-api/x-browser.service.ts @@ -0,0 +1,461 @@ +// src/modules/x-browser/x-browser.service.ts +import { + Injectable, + Logger, + OnModuleInit, + OnModuleDestroy, HttpException, +} from '@nestjs/common'; +import {chromium, Browser, BrowserContext, Page} from 'playwright'; +import {pick, rand} from "../../shared/helper"; +import {buildXCookies} from "./utils/x-headers.util"; + +export interface BrowserAccount { + accountId: string; + cookies: Array<{ + name: string; + value: string; + domain?: string; + path?: string; + }>; + proxy?: string; + userAgent?: string; +} + +export interface BrowserTweetResult { + success: boolean; + tweetId?: string; + error?: string; + needsRelogin?: boolean; +} + +@Injectable() +export class XBrowserService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(XBrowserService.name); + private browser: Browser | null = null; + private contextPool = new Map< + string, + { ctx: BrowserContext; lastUsed: number } + >(); + private readonly MAX_CONTEXTS = 5; + private readonly CONTEXT_TTL_MS = 15 * 60 * 1000; // 15 phút + + async onModuleInit() { + // Lazy launch – chỉ mở khi cần + setInterval(() => this.cleanupStaleContexts(), 60_000); + } + + private async ensureBrowser(): Promise { + if (this.browser && this.browser.isConnected()) return this.browser; + this.logger.log('Launching Chromium...'); + this.browser = await chromium.launch({ + headless: true, + args: [ + '--disable-blink-features=AutomationControlled', + '--no-sandbox', + '--disable-dev-shm-usage', + ], + }); + return this.browser; + } + + private async getOrCreateContext( + account: BrowserAccount, + useCache = true + ): Promise { + console.log('getOrCreateContext:1') + // console.log({account}); + const cached = this.contextPool.get(account.accountId); + if (useCache && cached) { + console.log('getOrCreateContext:cached'); + cached.lastUsed = Date.now(); + return cached.ctx; + } + console.log('getOrCreateContext:2') + + // LRU eviction + if (this.contextPool.size >= this.MAX_CONTEXTS) { + const oldest = [...this.contextPool.entries()].sort( + (a, b) => a[1].lastUsed - b[1].lastUsed, + )[0]; + await oldest[1].ctx.close().catch(() => null); + this.contextPool.delete(oldest[0]); + } + console.log('getOrCreateContext:3') + + const browser = await this.ensureBrowser(); + console.log('getOrCreateContext:4') + + const ctx = await browser.newContext({ + userAgent: + account.userAgent ?? + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + + '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + viewport: {width: 1366, height: 768}, + locale: 'en-US', + proxy: account.proxy ? {server: account.proxy} : undefined, + }); + console.log('getOrCreateContext:5') + + // Anti-detection: ẩn webdriver flag + await ctx.addInitScript(() => { + Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); + }); + console.log('getOrCreateContext:6') + + await ctx.addCookies( + account.cookies.map((c) => ({ + ...c, + domain: c.domain || '.x.com', + path: c.path || '/', + })), + ); + + this.contextPool.set(account.accountId, {ctx, lastUsed: Date.now()}); + console.log('getOrCreateContext:7') + + // console.log({ + // ctx + // }) + return ctx; + } + + async postTweet( + account: BrowserAccount, + text: string, + ): Promise { + let page: Page | null = null; + try { + const ctx = await this.getOrCreateContext(account); + const cookies = account.cookies.map((c) => ({ + ...c, + domain: c.domain || '.x.com', + path: c.path || '/', + })); + await ctx.addCookies(cookies); + page = await ctx.newPage(); + + // Intercept để lấy tweet id từ response + let capturedTweetId: string | undefined; + page.on('response', async (resp) => { + if (resp.url().includes('/CreateTweet')) { + try { + const json = await resp.json(); + capturedTweetId = + json?.data?.create_tweet?.tweet_results?.result?.rest_id; + } catch { + } + } + }); + // await page.keyboard.press('Mở trang ...'); + await page.goto('https://x.com/home', { + waitUntil: 'domcontentloaded', + timeout: 30_000, + }); + await page.waitForTimeout(2000 + (Math.random() + Math.random()) * 3000); + await page.mouse.wheel(0, rand(300, 500)); + await page.waitForTimeout(rand(1000, 4000)); + + // Detect login/challenge screen + if (page.url().includes('/login') || page.url().includes('/flow')) { + return { + success: false, + error: 'Redirected to login', + needsRelogin: true, + }; + } + await page.mouse.wheel(200, rand(300, 800)); + await page.waitForTimeout(rand(2000, 5000)); + await page.evaluate(() => window.scrollTo({top: 0, behavior: 'smooth'})); + await page.waitForTimeout(rand(1000, 2000)); + // page.mouse.move() + // Mở composer + // const composer = page.locator('a[href="/compose/post"]').first(); + // await page.waitForTimeout(2000 + (Math.random()+Math.random()) * 3000); + // await composer.click(); + + const textarea = page.locator('div[data-testid="tweetTextarea_0"]'); + await page.waitForTimeout(2000 + (Math.random() + Math.random()) * 3000); + await textarea.fill(text); + console.log(' Nhập tweet xong ...'); + await page.waitForTimeout(2000 + (Math.random() + Math.random()) * 3000); + await page.waitForTimeout(5000); + + // Chờ nút enable + // const postBtn = page.locator('button[data-testid="tweetButtonInline"]'); + // await postBtn.waitFor({state: 'visible', timeout: 5_000}); + // await postBtn.click(); + // await page.locator('button[data-testid="tweetButtonInline"]').click({ force: true }); + const btn = page.locator('button[data-testid="tweetButtonInline"]'); + const btnBox = await btn.boundingBox(); + console.log(btnBox); + console.log('Nhấn Control+Enter ...'); + + // @ts-ignore + // await page.mouse.click(btnBox?.x + btnBox.width / 2, btnBox.y + btnBox.height / 2); + await page.keyboard.press('Control+Enter'); + console.log('Nhấn Control+Enter done ...'); + await page.waitForTimeout(5000); + + // Chờ request CreateTweet hoàn tất + // await page.waitForResponse( + // (r) => r.url().includes('/CreateTweet') && r.status() === 200, + // {timeout: 15_000}, + // ); + + return {success: true, tweetId: capturedTweetId}; + } catch (err: any) { + this.logger.error(`Browser post failed: ${err.message}`); + // console.error(err); + return {success: false, error: err.message}; + } finally { + await page?.close().catch(() => null); + } + } + + async quoteTweet( + account: BrowserAccount, + tweetUrl: string, + quoteText: string, + ) { + + let ctx = await this.getOrCreateContext(account); + if (ctx.isClosed()) { + console.log('browser is closeed, reopen'); + ctx = await this.getOrCreateContext(account, false); + } + const cookies = account.cookies.map((c) => ({ + ...c, + domain: c.domain || '.x.com', + path: c.path || '/', + })); + console.log('cookies:', cookies); + await ctx.addCookies(cookies); + const page = await ctx.newPage(); + try { + // ===== SAFE GOTO ===== + try { + await page.goto(tweetUrl, {waitUntil: 'domcontentloaded', timeout: 30000}); + } catch (e) { + console.log('❌ Load fail'); + throw e; + } + + await page.waitForTimeout(rand(2000, 4000)); + + // ===== CHECK LOGIN ===== + if (await page.locator('input[name="text"]').count()) { + console.log('❌ Cookie die → bị redirect login'); + + throw new HttpException('❌ Không thấy nút retweet (tweet private?)', 500); + + } + + // ===== SCROLL (giống người thật) ===== + await page.mouse.wheel(0, rand(300, 800)); + await page.waitForTimeout(rand(1000, 5000)); + await page.mouse.wheel(0, rand(300, 800)); + await page.waitForTimeout(rand(4000, 8000)); + await page.evaluate(() => window.scrollTo({top: 0, behavior: 'smooth'})); + await page.waitForTimeout(rand(1000, 2000)); + + + // ===== CLICK RETWEET ===== + let retweetBtn = page.locator('[data-testid="retweet"]'); + + if (!(await retweetBtn.count())) { + console.log('❌ Không thấy nút retweet (tweet private?)'); + throw new HttpException('❌ Không thấy nút retweet (tweet private?)', 500); + } + + await retweetBtn.first().click(); + + await page.waitForTimeout(rand(1000, 2000)); + try { + await page.locator('a[href="/compose/post"]').click({timeout: 2000}); + } catch { + console.log('fallback → click by text'); + await page.locator('a[role="menuitem"]') + .filter({hasText: 'Quote'}) + .click(); + } + // // ===== CLICK QUOTE ===== + // let quoteBtn = page.locator('[data-testid="retweetWithComment"]'); + // + // if (!(await quoteBtn.count())) { + // console.log('❌ Không thấy nút quote'); + // return; + // } + // + // await quoteBtn.first().click(); + + await page.waitForTimeout(rand(2000, 3000)); + + // ===== TYPE LIKE HUMAN ===== + // const content = pick(quoteText); + const content = quoteText; + + const box = page.locator('div[role="textbox"]').first(); +// chọn đúng textbox đang visible +// const box = page.locator('div[role="textbox"]:visible').first(); + + if (!(await box.count())) { + console.log('❌ Không thấy textbox'); + throw new HttpException('❌ Không thấy textbox', 500); + } + +// đợi nó xuất hiện thật sự + await box.waitFor({state: 'visible', timeout: 7000}); + +// scroll nhẹ vào view (tránh bị offscreen) +// await box.scrollIntoViewIfNeeded(); + +// focus trước khi gõ + console.log('focus trước khi gõ') + await box.click({delay: rand(50, 150)}); + + + for (let char of content) { + await box.type(char, {delay: rand(50, 120)}); + } + console.log('gõ quote xong ...') + await page.waitForTimeout(rand(1000, 2000)); + + // ===== POST ===== + let postBtn = page.locator('[data-testid="tweetButton"]'); + console.log('count ...') + + if ((await postBtn.count())) { + console.log('click nút quote ...') + await postBtn.click({timeout: 7000}).catch(async (e) => { + console.log('❌ Nut click khong duoc, thử dùng bàn phím Control+Enter'); + await page.keyboard.press('Control+Enter'); + }); + await page.waitForTimeout(rand(4000, 6000)); + + console.log('✅ Quoted thành công'); + } else { + console.log('❌ Không thấy nút post, gọi Ctr + Enter'); + await page.keyboard.press('Control+Enter'); + await page.waitForTimeout(rand(4000, 6000)); + console.log('✅ Quoted thành công'); + } + return {success: true, error: ''}; + + } catch (err: any) { + this.logger.error(`Browser post quote failed: ${err.message}`); + // console.error(err); + return {success: false, error: `Browser post quote failed: ${err.message}`}; + } finally { + await page?.close().catch(() => null); + } + } + + async postReply(tweetUrl, content) { + if (!content) { + throw new HttpException('Nội dung trả lời không có', 500); + } + + const accountInfo = { + accountId: 'realflashkaze', + cookies: buildXCookies() + }; + + let ctx = await this.getOrCreateContext(accountInfo); + if (ctx.isClosed()) { + console.log('browser is closeed, reopen'); + ctx = await this.getOrCreateContext(accountInfo, false); + } + const cookies = accountInfo.cookies.map((c) => ({ + ...c, + domain: '.x.com', + path: '/', + })); + await ctx.addCookies(cookies); + const page = await ctx.newPage(); + + try { + // limit X + // content = content.slice(0, 280); + + // vào tweet + // ===== SAFE GOTO ===== + try { + await page.goto(tweetUrl, {waitUntil: 'domcontentloaded', timeout: 30000}); + } catch (e) { + console.log('❌ Load fail'); + throw e; + } + + // đợi UI ổn + console.log(`đợi UI ổn...`) + await page.waitForSelector('article', {timeout: 7000}); + + // scroll nhẹ + console.log(`scroll nhẹ ...`) + await page.mouse.wheel(0, 300); + await page.waitForTimeout(1000 + Math.random() * 2000); + + // lấy textbox visible + const box = page.locator('div[role="textbox"]:visible').first(); + + await box.waitFor({state: 'visible', timeout: 7000}); + + // focus + console.log(`box focus ...`) + await box.click(); + + // nhập content (fallback nếu type fail) + try { + await box.fill(''); // clear + await box.type(content, {delay: 30 + Math.random() * 150}); + } catch { + await box.fill(content); + } + console.log(`nhập nội dung xong ...`) + await page.waitForTimeout(800 + Math.random() * 1200); + + // nút reply + const btn = page.locator('[data-testid="tweetButtonInline"]:visible'); + + if (!(await btn.count())) { + console.log('❌ Không thấy nút reply'); + return false; + } + + await btn.click(); + console.log(`nhấn nút gửi ...`) + await page.waitForTimeout(3000); + + console.log('✅ Reply OK'); + return {success: true, error: ''}; + } catch (err) { + this.logger.error(`Browser reply failed: ${err.message}`); + // console.error(err); + return {success: false, error: `Browser reply failed: ${err.message}`}; + } finally { + await page?.close().catch(() => null); + + } + + + } + + private async cleanupStaleContexts() { + const now = Date.now(); + for (const [id, entry] of this.contextPool.entries()) { + if (now - entry.lastUsed > this.CONTEXT_TTL_MS) { + await entry.ctx.close().catch(() => null); + this.contextPool.delete(id); + this.logger.log(`Closed stale context ${id}`); + } + } + } + + async onModuleDestroy() { + for (const {ctx} of this.contextPool.values()) { + await ctx.close().catch(() => null); + } + this.contextPool.clear(); + await this.browser?.close().catch(() => null); + } +} diff --git a/src/modules/social-api/x-cookie.controller.ts b/src/modules/social-api/x-cookie.controller.ts new file mode 100644 index 0000000..1c543b9 --- /dev/null +++ b/src/modules/social-api/x-cookie.controller.ts @@ -0,0 +1,53 @@ +// x-poster.controller.ts +import {Body, Controller, Get, Post} from '@nestjs/common'; +import {CreateTweetDto} from './dto/create-tweet.dto'; +import {XCookieAccountDto} from "./dto/x-cookie-account.dto"; +import {XRouterService, XStrategy} from "./x-router.service"; +import {XCookieService} from "./x-cookie.service"; + +@Controller('x-cookie') +export class XCookieController { + constructor( + private readonly service: XRouterService, + private readonly xCookieService: XCookieService + ) { + } + + @Post('tweet') + async tweet( + @Body() tweet: CreateTweetDto, + ) { + const authToken = 'f5574950a0d98cf49ca2e574cc6a4139cc8a2d81'; + const ct0 = '75db96b40f4814925670722e3335840467b87c7eb403aab14fbe719297bf465347810f92dc2116c3d30f15153a332976c50d856983dfe19046d4f10413b3d1cd8bac6f7a99e5b03c6949bafdad0f01c0'; + return this.service.postTweet( + { + account: { + id: 'realflashkaze', + browser: { + accountId: 'realflashkaze', + cookies: [ + { + name: "ct0", + value: '75db96b40f4814925670722e3335840467b87c7eb403aab14fbe719297bf465347810f92dc2116c3d30f15153a332976c50d856983dfe19046d4f10413b3d1cd8bac6f7a99e5b03c6949bafdad0f01c0' + }, + { + name: "auth_token", + value: 'f5574950a0d98cf49ca2e574cc6a4139cc8a2d81' + }, + {name: "dnt", value: "1"}, + {name: "lang", value: "en"}, + {name: "twid", value: "u%3D2043937828644536320"}, + ] + } + }, + text: tweet.text, + strategy: XStrategy.BROWSER_ONLY + }); + } + + @Get('verify') + verify() { + return this.xCookieService.verifyCookie(); + // return this.xCookieService.verifyCookie('UserByScreenName'); + } +} diff --git a/src/modules/social-api/x-cookie.service.ts b/src/modules/social-api/x-cookie.service.ts new file mode 100644 index 0000000..22157b9 --- /dev/null +++ b/src/modules/social-api/x-cookie.service.ts @@ -0,0 +1,337 @@ +// x-cookie.service.ts +import {Injectable, Logger, BadRequestException} from '@nestjs/common'; +import axios, {AxiosInstance} from 'axios'; +import {HttpsProxyAgent} from 'https-proxy-agent'; +import {CreateTweetDto} from './dto/create-tweet.dto'; +import {XCookieAccount, TweetResult} from './interfaces/x-cookie.interface'; +import {buildXHeaders, X_COOKIE_NAME} from './utils/x-headers.util'; +import {ConfigService} from "@nestjs/config"; +import {parse} from "cookie"; + +interface GraphQLVariables { + [key: string]: any; +} + + +@Injectable() +export class XCookieService { + private readonly logger = new Logger(XCookieService.name); + private readonly client: AxiosInstance; + private userId: string; + private queryID: string; + private screenName: string | null = null; + private TWEET_URL = ''; + // Endpoint GraphQL tạo tweet (queryId có thể đổi theo thời gian) + private readonly CREATE_TWEET_URL = + 'https://x.com/i/api/graphql/bDE2rBtZb3uyrczSZ_pI9g/CreateTweet'; + + constructor(private readonly config: ConfigService) { + // ── Lấy cookie từ env ── + const authToken = X_COOKIE_NAME.auth_token; + const ct0 = X_COOKIE_NAME.cto; + const kdt = X_COOKIE_NAME.kdt; + // const authToken = this.config.get('TWITTER_AUTH_TOKEN'); + // const ct0 = this.config.get('TWITTER_CT0')!; + this.queryID = this.config.get('TWITTER_QUERY_ID') || ''; + + const createQid = this.config.get('TWITTER_CREATE_TWEET_QUERY_ID') || 'Qkq4oPdZYuNB_Qw3TDuFqQ'; + this.TWEET_URL = `https://x.com/i/api/graphql/${createQid}/CreateTweet`; + + + // Bearer token này gần như cố định cho Web App, có thể để default + const bearer = 'AAAAAAAAAAAAAAAAAAAAAF7OAAAAAAAPS6nVJjCEf6gW6rLVnQujDGAh8%3DkQS5VtDfPBTSO89WMK4HvSpSUYshWVio9dNNWQEvwfkGmL7nPF'; + + // Ghép chuỗi cookie để axios gửi kèm request + const cookies = [ + `auth_token=${authToken}`, + `ct0=${ct0}`, + `kdt=${kdt}`, + ].join('; '); + + const cookie = `__cuid=4035196cccd84e129f04429c872518a7; twid=u%3D2043937828644536320; guest_id_ads=v1%3A177685259136534867; guest_id_marketing=v1%3A177685259136534867; __cf_bm=hrKUIMlfUs3DfWLr3eOohfxqagtyPQVnHiLOu.Ogtu0-1777886768.8740897-1.0.1.1-ubUA2SEhNiWFs.uruUZ98qVRHCn_EoEmeKqsXTa1RjeOvd9CTXKkF3Su3mTf4fbr4.GdQPb5QClU0JE2raMBRBL_I6NmM.sqm0hKe5tOuF3AxxVXQb4J5.Dcw9uKpio9; external_referer=padhuUp37zjgzgv1mFWxJ12Ozwit7owX|0|8e8t2xd8A2w%3D; personalization_id="v1_plsX3tTzuhmy/NMuFYA8zg=="; ads_prefs="HBISAAA="; auth_token=f5574950a0d98cf49ca2e574cc6a4139cc8a2d81; ct0=75db96b40f4814925670722e3335840467b87c7eb403aab14fbe719297bf465347810f92dc2116c3d30f15153a332976c50d856983dfe19046d4f10413b3d1cd8bac6f7a99e5b03c6949bafdad0f01c0; dnt=1; guest_id=v1%3A177685259136534867; lang=en; _twpid=tw.1776151761301.87290159275149142; kdt=2JWxVxk3NZUEjkorxsQE6ilftoYnFQDI8u5IgsBz; __eoi=ID=1d23cba9bef5792f:T=1776090662:RT=1776090662:S=AA-AfjYo3lmC9kyBbnzpvdA3zf9a; __gads=ID=72ac119a9545f1cf:T=1776090662:RT=1776090662:S=ALNI_MYl23FAc-out8xXJykggkJnCBrxFQ; __gpi=UID=00001252726e7c96:T=1776090662:RT=1776090662:S=ALNI_MZUcHGy1ZnZJP5-uU2_I8WMY75QUA; cf_clearance=9R0VPss1VjPR85IFC8i9ydboDzfnQFh0pdZMmkgzpZk-1774104987-1.2.1.1-.Io13bhStMnBLS17Vl2TQfMafDfwpglq50K7Jc22v9k0eihCURaZu8dTy1imZyfVCUt227dQvQWVHJY1mSL8wUqmWCX9PIXSPrgcn_4A1qroXXYqpwQ1U0.UhtzIPplj3sCYqsJKnJ_BR981amqOpcpNV9IFIBERk70vIFPU0o5TIOFpkhCddj3Xhb3yxmZ82w3o95m2XVKw0wm_yXEEXypOJTl7Ubn5zYw3520gX_Q; d_prefs=MjoxLGNvbnNlbnRfdmVyc2lvbjoyLHRleHRfdmVyc2lvbjoxMDAw; ph_phc_TXdpocbGVeZVm5VJmAsHTMrCofBQu3e0kN8HGMNGTVW_posthog=%7B%22distinct_id%22%3A%2201991503-adac-7662-9300-2c5ed339def2%22%2C%22%24sesid%22%3A%5B1757078963247%2C%2201991a11-3e32-731b-b131-dd286bdf9c08%22%2C1757078961713%5D%7D`; + + // ── Tạo axios instance với header giả lập browser ── + // this.client = axios.create({ + // timeout: 30000, + // headers: { + // 'authorization': `Bearer ${bearer}`, + // 'content-type': 'application/json', + // 'cookie': cookies, // <-- Cookie đăng nhập + // 'x-csrf-token': ct0, // <-- BẮT BUỘC, không có là lỗi 403 + // 'x-twitter-auth-type': 'OAuth2Session', + // 'x-twitter-client-language': 'en', + // 'x-twitter-active-user': 'yes', + // 'referer': 'https://x.com/home', + // 'user-agent': + // 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', + // }, + // }); + // const cookies = parse(cookie); + // console.log(cookies); + // this.userId = cookies.twid?.split('u=')[1] || ''; + // if (!this.userId) throw new Error('Missing twid cookie (user ID)'); + console.log(`userId=${this.userId}`); + this.client = axios.create({ + baseURL: 'https://x.com/i/api', + headers: { + 'authorization': 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA', + 'x-csrf-token': ct0, + 'x-twitter-auth-type': 'OAuth2Session', + 'x-twitter-active-user': 'yes', + 'x-twitter-client-language': 'en', + 'content-type': 'application/json', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'cookie': cookies, // 'auth_token=xxx; ct0=yyy; ...' full string từ devtools + 'referer': 'https://x.com/compose/tweet', + 'origin': 'https://x.com', + }, + timeout: 10000, + }); + + // Interceptor: log lỗi chi tiết nếu bị 403/404/400 + this.client.interceptors.response.use( + (res) => res, + (err) => { + const status = err.response?.status; + const errors = err.response?.data?.errors; + this.logger.log(`Call Url: ${err.config.url}`) + this.logger.error(`Twitter HTTP ${status}:`, errors || err.message); + return Promise.reject(err); + }, + ); + } + + private buildClient(account: XCookieAccount): AxiosInstance { + const config: any = { + headers: buildXHeaders(account.authToken, account.ct0), + timeout: 20000, + }; + + if (account.proxy) { + const agent = new HttpsProxyAgent(account.proxy); + config.httpsAgent = agent; + config.proxy = false; + } + + const client = axios.create(config); + // Interceptor: log lỗi chi tiết nếu bị 403/404/400 + client.interceptors.response.use( + (res) => res, + (err) => { + const status = err.response?.status; + const errors = err.response?.data?.errors; + this.logger.log(`Call Url: ${err.config.url}`) + this.logger.error(`Twitter HTTP ${status}:`, errors || err.message); + return Promise.reject(err); + }, + ); + return client; + } + + /** + * ĐĂNG TWEET đơn + */ + async createTweet(text: string): Promise<{ id: string; url: string }> { + const payload = { + variables: { + tweet_text: text, + dark_request: false, + media: {media_entities: [], possibly_sensitive: false}, + semantic_annotation_id: [], + }, + features: { + tweets_nudges_moments: true, + tweet_with_visibility_results_prefetch_gql_enabled: true, + longform_notetweets_consumption_enabled: true, + responsive_web_edit_tweet_api_enabled: true, + graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + view_counts_everywhere_api_enabled: true, + interactive_text_enabled: true, + responsive_web_text_conversations_enabled: false, + longform_notetweets_richtext_consumption_enabled: false, + responsive_web_enhance_cards_enabled: false, + }, + }; + + const res = await this.client.post(this.TWEET_URL, payload); + + const result = res.data?.data?.create_tweet?.tweet_results?.result; + if (!result) { + throw new Error(`CreateTweet failed: ${JSON.stringify(res.data)}`); + } + + const tid = result.rest_id; + const screenName = result.core?.user_results?.result?.legacy?.screen_name || 'i'; + return {id: tid, url: `https://x.com/${screenName}/status/${tid}`}; + } + + // async createTweet( + // account: XCookieAccount, + // dto: CreateTweetDto, + // ): Promise { + // const client = this.buildClient(account); + // + // const variables: any = { + // tweet_text: dto.text, + // dark_request: false, + // media: { + // media_entities: (dto.mediaIds ?? []).map((id) => ({ + // media_id: id, + // tagged_users: [], + // })), + // possibly_sensitive: false, + // }, + // semantic_annotation_ids: [], + // disallowed_reply_hashtags: [], + // }; + // + // if (dto.replyToTweetId) { + // variables.reply = { + // in_reply_to_tweet_id: dto.replyToTweetId, + // exclude_reply_user_ids: [], + // }; + // } + // + // const features = { + // communities_web_enable_tweet_community_results_fetch: true, + // c9s_tweet_anatomy_moderator_badge_enabled: true, + // responsive_web_edit_tweet_api_enabled: true, + // graphql_is_translatable_rweb_tweet_is_translatable_enabled: true, + // view_counts_everywhere_api_enabled: true, + // longform_notetweets_consumption_enabled: true, + // responsive_web_twitter_article_tweet_consumption_enabled: true, + // tweet_awards_web_tipping_enabled: false, + // creator_subscriptions_quote_tweet_preview_enabled: false, + // longform_notetweets_rich_text_read_enabled: true, + // longform_notetweets_inline_media_enabled: true, + // rweb_video_timestamps_enabled: true, + // responsive_web_graphql_exclude_directive_enabled: true, + // verified_phone_label_enabled: false, + // freedom_of_speech_not_reach_fetch_enabled: true, + // standardized_nudges_misinfo: true, + // tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true, + // responsive_web_graphql_skip_user_profile_image_extensions_enabled: false, + // responsive_web_graphql_timeline_navigation_enabled: true, + // responsive_web_enhance_cards_enabled: false, + // }; + // + // try { + // const {data} = await client.post(this.CREATE_TWEET_URL, { + // variables, + // features, + // queryId: 'bDE2rBtZb3uyrczSZ_pI9g', + // }); + // + // const tweetId = + // data?.data?.create_tweet?.tweet_results?.result?.rest_id; + // + // if (!tweetId) { + // this.logger.warn('Tweet created but no id found', data); + // return {success: false, error: 'No tweet id returned'}; + // } + // + // return {success: true, tweetId}; + // } catch (err: any) { + // const msg = + // err.response?.data?.errors?.[0]?.message || + // err.message || + // 'Unknown error'; + // this.logger.error(`Create tweet failed: ${msg}`); + // + // if (err.response?.status === 401 || err.response?.status === 403) { + // throw new BadRequestException( + // 'Cookie expired or invalid. Need re-login.', + // ); + // } + // + // return {success: false, error: msg}; + // } + // } + + + /** + * VERIFY COOKIE (FIX lỗi 34): Dùng GraphQL UserByRestId (hash ổn định hơn v1.1) + * @param userHandle Optional: Nếu biết @username + */ + async verifyCookie(userHandle?: string): Promise<{ screen_name: string; name: string }> { + try { + let variables: GraphQLVariables; + let queryId: string; + + // if (userHandle) { + // UserByScreenName (hash hiện tại 2026: extract nếu lỗi) + queryId = 'IGgvgiOx4QZndDHuD3x9TQ'; // PLACEHOLDER → Extract (xem dưới) + variables = { + "screen_name": userHandle, + "withSafetyModeUserFields": true, + "withSuperFollowsUserFields": true + }; + // } + // else { + // // UserByRestId (dùng userId từ twid) + // queryId = 'IGgvgiOx4QZndDHuD3x9TQ'; // Placeholder → Extract + // variables = { "userId": this.userId, "withSafetyModeUserFields": true, "withSuperFollowsUserFields": true }; + // } + + const account = { + authToken: X_COOKIE_NAME.auth_token, + ct0: X_COOKIE_NAME.cto, + } + const client = this.buildClient(account); + const res = await client.get(`/graphql/${this.queryID}/UserByScreenName?variables=%7B%22screen_name%22%3A%22realflashkaze%22%2C%22withSafetyModeUserFields%22%3Atrue%7D&features=%7B%7D`, { // Hoặc UserByScreenName + // features: { + // "rweb_tipjar_consumption_enabled":true, + // "responsive_web_graphql_exclude_directive_enabled":true, + // "verified_phone_label_enabled":false, + // "creator_subscriptions_tweet_preview_api_enabled":true, + // "responsive_web_graphql_timeline_navigation_enabled":true, + // "responsive_web_graphql_skip_user_profile_image_extensions_enabled":false, + // "communities_web_preview_enabled":true + // }, + // Add more features nếu cần (copy từ devtools) + }); + return res.data.data.user; + const user = res.data.data.user; // Hoặc res.data.data.result + this.screenName = user.rest_id ? user.legacy.screen_name : user.screen_name; + this.logger.log(`Verified: @${this.screenName}`); + return {screen_name: this.screenName!, name: user.legacy.name}; + } catch (error: any) { + this.logger.error(`Verify failed: ${error.response?.data?.errors?.[0]?.message || error.message}`); + throw error; + } + } + + + /** + * Gọi Twitter API v1.1 để kiểm tra cookie còn sống. + * Nếu cookie chết sẽ throw lỗi 401/403. + */ + async getCurrentUser(): Promise<{ id: string; screen_name: string; name: string }> { + const url = 'https://api.twitter.com/1.1/account/verify_credentials.json?skip_status=true'; + + const res = await this.client.get(url); + const data = res.data; + + if (!data?.screen_name) { + throw new Error('verify_credentials không trả về screen_name'); + } + + return { + id: data.id_str, + screen_name: data.screen_name, + name: data.name, + }; + } + + /** + * Kiểm tra nhanh: true = sống, false = chết (không throw) + */ + async isCookieAlive(): Promise { + try { + await this.getCurrentUser(); + return true; + } catch { + return false; + } + } +} diff --git a/src/modules/social-api/x-router.service.ts b/src/modules/social-api/x-router.service.ts new file mode 100644 index 0000000..aaea572 --- /dev/null +++ b/src/modules/social-api/x-router.service.ts @@ -0,0 +1,153 @@ +// src/modules/x-router/x-router.service.ts +import {Injectable, Logger} from '@nestjs/common'; +import {XCookieAccount} from "./interfaces/x-cookie.interface"; +import {BrowserAccount, XBrowserService} from "./x-browser.service"; +import {TwitterClient} from "./twitter.client"; +import {XCookieService} from "./x-cookie.service"; + +export enum XStrategy { + COOKIE_FIRST = 'cookie_first', // rẻ nhất → fallback browser → api + API_FIRST = 'api_first', // ổn định nhất → fallback cookie → browser + BROWSER_FIRST = 'browser_first', // khi cần chống bot nặng + COOKIE_ONLY = 'cookie_only', + API_ONLY = 'api_only', + BROWSER_ONLY = 'browser_only', + AUTO = 'auto', // dựa vào health account + BROWSER_API = 'browser_api', + BROWSER_COOKIE='browser_cookie'// khi cần chống bot nặng +} + +export interface UnifiedAccount { + id: string; + api?: { accessToken: string; accessSecret: string; appKey: string; appSecret: string }; + cookie?: XCookieAccount; + browser?: BrowserAccount; +} + +export interface RouterResult { + success: boolean; + tweetId?: string; + via: 'api' | 'cookie' | 'browser'; + attempts: Array<{ method: string; error?: string }>; + error?: string; +} + +@Injectable() +export class XRouterService { + private readonly logger = new Logger(XRouterService.name); + + constructor( + private readonly apiSvc: TwitterClient, + private readonly cookieSvc: XCookieService, + private readonly browserSvc: XBrowserService, + ) { + } + + async verifyCookie(account: XCookieAccount): Promise { + return this.cookieSvc.verifyCookie('UserByScreenName') + } + + async postTweet(params: { + account: UnifiedAccount; + text: string; + strategy?: XStrategy; + }): Promise { + const strategy = params.strategy ?? XStrategy.COOKIE_FIRST; + const chain = this.buildChain(strategy, params.account); + const attempts: RouterResult['attempts'] = []; + + for (const method of chain) { + this.logger.log(`[${params.account.id}] Trying via ${method}`); + const result = await this.execute(method, params.account, params.text); + attempts.push({method, error: result.error}); + + if (result.success) { + return { + success: true, + tweetId: result.tweetId, + via: method, + attempts, + }; + } + + // Nếu lỗi không fallback được (vd: text quá dài) → dừng + if (this.isFatalError(result.error)) { + return { + success: false, + via: method, + attempts, + error: result.error, + }; + } + } + + return { + success: false, + via: chain[chain.length - 1], + attempts, + error: 'All methods failed', + }; + } + + /** Xây chain dựa trên strategy + account capabilities */ + private buildChain( + strategy: XStrategy, + account: UnifiedAccount, + ): Array<'api' | 'cookie' | 'browser'> { + const has = { + api: !!account.api, + cookie: !!account.cookie, + browser: !!account.browser, + }; + + const chains: Record> = { + [XStrategy.BROWSER_API]: ['browser', 'api'], + [XStrategy.COOKIE_FIRST]: ['cookie', 'browser', 'api'], + [XStrategy.API_FIRST]: ['api', 'cookie', 'browser'], + [XStrategy.BROWSER_FIRST]: ['browser', 'cookie', 'api'], + [XStrategy.COOKIE_ONLY]: ['cookie'], + [XStrategy.API_ONLY]: ['api'], + [XStrategy.BROWSER_ONLY]: ['browser'], + [XStrategy.BROWSER_COOKIE]: ['browser', 'cookie'], + [XStrategy.AUTO]: ['cookie', 'browser', 'api'], // có thể dựa health store + }; + + return chains[strategy].filter((m) => has[m]); + } + + private async execute( + method: 'api' | 'cookie' | 'browser', + account: UnifiedAccount, + text: string, + ): Promise<{ success: boolean; tweetId?: string; error?: string }> { + try { + if (method === 'api' && account.api) { + const {data: r} = await this.apiSvc.postSimpleTwitte(text); + return { + tweetId: r.id, + success: true, + } + } + if (method === 'cookie' && account.cookie) { + // return await this.cookieSvc.createTweet(account.cookie, {text}); + } + if (method === 'browser' && account.browser) { + return await this.browserSvc.postTweet(account.browser, text); + } + return {success: false, error: `Method ${method} not configured`}; + } catch (e: any) { + return {success: false, error: e.message}; + } + } + + private isFatalError(error?: string): boolean { + if (!error) return false; + const fatalPatterns = [ + /duplicate/i, + /too long/i, + /forbidden content/i, + /suspended/i, + ]; + return fatalPatterns.some((p) => p.test(error)); + } +} diff --git a/src/modules/sqs-module/sqs.module.ts b/src/modules/sqs-module/sqs.module.ts new file mode 100644 index 0000000..36ed148 --- /dev/null +++ b/src/modules/sqs-module/sqs.module.ts @@ -0,0 +1,10 @@ +// sqs.module.ts +import { Module, Global } from '@nestjs/common'; +import { SqsService } from './sqs.service'; + +@Global() +@Module({ + providers: [SqsService], + exports: [SqsService], +}) +export class SqsModule {} diff --git a/src/modules/sqs-module/sqs.post.service.ts b/src/modules/sqs-module/sqs.post.service.ts new file mode 100644 index 0000000..0f698de --- /dev/null +++ b/src/modules/sqs-module/sqs.post.service.ts @@ -0,0 +1,49 @@ +// post.service.ts +import {Injectable} from '@nestjs/common'; +import {SQS_QUEUES_NAME, SqsService} from './sqs.service'; + + +@Injectable() +export class SqsPostService { + constructor(private readonly sqs: SqsService) { + } + + async sendMessage(username: string, data) { + console.log(`SqsPostService -> sendMessage ${username} `); + switch (username) { + case 'realflashkaze': { + return this.postFlashKaze(data) + break; + } + case 'echcomvuive': { + return this.postEchCom(data) + break; + } + } + } + + async postFlashKaze(data: any): Promise { + console.log(`SqsPostService_postFlashKaze`) + return this.sqs.enqueue( + SQS_QUEUES_NAME.REALFLASHKAZE!, + data, + { + jobId: `acc1-${Date.now()}`, + delaySeconds: Math.floor(Math.random() * 30), + }, + ); + } + + async postEchCom(data: any) { + console.log(`SqsPostService_postEchCom`) + + await this.sqs.enqueue( + SQS_QUEUES_NAME.ECHCOMVUIVE!, + data, + { + jobId: `acc2-${Date.now()}`, + delaySeconds: Math.floor(Math.random() * 30), + }, + ); + } +} diff --git a/src/modules/sqs-module/sqs.service.ts b/src/modules/sqs-module/sqs.service.ts new file mode 100644 index 0000000..3df8138 --- /dev/null +++ b/src/modules/sqs-module/sqs.service.ts @@ -0,0 +1,152 @@ +// sqs.service.ts +import { + SQSClient, + SendMessageCommand, + ReceiveMessageCommand, + DeleteMessageCommand, + CreateQueueCommand, + GetQueueUrlCommand, +} from '@aws-sdk/client-sqs'; +import {Injectable, Logger, OnModuleInit} from '@nestjs/common'; + +export const SQS_QUEUES_NAME = { + REALFLASHKAZE: 'q_realflashkaze', + ECHCOMVUIVE: 'q_echcomvuive' +} + +@Injectable() +export class SqsService implements OnModuleInit { + private readonly logger = new Logger(SqsService.name); + private client: SQSClient; + + private baseUrl = process.env.SQS_ENDPOINT + '000000000000'; + + // // 👉 define tất cả queue ở đây + // private queues = [ + // 'post-acc1', + // 'post-acc2', + // ]; + + constructor() { + this.client = new SQSClient({ + region: 'elasticmq', + endpoint: process.env.SQS_ENDPOINT, + credentials: { + accessKeyId: 'x', + secretAccessKey: 'x', + }, + }); + } + + async onModuleInit() { + this.logger.log('Initializing queues...'); + for (const name of Object.values(SQS_QUEUES_NAME)) { + await this.ensureQueue(name); + } + this.logger.log('All queues ready ✅'); + } + + getQueueUrl(name: string) { + return `${this.baseUrl}/${name}`; + } + + private async ensureQueue(name: string) { + try { + await this.client.send(new CreateQueueCommand({ + QueueName: name, + })); + + this.logger.log(`Queue ensured: ${name}`); + } catch (err) { + this.logger.error(`Failed to ensure queue ${name}`, err); + } + } + + // private async ensureQueue(name: string) { + // try { + // await this.client.send(new GetQueueUrlCommand({ + // QueueName: name, + // })); + // + // this.logger.log(`Queue exists: ${name}`); + // } catch (err) { + // this.logger.warn(`Queue missing → creating: ${name}`); + // + // await this.client.send(new CreateQueueCommand({ + // QueueName: name, + // Attributes: { + // VisibilityTimeout: '60', + // MessageRetentionPeriod: '86400', // 1 ngày + // }, + // })); + // + // this.logger.log(`Queue created: ${name}`); + // } + // } + + // ===================== + // Core APIs + // ===================== + + async enqueue(name: string, data: any, opts?: { + delaySeconds?: number; + jobId?: string; + }) { + const queueUrl = this.getQueueUrl(name); + + // console.log(`QueueUrl: ${queueUrl}`); + + const body = { + ...data, + _jobId: opts?.jobId, + _ts: Date.now(), + }; + try { + const data = await this.client.send(new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify(body), + DelaySeconds: opts?.delaySeconds || 0, + })) + console.log("Gửi thành công! MessageId:", data.MessageId); + } catch (err) { + console.error("Lỗi khi gửi tin nhắn:", err.message); + throw err; + } + // return this.client.send(new SendMessageCommand({ + // QueueUrl: queueUrl, + // MessageBody: JSON.stringify(body), + // DelaySeconds: opts?.delaySeconds || 0, + // })).then(()=> { + // this.logger.log(`Queue enqueued: ${name}`); + // }); + } + + async receive(name: string) { + const queueUrl = this.getQueueUrl(name); + + const res = await this.client.send(new ReceiveMessageCommand({ + QueueUrl: queueUrl, + MaxNumberOfMessages: 1, + WaitTimeSeconds: 10, + VisibilityTimeout: 60, + })); + + if (!res.Messages?.length) return null; + + const msg = res.Messages[0]; + + return { + raw: msg, + body: JSON.parse(msg.Body!), + }; + } + + async ack(name: string, receiptHandle: string) { + const queueUrl = this.getQueueUrl(name); + + await this.client.send(new DeleteMessageCommand({ + QueueUrl: queueUrl, + ReceiptHandle: receiptHandle, + })); + } +} diff --git a/src/modules/tele-grammY/decorator/inject.grammy.bot.ts b/src/modules/tele-grammY/decorator/inject.grammy.bot.ts new file mode 100644 index 0000000..4ea8393 --- /dev/null +++ b/src/modules/tele-grammY/decorator/inject.grammy.bot.ts @@ -0,0 +1,3 @@ +export const InjectGrammyBot = () => { + +} diff --git a/src/modules/tele-grammY/tele-grammY.module.ts b/src/modules/tele-grammY/tele-grammY.module.ts new file mode 100644 index 0000000..7f391be --- /dev/null +++ b/src/modules/tele-grammY/tele-grammY.module.ts @@ -0,0 +1,51 @@ +import {DynamicModule, Global, Module, OnModuleInit} from "@nestjs/common"; +import {Bot} from "grammy"; + +@Global() +@Module({ + imports: [], + providers: [ + { + provide: 'TELEGRAM_GRAMMY_BOT', + useFactory: () => { + // const bot = new Bot("" + process.env.TELEGRAM_BOT_V2_TOKEN); + // console.log("✅ ✅ TeleGrammY Bot initialized"); + + // perform any setup like bot.api.setMyCommands(...) + // Start the bot. + // bot.start().then(() => { + // console.log("✅ ✅ TeleGrammY Bot Started"); + // }); + // + // + // return bot; + }, + }, + ], +}) +export class TeleGrammYModule implements OnModuleInit { + + onModuleInit(): any { + + console.log("✅ ✅ TeleGrammY Module initialized"); + // Start the bot. + // this.bot.start(); + } + + + // static forRootAsync(options: any): DynamicModule { + // return { + // module: TeleGrammYModule, + // imports: options.imports || [], + // providers: [ + // { + // provide: 'MY_OPTIONS', + // useFactory: options.useFactory, + // inject: options.inject || [], + // }, + // MyService, + // ], + // exports: [MyService], + // }; + // } +} diff --git a/src/modules/tele-grammY/tele-grammY.service.ts b/src/modules/tele-grammY/tele-grammY.service.ts new file mode 100644 index 0000000..30fcd19 --- /dev/null +++ b/src/modules/tele-grammY/tele-grammY.service.ts @@ -0,0 +1,11 @@ +import {Inject, Injectable} from "@nestjs/common"; +import {Bot} from "grammy"; + +@Injectable() +export class TeleGrammYService { + constructor(@Inject('TELEGRAM_GRAMMY_BOT') bot: Bot) { + } + + // @Command() + +} diff --git a/src/modules/telegram/guard/auth.guard.ts b/src/modules/telegram/guard/auth.guard.ts new file mode 100644 index 0000000..d7b1e7f --- /dev/null +++ b/src/modules/telegram/guard/auth.guard.ts @@ -0,0 +1,24 @@ +// auth.guard.ts +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { TelegrafExecutionContext } from 'nestjs-telegraf'; + +@Injectable() +export class AuthGuard implements CanActivate { + // Danh sách ID được phép (có thể để trong file .env) + private readonly allowedIds =(process.env.TELEGRAM_ALLOW_CHAT_IDS || '').split(','); + + canActivate(context: ExecutionContext): boolean { + console.log('AuthGuard'); + const ctx = TelegrafExecutionContext.create(context); + const { chat } = ctx.getContext(); + + // Kiểm tra nếu chat_id nằm trong danh sách cho phép + const isAllowed = this.allowedIds.includes(chat.id); + + if (!isAllowed) { + console.warn(`Truy cập trái phép từ ID: ${chat.id}`); + } + + return isAllowed; + } +} diff --git a/src/modules/telegram/telegram.constants.ts b/src/modules/telegram/telegram.constants.ts new file mode 100644 index 0000000..29ab1db --- /dev/null +++ b/src/modules/telegram/telegram.constants.ts @@ -0,0 +1,4 @@ +export const WIZARD_WRITER_SCENE_ID = 'WIZARD_WRITER_SCENE_ID'; +export const WIZARD_COMMENT_SCENE_ID = 'WIZARD_COMMENT_SCENE_ID'; +export const WIZARD_COMMENT2_SCENE_ID = 'WIZARD_COMMENT2_SCENE_ID'; +export const WIZARD_QUOTE_SCENE_MAIN = 'WIZARD_QUOTE_SCENE_MAIN'; diff --git a/src/modules/telegram/telegram.content.writer.completed.processor.ts b/src/modules/telegram/telegram.content.writer.completed.processor.ts new file mode 100644 index 0000000..faa9379 --- /dev/null +++ b/src/modules/telegram/telegram.content.writer.completed.processor.ts @@ -0,0 +1,110 @@ +// src/modules/telegram/telegram.processor.ts +import {OnWorkerEvent, Processor, WorkerHost} from '@nestjs/bullmq'; +import {Job} from 'bullmq'; +import {InjectBot} from 'nestjs-telegraf'; +import {Context, Telegraf} from 'telegraf'; +import {Injectable} from "@nestjs/common"; +import {PublishPageService} from "../social-api/publish.page.service"; +import {isEmpty, toNumber} from "lodash"; + +@Injectable() +@Processor('content_writer_completed_queue') // Lắng nghe hàng đợi của AI-B +export class TelegramContentWriterCompletedProcessor extends WorkerHost { + constructor( + @InjectBot() private readonly bot: Telegraf, + private readonly publishPageService: PublishPageService + ) { + super(); + } + + // Đây là logic xử lý task (nếu cần xử lý thêm ở tầng telegram) + async process(job: Job): Promise { + // Task viết bài đã được AI-B xử lý ở FacebookProcessor + // Ở đây chúng ta có thể thực hiện các logic hậu kỳ nếu muốn + console.log('TelegramProcessor_facebook_content_writer_completed_process ==> begin'); + // console.log({job}); + + const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + + console.log('TelegramProcessor_facebook_content_writer_completed_process=>posting') + //console.log(fbContent); + if (job.name === 'generate_post_completed') { + const {id, needConfirm, content, autoPublish, telegramChatId, xSubmitProvider} = job.data; + console.log({id, needConfirm, autoPublish, xSubmitProvider}) + const X_USERS = process.env.TWITTER_USERNAMES!.split(','); + console.log({X_USERS}); + //console.log({job}) + // if (autoPublish) { + // // await this.publishPageService.publishTwitter(id); + // // await this.publishPageService.publishToFacebook(id); + // const postId = toNumber(id); + // await this.bot.telegram.sendMessage(adminChatId, '🤖Đã viết xong tin auto:'); + // + // try { + // await Promise.allSettled([ + // this.publishPageService.publishToFacebook(postId).then(() => { + // this.bot.telegram.sendMessage(adminChatId, `Đăng bài lên FB số ${postId} thành công`); + // }).catch(err => { + // this.bot.telegram.sendMessage(adminChatId, `PublishToFacebook error ${err.message}`); + // + // }), + // this.publishPageService.publishTwitter(postId, xSubmitProvider).then((resp) => { + // this.bot.telegram.sendMessage(adminChatId, `Đăng bài lên X số ${postId} thành công via ${xSubmitProvider}, ${resp?.url}`); + // }).catch(err => { + // this.bot.telegram.sendMessage(adminChatId, `PublishToX error ${err.message}`); + // }), + // ] + // ) + // } catch (e) { + // await this.bot.telegram.sendMessage(adminChatId, `❌ Lỗi đăng bài: ${e.message}`); + // } + // return; + // } + await this.bot.telegram.sendMessage(telegramChatId || adminChatId, '🤖Đã viết xong:'); + await this.bot.telegram.sendMessage(telegramChatId || adminChatId, + `${content}\n\n`, + { + // parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + ...X_USERS.map((xuser) => { + return [{ + text: `↗️X ${xuser}`, + callback_data: `publish-post_twitter_${id}_${xuser}` + }]; + }), + [ + // {text: "↗️X", callback_data: `publish-post_twitter_${id}`}, + {text: "↗️FB", callback_data: `publish-post_facebook_${id}`}, + ], + [ + // {text: "↗️All", callback_data: `publish-post_all_${id}`}, + {text: "🗑️ Hủy bài", callback_data: `delete-post_${id}`}] + ] + } + } + ); + } + return {}; + + //return fbContent; + // const postFbId= await this.publishPageService.publishToFacebook(id) + // const twId= await this.publishPageService.publishTwitter(id) + // return job; + return { + //id: id, + // postFbId, + // twId, + // content: postContent, + //image: imageSuggestion, + status: 'posted' + }; + } + + @OnWorkerEvent('failed') + async onFailed(job: Job, error: Error) { + const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + const telegramChatId = job.data.telegramChatId + await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `❌ **Lỗi AI-B:** Job #${job.id} thất bại. \nLý do: ${error.message}`); + } +} diff --git a/src/modules/telegram/telegram.module.ts b/src/modules/telegram/telegram.module.ts new file mode 100644 index 0000000..808d052 --- /dev/null +++ b/src/modules/telegram/telegram.module.ts @@ -0,0 +1,70 @@ +import {Module, OnModuleInit} from '@nestjs/common'; +import {TelegramContentWriterCompletedProcessor} from "./telegram.content.writer.completed.processor"; +import {TelegramUpdates} from "./telegram.updates"; +import {ManagerModule} from "../manager/manager.module"; +import {PgPostService} from "../../shared/pg.post.service"; +import {PublishPageService} from "../social-api/publish.page.service"; +import {FacebookApi} from "../social-api/facebook.api"; +import {SocialModule} from "../social-api/social.module"; +import {InjectBot} from "nestjs-telegraf"; +import {Context, Telegraf} from "telegraf"; +import {XReaderService} from "../x-reader/x-reader.service"; +import {TrendsService} from "../trends/trends.service"; +import {CollectorModule} from "../collector/collector.module"; +import {WriterWizard} from "./wizard/writer.wizard"; +import {CommentWizard} from "./wizard/comment.wizard"; +import {QuoteWizard} from "./wizard/quote.wizard"; +import {XImageUploadService} from "../x-uploader/x-image.upload.service"; +import {Comment2Wizard} from "./wizard/comment2.wizard"; +import {SqsPostService} from "../sqs-module/sqs.post.service"; + + +@Module({ + imports: [ + ManagerModule, + SocialModule, + CollectorModule, + ], + providers: [ + TelegramUpdates, + TelegramContentWriterCompletedProcessor, + FacebookApi, + PgPostService, + PublishPageService, + XReaderService, + TrendsService, + WriterWizard, + CommentWizard, + QuoteWizard, + Comment2Wizard, + XImageUploadService, + SqsPostService + ], +}) +export class TelegramModule implements OnModuleInit{ + constructor(@InjectBot() private readonly bot: Telegraf) {} + + async onModuleInit() { + try { + await this.bot.telegram.setMyCommands( [ + { command: 'start', description: 'Khởi động bot' }, + { command: 'help', description: 'Trợ giúp' }, + { command: 'trendstat', description: 'trend statistic' }, + { command: 'analyzenews', description: 'Tìm trend' }, + { command: 'ask', description: 'Hoi AI' }, + { command: 'write', description: 'Version mới, dùng 2 AI, 1 viết và 1 reviewer' }, + { command: 'write_', description: 'Version mới, dùng 2 AI, 1 viết và 1 reviewer' }, + { command: 'writecheap', description: 'Version cũ, dùng deepseek' }, + { command: 'xreader', description: 'Lấy nội dung băng api' }, + { command: 'xreader_browser', description: 'Lấy nội dung bằng trình duyệt' }, + { command: 'collectrss', description: 'Lấy nội dung' }, + { command: 'comment', description: 'Viet reply X' }, + { command: 'comen', description: '/com[en,ja,vi,ko] comment as text' }, + ]); + console.log('✅ Đã cập nhật Menu commands thành công'); + } catch (error) { + console.error('❌ Lỗi khi cập nhật Menu:', error); + } + } +} + diff --git a/src/modules/telegram/telegram.updates.ts b/src/modules/telegram/telegram.updates.ts new file mode 100644 index 0000000..9ed6a5a --- /dev/null +++ b/src/modules/telegram/telegram.updates.ts @@ -0,0 +1,813 @@ +import {Action, Command, Ctx, On, Start, Update} from 'nestjs-telegraf'; +import {Context, Scenes} from 'telegraf'; +import {HttpException, Injectable} from "@nestjs/common"; +import {ManagerService} from "../manager/manager.service"; +import {PublishPageService} from "../social-api/publish.page.service"; +import {_toNum} from "../../shared/helper"; +import {XReaderService} from "../x-reader/x-reader.service"; +import {TrendsService} from "../trends/trends.service"; +import {get, isEmpty} from 'lodash' +import { + WIZARD_COMMENT2_SCENE_ID, + WIZARD_COMMENT_SCENE_ID, + WIZARD_QUOTE_SCENE_MAIN, + WIZARD_WRITER_SCENE_ID +} from "./telegram.constants"; +import {XImageUploadService} from "../x-uploader/x-image.upload.service"; +import {TextUtil} from "../../common/utils/text.util"; +import {XStrategy} from "../social-api/x-router.service"; +import {XCacheService} from "../x-cache/x-cache.service"; +import {SqsPostService} from "../sqs-module/sqs.post.service"; + +@Injectable() +@Update() +export class TelegramUpdates { + + constructor( + private readonly managerService: ManagerService, + private readonly publishPageService: PublishPageService, + private readonly xReaderService: XReaderService, + private readonly trendsService: TrendsService, + private readonly xUpload: XImageUploadService, + private readonly cacheService: XCacheService, + private readonly sqsPostService: SqsPostService + ) { + } + + @Start() + async onStart(ctx: Context) { + + await ctx.reply('Hệ thống Automation đã sẵn sàng! Gửi /help để xem lệnh.'); + } + + @Command('help') + async onHelpCommand(ctx: Context) { + await ctx.reply('Sau đây là các tính năng... ' + + '\n' + + '/writecheap_[en|ja|ko|vi] your_topic... (viết bài bằng AI deepseek)\n' + + '/write_[vi|en|ja|ko] your_topic... (viết bài bằng AI chatgpt + deepseek)\n' + + '/manual_publish_post postNum\n' + + '/analyzenews ... (tim trend,...) \n' + + '/trendstat \n' + + '/ask ... (hỏi với ai deepseek,...) \n' + + '/trend_stat ... (...) \n' + + '/xreader ... (...) \n' + + '/xreaderbrowser ... (...) \n' + + '/collectrss ... (...) \n' + + '/comment link X... (...) \n' + + '/quote link X... (...) \n' + + ''); + } + + @Command(['download', 'dl']) + async onDownloadVideo(ctx) { + // @ts-ignore + const {command, payload} = ctx; + + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} link tiktok/fb`); + return; + } + // Regex tổng hợp các loại link + const tiktokRegex = /https?:\/\/(?:www\.|vm\.|vt\.|m\.)?tiktok\.com\/[^\s]+/gi; + const youtubeRegex = /https?:\/\/(?:www\.)?(?:youtube\.com\/(?:watch\?v=|shorts\/)|youtu\.be\/)[^\s]+/gi; + const facebookRegex = /https?:\/\/(?:www\.|fb\.)?(?:facebook\.com\/reels\/|watch\/)[^\s]+/gi; + const mp4Regex = /https?:\/\/[^\s^"]+\.mp4(?:\?[^\s^"]*)?/gi; + +// Cách 1: Lấy tất cả vào một mảng + const allRegex = new RegExp(`${tiktokRegex.source}|${youtubeRegex.source}|${facebookRegex.source}|${mp4Regex.source}`, 'gi'); + const allLinks = payload.match(allRegex) || []; + console.log({allLinks}); + + console.log("Tất cả link tìm thấy:", allLinks); + + if (allLinks.length === 0) { + await ctx.reply(`Khong tim thay link download`); + return; + } + if (allLinks.length === 1) { + await this.managerService.handleDownloadVideo(allLinks[0], ctx.chat.id); + } else { + await this.managerService.handleDownloadVideoMulti(allLinks, ctx.chat.id); + } + await ctx.reply(`vui lòng chờ download ....`); + } + + @Command('downloadmp4url') + async onDownloadMp4Url(ctx) { + // @ts-ignore + const {command, payload} = ctx; + + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} link mp4`); + return; + } + await ctx.reply(`vui lòng chờ download....`); + await this.managerService.handleDownloadMp4Url(payload, 'x', ctx.chat.id); + } + + @Command('comment') + async onWizardCommand(@Ctx() ctx: Scenes.SceneContext): Promise { + await ctx.scene.enter(WIZARD_COMMENT_SCENE_ID); + } + + @Command([ + 'comvi', + 'comen', + 'comja', + 'comko', + 'comcn', + ]) + async onCommentAsTextCommand(@Ctx() ctx: Scenes.SceneContext): Promise { + + let language = 'en'; + // @ts-ignore + let {command, payload} = ctx; + if (['comvi', 'comvn', 'com_vi'].includes(command)) { + language = 'vi'; + } + if (['comko', 'comkr', 'com_ko'].includes(command)) { + language = 'ko'; + } + if (['comja', 'comjp', 'com_ja'].includes(command)) { + language = 'ja'; + } + if (['comcn'].includes(command)) { + language = 'cn'; + } + + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} bitcoin approach $100000`); + return; + } + const chatId = ctx.chat?.id; + payload = payload.trim(); // Gets "my_payload" + console.log({payload}); + let tweetId = undefined; + let tweetUrl = undefined; + let _linkX = payload.replace('twitter.com', 'x.com').split('?')[0]; + const match = _linkX.match(/status\/(\d+)/); + if (match) { + //nêu match => get content x + const xpost = await this.xReaderService.readXPost(_linkX); + console.log('==> content text:' + xpost.text); + payload = xpost.text; + tweetId = xpost.tweetId; + tweetUrl = _linkX; + } else { + + } + await ctx.scene.enter(WIZARD_COMMENT2_SCENE_ID, { + language, + userTopic: payload, + tweetId, + tweetUrl, + chatId + }); + } + + @Command([ + 'quote_vi', + 'quotevi', + 'quotevn', + 'quote_en', + 'quoteen', + 'quote_ja', + 'quoteja', + 'quote_jp', + 'quotejp', + 'quote_ko', + 'quoteko', + 'quotecn', + ]) + async onWizardQuote(@Ctx() ctx: Scenes.SceneContext): Promise { + // @ts-ignore + let {command, payload} = ctx; + if (['quote_jp', 'quotejp', 'quoteja'].includes(command)) { + command = 'quote_ja'; + } + if (['quote_vn', 'quotevi', 'quotevn'].includes(command)) { + command = 'quote_vi'; + } + if (['quoteen'].includes(command)) { + command = 'quote_en'; + } + if (['quoteko'].includes(command)) { + command = 'quote_ko'; + } + if (['quotecn'].includes(command)) { + command = 'quote_cn'; + } + if (!payload) { + await ctx.reply(`vui lòng đưa url, ví dụ: /${command} link_x_post ?`); + return; + } + const preferLanguage = command.split('_')[1]; + await ctx.scene.enter(WIZARD_QUOTE_SCENE_MAIN, { + language: preferLanguage, + linkUrl: payload, + quoteText: '', + quoteAs: 'quotelink', + }); + } + + @Command([ + 'quoteentext', + 'quotevitext', + 'quotejatext', + 'quotekotext', + 'quotecntext', + ]) + async onQuoteAsTextCommand(@Ctx() ctx: Scenes.SceneContext): Promise { + // @ts-ignore + let {command, payload} = ctx; + let language = 'en'; + if (['quotevitext'].includes(command)) { + language = 'vi'; + } + + if (['quotejatext'].includes(command)) { + language = 'ja'; + } + if (['quotecntext'].includes(command)) { + language = 'cn'; + } + + if (['quotekotext'].includes(command)) { + language = 'ko'; + } + + if (!payload) { + await ctx.reply(`ví dụ: /${command} nội dung quote ?`); + return; + } + await ctx.scene.enter(WIZARD_QUOTE_SCENE_MAIN, { + language: language, + quoteText: payload, + quoteAs: 'quotetext', + }); + } + + @Command('collectrss') + async onCollectRssCommand(ctx: Context) { + const result = await this.trendsService.collectAndStore('rss'); + + ctx.reply('Collection completed'); + await ctx.reply(JSON.stringify(result.stats)); + } + + @Command(['writecheap_vi', 'writecheap_en', 'writecheap_ja', 'writecheap_ko']) + async onWriteCheapCommand(@Ctx() ctx: Context) { + // @ts-ignore + const {command, payload} = ctx; + const preferLanguage = command.split('_')[1]; + + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} bitcoin approach $100000`); + return; + } + await this.managerService.manualTrigger(payload, '', preferLanguage); + + await ctx.reply(`Đang tìm kiếm tin tức về: ${payload} với ngôn ngữ ${preferLanguage}...`); + } + + @Command([ + 'write_vn', + 'write_vi', + 'writevi', + 'write_en', + 'writeen', + 'write_jp', + 'write_ja', + 'writeja', + 'write_kr', + 'write_ko', + 'writeko', + 'writecn', + 'write_cn', + ]) + async onWriteSmartWithAIReviewerCommand(@Ctx() ctx: Scenes.SceneContext) { + // @ts-ignore + let {command, payload} = ctx; + if (['write_jp', 'writejp', 'writeja'].includes(command)) { + command = 'write_ja'; + } + if (['write_kr', 'writekr', 'writeko'].includes(command)) { + command = 'write_ko'; + } + if (['write_vn', 'writevn', 'writevi'].includes(command)) { + command = 'write_vi'; + } + if (['writeen'].includes(command)) { + command = 'write_en'; + } + if (['writecn'].includes(command)) { + command = 'write_cn'; + } + const preferLanguage = command.split('_')[1]; + + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} bitcoin approach $100000`); + return; + } + const chatId = ctx.chat?.id; + if (payload.length < 200) { + //check has link X + const detectSendLinkX = TextUtil.detectLinkX(payload); + if (detectSendLinkX.hasLinkX) { + const xpost = await this.xReaderService.readXPost(detectSendLinkX.url); + payload = xpost.text; + } + } + + await ctx.scene.enter(WIZARD_WRITER_SCENE_ID, { + language: preferLanguage, + userTopic: payload, + chatId, + }); + // await this.managerService.manualTriggerWrite2(payload, '', preferLanguage); + + // await ctx.reply(`Đang tìm kiếm tin tức về: ${payload} với ngôn ngữ ${preferLanguage}...`); + } + + @Command('manual_publish_post') + async manual_publish_post(@Ctx() ctx: Context) { + // @ts-ignore + const {command, payload} = ctx; + if (!payload) { + await ctx.reply('thiếu thông tin post num'); + return; + } + const postNo = 1 * payload; + await ctx.reply(`Đang đẩy bài ${payload} lên Facebook, Twitter`); + + await ctx.reply( + `🤖 **Vui lòng chọn kênh**`, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + {text: "🐦 Post X", callback_data: `publish-post_twitter_${postNo}`}, + {text: "🚀 Post FB", callback_data: `publish-post_facebook_${postNo}`}, + ], + [ + {text: "🐦 Post X via browser", callback_data: `publish-post_twitterbrowser_${postNo}`}, + {text: "🚀 Post all", callback_data: `publish-post_all_${postNo}`}, + ], + [{text: "🗑️ Hủy bài", callback_data: `delete-post_${postNo}`}] + ] + } + } + ); + + // await this.publishPageService.publishToFacebook(postNo).then(() => { + // ctx.reply(`Đăng bài lên FB số ${postNo} thành công`); + // }).catch(err => { + // ctx.reply(`PublishToFacebook error ${err.message}`); + // }); + // await this.publishPageService.publishTwitter(postNo).then(() => { + // ctx.reply(`Đăng bài lên X số ${postNo} thành công`); + // }).catch(err => { + // ctx.reply(`PublishTwitter error ${err.message}`); + // }); + + } + + @Command('analyzenews') + async onAnalyzeNews(ctx: Context) { + // @ts-ignore + const {command, payload} = ctx; + + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} Hot news today in japan ?`); + return; + } + + await this.managerService.handleAnalyzeNewsTrend(payload); + await ctx.reply(`vui lòng đưa chờ phân tích ....`); + } + + @Command('ask') + async onAsk(ctx: Context) { + // @ts-ignore + const {command, payload} = ctx; + + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} Hot news today in japan ?`); + return; + } + console.log(ctx.chat); + await this.managerService.handleAskQues(payload, ctx.chat?.id); + await ctx.reply(`vui lòng đưa chờ phân tích ....`); + } + + @Command('askx') + async onAskXContext(ctx: Context) { + // @ts-ignore + const {command, payload} = ctx; + + if (!payload) { + await ctx.reply(`vui lòng đưa topic, ví dụ: /${command} Hot news today in japan ?`); + return; + } + console.log(ctx.chat); + await this.managerService.handleAskXContext(payload, ctx.chat?.id); + await ctx.reply(`vui lòng đưa chờ phân tích ....`); + } + + @Command(['xreader', 'xreader_browser']) + async onXReader(ctx: Context) { + // @ts-ignore + let {command, payload} = ctx; + if (!payload) { + await ctx.reply(`vui lòng đưa url, ví dụ: /${command} link_x_post ?`); + return; + } + await ctx.reply(`vui lòng chờ phân tích ....`); + + const content = await this.xReaderService.readXPost( + payload, + command === 'xreader_browser' ? 'browser' : 'any' + ).catch((err) => { + ctx.reply(err.message); + return err; + }); + let textMessage = ``; + try { + textMessage += `🤖 crawler by: ${content.via} \n\n`; + if (content) { + textMessage += `====== CONTENT ===== \n\n`; + textMessage += `text: ${content.text} \n----------------------- \n`; + if (!isEmpty(content.images)) { + textMessage += `images: ${content.images.join(' , ')} \n----------------------- \n`; + } + if (!isEmpty(content.videos)) { + textMessage += `videos: ${content.videos.join(' , ')} \n`; + } + + if (!isEmpty(content.quoted)) { + textMessage += `====== QUOTE ===== \n`; + textMessage += `quoted text: ${get(content, 'quoted.text')} \n----------------------- \n`; + if (!isEmpty(content?.quoted?.images)) { + textMessage += `quoted images: ${get(content, 'quoted.images', []).join(' , ')} \n----------------------- \n`; + } + if (!isEmpty(content?.quoted?.videos)) { + textMessage += `quoted videos: ${get(content, 'quoted.videos', []).join(' , ')} \n----------------------- \n`; + } + } + } + await ctx.reply(textMessage, {parse_mode: 'HTML'}); + } catch (err) { + console.log(err); + ctx.reply(err.message); + } + } + + @Action(/publish-post_(.+)/) + async onPublishFB(ctx: Context) { + const telegramChatId = ctx.chat?.id; + const callbackData = (ctx.callbackQuery as any).data; + const social = callbackData.split('_')[1]; + const postId = _toNum(callbackData.split('_')[2]); + const xUsername = callbackData.split('_')[3]; + + await ctx.answerCbQuery(`Đang đẩy bài lên Fb,X ${xUsername}...`); + + const promiseSettedFunc = []; + let allowTw = 0; + let allowFb = 0; + let twStrageryPost = XStrategy.API_ONLY; + let publishTo: any = []; + switch (social) { + case 'all': + allowTw = 1; + allowFb = 1; + publishTo = ['x', 'fb']; + break; + case 'twitter': + allowTw = 1; + publishTo = ['x']; + twStrageryPost=XStrategy.BROWSER_FIRST + break; + case 'twitterbrowser': + allowTw = 1; + publishTo = ['x']; + twStrageryPost = XStrategy.BROWSER_ONLY; + break; + case 'facebook': + allowFb = 1; + publishTo = ['fb']; + break; + } + + try { + // @ts-ignore + const messageText = ctx.callbackQuery.message.text; + + await this.sqsPostService.sendMessage(xUsername, + { + name: 'generate_post_completed', + type: 'X_POSTER_TWEET', + needConfirm: 1, + content: messageText, + autoPublish: 1, + telegramChatId, + xUsername, + publishTo, + xSubmitProvider: XStrategy.BROWSER_FIRST //cứ 3post api, có 1 post browser + } + ) + // const r = await this.publishPageService.relyX(messageText, tweetId) + //@ts-ignore + await ctx.editMessageText(`✅ ** Đã gửi tweet sang queue ${xUsername} ID bài viết: ${postId}`); + + + // await Promise.allSettled([ + // allowFb && this.publishPageService.publishToFacebook(postId).then(() => { + // ctx.reply(`Đăng bài lên FB số ${postId} thành công`); + // }).catch(err => { + // ctx.reply(`PublishToFacebook error ${err.message}`); + // }), + // allowTw && this.publishPageService.publishTwitter(postId, twStrageryPost).then((resp) => { + // ctx.reply(`Đăng bài lên X số ${postId} thành công, ${resp?.url}`); + // }).catch(err => { + // ctx.reply(`PublishTwitter error ${err.message}`); + // }), + // ] + // ) + + // await ctx.editMessageText(`✅ **ĐÃ ĐĂNG THÀNH CÔNG!**\nID bài viết: ${postId}`); + } catch (e) { + await ctx.reply(`❌ Lỗi gửi bài: ${e.message}`); + } + } + + @Action(/delete-post_(.+)/) + async onDelete(ctx: Context) { + const callbackData = (ctx.callbackQuery as any).data; + const postId = _toNum(callbackData.split('_')[1]); + // console.log(ctx.match); + console.log('delete_post_id=', postId); + + await this.publishPageService.rejectedPost(postId); + await ctx.answerCbQuery('Đã hủy bài viết.'); + await ctx.editMessageText('🗑️ Bài viết đã được loại bỏ khỏi hàng đợi.'); + } + + // @ts-ignore + @Command(['trendstat', 'trend_stat']) + async onTrendStatCommand(ctx: Context) { + await ctx.reply(`Đang query..`); + + const {topTrends, byCategory} = await this.trendsService.getStats(); + + let message = ``; + topTrends.forEach(trend => { + message += `\n${trend.title} - ${trend.category} - ${trend.score}\n`; + }); + await ctx.reply(message, { + parse_mode: 'HTML' + }); + + message = ''; + console.log({byCategory}); + byCategory.forEach(trend => { + message += `\n${trend.category} - ${trend._count._all} - ${trend._avg.score} \n`; + }); + await ctx.reply(message, { + parse_mode: 'HTML' + }); + ctx.reply('Done') + } + + @Action(/publish-reply-twitter1_(.+)/) + async onActionSubmitReplyX1(ctx: Context) { + const callbackData = (ctx.callbackQuery as any).data; + // @ts-ignore + const messageText = ctx.callbackQuery.message.text; + + const callbackDataArr = callbackData.split('_'); + const tweetId = callbackDataArr[1]; + const xUsername = callbackDataArr[2]; + + await ctx.answerCbQuery('Đang đẩy reply lên Twitter...'); + + console.log(messageText); + console.log(`publish-reply-twitter1_ ${tweetId} ${xUsername}`); + const telegramChatId = ctx.chat?.id; + try { + const tweetUrl = await this.cacheService.getCacheTweetUrlById(tweetId); + if (isEmpty(tweetUrl)) { + throw new HttpException(`Không tìm thấy tweet url từ Id : ${tweetId}`, 500); + } + await this.sqsPostService.sendMessage(xUsername, + { + tweetId: tweetId, + tweetUrl, + // name: 'generate_comment_twitter_completed', + type: 'X_POSTER_REPLY', + needConfirm: 1, + content: messageText, + autoPublish: 1, + telegramChatId, + xUsername, + xSubmitProvider: XStrategy.BROWSER_ONLY //cứ 3post api, có 1 post browser + } + ) + // const r = await this.publishPageService.relyX(messageText, tweetId) + //@ts-ignore + if (r.success) { + await ctx.editMessageText(`✅ ** Reply success !**\n ID bài viết: ${tweetId}`); + await ctx.reply(`✅ ** Reply success !`); + } else { + //@ts-ignore + await ctx.reply(`❌ Lỗi reply: ${r.error} `); + } + } catch (err) { + console.log(err); + const detail = get(err, 'data.detail', '') + await ctx.reply(`❌ Lỗi reply: ${err.message} `); + await ctx.reply(`=> ${detail}`); + } + } + + @Action(/publish-reply-twitter_(.+)/) + async onActionSubmitReplyX(ctx: Context) { + const callbackData = (ctx.callbackQuery as any).data; + // @ts-ignore + const messageText = ctx.callbackQuery.message.text; + + const callbackDataArr = callbackData.split('_'); + const tweetId = callbackDataArr[1]; + const xUsername = callbackDataArr[2]; + + await ctx.answerCbQuery('Đang đẩy reply lên Twitter...'); + console.log(`Đang đẩy reply lên Twitter 0...`); + + console.log(messageText); + const telegramChatId = ctx.chat?.id; + try { + const tweetUrl = await this.cacheService.getCacheTweetUrlById(tweetId); + if (isEmpty(tweetUrl)) { + console.error(`Không tìm thấy tweet url từ Id : ${tweetId}`); + throw new HttpException(`Không tìm thấy tweet url từ Id : ${tweetId}`, 500); + } + await this.sqsPostService.sendMessage(xUsername, + { + tweetId: tweetId, + tweetUrl, + // name: 'generate_comment_twitter_completed', + type: 'X_POSTER_REPLY', + needConfirm: 1, + content: messageText, + autoPublish: 1, + telegramChatId, + xUsername, + xSubmitProvider: XStrategy.BROWSER_ONLY //cứ 3post api, có 1 post browser + } + ) + // const r = await this.publishPageService.relyX(messageText, tweetId) + //@ts-ignore + await ctx.editMessageText(`✅ ** Đã gửi tin sang queue ${xUsername} ID bài viết: ${tweetId}`); + // await ctx.reply(`✅ Đã gửi tin sang queue ${xUsername}!, ID bài viết: ${tweetId}`); + // else { + // //@ts-ignore + // await ctx.reply(`❌ Lỗi reply: ${r.error} `); + // } + } catch (err) { + console.log(err); + const detail = get(err, 'data.detail', '') + await ctx.reply(`❌ Lỗi reply: ${err.message} `); + await ctx.reply(`=> ${detail}`); + } + } + + @Action(/publish-quote-twitter_(.+)/) + async onActionSubmitQuoteX(ctx: Context) { + const callbackData = (ctx.callbackQuery as any).data; + // @ts-ignore + const messageText = ctx.callbackQuery.message.text; + + // const tweetId = callbackData.split('_')[1]; + const callbackDataArr = callbackData.split('_'); + const tweetId = callbackDataArr[1]; + const xUsername = callbackDataArr[2]; + + await ctx.answerCbQuery('Đang đẩy quote lên Twitter...'); + console.log(`Đang đẩy quote lên Twitter...`); + + console.log(messageText); + console.log(tweetId); + console.log({xUsername, tweetId}); + + const telegramChatId = ctx.chat?.id; + + try { + const tweetUrl = await this.cacheService.getCacheTweetUrlById(tweetId); + if (isEmpty(tweetUrl)) { + console.error(`Không tìm thấy tweet url từ Id : ${tweetId}`); + + throw new HttpException(`Không tìm thấy tweet url từ Id : ${tweetId}`, 500); + } + + await this.sqsPostService.sendMessage(xUsername, + { + tweetId: tweetId, + tweetUrl, + // name: 'generate_comment_twitter_completed', + type: 'X_POSTER_QUOTE', + needConfirm: 1, + content: messageText, + autoPublish: 1, + telegramChatId, + xUsername, + xSubmitProvider: XStrategy.BROWSER_ONLY //cứ 3post api, có 1 post browser + } + ) + await ctx.reply(`✅ Gửi tin sang queue.`); + //await this.publishPageService.quoteX(messageText, tweetId) + //const r = await this.publishPageService.quoteX(messageText, tweetId) + // @ts-ignore + // if (r.success) { + // await ctx.editMessageText(`✅ ** Quote success !**`); + // await ctx.reply(`✅ ** Quote success !**`); + // } else { + // await ctx.reply(`❌ Lỗi quote`); + // // @ts-ignore + // await ctx.reply(r.error); + // } + } catch (err) { + console.log(err); + const detail = get(err, 'data.detail', '') + await ctx.reply(`❌ Lỗi quote: ${err.message} `); + await ctx.reply(`=> ${detail}`); + } + + + } + + @Action([ + /delete-reply_(.+)/, + /delete-quote_(.+)/ + ]) + async onDeleteReplyX(ctx: Context) { + const callbackData = (ctx.callbackQuery as any).data; + const postId = _toNum(callbackData.split('_')[1]); + // console.log(ctx.match); + console.log('delete_post_id=', postId); + + // await this.publishPageService.rejectedPost(postId); + await ctx.answerCbQuery('Đã hủy bài viết.'); + await ctx.editMessageText('🗑️ Bài viết đã được loại bỏ khỏi hàng đợi.'); + } + + @On('photo') + async handlePhoto(@Ctx() ctx: Context) { + try { + const photos = (ctx.message as any).photo; + const largest = photos[photos.length - 1]; + const fileId = largest.file_id; + + // Lấy link để tải ảnh về (nếu cần) + const fileUrl = await ctx.telegram.getFileLink(fileId); + + await ctx.reply(`Đã nhận được ảnh! Bạn có thể xem tại: ${fileUrl}`); + + // // 1. get file path + // const res = await axios.get( + // `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/getFile`, + // { + // params: {file_id: fileId}, + // } + // ); + // + // const filePath = res.data.result.file_path; + // + // // 2. build download url + // const fileUrl = `https://api.telegram.org/file/bot${process.env.TELEGRAM_BOT_TOKEN}/${filePath}`; + // + // // 3. download buffer + // const img = await axios.get(fileUrl, { + // responseType: 'arraybuffer', + // }); + // + // const buffer = Buffer.from(img.data); + + // 4. upload to X + const mediaId = await this.xUpload.uploadImageFromUrl(fileUrl.toString()); + + + await ctx.reply(`✅ Uploaded mediaId: ${mediaId}`); + } catch (e) { + console.error(e); + await ctx.reply('❌ Upload failed'); + } + } + + @Command('chatid') + async onGetChatId(ctx: Context) { + // @ts-ignore + ctx.reply(ctx.chat.id) + } +} diff --git a/src/modules/telegram/wizard/comment.wizard.ts b/src/modules/telegram/wizard/comment.wizard.ts new file mode 100644 index 0000000..90c59fa --- /dev/null +++ b/src/modules/telegram/wizard/comment.wizard.ts @@ -0,0 +1,100 @@ +import {Action, Command, Ctx, Message, On, Wizard, WizardStep} from "nestjs-telegraf"; +import {WIZARD_COMMENT_SCENE_ID} from "../telegram.constants"; +import * as scenes from "telegraf/scenes"; +import {ManagerService} from "../../manager/manager.service"; +import {PublishPageService} from "../../social-api/publish.page.service"; +import {XReaderService} from "../../x-reader/x-reader.service"; +import {TrendsService} from "../../trends/trends.service"; + +@Wizard(WIZARD_COMMENT_SCENE_ID) +export class CommentWizard { + constructor( + private readonly managerService: ManagerService, + ) { + } + + // Bắt lệnh /cancel để thoát + @Command('cancel') + async cancel(@Ctx() ctx: scenes.WizardContext) { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); // Thoát khỏi wizard ngay lập tức + } + + @WizardStep(1) + async onSceneEnter(@Ctx() ctx: scenes.WizardContext) { + // await ctx.wizard.next(); + // return 'Welcome to wizard scene ✋ Send me your name'; + + //@ts-ignore + const text = ctx.message?.text; + console.log(ctx.message); + const linkX = text.split(' ')[1]; // Gets "my_payload" + // + // // Pass it as the second argument (initial state) + //await ctx.scene.enter(WIZARD_COMMENT_SCENE_ID, {customPayload: payload}); + console.log(linkX); + + let _linkX = linkX.replace('twitter.com', 'x.com').split('?')[0]; + const match = _linkX.match(/status\/(\d+)/); + if (!match) { + await ctx.reply('URL X không hợp lệ'); + return ctx.scene.leave(); + } + const tweetId = match[1]; + + // 1. Lưu dữ liệu vào state + (ctx.wizard.state as any).linkUrl = _linkX; + (ctx.wizard.state as any).tweetId = tweetId; + await ctx.sendMessage( + `🤖 Bạn muốn viết bằng ngôn ngữ nào , bấm \cancel để huỷ `, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + {text: "🇬🇧English", callback_data: `write_comment_en`}, + {text: "🇯🇵Japan", callback_data: `write_comment_ja`}, + ], + [ + {text: "🇰🇷Korea", callback_data: `write_comment_ko`}, + {text: "🇻🇳VN", callback_data: `write_comment_vi`}, + ], + [{text: "❌Cancel", callback_data: `write_comment_cancel`},] + ] + } + } + ); + // const {payload} = ctx.scene; + + // await ctx.reply(`Gứi link của bài X bạn cần reply`); + + ctx.wizard.next(); + } + + @WizardStep(2) + @On('text') + @Action(/write_comment_(.+)/) + async step2(@Ctx() ctx: scenes.WizardContext) { + // 1. Lấy dữ liệu từ nút bấm + // @ts-ignore + const selectedItem = ctx.match[1] + if (selectedItem == 'cancel') { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + + // 2. Lưu vào state để dùng cho các bước sau (nếu cần) + (ctx.wizard.state as any).selectedLanguage = selectedItem; + + // 3. Phải gọi answerCbQuery để tắt biểu tượng "loading" trên nút bấm + await ctx.answerCbQuery(); + + await ctx.reply(`Bạn đã chọn ${selectedItem}. Vui lòng chờ phân tích ...`); + const url = (ctx.wizard.state as any).linkUrl; + await this.managerService.manualTriggerCommentLinkTwitter(url, selectedItem); + + // Chuyển sang bước 3 (nhận địa chỉ bằng text) + // ctx.wizard.next(); + await ctx.scene.leave(); + } +} diff --git a/src/modules/telegram/wizard/comment2.wizard.ts b/src/modules/telegram/wizard/comment2.wizard.ts new file mode 100644 index 0000000..0861895 --- /dev/null +++ b/src/modules/telegram/wizard/comment2.wizard.ts @@ -0,0 +1,227 @@ +import {Action, Command, Ctx, On, Wizard, WizardStep} from "nestjs-telegraf"; +import {WIZARD_COMMENT2_SCENE_ID} from "../telegram.constants"; +import * as scenes from "telegraf/scenes"; +import {ManagerService} from "../../manager/manager.service"; +import {TONE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/prompts/templates"; +import {ANGLE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/enum/angle.enum"; + +@Wizard(WIZARD_COMMENT2_SCENE_ID) +export class Comment2Wizard { + constructor( + private readonly managerService: ManagerService, + ) { + } + + // Bắt lệnh /cancel để thoát + @Command('cancel') + async cancel(@Ctx() ctx: scenes.WizardContext) { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); // Thoát khỏi wizard ngay lập tức + } + + @WizardStep(1) + async onSceneEnter(@Ctx() ctx: scenes.WizardContext) { + //@ts-ignore + const userTopic = ctx.message?.text; + // console.log(ctx.message); + // const userTopic = (ctx.wizard.state as any).userTopic; + if (!userTopic) { + await ctx.reply('Không có thông tin về chủ đề bạn cần viết bài'); + return ctx.scene.leave(); + } + // @ts-ignore + const inline_keyboards = []; + Object.keys(TONE_HINTS_TELEGRAM_BUTTON).map(key => { + // @ts-ignore + inline_keyboards.push([{ + text: TONE_HINTS_TELEGRAM_BUTTON[key].text, + callback_data: `comment_tone_${key}` + }]); + return; + }) + await ctx.sendMessage( + `🤖 Chọn phong cách viết , bấm \\cancel để huỷ `, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + ...inline_keyboards, + [ + {text: "🤖AI tự chọn", callback_data: `comment_tone_aineeddetect`}, + ], + [ + {text: "❌Cancel", callback_data: `comment_tone_cancel`}, + ] + ] + } + } + ); + ctx.wizard.next(); + } + + @WizardStep(2) + @On('text') + @Action(/comment_tone_(.+)/) + async step2(@Ctx() ctx: scenes.WizardContext) { + + if (ctx.text == 'cancel') { + await ctx.deleteMessage(); + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + // 1. Lấy dữ liệu từ nút bấm + + // @ts-ignore + if (ctx.match === undefined) { + await ctx.reply('Đã hủy quy trình.'); + await ctx.deleteMessage(); + return ctx.scene.leave(); + } + + let tone = undefined + // @ts-ignore + const selectedItem = ctx.match[1] + + await ctx.answerCbQuery('Đã ghi nhận tone=' + selectedItem); + if (selectedItem == 'cancel') { + // Xóa tin nhắn hiện tại + await ctx.deleteMessage(); + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + if (selectedItem == 'aineeddetect') { + tone = undefined; + } else { + tone = selectedItem + } + + + // Xóa tin nhắn hiện tại + await ctx.deleteMessage(); + + (ctx.wizard.state as any).tone = tone; + // @ts-ignore + const inline_keyboards = []; + Object.keys(ANGLE_HINTS_TELEGRAM_BUTTON).map(key => { + // @ts-ignore + inline_keyboards.push([{ + text: ANGLE_HINTS_TELEGRAM_BUTTON[key].text, + callback_data: `comment_agle_${key}` + }]); + return; + }) + await ctx.sendMessage( + `🤖 Chọn âm điệu , bấm \\cancel để huỷ `, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + ...inline_keyboards, + [ + {text: "🤖AI tự chọn", callback_data: `comment_agle_aineeddetect`}, + ], + [ + {text: "❌Cancel", callback_data: `comment_agle_cancel`}, + ] + ] + } + } + ); + + ctx.wizard.next(); + } + + @WizardStep(3) + @On('text') + @Action(/comment_agle_(.+)/) + async step3(@Ctx() ctx: scenes.WizardContext) { + if (ctx.text == 'cancel') { + await ctx.deleteMessage(); + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + // 1. Lấy dữ liệu từ nút bấm + + // @ts-ignore + if (ctx.match === undefined) { + await ctx.deleteMessage(); + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + + + + let agle = undefined + // @ts-ignore + const selectedItem = ctx.match[1] + + await ctx.answerCbQuery('Đã ghi nhận agle:' + selectedItem); + // Xóa tin nhắn hiện tại + await ctx.deleteMessage(); + + if (selectedItem == 'cancel') { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + if (selectedItem == 'aineeddetect') { + agle = undefined; + } else { + agle = selectedItem + } + + + (ctx.wizard.state as any).agle = agle; + const chatId = (ctx.wizard.state as any).chatId; + const tone = (ctx.wizard.state as any).tone; + const tweetId = (ctx.wizard.state as any).tweetId; + const tweetUrl = (ctx.wizard.state as any).tweetUrl; + const userTopic = (ctx.wizard.state as any).userTopic; + const language = (ctx.wizard.state as any).language; + const postLength = undefined; + + // @ts-ignore + await ctx.reply(` + Sẽ viết bài bằng ngôn ngữ ${language} độ dài ${postLength} với tone=${tone} và agle=${agle}.`); + + await this.managerService.manualTriggerCommentAsText({ + agle: agle, + comtext: userTopic, + tone: tone, + language: language, + tweetId, + tweetUrl, + chatId, + }) + await ctx.scene.leave(); + } + + async doAskTone(ctx: scenes.WizardContext) { + // @ts-ignore + const inline_keyboards = []; + Object.keys(TONE_HINTS_TELEGRAM_BUTTON).map(key => { + // @ts-ignore + inline_keyboards.push([{ + text: TONE_HINTS_TELEGRAM_BUTTON[key].text, + callback_data: `comment_tone_${key}` + }]); + return; + }) + await ctx.sendMessage( + `🤖 Chọn phong cách viết , bấm \\cancel để huỷ `, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + ...inline_keyboards, + [ + {text: "🤖AI tự chọn", callback_data: `comment_tone_aineeddetect`}, + ], + [ + {text: "❌Cancel", callback_data: `comment_tone_cancel`}, + ] + ] + } + } + ); + } +} diff --git a/src/modules/telegram/wizard/quote.wizard.ts b/src/modules/telegram/wizard/quote.wizard.ts new file mode 100644 index 0000000..bdc2e7c --- /dev/null +++ b/src/modules/telegram/wizard/quote.wizard.ts @@ -0,0 +1,143 @@ +import {Action, Command, Ctx, On, Wizard, WizardStep} from "nestjs-telegraf"; +import {WIZARD_QUOTE_SCENE_MAIN} from "../telegram.constants"; +import * as scenes from "telegraf/scenes"; +import {ManagerService} from "../../manager/manager.service"; +import {QUOTE_TYPE_TELEGRAM_BUTTON_SPECS} from "../../content-writer/prompts/quote.templates"; + +@Wizard(WIZARD_QUOTE_SCENE_MAIN) +export class QuoteWizard { + constructor( + private readonly managerService: ManagerService, + ) { + } + + // Bắt lệnh /cancel để thoát + @Command('cancel') + async cancel(@Ctx() ctx: scenes.WizardContext) { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); // Thoát khỏi wizard ngay lập tức + } + + @WizardStep(1) + async onSceneEnter(@Ctx() ctx: scenes.WizardContext) { + + const isQuoteUrl = (ctx.wizard.state as any).quoteAs === 'quotelink'; + + //@ts-ignore + const text = ctx.message?.text; + console.log(ctx.message); + if (isQuoteUrl) { + const linkX = text.split(' ')[1]; // Gets "my_payload" + + if (!linkX) { + await ctx.reply('URL X không hợp lệ'); + return ctx.scene.leave(); + } + + let _linkX = linkX.replace('twitter.com', 'x.com').split('?')[0]; + const match = _linkX.match(/status\/(\d+)/); + if (!match) { + await ctx.reply('URL X không hợp lệ'); + return ctx.scene.leave(); + } + const tweetId = match[1]; + // 1. Lưu dữ liệu vào state + (ctx.wizard.state as any).linkUrl = linkX; + (ctx.wizard.state as any).tweetId = tweetId; + } + // @ts-ignore + const inline_keyboards = []; + Object.keys(QUOTE_TYPE_TELEGRAM_BUTTON_SPECS).map(key => { + // @ts-ignore + inline_keyboards.push([{ + text: QUOTE_TYPE_TELEGRAM_BUTTON_SPECS[key].text, + callback_data: `quote_type_${key}` + }]); + return; + }) + await ctx.sendMessage( + `🤖 Chọn phong cách quote , bấm \\cancel để huỷ `, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + // @ts-ignore + ...inline_keyboards, + [ + {text: "🤖AI tự chọn", callback_data: `quote_type_aineeddetect`}, + ], + [ + {text: "❌Cancel", callback_data: `quote_type_cancel`}, + ] + ] + } + } + ); + ctx.wizard.next(); + } + + @WizardStep(2) + @On('text') + @Action(/quote_type_(.+)/) + async step2(@Ctx() ctx: scenes.WizardContext) { + await ctx.answerCbQuery('Đã ghi nhận'); + // Xóa tin nhắn hiện tại + await ctx.deleteMessage(); + if (ctx.text == 'cancel') { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + // 1. Lấy dữ liệu từ nút bấm + + // @ts-ignore + if (ctx.match === undefined) { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + + let quoteType = undefined + // @ts-ignore + const selectedItem = ctx.match[1] + if (selectedItem == 'cancel') { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + if (selectedItem == 'aineeddetect') { + quoteType = undefined; + } else { + quoteType = selectedItem + } + // 2. Lưu vào state để dùng cho các bước sau (nếu cần) + // (ctx.wizard.state as any).type = quoteType; + + const language = (ctx.wizard.state as any).language; + const quoteAs = (ctx.wizard.state as any).quoteAs; + + const telegramChatId = ''+ctx?.chat?.id!; + // @ts-ignore + await ctx.reply(`Sẽ viết quote bằng ngôn ngữ ${language} với phong cách ${quoteType}-${QUOTE_TYPE_TELEGRAM_BUTTON_SPECS[quoteType]?.text!} ...`); + + if (quoteAs === 'quotetext') { + const userTopic = (ctx.wizard.state as any).quoteText; + + await this.managerService.manualTriggerQuoteAsTextInput( + userTopic, + quoteType, + language, + telegramChatId + ) + } else { + const twitterUrl = (ctx.wizard.state as any).linkUrl; + + await this.managerService.manualTriggerQuoteLinkTwitter( + twitterUrl, + quoteType, + language, + telegramChatId + ); + } + + + await ctx.scene.leave(); + } +} diff --git a/src/modules/telegram/wizard/writer.wizard.ts b/src/modules/telegram/wizard/writer.wizard.ts new file mode 100644 index 0000000..a601093 --- /dev/null +++ b/src/modules/telegram/wizard/writer.wizard.ts @@ -0,0 +1,196 @@ +import {Action, Command, Ctx, On, Wizard, WizardStep} from "nestjs-telegraf"; +import {WIZARD_WRITER_SCENE_ID} from "../telegram.constants"; +import * as scenes from "telegraf/scenes"; +import {ManagerService} from "../../manager/manager.service"; +import {STYLE_HINTS_TELEGRAM_BUTTON, TONE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/prompts/templates"; + +@Wizard(WIZARD_WRITER_SCENE_ID) +export class WriterWizard { + constructor( + private readonly managerService: ManagerService, + ) { + } + + // Bắt lệnh /cancel để thoát + @Command('cancel') + async cancel(@Ctx() ctx: scenes.WizardContext) { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); // Thoát khỏi wizard ngay lập tức + } + + @WizardStep(1) + async onSceneEnter(@Ctx() ctx: scenes.WizardContext) { + //@ts-ignore + const text = ctx.message?.text; + console.log(ctx.message); + const userTopic = (ctx.wizard.state as any).userTopic; + if (!userTopic) { + await ctx.reply('Không có thông tin về chủ đề bạn cần viết bài'); + return ctx.scene.leave(); + } + // @ts-ignore + const inline_keyboards = []; + Object.keys(STYLE_HINTS_TELEGRAM_BUTTON).map(key => { + // @ts-ignore + inline_keyboards.push([{ + text: STYLE_HINTS_TELEGRAM_BUTTON[key].text, + callback_data: `write_type_${key}` + }]); + return; + }) + await ctx.sendMessage( + `🤖 Chọn phong cách viết , bấm \\cancel để huỷ `, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + ...inline_keyboards, + [ + {text: "🤖AI tự chọn", callback_data: `write_type_aineeddetect`}, + ], + [ + {text: "❌Cancel", callback_data: `write_type_cancel`}, + ] + ] + } + } + ); + ctx.wizard.next(); + } + + @WizardStep(2) + @On('text') + @Action(/write_type_(.+)/) + async step2(@Ctx() ctx: scenes.WizardContext) { + + if (ctx.text == 'cancel') { + await ctx.deleteMessage(); + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + // 1. Lấy dữ liệu từ nút bấm + + // @ts-ignore + if (ctx.match === undefined) { + await ctx.reply('Đã hủy quy trình.'); + await ctx.deleteMessage(); + return ctx.scene.leave(); + } + + let writeType = undefined + // @ts-ignore + const selectedItem = ctx.match[1] + + await ctx.answerCbQuery('Đã ghi nhận writing style=' + selectedItem); + if (selectedItem == 'cancel') { + // Xóa tin nhắn hiện tại + await ctx.deleteMessage(); + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + if (selectedItem == 'aineeddetect') { + writeType = undefined; + } else { + writeType = selectedItem + } + + + // Xóa tin nhắn hiện tại + await ctx.deleteMessage(); + + (ctx.wizard.state as any).writeType = writeType; + // @ts-ignore + const inline_keyboards = []; + Object.keys(TONE_HINTS_TELEGRAM_BUTTON).map(key => { + // @ts-ignore + inline_keyboards.push([{ + text: TONE_HINTS_TELEGRAM_BUTTON[key].text, + callback_data: `write_tone_${key}` + }]); + return; + }) + await ctx.sendMessage( + `🤖 Chọn âm điệu , bấm \\cancel để huỷ `, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + ...inline_keyboards, + [ + {text: "🤖AI tự chọn", callback_data: `write_tone_aineeddetect`}, + ], + [ + {text: "❌Cancel", callback_data: `write_tone_cancel`}, + ] + ] + } + } + ); + + ctx.wizard.next(); + } + + @WizardStep(3) + @On('text') + @Action(/write_tone_(.+)/) + async step3(@Ctx() ctx: scenes.WizardContext) { + if (ctx.text == 'cancel') { + await ctx.deleteMessage(); + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + // 1. Lấy dữ liệu từ nút bấm + + // @ts-ignore + if (ctx.match === undefined) { + await ctx.deleteMessage(); + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + + + + let writeTone = undefined + // @ts-ignore + const selectedItem = ctx.match[1] + + await ctx.answerCbQuery('Đã ghi nhận ngữ điệu:' + selectedItem); + // Xóa tin nhắn hiện tại + await ctx.deleteMessage(); + + if (selectedItem == 'cancel') { + await ctx.reply('Đã hủy quy trình.'); + return ctx.scene.leave(); + } + if (selectedItem == 'aineeddetect') { + writeTone = undefined; + } else { + writeTone = selectedItem + } + + + (ctx.wizard.state as any).writeTone = writeTone; + const writeType = (ctx.wizard.state as any).writeType; + const userTopic = (ctx.wizard.state as any).userTopic; + const language = (ctx.wizard.state as any).language; + const telegramChatId = (ctx.wizard.state as any).chatId; + const postLength = undefined; + + // @ts-ignore + await ctx.reply(` + Sẽ viết bài bằng ngôn ngữ ${language} độ dài ${postLength} với phong cách ${writeType} và màu sắc ${writeTone} ... + `); + + await this.managerService.manualTriggerWriteWithReview({ + style: writeType, + keyword: userTopic, + tone: writeTone, + language: language, + telegramChatId, + postLength + }) + await ctx.scene.leave(); + } + + +} diff --git a/src/modules/tiktok-download/tiktok.download.module.ts b/src/modules/tiktok-download/tiktok.download.module.ts new file mode 100644 index 0000000..710def4 --- /dev/null +++ b/src/modules/tiktok-download/tiktok.download.module.ts @@ -0,0 +1,15 @@ +import {Module} from "@nestjs/common"; +import {XTiktokDownloadService} from "./x-tiktok.download.service"; +import {BullModule} from "@nestjs/bullmq"; +import {VideoDownloadProcessor} from "./video.download.processor"; + +@Module({ + imports:[ + BullModule.registerQueue( + {name: 'download_video_queue'},// Hàng đợi cho AI-C + ), + ], + providers: [XTiktokDownloadService, VideoDownloadProcessor], + exports: [XTiktokDownloadService], +}) +export class TiktokDownloadModule {} diff --git a/src/modules/tiktok-download/uploadvideo.to.telegram.ts b/src/modules/tiktok-download/uploadvideo.to.telegram.ts new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/modules/tiktok-download/uploadvideo.to.telegram.ts @@ -0,0 +1 @@ + diff --git a/src/modules/tiktok-download/video.download.processor.ts b/src/modules/tiktok-download/video.download.processor.ts new file mode 100644 index 0000000..fc7214a --- /dev/null +++ b/src/modules/tiktok-download/video.download.processor.ts @@ -0,0 +1,194 @@ +import {OnWorkerEvent, Processor, WorkerHost} from "@nestjs/bullmq"; +import {Job} from "bullmq"; +import {Logger} from "@nestjs/common"; +import {XTiktokDownloadService} from "./x-tiktok.download.service"; +import {InjectBot} from "nestjs-telegraf"; +import {Context, Telegraf} from "telegraf"; +import {map} from "lodash"; +import {_sleep} from "../../shared/helper"; +import fs from "fs"; + + +@Processor('download_video_queue') +export class VideoDownloadProcessor extends WorkerHost { + private readonly logger = new Logger("VideoDownloadProcessor"); + private adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + + constructor( + private downloadTool: XTiktokDownloadService, + @InjectBot() private readonly bot: Telegraf, + ) { + super(); + } + + async process(job: Job, token: string | undefined): Promise { + + console.log(`VideoDownloadProcessor ==> process => ${job.name}`); + console.log('job_data', job.data); + + let {telegramChatId, url, sourceType, urls} = job.data; + const jobName = job.name; + + switch (jobName) { + case 'download': { + const result = await this.downloadTool.download(url); + console.log('telegramChatId:', telegramChatId); + if (result.status === 0) { + //error + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + `Có lỗi.`, + ); + return; + } + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + `đã download xong: ${result.title}`, + ); + + return { + status: 1, + filePath: result.filePath, + title: result.title, + telegramChatId, + jobName, + originalUrl: url, + }; + } + case 'download_multi': { + const totalLink = urls.length; + let done = 0; + for (const url of urls) { + await _sleep(1000); + console.log(`Downloading ${url}`); + const result = await this.downloadTool.download(url).catch((err) => { + console.log(err); + return { + status: 0, + error: err.message, + filePath: '', + title: '', + } + }); + if (result.status === 0) { + //error + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + `Có lỗi. ${result.error}`, + ); + return; + } else { + done++; + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + `đã download xong ${done}/${totalLink}: ${result.title}`, + ); + await this.sendVideoToTelegram(telegramChatId || this.adminChatId, result.filePath, result.title); + } + // // Dừng lại đợi ở đây + // results.push(data); + } + // map(urls, async (url) => { + // + // + // + // return { + // status: 1, + // filePath: result.filePath, + // title: result.title, + // telegramChatId, + // jobName, + // originalUrl: url, + // }; + // }) + + } + case 'download_tiktok': { + const result = await this.downloadTool.downloadTikTok(url); + console.log({result}); + console.log('telegramChatId:', telegramChatId); + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + `đã download xong: ${result.title}`, + ); + + return { + status: 1, + filePath: result.filePath, + title: result.title, + telegramChatId, + jobName, + originalUrl: url, + }; + } + case 'youtube': { + break; + } + case 'facebook_reels': { + const result = await this.downloadTool.downloadFacebookReels(url); + console.log({result}); + console.log('telegramChatId:', telegramChatId); + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + `đã download xong: ${result.title}`, + ); + + return { + status: 1, + filePath: result.filePath, + title: result.title, + telegramChatId, + jobName, + originalUrl: url, + }; + } + case 'mp4url': { + const result = await this.downloadTool.downloadUrlMp4(url, sourceType); + console.log({result}); + console.log('telegramChatId:', telegramChatId); + await this.bot.telegram.sendMessage(telegramChatId > 0 ? telegramChatId : this.adminChatId, + `đã download xong: ${result.title}`, + ); + + return { + status: 1, + filePath: result.filePath, + title: result.title, + telegramChatId, + jobName, + originalUrl: url, + }; + } + } + + return Promise.resolve({ + status: 0, + telegramChatId, + filePath: '', + title: '', + jobName, + originalUrl: url, + }); + } + + @OnWorkerEvent('completed') + async onCompleted(job: Job) { + console.log('VideoDownloadProcessor_completed'); + const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0; + const status = job.returnvalue.status; + const telegramChatId = job.returnvalue.telegramChatId; + const originalUrl = job.returnvalue.originalUrl; + if (status === 0) { + //const topic = job.returnvalue.topic; + await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `Lỗi download: ${originalUrl}`); + } else { + const filePath = job.returnvalue.filePath; + const title = job.returnvalue.title; + await this.sendVideoToTelegram(telegramChatId || adminChatId, filePath, title); + } + } + + + async sendVideoToTelegram(chatId, filePath, caption = '') { + await this.bot.telegram.sendMessage(chatId, 'chờ gửi lên telegram ...') + await this.bot.telegram.sendVideo(chatId, {source: filePath}, {caption}); + // Xoá file sau khi gửi + return fs.unlinkSync(filePath); + } + + +} diff --git a/src/modules/tiktok-download/x-tiktok.download.service.ts b/src/modules/tiktok-download/x-tiktok.download.service.ts new file mode 100644 index 0000000..d72b4fb --- /dev/null +++ b/src/modules/tiktok-download/x-tiktok.download.service.ts @@ -0,0 +1,216 @@ +import {exec} from 'child_process'; +import {promisify} from 'util'; +import path from 'path'; +import fs from 'fs'; +import {BadRequestException, Injectable, Logger} from "@nestjs/common"; +import axios from "axios"; +import {pipeline} from 'stream/promises'; + +const execAsync = promisify(exec); + +@Injectable() +export class XTiktokDownloadService { + private readonly logger = new Logger(XTiktokDownloadService.name); + private downloadDir = './downloads/' + + constructor() { + fs.mkdirSync(this.downloadDir, {recursive: true}); + // fs.mkdirSync(this.downloadDir + '/fb', {recursive: true}); + // fs.mkdirSync(this.downloadDir + '/tiktok', {recursive: true}); + // fs.mkdirSync(this.downloadDir + '/x', {recursive: true}); + // fs.mkdirSync(this.downloadDir + '/yt', {recursive: true}); + + } + + async download(url: string) { + const urlMp4Regex = /(https?:\/\/[^\s]+\.mp4)/i; + if (urlMp4Regex.test(url)) { + return this.downloadUrlMp4(url, ''); + } + if (url.indexOf('tiktok')) { + return this.downloadTikTok(url); + } + const regexFb = /https?:\/\/(www\.)?(facebook\.com\/reel\/\S+|fb\.watch\/\S+)/i + if (regexFb.test(url)) { + return this.downloadFacebookReels(url); + } + return { + status: 0, + filePath: '', + title: '', + error: '' + } + } + + async downloadTikTok(url) { + const timestamp = Date.now(); + const outputDir = path.resolve('./downloads/tiktok'); + const outputTemplate = path.join(outputDir, `tt_${timestamp}.%(ext)s`); + + // yt-dlp: tải TikTok không watermark, output mp4 + const cmd = `yt-dlp -f "best[ext=mp4]/best" -o "${outputTemplate}" --no-warnings "${url}"`; + + try { + const {stdout} = await execAsync(cmd, {maxBuffer: 1024 * 1024 * 50}); + + // Tìm file vừa download + const files = fs.readdirSync(outputDir) + .filter(f => f.startsWith(`tt_${timestamp}`)) + .map(f => path.join(outputDir, f)); + + if (!files.length) throw new Error('Không tìm thấy file đã tải'); + console.log({files}); + return { + filePath: files[0], + title: `TikTok_${timestamp}`, + status: 1, + error: '' + }; + } catch (err) { + throw new Error(`Lỗi download TikTok: ${err.message}`); + } + } + + // async downloadVideoQueue(url) {} + async downloadUrlMp4(mp4url: string, sourceType = '') { + const dir = this.downloadDir + `/${sourceType}`; + fs.mkdirSync(dir, {recursive: true}); + console.log('downloadUrlMp4:', mp4url); + // const filename = customName || this.extractFilename(mp4url); + + this.logger.log(`⬇️ Đang tải: ${mp4url}`); + const timestamp = Date.now(); + const filename = `${sourceType}_${timestamp}.mp4`; + const filePath = path.join(dir, filename); + + + const response = await axios({ + url: mp4url, + method: 'GET', + responseType: 'stream', + timeout: 80000, + headers: { + 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + Accept: 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5', + }, + }); + + const writer = fs.createWriteStream(filePath); + await pipeline(response.data, writer); + + // await new Promise((resolve, reject) => { + // const writer = fs.createWriteStream(filePath); + // response.data.pipe(writer); + // writer.on('finish', ()=>{ + // console.log(`download ${mp4url} done`); + // resolve(filePath); + // }); + // writer.on('error', reject); + // }); + console.log('✅ Tải xong:', mp4url); + return { + filePath, + title: filename, + status: 1, + error: '' + }; + } + + /** + * Download Facebook Reels + */ + async downloadFacebookReels(fbUrl: string) { + this.logger.log(`🔍 Đang phân tích Facebook Reels: ${fbUrl}`); + + // Chuẩn hóa URL + let targetUrl = fbUrl.trim(); + if (!targetUrl.startsWith('http')) targetUrl = `https://${targetUrl}`; + + // 1. Lấy HTML trang reels + const html = await this.fetchFacebookPage(targetUrl); + + // 2. Trích xuất direct video URL từ HTML + const videoUrl = this.extractFacebookVideoUrl(html); + if (!videoUrl) { + throw new BadRequestException('Không tìm thấy video URL trong trang Facebook. Có thể reels bị giới hạn hoặc Facebook đã đổi cấu trúc.'); + } + + this.logger.log(`📹 Tìm thấy video source: ${videoUrl.substring(0, 100)}...`); + + // 3. Tải về bằng axios stream + return this.downloadUrlMp4(videoUrl, 'fb'); + } + + /** + * Lấy HTML của trang Facebook (giả lập browser desktop) + */ + private async fetchFacebookPage(url: string): Promise { + try { + const {data} = await axios.get(url, { + timeout: 30000, + maxRedirects: 5, + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept-Language': 'en-US,en;q=0.9', + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Cache-Control': 'max-age=0', + 'sec-fetch-dest': 'document', + 'sec-fetch-mode': 'navigate', + 'sec-fetch-site': 'none', + 'sec-fetch-user': '?1', + 'upgrade-insecure-requests': '1', + }, + }); + return data as string; + } catch (err: any) { + this.logger.error(`Lỗi fetch Facebook page: ${err.message}`); + throw new BadRequestException(`Không thể truy cập link Facebook: ${err.message}`); + } + } + + /** + * Parse HTML Facebook để tìm video URL trực tiếp + */ + private extractFacebookVideoUrl(html: string): string | null { + // Cách 1: Meta tag og:video hoặc og:video:url + const ogVideoMatch = html.match(/]+property=["']og:video(:(url|secure_url))?["'][^>]+content=["']([^"']+)["']/i); + if (ogVideoMatch) { + const url = ogVideoMatch[3]; + // Loại bỏ HTML entities + return url.replace(/&/g, '&'); + } + + // Cách 2: Tìm trong script JSON chứa "video_url" hoặc "browser_native_sd_url" + const patterns = [ + /"video_url"\s*:\s*"([^"]+)"/, + /"browser_native_sd_url"\s*:\s*"([^"]+)"/, + /"browser_native_hd_url"\s*:\s*"([^"]+)"/, + /"playable_url"\s*:\s*"([^"]+)"/, + /"playable_url_quality_hd"\s*:\s*"([^"]+)"/, + ]; + + for (const pattern of patterns) { + const match = html.match(pattern); + if (match) { + return match[1].replace(/\\u0025/g, '%').replace(/\\/g, ''); + } + } + + // Cách 3: Tìm trong data-store hoặc các cấu trúc deferred data + const deferredMatch = html.match(/video_url\\":\\"([^"]+)\\"/); + if (deferredMatch) { + return deferredMatch[1].replace(/\\/g, ''); + } + + return null; + } + + private extractFilename(url: string): string { + try { + const name = path.basename(new URL(url).pathname); + if (name && name.includes('.')) return name; + } catch { /* ignore */ + } + return `video_${Date.now()}.mp4`; + } +} diff --git a/src/modules/trends/dto/get-trends.dto.ts b/src/modules/trends/dto/get-trends.dto.ts new file mode 100644 index 0000000..3113c13 --- /dev/null +++ b/src/modules/trends/dto/get-trends.dto.ts @@ -0,0 +1,55 @@ +import { IsOptional, IsString, IsNumber, Min, Max } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class GetTrendsDto { + @ApiPropertyOptional({ description: 'Filter by category' }) + @IsOptional() + @IsString() + category?: string; + + @ApiPropertyOptional({ description: 'Filter by source' }) + @IsOptional() + @IsString() + source?: string; + + @ApiPropertyOptional({ default: 50, minimum: 1, maximum: 200 }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(1) + @Max(200) + limit?: number = 50; + + @ApiPropertyOptional({ default: 0, description: 'Minimum trend score (0-100)' }) + @IsOptional() + @Type(() => Number) + @IsNumber() + @Min(0) + @Max(100) + minScore?: number = 0; + + @ApiPropertyOptional({ description: 'Search keyword in title' }) + @IsOptional() + @IsString() + q?: string; + + @ApiPropertyOptional({ + description: 'Max age in hours', + default: 24, + }) + @IsOptional() + @Type(() => Number) + @IsNumber() + maxAgeHours?: number = 24; +} + +export class CollectNowDto { + @ApiPropertyOptional({ + description: 'Specific source to collect from', + enum: ['google-trends', 'reddit', 'hackernews', 'rss', 'newsapi'], + }) + @IsOptional() + @IsString() + source?: string; +} diff --git a/src/modules/trends/dto/trend-response.dto.ts b/src/modules/trends/dto/trend-response.dto.ts new file mode 100644 index 0000000..b1c6749 --- /dev/null +++ b/src/modules/trends/dto/trend-response.dto.ts @@ -0,0 +1,64 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TrendResponseDto { + @ApiProperty() + id: string; + + @ApiProperty() + title: string; + + @ApiProperty() + description: string; + + @ApiProperty() + url: string; + + @ApiProperty() + source: string; + + @ApiProperty() + score: number; + + @ApiProperty() + category: string; + + @ApiProperty({ type: [String] }) + tags: string[]; + + @ApiProperty() + engagement: Record; + + @ApiProperty() + sourceTimestamp: Date; + + @ApiProperty() + createdAt: Date; +} + +export class CollectionStatsDto { + @ApiProperty() + totalRaw: number; + + @ApiProperty() + afterDedup: number; + + @ApiProperty() + bySource: Record; + + @ApiProperty({ type: [String] }) + errors: string[]; + + @ApiProperty() + durationMs: number; +} + +export class TrendsListResponseDto { + @ApiProperty({ type: [TrendResponseDto] }) + data: TrendResponseDto[]; + + @ApiProperty() + total: number; + + @ApiProperty() + filters: Record; +} diff --git a/src/modules/trends/trends.controller.ts b/src/modules/trends/trends.controller.ts new file mode 100644 index 0000000..67590f2 --- /dev/null +++ b/src/modules/trends/trends.controller.ts @@ -0,0 +1,66 @@ +import { + Controller, + Get, + Post, + Query, + Body, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { TrendsService } from './trends.service'; +import {CollectNowDto, GetTrendsDto} from "./dto/get-trends.dto"; +import {ApiOperation, ApiResponse} from "@nestjs/swagger"; +import {GoogleTrendsService} from "../collector/google-trends.service"; +import googleTrends from "@shaivpidadi/trends-js"; + +@Controller('trends') +export class TrendsController { + constructor( + private readonly trendsService: TrendsService, + private readonly googleTrendsService: GoogleTrendsService, + ) {} + + @Get() + async getTrends(@Query() dto: GetTrendsDto) { + return this.trendsService.getTrends(dto); + } + + @Get('stats') + async getStats() { + return this.trendsService.getStats(); + } + + @Post('collect') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Trigger thu thập trends ngay lập tức' }) + @ApiResponse({ status: 200, description: 'Kết quả thu thập' }) + async collectNow(@Body() dto: CollectNowDto) { + const result = await this.trendsService.collectAndStore(dto.source); + return { + message: 'Collection completed', + stats: result.stats, + }; + } + + @Post('cleanup') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Xóa trends cũ' }) + async cleanup(@Query('days') days: number = 7) { + const deleted = await this.trendsService.cleanupOldTrends(days); + return { message: `Deleted ${deleted} old trends` }; + } + + @Get('test') + async testTrends() { + // return googleTrends.dailyTrends({ + // trendDate: new Date(), + // geo: 'JP', + // }); + // const result = await googleTrends.realTimeTrends({ + // geo: 'JP', // Default: 'US' + // trendingHours: 4, // Default: 4 + // }); + // return result; + return this.googleTrendsService.collect('JP') + } +} diff --git a/src/modules/trends/trends.module.ts b/src/modules/trends/trends.module.ts new file mode 100644 index 0000000..cad3e77 --- /dev/null +++ b/src/modules/trends/trends.module.ts @@ -0,0 +1,13 @@ +import {Module} from '@nestjs/common'; +import {TrendsService} from './trends.service'; +import {TrendsController} from './trends.controller'; +import {CollectorModule} from "../collector/collector.module"; + +@Module({ + imports: [CollectorModule], + providers: [TrendsService], + controllers: [TrendsController], + exports: [TrendsService], +}) +export class TrendsModule { +} diff --git a/src/modules/trends/trends.service.ts b/src/modules/trends/trends.service.ts new file mode 100644 index 0000000..f0df149 --- /dev/null +++ b/src/modules/trends/trends.service.ts @@ -0,0 +1,242 @@ +import {Injectable, Logger} from '@nestjs/common'; +import {CollectorOrchestratorService, OrchestratorResult} from "../collector/collector-orchestrator.service"; +import {PrismaService} from "../../../prisma/prisma.service"; +import {TrendWhereInput} from "../../generated/prisma/models/Trend"; +import {TrendItem} from "../../common/interfaces/trend-item.interface"; +import {TextUtil} from "../../common/utils/text.util"; + +@Injectable() +export class TrendsService { + private readonly logger = new Logger(TrendsService.name); + + constructor( + private prisma: PrismaService, + private readonly orchestrator: CollectorOrchestratorService, + ) { + } + + /** + * Lấy trends từ DB với filters + */ + async getTrends(dto) { + const { + category, + source, + limit = 50, + minScore = 0, + q, + maxAgeHours = 24, + } = dto; + + const minDate = new Date(Date.now() - maxAgeHours * 60 * 60 * 1000); + const qbWhere: TrendWhereInput = {}; + qbWhere.score = {gte: minScore}; + qbWhere.sourceTimestamp = {gte: minDate}; + + if (category) { + qbWhere.category = category; + } + if (source) { + qbWhere.source = { + contains: source, + mode: 'insensitive', // Sẽ tìm thấy cả "Admin", "ADMIN", "adMin"... + }; + } + + if (q) { + qbWhere.title = { + contains: q, + mode: 'insensitive', + } + } + + const {data, total,} = await this.prisma.getManyAndCount(this.prisma.trend, + { + where: qbWhere, + orderBy: [{score: 'desc'}, {sourceTimestamp: 'desc'}], + take: limit, + }) + + return { + data, + total, + filters: {category, source, limit, minScore, q, maxAgeHours}, + }; + } + + /** + * Trigger collection thủ công + lưu vào DB + */ + async collectAndStore(source?: string): Promise { + console.debug('collectAndStore ==>: ', new Date().toISOString()); + let result: OrchestratorResult; + + if (source) { + const collectorResult = await this.orchestrator.collectFromSource(source); + result = { + items: collectorResult.items, + stats: { + totalRaw: collectorResult.items.length, + afterDedup: collectorResult.items.length, + bySource: {[source]: collectorResult.items.length}, + errors: collectorResult.error ? [collectorResult.error] : [], + durationMs: collectorResult.durationMs, + }, + }; + } else { + result = await this.orchestrator.collectAll(); + } + + // Lưu vào DB + await this.upsertTrends(result.items); + + return result; + } + + /** + * Upsert trends vào DB — update nếu đã tồn tại (by fingerprint) + */ + async upsertTrends(items: TrendItem[]): Promise { + let upsertCount = 0; + + for (const item of items) { + if (!item.title || item.title.trim().length === 0) continue; + + const fingerprint = TextUtil.fingerprint(item.title); + + try { + // Check existing by fingerprint + const existing = await this.prisma.trend.findFirst({ + where: {fingerprint}, + }); + + if (existing) { + // Update score nếu mới hơn hoặc cao hơn + if (item.score > existing.score) { + await this.prisma.trend.update({ + data: { + score: item.score, + engagement: item.engagement as any, + updatedAt: new Date(), + }, + where: {id: existing.id} + } + ); + upsertCount++; + } + } else { + // Insert new + await this.prisma.trend.create({ + data: { + title: item.title, + description: item.description || '', + url: item.url || '', + source: item.source, + score: item.score, + category: item.category || 'general', + tags: item.tags || [], + engagement: item.engagement || {}, + raw: item.raw || {}, + sourceTimestamp: item.timestamp || new Date(), + fingerprint, + } + }); + upsertCount++; + } + } catch (error) { + this.logger.warn( + `Failed to upsert trend "${item.title.substring(0, 50)}": ${error.message}`, + ); + } + } + + this.logger.log(`Upserted ${upsertCount}/${items.length} trends to DB`); + + return upsertCount; + } + + /** + * Thống kê nhanh + */ + async getStats() { + const total = await this.prisma.trend.count(); + + const last24h = await this.prisma.trend.count({ + where: { + sourceTimestamp: {gt: new Date(Date.now() - 24 * 60 * 60 * 1000)}, + }, + }); + + const byCategory = await this.prisma.trend.groupBy({ + by: ['category'], + _count: { + _all: true, + }, + _avg: { + score: true, // Trung bình cộng + }, + + }) + // .createQueryBuilder('trend') + // .select('trend.category', 'category') + // .addSelect('COUNT(*)', 'count') + // .addSelect('AVG(trend.score)', 'avgScore') + // .groupBy('trend.category') + // .getRawMany(); + + const bySource = await this.prisma.$queryRaw` + SELECT + COUNT(1)::int as count , + CASE + WHEN STRPOS(source, ':') > 0 + THEN SUBSTRING(source, 1, STRPOS(source, ':') - 1) + ELSE source + END, + source +FROM "Trend" +GROUP BY source; + ` + ; + console.log({bySource}); + + const topTrends = await this.prisma.trend.findMany({ + where: { + sourceTimestamp: {gt: new Date(Date.now() - 24 * 60 * 60 * 1000)} + }, + orderBy: {score: 'desc'}, + take: 10, + select: { + id: true, + title: true, + score: true, + category: true + } + }); + + return { + total, + last24h, + byCategory, + bySource, + topTrends, + }; + } + + /** + * Xóa trends cũ hơn N ngày + */ + async cleanupOldTrends(olderThanDays: number = 7): Promise { + const cutoff = new Date( + Date.now() - olderThanDays * 24 * 60 * 60 * 1000, + ); + + const {count} = await this.prisma.trend.deleteMany({ + where: { + sourceTimestamp: {lt: cutoff}, + } + }) + + this.logger.log(`Cleaned up ${count} old trends`); + return count; + } +} diff --git a/src/modules/x-cache/x-cache.module.ts b/src/modules/x-cache/x-cache.module.ts new file mode 100644 index 0000000..bcacd63 --- /dev/null +++ b/src/modules/x-cache/x-cache.module.ts @@ -0,0 +1,10 @@ +import {Global, Module} from '@nestjs/common'; +import {XCacheService} from "./x-cache.service"; + +@Global() +@Module({ + providers: [XCacheService], + exports: [XCacheService], +}) +export class XCacheModule { +} diff --git a/src/modules/x-cache/x-cache.service.ts b/src/modules/x-cache/x-cache.service.ts new file mode 100644 index 0000000..8b772fa --- /dev/null +++ b/src/modules/x-cache/x-cache.service.ts @@ -0,0 +1,44 @@ +import {Inject, Injectable} from "@nestjs/common"; +import {Cache, CACHE_MANAGER} from "@nestjs/cache-manager"; + +@Injectable() +export class XCacheService { + constructor( + @Inject(CACHE_MANAGER) private cacheManager: Cache + ) { + } + + async delCachedKey(key: string) { + return await this.cacheManager.del(key); // Retrieve data + } + + async getCachedData(key: string) { + return await this.cacheManager.get(key); // Retrieve data + } + + async setCachedKey(key: string, data, ttl_by_sec = 1000) { + await this.cacheManager.set(key, data, ttl_by_sec * 1000); // Save to Redis + return data; + } + + async setCacheTweetUrlById(tweetId, tweetUrl) { + await this.setCachedKey(`x_tweetId:${tweetId}`, tweetUrl, 24*3600); + return {tweetId, tweetUrl}; + } + + async getCacheTweetUrlById(tweetId) { + return await this.getCachedData(`x_tweetId:${tweetId}`) as string; + } + + async incrCountCollectNewsapi() { + //this.cacheManager.c + } + + async setTwitterRefreshToken(token: string) { + // return this.setCachedKey('twitter_refresh_token', token, 30 * 24 * 1000); + } + + async getTwitterRefreshToken() { + // return this.setCachedKey('twitter_refresh_token', token, 30 * 24 * 1000); + } +} diff --git a/src/modules/x-reader/x-reader.module.ts b/src/modules/x-reader/x-reader.module.ts new file mode 100644 index 0000000..fd58973 --- /dev/null +++ b/src/modules/x-reader/x-reader.module.ts @@ -0,0 +1,11 @@ +import {Global, Module} from '@nestjs/common'; +import {XReaderService} from './x-reader.service'; +import {XCacheService} from "../x-cache/x-cache.service"; + +@Global() +@Module({ + providers: [XReaderService, XCacheService], + exports: [XReaderService], +}) +export class XReaderModule { +} diff --git a/src/modules/x-reader/x-reader.service.ts b/src/modules/x-reader/x-reader.service.ts new file mode 100644 index 0000000..2c14516 --- /dev/null +++ b/src/modules/x-reader/x-reader.service.ts @@ -0,0 +1,357 @@ +import {chromium} from 'playwright'; +import {Injectable} from "@nestjs/common"; +import axios from "axios"; +import {XCacheService} from "../x-cache/x-cache.service"; + +@Injectable() +export class XReaderService { + constructor(private readonly cacheService: XCacheService) { + } + + async readXPostViaBrowserV2(url) { + // Normalize URL + console.log(`[X] XReaderService -> readXPostViaBrowserV2...`); + + url = url.replace('twitter.com', 'x.com').split('?')[0]; + // url = url.replace('x.com', 'nitter.net').split('?')[0]; + + const match = url.match(/status\/(\d+)/); + if (!match) throw new Error('URL X không hợp lệ'); + const tweetId = match[1]; + console.log({tweetId, url}); + const browser = await chromium.launch({ + headless: false, + args: ['--no-sandbox', '--disable-blink-features=AutomationControlled'] + }); + + const context = await browser.newContext({ + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', + viewport: {width: 1280, height: 900}, + locale: 'en-US' + }); + + // Inject cookie auth nếu có + // if (process.env.X_AUTH_TOKEN) { + // await context.addCookies([ + // { + // name: 'auth_token', + // value: process.env.X_AUTH_TOKEN, + // domain: '.x.com', + // path: '/', + // httpOnly: true, + // secure: true, + // sameSite: 'None' + // }, + // { + // name: 'ct0', + // value: process.env.X_CT0, + // domain: '.x.com', + // path: '/', + // secure: true, + // sameSite: 'Lax' + // } + // ]); + // } + + const page = await context.newPage(); + try { + await page.goto(url, {waitUntil: 'domcontentloaded', timeout: 45000}); + console.log(`==> 1`); + await Promise.race([ + page.waitForSelector('article[data-testid="tweet"]', {timeout: 20000}), + page.waitForSelector('[data-testid="tweetText"]', {timeout: 20000}), + + //nitter + // page.waitForSelector('div.main-tweet', {timeout: 20000}) + ]) + console.log(`==> 2`); + // Đợi media load + await page.waitForTimeout(2000); + console.log(`==> 2.1`); + const data = await page.evaluate( + (tweetId) => { + console.log(`==> 2.2`); + // ===== HELPER: parse 1 block tweet (dùng cho cả main + quoted) ===== + const parseBlock = (root, isQuoted = false) => { + console.log(`==> 2.3`); + if (!root) return null; + console.log(`==> 3`); + // Text + const textEl = root.querySelector('[data-testid="tweetText"]'); + const text = textEl?.innerText || ''; + console.log(`==> 4`); + // User info + const userEl = root.querySelector('[data-testid="User-Name"]'); + const userLines = userEl?.innerText.split('\n') || []; + const author = userLines[0] || 'Unknown'; + const handle = userLines.find(s => s.startsWith('@')) || ''; + console.log(`==> 5`); + // Time + const timeEl = root.querySelector('time'); + const time = timeEl?.getAttribute('datetime') || ''; + console.log(`==> 6`); + // Images — lọc avatar, emoji, card icons + const images = Array.from(root.querySelectorAll('img')) + // @ts-ignore + .map(i => i.src) + .filter(src => + src.includes('pbs.twimg.com/media') && + !src.includes('profile') && + !src.includes('emoji') + ); + console.log(`==> 7`); + // Videos + const videos = Array.from(root.querySelectorAll('video')) + // @ts-ignore + .map(v => v.src || v.querySelector('source')?.src) + .filter(Boolean); + + // Video poster (ảnh thumbnail khi không lấy được URL video) + const videoPosters = Array.from(root.querySelectorAll('video')) + // @ts-ignore + .map(v => v.poster) + .filter(Boolean); + console.log(`==> 8`); + return { + text, + author, + handle, + time, + images: [...new Set(images)], + videos, + videoPosters, + ...(isQuoted ? {} : {}) + }; + }; + + // ===== MAIN TWEET ===== + const article = document.querySelector('article[data-testid="tweet"]'); + console.log(`==> 2.2.1`); + if (!article) return null; + console.log(`==> 9`); + // ===== TÌM QUOTED TWEET ===== + // Quoted tweet là div[role="link"] bên trong article chính, + // có chứa [data-testid="tweetText"] hoặc User-Name riêng + let quotedEl = null; + const innerLinks = article.querySelectorAll('div[role="link"]'); + console.log(`==> 10`); + for (const el of innerLinks) { + // Quoted phải có cả User-Name (tránh nhầm với card link) + if ( + el.querySelector('[data-testid="User-Name"]') && + (el.querySelector('[data-testid="tweetText"]') || + el.querySelector('img[src*="pbs.twimg.com/media"]')) + ) { + // @ts-ignore + quotedEl = el; + break; + } + } + console.log(`==> 11`); + + // ===== CLONE article và REMOVE quoted để parse main cho sạch ===== + // (tránh ảnh/text của quoted bị gộp vào main) + const articleClone = article.cloneNode(true) as Document; + if (quotedEl) { + const clonedQuoted = articleClone.querySelector('div[role="link"]'); + // Tìm đúng node quoted trong clone + const innerLinksClone = articleClone.querySelectorAll('div[role="link"]'); + for (const el of innerLinksClone) { + if ( + el.querySelector('[data-testid="User-Name"]') && + (el.querySelector('[data-testid="tweetText"]') || + el.querySelector('img[src*="pbs.twimg.com/media"]')) + ) { + el.remove(); + break; + } + } + } + console.log(`==> 12`); + + const main = parseBlock(articleClone); + const quoted = quotedEl ? parseBlock(quotedEl, true) : null; + + // ===== STATS (like, retweet, reply, view) ===== + const stats: any = {}; + const statGroup = article.querySelector('[role="group"]'); + if (statGroup) { + console.log(`==> 13`); + + const buttons = statGroup.querySelectorAll('[data-testid]'); + buttons.forEach(btn => { + const testId = btn.getAttribute('data-testid'); + const label = btn.getAttribute('aria-label') || ''; + if (testId === 'reply') stats.replies = label; + if (testId === 'retweet') stats.retweets = label; + if (testId === 'like') stats.likes = label; + }); + // View count thường nằm trong link riêng + const viewLink = article.querySelector('a[href*="/analytics"]'); + if (viewLink) stats.views = viewLink.getAttribute('aria-label') || ''; + } + + // ===== Extract quoted URL từ href (nếu có) ===== + let quotedUrl = null; + if (quotedEl) { + console.log(`==> 14`); + + // @ts-ignore + const href = quotedEl.querySelector('a[href*="/status/"]')?.getAttribute('href'); + if (href) { // @ts-ignore + quotedUrl = 'https://x.com' + href.split('/photo')[0].split('/video')[0]; + } + } + + return { + ...main, + stats, + quoted: quoted ? {...quoted, url: quotedUrl} : {}, + via: 'browser', + tweetId: tweetId, + }; + } + , tweetId); + + await browser.close(); + if (!data) throw new Error(`Không parse được tweet ${tweetId} (có thể bị ẩn/xoá)`); + return data; + } catch (err) { + try { + await page.screenshot({path: `./downloads/x_error_${tweetId}.png`}); + console.log(`📸 Screenshot: ./downloads/x_error_${tweetId}.png`); + } catch { + } + await browser.close(); + console.log(err); + throw new Error(`Lỗi đọc X: ${err.message}`); + } + } + + async readXPostViaApi(url) { + console.log(`[X] XReaderService -> readXPostViaApi...`); + + // Extract tweet ID từ URL + const match = url.match(/status\/(\d+)/); + if (!match) throw new Error('URL X không hợp lệ'); + const tweetId = match[1]; + + // Token này là public token của syndication API (stable nhiều năm) + const token = ((Number(tweetId) / 1e15) * Math.PI) + .toString(6 ** 2) + .replace(/(0+|\.)/g, ''); + + const apiUrl = `https://cdn.syndication.twimg.com/tweet-result?id=${tweetId}&token=${token}&lang=en`; + + try { + const {data} = await axios.get(apiUrl, { + headers: { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + 'Accept': 'application/json' + }, + timeout: 15000 + }); + + // ===== MEDIA ===== + const images = (data.mediaDetails || []) + .filter(m => m.type === 'photo') + .map(m => m.media_url_https + '?format=jpg&name=large'); // lấy size lớn nhất + + const videos = (data.mediaDetails || []) + .filter(m => ['video', 'animated_gif'].includes(m.type)) + .map(m => { + const mp4 = (m.video_info?.variants || []) + .filter(v => v.content_type === 'video/mp4') + .sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0]; + return mp4?.url; + }) + .filter(Boolean); + + // ===== QUOTED TWEET (tweet trích dẫn) ===== + let quoted = null; + if (data.quoted_tweet) { + const q = data.quoted_tweet; + // @ts-ignore + quoted = { + text: q.text, + author: q.user?.name, + handle: '@' + q.user?.screen_name, + images: (q.mediaDetails || []) + .filter(m => m.type === 'photo') + .map(m => m.media_url_https) + }; + } + + // ===== CARD (link preview) ===== + let card = null; + if (data.card) { + const values = data.card.binding_values || {}; + // @ts-ignore + card = { + title: values.title?.string_value, + description: values.description?.string_value, + url: values.card_url?.string_value, + image: values.thumbnail_image_large?.image_value?.url + }; + } + + // ===== POLL ===== + let poll = null; + if (data.card?.name?.includes('poll')) { + const v = data.card.binding_values; + // @ts-ignore + poll = { + choices: Object.keys(v) + .filter(k => k.startsWith('choice') && k.endsWith('_label')) + .map(k => v[k].string_value) + }; + } + + return { + via: 'api', + apiUrl, + tweetId: tweetId, + text: data.text || '', + author: data.user?.name || 'Unknown', + handle: '@' + (data.user?.screen_name || ''), + avatar: data.user?.profile_image_url_https, + verified: data.user?.verified || data.user?.is_blue_verified, + time: data.created_at || '', + lang: data.lang, + images, + videos, + quoted, + card, + poll, + url: `https://x.com/${data.user?.screen_name}/status/${tweetId}`, + // Raw để debug nếu cần + _raw: data + }; + } catch (err) { + if (err.response?.status === 404) { + throw new Error('Tweet không tồn tại hoặc bị xoá'); + } + throw new Error(`Lỗi syndication API: ${err.message}`); + } + } + + async readXPost(url, crawlerType: string = 'any') { + console.log(`[X] XReaderService -> readXPost...`); + url = url.replace('twitter.com', 'x.com').split('?')[0]; + const match = url.match(/status\/(\d+)/); + if (!match) throw new Error('URL X không hợp lệ'); + const tweetId = match[1]; + console.log({tweetId, url}); + await this.cacheService.setCacheTweetUrlById(tweetId, url); + if (crawlerType === 'browser') { + return await this.readXPostViaBrowserV2(url); + } + try { + console.log('[X] Thử syndication API...'); + return await this.readXPostViaApi(url); + } catch (err) { + console.log(`[X] API fail (${err.message}), fallback sang browser...`); + return await this.readXPostViaBrowserV2(url); + } + } +} diff --git a/src/modules/x-uploader/x-image.upload.service.ts b/src/modules/x-uploader/x-image.upload.service.ts new file mode 100644 index 0000000..343c65c --- /dev/null +++ b/src/modules/x-uploader/x-image.upload.service.ts @@ -0,0 +1,53 @@ +import {Injectable, Logger} from "@nestjs/common"; +import {TwitterClient} from "../social-api/twitter.client"; +import axios from "axios"; + +@Injectable() +export class XImageUploadService { + private readonly logger = new Logger(XImageUploadService.name); + + constructor(private readonly client: TwitterClient) { + } + + async uploadImageFromUrl(imageUrl: string) { + // 1. download image + const response = await axios.get(imageUrl, { + responseType: 'arraybuffer', + }); + + // // 3. download buffer + // const img = await axios.get(fileUrl, { + // responseType: 'arraybuffer', + // }); + // + // const buffer = Buffer.from(img.data); + const buffer = Buffer.from(response.data); + + await this.client.uploadImageV1(buffer, { + media_type: 'image/png', + media_category: 'tweet_image', + + }); + + // return res.data.media_id_string; + } + + async getTelegramFileUrl(fileId: string): Promise { + const token = process.env.TELEGRAM_BOT_TOKEN; + + const res = await axios.get( + `https://api.telegram.org/bot${token}/getFile?file_id=${fileId}` + ); + + const filePath = res.data.result.file_path; + + return `https://api.telegram.org/file/bot${token}/${filePath}`; + } + + async uploadFromTelegram(fileId: string) { + const url = await this.getTelegramFileUrl(fileId); + const mediaId = await this.uploadImageFromUrl(url); + + return mediaId; + } +} diff --git a/src/modules/x-uploader/x-uploader.module.ts b/src/modules/x-uploader/x-uploader.module.ts new file mode 100644 index 0000000..433c4ea --- /dev/null +++ b/src/modules/x-uploader/x-uploader.module.ts @@ -0,0 +1,9 @@ +import {Module} from "@nestjs/common"; + +@Module({ + providers: [], + exports: [], +}) +export class XUploaderModule { + +} diff --git a/src/shared/ai.service.ts b/src/shared/ai.service.ts new file mode 100644 index 0000000..c4087ec --- /dev/null +++ b/src/shared/ai.service.ts @@ -0,0 +1,332 @@ +// src/shared/ai.service.ts +import "dotenv/config"; +import {Injectable, OnModuleInit} from '@nestjs/common'; +import {GenerativeModel, GoogleGenerativeAI} from '@google/generative-ai'; +import {OpenAI} from "openai"; +import {_JsonParseSafe} from "./helper"; +import {GrokProvider} from "../modules/content-writer/providers/grok.provider"; + +@Injectable() +export class AIService implements OnModuleInit { + private genAI: GoogleGenerativeAI; + private model: GenerativeModel; + private deepSeekModel; + private chatgptModel; + + constructor(private grokProvider: GrokProvider) { + } + + private prompts_config = { + crypto: "Written in the style of a financial expert, using many crypto terms, dramatic tone, adding hashtags like #Bitcoin #CryptoNews #Web3...", + war: "Written in the style of war news, objective but intense, using icons like 🚨, 🎖️, 📍.", + general: "Written in a humorous style, approachable to the online community, internet-friendly tone." + }; + private INTENT_MAP = { + just_in: "Write short breaking news immediately when an event just happened or is trending. Requirements: Start with: 🚨 JUST IN or 🔴 BREAKING, First sentence MUST answer: WHO + WHAT is happening, short: 2–4 sentences total, Tone: urgent, factual, no speculation", + breaking_news: "Write short, urgent, high-impact with strong hook.", + analysis: "Focus on insights, implications, and deeper meaning.", + educational: "Explain clearly in simple terms.", + thread: "Format as X thread, structured points.", + default: "" + }; + private preferLanguage = { + 'vi': 'việt nam', + 'vn': 'việt nam', + 'en': 'english', + 'us': 'english', + 'jp': 'japanese', + 'kr': 'korean', + } + + + onModuleInit() { + // "models/gemini-2.5-flash", + // "models/gemini-2.5-pro", + // "models/gemini-2.0-flash", + // "models/gemini-2.0-flash-001", + // "models/gemini-2.0-flash-lite-001", + // "models/gemini-2.0-flash-lite", + // "models/gemini-2.5-flash-lite", + // Khởi tạo SDK với API Key từ file .env + this.genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!); + + // Sử dụng gemini-1.5-flash cho tốc độ nhanh và giá rẻ (hoặc miễn phí) + const modelName: string = 'gemini-2.5-flash'; + + this.model = this.genAI.getGenerativeModel({ + model: modelName, + generationConfig: { + responseMimeType: "text/plain", + // Ép AI trả về JSON chuẩn + } + }); + console.log('Initializing gemini model...'); + + this.getDeepSeekModel(); + this.getChatgptModel(); + } + + + async getDeepSeekModel() { + if (!this.deepSeekModel) { + this.deepSeekModel = new OpenAI({ + baseURL: 'https://api.deepseek.com', + apiKey: process.env.DEEPSEEK_API_KEY, + }); + console.log('Initializing deepseek model...'); + } + return this.deepSeekModel; + + } + + async getChatgptModel() { + if (!this.chatgptModel) { + this.chatgptModel = new OpenAI({ + apiKey: process.env.CHATGPT_API_KEY, + }); + } + console.log('Initializing chatgpt model...'); + return this.chatgptModel; + } + + async listAvailableModels() { + try { + const response = await fetch(`https://generativelanguage.googleapis.com/v1/models?key=${process.env.GEMINI_API_KEY}`); + const data = await response.json(); + return data.models.map(m => m.name) + } catch (e) { + console.error('Không thể lấy danh sách model:', e); + } + } + + async detectStyle(text) { + if (/ btc| bitcoin| eth | crypto| defi| airdrop| xrp| web3/i.test(text)) return "crypto"; + if (/ war| attack| missile| army| weapon| conflict/i.test(text)) return "war"; + return "general"; + + } + + async detectIntent(text) { + if (/just in|alert/i.test(text)) return "just_in"; + if (/ breaking|just in|urgent|alert/i.test(text)) return "breaking_news"; + if (/why|how|analysis|insight/i.test(text)) return "analysis"; + if (/guide|what is|learn/i.test(text)) return "educational"; + if (/thread/i.test(text)) return "thread"; + return "default"; + } + + async detectTone(text) { + if (/pump|moon|bull/i.test(text)) return "bullish"; + if (/dump|crash|fear/i.test(text)) return "bearish"; + return "neutral"; + } + + // @ts-ignore + //private genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); + + async askQuestion(question) { + console.log(`câu hỏi: ${question} `); + + const finalUserPrompt = ` + Hãy trả lời yêu cầu sau: ${question} + `; + + const completion = await this.deepSeekModel.chat.completions.create({ + model: 'deepseek-chat', + messages: [ + {role: 'system', content: `Dùng văn bản thuần túy, không dùng định dạng Markdown`}, + {role: 'user', content: finalUserPrompt} + ], + },) + + let text = completion.choices[0]?.message?.content?.trim() ?? '' + console.log({finalUserPrompt, text}); + const tokensUsed= completion.usage?.total_tokens ?? 0; + return { + prompt: finalUserPrompt, + content: text, + title: question, + tokensUsed, + model: 'deepseek-chat', + } + } + async askQuestionenXcontext(question) { + const xContext = await this.grokProvider.enrichXContext(question); + return { + content: xContext, + title: question, + model: this.grokProvider.getModelName(), + } + } + + async analyzeTrend(content: string, style: string): Promise { + const finalUserPrompt = ` + Bạn là một chuyên gia phân tích tin tức. + Nội dung tin: ${content} + Chủ đề yêu cầu: ${style} + + Hãy phân tích và trả về JSON: + 1. is_relevant: (true/false) tin này có đúng chủ đề không? + 2. hot_score: (1-10) độ hot của tin. + 3. summary: tóm tắt ngắn gọn dưới 50 chữ. + 4. keywords: các từ khóa chính để tìm video TikTok. + `; + const sysPrompt = ` + Yêu cầu + ` + const userPrompt = ` + + Bạn là một hệ thống phân tích tin tức tự động. +Khi người dùng yêu cầu tìm: ${content}, bạn phải sử dụng công cụ tìm kiếm web để lấy thông tin mới nhất. + +Yêu cầu nghiêm ngặt về đầu ra: +- Chỉ trả về duy nhất một đối tượng JSON hợp lệ. +- Không được có bất kỳ ký tự, dòng text, giải thích hay markdown nào trước hoặc sau JSON. +- Không được dùng \`\`\`json ... \`\`\`. +- JSON phải đúng cú pháp, dùng dấu nháy kép ("). + + Cấu trúc JSON bắt buộc: +{ + "news": [ + { + "title": "tiêu đề tin", + "hot_score": 1-10, + "summary": "tóm tắt dưới 50 chữ", + "keywords": ["từ khóa 1", "từ khóa 2"], + "source": "tên nguồn báo hoặc web search và link", + "is_relevant": true, + "day": "ngày tin tức xuất hiện" + } + ] +} + +Chỉ trả về JSON. Không thêm bất kỳ điều gì khác. + + +Yêu cầu: chỉ lấy từ các nguồn báo uy tín + `; + // Ưu tiên tin về cá kinh tế, văn hóa, công nghệ hoặc xã hội. + + // const result = await this.deepSeekModel.(prompt); + const grokResult = await this.grokProvider.searchXContext(content); + return { + // prompt: userPrompt, + title: content, + result: grokResult + } + const completion = await this.deepSeekModel.chat.completions.create({ + model: 'deepseek-chat', + messages: [ + // {role: 'system', content: system_prompt}, + {role: 'user', content: userPrompt} + ], + },) + let respMessageContent = '' + completion.choices[0].message.content; + console.log('analyzeTrend=>response'); + console.log({userPrompt}); + console.log('respMessageContent:==>', respMessageContent); + const result = _JsonParseSafe(respMessageContent); + return { + prompt: userPrompt, + title: content, + result + } + } + + async generateContentViaDeepseek(topic: string, style: string, contentLang: string = 'en') { + const _style = await this.detectStyle(topic); + const _intent = await this.detectIntent(topic); + const _tone = await this.detectTone(topic); + const selectedStyle = this.prompts_config[_style] || this.prompts_config.general; + const selectedIntent = this.INTENT_MAP[_intent] || this.INTENT_MAP.default; + const selectedContentLang = this.preferLanguage[contentLang] || this.preferLanguage.en; + + console.log(`đang viết bài cho chủ đề băng deepseek: ${topic} với ngôn ngữ ${selectedContentLang} style ${_style} intent: ${_intent}`); + + const system_prompt = ` + You are a strictly monolingual AI. + +TARGET_LANGUAGE = ${selectedContentLang} + +You MUST: +- Respond ONLY in TARGET_LANGUAGE +- Ignore all other languages +- Never mix languages +- The names of people or countries may retain their original names. + ` + const finalUserPrompt = ` + [Target Language: ${selectedContentLang}] + IMPORTANT: Your previous answer violated language rules. + Rewrite strictly in ${selectedContentLang} only. + +Based on the following topic: "${topic}" with content in the language ${selectedContentLang} +Write a complete twitter post with content in the language ${selectedContentLang}. +Requirements: +- ${selectedStyle} +- ${selectedIntent} +- Length: Approximately 50-100-200-300-400 words depending on context or user input. +- Structure: Title -> Main content -> Interactive question -> Hashtag. +- If the topic is crypto/finance: Append this disclaimer: “⚠️ This content is for informational purposes only, not financial advice. DYOR.”,if not, skip disclaimer. + `; + + const completion = await this.deepSeekModel.chat.completions.create({ + model: 'deepseek-chat', + messages: [ + {role: 'system', content: system_prompt}, + {role: 'user', content: finalUserPrompt} + ], + },) + + let text = completion.choices[0].message.content; + const tokensUsed= completion.usage?.total_tokens ?? 0; + console.log({finalUserPrompt, text}); + return { + prompt: finalUserPrompt, + // content: text, + topic: topic, + title: topic, + final: text, + draft: undefined, + reviewNotes: undefined, + detectedStyle: selectedStyle, + detectedTone: selectedIntent, + tokensUsed, + model: 'deepseek-chat', + } + } + + // Thêm vào src/shared/ai.service.ts + + async generateContentViaGemini(topic: string, style: string, contentLang: string = 'vi'): Promise { + const _style = await this.detectStyle(topic) + const selectedPrompt = this.prompts_config[_style] || this.prompts_config.general; + // const selectedPrompt = this.prompts_config[style] || this.prompts_config.general; + const selectedContentLang = this.preferLanguage[contentLang] || this.preferLanguage.vi; + + + const finalUserPrompt = ` + [Target Language: ${selectedContentLang}] + IMPORTANT: Your previous answer violated language rules. + Rewrite strictly in ${selectedContentLang} only. + +Based on the following topic: "${topic}" with content in the language ${selectedContentLang} +Write a complete Facebook post with content in the language ${selectedContentLang}. +Requirements: +- ${selectedPrompt} +- Length: Approximately 200-300-400 words depending on context. +- Structure: Title -> Main content -> Interactive question -> Hashtag. +- If the topic is crypto/finance: Append this disclaimer: “⚠️ This content is for informational purposes only, not financial advice. DYOR.”,if not, skip disclaimer. +- response is text + `; + + console.log('finalPrompt==>', finalUserPrompt) + console.log(`đang viết bài cho chủ đề băng gemini: ${topic} với ngôn ngữ ${selectedContentLang} style ${_style}`); + + const result = await this.model.generateContent(finalUserPrompt); + return { + prompt: finalUserPrompt, + content: result.response.text() + } + } + +} diff --git a/src/shared/helper.ts b/src/shared/helper.ts new file mode 100644 index 0000000..a9812c7 --- /dev/null +++ b/src/shared/helper.ts @@ -0,0 +1,69 @@ +export const _toNum = (value) => { + return 1 * value; +} + +export const enforceSingleCashtag = (text) => { + const matches = text.match(/\$[A-Z]+/g); + if (!matches) return text; + + const first = matches[0]; + return text.replace(/\$[A-Z]+/g, (m, i) => (i === 0 ? m : "")); +} +export const normalizeTagsSingleCashtag = (text) => { + let count = 0; + + return text.replace(/\$([A-Z]+)/g, (match, token) => { + count++; + if (count === 1) return `$${token}`; // giữ 1 cashtag + return `#${token}`; // còn lại chuyển thành hashtag + }); +} + +export const _JsonParseSafe = (jsonString: string, defaultReturn = {}, allowThrowEx = false) => { + try { + return JSON.parse(jsonString); + } catch (error) { + if (allowThrowEx) { + throw error; + } + console.error('_JsonParseSafe_error:', error.message); + return defaultReturn; + } +} + +export const _JsonToStr = (jsonObj) => { + return JSON.stringify(jsonObj, null, 2); +} +export const _shuffle = (array) => { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; // Swap elements + } + return array; +} + +export function rand(min, max) { + return Math.floor(Math.random() * (max - min) + min); +} + +export function pick(arr) { + return arr[rand(0, arr.length)]; +} + +export const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +export const getLanguageByJSTTime = () => { + const now = new Date(); + // Lấy giờ hiện tại theo chuẩn UTC (0-23) + const utcHour = now.getUTCHours(); + + // Nhật 6h sáng (6:00) -> UTC là 21:00 (hôm trước) + // Nhật 6h tối (18:00) -> UTC là 09:00 (sáng nay) + + // Nếu giờ UTC từ 21h đêm đến trước 9h sáng hôm sau + if (utcHour >= 21 || utcHour < 9) { + return 'ja'; + } + + return 'en'; +} \ No newline at end of file diff --git a/src/shared/pg.post.service.ts b/src/shared/pg.post.service.ts new file mode 100644 index 0000000..ef3c0bc --- /dev/null +++ b/src/shared/pg.post.service.ts @@ -0,0 +1,30 @@ +import {Injectable} from "@nestjs/common"; +import {PrismaService} from "../../prisma/prisma.service"; +import {Post, Prisma} from "../generated/prisma/client"; + +@Injectable() +export class PgPostService { + constructor(private prisma: PrismaService) { + } + + async post(postWhereUniqueInput: Prisma.PostWhereUniqueInput): Promise { + return this.prisma.post.findUnique({ + where: postWhereUniqueInput, + }); + } + + async createPost(data: Prisma.PostCreateInput): Promise { + return this.prisma.post.create({ + data, + }); + } + + async updatePost(id, data: Prisma.PostUpdateInput): Promise { + return this.prisma.post.update({ + data, + where: { + id + } + }); + } +} diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts new file mode 100644 index 0000000..a767839 --- /dev/null +++ b/test/app.e2e-spec.ts @@ -0,0 +1,29 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); + + afterEach(async () => { + await app.close(); + }); +}); diff --git a/test/jest-e2e.json b/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..49f0ce8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false, + "preserveWatchOutput": true, + } +}