From 313527aea3060166a74aeddc1228957ae3f89a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=A0=E7=9A=84=E7=94=A8=E6=88=B7=E5=90=8D?= <你的邮箱> Date: Sat, 8 Nov 2025 16:11:21 +0800 Subject: [PATCH] feat: migrate to GramJS with TS --- .env.example | 5 +- .gitignore | 18 +- README.md | 123 +- package-lock.json | 2508 ++++++++++++++++++++++++++++++++++++++++ package.json | 26 + requirements.txt | 4 - src/__init__.py | 0 src/config.py | 79 -- src/config.ts | 101 ++ src/groupMonitor.ts | 283 +++++ src/group_monitor.py | 204 ---- src/keywords.py | 98 -- src/keywords.ts | 129 +++ src/logger.ts | 35 + src/main.py | 76 -- src/main.ts | 36 + src/reporter.py | 54 - src/reporter.ts | 63 + src/telegramClient.ts | 53 + src/utils/prompt.ts | 29 + src/utils/text.ts | 16 + tests/keywords.spec.ts | 58 + tsconfig.json | 19 + vitest.config.ts | 8 + 24 files changed, 3434 insertions(+), 591 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 requirements.txt delete mode 100644 src/__init__.py delete mode 100644 src/config.py create mode 100644 src/config.ts create mode 100644 src/groupMonitor.ts delete mode 100644 src/group_monitor.py delete mode 100644 src/keywords.py create mode 100644 src/keywords.ts create mode 100644 src/logger.ts delete mode 100644 src/main.py create mode 100644 src/main.ts delete mode 100644 src/reporter.py create mode 100644 src/reporter.ts create mode 100644 src/telegramClient.ts create mode 100644 src/utils/prompt.ts create mode 100644 src/utils/text.ts create mode 100644 tests/keywords.spec.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.env.example b/.env.example index 064b729..340cd52 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,11 @@ API_ID=123456 API_HASH=change_me -SESSION_NAME=userbot_session +SESSION_NAME=userbot +SESSION_DIR=./sessions USER_PHONE=+8613000000000 +TWO_FA_PASSWORD= GROUP_LINKS=https://t.me/example_group REPORT_CHAT_LINK=https://t.me/example_group TELEGRAM_BOT_TOKEN=your_bot_token_here KEYWORDS_FILE=keywords.yaml +LOG_LEVEL=info diff --git a/.gitignore b/.gitignore index 7626538..a158011 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,3 @@ -# Python artifacts -__pycache__/ -*.pyc -*.pyo -*.pyd -*.log -*.sqlite3 - -# Virtual environments -.venv/ -env/ -venv/ - # VSCode / IDE .vscode/ .idea/ @@ -21,3 +8,8 @@ venv/ # Secrets / sessions .env *.session + +# Node artifacts +node_modules/ +dist/ +coverage/ diff --git a/README.md b/README.md index 4f4e9d7..a821c95 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,79 @@ -# 群监控自动加群 +# 群监控(GramJS 版) -使用 Telethon 登陆用户账号(User-Bot),自动加入目标群、抓取群主信息、监控消息并通过单独的 Bot 汇报关键词命中结果。 +使用 [GramJS](https://github.com/gram-js/gramjs) 驱动的 Node.js User-Bot 自动加入目标群、识别群主并实时监听消息。一旦命中关键词,结果会通过单独的 Telegram Bot 安全汇报到指定群/频道。 -## 功能概述 -- 通过 `GROUP_LINKS` 列表批量加入群(支持公开链接与 `https://t.me/+xxxx` 邀请链接)。 -- 获取群主/创建者信息并在汇报群内确认当前监控状态。 -- 长连接监听群消息,命中关键词时推送到指定群/频道。 -- 关键词配置独立于代码(`keywords.yaml`),可随时增删并自动热加载。 -- 汇报通道使用 Bot Token,避免 user-bot 重复发言。 +## 功能亮点 +- **GramJS 长连接**:以 Node.js 形式运行,便于使用 Chrome DevTools Protocol 调试 Node 后端。 +- **自动入群**:支持公开群链接与 `https://t.me/+xxxx` 邀请链接,必要时自动执行 `ImportChatInvite`。 +- **群主洞察**:拉取 `ChannelParticipantsAdmins`,找到创建者后写入汇报,方便对齐「自动添加群主」的状态。 +- **关键词热加载**:`keywords.yaml` 被修改后自动重新解析,支持子串匹配与 `(?i)`、`(?im)` 等内联正则修饰符。 +- **Bot 汇报隔离**:监听使用 user-bot,会话由用户账号维持;真正的告警与附件提醒交由 Bot Token 发出,避免刷屏。 ## 环境要求 -- Python 3.10+ -- Telegram API 凭据(`api_id`/`api_hash`) -- 目标账号可登录的手机号(首登需要验证码) -- Bot Token(用于把监控结果发到 @kt500_bot 已在的群) +- Node.js 18+(建议 LTS) +- Telegram API 凭据:`api_id` / `api_hash` +- 可登录的手机号(首次运行需验证码) +- Telegram Bot Token(Bot 必须已经在汇报群内) ## 快速开始 1. 安装依赖: ```bash - python3 -m venv .venv && source .venv/bin/activate - pip install -r requirements.txt + npm install ``` -2. 准备配置: +2. 配置环境变量: ```bash cp .env.example .env ``` - 按需填写: - - `API_ID` / `API_HASH`:来自 [my.telegram.org](https://my.telegram.org)。 - - `SESSION_NAME`:本地 session 文件名,首次登录会生成 `SESSION_NAME.session`。 - - `USER_PHONE`:用于自动登录(可留空,运行时手输)。 - - `GROUP_LINKS`:逗号分隔的群链接,可直接填 `https://t.me/+tvVm--E19cxkNWJl`。 - - `REPORT_CHAT_LINK`:用于汇报的群/频道链接,如果就地汇报可与 `GROUP_LINKS` 中某一项相同。 - - `TELEGRAM_BOT_TOKEN`:你的 @kt500_bot Token。 - - `KEYWORDS_FILE`:关键词配置文件路径,默认 `keywords.yaml`。 -3. 配置关键词:编辑 `keywords.yaml`,示例: - ```yaml - keywords: - - name: promo - patterns: - - "推广" - - "广告" - regex: false - - name: join_request - patterns: - - "(?i)拉群" - - "(?i)加好友" - regex: true - ``` - - `regex: false` 表示普通子串匹配(自动转换小写)。 - - `regex: true` 将整条 `pattern` 按正则表达式处理,可使用 `(?i)` 等修饰。 - - 文件被修改后,程序会自动检测更新时间并重新加载。 -4. 运行: + 填写下方参数;`SESSION_DIR` 会自动创建并持久化 `*.session` 文件。 +3. 配置 `keywords.yaml`(示例见下节)。 +4. 启动: ```bash - python3 -m src.main + npm run dev # tsx 直接运行 TypeScript + # 或 + npm run build && npm run start ``` - - 首次运行会提示输入验证码 / 二步验证密码。 - - 成功后会看到“群监控已启用”提示,同时在 `REPORT_CHAT_LINK` 对应的群内收到确认消息。 + 首次登陆会提示输入手机号验证码及二步验证密码(若有)。 -## 工作流程 -1. **自动入群**:对每个链接先尝试 `get_entity`,若失败且为邀请链接,则执行 `ImportChatInviteRequest` 加入。加入或已在群内后会开始监听。 -2. **群主识别**:调用 `GetParticipantsRequest(... ChannelParticipantsAdmins ...)` 找到 `ChannelParticipantCreator`,将结果写入汇报中,便于核对“自动添加群主”状态。 -3. **关键词监控**:`events.NewMessage` 监听指定群,命中关键词时将群名、消息 ID、发送人、关键词、时间与截断内容推送到 Bot 所在的群。 -4. **多次触发 & 附件提醒**:文本命中会附带一则消息,若原消息含媒体,还会追加“附件提醒”。 +## 环境变量说明 +- `API_ID` / `API_HASH`:来自 [my.telegram.org](https://my.telegram.org)。 +- `SESSION_NAME` / `SESSION_DIR`:本地 session 文件名及所在目录。 +- `USER_PHONE`:可选,预填手机号。 +- `TWO_FA_PASSWORD`:可选,账号开启二步验证时使用。 +- `GROUP_LINKS`:逗号分隔的群链接列表,可混合公开链接与 `https://t.me/+xxxx`。 +- `REPORT_CHAT_LINK`:汇报消息要发送到的群/频道;留空时默认取 `GROUP_LINKS` 第一项。 +- `TELEGRAM_BOT_TOKEN`:Bot HTTP API Token。 +- `KEYWORDS_FILE`:关键词配置路径,默认 `keywords.yaml`。 +- `LOG_LEVEL`:`trace|debug|info|warn|error`,默认 `info`。 -## 常见扩展 -- **新增关键词**:直接编辑 `keywords.yaml`,保存后生效(无需重启)。 -- **新增群**:在 `.env` 的 `GROUP_LINKS` 中添加链接,重启程序即可。 -- **自定义汇报格式**:在 `src/group_monitor.py` 的 `_handle_new_message` 中调整 `lines` 内容。 -- **落地数据库**:可在 `_handle_new_message` 中追加写库逻辑,然后调用 `reporter.send_safe` 做通知。 +## 关键词配置 +`keywords.yaml` 结构与旧版 Python 一致: + +```yaml +keywords: + - name: promo + patterns: + - "推广" + - "广告" + regex: false + - name: join_request + patterns: + - "(?i)拉群" + - "(?im)^加好友" + regex: true +``` + +- `regex: false`:子串匹配,自动转为小写比较。 +- `regex: true`:使用 JavaScript 正则。保留了 Python 示例中的 `(?i)`、`(?im)` 等内联修饰符,会自动拆解为对应的 `i/m/s` flags。 +- 文件保存后会触发热加载,无需重启进程。 + +## 常用脚本 +- `npm run dev`:tsx 直接运行 `src/main.ts`。 +- `npm run build`:编译到 `dist/`,配合 `npm run start` 运行。 +- `npm run lint`:`tsc --noEmit` 类型检查。 +- `npm run test`:Vitest 单测,当前覆盖 `KeywordStore` 热加载逻辑。 ## 注意事项 -- 请确保 user-bot 与 @kt500_bot 均已在 `REPORT_CHAT_LINK` 对应的群里,并授予发送消息权限。 -- Telegram 对频繁入群/拉人有限制,若日志出现 `FloodWaitError`,需等待对应秒数。 -- Session 文件包含账户授权信息,只应保存在可信设备中。 -- 若长期运行,建议用 `supervisor`/`systemd` 守护,并开启日志轮转。 - -## 后续工作 -- 若需要把“自动添加群主”升级为主动发送好友请求,可结合 `contacts.AddContactRequest`,条件是群主公开手机号。 -- 可根据需要加入异常上报(例如钉钉/企业微信)或统计报表。 +- user-bot 与汇报 Bot 都必须在 `REPORT_CHAT_LINK` 对应的群/频道里,且 Bot 拥有发言权限。 +- Telegram 对频繁入群会触发 `FLOOD_WAIT_xx`,日志会提示需要等待的秒数。 +- `sessions/*.session` 含账号授权信息,请妥善保管。 +- 长期部署建议使用 `pm2 / systemd` 等守护方式并接入日志轮转。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7c04da1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2508 @@ +{ + "name": "qun-monitor", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "qun-monitor", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "dotenv": "^16.4.5", + "telegram": "^2.26.22", + "undici": "^6.19.8", + "yaml": "^2.6.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "vitest": "^2.1.4" + } + }, + "node_modules/@cryptography/aes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@cryptography/aes/-/aes-0.1.1.tgz", + "integrity": "sha512-PcYz4FDGblO6tM2kSC+VzhhK62vml6k6/YAkiWtyPvrgJVfnDRoHGDtKn5UiaRRUrvUTTocBpvc2rRgTCqxjsg==", + "license": "GPL-3.0-or-later" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.1.tgz", + "integrity": "sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.1.tgz", + "integrity": "sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.1.tgz", + "integrity": "sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.1.tgz", + "integrity": "sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.1.tgz", + "integrity": "sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.1.tgz", + "integrity": "sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.1.tgz", + "integrity": "sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.1.tgz", + "integrity": "sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.1.tgz", + "integrity": "sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.1.tgz", + "integrity": "sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.1.tgz", + "integrity": "sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.1.tgz", + "integrity": "sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.1.tgz", + "integrity": "sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.1.tgz", + "integrity": "sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.1.tgz", + "integrity": "sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.1.tgz", + "integrity": "sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.1.tgz", + "integrity": "sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.1.tgz", + "integrity": "sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.1.tgz", + "integrity": "sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.1.tgz", + "integrity": "sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.1.tgz", + "integrity": "sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.1.tgz", + "integrity": "sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz", + "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async-mutex": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", + "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bufferutil": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "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-localstorage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-2.2.1.tgz", + "integrity": "sha512-vv8fJuOUCCvSPjDjBLlMqYMHob4aGjkmrkaE42/mZr0VT+ZAU10jRF8oTnX9+pgU9/vYJ8P7YT3Vd6ajkmzSCw==", + "license": "MIT", + "dependencies": { + "write-file-atomic": "^1.1.4" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/real-cancellable-promise": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/real-cancellable-promise/-/real-cancellable-promise-1.2.3.tgz", + "integrity": "sha512-hBI5Gy/55VEeeMtImMgEirD7eq5UmqJf1J8dFZtbJZA/3rB0pYFZ7PayMGueb6v4UtUtpKpP+05L0VwyE1hI9Q==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.53.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.1.tgz", + "integrity": "sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.1", + "@rollup/rollup-android-arm64": "4.53.1", + "@rollup/rollup-darwin-arm64": "4.53.1", + "@rollup/rollup-darwin-x64": "4.53.1", + "@rollup/rollup-freebsd-arm64": "4.53.1", + "@rollup/rollup-freebsd-x64": "4.53.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.1", + "@rollup/rollup-linux-arm-musleabihf": "4.53.1", + "@rollup/rollup-linux-arm64-gnu": "4.53.1", + "@rollup/rollup-linux-arm64-musl": "4.53.1", + "@rollup/rollup-linux-loong64-gnu": "4.53.1", + "@rollup/rollup-linux-ppc64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-gnu": "4.53.1", + "@rollup/rollup-linux-riscv64-musl": "4.53.1", + "@rollup/rollup-linux-s390x-gnu": "4.53.1", + "@rollup/rollup-linux-x64-gnu": "4.53.1", + "@rollup/rollup-linux-x64-musl": "4.53.1", + "@rollup/rollup-openharmony-arm64": "4.53.1", + "@rollup/rollup-win32-arm64-msvc": "4.53.1", + "@rollup/rollup-win32-ia32-msvc": "4.53.1", + "@rollup/rollup-win32-x64-gnu": "4.53.1", + "@rollup/rollup-win32-x64-msvc": "4.53.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/store2": { + "version": "2.14.4", + "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", + "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "license": "MIT" + }, + "node_modules/telegram": { + "version": "2.26.22", + "resolved": "https://registry.npmjs.org/telegram/-/telegram-2.26.22.tgz", + "integrity": "sha512-EIj7Yrjiu0Yosa3FZ/7EyPg9s6UiTi/zDQrFmR/2Mg7pIUU+XjAit1n1u9OU9h2oRnRM5M+67/fxzQluZpaJJg==", + "license": "MIT", + "dependencies": { + "@cryptography/aes": "^0.1.1", + "async-mutex": "^0.3.0", + "big-integer": "^1.6.48", + "buffer": "^6.0.3", + "htmlparser2": "^6.1.0", + "mime": "^3.0.0", + "node-localstorage": "^2.2.1", + "pako": "^2.0.3", + "path-browserify": "^1.0.1", + "real-cancellable-promise": "^1.1.1", + "socks": "^2.6.2", + "store2": "^2.13.0", + "ts-custom-error": "^3.2.0", + "websocket": "^1.0.34" + }, + "optionalDependencies": { + "bufferutil": "^4.0.3", + "utf-8-validate": "^5.0.5" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", + "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/websocket": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", + "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", + "license": "Apache-2.0", + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.63", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/websocket/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/websocket/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/write-file-atomic": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", + "integrity": "sha512-SdrHoC/yVBPpV0Xq/mUZQIpW2sWXAShb/V4pomcJXh92RuaO+f3UTWItiR3Px+pLnV2PvC2/bfn5cwr5X6Vfxw==", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "slide": "^1.1.5" + } + }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "engines": { + "node": ">=0.10.32" + } + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d3d8a67 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "qun-monitor", + "version": "2.0.0", + "type": "module", + "license": "MIT", + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node dist/main.js", + "dev": "tsx src/main.ts", + "lint": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "dotenv": "^16.4.5", + "telegram": "^2.26.22", + "undici": "^6.19.8", + "yaml": "^2.6.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "tsx": "^4.19.1", + "typescript": "^5.6.3", + "vitest": "^2.1.4" + } +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 36ff572..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -telethon==1.35.0 -python-dotenv==1.0.1 -PyYAML==6.0.2 -httpx==0.27.2 diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 2618a26..0000000 --- a/src/config.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path -from typing import List, Optional -import os - -from dotenv import load_dotenv - - -@dataclass -class CredentialsConfig: - api_id: int - api_hash: str - session_name: str - phone: Optional[str] - - -@dataclass -class ReportingConfig: - bot_token: str - chat_link: Optional[str] - - -@dataclass -class MonitorConfig: - group_links: List[str] - keywords_file: Path - - -@dataclass -class AppConfig: - credentials: CredentialsConfig - reporting: ReportingConfig - monitor: MonitorConfig - - -def _comma_split(value: str) -> List[str]: - return [chunk.strip() for chunk in value.split(",") if chunk.strip()] - - -def load_config(env_file: str = ".env") -> AppConfig: - if Path(env_file).exists(): - load_dotenv(env_file) - - try: - api_id = int(os.environ["API_ID"]) - api_hash = os.environ["API_HASH"] - except KeyError as exc: - raise RuntimeError("API_ID 和 API_HASH 必填。") from exc - - session_name = os.environ.get("SESSION_NAME", "userbot") - phone = os.environ.get("USER_PHONE") - - bot_token = os.environ.get("TELEGRAM_BOT_TOKEN") - if not bot_token: - raise RuntimeError("TELEGRAM_BOT_TOKEN 缺失,以便推送汇报。") - - group_links_env = os.environ.get("GROUP_LINKS", "").strip() - if not group_links_env: - raise RuntimeError("GROUP_LINKS 至少包含一个要监听的群链接。") - group_links = _comma_split(group_links_env) - - keywords_file = Path(os.environ.get("KEYWORDS_FILE", "keywords.yaml")).expanduser() - - report_link = os.environ.get("REPORT_CHAT_LINK") - - credentials = CredentialsConfig( - api_id=api_id, - api_hash=api_hash, - session_name=session_name, - phone=phone, - ) - reporting = ReportingConfig(bot_token=bot_token, chat_link=report_link) - monitor = MonitorConfig( - group_links=group_links, - keywords_file=keywords_file, - ) - return AppConfig(credentials=credentials, reporting=reporting, monitor=monitor) diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..8daaddd --- /dev/null +++ b/src/config.ts @@ -0,0 +1,101 @@ +import { config as loadEnv } from "dotenv"; +import { existsSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { z } from "zod"; + +loadEnv(); + +const envSchema = z + .object({ + API_ID: z.string(), + API_HASH: z.string().min(32), + SESSION_NAME: z.string().default("userbot"), + SESSION_DIR: z.string().default("./sessions"), + USER_PHONE: z.string().optional(), + TWO_FA_PASSWORD: z.string().optional(), + GROUP_LINKS: z.string(), + REPORT_CHAT_LINK: z.string().optional(), + TELEGRAM_BOT_TOKEN: z.string(), + KEYWORDS_FILE: z.string().default("keywords.yaml"), + CONNECTION_RETRIES: z.string().optional(), + }) + .strict(); + +export type TelegramCredentials = { + apiId: number; + apiHash: string; + sessionName: string; + sessionFile: string; + phone?: string; + twoFactorPassword?: string; + connectionRetries: number; +}; + +export type MonitorConfig = { + groupLinks: string[]; + keywordsFile: string; +}; + +export type ReporterConfig = { + botToken: string; + chatLink?: string; +}; + +export type AppConfig = { + telegram: TelegramCredentials; + monitor: MonitorConfig; + reporter: ReporterConfig; +}; + +function ensureDirectory(pathname: string) { + if (!existsSync(pathname)) { + mkdirSync(pathname, { recursive: true }); + } +} + +export function loadConfig(): AppConfig { + const result = envSchema.safeParse(process.env); + if (!result.success) { + throw new Error(`环境变量解析失败: ${result.error.message}`); + } + + const data = result.data; + const groupLinks = data.GROUP_LINKS.split(",").map((chunk) => chunk.trim()).filter(Boolean); + if (!groupLinks.length) { + throw new Error("GROUP_LINKS 至少包含一个群链接。"); + } + + const apiId = Number(data.API_ID); + if (Number.isNaN(apiId)) { + throw new Error("API_ID 必须是数字。"); + } + + const sessionDir = resolve(process.cwd(), data.SESSION_DIR); + ensureDirectory(sessionDir); + const sessionFile = resolve(sessionDir, `${data.SESSION_NAME}.session`); + ensureDirectory(dirname(sessionFile)); + + const keywordsFile = resolve(process.cwd(), data.KEYWORDS_FILE); + + const connectionRetries = data.CONNECTION_RETRIES ? Number(data.CONNECTION_RETRIES) : 5; + + return { + telegram: { + apiId, + apiHash: data.API_HASH, + sessionName: data.SESSION_NAME, + sessionFile, + phone: data.USER_PHONE, + twoFactorPassword: data.TWO_FA_PASSWORD, + connectionRetries: Number.isFinite(connectionRetries) ? connectionRetries : 5, + }, + monitor: { + groupLinks, + keywordsFile, + }, + reporter: { + botToken: data.TELEGRAM_BOT_TOKEN, + chatLink: data.REPORT_CHAT_LINK || groupLinks[0], + }, + }; +} diff --git a/src/groupMonitor.ts b/src/groupMonitor.ts new file mode 100644 index 0000000..5c71051 --- /dev/null +++ b/src/groupMonitor.ts @@ -0,0 +1,283 @@ +import type { TelegramClient } from "telegram"; +import { Api } from "telegram"; +import { NewMessage } from "telegram/events"; +import type { NewMessageEvent } from "telegram/events"; +import bigInt from "big-integer"; + +import { KeywordStore } from "./keywords.js"; +import { logger } from "./logger.js"; +import { Reporter } from "./reporter.js"; +import { escapeMarkdown, truncate } from "./utils/text.js"; + +type ChannelEntity = Api.Channel; +type RpcErrorLike = { + errorMessage?: string; +}; + +function extractInviteHash(link: string): string | undefined { + if (!link) { + return undefined; + } + const normalized = link.trim(); + if (normalized.includes("t.me/+")) { + const [, hash] = normalized.split("t.me/+", 2); + return hash?.replace(/\/+$/, ""); + } + if (normalized.includes("t.me/joinchat/")) { + const [, hash] = normalized.split("t.me/joinchat/", 2); + return hash?.replace(/\/+$/, ""); + } + if (normalized.startsWith("+")) { + return normalized.slice(1); + } + return undefined; +} + +function getDisplayName(entity?: Api.TypeUser | Api.TypeChat | null): string { + if (!entity) { + return ""; + } + if (entity instanceof Api.User) { + return [entity.firstName, entity.lastName].filter(Boolean).join(" ") || entity.username || String(entity.id); + } + if ("title" in entity && entity.title) { + return entity.title; + } + if ("username" in entity && entity.username) { + return entity.username; + } + return String("id" in entity ? entity.id : "unknown"); +} + +function getRpcMessage(error: unknown): string | undefined { + if (error && typeof (error as RpcErrorLike).errorMessage === "string") { + return (error as RpcErrorLike).errorMessage; + } + if (error instanceof Error) { + return error.message; + } + return undefined; +} + +function rpcErrorIncludes(error: unknown, code: string): boolean { + const message = getRpcMessage(error); + return Boolean(message && message.includes(code)); +} + +function floodWaitSeconds(error: unknown): number | undefined { + const message = getRpcMessage(error); + if (!message) { + return undefined; + } + const match = message.match(/FLOOD_(?:WAIT|TEST_PHONE_WAIT)_(\d+)/); + return match ? Number(match[1]) : undefined; +} + +function hasChats(update: Api.TypeUpdates | Api.TypeUpdate): update is Api.Updates | Api.UpdatesCombined { + return "chats" in update; +} + +export class GroupMonitor { + private readonly entities: ChannelEntity[] = []; + private readonly peerIds = new Set(); + + constructor( + private readonly client: TelegramClient, + private readonly keywordStore: KeywordStore, + private readonly reporter: Reporter, + private readonly groupLinks: string[], + ) {} + + async start() { + await this.ensureMemberships(); + if (!this.entities.length) { + throw new Error("没有可监听的群,停止启动。"); + } + await this.reporter.prepare(this.client); + this.client.addEventHandler((event) => this.handleNewMessage(event), new NewMessage({ chats: this.entities })); + logger.info("事件监听已注册,等待消息。"); + } + + private async ensureMemberships() { + for (const link of this.groupLinks) { + const { entity, joinedViaInvite } = await this.resolveEntity(link); + if (!entity) { + logger.error(`无法解析群链接 ${link}`); + continue; + } + + if (!joinedViaInvite) { + try { + await this.client.invoke(new Api.channels.JoinChannel({ channel: entity })); + logger.info(`加入群 ${entity.title} 成功`); + } catch (error) { + if (rpcErrorIncludes(error, "USER_ALREADY_PARTICIPANT")) { + logger.info(`已在群 ${entity.title} 中`); + } else if (rpcErrorIncludes(error, "FLOOD_WAIT")) { + const wait = floodWaitSeconds(error); + logger.error(`加入群 ${link} 触发 FloodWait,需要等待 ${wait ?? "未知"} 秒`); + continue; + } else if (rpcErrorIncludes(error, "INVITE_HASH_EXPIRED") || rpcErrorIncludes(error, "INVITE_HASH_INVALID")) { + logger.error(`加入群 ${link} 失败: ${getRpcMessage(error)}`); + continue; + } else if (rpcErrorIncludes(error, "CHANNELS_TOO_MUCH")) { + logger.error(`帐号加入的群过多,无法加入 ${link}`); + continue; + } else { + logger.error(`加入群 ${link} 失败: ${(error as Error).message}`); + continue; + } + } + } + + const peerId = await this.client.getPeerId(entity); + if (this.peerIds.has(peerId)) { + continue; + } + this.peerIds.add(peerId); + this.entities.push(entity); + const owner = await this.discoverOwner(entity); + const ownerLabel = escapeMarkdown(owner ?? "未找到"); + const escapedPeerId = escapeMarkdown(peerId); + await this.reporter.prepare(this.client); + await this.reporter.sendSafe( + [ + "✅ *群监控已启用*", + `群: \`${escapeMarkdown(entity.title ?? peerId)}\``, + `ID: \`${escapedPeerId}\``, + `群主: \`${ownerLabel}\``, + ].join("\n"), + ); + } + } + + private async discoverOwner(entity: ChannelEntity): Promise { + try { + const result = await this.client.invoke( + new Api.channels.GetParticipants({ + channel: entity, + filter: new Api.ChannelParticipantsAdmins(), + offset: 0, + limit: 200, + hash: bigInt.zero, + }), + ); + if (!(result instanceof Api.channels.ChannelParticipants)) { + logger.warn(`获取群主返回类型 ${result.className}`); + return undefined; + } + for (const participant of result.participants) { + if (participant instanceof Api.ChannelParticipantCreator) { + const user = result.users.find((candidate) => candidate.id === participant.userId); + if (user) { + return getDisplayName(user); + } + } + } + } catch (error) { + if (rpcErrorIncludes(error, "CHAT_ADMIN_REQUIRED")) { + logger.warn(`没有权限获取 ${entity.title ?? ""} 管理员信息`); + } else { + logger.warn(`获取群主失败 ${entity.title}: ${(error as Error).message}`); + } + } + return undefined; + } + + private async handleNewMessage(event: NewMessageEvent) { + try { + const message = event.message; + const text = message?.message ?? ""; + const hits = this.keywordStore.match(text); + if (!hits.length) { + return; + } + + const chat = await message.getChat(); + const sender = await message.getSender(); + const chatLabel = escapeMarkdown(getDisplayName(chat) || message.chatId?.toString() || "unknown"); + const senderName = escapeMarkdown(getDisplayName(sender)); + const senderIdRaw = sender && "id" in sender ? String((sender as { id?: unknown }).id ?? "unknown") : message.senderId?.toString() ?? "unknown"; + const senderId = escapeMarkdown(senderIdRaw); + const keywords = Array.from(new Set(hits.map((hit) => hit.keyword))).join(", "); + + const lines: string[] = [ + "⚠️ *关键词触发*", + `群: \`${chatLabel}\``, + `消息ID: \`${message.id}\``, + `用户: \`${senderName}\` (\`${senderId}\`)`, + `关键词: \`${escapeMarkdown(keywords)}\``, + `时间: \`${new Date().toISOString()}\``, + ]; + + if (chat instanceof Api.Channel && chat.username) { + lines.push(`链接: https://t.me/${chat.username}/${message.id}`); + } + + const preview = escapeMarkdown(truncate(text.trim())); + if (preview) { + lines.push("----"); + lines.push(preview); + } + + await this.reporter.sendSafe(lines.join("\n")); + + if (message.media) { + await this.reporter.sendSafe( + [ + "📎 *附件提醒*", + `群: \`${chatLabel}\``, + `消息ID: \`${message.id}\` 含有非文本内容`, + ].join("\n"), + ); + } + } catch (error) { + logger.error(`处理消息失败: ${(error as Error).message}`); + } + } + + private async resolveEntity(link: string): Promise<{ entity?: ChannelEntity; joinedViaInvite: boolean }> { + try { + const entity = await this.client.getEntity(link); + if (entity instanceof Api.Channel) { + return { entity, joinedViaInvite: false }; + } + } catch (error) { + logger.debug(`直接解析 ${link} 失败: ${(error as Error).message}`); + } + + const inviteHash = extractInviteHash(link); + if (!inviteHash) { + return { joinedViaInvite: false }; + } + + try { + const result = await this.client.invoke(new Api.messages.ImportChatInvite({ hash: inviteHash })); + if (hasChats(result)) { + for (const chat of result.chats) { + if (chat instanceof Api.Channel) { + logger.info(`通过邀请链接加入 ${chat.title}`); + return { entity: chat, joinedViaInvite: true }; + } + } + } + } catch (error) { + if (rpcErrorIncludes(error, "USER_ALREADY_PARTICIPANT")) { + logger.info(`邀请链接 ${link} 显示已在群内,重试 getEntity。`); + try { + const entity = await this.client.getEntity(link); + if (entity instanceof Api.Channel) { + return { entity, joinedViaInvite: false }; + } + } catch (inner) { + logger.error(`邀请链接 ${link} 解析实体失败: ${(inner as Error).message}`); + } + } else if (rpcErrorIncludes(error, "INVITE_HASH_EXPIRED") || rpcErrorIncludes(error, "INVITE_HASH_INVALID")) { + logger.error(`邀请链接 ${link} 不可用: ${getRpcMessage(error)}`); + } else { + logger.error(`通过邀请链接加入 ${link} 失败: ${(error as Error).message}`); + } + } + return { joinedViaInvite: false }; + } +} diff --git a/src/group_monitor.py b/src/group_monitor.py deleted file mode 100644 index 493f7a4..0000000 --- a/src/group_monitor.py +++ /dev/null @@ -1,204 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -import logging -from typing import List, Optional - -from telethon import events -from telethon.errors import ( - ChannelsTooMuchError, - ChatAdminRequiredError, - FloodWaitError, - InviteHashExpiredError, - InviteHashInvalidError, - UserAlreadyParticipantError, -) -from telethon.tl.functions.channels import GetParticipantsRequest, JoinChannelRequest -from telethon.tl.functions.messages import ImportChatInviteRequest -from telethon.tl.types import Channel, ChannelParticipantCreator, ChannelParticipantsAdmins -from telethon.utils import get_display_name, get_peer_id - -from .keywords import KeywordStore -from .reporter import Reporter - -logger = logging.getLogger(__name__) - - -def _escape_md(value: Optional[str]) -> str: - if value is None: - return "" - return value.replace("`", r"\`") - - -def _extract_invite_hash(link: str) -> Optional[str]: - if not link: - return None - if "t.me/+" in link: - return link.split("t.me/+", 1)[1] - if "t.me/joinchat/" in link: - return link.rstrip("/").split("t.me/joinchat/", 1)[1] - if link.startswith("+"): - return link[1:] - return None - - -class GroupMonitor: - def __init__( - self, - client, - keyword_store: KeywordStore, - reporter: Reporter, - group_links: List[str], - ) -> None: - self.client = client - self.keyword_store = keyword_store - self.reporter = reporter - self.group_links = group_links - self.entities: List[Channel] = [] - - async def start(self) -> None: - await self._ensure_memberships() - if not self.entities: - raise RuntimeError("没有可监听的群,停止启动。") - await self.reporter.prepare(self.client) - self.client.add_event_handler(self._handle_new_message, events.NewMessage(chats=[get_peer_id(e) for e in self.entities])) - logger.info("事件监听已注册,等待消息。") - - async def _ensure_memberships(self) -> None: - for link in self.group_links: - entity, joined_via_invite = await self._resolve_entity(link) - if not entity: - logger.error("无法解析群链接 %s", link) - continue - - if not isinstance(entity, Channel): - logger.warning("%s 不是超级群/频道,当前实现仅支持 Channel。", link) - continue - - if not joined_via_invite: - try: - await self.client(JoinChannelRequest(entity)) - logger.info("加入群 %s 成功。", entity.title) - except UserAlreadyParticipantError: - logger.info("已在群 %s 中。", entity.title) - except (ChannelsTooMuchError, FloodWaitError, InviteHashExpiredError, InviteHashInvalidError) as exc: - logger.error("加入群 %s 失败: %s", link, exc) - continue - - peer_id = get_peer_id(entity) - if any(get_peer_id(e) == peer_id for e in self.entities): - continue - self.entities.append(entity) - owner = await self._discover_owner(entity) - owner_label = _escape_md(owner or "未找到") - await self.reporter.send_safe( - ( - "✅ *群监控已启用*\n" - f"群: `{_escape_md(entity.title)}`\n" - f"ID: `{get_peer_id(entity)}`\n" - f"群主: `{owner_label}`" - ) - ) - - async def _discover_owner(self, entity: Channel) -> Optional[str]: - try: - result = await self.client( - GetParticipantsRequest( - channel=entity, - filter=ChannelParticipantsAdmins(), - offset=0, - limit=200, - hash=0, - ) - ) - except ChatAdminRequiredError: - logger.warning("没有足够权限获取 %s 管理员信息。", entity.title) - return None - except Exception as exc: - logger.warning("获取群主失败 %s: %s", entity.title, exc) - return None - - creator: Optional[str] = None - for participant in result.participants: - if isinstance(participant, ChannelParticipantCreator): - owner_user = next((u for u in result.users if u.id == participant.user_id), None) - if owner_user: - creator = get_display_name(owner_user) or str(owner_user.id) - break - return creator - - async def _handle_new_message(self, event: events.NewMessage.Event) -> None: - try: - chat = await event.get_chat() - sender = await event.get_sender() - except Exception as exc: - logger.warning("无法获取消息上下文: %s", exc) - return - - text = event.raw_text or "" - hits = self.keyword_store.match(text) - if not hits: - return - - keyword_summary = ", ".join(dict.fromkeys(hit.keyword for hit in hits)) - sender_name = _escape_md(get_display_name(sender)) - chat_name = _escape_md(getattr(chat, "title", str(event.chat_id))) - message_link = None - if getattr(chat, "username", None): - message_link = f"https://t.me/{chat.username}/{event.id}" - - lines = [ - "⚠️ *关键词触发*", - f"群: `{chat_name}`", - f"消息ID: `{event.id}`", - f"用户: `{sender_name}` (`{sender.id}`)", - f"关键词: `{_escape_md(keyword_summary)}`", - f"时间: `{datetime.utcnow().isoformat()}Z`", - ] - if message_link: - lines.append(f"链接: {message_link}") - preview = _escape_md(text.strip()) - if preview: - if len(preview) > 500: - preview = preview[:500] + "..." - lines.append("----") - lines.append(preview) - - await self.reporter.send_safe("\n".join(lines)) - - if event.message.media: - await self.reporter.send_safe( - ( - "📎 *附件提醒*\n" - f"群: `{chat_name}`\n" - f"消息ID: `{event.id}` 含有非文本内容" - ) - ) - - async def _resolve_entity(self, link: str) -> tuple[Optional[Channel], bool]: - try: - entity = await self.client.get_entity(link) - if isinstance(entity, Channel): - return entity, False - except (ValueError, TypeError): - logger.debug("直接解析 %s 失败,尝试邀请链接流程。", link) - - invite_hash = _extract_invite_hash(link) - if invite_hash: - try: - result = await self.client(ImportChatInviteRequest(invite_hash)) - for chat in result.chats: - if isinstance(chat, Channel): - logger.info("通过邀请链接加入 %s", chat.title) - return chat, True - except UserAlreadyParticipantError: - logger.info("邀请链接 %s 提示已在群内,尝试再次解析实体。", link) - try: - entity = await self.client.get_entity(link) - if isinstance(entity, Channel): - return entity, False - except Exception as exc: - logger.error("邀请链接 %s 解析实体失败: %s", link, exc) - except (InviteHashExpiredError, InviteHashInvalidError) as exc: - logger.error("邀请链接 %s 不可用: %s", link, exc) - return None, False diff --git a/src/keywords.py b/src/keywords.py deleted file mode 100644 index dd8e21e..0000000 --- a/src/keywords.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field -from pathlib import Path -from typing import List -import logging -import re -import threading - -import yaml - -logger = logging.getLogger(__name__) - - -@dataclass -class KeywordEntry: - name: str - patterns: List[str] - regex: bool = False - compiled: List[re.Pattern] = field(default_factory=list) - - -@dataclass -class KeywordHit: - keyword: str - pattern: str - - -class KeywordStore: - """加载并匹配关键词,支持热加载。""" - - def __init__(self, file_path: Path): - self.file_path = file_path - self._entries: List[KeywordEntry] = [] - self._last_mtime: float | None = None - self._lock = threading.Lock() - self._reload(force=True) - - def _reload(self, force: bool = False) -> None: - if not self.file_path.exists(): - if force: - logger.warning("关键词文件 %s 不存在,暂无匹配项。", self.file_path) - self._entries = [] - self._last_mtime = None - return - - mtime = self.file_path.stat().st_mtime - if not force and self._last_mtime == mtime: - return - - raw = yaml.safe_load(self.file_path.read_text(encoding="utf-8")) or {} - entries: List[KeywordEntry] = [] - for item in raw.get("keywords", []): - name = str(item.get("name", "unnamed")) - patterns = [str(p) for p in item.get("patterns", []) if p] - if not patterns: - continue - regex = bool(item.get("regex", False)) - if regex: - compiled = [] - for pattern in patterns: - try: - compiled.append(re.compile(pattern)) - except re.error as exc: - logger.warning("忽略非法正则 %s: %s", pattern, exc) - entry = KeywordEntry(name=name, patterns=patterns, regex=True, compiled=compiled) - else: - entry = KeywordEntry(name=name, patterns=[p.lower() for p in patterns], regex=False) - entries.append(entry) - - self._entries = entries - self._last_mtime = mtime - logger.info("加载 %d 个关键词分组。", len(entries)) - - def _ensure_fresh(self) -> None: - with self._lock: - self._reload() - - def match(self, text: str | None) -> List[KeywordHit]: - if not text: - return [] - self._ensure_fresh() - payload = text.strip() - if not payload: - return [] - - hits: List[KeywordHit] = [] - lowered = payload.lower() - for entry in self._entries: - if entry.regex: - for regex in entry.compiled: - if regex.search(payload): - hits.append(KeywordHit(keyword=entry.name, pattern=regex.pattern)) - else: - for pattern in entry.patterns: - if pattern in lowered: - hits.append(KeywordHit(keyword=entry.name, pattern=pattern)) - return hits diff --git a/src/keywords.ts b/src/keywords.ts new file mode 100644 index 0000000..62e6fdc --- /dev/null +++ b/src/keywords.ts @@ -0,0 +1,129 @@ +import { existsSync, readFileSync, statSync } from "node:fs"; +import { parse } from "yaml"; + +import { logger } from "./logger.js"; + +const INLINE_FLAG_PATTERN = /^\(\?([ims]+)\)/i; + +type KeywordDefinition = { + name: string; + patterns: string[]; + regex: boolean; + compiled: RegExp[]; +}; + +export type KeywordHit = { + keyword: string; + pattern: string; +}; + +/** + * 关键词存储与匹配逻辑,对应 Python 版本的 KeywordStore。 + */ +export class KeywordStore { + private entries: KeywordDefinition[] = []; + private lastMtime?: number; + + constructor(private readonly filePath: string) { + this.reload(true); + } + + private reload(force = false) { + if (!existsSync(this.filePath)) { + if (force) { + logger.warn(`关键词文件 ${this.filePath} 不存在`); + this.entries = []; + this.lastMtime = undefined; + } + return; + } + + const mtime = statSync(this.filePath).mtimeMs; + if (!force && this.lastMtime === mtime) { + return; + } + + const content = readFileSync(this.filePath, "utf-8"); + const raw = (parse(content) ?? {}) as { keywords?: Array> }; + + const parsed: KeywordDefinition[] = []; + for (const entry of raw.keywords ?? []) { + const name = String(entry.name ?? "unnamed"); + const patterns = Array.isArray(entry.patterns) ? entry.patterns.map((p) => String(p)).filter(Boolean) : []; + if (!patterns.length) { + continue; + } + const regex = Boolean(entry.regex); + if (regex) { + const compiled: RegExp[] = []; + for (const pattern of patterns) { + const created = buildRegExp(pattern); + if (created) { + compiled.push(created); + } + } + parsed.push({ name, patterns, regex: true, compiled }); + } else { + parsed.push({ name, patterns: patterns.map((p) => p.toLowerCase()), regex: false, compiled: [] }); + } + } + + this.entries = parsed; + this.lastMtime = mtime; + logger.info(`加载 ${parsed.length} 个关键词分组`); + } + + private ensureFresh() { + try { + this.reload(); + } catch (error) { + logger.error(`刷新关键词失败: ${(error as Error).message}`); + } + } + + match(text?: string | null): KeywordHit[] { + if (!text) { + return []; + } + this.ensureFresh(); + const payload = text.trim(); + if (!payload) { + return []; + } + const lower = payload.toLowerCase(); + const hits: KeywordHit[] = []; + for (const entry of this.entries) { + if (entry.regex) { + for (const regex of entry.compiled) { + if (regex.test(payload)) { + hits.push({ keyword: entry.name, pattern: regex.source }); + } + } + } else { + for (const pattern of entry.patterns) { + if (lower.includes(pattern)) { + hits.push({ keyword: entry.name, pattern }); + } + } + } + } + return hits; + } +} + +function buildRegExp(pattern: string): RegExp | undefined { + let source = pattern; + let flags = ""; + const match = pattern.match(INLINE_FLAG_PATTERN); + if (match) { + const inlineFlags = Array.from(new Set(match[1].toLowerCase().split(""))); + flags = inlineFlags.filter((flag) => ["i", "m", "s"].includes(flag)).join(""); + source = pattern.slice(match[0].length); + } + try { + return new RegExp(source, flags); + } catch (error) { + logger.warn(`忽略非法正则 ${pattern}: ${(error as Error).message}`); + return undefined; + } +} diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 0000000..6334413 --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,35 @@ +const LEVELS = { + trace: 10, + debug: 20, + info: 30, + warn: 40, + error: 50, +} as const; + +type LevelName = keyof typeof LEVELS; + +const envLevel = (process.env.LOG_LEVEL ?? "info").toLowerCase(); +const threshold = LEVELS[(envLevel as LevelName) || "info"] ?? LEVELS.info; + +function emit(level: LevelName, message: string, meta?: unknown) { + if (LEVELS[level] < threshold) { + return; + } + const payload = meta ? `${message} ${JSON.stringify(meta)}` : message; + const ts = new Date().toISOString(); + if (level === "error") { + console.error(`${ts} [${level.toUpperCase()}] ${payload}`); + } else if (level === "warn") { + console.warn(`${ts} [${level.toUpperCase()}] ${payload}`); + } else { + console.log(`${ts} [${level.toUpperCase()}] ${payload}`); + } +} + +export const logger = { + trace: (message: string, meta?: unknown) => emit("trace", message, meta), + debug: (message: string, meta?: unknown) => emit("debug", message, meta), + info: (message: string, meta?: unknown) => emit("info", message, meta), + warn: (message: string, meta?: unknown) => emit("warn", message, meta), + error: (message: string, meta?: unknown) => emit("error", message, meta), +}; diff --git a/src/main.py b/src/main.py deleted file mode 100644 index 769b01f..0000000 --- a/src/main.py +++ /dev/null @@ -1,76 +0,0 @@ -from __future__ import annotations - -import asyncio -import logging -import signal -from typing import Optional - -from telethon import TelegramClient - -from .config import load_config -from .group_monitor import GroupMonitor -from .keywords import KeywordStore -from .reporter import Reporter - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", -) -logger = logging.getLogger(__name__) - - -def _resolve_phone(preconfigured: Optional[str]) -> str: - if preconfigured: - return preconfigured - phone = input("请输入 Telegram 手机号(含国家码,如 +8613712345678):").strip() - if not phone: - raise RuntimeError("未提供手机号,无法完成首次登录。") - return phone - - -async def main() -> None: - config = load_config() - - client = TelegramClient( - session=config.credentials.session_name, - api_id=config.credentials.api_id, - api_hash=config.credentials.api_hash, - ) - - await client.start(phone=_resolve_phone(config.credentials.phone)) - logger.info("User-Bot 登录完成。") - - keyword_store = KeywordStore(config.monitor.keywords_file) - reporter = Reporter( - bot_token=config.reporting.bot_token, - chat_link=config.reporting.chat_link or config.monitor.group_links[0], - ) - - monitor = GroupMonitor( - client=client, - keyword_store=keyword_store, - reporter=reporter, - group_links=config.monitor.group_links, - ) - - await monitor.start() - logger.info("监控已启动,等待消息...") - - stop_event = asyncio.Event() - - def _handle_sig(*_args): - stop_event.set() - - for sig in (signal.SIGINT, signal.SIGTERM): - try: - asyncio.get_running_loop().add_signal_handler(sig, _handle_sig) - except NotImplementedError: - pass - - await stop_event.wait() - logger.info("收到退出信号,断开连接。") - await client.disconnect() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..0444ca8 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,36 @@ +import { loadConfig } from "./config.js"; +import { GroupMonitor } from "./groupMonitor.js"; +import { KeywordStore } from "./keywords.js"; +import { logger } from "./logger.js"; +import { Reporter } from "./reporter.js"; +import { createTelegramClient } from "./telegramClient.js"; + +async function waitForExit() { + await new Promise((resolve) => { + const handler = () => { + process.off("SIGINT", handler); + process.off("SIGTERM", handler); + resolve(); + }; + process.on("SIGINT", handler); + process.on("SIGTERM", handler); + }); +} + +async function main() { + const config = loadConfig(); + const client = await createTelegramClient(config.telegram); + const keywordStore = new KeywordStore(config.monitor.keywordsFile); + const reporter = new Reporter(config.reporter); + const monitor = new GroupMonitor(client, keywordStore, reporter, config.monitor.groupLinks); + await monitor.start(); + logger.info("监控已启动,等待消息..."); + await waitForExit(); + logger.info("收到退出信号,断开连接。"); + await client.disconnect(); +} + +main().catch((error) => { + logger.error(`程序异常退出: ${(error as Error).stack ?? (error as Error).message}`); + process.exit(1); +}); diff --git a/src/reporter.py b/src/reporter.py deleted file mode 100644 index 5c04867..0000000 --- a/src/reporter.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from typing import Optional -import logging - -import httpx -from telethon import utils as tg_utils - -logger = logging.getLogger(__name__) - - -@dataclass -class Reporter: - bot_token: str - chat_link: Optional[str] - chat_id: Optional[int] = None - timeout: float = 10.0 - - async def prepare(self, client) -> None: - if self.chat_id: - return - if not self.chat_link: - raise RuntimeError("REPORT_CHAT_LINK 未设置,无法汇报。") - entity = await client.get_entity(self.chat_link) - self.chat_id = tg_utils.get_peer_id(entity) - logger.info("汇报目标 chat_id=%s 来自 %s", self.chat_id, self.chat_link) - - async def send(self, text: str, parse_mode: str = "Markdown") -> None: - if not self.chat_id: - raise RuntimeError("Reporter 还未完成 prepare,无法发送。") - url = f"https://api.telegram.org/bot{self.bot_token}/sendMessage" - payload = { - "chat_id": self.chat_id, - "text": text, - "parse_mode": parse_mode, - "disable_web_page_preview": True, - } - try: - async with httpx.AsyncClient(timeout=self.timeout) as client: - response = await client.post(url, json=payload) - response.raise_for_status() - data = response.json() - if not data.get("ok", False): - raise RuntimeError(f"Bot API 返回失败: {data}") - except Exception as exc: - logger.error("发送汇报失败: %s", exc) - raise - - async def send_safe(self, text: str, parse_mode: str = "Markdown") -> None: - try: - await self.send(text, parse_mode=parse_mode) - except Exception: - logger.exception("忽略汇报发送异常") diff --git a/src/reporter.ts b/src/reporter.ts new file mode 100644 index 0000000..bfc8c3a --- /dev/null +++ b/src/reporter.ts @@ -0,0 +1,63 @@ +import type { TelegramClient } from "telegram"; +import { request } from "undici"; + +import type { ReporterConfig } from "./config.js"; +import { logger } from "./logger.js"; + +const DEFAULT_TIMEOUT = 10_000; + +/** + * 使用 Telegram Bot API 推送事件,与原实现保持一致。 + */ +export class Reporter { + private chatId?: string; + + constructor(private readonly config: ReporterConfig, private readonly timeout = DEFAULT_TIMEOUT) {} + + async prepare(client: TelegramClient) { + if (this.chatId) { + return; + } + if (!this.config.chatLink) { + throw new Error("REPORT_CHAT_LINK 未设置,无法推送汇报。"); + } + const entity = await client.getEntity(this.config.chatLink); + this.chatId = await client.getPeerId(entity); + logger.info(`汇报 chat_id=${this.chatId} link=${this.config.chatLink}`); + } + + async send(text: string, parseMode: "MarkdownV2" | "Markdown" = "MarkdownV2") { + if (!this.chatId) { + throw new Error("Reporter 还未完成 prepare。"); + } + + const url = `https://api.telegram.org/bot${this.config.botToken}/sendMessage`; + const payload = { + chat_id: this.chatId, + text, + parse_mode: parseMode, + disable_web_page_preview: true, + }; + + const response = await request(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(payload), + bodyTimeout: this.timeout, + headersTimeout: this.timeout, + }); + + const body = (await response.body.json()) as { ok: boolean }; + if (!body.ok) { + throw new Error(`Bot API 调用失败: ${JSON.stringify(body)}`); + } + } + + async sendSafe(text: string, parseMode?: "MarkdownV2" | "Markdown") { + try { + await this.send(text, parseMode); + } catch (error) { + logger.error(`忽略汇报发送异常: ${(error as Error).message}`); + } + } +} diff --git a/src/telegramClient.ts b/src/telegramClient.ts new file mode 100644 index 0000000..038c26e --- /dev/null +++ b/src/telegramClient.ts @@ -0,0 +1,53 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { TelegramClient } from "telegram"; +import { StringSession } from "telegram/sessions"; + +import type { TelegramCredentials } from "./config.js"; +import { logger } from "./logger.js"; +import { prompt } from "./utils/prompt.js"; + +function loadSessionString(filePath: string): string { + if (!existsSync(filePath)) { + return ""; + } + return readFileSync(filePath, "utf-8").trim(); +} + +function saveSessionString(filePath: string, session: string) { + writeFileSync(filePath, session, { encoding: "utf-8" }); + logger.info(`会话写入 ${filePath}`); +} + +async function resolvePhone(preconfigured?: string): Promise { + if (preconfigured) { + return preconfigured; + } + const phone = await prompt("请输入 Telegram 手机号 (含国家码)"); + if (!phone) { + throw new Error("未提供手机号,无法完成登录。\n请设置 USER_PHONE 或在提示时输入。"); + } + return phone; +} + +export async function createTelegramClient(credentials: TelegramCredentials): Promise { + const session = new StringSession(loadSessionString(credentials.sessionFile)); + const client = new TelegramClient(session, credentials.apiId, credentials.apiHash, { + connectionRetries: credentials.connectionRetries, + }); + + await client.start({ + phoneNumber: async () => await resolvePhone(credentials.phone), + password: async () => credentials.twoFactorPassword ?? (await prompt("请输入二步验证密码 (如无直接回车)")), + phoneCode: async () => await prompt("请输入短信/Telegram 验证码"), + onError: (error) => logger.error(`登录失败: ${error.message}`), + }); + + logger.info("User-Bot 登录完成。"); + const serialized = client.session.save(); + if (typeof serialized === "string" && serialized) { + saveSessionString(credentials.sessionFile, serialized); + } else { + logger.warn("会话未能序列化,跳过持久化。"); + } + return client; +} diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts new file mode 100644 index 0000000..f85351c --- /dev/null +++ b/src/utils/prompt.ts @@ -0,0 +1,29 @@ +import readline from "node:readline"; + +type PromptOptions = { + defaultValue?: string; +}; + +/** + * Minimal readline prompt helper,为登录流程提供交互输入。 + */ +export function prompt(question: string, options: PromptOptions = {}): Promise { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const { defaultValue } = options; + const q = question.trim().endsWith(":") ? question : `${question.trim()}: `; + rl.question(q, (answer) => { + rl.close(); + const trimmed = answer.trim(); + if (!trimmed && defaultValue) { + resolve(defaultValue); + } else { + resolve(trimmed); + } + }); + }); +} diff --git a/src/utils/text.ts b/src/utils/text.ts new file mode 100644 index 0000000..1069501 --- /dev/null +++ b/src/utils/text.ts @@ -0,0 +1,16 @@ +/** + * Telegram MarkdownV2 兼容的简单转义。 + */ +export function escapeMarkdown(value?: string | null): string { + if (!value) { + return ""; + } + return value.replace(/[`*_\[\]()~>#+\-=|{}.!]/g, (match) => `\\${match}`); +} + +export function truncate(value: string, limit = 500): string { + if (value.length <= limit) { + return value; + } + return `${value.slice(0, limit)}...`; +} diff --git a/tests/keywords.spec.ts b/tests/keywords.spec.ts new file mode 100644 index 0000000..7189756 --- /dev/null +++ b/tests/keywords.spec.ts @@ -0,0 +1,58 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { KeywordStore } from "../src/keywords.js"; + +describe("KeywordStore", () => { + const tempDir = mkdtempSync(join(tmpdir(), "keywords-test-")); + const keywordsFile = join(tempDir, "keywords.yaml"); + + beforeAll(() => { + writeFileSync( + keywordsFile, + [ + "keywords:", + " - name: promo", + " patterns:", + ' - "推广"', + " regex: false", + " - name: regex", + " patterns:", + ' - "(?i)验证码"', + " regex: true", + ].join("\n"), + "utf-8", + ); + }); + + afterAll(() => { + rmSync(tempDir, { recursive: true, force: true }); + }); + + it("匹配子串与正则,并在文件更新后热加载", async () => { + const store = new KeywordStore(keywordsFile); + const firstHits = store.match("这是一个推广内容"); + expect(firstHits.some((hit) => hit.keyword === "promo")).toBe(true); + + const regexHits = store.match("验证码123"); + expect(regexHits.some((hit) => hit.keyword === "regex")).toBe(true); + + await new Promise((resolve) => setTimeout(resolve, 20)); + writeFileSync( + keywordsFile, + [ + "keywords:", + " - name: new", + " patterns:", + ' - "新品"', + ].join("\n"), + "utf-8", + ); + + const updated = store.match("这是一条新品"); + expect(updated.some((hit) => hit.keyword === "new")).toBe(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a576255 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ES2022", + "moduleResolution": "node", + "lib": ["ES2021", "DOM"], + "outDir": "dist", + "rootDir": ".", + "strict": true, + "noImplicitOverride": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["src", "tests", "vitest.config.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..258a588 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.spec.ts"], + }, +});