3 Commits

Author SHA1 Message Date
dependabot[bot] 383d69f2dd Bump drizzle-kit from 1.0.0-beta.9-e89174b to 1.0.0-beta.15-9485290
Bumps [drizzle-kit](https://github.com/drizzle-team/drizzle-orm) from 1.0.0-beta.9-e89174b to 1.0.0-beta.15-9485290.
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/commits)

---
updated-dependencies:
- dependency-name: drizzle-kit
  dependency-version: 1.0.0-beta.15-9485290
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 21:02:57 +00:00
melod1n 1f96e3553e * environment variable to enable/disable processing links in messages (for downloading videos)
* bot will not automatically download video from youtube or load it from cache. It will fetch video info first, then suggest to download or get video from cache (with retry options)
* some rewriting in sending/editing/replying to messages
2026-03-03 00:01:44 +03:00
melod1n 3f34a48d41 bump libs 2026-03-02 23:58:57 +03:00
21 changed files with 478 additions and 151 deletions
+13 -13
View File
@@ -5,18 +5,18 @@
"": { "": {
"name": "tg-chat-bot", "name": "tg-chat-bot",
"dependencies": { "dependencies": {
"@google/genai": "^1.41.0", "@google/genai": "^1.42.0",
"@libsql/client": "^0.17.0", "@libsql/client": "^0.17.0",
"@mistralai/mistralai": "^1.14.0", "@mistralai/mistralai": "^1.14.0",
"@napi-rs/canvas": "^0.1.91", "@napi-rs/canvas": "^0.1.95",
"axios": "^1.13.5", "axios": "^1.13.5",
"dotenv": "^17.2.4", "dotenv": "^17.3.1",
"drizzle-orm": "^1.0.0-beta.9-e89174b", "drizzle-orm": "^1.0.0-beta.9-e89174b",
"emoji-regex": "^10.6.0", "emoji-regex": "^10.6.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"ollama": "^0.6.3", "ollama": "^0.6.3",
"openai": "^6.21.0", "openai": "^6.25.0",
"puppeteer": "^24.37.2", "puppeteer": "^24.37.5",
"puppeteer-extra": "^3.3.6", "puppeteer-extra": "^3.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2", "puppeteer-extra-plugin-stealth": "^2.11.2",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
@@ -30,12 +30,12 @@
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.9", "@types/bun": "^1.3.9",
"@types/fluent-ffmpeg": "^2.1.28", "@types/fluent-ffmpeg": "^2.1.28",
"@types/node": "^25.2.3", "@types/node": "^25.3.0",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@typescript-eslint/eslint-plugin": "^8.55.0", "@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.55.0", "@typescript-eslint/parser": "^8.56.1",
"drizzle-kit": "^1.0.0-beta.9-e89174b", "drizzle-kit": "^1.0.0-beta.9-e89174b",
"eslint": "^9.39.2", "eslint": "^9.39.3",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
}, },
@@ -156,7 +156,7 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
"@google/genai": ["@google/genai@1.42.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-+3nlMTcrQufbQ8IumGkOphxD5Pd5kKyJOzLcnY0/1IuE8upJk5aLmoexZ2BJhBp1zAjRJMEB4a2CJwKI9e2EYw=="], "@google/genai": ["@google/genai@1.43.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-hklCsJNdMlDM1IwcCVcGQFBg2izY0+t5BIGbRsxi2UnKi6AGKL7pqJqmBDNRbw0bYCs4y3NA7TB+fkKfP/Nrdw=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
@@ -248,7 +248,7 @@
"@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="],
"@mistralai/mistralai": ["@mistralai/mistralai@1.14.0", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-6zaj2f2LCd37cRpBvCgctkDbXtYBlAC85p+u4uU/726zjtsI+sdVH34qRzkm9iE3tRb8BoaiI0/P7TD+uMvLLQ=="], "@mistralai/mistralai": ["@mistralai/mistralai@1.14.1", "", { "dependencies": { "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ=="],
"@napi-rs/canvas": ["@napi-rs/canvas@0.1.95", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.95", "@napi-rs/canvas-darwin-arm64": "0.1.95", "@napi-rs/canvas-darwin-x64": "0.1.95", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.95", "@napi-rs/canvas-linux-arm64-gnu": "0.1.95", "@napi-rs/canvas-linux-arm64-musl": "0.1.95", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.95", "@napi-rs/canvas-linux-x64-gnu": "0.1.95", "@napi-rs/canvas-linux-x64-musl": "0.1.95", "@napi-rs/canvas-win32-arm64-msvc": "0.1.95", "@napi-rs/canvas-win32-x64-msvc": "0.1.95" } }, "sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w=="], "@napi-rs/canvas": ["@napi-rs/canvas@0.1.95", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.95", "@napi-rs/canvas-darwin-arm64": "0.1.95", "@napi-rs/canvas-darwin-x64": "0.1.95", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.95", "@napi-rs/canvas-linux-arm64-gnu": "0.1.95", "@napi-rs/canvas-linux-arm64-musl": "0.1.95", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.95", "@napi-rs/canvas-linux-x64-gnu": "0.1.95", "@napi-rs/canvas-linux-x64-musl": "0.1.95", "@napi-rs/canvas-win32-arm64-msvc": "0.1.95", "@napi-rs/canvas-win32-x64-msvc": "0.1.95" } }, "sha512-lkg23ge+rgyhgUwXmlbkPEhuhHq/hUi/gXKH+4I7vO+lJrbNfEYcQdJLIGjKyXLQzgFiiyDAwh5vAe/tITAE+w=="],
@@ -318,7 +318,7 @@
"@types/mssql": ["@types/mssql@9.1.8", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-mt9h5jWj+DYE5jxnKaWSV/GqDf9FV52XYVk6T3XZF69noEe+JJV6MKirii48l81+cjmAkSq+qeKX+k61fHkYrQ=="], "@types/mssql": ["@types/mssql@9.1.8", "", { "dependencies": { "@types/node": "*", "tarn": "^3.0.1", "tedious": "*" } }, "sha512-mt9h5jWj+DYE5jxnKaWSV/GqDf9FV52XYVk6T3XZF69noEe+JJV6MKirii48l81+cjmAkSq+qeKX+k61fHkYrQ=="],
"@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
"@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="],
@@ -376,7 +376,7 @@
"asynckit": ["asynckit@0.4.0", "", {}, "sha1-x57Zf380y48robyXkLzDZkdLS3k="], "asynckit": ["asynckit@0.4.0", "", {}, "sha1-x57Zf380y48robyXkLzDZkdLS3k="],
"axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="], "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
"b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="], "b4a": ["b4a@1.7.3", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q=="],
+31 -21
View File
@@ -8,11 +8,11 @@
"name": "tg-chat-bot", "name": "tg-chat-bot",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@google/genai": "^1.42.0", "@google/genai": "^1.43.0",
"@libsql/client": "^0.17.0", "@libsql/client": "^0.17.0",
"@mistralai/mistralai": "^1.14.0", "@mistralai/mistralai": "^1.14.1",
"@napi-rs/canvas": "^0.1.95", "@napi-rs/canvas": "^0.1.95",
"axios": "^1.13.5", "axios": "^1.13.6",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"drizzle-orm": "^1.0.0-beta.9-e89174b", "drizzle-orm": "^1.0.0-beta.9-e89174b",
"emoji-regex": "^10.6.0", "emoji-regex": "^10.6.0",
@@ -33,11 +33,11 @@
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.9", "@types/bun": "^1.3.9",
"@types/fluent-ffmpeg": "^2.1.28", "@types/fluent-ffmpeg": "^2.1.28",
"@types/node": "^25.3.0", "@types/node": "^25.3.3",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1", "@typescript-eslint/parser": "^8.56.1",
"drizzle-kit": "^1.0.0-beta.9-e89174b", "drizzle-kit": "^1.0.0-beta.15-9485290",
"eslint": "^9.39.3", "eslint": "^9.39.3",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
@@ -996,9 +996,9 @@
} }
}, },
"node_modules/@google/genai": { "node_modules/@google/genai": {
"version": "1.42.0", "version": "1.43.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.42.0.tgz", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.43.0.tgz",
"integrity": "sha512-+3nlMTcrQufbQ8IumGkOphxD5Pd5kKyJOzLcnY0/1IuE8upJk5aLmoexZ2BJhBp1zAjRJMEB4a2CJwKI9e2EYw==", "integrity": "sha512-hklCsJNdMlDM1IwcCVcGQFBg2izY0+t5BIGbRsxi2UnKi6AGKL7pqJqmBDNRbw0bYCs4y3NA7TB+fkKfP/Nrdw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"google-auth-library": "^10.3.0", "google-auth-library": "^10.3.0",
@@ -1734,9 +1734,9 @@
] ]
}, },
"node_modules/@mistralai/mistralai": { "node_modules/@mistralai/mistralai": {
"version": "1.14.0", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.14.0.tgz", "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.14.1.tgz",
"integrity": "sha512-6zaj2f2LCd37cRpBvCgctkDbXtYBlAC85p+u4uU/726zjtsI+sdVH34qRzkm9iE3tRb8BoaiI0/P7TD+uMvLLQ==", "integrity": "sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==",
"dependencies": { "dependencies": {
"ws": "^8.18.0", "ws": "^8.18.0",
"zod": "^3.25.0 || ^4.0.0", "zod": "^3.25.0 || ^4.0.0",
@@ -2276,9 +2276,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.3.0", "version": "25.3.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz",
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "undici-types": "~7.18.0"
@@ -2743,9 +2743,9 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.13.5", "version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.11", "follow-redirects": "^1.15.11",
@@ -3422,16 +3422,16 @@
} }
}, },
"node_modules/drizzle-kit": { "node_modules/drizzle-kit": {
"version": "1.0.0-beta.9-e89174b", "version": "1.0.0-beta.15-9485290",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-1.0.0-beta.9-e89174b.tgz", "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-1.0.0-beta.15-9485290.tgz",
"integrity": "sha512-Xrw3k8E2CbSZr+kqe3k5W4oxd2fbEyczjKtyGIkAq0x9Wqpa/VtAT6Mkh83sIzqG4OSN7lOoUafsDxSE/AR7RA==", "integrity": "sha512-jkQFL8LycDbpKCGo37sX/pv44qGpLF1KSaZejvM0JuYSIBx8sXDPaNdHKNg63hrGf1uXf/zjPqpQju4P6GIyWw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@drizzle-team/brocli": "^0.11.0", "@drizzle-team/brocli": "^0.11.0",
"@js-temporal/polyfill": "^0.5.1", "@js-temporal/polyfill": "^0.5.1",
"esbuild": "^0.25.10", "esbuild": "^0.25.10",
"tsx": "^4.20.6" "jiti": "^2.6.1"
}, },
"bin": { "bin": {
"drizzle-kit": "bin.cjs" "drizzle-kit": "bin.cjs"
@@ -4879,6 +4879,16 @@
"@pkgjs/parseargs": "^0.11.0" "@pkgjs/parseargs": "^0.11.0"
} }
}, },
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-base64": { "node_modules/js-base64": {
"version": "3.7.8", "version": "3.7.8",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
+5 -5
View File
@@ -8,11 +8,11 @@
"bun:start": "bun run dist/index.js" "bun:start": "bun run dist/index.js"
}, },
"dependencies": { "dependencies": {
"@google/genai": "^1.42.0", "@google/genai": "^1.43.0",
"@libsql/client": "^0.17.0", "@libsql/client": "^0.17.0",
"@mistralai/mistralai": "^1.14.0", "@mistralai/mistralai": "^1.14.1",
"@napi-rs/canvas": "^0.1.95", "@napi-rs/canvas": "^0.1.95",
"axios": "^1.13.5", "axios": "^1.13.6",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"drizzle-orm": "^1.0.0-beta.9-e89174b", "drizzle-orm": "^1.0.0-beta.9-e89174b",
"emoji-regex": "^10.6.0", "emoji-regex": "^10.6.0",
@@ -32,12 +32,12 @@
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.9", "@types/bun": "^1.3.9",
"@types/node": "^25.3.0", "@types/node": "^25.3.3",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/fluent-ffmpeg": "^2.1.28", "@types/fluent-ffmpeg": "^2.1.28",
"@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1", "@typescript-eslint/parser": "^8.56.1",
"drizzle-kit": "^1.0.0-beta.9-e89174b", "drizzle-kit": "^1.0.0-beta.15-9485290",
"eslint": "^9.39.3", "eslint": "^9.39.3",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
@@ -0,0 +1,36 @@
import {CallbackCommand} from "../base/callback-command";
import {CallbackQuery} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {commands} from "../index";
import {YouTubeDownload} from "../commands/youtube-download";
const downloadText = " 📥 Скачать";
const getFromCacheText = "📥 Загрузить из кэша";
export class DownloadYtVideo extends CallbackCommand {
data = "/ytdl";
text = " 📥 Скачать";
requirements = Requirements.Build(Requirement.SAME_USER);
constructor(text?: string, data?: string) {
super();
this.text = text || this.text;
this.data = data || this.data;
}
static withData(inCache?: boolean, data?: string): DownloadYtVideo {
return new DownloadYtVideo(inCache ? getFromCacheText : downloadText, data);
}
async execute(query: CallbackQuery): Promise<void> {
const videoId = query.data.split(" ")[1];
if (!videoId) return;
const yt = commands.find(c => c instanceof YouTubeDownload);
if (!yt) return;
await yt.downloadYouTubeVideo(query.message, {videoId: videoId});
}
}
+21
View File
@@ -0,0 +1,21 @@
import {CallbackCommand} from "../base/callback-command";
export class TryAgain extends CallbackCommand {
data = "";
text = "🔁 Повторить";
constructor(text?: string, data?: string) {
super();
this.text = text ?? this.text;
this.data = data ?? this.data;
}
static withData(data?: string): TryAgain {
return new TryAgain(null, data);
}
async execute(): Promise<void> {
return Promise.resolve();
}
}
+15
View File
@@ -0,0 +1,15 @@
import {CallbackCommand} from "../base/callback-command";
import {CallbackQuery} from "typescript-telegram-bot-api";
import {processYouTubeLink} from "../util/utils";
export class YtInfo extends CallbackCommand {
data = "/ytinfo";
text: string;
async execute(query: CallbackQuery): Promise<void> {
const videoUrl = query.data.split(" ")[1];
if (!videoUrl) return;
await processYouTubeLink(query.message, videoUrl);
}
}
+1 -1
View File
@@ -80,7 +80,7 @@ export class GeminiChat extends ChatCommand {
try { try {
waitMessage = await bot.sendMessage({ waitMessage = await bot.sendMessage({
chat_id: chatId, chat_id: chatId,
text: Environment.waitText, text: Environment.waitThinkText,
reply_parameters: { reply_parameters: {
chat_id: chatId, chat_id: chatId,
message_id: msg.message_id message_id: msg.message_id
+1 -1
View File
@@ -53,7 +53,7 @@ export class GeminiGenerateImage extends Command {
await replyToMessage({ await replyToMessage({
message: waitMessage, message: waitMessage,
text: `Произошла ошибка!\n${e.toString()}`, text: `Произошла ошибка!\n${e.toString()}`,
disableLinkPreview: true link_preview_options: {is_disabled: true}
}).catch(logError); }).catch(logError);
} }
} }
+1 -1
View File
@@ -90,7 +90,7 @@ export class MistralChat extends ChatCommand {
chat_id: chatId, chat_id: chatId,
text: imagesCount ? text: imagesCount ?
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
: Environment.waitText, : Environment.waitThinkText,
reply_parameters: { reply_parameters: {
chat_id: chatId, chat_id: chatId,
+1 -1
View File
@@ -96,7 +96,7 @@ export class OllamaChat extends ChatCommand {
message: msg, message: msg,
text: (!think && imagesCount) ? text: (!think && imagesCount) ?
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
: Environment.waitText : Environment.waitThinkText
}); });
const stream = await ollama.chat({ const stream = await ollama.chat({
+1 -1
View File
@@ -37,7 +37,7 @@ export class OllamaPrompt extends Command {
waitMessage = await bot.sendMessage({ waitMessage = await bot.sendMessage({
chat_id: chatId, chat_id: chatId,
text: Environment.waitText, text: Environment.waitThinkText,
reply_parameters: { reply_parameters: {
chat_id: chatId, chat_id: chatId,
message_id: msg.message_id message_id: msg.message_id
+3 -3
View File
@@ -4,7 +4,7 @@ import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {bot, ollama} from "../index"; import {bot, ollama} from "../index";
import {WebSearchResponse} from "../model/web-search-response"; import {WebSearchResponse} from "../model/web-search-response";
import {editMessageText, logError} from "../util/utils"; import {oldEditMessageText, logError} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
export class OllamaSearch extends Command { export class OllamaSearch extends Command {
@@ -23,7 +23,7 @@ export class OllamaSearch extends Command {
try { try {
const wait = await bot.sendMessage({ const wait = await bot.sendMessage({
chat_id: chatId, chat_id: chatId,
text: Environment.waitText, text: Environment.waitThinkText,
reply_parameters: { reply_parameters: {
chat_id: chatId, chat_id: chatId,
message_id: msg.message_id message_id: msg.message_id
@@ -40,7 +40,7 @@ export class OllamaSearch extends Command {
message += `${index + 1}. ${r.url}\n`; message += `${index + 1}. ${r.url}\n`;
}); });
await editMessageText(chatId, wait.message_id, message); await oldEditMessageText(chatId, wait.message_id, message);
} catch (error) { } catch (error) {
logError(error); logError(error);
} }
+1 -1
View File
@@ -71,7 +71,7 @@ export class OpenAIChat extends ChatCommand {
try { try {
waitMessage = await bot.sendMessage({ waitMessage = await bot.sendMessage({
chat_id: chatId, chat_id: chatId,
text: Environment.waitText, text: Environment.waitThinkText,
reply_parameters: { reply_parameters: {
chat_id: chatId, chat_id: chatId,
message_id: msg.message_id message_id: msg.message_id
+2 -2
View File
@@ -5,7 +5,7 @@ import {Requirement} from "../base/requirement";
import {bot, openAi, photoGenDir} from "../index"; import {bot, openAi, photoGenDir} from "../index";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import {editMessageText, logError, replyToMessage} from "../util/utils"; import {oldEditMessageText, logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {APIError} from "openai"; import {APIError} from "openai";
@@ -102,7 +102,7 @@ export class OpenAIGenImage extends ChatCommand {
const text = "❌ Мне запрещено такое генерировать 😠"; const text = "❌ Мне запрещено такое генерировать 😠";
if (waitMessage) { if (waitMessage) {
await editMessageText(msg.chat.id, waitMessage.message_id, text).catch(logError); await oldEditMessageText(msg.chat.id, waitMessage.message_id, text).catch(logError);
} else { } else {
await replyToMessage({message: msg, text: text}).catch(logError); await replyToMessage({message: msg, text: text}).catch(logError);
} }
+23 -10
View File
@@ -1,8 +1,10 @@
import {Command} from "../base/command"; import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils"; import {editMessageText, logError, replyToMessage} from "../util/utils";
import {bot} from "../index"; import {bot, botUser} from "../index";
import {downloadVideoFromYouTube} from "../util/ytdl"; import {DownloadOptions, downloadVideoFromYouTube, getYouTubeVideoId} from "../util/ytdl";
import {Environment} from "../common/environment";
import {TryAgain} from "../callback_commands/try-again";
export class YouTubeDownload extends Command { export class YouTubeDownload extends Command {
command = ["ytdl", "youtube"]; command = ["ytdl", "youtube"];
@@ -10,16 +12,22 @@ export class YouTubeDownload extends Command {
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const url = match?.[3]; const url = match?.[3];
return this.downloadYouTubeVideo(msg, url); return this.downloadYouTubeVideo(msg, {url: url});
} }
async downloadYouTubeVideo(msg: Message, url: string): Promise<void> { async downloadYouTubeVideo(msg: Message, options: DownloadOptions): Promise<void> {
let waitMessage: Message | null = null; // TODO: 02.03.2026, Danil Nikolaev: add check for date
let waitMessage: Message | null = (msg.from.id === botUser.id) ? msg : null;
const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
try { try {
waitMessage = await replyToMessage({message: msg, text: "⏳ Секунду..."}); if (!waitMessage) {
waitMessage = await replyToMessage({message: msg, text: Environment.waitText});
} else {
await editMessageText({message: msg, text: Environment.waitText});
}
const {time, exists, buffer} = await downloadVideoFromYouTube(url); const {time, exists, buffer} = await downloadVideoFromYouTube({videoId: videoId});
if (buffer) { if (buffer) {
const start = Date.now(); const start = Date.now();
waitMessage = await bot.editMessageMedia({ waitMessage = await bot.editMessageMedia({
@@ -35,7 +43,7 @@ export class YouTubeDownload extends Command {
waitMessage = await bot.editMessageCaption({ waitMessage = await bot.editMessageCaption({
chat_id: msg.chat.id, chat_id: msg.chat.id,
message_id: waitMessage.message_id, message_id: waitMessage.message_id,
caption: `✅ [Видео](${url})` + (exists ? " загружено из кэша" : " успешно скачано") + " за " + (time + diff) + "мс", caption: "✅ [Видео]" + (exists ? " загружено из кэша" : " успешно скачано") + " за " + (time + diff) + "мс",
parse_mode: "MarkdownV2" parse_mode: "MarkdownV2"
}) as Message; }) as Message;
} }
@@ -46,7 +54,12 @@ export class YouTubeDownload extends Command {
await bot.editMessageText({ await bot.editMessageText({
chat_id: msg.chat.id, chat_id: msg.chat.id,
message_id: waitMessage.message_id, message_id: waitMessage.message_id,
text: `⚠️ Произошла ошибка.\n${e}`, text: Environment.errorText,
reply_markup: {
inline_keyboard: [[
TryAgain.withData("/ytdl " + videoId).asButton()
]]
}
}); });
} }
} }
+7 -1
View File
@@ -27,6 +27,8 @@ export class Environment {
static MAX_PHOTO_SIZE: number; static MAX_PHOTO_SIZE: number;
static PROCESS_LINKS: boolean;
static DEFAULT_AI_PROVIDER: AiProvider; static DEFAULT_AI_PROVIDER: AiProvider;
static SYSTEM_PROMPT?: string; static SYSTEM_PROMPT?: string;
@@ -49,7 +51,9 @@ export class Environment {
static OPENAI_MODEL: string; static OPENAI_MODEL: string;
static OPENAI_IMAGE_MODEL: string; static OPENAI_IMAGE_MODEL: string;
static waitText = "⏳ Дайте-ка подумать..."; static errorText = "⚠️ Произошла ошибка.";
static waitText = "⏳ Секунду...";
static waitThinkText = "⏳ Дайте-ка подумать...";
static analyzingPictureText = "🔍 Внимательно изучаю изображение..."; static analyzingPictureText = "🔍 Внимательно изучаю изображение...";
static analyzingPicturesText = "🔍 Внимательно изучаю изображения..."; static analyzingPicturesText = "🔍 Внимательно изучаю изображения...";
static genImageText = "👨‍🎨 Генерирую изображение..."; static genImageText = "👨‍🎨 Генерирую изображение...";
@@ -73,6 +77,8 @@ export class Environment {
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280"); Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280");
Environment.PROCESS_LINKS = ifTrue(process.env.PROCESS_LINKS);
const aiProvider = process.env.DEFAULT_AI_PROVIDER || "OLLAMA"; const aiProvider = process.env.DEFAULT_AI_PROVIDER || "OLLAMA";
if (Object.values(AiProvider).includes(aiProvider as AiProvider)) { if (Object.values(AiProvider).includes(aiProvider as AiProvider)) {
Environment.DEFAULT_AI_PROVIDER = aiProvider as AiProvider; Environment.DEFAULT_AI_PROVIDER = aiProvider as AiProvider;
+5 -1
View File
@@ -79,6 +79,8 @@ import {OpenAISetModel} from "./commands/openai-set-model";
import {Info} from "./commands/info"; import {Info} from "./commands/info";
import {OpenAIGenImage} from "./commands/openai-gen-image"; import {OpenAIGenImage} from "./commands/openai-gen-image";
import {clearUpFolderFromOldFiles} from "./util/files"; import {clearUpFolderFromOldFiles} from "./util/files";
import {DownloadYtVideo} from "./callback_commands/download-yt-video";
import {YtInfo} from "./callback_commands/yt-info";
process.setUncaughtExceptionCaptureCallback(logError); process.setUncaughtExceptionCaptureCallback(logError);
@@ -171,7 +173,9 @@ if (Environment.ENABLE_UNSAFE_EVAL) {
} }
export const callbackCommands: CallbackCommand[] = [ export const callbackCommands: CallbackCommand[] = [
new OllamaCancel() new OllamaCancel(),
new DownloadYtVideo(),
new YtInfo()
]; ];
if (Environment.OLLAMA_ADDRESS && Environment.OLLAMA_MODEL && Environment.SYSTEM_PROMPT) { if (Environment.OLLAMA_ADDRESS && Environment.OLLAMA_MODEL && Environment.SYSTEM_PROMPT) {
+15
View File
@@ -0,0 +1,15 @@
import {InlineKeyboardMarkup, Message, ParseMode} from "typescript-telegram-bot-api";
import {LinkPreviewOptions, MessageEntity} from "typescript-telegram-bot-api/dist/types";
export type EditOptions = ({
message: Message
} | {
chat_id: number;
message_id: number;
}) & {
text: string;
parse_mode?: ParseMode;
entities?: MessageEntity[];
link_preview_options?: LinkPreviewOptions;
reply_markup?: InlineKeyboardMarkup;
}
+77
View File
@@ -0,0 +1,77 @@
import {InlineKeyboardMarkup, Message, ParseMode} from "typescript-telegram-bot-api";
import {
ForceReply,
LinkPreviewOptions,
MessageEntity, ReplyKeyboardMarkup, ReplyKeyboardRemove,
ReplyParameters,
SuggestedPostParameters
} from "typescript-telegram-bot-api/dist/types";
export type SendOptions = ({
message: Message
} | {
/**
* Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
*/
chat_id: number | string;
message_id?: number;
}) & {
/**
* Unique identifier for the target message thread (topic) of the forum; for forum supergroups only
*/
message_thread_id?: number;
/**
* Identifier of the direct messages topic to which the message will be sent; required if the message is sent to a
* direct messages chat
*/
direct_messages_topic_id?: number;
/**
* Text of the message to be sent, 1-4096 characters after entities parsing
*/
text: string;
/**
* Mode for parsing entities in the message text. See formatting options for more details.
*/
parse_mode?: ParseMode;
/**
* A JSON-serialized list of special entities that appear in message text, which can be specified instead of
* parse_mode
*/
entities?: MessageEntity[];
/**
* Link preview generation options for the message
*/
link_preview_options?: LinkPreviewOptions;
/**
* Sends the message silently. Users will receive a notification with no sound.
*/
disable_notification?: boolean;
/**
* Protects the contents of the sent message from forwarding and saving
*/
protect_content?: boolean;
/**
* Pass True to allow up to 1000 messages per second, ignoring
* [broadcasting limits](https://core.telegram.org/bots/faq#how-can-i-message-all-of-my-bot-39s-subscribers-at-once)
* for a fee of 0.1 Telegram Stars per message. The relevant Stars will be withdrawn from the bot's balance
*/
allow_paid_broadcast?: boolean;
/**
* Unique identifier of the message effect to be added to the message; for private chats only
*/
message_effect_id?: string;
/**
* A JSON-serialized object containing the parameters of the suggested post to send; for direct messages chats only.
* If the message is sent as a reply to another suggested post, then that suggested post is automatically declined.
*/
suggested_post_parameters?: SuggestedPostParameters;
/**
* Description of the message to reply to
*/
reply_parameters?: ReplyParameters;
/**
* Additional interface options. A JSON-serialized object for an inline keyboard, custom reply keyboard,
* instructions to remove a reply keyboard or to force a reply from the user
*/
reply_markup?: InlineKeyboardMarkup | ReplyKeyboardMarkup | ReplyKeyboardRemove | ForceReply;
};
+171 -82
View File
@@ -11,6 +11,7 @@ import {
Message, Message,
ParseMode, ParseMode,
PhotoSize, PhotoSize,
TelegramBot,
User User
} from "typescript-telegram-bot-api"; } from "typescript-telegram-bot-api";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
@@ -30,7 +31,7 @@ import {MessageStore} from "../common/message-store";
import {SystemInfo} from "../commands/system-info"; import {SystemInfo} from "../commands/system-info";
import {PrefixResponse} from "../commands/prefix-response"; import {PrefixResponse} from "../commands/prefix-response";
import {OllamaChat} from "../commands/ollama-chat"; import {OllamaChat} from "../commands/ollama-chat";
import {getYouTubeVideoId} from "./ytdl"; import {getYouTubeVideoId, getYouTubeVideoInfo, isVideoExists} from "./ytdl";
import {YouTubeDownload} from "../commands/youtube-download"; import {YouTubeDownload} from "../commands/youtube-download";
import {ChatCommand} from "../base/chat-command"; import {ChatCommand} from "../base/chat-command";
import {WebSearchResponse} from "../model/web-search-response"; import {WebSearchResponse} from "../model/web-search-response";
@@ -43,6 +44,11 @@ import {OllamaGetModel} from "../commands/ollama-get-model";
import {GeminiGetModel} from "../commands/gemini-get-model"; import {GeminiGetModel} from "../commands/gemini-get-model";
import {MistralGetModel} from "../commands/mistral-get-model"; import {MistralGetModel} from "../commands/mistral-get-model";
import {OpenAIGetModel} from "../commands/openai-get-model"; import {OpenAIGetModel} from "../commands/openai-get-model";
import {SendOptions} from "../model/send-options";
import {EditOptions} from "../model/edit-options";
import VideoInfo from "youtubei.js/dist/src/parser/youtube/VideoInfo";
import {DownloadYtVideo} from "../callback_commands/download-yt-video";
import {TryAgain} from "../callback_commands/try-again";
export const ignore = () => { export const ignore = () => {
}; };
@@ -54,7 +60,7 @@ export const ignoreIfNotChanged = (e: Error | TelegramError) => {
}; };
export const ignoreIfMarkupFailed = (e: Error | TelegramError) => { export const ignoreIfMarkupFailed = (e: Error | TelegramError) => {
if (!(e instanceof TelegramError && e?.response?.description?.startsWith("Bad Request: can't parse entities"))) { if (!isMarkupFailed(e)) {
throw e; throw e;
} }
}; };
@@ -67,6 +73,18 @@ export const errorPlaceholder = async (msg: Message) => {
await sendErrorPlaceholder(msg).catch(logError); await sendErrorPlaceholder(msg).catch(logError);
}; };
export const isMarkupFailed = (e: Error | TelegramError) => {
return TelegramBot.isTelegramError(e) && e?.response?.description?.startsWith("Bad Request: can't parse entities");
};
export const isTooManyRequests = (e: Error | TelegramError) => {
return TelegramBot.isTelegramError(e) && e.response.description.includes("Too Many Requests");
};
export const isMessageTooLong = (e: Error | TelegramError) => {
return TelegramBot.isTelegramError(e) && e.response.description.includes("MESSAGE_TOO_LONG");
};
export function searchChatCommand( export function searchChatCommand(
commands: Command[], commands: Command[],
text: string, text: string,
@@ -117,7 +135,7 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
const cbId = cb?.id; const cbId = cb?.id;
const chatId = msg?.chat?.id || cb?.message?.chat?.id || -1; const chatId = msg?.chat?.id || cb?.message?.chat?.id || -1;
const messageId = msg?.message_id || cb?.message?.message_id || -1; const messageId = msg?.message_id || (cb && cb.message && "reply_to_message" in cb.message ? cb.message.reply_to_message.message_id : null) || -1;
const fromId = msg?.from?.id || cb?.from?.id || -1; const fromId = msg?.from?.id || cb?.from?.id || -1;
const chatType = msg?.chat?.type || cb?.message?.chat?.type || null; const chatType = msg?.chat?.type || cb?.message?.chat?.type || null;
@@ -196,11 +214,8 @@ export async function checkRequirements(cmd: Command | CallbackCommand | null, m
if (reqs.isRequiresSameUser()) { if (reqs.isRequiresSameUser()) {
let originalFromId: number | null; let originalFromId: number | null;
try { try {
const queryMessage = await MessageStore.get(chatId, messageId); const originalMessage = await MessageStore.get(chatId, messageId);
if (queryMessage && queryMessage.replyToMessageId) { originalFromId = originalMessage?.fromId;
const originalMessage = await MessageStore.get(chatId, queryMessage.replyToMessageId);
originalFromId = originalMessage?.fromId;
}
} catch (e) { } catch (e) {
logError(e); logError(e);
originalFromId = null; originalFromId = null;
@@ -239,92 +254,87 @@ export async function findAndExecuteCallbackCommand(commands: CallbackCommand[],
return true; return true;
} }
export async function editMessageText(chatId: number, messageId: number, messageText: string, parseMode?: ParseMode, replyMarkup?: InlineKeyboardMarkup): Promise<void> { export async function oldEditMessageText(chatId: number, messageId: number, messageText: string, parseMode?: ParseMode, replyMarkup?: InlineKeyboardMarkup): Promise<boolean | Message> {
if (messageText.trim().length === 0) return Promise.resolve(); return editMessageText({
chat_id: chatId,
message_id: messageId,
text: messageText,
parse_mode: parseMode,
reply_markup: replyMarkup,
link_preview_options: {is_disabled: true}
});
}
export async function editMessageText(options: EditOptions) {
if (options.text.trim().length === 0) return Promise.resolve(false);
try { try {
await bot.editMessageText({ const message = await bot.editMessageText({
chat_id: chatId, chat_id: "message" in options ? options.message.chat.id : options.chat_id,
message_id: messageId, message_id: "message" in options ? options.message.message_id : options.message_id,
text: messageText, text: options.text,
parse_mode: parseMode, parse_mode: options.parse_mode,
link_preview_options: { reply_markup: options.reply_markup,
is_disabled: true link_preview_options: options.link_preview_options,
}, });
reply_markup: replyMarkup return Promise.resolve(message);
}).catch(ignoreIfMarkupFailed);
return Promise.resolve();
} catch (e) { } catch (e) {
logError(e); logError(e);
if (e instanceof TelegramError && e.response.description.includes("Too Many Requests")) { if (isMarkupFailed(e)) {
return Promise.resolve(true);
} else if (isTooManyRequests(e)) {
const delay = Number(e.message.split("retry after ")[1]) || 30; const delay = Number(e.message.split("retry after ")[1]) || 30;
setTimeout(() => { setTimeout(() => {
return Promise.resolve(); return Promise.resolve();
}, delay * 1000); }, delay * 1000);
} else if (e instanceof TelegramError && e.response.description.includes("MESSAGE_TOO_LONG")) {
return Promise.reject(e);
} else { } else {
return Promise.resolve(); return Promise.reject(e);
} }
} }
} }
export type SendOptions = {
chat_id?: number;
message?: Message,
message_id?: number;
text: string,
parse_mode?: ParseMode,
disableLinkPreview?: boolean
};
export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> { export async function oldSendMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
const response = await bot.sendMessage({ return sendMessage({
chat_id: message.chat.id, message: message,
text: text, text: text,
parse_mode: parseMode parse_mode: parseMode
}); });
return Promise.resolve(response);
} }
export async function sendMessage(options: SendOptions): Promise<Message> { export async function sendMessage(options: SendOptions): Promise<Message> {
const response = await bot.sendMessage({ const response = await bot.sendMessage({
chat_id: options.chat_id ?? options.message?.chat?.id, chat_id: "message" in options ? options.message.chat.id : options.chat_id,
text: options.text, text: options.text,
parse_mode: options.parse_mode, parse_mode: options.parse_mode,
link_preview_options: { link_preview_options: options.link_preview_options,
is_disabled: options.disableLinkPreview reply_markup: options.reply_markup,
}
});
return Promise.resolve(response);
}
export async function replyToMessage(options: SendOptions): Promise<Message> {
const response = await bot.sendMessage({
chat_id: options.chat_id ?? options.message?.chat?.id,
text: options.text,
parse_mode: options.parse_mode,
reply_parameters: {
message_id: options.message_id || options.message?.message_id
},
link_preview_options: {
is_disabled: options.disableLinkPreview
}
}); });
return Promise.resolve(response); return Promise.resolve(response);
} }
export async function oldReplyToMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> { export async function oldReplyToMessage(message: Message, text: string, parseMode?: ParseMode): Promise<Message> {
const response = await bot.sendMessage({ return replyToMessage({
chat_id: message.chat.id, message: message,
text: text, text: text,
parse_mode: parseMode
});
}
export async function replyToMessage(options: SendOptions): Promise<Message> {
if (!("message" in options) && !options.message_id) {
return Promise.reject("for reply there must be message or message_id");
}
const response = await bot.sendMessage({
chat_id: "message" in options ? options.message.chat.id : options.chat_id,
text: options.text,
parse_mode: options.parse_mode,
reply_parameters: { reply_parameters: {
message_id: message.message_id message_id: "message" in options ? options.message.message_id : options.message_id
}, },
parse_mode: parseMode, link_preview_options: options.link_preview_options
}); });
return Promise.resolve(response); return Promise.resolve(response);
@@ -1200,27 +1210,8 @@ export async function processNewMessage(msg: Message): Promise<void> {
} }
const textToCheck = startsWithPrefix ? messageWithoutPrefix : cmdText; const textToCheck = startsWithPrefix ? messageWithoutPrefix : cmdText;
if (msg.entities) {
const urlEntities = msg.entities.filter(e => e.type === "url");
if (urlEntities.length) {
for (const e of urlEntities) {
const url = msg.text.substring(e.offset, e.offset + e.length);
// TODO: 31/01/2026, Danil Nikolaev: implement proper checking
try {
getYouTubeVideoId(url);
const yt = commands.find(e => e instanceof YouTubeDownload);
if (await checkRequirements(yt, msg)) {
await yt.downloadYouTubeVideo(msg, url);
}
return;
} catch (e) {
logError(e);
}
}
}
}
if (Environment.PROCESS_LINKS && await processYouTubeLink(msg, getFirstLink(msg))) return;
if (!startsWithPrefix && msg.chat.type !== "private") return; if (!startsWithPrefix && msg.chat.type !== "private") return;
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return; if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
@@ -1244,6 +1235,104 @@ export async function processNewMessage(msg: Message): Promise<void> {
} }
} }
function getFirstLink(msg: Message): string | null {
if (msg.entities) {
const urlEntities = msg.entities.filter(e => e.type === "url");
if (urlEntities.length) {
const e = urlEntities[0];
return msg.text.substring(e.offset, e.offset + e.length);
}
}
return null;
}
export async function processYouTubeLink(msg: Message, url: string): Promise<boolean> {
if (!url) return false;
try {
const videoId = getYouTubeVideoId(url);
const yt = commands.find(e => e instanceof YouTubeDownload);
if (await checkRequirements(yt, msg)) {
const waitMessage = msg.from.id === botUser.id ? msg : await replyToMessage({
message: msg,
text: "⏳ Ищу информацию о видео..."
});
if (msg.from.id === botUser.id) {
await editMessageText({message: msg, text: "⏳ Ищу информацию о видео..."});
}
let videoInfo: VideoInfo | null = null;
let ytError: string = null;
try {
videoInfo = await getYouTubeVideoInfo(videoId);
} catch (e) {
logError(e);
if ("version" in e) {
ytError = e.message;
}
}
console.log("VIDEO_INFO", videoInfo);
let text: string = null;
const inCache = isVideoExists({videoId: videoId});
const duration = videoInfo?.basic_info?.duration || null;
const canDownload = inCache || duration && duration <= 300;
if (videoInfo) {
text = "Видео с YouTube\n\n" +
`Название: ${videoInfo.basic_info?.title}\n` +
`Автор: ${videoInfo.secondary_info?.owner?.author?.name}\n` +
`Длительность: ${duration} сек.`;
if (!canDownload) {
text += `\n\nВидео слишком длинное (${duration} сек. > 300 сек.)`;
}
} else if (!ytError) {
text = "Информация о видео не найдена";
}
const errorButInCache = !videoInfo && ytError && inCache;
if (errorButInCache) {
text = "Я не смогу получить информацию о видео, но нашёл его в кэше.";
}
if (!text && ytError) {
await editMessageText({
message: waitMessage,
text: Environment.errorText,
reply_markup: {
inline_keyboard: [[
TryAgain.withData("/ytinfo " + url).asButton()
]]
}
});
} else {
await editMessageText({
message: waitMessage,
text: text,
reply_markup: canDownload ? {
inline_keyboard: [[
DownloadYtVideo.withData(inCache, "/ytdl " + videoId).asButton()
]]
} : {inline_keyboard: []}
});
}
}
return true;
} catch (e) {
logError(e);
}
return false;
}
export async function processEditedMessage(msg: Message): Promise<void> { export async function processEditedMessage(msg: Message): Promise<void> {
console.log("Edited Message", msg); console.log("Edited Message", msg);
+48 -7
View File
@@ -6,6 +6,21 @@ import Innertube, {Platform, Types} from "youtubei.js";
import {Readable} from "node:stream"; import {Readable} from "node:stream";
import {logError} from "./utils"; import {logError} from "./utils";
import {performFFmpeg} from "./ffmpeg"; import {performFFmpeg} from "./ffmpeg";
import VideoInfo from "youtubei.js/dist/src/parser/youtube/VideoInfo";
let innertube: Innertube | null = null;
export async function getYT(): Promise<Innertube> {
if (innertube) {
return innertube;
} else {
innertube = await Innertube.create({
generate_session_locally: true,
retrieve_player: true
});
return innertube;
}
}
export function getYouTubeVideoId(url: string): string { export function getYouTubeVideoId(url: string): string {
const regex = /(?:(?:youtube\.com|music\.youtube\.com)\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?|shorts|clip)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i; const regex = /(?:(?:youtube\.com|music\.youtube\.com)\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?|shorts|clip)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/i;
@@ -14,7 +29,34 @@ export function getYouTubeVideoId(url: string): string {
return match[1]; return match[1];
} }
export async function downloadVideoFromYouTube(url: string): Promise<{ export async function getYouTubeVideoInfo(videoId: string): Promise<VideoInfo> {
try {
return (await getYT()).getInfo(videoId, {client: "ANDROID"});
} catch (e) {
logError(e);
}
}
export function isVideoExists(options: DownloadOptions): boolean {
const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
const filePath = path.join(videoDir, `${videoId}.mp4`);
return fs.existsSync(filePath);
}
export function getVideoFromCache(videoId: string): Buffer | null {
if (!isVideoExists({videoId: videoId})) return null;
const filePath = path.join(videoDir, `${videoId}.mp4`);
return Buffer.from(fs.readFileSync(filePath));
}
export type DownloadOptions = {
url: string
} | {
videoId: string;
}
export async function downloadVideoFromYouTube(options: DownloadOptions): Promise<{
time: number, time: number,
exists?: boolean, exists?: boolean,
buffer: Buffer | null buffer: Buffer | null
@@ -23,7 +65,7 @@ export async function downloadVideoFromYouTube(url: string): Promise<{
let buffer: Buffer | null = null; let buffer: Buffer | null = null;
try { try {
const videoId = getYouTubeVideoId(url); const videoId = "videoId" in options ? options.videoId : getYouTubeVideoId(options.url);
const filePath = path.join(videoDir, `${videoId}.mp4`); const filePath = path.join(videoDir, `${videoId}.mp4`);
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
const buffer = Buffer.from(fs.readFileSync(filePath)); const buffer = Buffer.from(fs.readFileSync(filePath));
@@ -42,12 +84,11 @@ export async function downloadVideoFromYouTube(url: string): Promise<{
const code = `${data.output}\nreturn { ${properties.join(", ")} }`; const code = `${data.output}\nreturn { ${properties.join(", ")} }`;
return new Function(code)(); return new Function(code)();
}; };
const yt = await Innertube.create({
generate_session_locally: true, const yt = await getYT();
retrieve_player: true
});
const videoInfo = await yt.getInfo(videoId, {client: "ANDROID"}); const videoInfo = await yt.getInfo(videoId, {client: "ANDROID"});
console.log("Video info", videoInfo);
console.log(`Fetching metadata for: ${videoId}...`); console.log(`Fetching metadata for: ${videoId}...`);
@@ -119,7 +160,7 @@ export async function downloadVideoFromYouTube(url: string): Promise<{
const end = Date.now(); const end = Date.now();
const diff = end - start; const diff = end - start;
console.log(`Video downloaded. URL: ${url}\ntook ${diff}ms`); console.log(`Video downloaded.\ntook ${diff}ms`);
return { return {
time: diff, time: diff,