diff --git a/.gitignore b/.gitignore index 710df9f..97a3e92 100644 --- a/.gitignore +++ b/.gitignore @@ -45,8 +45,14 @@ letta-code-sdk/ # WhatsApp session (contains credentials) data/whatsapp-session/ +# Telegram MTProto session (contains authenticated session data) +data/telegram-mtproto/ + # Config with secrets lettabot.yaml lettabot.yml bun.lock .tool-versions + +# Telegram MTProto session data (contains auth secrets) +data/telegram-mtproto/ diff --git a/docs/telegram-mtproto-setup.md b/docs/telegram-mtproto-setup.md new file mode 100644 index 0000000..dad66fa --- /dev/null +++ b/docs/telegram-mtproto-setup.md @@ -0,0 +1,251 @@ +# Telegram MTProto (User Account) Setup Guide + +This guide explains how to run LettaBot as a Telegram **user account** instead of a bot. This uses the MTProto protocol via TDLib, giving you full user capabilities. + +## Overview: Bot API vs MTProto + +| Feature | Bot API | MTProto (User) | +|---------|---------|----------------| +| **Setup** | Simple (BotFather token) | Phone + API credentials | +| **DM users first** | No (must wait for user) | Yes | +| **File size limit** | 50 MB | 2 GB | +| **Privacy mode** | Restricted in groups | Full access | +| **Rate limits** | 30 req/sec | Higher limits | +| **Appears as** | Bot account | Regular user | + +**Choose MTProto if you need:** User-first DMs, larger files, or full group access. + +## Prerequisites + +1. **Telegram account** with a phone number +2. **API credentials** from my.telegram.org (see below) +3. **LettaBot** installed with dependencies + +## Getting API Credentials + +1. Go to [my.telegram.org](https://my.telegram.org) +2. Log in with your phone number +3. Click **"API development tools"** +4. Fill in the form: + - **App title**: LettaBot (or any name) + - **Short name**: lettabot + - **Platform**: Desktop + - **Description**: AI assistant +5. Click **"Create application"** +6. Note your **API ID** and **API Hash** + +> **Security Note**: Keep your API credentials secret. Never commit them to git or share them publicly. They are tied to your Telegram account. + +## Configuration + +Add these to your `.env` file: + +```bash +# Telegram MTProto User Mode +TELEGRAM_PHONE_NUMBER=+1234567890 +TELEGRAM_API_ID=12345678 +TELEGRAM_API_HASH=abcdef1234567890abcdef1234567890 + +# Optional: Custom database directory (default: ./data/telegram-mtproto) +# TELEGRAM_MTPROTO_DB_DIR=./data/telegram-mtproto + +# DM policy (same as bot mode) +TELEGRAM_DM_POLICY=pairing +# TELEGRAM_ALLOWED_USERS=123456789,987654321 +``` + +**Important**: Do NOT set `TELEGRAM_BOT_TOKEN` at the same time. You must choose one mode or the other. + +## First Run Authentication + +On first run, you'll see prompts for authentication: + +``` +$ lettabot server + +[Telegram MTProto] Starting authentication... +[Telegram MTProto] Sending phone number... +[Telegram MTProto] Verification code sent to your Telegram app +Enter verification code: β–ˆ +``` + +1. Open your Telegram app on another device +2. You'll receive a login code message +3. Enter the code in the terminal + +If you have 2FA enabled: + +``` +[Telegram MTProto] 2FA password required +Enter 2FA password: β–ˆ +``` + +Enter your Telegram 2FA password. + +On success: + +``` +[Telegram MTProto] Authenticated successfully! +[Telegram MTProto] Session saved to ./data/telegram-mtproto/ +``` + +## Subsequent Runs + +After initial authentication, the session is saved. You won't need to enter codes again: + +``` +$ lettabot server + +[Telegram MTProto] Starting adapter... +[Telegram MTProto] Authenticated successfully! +[Telegram MTProto] Session saved to ./data/telegram-mtproto/ +[Telegram MTProto] Adapter started +``` + +## Troubleshooting + +### "Phone number banned" or "PHONE_NUMBER_BANNED" + +Your phone number may be flagged by Telegram. This can happen if: +- The number was recently used for spam +- Too many failed login attempts +- Account previously terminated + +**Solution**: Contact Telegram support or use a different number. + +### "FLOOD_WAIT_X" errors + +You're sending too many requests. TDLib handles this automatically by waiting, but you'll see delay messages in logs. + +**Solution**: This is normal - TDLib will retry automatically. + +### Session keeps asking for code + +The session database may be corrupted. + +**Solution**: Delete the database directory and re-authenticate: +```bash +rm -rf ./data/telegram-mtproto +lettabot server +``` + +### "API_ID_INVALID" or "API_HASH_INVALID" + +Your API credentials are incorrect. + +**Solution**: Double-check the values from my.telegram.org. + +### Database grows very large + +TDLib caches data locally, which can grow to 50+ MB quickly. + +**Solution**: This is normal. For very long sessions, you may want to periodically clear the database and re-authenticate. + +## Switching Between Bot and MTProto + +To switch modes: + +1. **Stop LettaBot** +2. **Edit `.env`**: + - For Bot mode: Set `TELEGRAM_BOT_TOKEN`, remove/comment `TELEGRAM_PHONE_NUMBER` + - For MTProto: Set `TELEGRAM_PHONE_NUMBER` + API credentials, remove/comment `TELEGRAM_BOT_TOKEN` +3. **Start LettaBot** + +You cannot run both modes simultaneously. + +## Security Notes + +1. **API credentials**: Treat like passwords. They can be used to access your Telegram account. + +2. **Session files**: The `./data/telegram-mtproto/` directory contains your authenticated session. Anyone with these files can act as your Telegram account. + +3. **gitignore**: The session directory is automatically gitignored. Never commit it. + +4. **Account security**: Consider using a dedicated phone number for bots rather than your personal number. + +5. **Logout**: To revoke the session: + - Go to Telegram Settings β†’ Devices + - Find "TDLib" or the session + - Click "Terminate Session" + +## Using with DM Policy + +MTProto mode supports the same DM policies as bot mode: + +- **pairing** (default): Unknown users must be approved before chatting +- **allowlist**: Only users in `TELEGRAM_ALLOWED_USERS` can message +- **open**: Anyone can message + +```bash +# Pairing mode (recommended for most users) +TELEGRAM_DM_POLICY=pairing + +# Or pre-approve specific users +TELEGRAM_ALLOWED_USERS=123456789,987654321 +``` + +### Admin Notifications for Pairing + +When using pairing mode, you can set up an admin chat to receive pairing requests: + +```bash +# Your Telegram user ID or a group chat ID for admin notifications +TELEGRAM_ADMIN_CHAT_ID=137747014 +``` + +**How it works:** + +1. Unknown user sends a message +2. User sees: *"Your request has been passed on to the admin."* +3. Admin chat receives notification with username and user ID +4. Admin replies **"approve"** or **"deny"** to the notification +5. Both user and admin get confirmation + +**Approve/Deny keywords:** +- Approve: `approve`, `yes`, `y` +- Deny: `deny`, `no`, `n`, `reject` + +If no admin chat is configured, pairing codes are logged to the console instead. + +**Pairing request behavior:** +- Repeated messages from the same unapproved user do not create duplicate admin notifications. +- If the pending pairing queue is full, the user gets: *"Too many pending pairing requests. Please try again later."* + +## Group Chat Policy + +Since MTProto gives you full group access, you need to control when the agent responds in groups. The **group policy** determines this: + +| Policy | Behavior | +|--------|----------| +| **mention** | Only respond when @mentioned by username | +| **reply** | Only respond when someone replies to agent's message | +| **both** | Respond to mentions OR replies (default) | +| **off** | Never respond in groups, DMs only | + +```bash +# Only respond when @mentioned (recommended for busy groups) +TELEGRAM_GROUP_POLICY=mention + +# Only respond to replies +TELEGRAM_GROUP_POLICY=reply + +# Respond to either mentions or replies (default) +TELEGRAM_GROUP_POLICY=both + +# Never respond in groups +TELEGRAM_GROUP_POLICY=off +``` + +**Note**: Group policy does NOT affect DMs - direct messages always work based on your DM policy. + +### How Mentions Work + +The agent responds when: +- Someone types `@yourusername` in their message +- Someone uses Telegram's mention feature (clicking your name in the member list) + +### How Reply Detection Works + +The agent tracks messages it sends. When someone replies to one of those messages (using Telegram's reply feature), the agent will respond. + +**Tip**: For busy groups, use `mention` policy. For small groups or channels, `both` works well. diff --git a/package-lock.json b/package-lock.json index c7eddbc..cbd88cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,9 @@ "node-schedule": "^2.1.1", "open": "^11.0.0", "openai": "^6.17.0", + "prebuilt-tdlib": "^0.1008060.0", "qrcode-terminal": "^0.12.0", + "tdl": "^8.0.2", "telegramify-markdown": "^1.0.0", "tsx": "^4.21.0", "typescript": "^5.9.3", @@ -50,8 +52,7 @@ "optionalDependencies": { "@slack/bolt": "^4.6.0", "@whiskeysockets/baileys": "6.7.21", - "discord.js": "^14.25.1", - "slackify-markdown": "^5.0.0" + "discord.js": "^14.25.1" } }, "letta-code": { @@ -62,7 +63,6 @@ "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.4.tgz", "integrity": "sha512-HTgrrTgZ9Jgeo6Z3oqbQ7lifOVvRR14vaDuBGPPUxk9Thm+vObaO4QfYYYWw4Zo5CWQDBEfsinFA6Gre+AqwNQ==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -76,7 +76,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "license": "MIT", - "peer": true, "dependencies": { "get-east-asian-width": "^1.3.1" }, @@ -1459,6 +1458,78 @@ "node": ">=12" } }, + "node_modules/@prebuilt-tdlib/darwin-arm64": { + "version": "0.1008060.0", + "resolved": "https://registry.npmjs.org/@prebuilt-tdlib/darwin-arm64/-/darwin-arm64-0.1008060.0.tgz", + "integrity": "sha512-k8QLwLjrxVYAE68r8v9nD8yvagWvJTUUTe2MZfiFrX5JSiZ8bULu2rcdDQXcVJ/67ScAnYewTApQo18ApP1f9A==", + "cpu": [ + "arm64" + ], + "license": "0BSD", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@prebuilt-tdlib/darwin-x64": { + "version": "0.1008060.0", + "resolved": "https://registry.npmjs.org/@prebuilt-tdlib/darwin-x64/-/darwin-x64-0.1008060.0.tgz", + "integrity": "sha512-BbczSMfJDSdRPxcmOK0XSrbsT6adc/dfcnhgbi3nyi2cjlvhbNXdJmArOLQF/mquoKngd75jaUbeFGie+TBPtg==", + "cpu": [ + "x64" + ], + "license": "0BSD", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@prebuilt-tdlib/linux-arm64-glibc": { + "version": "0.1008060.0", + "resolved": "https://registry.npmjs.org/@prebuilt-tdlib/linux-arm64-glibc/-/linux-arm64-glibc-0.1008060.0.tgz", + "integrity": "sha512-BY3Jp7ladBBfhYwkjV7SGiQ7LgxDdd5DCt9MVESA65dsZWEeBNDgdENz8lUho11OSVJcgEdWDmFmM7pA1mHAJA==", + "cpu": [ + "arm64" + ], + "license": "0BSD", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@prebuilt-tdlib/linux-x64-glibc": { + "version": "0.1008060.0", + "resolved": "https://registry.npmjs.org/@prebuilt-tdlib/linux-x64-glibc/-/linux-x64-glibc-0.1008060.0.tgz", + "integrity": "sha512-4Y44ugXMkx1YUHtEvLyRcnEjMdGf9Pi2O8U03WHJMwVzR4r2+vXdbhVseUfLg4pZL+sdBn1q1+5X1ogeElXW9g==", + "cpu": [ + "x64" + ], + "license": "0BSD", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@prebuilt-tdlib/types": { + "version": "0.1008060.0", + "resolved": "https://registry.npmjs.org/@prebuilt-tdlib/types/-/types-0.1008060.0.tgz", + "integrity": "sha512-KHzTyD17XNYo2iSAIwGiNvEuJXqHvU6uE+hP0r/ep+p+brDUsEVbnEtGP2nqhNYi+P2NXAeKPKCwKez86ulw5w==", + "license": "0BSD", + "optional": true + }, + "node_modules/@prebuilt-tdlib/win32-x64": { + "version": "0.1008060.0", + "resolved": "https://registry.npmjs.org/@prebuilt-tdlib/win32-x64/-/win32-x64-0.1008060.0.tgz", + "integrity": "sha512-G2aS7b4kCs+z2xl2MCBxOnWiwLZh26kweNYw22h3Qw+AiTqXf2sxOjDxai4QNrKQP3YgTKiS/ww89EH2zJ3YzA==", + "cpu": [ + "x64" + ], + "license": "0BSD", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2101,16 +2172,6 @@ "@types/node": "*" } }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/ms": "*" - } - }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2130,6 +2191,7 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -2172,16 +2234,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -2245,13 +2297,6 @@ "@types/node": "*" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT", - "optional": true - }, "node_modules/@types/update-notifier": { "version": "6.0.8", "resolved": "https://registry.npmjs.org/@types/update-notifier/-/update-notifier-6.0.8.tgz", @@ -2713,7 +2758,6 @@ "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", "license": "MIT", - "peer": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -2733,17 +2777,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "optional": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2969,17 +3002,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "optional": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3002,17 +3024,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "optional": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/character-entities-legacy": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", @@ -3050,7 +3061,6 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", "license": "MIT", - "peer": true, "dependencies": { "restore-cursor": "^4.0.0" }, @@ -3066,7 +3076,6 @@ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", "license": "MIT", - "peer": true, "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" @@ -3083,7 +3092,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", "license": "MIT", - "peer": true, "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" @@ -3100,7 +3108,6 @@ "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", "license": "MIT", - "peer": true, "dependencies": { "convert-to-spaces": "^2.0.1" }, @@ -3200,7 +3207,6 @@ "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", "license": "MIT", - "peer": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } @@ -3282,20 +3288,6 @@ } } }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3364,16 +3356,6 @@ "node": ">= 0.8" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3383,20 +3365,6 @@ "node": ">=8" } }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "optional": true, - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/discord-api-types": { "version": "0.38.38", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.38.tgz", @@ -3582,7 +3550,6 @@ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", "license": "MIT", - "peer": true, "workspaces": [ "docs", "benchmarks" @@ -3652,7 +3619,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -4417,7 +4383,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4445,7 +4410,6 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-6.7.0.tgz", "integrity": "sha512-dhB16KfdTO8yYwF2K0E4wPXpL88tdrjjB6w44AZ0ljSktYoUQQcxccq9KL1vpRhk8JIa0A7B7zvjajHqI42teA==", "license": "MIT", - "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", @@ -4512,22 +4476,19 @@ "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/ink/node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/ink/node_modules/string-width": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", "license": "MIT", - "peer": true, "dependencies": { "get-east-asian-width": "^1.3.0", "strip-ansi": "^7.1.0" @@ -4576,7 +4537,6 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -4594,7 +4554,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", - "peer": true, "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -4728,7 +4687,6 @@ "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", "license": "MIT", - "peer": true, "bin": { "is-in-ci": "cli.js" }, @@ -4809,19 +4767,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -4948,6 +4893,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "license": "MIT", + "peer": true, "dependencies": { "@keyv/serialize": "^1.1.1" } @@ -5116,17 +5062,6 @@ "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==", "license": "MIT" }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "optional": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5159,17 +5094,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "optional": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5179,219 +5103,6 @@ "node": ">= 0.4" } }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -5413,597 +5124,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "optional": true, - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "optional": true, - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "optional": true, - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "optional": true, - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "optional": true - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -6034,7 +5154,6 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -6138,6 +5257,12 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -6178,6 +5303,17 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-schedule": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", @@ -6251,7 +5387,6 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "license": "MIT", - "peer": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -6430,7 +5565,6 @@ "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", "license": "MIT", - "peer": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } @@ -6496,6 +5630,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6584,6 +5719,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/prebuilt-tdlib": { + "version": "0.1008060.0", + "resolved": "https://registry.npmjs.org/prebuilt-tdlib/-/prebuilt-tdlib-0.1008060.0.tgz", + "integrity": "sha512-OcBunKFWGOviG7uJwdTm8tLTtnLsczpLjfORp5hjZJYTUX20TM56+7JJe61WIuDSHUrH0tuvT9bh/iIrMiBbbw==", + "license": "MIT", + "optionalDependencies": { + "@prebuilt-tdlib/darwin-arm64": "0.1008060.0", + "@prebuilt-tdlib/darwin-x64": "0.1008060.0", + "@prebuilt-tdlib/linux-arm64-glibc": "0.1008060.0", + "@prebuilt-tdlib/linux-x64-glibc": "0.1008060.0", + "@prebuilt-tdlib/types": "0.1008060.0", + "@prebuilt-tdlib/win32-x64": "0.1008060.0" + } + }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -6770,7 +5919,6 @@ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6818,58 +5966,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", @@ -6893,7 +5989,6 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "license": "MIT", - "peer": true, "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -6909,8 +6004,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/retry": { "version": "0.13.1", @@ -7050,8 +6144,7 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/section-matter": { "version": "1.0.0", @@ -7135,6 +6228,7 @@ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -7291,31 +6385,11 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "license": "MIT" }, - "node_modules/slackify-markdown": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slackify-markdown/-/slackify-markdown-5.0.0.tgz", - "integrity": "sha512-jTWvwjOYGAqS0NNyhTm+9H8S0/tux2anPEwyeIBCayH14jGHIrN70yj4afUYP2zq54aUwEFkASdXytjO/5GA+A==", - "license": "MIT", - "optional": true, - "dependencies": { - "mdast-util-to-markdown": "^2.1.2", - "remark-gfm": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.5", - "unist-util-remove": "^4.0.0", - "unist-util-visit": "^5.0.0" - }, - "engines": { - "node": ">=22" - } - }, "node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -7332,7 +6406,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "license": "MIT", - "peer": true, "dependencies": { "get-east-asian-width": "^1.3.1" }, @@ -7390,7 +6463,6 @@ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "license": "MIT", - "peer": true, "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -7608,6 +6680,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tdl": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/tdl/-/tdl-8.0.2.tgz", + "integrity": "sha512-KYxlJ4eao7FUu91U1dCDkaHmK70JAyZ1KqitkKqpPC7rxAiXWhaYxddWvt84UxIYoWbgdd0B70FYJ4p/YqpFCA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "node-addon-api": "^7.1.1", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.14.0" + } + }, "node_modules/telegramify-markdown": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/telegramify-markdown/-/telegramify-markdown-1.3.2.tgz", @@ -8236,17 +7323,6 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "optional": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/ts-mixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", @@ -8276,6 +7352,7 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -8358,101 +7435,6 @@ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", - "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -8516,42 +7498,13 @@ "node": ">= 0.8" } }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -9442,6 +8395,7 @@ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -9467,19 +8421,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", - "license": "MIT", - "peer": true - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "optional": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "license": "MIT" } } } diff --git a/package.json b/package.json index d7643ce..63e052e 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,9 @@ "@slack/bolt": "^4.6.0", "@whiskeysockets/baileys": "6.7.21", "discord.js": "^14.25.1", - "slackify-markdown": "^5.0.0" + "prebuilt-tdlib": "^0.1008060.0", + "slackify-markdown": "^5.0.0", + "tdl": "^8.0.2" }, "devDependencies": { "@types/update-notifier": "^6.0.8", diff --git a/src/channels/index.ts b/src/channels/index.ts index 206ea91..93abc3b 100644 --- a/src/channels/index.ts +++ b/src/channels/index.ts @@ -5,6 +5,7 @@ export * from './types.js'; export * from './setup.js'; export * from './telegram.js'; +export * from './telegram-mtproto.js'; export * from './slack.js'; export * from './whatsapp/index.js'; export * from './signal.js'; diff --git a/src/channels/telegram-mtproto-format.ts b/src/channels/telegram-mtproto-format.ts new file mode 100644 index 0000000..b2185f1 --- /dev/null +++ b/src/channels/telegram-mtproto-format.ts @@ -0,0 +1,311 @@ +/** + * Telegram MTProto Text Formatting + * + * Converts markdown to TDLib formattedText format with proper UTF-16 entity offsets. + * + * CRITICAL: TDLib uses UTF-16 code units for entity offsets, not byte offsets or + * Unicode code points. JavaScript's string.length already returns UTF-16 code units, + * so we can use it directly. However, emoji and other characters outside the BMP + * take 2 UTF-16 code units (surrogate pairs). + * + * Entity types supported: + * - Bold: **text** or __text__ + * - Italic: *text* or _text_ + * - Code: `code` + * - Pre: ```code block``` + * - Strikethrough: ~~text~~ + */ + +export interface TdlibTextEntity { + _: 'textEntity'; + offset: number; // UTF-16 code units from start + length: number; // UTF-16 code units + type: TdlibTextEntityType; +} + +export type TdlibTextEntityType = + | { _: 'textEntityTypeBold' } + | { _: 'textEntityTypeItalic' } + | { _: 'textEntityTypeCode' } + | { _: 'textEntityTypePre'; language?: string } + | { _: 'textEntityTypeStrikethrough' } + | { _: 'textEntityTypeUnderline' } + | { _: 'textEntityTypeTextUrl'; url: string }; + +export interface TdlibFormattedText { + _: 'formattedText'; + text: string; + entities: TdlibTextEntity[]; +} + +/** + * Calculate UTF-16 length of a string. + * JavaScript strings are UTF-16 encoded, so string.length gives UTF-16 code units. + */ +export function utf16Length(str: string): number { + return str.length; +} + +/** + * Convert markdown text to TDLib formattedText structure. + * Handles bold, italic, code, pre, and strikethrough. + */ +export function markdownToTdlib(markdown: string): TdlibFormattedText { + const entities: TdlibTextEntity[] = []; + let plainText = ''; + let i = 0; + + while (i < markdown.length) { + // Code block: ```language\ncode``` or ```code``` + if (markdown.slice(i, i + 3) === '```') { + const blockStart = i; + i += 3; + + // Check for language specifier + let language = ''; + const langMatch = markdown.slice(i).match(/^(\w+)\n/); + if (langMatch) { + language = langMatch[1]; + i += langMatch[0].length; + } else if (markdown[i] === '\n') { + i++; // Skip newline after ``` + } + + // Find closing ``` + const closeIdx = markdown.indexOf('```', i); + if (closeIdx !== -1) { + const content = markdown.slice(i, closeIdx); + const entityOffset = utf16Length(plainText); + const entityLength = utf16Length(content); + + entities.push({ + _: 'textEntity', + offset: entityOffset, + length: entityLength, + type: language + ? { _: 'textEntityTypePre', language } + : { _: 'textEntityTypePre' } + }); + + plainText += content; + i = closeIdx + 3; + continue; + } + // No closing ```, treat as literal + plainText += '```'; + i = blockStart + 3; + continue; + } + + // Inline code: `code` + if (markdown[i] === '`') { + const closeIdx = markdown.indexOf('`', i + 1); + if (closeIdx !== -1 && closeIdx > i + 1) { + const content = markdown.slice(i + 1, closeIdx); + const entityOffset = utf16Length(plainText); + const entityLength = utf16Length(content); + + entities.push({ + _: 'textEntity', + offset: entityOffset, + length: entityLength, + type: { _: 'textEntityTypeCode' } + }); + + plainText += content; + i = closeIdx + 1; + continue; + } + } + + // Bold: **text** (check before single *) + if (markdown.slice(i, i + 2) === '**') { + const closeIdx = markdown.indexOf('**', i + 2); + if (closeIdx !== -1) { + const content = markdown.slice(i + 2, closeIdx); + const entityOffset = utf16Length(plainText); + const entityLength = utf16Length(content); + + entities.push({ + _: 'textEntity', + offset: entityOffset, + length: entityLength, + type: { _: 'textEntityTypeBold' } + }); + + plainText += content; + i = closeIdx + 2; + continue; + } + } + + // Bold alternate: __text__ (check before single _) + if (markdown.slice(i, i + 2) === '__') { + const closeIdx = markdown.indexOf('__', i + 2); + if (closeIdx !== -1) { + const content = markdown.slice(i + 2, closeIdx); + const entityOffset = utf16Length(plainText); + const entityLength = utf16Length(content); + + entities.push({ + _: 'textEntity', + offset: entityOffset, + length: entityLength, + type: { _: 'textEntityTypeBold' } + }); + + plainText += content; + i = closeIdx + 2; + continue; + } + } + + // Strikethrough: ~~text~~ + if (markdown.slice(i, i + 2) === '~~') { + const closeIdx = markdown.indexOf('~~', i + 2); + if (closeIdx !== -1) { + const content = markdown.slice(i + 2, closeIdx); + const entityOffset = utf16Length(plainText); + const entityLength = utf16Length(content); + + entities.push({ + _: 'textEntity', + offset: entityOffset, + length: entityLength, + type: { _: 'textEntityTypeStrikethrough' } + }); + + plainText += content; + i = closeIdx + 2; + continue; + } + } + + // Italic: *text* (single asterisk) + if (markdown[i] === '*' && markdown[i + 1] !== '*') { + const closeIdx = findClosingMark(markdown, i + 1, '*'); + if (closeIdx !== -1) { + const content = markdown.slice(i + 1, closeIdx); + const entityOffset = utf16Length(plainText); + const entityLength = utf16Length(content); + + entities.push({ + _: 'textEntity', + offset: entityOffset, + length: entityLength, + type: { _: 'textEntityTypeItalic' } + }); + + plainText += content; + i = closeIdx + 1; + continue; + } + } + + // Italic alternate: _text_ (single underscore) + if (markdown[i] === '_' && markdown[i + 1] !== '_') { + const closeIdx = findClosingMark(markdown, i + 1, '_'); + if (closeIdx !== -1) { + const content = markdown.slice(i + 1, closeIdx); + const entityOffset = utf16Length(plainText); + const entityLength = utf16Length(content); + + entities.push({ + _: 'textEntity', + offset: entityOffset, + length: entityLength, + type: { _: 'textEntityTypeItalic' } + }); + + plainText += content; + i = closeIdx + 1; + continue; + } + } + + // Regular character - copy to output + plainText += markdown[i]; + i++; + } + + return { + _: 'formattedText', + text: plainText, + entities + }; +} + +/** + * Find closing mark that isn't preceded by backslash and isn't part of a double mark. + */ +function findClosingMark(str: string, startIdx: number, mark: string): number { + for (let i = startIdx; i < str.length; i++) { + if (str[i] === mark) { + // Check it's not escaped + if (i > 0 && str[i - 1] === '\\') continue; + // Check it's not part of a double mark (** or __) + if (str[i + 1] === mark) continue; + return i; + } + } + return -1; +} + +/** + * Convert plain text to formattedText with no entities. + */ +export function plainToTdlib(text: string): TdlibFormattedText { + return { + _: 'formattedText', + text, + entities: [] + }; +} + +/** + * Create a bold text entity for a portion of text. + */ +export function createBoldEntity(offset: number, length: number): TdlibTextEntity { + return { + _: 'textEntity', + offset, + length, + type: { _: 'textEntityTypeBold' } + }; +} + +/** + * Create an italic text entity. + */ +export function createItalicEntity(offset: number, length: number): TdlibTextEntity { + return { + _: 'textEntity', + offset, + length, + type: { _: 'textEntityTypeItalic' } + }; +} + +/** + * Create a code entity (inline code). + */ +export function createCodeEntity(offset: number, length: number): TdlibTextEntity { + return { + _: 'textEntity', + offset, + length, + type: { _: 'textEntityTypeCode' } + }; +} + +/** + * Create a pre entity (code block). + */ +export function createPreEntity(offset: number, length: number, language?: string): TdlibTextEntity { + return { + _: 'textEntity', + offset, + length, + type: language ? { _: 'textEntityTypePre', language } : { _: 'textEntityTypePre' } + }; +} diff --git a/src/channels/telegram-mtproto.ts b/src/channels/telegram-mtproto.ts new file mode 100644 index 0000000..61f2391 --- /dev/null +++ b/src/channels/telegram-mtproto.ts @@ -0,0 +1,929 @@ +/** + * Telegram MTProto Channel Adapter + * + * Uses TDLib/MTProto for Telegram user account messaging. + * Allows Letta agents to operate as Telegram users (not bots). + * + * Key differences from Bot API: + * - Full user capabilities (DM anyone first, larger files, no privacy mode) + * - Phone number authentication (not bot token) + * - Session persistence via TDLib database + * - UTF-16 entity offsets for text formatting + * + * Requirements: + * - npm install tdl prebuilt-tdlib + * - Telegram API credentials from https://my.telegram.org + */ + +import type { ChannelAdapter } from './types.js'; +import type { InboundMessage, OutboundMessage } from '../core/types.js'; +import type { DmPolicy } from '../pairing/types.js'; +import { isUserAllowed, upsertPairingRequest, approvePairingCode } from '../pairing/store.js'; +import { markdownToTdlib } from './telegram-mtproto-format.js'; +import * as readline from 'node:readline'; + +// TDLib imports - configured at runtime +let tdlModule: typeof import('tdl'); +let getTdjson: () => string; + +export type GroupPolicy = 'mention' | 'reply' | 'both' | 'off'; + +export interface TelegramMTProtoConfig { + phoneNumber: string; // E.164 format: +1234567890 + apiId: number; // From my.telegram.org + apiHash: string; // From my.telegram.org + databaseDirectory?: string; // Default: ./data/telegram-mtproto + // Security + dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open' + allowedUsers?: number[]; // Telegram user IDs (config allowlist) + // Group behavior + groupPolicy?: GroupPolicy; // 'mention', 'reply', 'both' (default), or 'off' + // Admin notifications + adminChatId?: number; // Chat ID for pairing request notifications +} + +// TDLib client type (simplified for our needs) +interface TdlibClient { + invoke(method: object): Promise; + iterUpdates(): AsyncIterable; + close(): Promise; + on(event: 'error', handler: (err: Error) => void): void; +} + +export class TelegramMTProtoAdapter implements ChannelAdapter { + readonly id = 'telegram-mtproto' as const; + readonly name = 'Telegram (MTProto)'; + + private config: TelegramMTProtoConfig; + private running = false; + private client: TdlibClient | null = null; + private updateLoopPromise: Promise | null = null; + private stopRequested = false; + + // Auth state machine (single update loop handles both auth and runtime) + private authState: 'initializing' | 'waiting_phone' | 'waiting_code' | 'waiting_password' | 'ready' = 'initializing'; + private authResolve: ((value: void) => void) | null = null; + private authReject: ((error: Error) => void) | null = null; + + // For group policy - track our identity and sent messages + private myUserId: number | null = null; + private myUsername: string | null = null; + private sentMessageIds = new Set(); // Track our messages for reply detection + + // For pairing approval via reply - track admin notification messages + // Maps admin notification messageId -> { code, userId, username } + private pendingPairingApprovals = new Map(); + + onMessage?: (msg: InboundMessage) => Promise; + onCommand?: (command: string) => Promise; + + constructor(config: TelegramMTProtoConfig) { + this.config = { + ...config, + dmPolicy: config.dmPolicy || 'pairing', + databaseDirectory: config.databaseDirectory || './data/telegram-mtproto', + groupPolicy: config.groupPolicy || 'both', + }; + } + + /** + * Check if a user is authorized based on dmPolicy + */ + private async checkAccess(userId: number): Promise<'allowed' | 'blocked' | 'pairing'> { + const policy = this.config.dmPolicy || 'pairing'; + + if (policy === 'open') { + return 'allowed'; + } + + const allowed = await isUserAllowed( + 'telegram-mtproto', + String(userId), + this.config.allowedUsers?.map(String) + ); + if (allowed) { + return 'allowed'; + } + + if (policy === 'allowlist') { + return 'blocked'; + } + + return 'pairing'; + } + + /** + * Format user-facing pairing message (simple, no implementation details) + */ + private formatUserPairingMessage(): string { + return `Your request has been passed on to the admin.`; + } + + /** + * Format admin notification for pairing request + */ + private formatAdminPairingNotification(username: string, userId: string, code: string, messageText?: string): string { + const userDisplay = username ? `@${username}` : `User`; + const messagePreview = messageText + ? `\n\nπŸ’¬ Message:\n${messageText.slice(0, 500)}${messageText.length > 500 ? '...' : ''}` + : ''; + return `πŸ”” **New pairing request** + +${userDisplay} (ID: ${userId}) wants to chat.${messagePreview} + +Reply **approve** or **deny** to this message.`; + } + + /** + * Get user info (username, first name) from Telegram + */ + private async getUserInfo(userId: number): Promise<{ username: string | null; firstName: string | null }> { + if (!this.client) return { username: null, firstName: null }; + + try { + const user = await this.client.invoke({ _: 'getUser', user_id: userId }); + return { + username: user.usernames?.editable_username || user.username || null, + firstName: user.first_name || null, + }; + } catch (err) { + console.warn(`[Telegram MTProto] Could not get user info for ${userId}:`, err); + return { username: null, firstName: null }; + } + } + + /** + * Get the private chat ID for a user (TDLib chat_id != user_id) + */ + private async getPrivateChatId(userId: number): Promise { + if (!this.client) return null; + + try { + const chat = await this.client.invoke({ _: 'createPrivateChat', user_id: userId, force: false }); + return chat.id; + } catch (err) { + console.warn(`[Telegram MTProto] Could not get private chat for user ${userId}:`, err); + return null; + } + } + + /** + * Prompt user for input (verification code or 2FA password) + */ + private async promptForInput(type: 'code' | 'password'): Promise { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const prompt = type === 'code' + ? '[Telegram MTProto] Enter verification code: ' + : '[Telegram MTProto] Enter 2FA password: '; + + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); + } + + /** + * Initialize TDLib client + */ + private async initializeClient(): Promise { + // Dynamic import to avoid issues if packages aren't installed + try { + tdlModule = await import('tdl'); + const prebuiltModule = await import('prebuilt-tdlib'); + getTdjson = prebuiltModule.getTdjson; + } catch (err: any) { + if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') { + throw new Error( + 'Telegram MTProto adapter requires tdl and prebuilt-tdlib packages.\n' + + 'Install them with: npm install tdl prebuilt-tdlib\n' + + 'See: https://github.com/Bannerets/tdl#installation' + ); + } + throw err; + } + + // CRITICAL: Configure tdl BEFORE creating client + tdlModule.configure({ tdjson: getTdjson() }); + + this.client = tdlModule.createClient({ + apiId: this.config.apiId, + apiHash: this.config.apiHash, + databaseDirectory: this.config.databaseDirectory, + filesDirectory: `${this.config.databaseDirectory}/files`, + }) as TdlibClient; + + // CRITICAL: Always attach error handler + this.client.on('error', (err) => { + console.error('[Telegram MTProto] Client error:', err); + }); + } + + /** + * Single update loop - handles both auth and runtime updates + * This ensures we only consume iterUpdates() once + */ + private async runUpdateLoop(): Promise { + if (!this.client) { + throw new Error('Client not initialized'); + } + + console.log('[Telegram MTProto] Starting update loop...'); + + for await (const update of this.client.iterUpdates()) { + if (this.stopRequested) { + console.log('[Telegram MTProto] Stop requested, exiting update loop'); + if (this.authReject) { + this.authReject(new Error('Stop requested')); + } + break; + } + + try { + // Handle auth updates until ready + if (this.authState !== 'ready' && update._ === 'updateAuthorizationState') { + await this.handleAuthUpdate(update); + } else if (this.authState === 'ready') { + // Normal runtime updates + await this.handleUpdate(update); + } + // Ignore non-auth updates before we're ready + } catch (err) { + console.error('[Telegram MTProto] Error handling update:', err); + // If auth fails, reject the auth promise + if (this.authState !== 'ready' && this.authReject) { + this.authReject(err as Error); + break; + } + } + } + } + + /** + * Handle authorization state updates + */ + private async handleAuthUpdate(update: any): Promise { + const state = update.authorization_state; + + switch (state._) { + case 'authorizationStateWaitTdlibParameters': + this.authState = 'initializing'; + // TDLib handles this automatically with createClient options + break; + + case 'authorizationStateWaitPhoneNumber': + this.authState = 'waiting_phone'; + console.log('[Telegram MTProto] Sending phone number...'); + await this.client!.invoke({ + _: 'setAuthenticationPhoneNumber', + phone_number: this.config.phoneNumber, + }); + break; + + case 'authorizationStateWaitCode': + this.authState = 'waiting_code'; + console.log('[Telegram MTProto] Verification code sent to your Telegram app'); + const code = await this.promptForInput('code'); + if (this.stopRequested) throw new Error('Stop requested'); + await this.client!.invoke({ + _: 'checkAuthenticationCode', + code, + }); + break; + + case 'authorizationStateWaitPassword': + this.authState = 'waiting_password'; + console.log('[Telegram MTProto] 2FA password required'); + const password = await this.promptForInput('password'); + if (this.stopRequested) throw new Error('Stop requested'); + await this.client!.invoke({ + _: 'checkAuthenticationPassword', + password, + }); + break; + + case 'authorizationStateReady': + this.authState = 'ready'; + console.log('[Telegram MTProto] Authenticated successfully!'); + console.log(`[Telegram MTProto] Session saved to ${this.config.databaseDirectory}/`); + // Get our own user info for mention/reply detection + try { + const me = await this.client!.invoke({ _: 'getMe' }); + this.myUserId = me.id; + this.myUsername = me.usernames?.editable_username || me.username || null; + console.log(`[Telegram MTProto] Logged in as: ${this.myUsername || this.myUserId}`); + } catch (err) { + console.warn('[Telegram MTProto] Could not fetch user info:', err); + } + // Signal that auth is complete + if (this.authResolve) { + this.authResolve(); + this.authResolve = null; + this.authReject = null; + } + break; + + case 'authorizationStateClosed': + case 'authorizationStateClosing': + throw new Error('Client is closing'); + + case 'authorizationStateLoggingOut': + throw new Error('Client is logging out'); + } + } + + /** + * Handle a single TDLib update + */ + private async handleUpdate(update: any): Promise { + switch (update._) { + case 'updateNewMessage': + await this.handleNewMessage(update.message); + break; + + case 'updateMessageSendSucceeded': + // Track the real message ID for reply detection + // old_message_id is the temp ID, message.id is the real server ID + if (update.old_message_id && update.message?.id) { + this.sentMessageIds.add(update.message.id); + + // Also update pending pairing approvals if this was an admin notification + const pending = this.pendingPairingApprovals.get(update.old_message_id); + if (pending) { + this.pendingPairingApprovals.delete(update.old_message_id); + this.pendingPairingApprovals.set(update.message.id, pending); + } + } + break; + + case 'updateConnectionState': + this.handleConnectionState(update.state); + break; + + // Add other update types as needed + } + } + + /** + * Handle incoming message + */ + private async handleNewMessage(message: any): Promise { + // Skip outgoing messages (messages we sent) + if (message.is_outgoing) return; + + // Check for pairing approval reply from admin + const replyToId = message.reply_to?.message_id; + if (replyToId && this.pendingPairingApprovals.has(replyToId)) { + await this.handlePairingApprovalReply(message, replyToId); + return; + } + + // Skip ALL messages from admin chat (don't trigger agent) + const msgChatId = message.chat_id; + if (this.config.adminChatId && msgChatId === this.config.adminChatId) { + // Only process replies to pairing notifications (handled above) + // All other messages in admin chat are ignored + return; + } + + // Skip if no handler (for normal messages) + if (!this.onMessage) return; + + // Skip non-text messages for now + if (message.content?._ !== 'messageText') return; + + // Get sender ID - must be a user + const senderId = message.sender_id; + if (!senderId || senderId._ !== 'messageSenderUser') return; + + const userId = senderId.user_id; + const chatId = message.chat_id; + const text = message.content?.text?.text || ''; + const messageId = String(message.id); + + // Check if this is a group chat and apply group policy + const isGroup = await this.isGroupChat(chatId); + if (isGroup) { + const shouldRespond = await this.shouldRespondInGroup(message, chatId); + if (!shouldRespond) { + return; + } + } + + // Check access (DM policy) + const access = await this.checkAccess(userId); + + if (access === 'blocked') { + console.log(`[Telegram MTProto] Blocked message from user ${userId}`); + return; + } + + if (access === 'pairing') { + // Create pairing request + const { code, created } = await upsertPairingRequest('telegram-mtproto', String(userId)); + + // Pairing queue is full: notify user and stop + if (!code) { + await this.sendMessage({ + chatId: String(chatId), + text: 'Too many pending pairing requests. Please try again later.', + }); + return; + } + + // Existing pending request: don't send duplicate notifications + if (!created) { + return; + } + + // Send simple acknowledgment to user (no implementation details) + await this.sendMessage({ chatId: String(chatId), text: this.formatUserPairingMessage() }); + + // Send admin notification if admin chat is configured + if (this.config.adminChatId) { + const userInfo = await this.getUserInfo(userId); + const adminMsg = this.formatAdminPairingNotification( + userInfo.username || userInfo.firstName || '', + String(userId), + code, + text + ); + try { + const result = await this.sendMessage({ chatId: String(this.config.adminChatId), text: adminMsg }); + + // Track this notification for reply-based approval + this.pendingPairingApprovals.set(Number(result.messageId), { + code, + userId: String(userId), + username: userInfo.username || userInfo.firstName || String(userId), + }); + + // Clean up old entries (keep last 100) + if (this.pendingPairingApprovals.size > 100) { + const oldest = this.pendingPairingApprovals.keys().next().value; + if (oldest !== undefined) { + this.pendingPairingApprovals.delete(oldest); + } + } + } catch (err) { + console.error(`[Telegram MTProto] Failed to send admin notification:`, err); + // Fall back to console + console.log(`[Telegram MTProto] Pairing request from ${userInfo.username || userId}: ${code}`); + console.log(`[Telegram MTProto] To approve: lettabot pairing approve telegram-mtproto ${code}`); + } + } else { + // No admin chat configured, log to console + const userInfo = await this.getUserInfo(userId); + console.log(`[Telegram MTProto] Pairing request from ${userInfo.username || userId}: ${code}`); + console.log(`[Telegram MTProto] To approve: lettabot pairing approve telegram-mtproto ${code}`); + } + return; + } + + // Build inbound message + const inboundMsg: InboundMessage = { + channel: 'telegram-mtproto', + chatId: String(chatId), + userId: String(userId), + text, + messageId, + timestamp: new Date(message.date * 1000), + }; + + // Call handler + await this.onMessage(inboundMsg); + } + + /** + * Handle pairing approval/denial via reply to admin notification + */ + private async handlePairingApprovalReply(message: any, replyToId: number): Promise { + const pending = this.pendingPairingApprovals.get(replyToId); + if (!pending) return; + + const text = (message.content?.text?.text || '').toLowerCase().trim(); + const chatId = message.chat_id; + + if (text === 'approve' || text === 'yes' || text === 'y') { + // Approve the pairing + const result = await approvePairingCode('telegram-mtproto', pending.code); + + if (result) { + // Notify admin + await this.sendMessage({ + chatId: String(chatId), + text: `βœ… Approved! ${pending.username} can now chat.`, + }); + + // Notify user (need to get their chat ID, not user ID) + const userChatId = await this.getPrivateChatId(Number(pending.userId)); + if (userChatId) { + await this.sendMessage({ + chatId: String(userChatId), + text: `You've been approved! You can now chat.`, + }); + } + + console.log(`[Telegram MTProto] Approved pairing for ${pending.username} (${pending.userId})`); + } else { + await this.sendMessage({ + chatId: String(chatId), + text: `❌ Could not approve: Code not found or expired.`, + }); + } + + // Remove from pending + this.pendingPairingApprovals.delete(replyToId); + + } else if (text === 'deny' || text === 'no' || text === 'n' || text === 'reject') { + // Deny the pairing (just remove from pending, don't add to allowlist) + // Silent denial - don't notify the user (security/privacy) + await this.sendMessage({ + chatId: String(chatId), + text: `❌ Denied. ${pending.username} will not be able to chat.`, + }); + + console.log(`[Telegram MTProto] Denied pairing for ${pending.username} (${pending.userId})`); + + // Remove from pending + this.pendingPairingApprovals.delete(replyToId); + } + // If text is something else, just ignore (don't process as regular message) + } + + /** + * Handle connection state changes + */ + private handleConnectionState(state: any): void { + switch (state._) { + case 'connectionStateReady': + console.log('[Telegram MTProto] Connected'); + break; + case 'connectionStateConnecting': + console.log('[Telegram MTProto] Connecting...'); + break; + case 'connectionStateUpdating': + console.log('[Telegram MTProto] Updating...'); + break; + case 'connectionStateWaitingForNetwork': + console.log('[Telegram MTProto] Waiting for network...'); + break; + } + } + + // ==================== Group Policy Helpers ==================== + + /** + * Check if a chat is a group (basic group or supergroup) + */ + private async isGroupChat(chatId: number): Promise { + if (!this.client) return false; + + try { + const chat = await this.client.invoke({ _: 'getChat', chat_id: chatId }); + const chatType = chat.type?._; + return chatType === 'chatTypeBasicGroup' || chatType === 'chatTypeSupergroup'; + } catch (err) { + console.warn('[Telegram MTProto] Could not determine chat type:', err); + return false; + } + } + + /** + * Check if we are mentioned in the message + * Checks for @username mentions and user ID mentions + */ + private isMentioned(message: any): boolean { + if (!this.myUserId) return false; + + const text = message.content?.text?.text || ''; + const entities = message.content?.text?.entities || []; + + for (const entity of entities) { + const entityType = entity.type?._; + + // Check for @username mention + if (entityType === 'textEntityTypeMention') { + const mentionText = text.substring(entity.offset, entity.offset + entity.length); + // Compare without @ prefix, case-insensitive + if (this.myUsername && mentionText.toLowerCase() === `@${this.myUsername.toLowerCase()}`) { + return true; + } + } + + // Check for mention by user ID (textEntityTypeMentionName) + if (entityType === 'textEntityTypeMentionName' && entity.type.user_id === this.myUserId) { + return true; + } + } + + return false; + } + + /** + * Check if message is a reply to one of our messages + */ + private isReplyToUs(message: any): boolean { + const replyTo = message.reply_to?.message_id || message.reply_to_message_id; + if (!replyTo) return false; + return this.sentMessageIds.has(replyTo); + } + + /** + * Apply group policy to determine if we should respond + * Returns true if we should process the message, false to ignore + */ + private async shouldRespondInGroup(message: any, chatId: number): Promise { + const policy = this.config.groupPolicy || 'both'; + + // 'off' means never respond in groups + if (policy === 'off') { + console.log('[Telegram MTProto] Group policy is off, ignoring group message'); + return false; + } + + const mentioned = this.isMentioned(message); + const isReply = this.isReplyToUs(message); + + switch (policy) { + case 'mention': + if (!mentioned) { + console.log('[Telegram MTProto] Not mentioned in group, ignoring'); + return false; + } + return true; + + case 'reply': + if (!isReply) { + console.log('[Telegram MTProto] Not a reply to us in group, ignoring'); + return false; + } + return true; + + case 'both': + default: + if (!mentioned && !isReply) { + // Silent ignore - don't log every message in busy groups + return false; + } + return true; + } + } + + // ==================== ChannelAdapter Interface ==================== + + async start(): Promise { + if (this.running) return; + + console.log('[Telegram MTProto] Starting adapter...'); + this.stopRequested = false; + this.authState = 'initializing'; + + try { + // Initialize TDLib client + await this.initializeClient(); + + // Create auth promise - will be resolved when authorizationStateReady is received + const authPromise = new Promise((resolve, reject) => { + this.authResolve = resolve; + this.authReject = reject; + }); + + // Start single update loop in background (handles both auth and runtime) + this.updateLoopPromise = this.runUpdateLoop().catch((err) => { + if (this.running && !this.stopRequested) { + console.error('[Telegram MTProto] Update loop error:', err); + this.running = false; + } + }); + + // Wait for auth to complete + await authPromise; + + this.running = true; + console.log('[Telegram MTProto] Adapter started'); + } catch (err) { + console.error('[Telegram MTProto] Failed to start:', err); + throw err; + } + } + + async stop(): Promise { + // Always allow stop, even during auth (handles ctrl+c during code/password prompt) + console.log('[Telegram MTProto] Stopping adapter...'); + this.stopRequested = true; + this.running = false; + + if (this.client) { + try { + await this.client.close(); + } catch (err) { + // Ignore errors during shutdown (client may already be closing) + if (!String(err).includes('closed')) { + console.error('[Telegram MTProto] Error closing client:', err); + } + } + this.client = null; + } + + console.log('[Telegram MTProto] Adapter stopped'); + } + + isRunning(): boolean { + return this.running; + } + + supportsEditing(): boolean { + // Disabled for now: TDLib sendMessage returns temporary IDs, + // and editMessage fails with "Message not found" until + // updateMessageSendSucceeded provides the real ID. + // TODO: Implement message ID tracking to enable streaming edits + return false; + } + + async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> { + if (!this.client) { + throw new Error('Client not initialized'); + } + + const formatted = markdownToTdlib(msg.text); + + const result = await this.client.invoke({ + _: 'sendMessage', + chat_id: this.safeChatId(msg.chatId), + input_message_content: { + _: 'inputMessageText', + text: formatted, + link_preview_options: null, + clear_draft: false, + }, + }); + + // Track this message ID for reply detection in groups + // Note: This is the temp ID; the real ID comes via updateMessageSendSucceeded + // For reply detection, we track both temp and real IDs + this.sentMessageIds.add(result.id); + + // Limit set size to prevent memory leak (keep last 1000 messages) + // Delete 100 oldest entries at once to avoid constant single deletions + if (this.sentMessageIds.size > 1000) { + const iterator = this.sentMessageIds.values(); + for (let i = 0; i < 100; i++) { + const oldest = iterator.next().value; + if (oldest !== undefined) { + this.sentMessageIds.delete(oldest); + } + } + } + + return { messageId: String(result.id) }; + } + + async editMessage(chatId: string, messageId: string, text: string): Promise { + if (!this.client) { + throw new Error('Client not initialized'); + } + + const formatted = markdownToTdlib(text); + + await this.client.invoke({ + _: 'editMessageText', + chat_id: this.safeChatId(chatId), + message_id: this.safeMessageId(messageId), + input_message_content: { + _: 'inputMessageText', + text: formatted, + }, + }); + } + + async sendTypingIndicator(chatId: string): Promise { + if (!this.client) return; + + try { + await this.client.invoke({ + _: 'sendChatAction', + chat_id: this.safeChatId(chatId), + action: { _: 'chatActionTyping' }, + }); + } catch (err) { + // Typing indicators are best-effort, don't throw + console.warn('[Telegram MTProto] Failed to send typing indicator:', err); + } + } + + // ==================== Helpers ==================== + + /** + * Safely convert chatId to number, checking for safe integer bounds + * @throws Error if chatId exceeds JavaScript safe integer bounds + */ + private safeChatId(chatId: string): number { + const num = Number(chatId); + if (!Number.isSafeInteger(num)) { + throw new Error(`Chat ID ${chatId} exceeds safe integer bounds (max: ${Number.MAX_SAFE_INTEGER}). This chat cannot be used with TDLib's number-based API.`); + } + return num; + } + + /** + * Safely convert messageId to number + * @throws Error if messageId exceeds JavaScript safe integer bounds + */ + private safeMessageId(messageId: string): number { + const num = Number(messageId); + if (!Number.isSafeInteger(num)) { + throw new Error(`Message ID ${messageId} exceeds safe integer bounds (max: ${Number.MAX_SAFE_INTEGER}).`); + } + return num; + } + + // ==================== Public API for Letta Tools ==================== + + /** + * Get public user info (for Letta telegram_get_user_info tool) + */ + async getPublicUserInfo(userId: number): Promise<{ username: string | null; firstName: string | null; lastName: string | null }> { + if (!this.client) throw new Error('Client not initialized'); + + try { + const user = await this.client.invoke({ _: 'getUser', user_id: userId }); + return { + username: user.usernames?.editable_username || user.username || null, + firstName: user.first_name || null, + lastName: user.last_name || null, + }; + } catch (err) { + console.warn(`[Telegram MTProto] Could not get user info for ${userId}:`, err); + throw err; + } + } + + /** + * Initiate a direct message to a user (for Letta telegram_send_dm tool) + * Creates a private chat if needed, then sends the message. + */ + async initiateDirectMessage(userId: number, text: string): Promise<{ chatId: string; messageId: string }> { + if (!this.client) throw new Error('Client not initialized'); + + // Create private chat (or get existing) + const chat = await this.client.invoke({ _: 'createPrivateChat', user_id: userId, force: false }); + const chatId = chat.id; + + // Send the message + const formatted = markdownToTdlib(text); + const result = await this.client.invoke({ + _: 'sendMessage', + chat_id: chatId, + input_message_content: { + _: 'inputMessageText', + text: formatted, + link_preview_options: null, + clear_draft: false, + }, + }); + + // Track message for reply detection + this.sentMessageIds.add(result.id); + if (this.sentMessageIds.size > 1000) { + const oldest = this.sentMessageIds.values().next().value; + if (oldest !== undefined) { + this.sentMessageIds.delete(oldest); + } + } + + return { chatId: String(chatId), messageId: String(result.id) }; + } + + /** + * Search for a user by username (for Letta telegram_find_user tool) + */ + async searchUser(username: string): Promise<{ userId: number; username: string | null; firstName: string | null } | null> { + if (!this.client) throw new Error('Client not initialized'); + + try { + // Remove @ prefix if present + const cleanUsername = username.replace(/^@/, ''); + const result = await this.client.invoke({ _: 'searchPublicChat', username: cleanUsername }); + + if (result.type?._ === 'chatTypePrivate') { + const userId = result.type.user_id; + const userInfo = await this.getPublicUserInfo(userId); + return { + userId, + username: userInfo.username, + firstName: userInfo.firstName, + }; + } + return null; + } catch (err) { + console.warn(`[Telegram MTProto] Could not find user @${username}:`, err); + return null; + } + } +} diff --git a/src/channels/tests/telegram-mtproto-format.test.ts b/src/channels/tests/telegram-mtproto-format.test.ts new file mode 100644 index 0000000..f186e10 --- /dev/null +++ b/src/channels/tests/telegram-mtproto-format.test.ts @@ -0,0 +1,242 @@ +/** + * Tests for telegram-mtproto-format.ts + * + * CRITICAL: These tests verify UTF-16 offset calculations. + * TDLib uses UTF-16 code units, and emoji/surrogate pairs take 2 units. + */ + +import { describe, it, expect } from 'vitest'; +import { + markdownToTdlib, + plainToTdlib, + utf16Length, + TdlibFormattedText +} from '../telegram-mtproto-format.js'; + +describe('utf16Length', () => { + it('returns correct length for ASCII text', () => { + expect(utf16Length('hello')).toBe(5); + expect(utf16Length('')).toBe(0); + expect(utf16Length('a')).toBe(1); + }); + + it('returns correct length for basic emoji (BMP)', () => { + // Most common emoji are actually outside BMP + expect(utf16Length('☺')).toBe(1); // U+263A is in BMP + }); + + it('returns correct length for emoji with surrogate pairs', () => { + // πŸ‘‹ U+1F44B is outside BMP, takes 2 UTF-16 code units + expect(utf16Length('πŸ‘‹')).toBe(2); + expect(utf16Length('Hello πŸ‘‹')).toBe(8); // 6 + 2 + expect(utf16Length('πŸ‘‹πŸ‘‹')).toBe(4); // 2 + 2 + }); + + it('returns correct length for complex emoji sequences', () => { + // πŸ‘¨β€πŸ‘©β€πŸ‘§ family emoji (multiple code points joined with ZWJ) + const family = 'πŸ‘¨β€πŸ‘©β€πŸ‘§'; + expect(utf16Length(family)).toBe(8); // Each person is 2, ZWJ is 1 each + }); + + it('returns correct length for mixed content', () => { + expect(utf16Length('Hi πŸ‘‹ there!')).toBe(12); // 3 + 2 + 7 + }); +}); + +describe('plainToTdlib', () => { + it('creates formattedText with no entities', () => { + const result = plainToTdlib('Hello world'); + expect(result._).toBe('formattedText'); + expect(result.text).toBe('Hello world'); + expect(result.entities).toEqual([]); + }); + + it('handles empty string', () => { + const result = plainToTdlib(''); + expect(result.text).toBe(''); + expect(result.entities).toEqual([]); + }); +}); + +describe('markdownToTdlib', () => { + describe('plain text', () => { + it('passes through plain text unchanged', () => { + const result = markdownToTdlib('Hello world'); + expect(result.text).toBe('Hello world'); + expect(result.entities).toEqual([]); + }); + + it('handles empty string', () => { + const result = markdownToTdlib(''); + expect(result.text).toBe(''); + expect(result.entities).toEqual([]); + }); + }); + + describe('bold formatting', () => { + it('handles **bold** syntax', () => { + const result = markdownToTdlib('Hello **world**'); + expect(result.text).toBe('Hello world'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0]).toEqual({ + _: 'textEntity', + offset: 6, + length: 5, + type: { _: 'textEntityTypeBold' } + }); + }); + + it('handles __bold__ syntax', () => { + const result = markdownToTdlib('Hello __world__'); + expect(result.text).toBe('Hello world'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0].type._).toBe('textEntityTypeBold'); + }); + + it('handles bold with emoji', () => { + const result = markdownToTdlib('Hello **πŸ‘‹ wave**'); + expect(result.text).toBe('Hello πŸ‘‹ wave'); + expect(result.entities[0].offset).toBe(6); + expect(result.entities[0].length).toBe(7); // 2 (emoji) + 5 (space + wave) + }); + }); + + describe('italic formatting', () => { + it('handles *italic* syntax', () => { + const result = markdownToTdlib('Hello *world*'); + expect(result.text).toBe('Hello world'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0]).toEqual({ + _: 'textEntity', + offset: 6, + length: 5, + type: { _: 'textEntityTypeItalic' } + }); + }); + + it('handles _italic_ syntax', () => { + const result = markdownToTdlib('Hello _world_'); + expect(result.text).toBe('Hello world'); + expect(result.entities[0].type._).toBe('textEntityTypeItalic'); + }); + }); + + describe('code formatting', () => { + it('handles `inline code`', () => { + const result = markdownToTdlib('Use `npm install`'); + expect(result.text).toBe('Use npm install'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0]).toEqual({ + _: 'textEntity', + offset: 4, + length: 11, + type: { _: 'textEntityTypeCode' } + }); + }); + + it('handles code blocks without language', () => { + const result = markdownToTdlib('```\nconst x = 1;\n```'); + expect(result.text).toBe('const x = 1;\n'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0].type).toEqual({ _: 'textEntityTypePre' }); + }); + + it('handles code blocks with language', () => { + const result = markdownToTdlib('```typescript\nconst x: number = 1;\n```'); + expect(result.text).toBe('const x: number = 1;\n'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0].type).toEqual({ + _: 'textEntityTypePre', + language: 'typescript' + }); + }); + }); + + describe('strikethrough formatting', () => { + it('handles ~~strikethrough~~', () => { + const result = markdownToTdlib('This is ~~deleted~~ text'); + expect(result.text).toBe('This is deleted text'); + expect(result.entities).toHaveLength(1); + expect(result.entities[0]).toEqual({ + _: 'textEntity', + offset: 8, + length: 7, + type: { _: 'textEntityTypeStrikethrough' } + }); + }); + }); + + describe('multiple entities', () => { + it('handles multiple formatting types', () => { + const result = markdownToTdlib('**bold** and *italic*'); + expect(result.text).toBe('bold and italic'); + expect(result.entities).toHaveLength(2); + expect(result.entities[0].type._).toBe('textEntityTypeBold'); + expect(result.entities[1].type._).toBe('textEntityTypeItalic'); + }); + + it('calculates correct offsets for sequential entities', () => { + const result = markdownToTdlib('**A** **B** **C**'); + expect(result.text).toBe('A B C'); + expect(result.entities).toHaveLength(3); + expect(result.entities[0].offset).toBe(0); // A + expect(result.entities[1].offset).toBe(2); // B + expect(result.entities[2].offset).toBe(4); // C + }); + }); + + describe('UTF-16 offset edge cases', () => { + it('calculates correct offset after emoji', () => { + // "πŸ‘‹ **bold**" - emoji takes 2 units, space is 1 + const result = markdownToTdlib('πŸ‘‹ **bold**'); + expect(result.text).toBe('πŸ‘‹ bold'); + expect(result.entities[0].offset).toBe(3); // 2 (emoji) + 1 (space) + expect(result.entities[0].length).toBe(4); + }); + + it('handles emoji inside formatted text', () => { + const result = markdownToTdlib('**Hello πŸ‘‹ World**'); + expect(result.text).toBe('Hello πŸ‘‹ World'); + expect(result.entities[0].offset).toBe(0); + expect(result.entities[0].length).toBe(14); // 6 + 2 + 6 + }); + + it('handles multiple emoji', () => { + const result = markdownToTdlib('πŸ‘‹πŸ‘‹ **test** πŸ‘‹'); + expect(result.text).toBe('πŸ‘‹πŸ‘‹ test πŸ‘‹'); + // Offset: 4 (two emoji) + 1 (space) = 5 + expect(result.entities[0].offset).toBe(5); + expect(result.entities[0].length).toBe(4); + }); + + it('handles flag emoji (multi-codepoint)', () => { + // πŸ‡ΊπŸ‡Έ is two regional indicator symbols + const flag = 'πŸ‡ΊπŸ‡Έ'; + expect(utf16Length(flag)).toBe(4); // Each regional indicator is 2 units + + const result = markdownToTdlib(`${flag} **USA**`); + expect(result.text).toBe('πŸ‡ΊπŸ‡Έ USA'); + expect(result.entities[0].offset).toBe(5); // 4 (flag) + 1 (space) + }); + }); + + describe('unclosed formatting', () => { + it('treats unclosed ** as literal', () => { + const result = markdownToTdlib('Hello **world'); + expect(result.text).toBe('Hello **world'); + expect(result.entities).toEqual([]); + }); + + it('treats unclosed ` as literal', () => { + const result = markdownToTdlib('Hello `world'); + expect(result.text).toBe('Hello `world'); + expect(result.entities).toEqual([]); + }); + + it('treats unclosed ``` as literal', () => { + const result = markdownToTdlib('```code without close'); + expect(result.text).toBe('```code without close'); + expect(result.entities).toEqual([]); + }); + }); +}); diff --git a/src/channels/tests/telegram-mtproto-pairing.test.ts b/src/channels/tests/telegram-mtproto-pairing.test.ts new file mode 100644 index 0000000..8a15e27 --- /dev/null +++ b/src/channels/tests/telegram-mtproto-pairing.test.ts @@ -0,0 +1,108 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../../pairing/store.js', () => ({ + isUserAllowed: vi.fn(), + upsertPairingRequest: vi.fn(), + approvePairingCode: vi.fn(), +})); + +import { TelegramMTProtoAdapter } from '../telegram-mtproto.js'; +import { isUserAllowed, upsertPairingRequest } from '../../pairing/store.js'; + +const mockedIsUserAllowed = vi.mocked(isUserAllowed); +const mockedUpsertPairingRequest = vi.mocked(upsertPairingRequest); + +function makeAdapter(overrides: Partial[0]> = {}) { + return new TelegramMTProtoAdapter({ + phoneNumber: '+15551234567', + apiId: 12345, + apiHash: 'test-hash', + dmPolicy: 'pairing', + ...overrides, + }); +} + +function makeIncomingTextMessage(overrides: Record = {}) { + return { + is_outgoing: false, + chat_id: 1001, + id: 5001, + date: Math.floor(Date.now() / 1000), + sender_id: { _: 'messageSenderUser', user_id: 42 }, + content: { + _: 'messageText', + text: { text: 'hello', entities: [] }, + }, + ...overrides, + }; +} + +describe('TelegramMTProtoAdapter pairing flow', () => { + beforeEach(() => { + vi.clearAllMocks(); + mockedIsUserAllowed.mockResolvedValue(false); + }); + + it('sends queue-full notice when no pairing code can be allocated', async () => { + const adapter = makeAdapter(); + adapter.onMessage = vi.fn().mockResolvedValue(undefined); + + mockedUpsertPairingRequest.mockResolvedValue({ code: '', created: false }); + const sendSpy = vi.spyOn(adapter, 'sendMessage').mockResolvedValue({ messageId: '1' }); + + await (adapter as any).handleNewMessage(makeIncomingTextMessage()); + + expect(mockedUpsertPairingRequest).toHaveBeenCalledWith('telegram-mtproto', '42'); + expect(sendSpy).toHaveBeenCalledTimes(1); + expect(sendSpy).toHaveBeenCalledWith({ + chatId: '1001', + text: 'Too many pending pairing requests. Please try again later.', + }); + expect(adapter.onMessage).not.toHaveBeenCalled(); + }); + + it('silently deduplicates existing pending pairing requests', async () => { + const adapter = makeAdapter(); + adapter.onMessage = vi.fn().mockResolvedValue(undefined); + + mockedUpsertPairingRequest.mockResolvedValue({ code: 'ABC123', created: false }); + const sendSpy = vi.spyOn(adapter, 'sendMessage').mockResolvedValue({ messageId: '1' }); + + await (adapter as any).handleNewMessage(makeIncomingTextMessage()); + + expect(mockedUpsertPairingRequest).toHaveBeenCalledWith('telegram-mtproto', '42'); + expect(sendSpy).not.toHaveBeenCalled(); + expect(adapter.onMessage).not.toHaveBeenCalled(); + }); + + it('notifies user and admin once for newly created pairing requests', async () => { + const adapter = makeAdapter({ adminChatId: 9999 }); + adapter.onMessage = vi.fn().mockResolvedValue(undefined); + + mockedUpsertPairingRequest.mockResolvedValue({ code: 'ABC123', created: true }); + vi.spyOn(adapter as any, 'getUserInfo').mockResolvedValue({ username: 'alice', firstName: null }); + const sendSpy = vi.spyOn(adapter, 'sendMessage') + .mockResolvedValueOnce({ messageId: '100' }) // user notice + .mockResolvedValueOnce({ messageId: '200' }); // admin notice + + await (adapter as any).handleNewMessage( + makeIncomingTextMessage({ + content: { + _: 'messageText', + text: { text: 'can you help?', entities: [] }, + }, + }) + ); + + expect(sendSpy).toHaveBeenCalledTimes(2); + expect(sendSpy.mock.calls[0][0]).toEqual({ + chatId: '1001', + text: 'Your request has been passed on to the admin.', + }); + expect(sendSpy.mock.calls[1][0].chatId).toBe('9999'); + + const pendingApprovals = (adapter as any).pendingPairingApprovals as Map; + expect(pendingApprovals.size).toBe(1); + expect([...pendingApprovals.values()][0].code).toBe('ABC123'); + }); +}); diff --git a/src/config/io.ts b/src/config/io.ts index 4bbb8f6..7dcebc6 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -131,6 +131,32 @@ export function configToEnv(config: LettaBotConfig): Record { env.TELEGRAM_DM_POLICY = config.channels.telegram.dmPolicy; } } + // Telegram MTProto (user account mode) + const mtproto = config.channels['telegram-mtproto']; + if (mtproto?.enabled && mtproto.phoneNumber) { + env.TELEGRAM_MTPROTO_PHONE = mtproto.phoneNumber; + if (mtproto.apiId) { + env.TELEGRAM_MTPROTO_API_ID = String(mtproto.apiId); + } + if (mtproto.apiHash) { + env.TELEGRAM_MTPROTO_API_HASH = mtproto.apiHash; + } + if (mtproto.databaseDirectory) { + env.TELEGRAM_MTPROTO_DB_DIR = mtproto.databaseDirectory; + } + if (mtproto.dmPolicy) { + env.TELEGRAM_MTPROTO_DM_POLICY = mtproto.dmPolicy; + } + if (mtproto.allowedUsers?.length) { + env.TELEGRAM_MTPROTO_ALLOWED_USERS = mtproto.allowedUsers.join(','); + } + if (mtproto.groupPolicy) { + env.TELEGRAM_MTPROTO_GROUP_POLICY = mtproto.groupPolicy; + } + if (mtproto.adminChatId) { + env.TELEGRAM_MTPROTO_ADMIN_CHAT_ID = String(mtproto.adminChatId); + } + } if (config.channels.slack?.appToken) { env.SLACK_APP_TOKEN = config.channels.slack.appToken; } diff --git a/src/config/types.ts b/src/config/types.ts index 7aa8900..3bc1c76 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -22,6 +22,7 @@ export interface AgentConfig { /** Channels this agent connects to */ channels: { telegram?: TelegramConfig; + 'telegram-mtproto'?: TelegramMTProtoConfig; slack?: SlackConfig; whatsapp?: WhatsAppConfig; signal?: SignalConfig; @@ -76,6 +77,7 @@ export interface LettaBotConfig { // Channel configurations channels: { telegram?: TelegramConfig; + 'telegram-mtproto'?: TelegramMTProtoConfig; slack?: SlackConfig; whatsapp?: WhatsAppConfig; signal?: SignalConfig; @@ -168,6 +170,18 @@ export interface TelegramConfig { groups?: Record; // Per-group settings, "*" for defaults } +export interface TelegramMTProtoConfig { + enabled: boolean; + phoneNumber?: string; // E.164 format: +1234567890 + apiId?: number; // From my.telegram.org + apiHash?: string; // From my.telegram.org + databaseDirectory?: string; // Default: ./data/telegram-mtproto + dmPolicy?: 'pairing' | 'allowlist' | 'open'; + allowedUsers?: number[]; // Telegram user IDs + groupPolicy?: 'mention' | 'reply' | 'both' | 'off'; + adminChatId?: number; // Chat ID for pairing request notifications +} + export interface SlackConfig { enabled: boolean; appToken?: string; @@ -223,6 +237,25 @@ export interface DiscordConfig { groups?: Record; // Per-guild/channel settings, "*" for defaults } +/** + * Telegram MTProto (user account) configuration. + * Uses TDLib for user account mode instead of Bot API. + * Cannot be used simultaneously with TelegramConfig (bot mode). + */ +export interface TelegramMTProtoConfig { + enabled: boolean; + phoneNumber?: string; // E.164 format: +1234567890 + apiId?: number; // From my.telegram.org + apiHash?: string; // From my.telegram.org + databaseDirectory?: string; // Default: ./data/telegram-mtproto + dmPolicy?: 'pairing' | 'allowlist' | 'open'; + allowedUsers?: number[]; // Telegram user IDs + groupPolicy?: 'mention' | 'reply' | 'both' | 'off'; + adminChatId?: number; // Chat ID for pairing request notifications + groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate) + instantGroups?: string[]; // Chat IDs that bypass batching +} + export interface GoogleAccountConfig { account: string; services?: string[]; // e.g., ['gmail', 'calendar', 'drive', 'contacts', 'docs', 'sheets'] @@ -352,6 +385,10 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] { normalizeLegacyGroupFields(telegram, `${sourcePath}.telegram`); normalized.telegram = telegram; } + // telegram-mtproto: check apiId as the key credential + if (channels['telegram-mtproto']?.enabled !== false && channels['telegram-mtproto']?.apiId) { + normalized['telegram-mtproto'] = channels['telegram-mtproto']; + } if (channels.slack?.enabled !== false && channels.slack?.botToken && channels.slack?.appToken) { const slack = { ...channels.slack }; normalizeLegacyGroupFields(slack, `${sourcePath}.slack`); @@ -406,6 +443,20 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] { allowedUsers: parseList(process.env.TELEGRAM_ALLOWED_USERS), }; } + // telegram-mtproto env var fallback (only if telegram bot not configured) + if (!channels.telegram && !channels['telegram-mtproto'] && process.env.TELEGRAM_API_ID && process.env.TELEGRAM_API_HASH && process.env.TELEGRAM_PHONE_NUMBER) { + channels['telegram-mtproto'] = { + enabled: true, + apiId: parseInt(process.env.TELEGRAM_API_ID, 10), + apiHash: process.env.TELEGRAM_API_HASH, + phoneNumber: process.env.TELEGRAM_PHONE_NUMBER, + databaseDirectory: process.env.TELEGRAM_MTPROTO_DB_DIR || './data/telegram-mtproto', + dmPolicy: (process.env.TELEGRAM_DM_POLICY as 'pairing' | 'allowlist' | 'open') || 'pairing', + allowedUsers: parseList(process.env.TELEGRAM_ALLOWED_USERS)?.map(s => parseInt(s, 10)).filter(n => !isNaN(n)), + groupPolicy: (process.env.TELEGRAM_GROUP_POLICY as 'mention' | 'reply' | 'both' | 'off') || 'both', + adminChatId: process.env.TELEGRAM_ADMIN_CHAT_ID ? parseInt(process.env.TELEGRAM_ADMIN_CHAT_ID, 10) : undefined, + }; + } if (!channels.slack && process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) { channels.slack = { enabled: true, diff --git a/src/core/group-batching-config.test.ts b/src/core/group-batching-config.test.ts new file mode 100644 index 0000000..5e14960 --- /dev/null +++ b/src/core/group-batching-config.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import type { AgentConfig } from '../config/types.js'; +import { collectGroupBatchingConfig, resolveDebounceMs } from './group-batching-config.js'; + +describe('resolveDebounceMs', () => { + it('prefers groupDebounceSec over deprecated groupPollIntervalMin', () => { + expect(resolveDebounceMs({ groupDebounceSec: 2, groupPollIntervalMin: 9 })).toBe(2000); + }); + + it('falls back to default when no debounce config is provided', () => { + expect(resolveDebounceMs({})).toBe(5000); + }); +}); + +describe('collectGroupBatchingConfig', () => { + it('uses telegram-mtproto key for mtproto debounce settings', () => { + const channels: AgentConfig['channels'] = { + 'telegram-mtproto': { + enabled: true, + apiId: 12345, + groupDebounceSec: 1, + }, + }; + + const { intervals } = collectGroupBatchingConfig(channels); + + expect(intervals.get('telegram-mtproto')).toBe(1000); + expect(intervals.has('telegram')).toBe(false); + }); + + it('prefixes mtproto instant groups with telegram-mtproto channel id', () => { + const channels: AgentConfig['channels'] = { + 'telegram-mtproto': { + enabled: true, + apiId: 12345, + instantGroups: ['-1001', '-1002'], + }, + }; + + const { instantIds } = collectGroupBatchingConfig(channels); + + expect(instantIds.has('telegram-mtproto:-1001')).toBe(true); + expect(instantIds.has('telegram-mtproto:-1002')).toBe(true); + expect(instantIds.has('telegram:-1001')).toBe(false); + }); + + it('collects listening groups for supported channels', () => { + const channels: AgentConfig['channels'] = { + slack: { + enabled: true, + botToken: 'xoxb-test', + appToken: 'xapp-test', + listeningGroups: ['C001'], + }, + }; + + const { listeningIds } = collectGroupBatchingConfig(channels); + + expect(listeningIds.has('slack:C001')).toBe(true); + }); +}); diff --git a/src/core/group-batching-config.ts b/src/core/group-batching-config.ts new file mode 100644 index 0000000..03b09e3 --- /dev/null +++ b/src/core/group-batching-config.ts @@ -0,0 +1,58 @@ +import type { AgentConfig } from '../config/types.js'; + +type DebounceConfig = { groupDebounceSec?: number; groupPollIntervalMin?: number }; +type GroupBatchingConfig = DebounceConfig & { + instantGroups?: string[]; + listeningGroups?: string[]; +}; + +/** + * Resolve group debounce value to milliseconds. + * Prefers groupDebounceSec, falls back to deprecated groupPollIntervalMin. + * Default: 5 seconds (5000ms). + */ +export function resolveDebounceMs(channel: DebounceConfig): number { + if (channel.groupDebounceSec !== undefined) return channel.groupDebounceSec * 1000; + if (channel.groupPollIntervalMin !== undefined) return channel.groupPollIntervalMin * 60 * 1000; + return 5000; +} + +/** + * Build per-channel group batching configuration for an agent. + */ +export function collectGroupBatchingConfig( + channels: AgentConfig['channels'], +): { intervals: Map; instantIds: Set; listeningIds: Set } { + const intervals = new Map(); + const instantIds = new Set(); + const listeningIds = new Set(); + + const addChannel = (channel: string, config?: GroupBatchingConfig): void => { + if (!config) return; + intervals.set(channel, resolveDebounceMs(config)); + for (const id of config.instantGroups || []) { + instantIds.add(`${channel}:${id}`); + } + for (const id of config.listeningGroups || []) { + listeningIds.add(`${channel}:${id}`); + } + }; + + addChannel('telegram', channels.telegram); + + const mtprotoConfig = channels['telegram-mtproto']; + if (mtprotoConfig) { + // MTProto does not currently support listeningGroups, only instant/debounce behavior. + intervals.set('telegram-mtproto', resolveDebounceMs(mtprotoConfig)); + for (const id of mtprotoConfig.instantGroups || []) { + instantIds.add(`telegram-mtproto:${id}`); + } + } + + addChannel('slack', channels.slack); + addChannel('whatsapp', channels.whatsapp); + addChannel('signal', channels.signal); + addChannel('discord', channels.discord); + + return { intervals, instantIds, listeningIds }; +} diff --git a/src/core/types.ts b/src/core/types.ts index 74157f4..2d1e9a2 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -43,7 +43,7 @@ export interface TriggerContext { // Original Types // ============================================================================= -export type ChannelId = 'telegram' | 'slack' | 'whatsapp' | 'signal' | 'discord' | 'mock'; +export type ChannelId = 'telegram' | 'telegram-mtproto' | 'slack' | 'whatsapp' | 'signal' | 'discord' | 'mock'; export interface InboundAttachment { id?: string; diff --git a/src/main.ts b/src/main.ts index 5bbb182..ef13493 100644 --- a/src/main.ts +++ b/src/main.ts @@ -148,11 +148,13 @@ import { normalizeAgents } from './config/types.js'; import { LettaGateway } from './core/gateway.js'; import { LettaBot } from './core/bot.js'; import { TelegramAdapter } from './channels/telegram.js'; +import { TelegramMTProtoAdapter } from './channels/telegram-mtproto.js'; import { SlackAdapter } from './channels/slack.js'; import { WhatsAppAdapter } from './channels/whatsapp/index.js'; import { SignalAdapter } from './channels/signal.js'; import { DiscordAdapter } from './channels/discord.js'; import { GroupBatcher } from './core/group-batcher.js'; +import { collectGroupBatchingConfig } from './core/group-batching-config.js'; import { CronService } from './cron/service.js'; import { HeartbeatService } from './cron/heartbeat.js'; import { PollingService, parseGmailAccounts } from './polling/service.js'; @@ -272,17 +274,44 @@ function createChannelsForAgent( ): import('./channels/types.js').ChannelAdapter[] { const adapters: import('./channels/types.js').ChannelAdapter[] = []; - if (agentConfig.channels.telegram?.token) { + // Mutual exclusion: cannot use both Telegram Bot API and MTProto simultaneously + const hasTelegramBot = !!agentConfig.channels.telegram?.token; + const hasTelegramMtproto = !!(agentConfig.channels['telegram-mtproto'] as any)?.apiId; + + if (hasTelegramBot && hasTelegramMtproto) { + console.error(`\n Error: Agent "${agentConfig.name}" has both telegram and telegram-mtproto configured.`); + console.error(' The Bot API adapter and MTProto adapter cannot run together.'); + console.error(' Choose one: telegram (bot token) or telegram-mtproto (user account).\n'); + process.exit(1); + } + + if (hasTelegramBot) { adapters.push(new TelegramAdapter({ - token: agentConfig.channels.telegram.token, - dmPolicy: agentConfig.channels.telegram.dmPolicy || 'pairing', - allowedUsers: agentConfig.channels.telegram.allowedUsers && agentConfig.channels.telegram.allowedUsers.length > 0 - ? agentConfig.channels.telegram.allowedUsers.map(u => typeof u === 'string' ? parseInt(u, 10) : u) + token: agentConfig.channels.telegram!.token!, + dmPolicy: agentConfig.channels.telegram!.dmPolicy || 'pairing', + allowedUsers: agentConfig.channels.telegram!.allowedUsers && agentConfig.channels.telegram!.allowedUsers.length > 0 + ? agentConfig.channels.telegram!.allowedUsers.map(u => typeof u === 'string' ? parseInt(u, 10) : u) : undefined, attachmentsDir, attachmentsMaxBytes, - groups: agentConfig.channels.telegram.groups, - mentionPatterns: agentConfig.channels.telegram.mentionPatterns, + groups: agentConfig.channels.telegram!.groups, + mentionPatterns: agentConfig.channels.telegram!.mentionPatterns, + })); + } + + if (hasTelegramMtproto) { + const mtprotoConfig = agentConfig.channels['telegram-mtproto'] as any; + adapters.push(new TelegramMTProtoAdapter({ + apiId: mtprotoConfig.apiId, + apiHash: mtprotoConfig.apiHash, + phoneNumber: mtprotoConfig.phoneNumber, + databaseDirectory: mtprotoConfig.databaseDirectory || './data/telegram-mtproto', + dmPolicy: mtprotoConfig.dmPolicy || 'pairing', + allowedUsers: mtprotoConfig.allowedUsers && mtprotoConfig.allowedUsers.length > 0 + ? mtprotoConfig.allowedUsers.map((u: string | number) => typeof u === 'string' ? parseInt(u, 10) : u) + : undefined, + groupPolicy: mtprotoConfig.groupPolicy || 'both', + adminChatId: mtprotoConfig.adminChatId, })); } @@ -359,17 +388,6 @@ function createChannelsForAgent( return adapters; } -/** - * Resolve group debounce value to milliseconds. - * Prefers groupDebounceSec, falls back to deprecated groupPollIntervalMin. - * Default: 5 seconds (5000ms). - */ -function resolveDebounceMs(channel: { groupDebounceSec?: number; groupPollIntervalMin?: number }): number { - if (channel.groupDebounceSec !== undefined) return channel.groupDebounceSec * 1000; - if (channel.groupPollIntervalMin !== undefined) return channel.groupPollIntervalMin * 60 * 1000; - return 5000; // 5 seconds default -} - /** * Create and configure a group batcher for an agent */ @@ -377,22 +395,7 @@ function createGroupBatcher( agentConfig: import('./config/types.js').AgentConfig, bot: import('./core/interfaces.js').AgentSession, ): { batcher: GroupBatcher | null; intervals: Map; instantIds: Set; listeningIds: Set } { - const intervals = new Map(); // channel -> debounce ms - const instantIds = new Set(); - const listeningIds = new Set(); - - const channelNames = ['telegram', 'slack', 'whatsapp', 'signal', 'discord'] as const; - for (const channel of channelNames) { - const cfg = agentConfig.channels[channel]; - if (!cfg) continue; - intervals.set(channel, resolveDebounceMs(cfg)); - for (const id of (cfg as any).instantGroups || []) { - instantIds.add(`${channel}:${id}`); - } - for (const id of (cfg as any).listeningGroups || []) { - listeningIds.add(`${channel}:${id}`); - } - } + const { intervals, instantIds, listeningIds } = collectGroupBatchingConfig(agentConfig.channels); if (instantIds.size > 0) { console.log(`[Groups] Instant groups: ${[...instantIds].join(', ')}`); @@ -523,7 +526,7 @@ async function main() { initialStatus = bot.getStatus(); } } - + // Container deploy: discover by name if (!initialStatus.agentId && isContainerDeploy) { const found = await findAgentByName(agentConfig.name); @@ -533,7 +536,7 @@ async function main() { initialStatus = bot.getStatus(); } } - + if (!initialStatus.agentId) { console.log(`[Agent:${agentConfig.name}] No agent found - will create on first message`); } @@ -642,7 +645,7 @@ async function main() { host: apiHost, corsOrigin: apiCorsOrigin, }); - + // Status logging console.log('\n================================='); console.log(`LettaBot is running! (${gateway.size} agent${gateway.size > 1 ? 's' : ''})`); diff --git a/src/types/optional-modules.d.ts b/src/types/optional-modules.d.ts new file mode 100644 index 0000000..ffa1f95 --- /dev/null +++ b/src/types/optional-modules.d.ts @@ -0,0 +1,2 @@ +declare module 'tdl'; +declare module 'prebuilt-tdlib'; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7ce3cec --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + exclude: ['dist/**', 'node_modules/**'], + }, +}); +