├── config ├── ver ├── proxy.js-template ├── minimum_user.conf.js ├── wx-emoji-map.js ├── confLoader.js ├── def.conf.js └── native_emoji_map.js ├── downloaded ├── audio │ └── 0.0 ├── file │ └── 0.0 ├── fileTG │ └── 0.0 ├── photo │ └── 0.0 ├── photoTG │ └── 0.0 ├── video │ └── 0.0 ├── videoTG │ └── 0.0 ├── voiceTG │ └── 0.0 ├── customEmotion │ └── 0.0 └── stickerTG │ └── 0.0 ├── static ├── template___downloaded │ ├── audio │ │ └── 0.0 │ ├── file │ │ └── 0.0 │ ├── photo │ │ └── 0.0 │ ├── video │ │ └── 0.0 │ ├── fileTG │ │ └── 0.0 │ ├── photoTG │ │ └── 0.0 │ ├── stickerTG │ │ └── 0.0 │ ├── videoTG │ │ └── 0.0 │ ├── voiceTG │ │ └── 0.0 │ └── customEmotion │ │ └── 0.0 ├── track_of_codes.txt ├── some_idea_about_Project.txt ├── init-project.bat ├── Dockerfile-type1 ├── 1.Dockerfile ├── docker_launcher.sh ├── README-v1.md ├── ctBotReport.php ├── 2.entry.sh └── CTBR_Docs1_zh.md ├── src ├── ctMisc.js ├── mod_template.js ├── dataStorage.api.js ├── audioRecognition.js ├── upyunMiddleware.js ├── init-wx.js ├── m_keepalive.js ├── wxMddw.js ├── common.js ├── init-tg.js └── tgProcessor.js ├── ctBridgeBot.iml ├── data └── README.md ├── wcferry-puppet-c ├── package.json ├── index.d.mts └── index.d.ts ├── package.json └── README.md /config/ver: -------------------------------------------------------------------------------- 1 | 4.2.1 -------------------------------------------------------------------------------- /downloaded/audio/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/file/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/fileTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/photo/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/photoTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/video/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/videoTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/voiceTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/customEmotion/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /downloaded/stickerTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/audio/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/file/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/photo/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/video/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/fileTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/photoTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/stickerTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/videoTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/voiceTG/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/template___downloaded/customEmotion/0.0: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/track_of_codes.txt: -------------------------------------------------------------------------------- 1 | #23382 Quoted message name debug: ${match[1]} / ${state.s.selfName} / ${args.peerName} 2 | -------------------------------------------------------------------------------- /src/ctMisc.js: -------------------------------------------------------------------------------- 1 | let env; 2 | 3 | async function a() { 4 | const {} = env; 5 | } 6 | 7 | function b() { 8 | const {} = env; 9 | } 10 | 11 | module.exports = (incomingEnv) => { 12 | env = incomingEnv; 13 | return {}; 14 | }; -------------------------------------------------------------------------------- /src/mod_template.js: -------------------------------------------------------------------------------- 1 | let env; 2 | 3 | async function a() { 4 | const {} = env; 5 | } 6 | 7 | function b() { 8 | const {} = env; 9 | } 10 | 11 | module.exports = (incomingEnv) => { 12 | env = incomingEnv; 13 | return {}; 14 | }; -------------------------------------------------------------------------------- /ctBridgeBot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/some_idea_about_Project.txt: -------------------------------------------------------------------------------- 1 | In test, sending a TG sticker to Wx takes about 8 seconds. 2 | 3 | If a network error is classified as WARN, then maybe a message failed to send. 4 | 5 | For a group rename event, its origin is old group, and sender name is new group, with content { changed the group name to "" } 6 | 7 | Notes for the future document: 8 | 1. run initialisation script to create necessary file 9 | 2. put proxy.js inside data/ to use at docker 10 | 3. Docker will become available next step -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | ## data/ 2 | This directory is used to store user-related data, 3 | and will NOT be overwritten when updating version. 4 | 5 | When changing device where ctBridgeBot runs on, please move the entire folder forward 6 | to keep your data consistency. 7 | 8 | Contains: 9 | - `user.conf.js` : User configuration file, change anytime you want; 10 | - `sticker_l4.json` : Sticker Hash & tg Image Bind Table, with first 4 chars as index; 11 | - `ctbridgebot.memory-card.json` : Generated when logging WeChat in and updated during time flies. 12 | -------------------------------------------------------------------------------- /static/init-project.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | cd %~dp0 3 | cd .. 4 | 5 | echo This program will probably DESTROY ALL your config and data in project directory! 6 | echo Please be sure that you want to run this batch program to initiate some files on Windows. 7 | pause 8 | 9 | copy "config\minimum_user.conf.js" "data\CHANGE_ME)user.conf.js" 10 | copy "config/proxy.js-template" "data/proxy.js" 11 | echo {} > data\sticker_l4.json 12 | @REM xcopy "static\template___downloaded" "downloaded" 13 | 14 | echo Completed! Please check if all files in right position. 15 | timeout -t 5 -------------------------------------------------------------------------------- /config/proxy.js-template: -------------------------------------------------------------------------------- 1 | // This is a device-specific proxy setting, which is used to bypass the firewall. 2 | // When updating the code, you could make no change to this file. 3 | // ===================================================================== 4 | 5 | // Please write your HTTP proxy with patterns below: 6 | // Eg. v2rayN 7 | // module.exports = "http://127.0.0.1:10809" ; 8 | // Eg. NekoRay 9 | // module.exports = "http://127.0.0.1:7080" ; 10 | 11 | // Other Proxies you like 12 | // module.exports = "http://192.168.1.1:7890" ; 13 | 14 | // Or, If you don't want to use proxy at all: 15 | module.exports = "" ; 16 | -------------------------------------------------------------------------------- /static/Dockerfile-type1: -------------------------------------------------------------------------------- 1 | # Only Use this file, if you don't want Docker to handle all files! 2 | # This image will only serve necessary environment for the bot to run, 3 | # and you have to clone the repo and map the volume to the container! 4 | # However, it'll make you easier to alter codes, access logs, and so on. 5 | # (Like me, I'm using this image currently, cuz I can sync code from and to my local machine.) 6 | 7 | FROM wechaty/wechaty 8 | 9 | # Set the working directory 10 | WORKDIR /bot 11 | 12 | ## [ Manually volume /bot !!] ## 13 | 14 | # Copy your application files 15 | # COPY ./_auto.sh . 16 | 17 | # Set the command to run when the container starts 18 | ENTRYPOINT /bot/static/2.entry.sh 19 | -------------------------------------------------------------------------------- /wcferry-puppet-c/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wcferry-custom", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "description": "Custom modified version of wcferry puppet", 6 | "private": true, 7 | "exports": { 8 | ".": { 9 | "types": "./index.d.ts", 10 | "import": "./index.mjs" 11 | } 12 | }, 13 | "main": "./wcfpuppet.mjs", 14 | "module": "./wcfpuppet.mjs", 15 | "types": "./index.d.ts", 16 | "dependencies": { 17 | "file-box": "^1.4.15", 18 | "knex": "^3.1.0", 19 | "unstorage": "^1.10.2", 20 | "wechaty-puppet": "^1.20.2", 21 | "xml2js": "^0.6.2", 22 | "@wechatferry/agent": "0.0.24", 23 | "@wechatferry/core": "0.0.24" 24 | } 25 | } -------------------------------------------------------------------------------- /static/1.Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile type 2 to bootstrap ctBridgeBot with single volume 2 | 3 | # Use node:20-bookworm as base image, as alpine lack some unknown libraries 4 | FROM node:20-bookworm 5 | 6 | LABEL maintainer="Eddy0644 " 7 | 8 | # Install necessary packages required by chromium 9 | RUN apt-get update && apt-get install -y \ 10 | # this line to solve 'shared libraries: libnss3.so ENOENT' 11 | libnss3-dev libgdk-pixbuf2.0-dev libgtk-3-dev libxss-dev\ 12 | # this line to solve 'shared libraries: libasound.so.2 ENOENT' 13 | libasound2\ 14 | && rm -rf /var/lib/apt/lists/*\ 15 | # mkdir for workdir ? 16 | && mkdir /bot && touch /bot/docker.flag 17 | 18 | WORKDIR /bot 19 | 20 | # Must do this to save traffic for author and all users! 21 | COPY package.json /bot 22 | RUN npm install 23 | 24 | COPY . /bot 25 | 26 | RUN chmod +x /bot/static/2.entry.sh 27 | ENTRYPOINT /bot/static/2.entry.sh 28 | 29 | CMD ["/bot/static/2.entry.sh", "go"] -------------------------------------------------------------------------------- /src/dataStorage.api.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs').promises; 2 | 3 | class DataStorage { 4 | constructor(filename) { 5 | this.filename = filename; 6 | } 7 | 8 | async get(key) { 9 | const data = await this.readDataFromFile(); 10 | return data[key] || null; 11 | } 12 | 13 | async set(key, value) { 14 | const data = await this.readDataFromFile(); 15 | data[key] = value; 16 | await this.writeDataToFile(data); 17 | } 18 | 19 | async readDataFromFile() { 20 | try { 21 | const fileContents = await fs.readFile(this.filename, 'utf8'); 22 | return JSON.parse(fileContents); 23 | } catch (err) { 24 | // If the file doesn't exist or is empty, return an empty object 25 | return {}; 26 | } 27 | } 28 | 29 | async writeDataToFile(data) { 30 | const fileContents = JSON.stringify(data); 31 | await fs.writeFile(this.filename, fileContents, 'utf8'); 32 | } 33 | } 34 | 35 | module.exports = DataStorage; 36 | -------------------------------------------------------------------------------- /static/docker_launcher.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Please skip this script ---------------------------- 4 | 5 | # Set default values 6 | screenName="CT" 7 | dockerName="51" 8 | cmd1="cd /mnt" 9 | cmd2="npm start" 10 | targetShell="bash" 11 | 12 | # Check if no command-line argument is provided 13 | if [[ $# -eq 0 ]]; then 14 | echo 'No parameter!' 15 | echo '$1 for ct, mr, and tq.' 16 | echo '$2 for EMPTY or r.' 17 | exit 18 | fi 19 | 20 | # Check if 1 command-line argument is provided 21 | if [[ $# -eq 1 ]]; then 22 | if [[ "$1" == "tq" ]]; then 23 | screenName="tq" 24 | dockerName="tq2" 25 | cmd1="cd /tq" 26 | cmd2="node src/i.js" 27 | targetShell="sh" 28 | elif [[ "$1" == "mr" ]]; then 29 | screenName="mr" 30 | dockerName="mr1" 31 | cmd1="cd /mnt" 32 | cmd2="./mcl" 33 | targetShell="bash" 34 | elif [[ "$1" == "ct" ]]; then 35 | screenName="ct" 36 | dockerName="ct3" 37 | cmd1="cd /bot" 38 | cmd2="npm run h0" 39 | targetShell="bash" 40 | fi 41 | fi 42 | 43 | # Check if 2 command-line argument is provided 44 | if [[ $# -eq 2 ]]; then 45 | if [[ "$2" == "r" ]]; then 46 | screen -x "$1" 47 | exit 48 | fi 49 | fi 50 | 51 | docker start "$dockerName" 52 | screen -dmS "$screenName" docker exec -it "$dockerName" "$targetShell" 53 | sleep 2 54 | # Execute the commands inside the screen session 55 | screen -S "$screenName" -X stuff "$cmd1\n" 56 | sleep 1 57 | screen -S "$screenName" -X stuff "$cmd2\n" 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ctbridgebot", 3 | "version": "4.2.0", 4 | "description": "Bridge messages between WeChat and Telegram. maximized customization.", 5 | "superagent": "src/BotIndex.js", 6 | "scripts": { 7 | "build": "rm -f ct_temp.tar.gz && tar czvf ct_temp.tar.gz src data config package.json", 8 | "p": "node src/BotIndex.js poll", 9 | "verify": "bash static/2.entry.sh verify", 10 | "h1": "node src/BotIndex.js hook 1", 11 | "h2": "node src/BotIndex.js hook 2", 12 | "h0": "node src/BotIndex.js hook 0", 13 | "start": "node src/BotIndex.js poll", 14 | "dbg": "node --inspect src/BotIndex.js poll", 15 | "puppet-install": "node node_modules/.bin/wechaty-puppet-install" 16 | }, 17 | "keywords": [ 18 | "wechat", 19 | "telegram" 20 | ], 21 | "author": "Eddy0644", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@seald-io/nedb": "^4.1.1", 25 | "@wechatferry/agent": "^0.0.26", 26 | "@wechatferry/puppet": "^0.0.24", 27 | "cheerio": "^1.0.0-rc.12", 28 | "dayjs": "^1.11.10", 29 | "file-box": "^1.5.5", 30 | "https-proxy-agent": "^7.0.4", 31 | "log4js": "^6.9.1", 32 | "md5": "^2.3.0", 33 | "node-machine-id": "^1.1.12", 34 | "node-schedule": "^2.1.1", 35 | "node-telegram-bot-api": "^0.65.1", 36 | "p-retry": "^6.2.1", 37 | "qrcode-terminal": "^0.12.0", 38 | "sharp": "^0.33.3", 39 | "superagent": "^8.1.2", 40 | "tencentcloud-sdk-nodejs-asr": "^4.0.837", 41 | "wechaty": "^1.20.2", 42 | "xml2js": "^0.6.2" 43 | }, 44 | "repository": "https://github.com/Eddy0644/ctBridgeBot" 45 | } 46 | -------------------------------------------------------------------------------- /config/minimum_user.conf.js: -------------------------------------------------------------------------------- 1 | // noinspection SpellCheckingInspection 2 | // ------------- 3 | // User-side Configuration File, will never be overwritten by update, and should be backed up. 4 | 5 | 6 | /* 7 | * This is a minimum example of user.conf.js, if you want to bootstrap the project quickly, 8 | * please copy this file to user.conf.js and fill in the necessary information. 9 | * If there is any function that you want to add or modify, please refer to def.conf.js, 10 | * find corresponding setting, and copy to current file. 11 | * */ 12 | module.exports = { 13 | ctToken: 'EnterYourCtTokenHere##############', 14 | tgbot: { 15 | botToken: '5000:ABCDE', 16 | botName: '@your_bot_username_ending_in_this_suffix__bot', 17 | tgAllowList: [5000000001], 18 | }, 19 | class: { 20 | "def": { 21 | "tgid": -100000, 22 | }, 23 | "push": { 24 | // Could set to the same as default channel for short use 25 | "tgid": -10000, 26 | }, 27 | "C2C": [ 28 | { 29 | "tgid": -1001006, 30 | "wx": ["wx Contact 1's name", true], 31 | "flag": "", 32 | }, 33 | ], 34 | // Below is a more recommended way for a supergroup containing many chats. 35 | "C2C_generator": { 36 | "-1001888888888": [ 37 | [1, "name of group 1", "Group", "flags_here"], 38 | [4, "name of person 1", "Person", ""], 39 | ], 40 | }, 41 | }, 42 | misc: { 43 | deliverPushMessage: true, 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ctBridgeBot 2 | Bridge your messages from a notorious IM in China: `WeChat`, to an excellent IM:`Telegram`, 3 | with many customization and features that other program will never have. 4 | 5 | For Full Documents in **Chinese**, please see 6 | [Online Version](https://blog.ryancc.top/2023/08/01/ctbr_docs1/) (Recommended) 7 | or in-project file [static/CTBR_Docs1_zh.md](static/CTBR_Docs1_zh.md). 8 | 9 | > Sorry, but this project is discontinued. Although I'm still using it, but I cannot guarantee that your account won't get banned by Tencent if you use any robot program like this one. 10 | And, due to changes in upstream, the project cannot be deployed with Docker anymore, user must setup a Windows VM and install VisualStudio runtime to run it. If you still interested in this program, please go ahead. 11 | 12 | > 很抱歉,这个项目已经停止了。虽然我仍然在继续使用它,但我无法保证你的账号在使用像这样的机器人程序的过程中不会被腾讯封禁。 13 | 而且,由于上游的变化,该项目无法再使用Docker进行部署,用户必须设置一个Windows虚拟机并安装VisualStudio运行时才能运行它。如果你仍然对这个项目感兴趣,请继续。 14 | 15 | 16 | This is a personal training project, as there may be some problems that I didn't notice, so issues and PRs are welcome, if you have any suggestion! 17 | 18 | \* The project may contain some function that requires a valid `ctToken` to proceed. You can get a 'trial' token for free (please find link in Q&A group or online document) or [donate here](https://afdian.com/item/b6b1c37a2d5011ee88eb52540025c377) to get a 'donated' token (will unlock more functions if possible). 19 | 20 | Thanks for your support. 21 | 22 | And, if you have any question that the online document failed to resolve, you can [join the Q&A group](https://t.me/+AHsMZ9yvKK5lMTRl) 23 | OR browse [this channel](https://t.me/rych0814) and find bot link in channel description to contact me directly. 24 | 25 | However, if the question reveals a bug, or you don't want to join groups, please open an issue, I would reply ASAP. 26 | -------------------------------------------------------------------------------- /static/README-v1.md: -------------------------------------------------------------------------------- 1 | # ctBridgeBot 2 | 3 | Bridge your messages from a notorious IM in China: `WeChat`, to an excellent IM:`Telegram`, with many custom configurations and features that other program will never have. 4 | 5 | _This document is no longer maintained and is lack of detail, please refer to [My Blog](https://blog.ryancc.top/2023/08/01/ctbr_docs1/) for newer version. (Chinese)_ 6 | 7 | ## Installation 8 | 1. log into your server (eg. Cloud Server or docker container). 9 | 2. Clone the whole repository into your disk (assume that code are under `/opt/ctBridgeBot`, then you should run `cd /opt && git clone https://github.com/Eddy0644/ctBridgeBot.git`). 10 | 3. copy `config/def.conf.js` into `config/user.conf.js` and make changes to some required parameter and credentials, like TG bot token or so. 11 | 4. move `static/template___downloaded` into `downloaded/` as the default folder of media received upon wx or tg message. 12 | 5. edit `proxy.js` to your real proxy server address. 13 | 6. - if you have webHook successfully setup on remote server, you could start the application using `npm run h0` to start the app using `https://www.com/webHook0`. 14 | - if you want to use poll method temporary, please run `npm run p`. 15 | 7. All Done! Wait for scanning QR code via Wx and then you would get rid of that green app! 16 | 17 | 18 | ## Project Directory Structure 19 | 20 | 1. `config/` : all your custom configs in one directory (containing a user config file and a default config which will be replaced when updating code) 21 | 2. `downloaded/` : all your downloaded media files from WeChat or Telegram. 22 | 3. `src/` : all source code of this project. 23 | 4. `log/` : two types of log files, one is for verbosed program log, and the other is for WeChat raw message log. 24 | 5. `static/` : containing some files you may need to use or read when deploying this project. 25 | 6. `data/` : containing some variable file which may change during program runtime. 26 | 7. `proxy.js` : a proxy server configuration for Telegram, which is used to get rid of the network ban in specific region. (recommended to stay unoutched upon deployment) 27 | 28 | Our advice is, replacing whole `config` and `src` directory when updating code, and keeping `downloaded` directory untouched. As for changing device, you should at least move the `data` directory to the new device, and `downloaded` directory if better. 29 | 30 | 31 | 32 | --- 33 | 34 | Maybe this document isn't good enough; Issues and PRs are welcome! 35 | 36 | And talk to me directly with [Ryan_Contact_Intermediate_bot](https://t.me/Ryan_Contact_Intermediate_bot) ! 37 | -------------------------------------------------------------------------------- /src/audioRecognition.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const tencentcloud = require("tencentcloud-sdk-nodejs-asr"); 3 | const AsrClient = tencentcloud.asr.v20190614.Client; 4 | 5 | let env; 6 | 7 | // async function a() { 8 | // const {} = env; 9 | // } 10 | // 11 | // function b() { 12 | // const {} = env; 13 | // } 14 | 15 | async function VTT_by_tx(audioPath, voiceFormat = "mp3") { 16 | const {secret, defLogger} = env; 17 | if (secret.txyun.switch !== "on") return "ERR!."; // VTT disabled! 18 | try { 19 | // 尝试调用腾讯云一句话识别API自动转文字(准确率略低于wx) 20 | const client = new AsrClient({ 21 | credential: secret.txyun, 22 | region: "", 23 | profile: { 24 | httpProfile: { 25 | endpoint: "asr.tencentcloudapi.com", 26 | }, 27 | }, 28 | }); 29 | const base64Data = (await fs.promises.readFile(audioPath)).toString('base64'); 30 | const fileSize = (await fs.promises.stat(audioPath)).size; 31 | const result = await client.SentenceRecognition({ 32 | "SubServiceType": 2, 33 | "EngSerViceType": "16k_zh_dialect", 34 | "SourceType": 1, 35 | "VoiceFormat": voiceFormat, 36 | "Data": base64Data, 37 | "DataLen": fileSize 38 | }); 39 | defLogger.trace(`VTT success, content:{${result.Result}}`); 40 | return result.Result; 41 | } catch (e) { 42 | defLogger.debug(`Try to send audio file to Txyun but failed in the process.`); 43 | return "ERR!."; 44 | } 45 | } 46 | 47 | async function wx_audio_VTT(saveTarget, audioPath, voiceFormat = "mp3") { 48 | const {secret} = env; 49 | let timerLabel; 50 | if (secret.misc.debug_add_console_timers) { 51 | timerLabel = `wx_audio_VTT by tencent cloud | #${process.uptime().toFixed(2)} used`; 52 | console.time(timerLabel); 53 | } 54 | const result = await VTT_by_tx(audioPath, "mp3"); 55 | if (result !== "ERR!.") { 56 | saveTarget.audioParsed = ` :\n"${result}"`; 57 | } else saveTarget.audioParsed = ""; 58 | if (timerLabel) console.timeEnd(timerLabel); 59 | } 60 | 61 | async function tg_audio_VTT(audioPath) { 62 | const {defLogger} = env; 63 | const result = await VTT_by_tx(audioPath, "ogg-opus"); 64 | if (result !== "ERR!.") { 65 | defLogger.trace(`Transcript result: ${result}`); 66 | return result; 67 | } else return ""; 68 | } 69 | 70 | module.exports = (incomingEnv) => { 71 | env = incomingEnv; 72 | return {wx_audio_VTT, tg_audio_VTT}; 73 | }; -------------------------------------------------------------------------------- /static/ctBotReport.php: -------------------------------------------------------------------------------- 1 | 0, 4 | "_last" => [], 5 | "runningTime" => 0, 6 | "logText" => "", 7 | "poolToDelete" => [], 8 | "generationDate" => "" 9 | ]; 10 | // echo "11111"; 11 | if (isset($_REQUEST["s"])) { 12 | if ($_REQUEST["s"] === "create") try { 13 | $recvJson = file_get_contents("php://input"); 14 | $recvArr = json_decode($recvJson, true); 15 | //echo $recvJson; 16 | if (!$recvArr["status"]) throw new Exception("Invalid incoming JSON"); 17 | $fName = date("Ymd-His") . "." . rand(100, 700) . ".json"; 18 | $recvArr["generationDate"] = date("Ymd-His"); 19 | file_put_contents($fName, json_encode($recvArr, JSON_PRETTY_PRINT)); 20 | // echo json_encode([ 21 | // "success" => "1", 22 | // "filename" => $fName 23 | // ]); 24 | echo $fName; 25 | die(); 26 | } catch (Exception $e) { 27 | header("HTTP/1.1 400 Bad Request"); 28 | //header("400 Bad Request"); 29 | print_r($e); 30 | die(); 31 | } 32 | // no valid .php?s=_____ 33 | header("HTTP/1.1 400 Bad Request"); 34 | die(); 35 | } else if (isset($_REQUEST["n"])) { 36 | // read former saved JSON then output it. 37 | try { 38 | $readJson = file_get_contents($_REQUEST["n"]); 39 | $c = json_decode($readJson, true); 40 | } catch (Exception $e) { 41 | header("500 Internal Error"); 42 | die(); 43 | } 44 | } else { 45 | // no any valid flag, display default page 46 | header("HTTP/1.1 400 Bad Request"); 47 | die("STOPSTOPSTOP.STOPSTOPSTOP.STOPSTOPSTOP.STOPSTOPSTOP.STOPSTOPSTOP"); 48 | } 49 | ?> 50 | 51 | 52 | CTBot status page 53 | 54 | 55 | 56 | 90 | 91 | 108 | 109 | -------------------------------------------------------------------------------- /src/upyunMiddleware.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | const fs = require("fs"); 3 | const https = require("https"); 4 | const FileBox = require("file-box").FileBox; 5 | let env; 6 | 7 | async function uploadWebp(filename) { 8 | const {secret} = env; 9 | // Orig: version 20230606 from ctBridge 10 | return new Promise(async (resolve) => { 11 | const {password, webFilePathPrefix, operatorName, urlPrefix, urlPathPrefix} = secret.upyun; 12 | const generateAPIKey = (password) => crypto.createHash('md5').update(password).digest('hex'); 13 | const generateSignature = (apiKey, signatureData) => { 14 | const hmac = crypto.createHmac('sha1', apiKey); 15 | hmac.update(signatureData); 16 | return hmac.digest('base64'); 17 | }; 18 | const getFileContentMD5 = async (filePath2) => { 19 | const fileContent = fs.readFileSync(filePath2); 20 | return crypto.createHash('md5').update(fileContent).digest('hex'); 21 | }; 22 | const apiKey = generateAPIKey(password); 23 | const method = 'PUT'; 24 | const date = new Date().toUTCString(); // Generate UTC timestamp 25 | const filePathPrefix = `./downloaded/stickerTG/`; 26 | const filePath = `${webFilePathPrefix}/${filename}`; 27 | const fileStream = fs.createReadStream(`${filePathPrefix}${filename}`); 28 | const contentMD5 = await getFileContentMD5(`${filePathPrefix}${filename}`); 29 | const signatureData = `${method}&${filePath}&${date}&${contentMD5}`; 30 | const signature = generateSignature(apiKey, signatureData); 31 | const authHeader = `UPYUN ${operatorName}:${signature}`; 32 | const requestUrl = `https://v0.api.upyun.com${filePath}`; 33 | 34 | const requestOptions = { 35 | method, 36 | headers: { 37 | 'Authorization': authHeader, 38 | 'Content-Type': 'image/webp', 39 | 'Date': date, 40 | 'Content-MD5': contentMD5, 41 | } 42 | }; 43 | const req = https.request(requestUrl, requestOptions, (res) => { 44 | let data = ""; 45 | res.on('data', (chunk) => { 46 | data = data + chunk.toString(); 47 | }); 48 | res.on('end', () => { 49 | if (res.statusCode === 200) { 50 | resolve({ 51 | ok: 1, 52 | filePath: `${urlPrefix}${urlPathPrefix}/${filename}`, 53 | msg: data 54 | }); 55 | } else { 56 | resolve({ 57 | ok: 0, 58 | msg: `Upyun server returned non-200 response.\n${data}` 59 | }); 60 | } 61 | }); 62 | }); 63 | req.on('error', (e) => { 64 | resolve({ 65 | ok: 0, 66 | msg: `Error occurred during upload-to-Upyun request: ${e.toString()}` 67 | }); 68 | }); 69 | 70 | fileStream.pipe(req); 71 | fileStream.on('end', () => req.end()); 72 | }); 73 | } 74 | 75 | async function webpToJpg(local_path, rand1) { 76 | const {defLogger} = env; 77 | const filename = local_path.replace('./downloaded/stickerTG/', ''); 78 | const uploadResult = await uploadWebp(filename); 79 | if (uploadResult.ok) { 80 | await FileBox.fromUrl(uploadResult.filePath + '!/format/jpg').toFile(`./downloaded/stickerTG/${rand1}.jpg`); 81 | return local_path.replace('.webp', '.jpg'); 82 | } else { 83 | defLogger.warn(`Error on .webp-to-.jpg pre-process:\n\t${uploadResult.msg}`); 84 | return local_path; 85 | } 86 | } 87 | 88 | module.exports = (incomingEnv) => { 89 | env = incomingEnv; 90 | return { 91 | // uploadWebp, 92 | webpToJpg, 93 | }; 94 | }; -------------------------------------------------------------------------------- /src/init-wx.js: -------------------------------------------------------------------------------- 1 | const {WechatyBuilder} = require('wechaty'); 2 | const {WechatferryPuppet} = require('../wcferry-puppet-c'); 3 | // const {WechatferryPuppet} = require('@wechatferry/puppet'); 4 | const qrcodeTerminal = require("qrcode-terminal"); 5 | // const config = require("../config/secret"); 6 | const secret = require("../config/confLoader"); 7 | const {downloader} = require("./common")(); 8 | const fs = require("fs"); 9 | 10 | const wxbot = WechatyBuilder.build({ 11 | puppet: new WechatferryPuppet() 12 | // name: 'data/ctbridgebot', 13 | // puppet: 'wechaty-puppet-wechat', 14 | // puppetOptions: {uos: true} 15 | }); 16 | const DTypes = { 17 | Default: -1, 18 | NotSend: 0, 19 | Text: 1, 20 | Image: 2, 21 | Audio: 3, 22 | CustomEmotion: 4, 23 | File: 5, 24 | Push: 6, 25 | }; 26 | 27 | module.exports = (tgBotDo, wxLogger) => { 28 | // running instance of wxbot-pre 29 | let needLoginStat = 0; 30 | wxbot.on('scan', async (qrcode, status) => { 31 | const qrcodeImageUrl = [ 32 | 'https://api.qrserver.com/v1/create-qr-code/?data=', 33 | encodeURIComponent(qrcode), 34 | ].join(''); 35 | if (status === 2) { 36 | qrcodeTerminal.generate(qrcode, {small: true}); // show QRcode in terminal 37 | console.log(qrcodeImageUrl); 38 | // if need User Login 39 | if (needLoginStat === 0) { 40 | needLoginStat = 1; 41 | const isUserTriggeredRelogin = fs.existsSync("data/userTriggerRelogin.flag"); 42 | setTimeout(async () => { 43 | if (needLoginStat === 1) { 44 | if (secret.notification.send_relogin_via_tg) await tgBotDo.SendMessage(null, 45 | `${secret.c11n.wxLoginQRCodeHint}\n${qrcodeImageUrl}`, false, "HTML"); 46 | if (!isUserTriggeredRelogin) with (secret.notification) await downloader.httpsCurl(baseUrl + prompt_relogin_required + default_arg); 47 | wxLogger.info(`Login notification has been delivered to user.`); 48 | } 49 | }, isUserTriggeredRelogin ? 500 : 27000); 50 | // delete the flag file after sent notification. 51 | if (isUserTriggeredRelogin) fs.unlinkSync("data/userTriggerRelogin.flag"); 52 | } 53 | 54 | } else if (status === 3) { 55 | wxLogger.info(`------[The code is already scanned.]------`); 56 | needLoginStat = 0; 57 | } else { 58 | console.log(`User may accepted login. Continue listening...`); 59 | } 60 | }); 61 | 62 | // wxbot.on('logout', ...) is defined in BotIndex.js. 63 | 64 | let wxBotErrorStat = 0; 65 | wxbot.on('error', async (e) => { 66 | // This error handling function should be remastered! 67 | // TODO add tg reminder; to wxbot.error 68 | const conf1 = secret.misc.auto_reboot_after_error_detected; 69 | let msg = e.toString(); 70 | const isWDogErr = e.toString().includes("WatchdogAgent reset: lastFood:"); 71 | if (wxBotErrorStat === 0 && isWDogErr) { 72 | wxBotErrorStat = 1; 73 | // No need to output any console log now, full of errors! 74 | with (secret.notification) await downloader.httpsCurl(baseUrl + prompt_wx_stuck + default_arg); 75 | wxLogger.error(msg + `\nFirst Time;\n\n\n\n`); 76 | setTimeout(() => { 77 | if (wxBotErrorStat > 6) { 78 | wxLogger.error(`Due to wx error, initiated self restart procedure! (If activated)\n\n`); 79 | if (conf1) setTimeout(() => process.exit(1), 2000); 80 | } else { 81 | wxLogger.info("wxBotErrorStat not reaching threshold, not exiting.\t" + wxBotErrorStat); 82 | } 83 | }, 20000); 84 | } else if (wxBotErrorStat > 0 && isWDogErr) { 85 | wxBotErrorStat++; 86 | // following watchdog error, skipped 87 | } else { 88 | if (msg.includes("TypeError: Cannot read properties of null (reading 'userName')")) return wxLogger.debug("Dropped an error produced by a WXWork message (not implemented)."); 89 | if (msg.includes("SIGINT")) return; // means that user stopped the program. 90 | wxLogger.warn(`[From Puppet] ` + msg); 91 | wxLogger.debug(`[Stack] ${e.stack.split("\n").slice(0, 5).join("\n")}\nSee log file for detail.`); 92 | 93 | } 94 | }); 95 | 96 | return { 97 | wxbot: wxbot, 98 | DTypes: DTypes, 99 | }; 100 | }; 101 | -------------------------------------------------------------------------------- /config/wx-emoji-map.js: -------------------------------------------------------------------------------- 1 | // Hard work on collecting this emoji map from WeChat and TG 2 | module.exports = { 3 | "5080295825486119684": "[Smile]", 4 | "5080451427856286273": "[Grimace]", 5 | "5080484868471652915": "[Drool]", 6 | "5080575320482906705": "[Scowl]", 7 | "5080553222876169116": "[CoolGuy]", 8 | "5082528237817430465": "[Sob]", 9 | "5080144874565534195": "[Shy]", 10 | "5080272512403637024": "[Silent]", 11 | "5080622328899961708": "[Sleep]", 12 | "5080236056721228466": "[Cry]", 13 | "5080185071164457550": "[Awkward]", 14 | "5080067517909566120": "[Angry]", 15 | "5082812233939944047": "[Tongue]", 16 | "5082737626063045208": "[Grin]", 17 | "5082694118044336674": "[Surprise]", 18 | "5080544383833473913": "[Frown]", 19 | "5080365700309058139": "[Blush]", 20 | "5082731398360466177": "[Scream]", 21 | "5082745799385809415": "[Puke]", 22 | "5082477132001575613": "[Chuckle]", 23 | "5080401198213759834": "[Joyful]", 24 | "5080330365613114006": "[Slight]", 25 | "5080384692654441285": "[Smug]", 26 | "5082780683110187527": "[Drowsy]", 27 | "5080272894655726111": "[Panic]", 28 | "5080506184394343051": "[Laugh]", 29 | "5080557895800586978": "[Commando]", 30 | "5082437768626307802": "[Scold]", 31 | "5080138109992043081": "[Shocked]", 32 | "5082750833087480423": "[Shhh]", 33 | "5082760402274615843": "[Dizzy]", 34 | "5082815249006985670": "[Toasted]", 35 | "5082680374148989488": "[Skull]", 36 | "5082664680338489809": "[Hammer]", 37 | "5082455025804903368": "[Bye]", 38 | "5084879075281994478": "[Speechless]", 39 | "5084969265300243028": "[NosePick]", 40 | "5082663898654442452": "[Clap]", 41 | "5082815309136528001": "[Trick]", 42 | // Left heng-heng maybe removed by WeChat, here may not work 43 | "5082695934815503117": "[Bah!L]", 44 | "5082470753975141213": "[Bah!R]", 45 | "5082481250875212458": "[Pooh-pooh]", 46 | "5082444863912280557": "[Shrunken]", 47 | "5082714325865464407": "[TearingUp]", 48 | "5082385249766212218": "[Sly]", 49 | "5082817284821484197": "[Kiss]", 50 | "5082812182400336444": "[Whimper]", 51 | "5082806465798865635": "[Happy]", 52 | "5082395321464521904": "[Sick]", 53 | "5082771951441674836": "[Flushed]", 54 | "5082780777599468044": "[Lol]", 55 | "5082770813275341412": "[Terror]", 56 | "5085061705881354882": "[Let Down]", 57 | "5082377179522663053": "[Duh]", 58 | "5082521417409364750": "[Hey]", 59 | "5082871006272422938": "[Facepalm]", 60 | "5082838609334108997": "[Smirk]", 61 | "5084832719699968599": "[Smart]", 62 | "5082811963357004318": "[Concerned]", 63 | "5084631801129862037": "[Yeah!]", 64 | "5082614832948052586": "[Onlooker]", 65 | "5082738596725653986": "[GoForIt]", 66 | "5082466154065167019": "[Sweats]", 67 | "5085097195196121673": "[OMG]", 68 | "5082497842333876800": "[Emm]", 69 | "5082863451424948887": "[Respect]", 70 | "5082762807456301667": "[Doge]", 71 | "5084699416800003135": "[NoProb]", 72 | "5082701105956127237": "[MyBad]", 73 | "5082588977244930819": "[Wow]", 74 | "5082402854837159346": "[Boring]", 75 | "5082874867448021770": "[Awesome]", 76 | "5082530544214868610": "[LetMeSee]", 77 | "5082324634892763873": "[Sigh]", 78 | "5082350653804643049": "[Hurt]", 79 | "5082684480137724746": "[Broken]", 80 | "5082785927265256060": "[Lips]", 81 | "5082853396906508892": "[Heart]", 82 | "5082804507293778768": "[BrokenHeart]", 83 | "5082814308409148160": "[Hug]", 84 | "5082584927090770487": "[ThumbsUp]", 85 | "5082776972258443890": "[ThumbsDown]", 86 | "5082807556720558922": "[Shake]", 87 | "5082417045409104600": "[Peace]", 88 | "5084820491928077427": "[Salute]", 89 | "5084737032123580935": "[Beckon]", 90 | "5084923352099848696": "[Fist]", 91 | "5082751129440223745": "[OK]", 92 | "5082759113784427126": "[Worship]", 93 | "5082572849642734057": "[Beer]", 94 | "5082489943889019532": "[Coffee]", 95 | "5082527752486126278": "[Cake]", 96 | "5082661029616288422": "[Rose]", 97 | "5082685472275172193": "[Wilt]", 98 | "5082621000521089995": "[Cleaver]", 99 | "5082313455092892122": "[Bomb]", 100 | "5085107477347828320": "[Poop]", 101 | "5082475637352956494": "[Moon]", 102 | "5082471235011478101": "[Sun]", 103 | "5082513067992941359": "[Party]", 104 | "5082528577119847200": "[Gift]", 105 | "5082424948148929123": "[Packet]", 106 | "5082764894810407536": "[Blessing]", 107 | "5084908216635097536": "[Fireworks]", 108 | "5082592103981122202": "[Pig]", 109 | "5085121173998535287": "[Waddle]", 110 | "5084691118923187141": "[Tremble]", 111 | "5082630183161168344": "[Twirl]" 112 | }; 113 | -------------------------------------------------------------------------------- /static/2.entry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ##################### 3 | ## A bash script which is used to check if the project is ready to run, 4 | ## or being the entrypoint of Docker image. 5 | ## For Linux users only. 6 | ##################### 7 | 8 | # Now we use this to ensure it sits in the right directory -- project root 9 | cd "$(dirname "$0")/.." 10 | 11 | ins_ok=0 12 | if [ -e "data/install.ok" ]; then 13 | ins_ok=1 14 | else 15 | pwd 16 | fi 17 | 18 | if [ -e "package.json" ]; then 19 | #cd .. this is a silly error 20 | [ $ins_ok -eq 1 ] || echo "1. [PASSED] We are currently in project root." 21 | else 22 | echo "1. [FAILED] cannot find package.json. Please run this script in the project root." 23 | echo " For this reason, the program will stop after 5 seconds." 24 | sleep 5 25 | exit 26 | fi 27 | 28 | if [ -e "data/README.md" ] && [ -e "docker.flag" ]; then 29 | echo "1. [ WARN ] data directory abnormal." 30 | echo "It seems that you are running the project in Docker, and we detected a README file in 'data' directory, which shouldn't exist in our thought. " 31 | echo "Due to above reason, I must tell you to check if the volume mapping is done correct. It should be: mapping a dir on your actual storage, to /bot/data." 32 | echo "If you didn't use an empty folder to map or know what you've done, then ignore this message, after a grace period of 5 seconds." 33 | sleep 5 34 | fi 35 | 36 | if [ ! -e "data/proxy.js" ] && [ ! -e "proxy.js" ]; then 37 | echo "2. [ WARN ] proxy setting not found." 38 | echo " Copying proxy.js-template to data/ dir." 39 | echo " Please change that file to your current proxy setting." 40 | cp "config/proxy.js-template" "data/proxy.js" 41 | # Check if HTTPS_PROXY environment variable is set 42 | if [ -n "$HTTPS_PROXY" ]; then 43 | # Write module.exports="$HTTPS_PROXY" into data/proxy.js 44 | echo "module.exports=\"$HTTPS_PROXY\";" > "data/proxy.js" 45 | echo " HTTPS_PROXY environment variable set. Writing to data/proxy.js." 46 | echo " Please notice that only when data/proxy.js not exist, the environment variable will be used." 47 | 48 | fi 49 | else 50 | [ $ins_ok -eq 1 ] || echo "2. [PASSED] proxy setting is in place." 51 | fi 52 | 53 | if [ ! -e "data/user.conf.js" ]; then 54 | # no effective user config file 55 | 56 | # We added below code to avoid user written their config with heart but forgot to rename it 57 | if [ ! -e "data/CHANGE_ME)user.conf.js" ]; then 58 | # no template user config file 59 | echo "3. [ WARN ] user config file not exist." 60 | echo " Copying user.conf.js template to data/ dir." 61 | cp "config/minimum_user.conf.js" "data/CHANGE_ME)user.conf.js" 62 | echo " Please set necessary values in that file, and rename it to 'user.conf.js'." 63 | echo " For this reason, the program will stop after 5 seconds." 64 | sleep 5 65 | exit 66 | else 67 | echo "3. [ WARN ] user config file not complete." 68 | echo " Did you forget to rename it to 'user.conf.js'?" 69 | echo " For this reason, the program will stop after 5 seconds." 70 | sleep 5 71 | exit 72 | fi 73 | else 74 | [ $ins_ok -eq 1 ] || echo "3. [PASSED] user config is in place." 75 | fi 76 | 77 | #if [ -e "data/CHANGE_ME)user.conf.js" ]; then 78 | # echo "'data/CHANGE_ME)user.conf.js' exists. " 79 | # echo "Please stop the container, set proper value in that file, and rename to 'user.conf.js'." 80 | # echo "The program will stop after 5 seconds." 81 | # sleep 5 82 | # exit 83 | #fi 84 | 85 | # Check for 'data/sticker_l4.json' and create an empty one if it doesn't exist 86 | if [ ! -e "data/sticker_l4.json" ]; then 87 | echo "4. [ INFO ] current sticker storage is not exist." 88 | echo " Creating an empty 'data/sticker_l4.json'." 89 | echo "{}" > "data/sticker_l4.json" 90 | else 91 | [ $ins_ok -eq 1 ] || echo "4. [PASSED] sticker storage is in place." 92 | fi 93 | 94 | # Check if 'downloaded/' exists, if not copy everything from 'static/template___downloaded/' 95 | #if [ ! -d "downloaded/" ]; then 96 | # echo "'downloaded/' directory does not exist. Copying from 'static/template___downloaded/'." 97 | # cp -r "static/template___downloaded/" "downloaded/" 98 | #fi 99 | 100 | if [ ! -e "data/install.ok" ]; then 101 | # first complete install 102 | echo -e "\n\n\n" 103 | echo "You are all set! The script will mark you as completed installation." 104 | echo "If you want to re-run install procedure, please remove 'data/install.ok' file." 105 | echo "We are ready to run in 3 seconds..." 106 | sleep 3 107 | echo "{}" > "data/install.ok" 108 | fi 109 | 110 | #=============================================== 111 | 112 | #export WECHATY_LOG=silly 113 | 114 | if [ "$1" = "verify" ]; then 115 | exit 0 116 | fi 117 | npm run p 118 | -------------------------------------------------------------------------------- /config/confLoader.js: -------------------------------------------------------------------------------- 1 | // confLoader.js 2 | 3 | const defaultConfig = require("./def.conf.js"); 4 | // const userConfigPath = require("path").join(__dirname, "user.conf.js"); 5 | const userConfigPath = "../data/user.conf.js"; 6 | 7 | // const {ctLogger} = require('../src/common')("lite"); 8 | 9 | function mergeConfig(defaultConfig, userConfig) { 10 | 11 | function mergeObjects(defaultObj, userObj) { 12 | for (const [key, userValue] of Object.entries(userObj)) { 13 | if (typeof userValue === "object" && "switch" in userValue) { 14 | if (userValue.switch === "on") { 15 | defaultObj[key] = userValue; 16 | } 17 | } else if (typeof defaultObj[key] === "object" && typeof userValue === "object") { 18 | mergeObjects(defaultObj[key], userValue); 19 | } else { 20 | defaultObj[key] = userValue; 21 | } 22 | } 23 | } 24 | 25 | 26 | mergeObjects(defaultConfig, userConfig); 27 | } 28 | 29 | function loadConfig() { 30 | try { 31 | const userConfig = require(userConfigPath); 32 | mergeConfig(defaultConfig, userConfig); 33 | return defaultConfig; 34 | } catch (error) { 35 | console.error("\nError loading user configuration:", error, "\nProgram Will take default Config!!\n\n\n"); 36 | // ctLogger.error("Error loading user configuration:", error, "\nProgram Will take default Config!!"); 37 | return defaultConfig; 38 | } 39 | } 40 | 41 | const config = loadConfig(); 42 | 43 | config.bundle = { 44 | getTGFileURL: suffix => `https://api.telegram.org/file/bot${config.tgbot.botToken}/${suffix}`, 45 | getTGBotHookURL: suffix => `${config.tgbot.webHookUrlPrefix}${suffix}/bot${config.tgbot.botToken}`, 46 | }; 47 | delete config.class.C2C_generator["-1001888888888"]; 48 | 49 | // Prepare and reify C2C-generator 50 | { 51 | const generator = config.class.C2C_generator; 52 | const C2C_result = config.class.C2C; 53 | for (const tgid in generator) if (generator.hasOwnProperty(tgid)) { 54 | const items = generator[tgid]; 55 | for (const item of items) { 56 | // item = [1001,"name", false, ""] 57 | let item_type = item[2] || "P"; 58 | item_type = item_type.replace("Person", "P").replace("Room", "R").replace("Group", "R"); 59 | const newC2C = { 60 | "tgid": parseInt(tgid), 61 | "threadId": item[0], 62 | "wx": [item[1], /* isGroup */item_type === "R"], 63 | "flag": item[3] || "", 64 | }; 65 | C2C_result.push(newC2C); 66 | } 67 | } 68 | } 69 | // Parsing flags and chatOptions for each C2C 70 | { 71 | config.class.def.opts = {}; 72 | const def = config.chatOptions; 73 | // below lists ALL supported internal boolean/number properties 74 | const single_props = ['mixed', 'merge', 'skipSticker', 'nameType', 'onlyReceive', 'hideMemberName']; 75 | // apply defaults for default channel first 76 | for (const propName in def) if (def.hasOwnProperty(propName)) { 77 | config.class.def.opts[propName] = def[propName]; 78 | } 79 | // process each C2C 80 | for (const oneC2C of config.class.C2C) { 81 | oneC2C.flag = oneC2C.flag || ""; // in case no flag specified 82 | oneC2C.opts = {}; 83 | for (const propName in def) if (def.hasOwnProperty(propName)) { 84 | // copy all in def to opts, a.k.a load defaults 85 | oneC2C.opts[propName] = def[propName]; 86 | } 87 | // [Applying C2C flag settings] 88 | for (const prop of oneC2C.flag.split(" ")) { 89 | if (prop === "") continue; // skip empty string 90 | const parts = prop.split("="); // split by "=" 91 | if (single_props.includes(parts[0])) { 92 | // -[internal boolean/number properties]-------- 93 | if (parts.length === 1) 94 | oneC2C.opts[parts[0]] = true; // just enable that option 95 | else if (parts.length === 2) { 96 | // user chose the value of that option 97 | oneC2C.opts[parts[0]] = parseInt(parts[1]); 98 | } 99 | } else if (parts[0].startsWith("rule_")) { 100 | // -[rules override]-------- 101 | // Hereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 102 | } else { 103 | console.error(`Unparsed Flags entry: "${prop}", please check!`); 104 | } 105 | } 106 | // [Applying C2C.chatOptions settings] 107 | for (const propName in oneC2C.chatOptions) if (oneC2C.chatOptions.hasOwnProperty(propName)) { 108 | // copy all in chatOptions to opts 109 | oneC2C.opts[propName] = oneC2C.chatOptions[propName]; 110 | } 111 | } 112 | // Now can use C2C.opts in later code 113 | } 114 | 115 | module.exports = config; 116 | -------------------------------------------------------------------------------- /src/m_keepalive.js: -------------------------------------------------------------------------------- 1 | const dayjs = require("dayjs"); 2 | const {downloader} = require('./common')(); 3 | let env; 4 | 5 | async function triggerCheck() { 6 | const {secret, state, wxLogger} = env; 7 | const t_conf = secret.mods.keepalive; 8 | const t_state = state.v.keepalive; 9 | // If now is within the pauseCheckAfterResume_hr, skip the check 10 | if (t_state.last_resume_ts !== 0 && dayjs().unix() - t_state.last_resume_ts < t_conf.pauseCheckAfterResume_hr * 3600) { 11 | if (secret.misc.debug_show_additional_log) console.log(`[Keepalive check] Skipped. `); 12 | return; 13 | } 14 | // Parse trigger timespan 15 | const {start, end} = t_conf.trigger_v1[0]; 16 | const [startHour, startMinute] = start.split(':').map(Number), [endHour, endMinute] = end.split(':').map(Number); 17 | const startTime = dayjs().startOf('minute').hour(startHour).minute(startMinute); 18 | const endTime = dayjs().startOf('minute').hour(endHour).minute(endMinute); 19 | const isNowBetween = dayjs().isAfter(startTime) && dayjs().isBefore(endTime); 20 | 21 | if (isNowBetween) { // If time between range, start checking 22 | if (secret.misc.debug_show_additional_log) wxLogger.debug("Report on keepalive.triggerCheck/msgCount: ", t_state.msgCounter_prev, " ", state.v.wxStat.MsgTotal); 23 | 24 | if (t_state.msgCounter_prev < state.v.wxStat.MsgTotal) { 25 | // There are new messages since last timer run, so update idle timer 26 | t_state.msgCounter_prev = state.v.wxStat.MsgTotal; 27 | t_state.idle_start_ts = dayjs().unix(); 28 | if (t_state.state === -1) { 29 | // The bot is operational again! Reset state. 30 | t_state.state = 0; 31 | if (t_conf.pauseCheckAfterResume_hr) { 32 | t_state.last_resume_ts = dayjs().unix(); 33 | wxLogger.info(`[Keepalive check] The bot is operational again! Pausing check for ${t_conf.pauseCheckAfterResume_hr} hours.`); 34 | // TODO write this to a persistent data file. 35 | } 36 | } 37 | } else { 38 | // No new messages since last timer run, so check if idle timer exceeds 39 | const idle_length = dayjs().unix() - t_state.idle_start_ts; 40 | const idle_max = t_conf.trigger_v1[0].max_idle_minutes * 60; 41 | if (t_state.idle_start_ts !== 0 && idle_length > idle_max) { 42 | if (t_state.state === -1) { 43 | // User did not solve the last fail check, so let's just skip 44 | return; 45 | } 46 | // Idle timer exceeds, so trigger keepalive 47 | wxLogger.info(`Keepalive triggered: last update of idle timer is ${dayjs(t_state.idle_start_ts).format("HH:mm")}, which exceeds ${t_conf.trigger_v1[0].max_idle_minutes} minutes from now.`); 48 | // Check functions should be executed here... 49 | // First perform avatar-url check 50 | if (t_conf.check_byAvatarUrl.switch === "on") await check_byAvatarUrl(); // skip his response 51 | // Then perform send-msg check 52 | let ok = 0; 53 | if (t_conf.check_bySendMsg.switch === "on") ok = await check_bySendMsg(); 54 | if (ok === 1) { 55 | // "False positive", Reset idle timer... 56 | wxLogger.info(`[Keepalive check] Not in suspended status, discarding "false positive".`); 57 | t_state.msgCounter_prev = state.v.wxStat.MsgTotal; 58 | t_state.idle_start_ts = dayjs().unix(); 59 | } else if (ok === 0) { 60 | t_state.state = -1; 61 | // avoid the test message affects checks 62 | t_state.msgCounter_prev++; 63 | // Notify user 64 | wxLogger.warn(`[Keepalive check] failed, no response received.`); 65 | with (secret.notification) await downloader.httpsCurl(baseUrl + prompt_wx_suspended + default_arg); 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | async function check_byAvatarUrl() { 73 | const {wxLogger, wxbot, secret} = env; 74 | const t_conf2 = secret.mods.keepalive.check_byAvatarUrl; 75 | // wxbot.currentUser.avatar().then(e => e.toBase64().then(console.log)); // Fallback 76 | const str = await (await wxbot.currentUser.avatar()).toBase64(); 77 | wxLogger.info(`Current avatar base64 length: ${str.length}`); 78 | wxLogger.trace(str.substring(0, str.length > 200 ? 200 : str.length)); 79 | // TO-DO (compare with default avatar) continue development after collecting data 80 | // This check method is not feasible, maybe because it's hard to bypass the cache, so we cannot get real statuses with wechaty api. 81 | return -1; // refer to other check methods 82 | } 83 | 84 | async function check_bySendMsg() { 85 | const {wxLogger, state, secret, wxbot} = env; 86 | const t_conf2 = secret.mods.keepalive.check_bySendMsg; 87 | const originalCounter = state.v.wxStat.notSelfTotal; 88 | state.v.keepalive.state = 1; // Mark the process of this check. 89 | // Preparing variables 90 | let msgTarget = await wxbot.Contact.find({name: t_conf2.sendTarget}); 91 | msgTarget = msgTarget || await wxbot.Contact.find({alias: t_conf2.sendTarget}); 92 | const msgText = t_conf2.sendContents[Math.floor(Math.random() * t_conf2.sendContents.length)]; 93 | wxLogger.debug(`[Keepalive check] sending {${msgText}} to {${msgTarget}}...`); 94 | await msgTarget.say(msgText); 95 | // Check in 20s period 96 | for (let i = 0; i < 5; i++) { 97 | await new Promise(resolve => setTimeout(resolve, t_conf2.watchTimeRange_sec * 200)); 98 | console.log(`\t[Keepalive check] waiting for response... ${i + 1}/5`); 99 | if (state.v.wxStat.notSelfTotal > originalCounter) { 100 | state.v.keepalive.state = 0; 101 | wxLogger.debug(`[Keepalive check] Received Response, check completed.`); 102 | return 1; 103 | } 104 | } 105 | // No response received, should notify user now. 106 | return 0; 107 | } 108 | 109 | async function util_resetState() { 110 | const {} = env; 111 | } 112 | 113 | async function a() { 114 | const {} = env; 115 | } 116 | 117 | function b() { 118 | const {} = env; 119 | } 120 | 121 | module.exports = (incomingEnv) => { 122 | env = incomingEnv; 123 | return {triggerCheck, check_byAvatarUrl, check_bySendMsg}; 124 | }; -------------------------------------------------------------------------------- /src/wxMddw.js: -------------------------------------------------------------------------------- 1 | const xml2js = require("xml2js"); 2 | const dayjs = require("dayjs"); 3 | const fs = require("fs"); 4 | const secret = require("../config/confLoader"); 5 | 6 | 7 | let env; 8 | 9 | // async function a() { 10 | // const {} = env; 11 | // } 12 | 13 | async function handlePushMessage(rawContent, msg, name) { 14 | const {wxLogger, secret} = env; 15 | 16 | if (secret.filtering.wxPostOriginBlackList.some(i => name.includes(i))) { 17 | wxLogger.debug(`New Posts from [${name}] --> ❎(BlackList)`); 18 | return 0; 19 | } 20 | if (secret.misc.deliverPushMessage === false) { 21 | wxLogger.debug(`New Posts from [${name}] --> ❎(denial config)`); 22 | return 0; 23 | } 24 | const ps = await parseXML(rawContent.replaceAll("<", "<").replaceAll(">", ">").replaceAll("
", "\n")); 25 | if (ps === false) return 0; 26 | // noinspection JSUnresolvedVariable 27 | try { 28 | // noinspection JSUnresolvedVariable 29 | const appname = ps.msg.appinfo[0].appname[0]; 30 | // noinspection JSUnresolvedVariable 31 | const items = ps.msg.appmsg[0].mmreader[0].category[0].item; 32 | let out = `📬 Posts from [#${appname}]\n`; 33 | for (const item of items) { 34 | let itemStr = ""; 35 | const {title, url, digest, is_pay_subscribe} = item; 36 | itemStr += `→ ${title[0]}`; 37 | if (is_pay_subscribe[0] !== '0') itemStr += `\n [Pay Subscribe Post]`; 38 | if (digest[0].length > 85) itemStr += `
${digest[0]}
`; 39 | else if (digest[0].length > 1) itemStr += `
${digest[0]}
`; 40 | else itemStr += "\n"; 41 | out += itemStr; 42 | } 43 | // Success 44 | { 45 | const s = secret.misc.deliverPushMessage; 46 | if (s === true) msg.receiver = secret.class.push; 47 | if (s.tgid) msg.receiver = s; 48 | } 49 | return out.replaceAll("&", "&"); 50 | } catch (e) { 51 | wxLogger.warn(`Error occurred when reading xml detail from posts.`); 52 | return 0; 53 | } 54 | } 55 | 56 | async function parseCardMsg(rawContent, isOfficial = true) { 57 | const {wxLogger, secret} = env; 58 | const ps = await parseXML(rawContent.replaceAll("<", "<").replaceAll(">", ">").replaceAll("
", "")); 59 | if (ps === false) return rawContent; 60 | // noinspection JSUnresolvedVariable 61 | try { 62 | // TODO brandSubscriptConfigUrl 63 | if (isOfficial) return secret.c11n.officialAccountParser(ps.msg.$); 64 | else return secret.c11n.personCardParser(ps.msg.$); 65 | } catch (e) { 66 | wxLogger.info(`Error occurred when reading xml detail of AccountCard_Msg. Skipping...`); 67 | return rawContent; 68 | } 69 | } 70 | 71 | 72 | async function handleVideoMessage(msg, name) { 73 | const {wxLogger, tgBotDo, tgLogger} = env; 74 | let videoPath = `./downloaded/video/${dayjs().format("YYYYMMDD-HHmmss").toString()}-(${name.replaceAll(/[\/\\]/g, ",")}).mp4`; 75 | wxLogger.debug(`Detected as Video, Downloading...`); 76 | tgBotDo.SendChatAction("record_video", msg.receiver).then(tgBotDo.empty) 77 | const fBox = await msg.toFileBox(); 78 | await fBox.toFile(videoPath); 79 | if (!fs.existsSync(videoPath)) { 80 | wxLogger.info("Download Video failed. Please remind the console."); 81 | return 0; 82 | } 83 | const videoInfo = await getVideoFileInfo(videoPath); 84 | if (videoInfo[0] === -1) { 85 | wxLogger.info("Parse Video Info failed.\n" + videoInfo[2]); 86 | } else if (videoInfo[0] === 0) { 87 | if (videoInfo[2] === "NOMODULE") wxLogger.warn("Error occurred when loading ffprobe-related modules. We'll try to send the video directly."); 88 | wxLogger.info("Parse Video Info (Play Length) failed."); 89 | wxLogger.debug(`Video Info: size(${videoInfo[1].toFixed(2)})MB, length( PARSE FAILURE )`); 90 | } else if (videoInfo[0] === 1) { 91 | wxLogger.debug(`Video Info: size(${videoInfo[1].toFixed(2)})MB, length(${videoInfo[2]}).\n${videoInfo[3]}`); 92 | wxLogger.trace(`video local path for above:(${videoInfo}), more info: ${JSON.stringify(videoInfo[4])}`); 93 | } 94 | if (videoInfo[1] > 49) return "sizeLimit"; 95 | tgBotDo.SendChatAction("upload_video", msg.receiver).then(tgBotDo.empty) 96 | const stream = fs.createReadStream(videoPath); 97 | let tgMsg = await tgBotDo.SendVideo(msg.receiver, `from [${name}]`, stream, true); 98 | tgBotDo.SendChatAction("choose_sticker", msg.receiver).then(tgBotDo.empty) 99 | if (!tgMsg) { 100 | tgLogger.warn("Got invalid TG receipt, resend wx file failed."); 101 | return "sendFailure"; 102 | } else return "Success"; 103 | } 104 | 105 | async function getVideoFileInfo(videoPath) { 106 | let included = 0, fileSizeMB = -1; 107 | try { 108 | const util = require('util'); 109 | const statAsync = util.promisify(fs.stat); 110 | const stats = await statAsync(videoPath); 111 | const fileSizeBytes = stats.size; 112 | fileSizeMB = fileSizeBytes / (1024 * 1024); 113 | 114 | const ffprobePath = require('ffprobe-static').path; 115 | const ffprobe = require('ffprobe'); 116 | const ffprobeOptions = {path: ffprobePath}; 117 | const ffprobeAsync = util.promisify(ffprobe); 118 | 119 | // included = 1; 120 | 121 | const info = await ffprobeAsync(videoPath, ffprobeOptions); 122 | if (info.streams && info.streams.length > 0 && info.streams[0].duration) { 123 | const playlengthSeconds = parseFloat(info.streams[0].duration); 124 | const playlengthMinutes = Math.floor(playlengthSeconds / 60); 125 | const playlengthSecondsRemaining = Math.floor(playlengthSeconds % 60); 126 | // noinspection JSUnresolvedVariable 127 | const additional = `Codec: ${info.streams[0].codec_name}/${info.streams[0].codec_tag_string}, ` 128 | + `Frame: ${info.streams[0].coded_width}x${info.streams[0].coded_height}, ` 129 | + `Bitrate: ${parseInt(info.streams[0].bit_rate) / 1000}Kbps, ${info.streams[0].avg_frame_rate}s`; 130 | return [1, fileSizeMB, `${playlengthMinutes}:${playlengthSecondsRemaining}`, additional, info.streams]; 131 | } else { 132 | return [0, fileSizeMB]; 133 | } 134 | } catch (err) { 135 | return [!included ? 0 : -1, fileSizeMB, !included ? "NOMODULE" : err]; 136 | } 137 | } 138 | 139 | // function b() { 140 | // const {} = env; 141 | // } 142 | 143 | function parseXML(xml) { 144 | const {defLogger} = env; 145 | return new Promise((resolve) => { 146 | xml2js.parseString(xml, (err, result) => { 147 | if (err) { 148 | defLogger.warn(`XML parse to dot notation failed.`); 149 | resolve(false); 150 | } else { 151 | resolve(result); 152 | } 153 | }); 154 | }); 155 | } 156 | 157 | module.exports = (incomingEnv) => { 158 | env = incomingEnv; 159 | return {handlePushMessage, handleVideoMessage, parseCardMsg, parseXML}; 160 | }; 161 | -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | const log4js = require('log4js'), fs = require("fs"), dayjs = require("dayjs"), https = require("https"), 2 | agentEr = require("https-proxy-agent"); 3 | const proxy = require((fs.existsSync('data/proxy.js')) ? '../data/proxy.js' : '../proxy.js'); 4 | const logger_pattern = "[%d{hh:mm:ss.SSS}] %3.3c:[%5.5p] %m"; 5 | const logger_pattern_month = "[%d{yyMMdd/hh:mm:ss.SSS}] %3.3c:[%5.5p] %m"; 6 | const logger_pattern_console = "%[[%d{dd/hh:mm:ss}] %1.1p/%c%] %m"; 7 | 8 | process.env.TZ = 'Asia/Shanghai'; 9 | log4js.configure({ 10 | appenders: { 11 | "console": { 12 | type: "console", 13 | layout: { 14 | type: "pattern", 15 | pattern: logger_pattern_console 16 | }, 17 | }, 18 | "dated": { 19 | type: "dateFile", 20 | filename: "log/day/d", 21 | pattern: "yy-MM-dd.log", 22 | alwaysIncludePattern: true, 23 | layout: { 24 | type: "pattern", 25 | pattern: logger_pattern 26 | }, 27 | }, 28 | "dated_warn_out": { 29 | type: "dateFile", 30 | filename: "log/warn", 31 | pattern: "yy-MM.log", 32 | alwaysIncludePattern: true, 33 | layout: { 34 | type: "pattern", 35 | pattern: logger_pattern_month 36 | }, 37 | }, 38 | "dated_warn": { 39 | type: "logLevelFilter", 40 | appender: "dated_warn_out", 41 | level: "warn", 42 | }, 43 | "wxMsgDetail_dated": { 44 | type: "dateFile", 45 | filename: "log/msgDT/wx", 46 | pattern: "yy-MM-dd.log", 47 | alwaysIncludePattern: true, 48 | layout: { 49 | type: "pattern", 50 | pattern: "[%d{hh:mm:ss.SSS}] %m%n%n" 51 | }, 52 | }, 53 | "debug_to_con": { 54 | type: "logLevelFilter", 55 | appender: "console", 56 | level: "debug", 57 | } 58 | }, 59 | categories: { 60 | "default": {appenders: ["dated"], level: "debug"}, 61 | "con": {appenders: ["console"], level: "trace"}, 62 | "ct": {appenders: ["dated", "debug_to_con", "dated_warn"], level: "trace"}, 63 | "wx": {appenders: ["dated", "debug_to_con", "dated_warn"], level: "trace"}, 64 | "wxMsg": {appenders: ["wxMsgDetail_dated"], level: "info"}, 65 | "tg": {appenders: ["dated", "debug_to_con", "dated_warn"], level: "trace"}, 66 | } 67 | }); 68 | 69 | const commonEnv = {}; 70 | 71 | const part1 = { 72 | wxLogger: log4js.getLogger("wx"), 73 | tgLogger: log4js.getLogger("tg"), 74 | conLogger: log4js.getLogger("con"), 75 | ctLogger: log4js.getLogger("ct"), 76 | }; 77 | 78 | const part2 = { 79 | LogWxMsg: (msg, type) => { 80 | const isMessageDropped = type === 1; 81 | let msgToStr = `${msg}`; 82 | // fixed here to avoid contamination of { 95 | logger.error(text); 96 | const stacks = `[Stack] ${e.stack.split("\n").slice(0, stackMax).join("\n")}`; 97 | logger.debug(stacks); 98 | if (commonEnv.tgNotifier) commonEnv.tgNotifier(text, 1, stacks); 99 | }, 100 | //////-----------Above is mostly of logger ---------------------////// 101 | nil: () => { 102 | }, 103 | STypes: { 104 | Chat: 1, 105 | FindMode: 2, 106 | }, 107 | CommonData: { 108 | TGBotCommands: [ 109 | // {command: '/find', description: 'Find Person or Group Chat'}, 110 | {command: '/clear', description: 'Clear Current Selection'}, 111 | {command: '/help', description: 'Get a detail of more bot commands.'}, 112 | // {command: '/keyboard', description: 'Get a persistent versatile quick keyboard.'}, 113 | // {command: '/info', description: 'Get current system variables'}, 114 | // {command: '/placeholder', description: 'Display a placeholder to hide former messages | Output a blank message to cover your sensitive data.'}, 115 | // {command: '/slet', description: 'Set last explicit talker as last talker.'}, 116 | // {command: '/log', description: 'Get a copy of program verbose log of 1000 chars by default.'}, 117 | {command: '/lock', description: 'Lock the target talker to avoid being interrupted.'}, 118 | {command: '/spoiler', description: 'Add spoiler to the replied message.'}, 119 | {command: '/create_topic', description: 'Create a new topic for current talker.'}, 120 | // TODO fix /drop_toggle 121 | // {command: '/drop_toggle', description: 'Toggle /drop status. (Incomplete)'}, 122 | // { 123 | // command: '/reloginWX_2', 124 | // description: 'Immediately invalidate current WX login credential and reboot.' 125 | // }, 126 | 127 | // Add more commands as needed 128 | ], 129 | TGBotHelpCmdText: (state) => `/drop_on & /drop_off : [msg drop] 130 | /sync_on & /sync_off : [self sync] 131 | /info ; /placeholder ; /try_edit 132 | /create_topic_1 /create_topic_2 ; 133 | Lock: (${state.v.targetLock}) Last: [${(state.last && state.last.name) ? state.last.name : "-"}]`, 134 | wxPushMsgFilterWord: [ 135 | ["公众号", "已更改名称为", "查看详情"], 136 | ["关于公众号进行帐号迁移的说明"], 137 | ["关于公众号进行账号迁移的说明"], // must f*k WeChat here 138 | ], 139 | }, 140 | downloader: { 141 | httpsWithProxy: async function (url, pathName) { 142 | return new Promise((resolve, reject) => { 143 | if (!pathName) log4js.getLogger("default").error(`Undefined Download target!`); 144 | const file = fs.createWriteStream(pathName); 145 | const https_opt = proxy === "" ? {} : {agent: new agentEr.HttpsProxyAgent(proxy)}; 146 | https.get(url, https_opt, (response) => { 147 | response.pipe(file); 148 | file.on('finish', () => { 149 | file.close(); 150 | resolve("SUCCESS"); 151 | }); 152 | }).on('error', (error) => { 153 | fs.unlink(pathName, () => reject(error)); 154 | }); 155 | }); 156 | }, 157 | httpsCurl: async function (url) { 158 | if (url.includes("YourBarkAddress")) { 159 | log4js.getLogger("ct").debug(`A notification was skipped because bark notification not configured!\n${url}`); 160 | return new Promise((resolve) => { 161 | resolve(0); 162 | }); 163 | } 164 | return new Promise((resolve) => { 165 | https.get(url, {}, (res) => { 166 | if (res.statusCode === 200) resolve("SUCCESS"); 167 | else resolve(res.statusMessage); 168 | }).on('error', () => { 169 | console.error(`[Error] Failed on httpsCurl request. Probably network has been disconnected, so notifications have no need to launch now. Wait for Exit...`); 170 | setTimeout(() => resolve("NETWORK_DISCONNECTED"), 5000); 171 | }); 172 | }); 173 | }, 174 | httpsGet: async function (url) { 175 | return new Promise((resolve) => { 176 | https.get(url, (res) => { 177 | let data = ''; 178 | res.on('data', (chunk) => { 179 | data += chunk; 180 | }); 181 | res.on('end', () => { 182 | resolve([res.statusCode, data]); 183 | }); 184 | }).on('error', (err) => { 185 | resolve([0, err.message]); 186 | }); 187 | }); 188 | }, 189 | }, 190 | }; 191 | 192 | function isTimeValid(targetTS, maxDelay) { 193 | const nowDate = dayjs().unix(); 194 | return (nowDate - targetTS < maxDelay); 195 | } 196 | 197 | function filterFilename(orig) { 198 | return orig.replaceAll(/[\/\\]/g, ","); 199 | } 200 | 201 | const util = { 202 | isTimeValid, filterFilename, 203 | delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)), 204 | 205 | }; 206 | 207 | module.exports = (param, ext = null) => { 208 | if (param === "startup") { 209 | log4js.getLogger("default").debug(`Program Starting... 210 | ________ ____ __ 211 | / ____/ /_/ __ )____ / /_ 212 | / / / __/ __ / __ \\/ __/ 213 | / /___/ /_/ /_/ / /_/ / /_ 214 | \\____/\\__/_____/\\____/\\__/ 215 | 216 | `); 217 | commonEnv.tgNotifier = ext.tgNotifier || null; 218 | } 219 | // else return log4js.getLogger(param); 220 | else { // noinspection JSUnresolvedVariable 221 | 222 | if (param === "lite") return part1; 223 | else return {...part1, ...part2, util}; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/init-tg.js: -------------------------------------------------------------------------------- 1 | const secret = require('../config/confLoader'); 2 | const TelegramBot = require("node-telegram-bot-api"); 3 | const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 4 | 5 | // We choose to include another log4js here, as common.js finishes the initialization of log4js before init-tg and init-wx. 6 | // const {tgLogger} = require('./common')(); 7 | const tgLogger = require('log4js').getLogger("tg"); 8 | // We already give wxLogger during initialization of init-wx, so we don't need to require it again ToT. 9 | 10 | const isPolling = (!(process.argv.length >= 3 && process.argv[2] === "hook")); 11 | process.env["NTBA_FIX_350"] = "1"; 12 | const {downloader} = require("./common")(); 13 | 14 | const proxy = require((require("fs").existsSync('data/proxy.js')) ? '../data/proxy.js' : '../proxy.js'); 15 | 16 | let tgbot; 17 | if (isPolling) { 18 | tgbot = new TelegramBot(secret.tgbot.botToken, 19 | {polling: {interval: secret.tgbot.polling.interval}, request: {proxy},}); 20 | tgbot.deleteWebHook().then(() => { 21 | }); 22 | } else { 23 | tgbot = new TelegramBot(secret.tgbot.botToken, { 24 | webHook: { 25 | port: 8443, 26 | max_connections: 3, 27 | healthEndpoint: "/health", 28 | key: "config/srv.pem", 29 | cert: "config/cli.pem", 30 | }, 31 | request: {proxy} 32 | }); 33 | tgbot.setWebHook(secret.bundle.getTGBotHookURL(process.argv[3]), { 34 | drop_pending_updates: true 35 | /* Please, remove this line after the bot have ability to control messages between instances!!! */ 36 | }).then(() => { 37 | }); 38 | tgbot.openWebHook().then(() => { 39 | }); 40 | } 41 | 42 | function parseRecv(receiver, form) { 43 | if (receiver && receiver.s && receiver.s === 0) { 44 | if (secret.class.def.threadId) form.message_thread_id = secret.class.def.threadId; 45 | return secret.class.def.tgid; 46 | } else if (receiver && receiver.s) { 47 | // incoming object is tgMsg.matched 48 | return receiver.p.tgid; 49 | } else if (receiver && receiver.tgid) { 50 | if (receiver.threadId) form.message_thread_id = receiver.threadId; 51 | return receiver.tgid; 52 | } else if (typeof receiver === "number") { 53 | return receiver; 54 | } else { 55 | return secret.class.def.tgid; 56 | } 57 | } 58 | 59 | const tgBotDo = { 60 | SendMessage: async (receiver = null, msg, isSilent = false, parseMode = null, form = {}) => { 61 | if (isSilent) form.disable_notification = true; 62 | if (parseMode) form.parse_mode = parseMode; 63 | return await retryWithLogging(async () => { 64 | return await tgbot.sendMessage(parseRecv(receiver, form), msg, form); 65 | }, 2, 3800, `Text [${msg.substring(0, msg.length > 17 ? 17 : msg.length)}]`); 66 | }, 67 | RevokeMessage: async (msgId, receiver = null) => { 68 | return await retryWithLogging(async () => { 69 | return await tgbot.deleteMessage(parseRecv(receiver, {}), msgId); 70 | }, 2, 3800, `RevokeMessage`); 71 | }, 72 | SendChatAction: async (action, receiver = null) => { 73 | return await retryWithLogging(async () => { 74 | return await tgbot.sendChatAction(parseRecv(receiver, {}), action); 75 | }, 2, 3800, `SendChatAction`); 76 | }, 77 | SendAnimation: async (msg, path, isSilent = false, hasSpoiler = false) => { 78 | // await delay(100); 79 | let form = { 80 | caption: msg, 81 | has_spoiler: hasSpoiler, 82 | width: 100, 83 | height: 100, 84 | parse_mode: "HTML", 85 | }; 86 | const receiver = (() => { 87 | const s = secret.misc.deliverSticker; 88 | if (s === false) return 0; // already filtered in main js 89 | if (s === true) return secret.class.push; 90 | if (s.tgid) return s; 91 | })(); 92 | if (isSilent) form.disable_notification = true; 93 | // Temp. change for classifying stickers 94 | return await retryWithLogging(async () => { 95 | return await tgbot.sendAnimation(parseRecv(receiver, form), path, form, {contentType: 'image/gif'}) 96 | }, 3, 6000, `Animation`); 97 | }, 98 | SendPhoto: async (receiver = null, msg, path, isSilent = false, hasSpoiler = false) => { 99 | // await delay(100); 100 | let form = { 101 | caption: msg, 102 | has_spoiler: hasSpoiler, 103 | width: 100, 104 | height: 100, 105 | parse_mode: "HTML", 106 | }; 107 | if (isSilent) form.disable_notification = true; 108 | return await retryWithLogging(async () => { 109 | return await tgbot.sendPhoto(parseRecv(receiver, form), path, form, {contentType: 'image/jpeg'}); 110 | }, 3, 5200, `Photo`); 111 | }, 112 | EditMessageText: async (text, former_tgMsg, receiver = null) => { 113 | let form = { 114 | chat_id: parseRecv(receiver || former_tgMsg.matched, {}), 115 | message_id: former_tgMsg.message_id, 116 | parse_mode: "HTML" 117 | }; 118 | return await retryWithLogging(async () => { 119 | return await tgbot.editMessageText(text, form); 120 | }, 2, 3800, `EditMessageText`); 121 | }, 122 | EditMessageMedia: async (file_id, formerMsg, hasSpoiler = false, receiver = null) => { 123 | let form = { 124 | chat_id: parseRecv(receiver, {}), 125 | message_id: formerMsg.message_id, 126 | parse_mode: "HTML", 127 | }; 128 | return await retryWithLogging(async () => { 129 | const res = await tgbot.editMessageMedia({ 130 | type: "photo", 131 | media: file_id, 132 | has_spoiler: hasSpoiler, 133 | parse_mode: "HTML", 134 | caption: formerMsg.caption 135 | }, form); 136 | if (res) return true; 137 | return "Unknown Error."; 138 | }, 2, 3800, `EditMessageMedia`); 139 | }, 140 | SendAudio: async (receiver = null, msg, path, isSilent = false) => { 141 | let form = { 142 | caption: msg, 143 | parse_mode: "HTML", 144 | }; 145 | if (isSilent) form.disable_notification = true; 146 | return await retryWithLogging(async () => { 147 | return await tgbot.sendVoice(parseRecv(receiver, form), path, form, {contentType: 'audio/mp3'}); 148 | }, 2, 3800, `SendAudio`); 149 | }, 150 | SendLocation: async (receiver = null, latitude, longitude) => { 151 | let form = { 152 | disable_notification: true 153 | }; 154 | return await retryWithLogging(async () => { 155 | return await tgbot.sendLocation(parseRecv(receiver, form), latitude, longitude, form); 156 | }, 2, 3800, `SendLocation`); 157 | }, 158 | SendDocument: async (receiver = null, msg, path, isSilent = false) => { 159 | let form = { 160 | caption: msg, 161 | parse_mode: "HTML", 162 | }; 163 | if (isSilent) form.disable_notification = true; 164 | return await retryWithLogging(async () => { 165 | return await tgbot.sendDocument(parseRecv(receiver, form), path, form, {contentType: 'application/octet-stream'}) 166 | }, 3, 5000, `Document`); 167 | }, 168 | SendVideo: async (receiver = null, msg, path, isSilent = false) => { 169 | let form = { 170 | caption: msg, 171 | parse_mode: "HTML", 172 | }; 173 | if (isSilent) form.disable_notification = true; 174 | return await retryWithLogging(async () => { 175 | return await tgbot.sendVideo(parseRecv(receiver, form), path, form, {contentType: 'video/mp4'}); 176 | }, 2, 3800, `SendVideo`); 177 | }, 178 | empty: () => { 179 | } 180 | }; 181 | let errorStat = 0; 182 | tgbot.on('polling_error', async (e) => { 183 | let msg = "Polling - " + e.message.replace("Error: ", ""), msg2 = `[${process.uptime().toFixed(2)}]\t`, 184 | msg3 = "[Err]\t"; 185 | if (errorStat === 0) { 186 | errorStat = 1; 187 | setTimeout(async () => { 188 | if (errorStat > secret.tgbot.polling.pollFailNoticeThres) { 189 | // Following error count exceed the threshold after the timer set up by first error 190 | tgLogger.warn(`Frequent network issue detected! (${errorStat} errors in past 30 seconds) Please check network!\n${msg}`); 191 | with (secret.notification) await downloader.httpsCurl(baseUrl + prompt_network_problematic + default_arg); 192 | } else { 193 | // no other error during this period, discarding notify initiation 194 | errorStat = 0; 195 | tgLogger.info(`There may be a temporary network issue but now disappeared. If possible, please check your network config.`); 196 | 197 | } 198 | }, 42000); 199 | console.warn(msg3 + msg); 200 | } else if (errorStat > 0) { 201 | errorStat++; 202 | msg = msg.replace("Client network socket disconnected before secure TLS connection was established", "E_Socket_Disconnected").replace("EFATAL: ", ""); 203 | 204 | console.warn(msg2 + msg); 205 | } else { 206 | console.warn(msg2 + msg); 207 | } 208 | }); 209 | tgbot.on('webhook_error', async (e) => { 210 | tgLogger.warn("Webhook - " + e.message.replace("Error: ", "")); 211 | }); 212 | const retryWithLogging = async (func, maxRetries = 2, retryDelay = 4200, err_suffix = "") => { 213 | let retries = 0; 214 | const doWarn = (text) => { 215 | tgLogger.warn(text); 216 | if (secret.misc.deliverLogToTG !== 0) { 217 | const ignoredErrors = ["socket hang up", "Client network socket", "Too Many Requests", "ETIMEDOUT", "⚠️ctBridgeBot"]; 218 | if (ignoredErrors.some(error => text.includes(error))) return; 219 | tgBotDo.SendMessage(null, `⚠️ctBridgeBot Error\n
${text}
`, true, "HTML").then(() => { 220 | }); 221 | } 222 | }; 223 | while (retries < maxRetries) { 224 | try { 225 | const res = await func(); 226 | errorStat = 0; 227 | return res; 228 | } catch (error) { 229 | const noNeedRetry = (error.code === 'ETELEGRAM') && !(error.message.includes("retry after")); 230 | let errorMessage = `MsgSendFail:` + error.message.replace(/(Error:)/g, '').trim() + ` ${err_suffix}`; 231 | if (noNeedRetry) return doWarn(errorMessage); // no more retries! 232 | else doWarn(`(${retries + 1}/${maxRetries})` + errorMessage); 233 | await delay(retryDelay); 234 | retries++; 235 | } 236 | } 237 | // If the maximum number of retries is reached, you can handle it here if needed. 238 | tgLogger.warn("Retry failed. Could not complete the Telegram operation."); 239 | }; 240 | 241 | // function logErrorDuringTGSend(err) { 242 | // let err2 = err.toString().replaceAll("Error:", ""); 243 | // tgLogger.warn(`tgMsgSendFail: ${err2}`); 244 | // } 245 | 246 | module.exports = { 247 | tgbot, 248 | tgBotDo 249 | } -------------------------------------------------------------------------------- /config/def.conf.js: -------------------------------------------------------------------------------- 1 | // noinspection SpellCheckingInspection 2 | // ------------- 3 | // Configuration File, updated upon every version update: 4 | 5 | // Instruction: 6 | // The following abbreviation is used: 7 | // - wx: WeChat; tg: Telegram; tg cmds: Telegram Bot Commands; ct: ctBridgeBot; C2C: 'Chat to Chat'; tgid: Telegram Chat ID; 8 | // And inside 'user.conf.js', please just copy any part if modified by you, the two config files would be added together. 9 | 10 | module.exports = { 11 | ctToken: 'EnterYourCtTokenHere##############', 12 | tgbot: { 13 | botToken: '5000:ABCDE', 14 | botName: '@your_bot_username_ending_in_bot', 15 | tgAllowList: [5000000001], 16 | webHookUrlPrefix: 'https://your.domain/webHook', 17 | statusReport: { 18 | // Status Report Page function, see detail in docs. Not essential for most users. 19 | switch: "off", 20 | host: "your.domain", 21 | path: "/ctBot/rp.php" 22 | }, 23 | polling: { 24 | pollFailNoticeThres: 3, 25 | // Polling interval, which determines how often the bot checks for new messages on tg. 26 | // Set to smaller values (in ms) to get faster response, but it may cause tg API rate limit. 27 | interval: 2000, 28 | }, 29 | }, 30 | class: { 31 | "def": { 32 | "tgid": -100000, 33 | }, 34 | "push": { 35 | "tgid": -10000, 36 | }, 37 | "C2C": [ 38 | { 39 | "tgid": -1001006, 40 | "wx": ["wx Contact 1's name", true], 41 | "flag": "", 42 | }, 43 | ], 44 | // Below is a more recommended way for defining a supergroup containing many different chats. 45 | "C2C_generator": { 46 | // If you want to use `/create_topic` then remind the order of tgids, and the position of anchor. 47 | "-1001888888888": [ 48 | /* |autoCreateTopic Anchor| */ 49 | [1, "name of group 1 in wechat", "Group", "flags_here_if_you_need_it"], 50 | [4, "name of person 1", "Person", ""], 51 | ], 52 | }, 53 | }, 54 | filtering: { 55 | 56 | // Use this, only if you didn't bind some contacts in C2C, but you often chat with them. 57 | // Now this option is NOT recommended to use, unless you'll use find function very often. 58 | wxFindNameReplaceList: [ 59 | //["Shortened Name 1", "Original Name 1"], 60 | ], 61 | // mainly used to replace wx-side emoji to universal emoji 62 | wxContentReplaceList: [ 63 | ["[Pout]", "{😠}"], 64 | ["[Facepalm]", "{😹}"], 65 | ["[Hurt]", "{😭}"], 66 | ], 67 | // mainly used to replace universal emoji to wx-side emoji 68 | tgContentReplaceList: [ 69 | ["😡", "[Pout]"], 70 | ["😄", "[Doge]"], 71 | ["😏", "[Onlooker]"], 72 | ["😣", "[Panic]"], 73 | ["😮‍💨", "[Sigh]"], 74 | ], 75 | wxNameFilterStrategy: { 76 | // You can choose to use either 'blackList' or 'whiteList' (only one can be activated at a time) 77 | useBlackList: true, 78 | blackList: [ 79 | "美团", 80 | ], 81 | whiteList: [], 82 | }, 83 | wxMessageExcludeKeyword: [], 84 | wxPostOriginBlackList: [ 85 | "不接收消息的订阅号名称列表", 86 | ], 87 | }, 88 | notification: { 89 | // Remember to change the two '(YourBarkAddress)'! 90 | // Maybe you could use apis provided by 'api.day.app', from the Bark developer. 91 | baseUrl: "https://(YourBarkAddress)/BridgeBot_WARN[ct]/", 92 | default_arg: "?group=ctBridge&icon=https://ccdn.ryancc.top/bot.jpg", 93 | prompt_network_problematic: "Several network connectivity problems appeared. Please settle that immediately.", 94 | prompt_relogin_required: "Your previous login credential have already expired. Please re-login soon!", 95 | prompt_wx_stuck: "The WX puppet seems stuck, please check console and restart program soon!", 96 | prompt_wx_suspended: "The WX puppet was probably disconnected by Tencent server, please launch your mobile WeChat!", 97 | prompt_network_issue_happened: "ctBridgeBot is facing network issue, that causing message delay!", 98 | incoming_call_webhook: name => `https://(YourBarkAddress)/BridgeBot_Call/You have a incoming call from ${encodeURIComponent(name)} In WeChat.?sound=minuet&level=timeSensitive&group=ctBridge&icon=https://ccdn.ryancc.top/call.jpg`, 99 | send_relogin_via_tg: 1, 100 | 101 | }, 102 | misc: { 103 | wxDownloadDir: "", // Like C:\...\WeChat Files\wxid_*****\FileStorage\File, no trailing \ 104 | /* ------------ [ Delivery Options ] ------------ */ 105 | 106 | // s=false, no delivery 107 | // s=true, send to Push channel [defined in 'root.class'] 108 | // s=, send to this target 109 | deliverPushMessage: true, 110 | 111 | // This option defines where WeChat stickers will settle down. 112 | // It is recommended to create another new group chat to hold it. 113 | // as there are additional information, this section can NOT be set to . 114 | // either an object like below, or a simple . 115 | deliverSticker: { 116 | tgid: -100000, threadId: 777, 117 | urlPrefix: "https://t.me/c/000/777/", 118 | }, 119 | 120 | // 0, no advance (default); 1, only when not being filtered; 2, apply on all room chats 121 | deliverRoomRedPacketInAdvance: 2, 122 | 123 | /* ------------ [ Merging Options ] ------------ */ 124 | 125 | // define how many seconds between this and last msg, to stop merging 126 | mergeResetTimeout: { 127 | forPerson: 20, 128 | forGroup: 80, 129 | }, 130 | // this option defines how many messages should be merged into single TG message at most, 131 | // in time span(started from the first message), media count(as so many media will let the text going far away), 132 | // and total message count. When any of them matched, the merge will be restarted. 133 | onceMergeCapacity: { 134 | timeSpan: 15 * 60 * 60, 135 | mediaCount: 5, 136 | messageCount: 50, 137 | }, 138 | 139 | /* ------------ [ Debug Options ] ------------ */ 140 | 141 | // If you want to use /eval in tg commands, then set this to . 142 | // WARNING this may be a security risk, as it allows arbitrary code execution. 143 | debug_evalEnabled: false, 144 | 145 | // Set either to display related message about your ctToken, set to 0 to depress. 146 | display_ctToken_info: 1, 147 | 148 | // The level of debug timers you want to see in console, which are used to measure operation time. 149 | // Currently only 1 or 0 is accepted. 150 | debug_add_console_timers: 1, 151 | // Show additional processing intermediates in logfile. Default off to reduce user disk I/Os. 152 | debug_show_additional_log: 0, 153 | 154 | // Deliver logger-produced error to tg default channel. 0=disable, 1=only errors, 2=error+warn 155 | deliverLogToTG: 0, 156 | 157 | /* ------------ [ ] ------------ */ 158 | 159 | // If set to , all post message (from subscribed official account) won't be copied to log, 160 | // as only a single post would take up to 40KB in log file. 161 | // If you have spare disk space, why not keep these stuff? [lol] 162 | savePostRawDataInDetailedLog: false, 163 | 164 | // This option is designed to separate authentic links from fake links like Sticker Pointer. 165 | // -1: no addition; 0: only add to wx Link; 1: add to wx Link and text link 166 | addHashCtLinkToMsg: -1, 167 | 168 | // This option defines whether to display the description of a card URL message(set from sender phone) after the URL. 169 | showCardDescAfterUrl: 1, 170 | 171 | // This option defines how the program behaviors when it encounters unrecognized tg command. 172 | // 1: will be sent to your chat peer directly; 0: do nothing and won't be sent to WeChat. 173 | // PS: If you always click button or link to use tg commands rather than typing them, then set to 1 to avoid your messages started with '/' not being delivered. 174 | passUnrecognizedCmdNext: 0, 175 | 176 | // This option defined what service should be used to convert tg_sticker.webp to gif 177 | // 0 means bypass and will send .webp directly to WeChat; 178 | // 1 means using upyun Object Storage as image converter, not suggested by now; 179 | // 2 means using local node module 'sharp' to convert. 180 | // Note that this module requires Node.js(^18.17.0 or >= 20.3.0) and libvips, which may be unavailable for some users. 181 | // So we offered a switch here. And, as for now, when sharp is not available, we will fall back to {1}. 182 | service_type_on_webp_conversion: 2, 183 | 184 | // This option defines whether to add a time-based identifier to media messages inside a merged message. 185 | // 0 means disable; 1 means only for group chats; 2 means for all chats. 186 | add_identifier_to_merged_image: 1, 187 | 188 | // This option defines an array of group names, messages in each of them will be delivered to telegram, 189 | // without checking if the message was sent by yourself on other devices. 190 | // By using this, you can create a 'filehelper' group chat manually. 191 | wechat_synced_group: [], 192 | 193 | // This option defines whether to keep the help text (by /help tg command), 194 | // after a command is sucessfully delivered. By default, it would be deleted to keep your default channel clean. 195 | // (I don't know if you need this, so it's off by default >_< ) 196 | keep_help_text_after_command_received: 0, 197 | 198 | // If it is 1, then these voice messages sent by you via your mobile WeChat will not be skipped. 199 | do_not_skip_voice_from_mobile_wx: 1, 200 | 201 | 202 | /////////--------[ Advanced or deprecated Setting, less need to edit ]--------////////// 203 | 204 | // This option defines whether to keep the file placeholder message (wait for user action on downloading) after a file is successfully uploaded to TG. 205 | remove_file_placeholder_msg_after_success: 1, 206 | 207 | // Interval between each automatic status report [to Console]. 208 | status_report_interval: 4 * 3600, 209 | // How many 5-seconds should system wait before auto cancel /drop_on command. 210 | keep_drop_on_x5s: 100, 211 | // This variable is deprecated, therefore not recommended to change. 212 | addSelfReplyTimestampToRoomMergedMsg: false, 213 | // This option is also limited by TG bot API, so cannot be much larger. 214 | wxAutoDownloadSizeThreshold: 30 * 1048576, 215 | tgCmdPlaceholder: `Start---\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nStop----`, 216 | 217 | enableInlineSearchForUnreplaced: true, 218 | // Determine whether the first item in a merged person msg should have a title of timestamp, 219 | // be like: [11:00:00] a \n[11:00:02] b 220 | PutStampBeforeFirstMergedMsg: false, 221 | }, 222 | chatOptions: { 223 | // this section declares default behaviors of chats when not specified in C2C flag. 224 | // Notice: for boolean variables, set to exactly 0 to explicitly disable! 225 | 226 | // whether accept *-prefix in TG message as indicator of not forwarding to WX 227 | "mixed": 1, 228 | // whether merge WX-side messages by default 229 | "merge": 1, 230 | // whether skip all sticker delivery by default 231 | "skipSticker": 0, 232 | // which name should be used as title of a person in a room chat, 233 | // 0 means their WeChat name, 1 means your alias for talker, 2 means their group alias. 234 | "nameType": 1, 235 | // should a C2C chat only be a copy of WX-side message, which stops forwarding any TG msg to WX. 236 | "onlyReceive": 0, 237 | 238 | 239 | // # # # # # # [Below are special flags to be used at C2Cs only, which shall not be modified here.] # # # # # # 240 | 241 | 242 | // If turned on, member names in messages would be removed when delivering from wx to tg. 243 | // Then it will look like all messages are sent from a person rather than a group. (Maybe alike tg's Remain Anonymous?) 244 | "hideMemberName": 0, // Incomplete 245 | }, 246 | mods: { 247 | keepalive: { 248 | switch: "off", 249 | 250 | // version 1 of trigger rule, which is used to check if the bot is still alive. 251 | trigger_v1: [ 252 | // The timespan boundary should be calculated with your own situation; 253 | // just find when your 1st message (any in wx) of a day was received. 254 | // If there is no early messages, you could subscribe to a news Official Account, and then you'll have some push messages in the early morning. 255 | // During the timespan, if the program didn't receive any message within ${max_idle_minutes} minutes, then some check measures will apply. 256 | {start: "7:00", end: "17:00", max_idle_minutes: 70}, 257 | ], 258 | check_byAvatarUrl: { 259 | switch: "on", 260 | // Wait to retrieve what the bad avatar is. 261 | }, 262 | check_bySendMsg: { 263 | switch: "on", 264 | sendTarget: "微信支付", 265 | sendContents: ["你好", "今天天气如何", "hello"], // Welcome to contribute here! 266 | // wait 20 seconds to check if there is a response from ${sendTarget}. 267 | watchTimeRange_sec: 20, 268 | }, 269 | // This option defines how many hours should the program pauses the keepalive check, 270 | // after a user interference letting the bot online again. Just keep recommended value is probably ok. 271 | // Normally if you did not use mobile WeChat for 24-48 hours, then disconnection would happen. 272 | pauseCheckAfterResume_hr: 20, 273 | }, 274 | 275 | }, 276 | rules: { 277 | // Rules are defined here. 278 | "example1": { 279 | "autoApply": [1, 0], // Person Chat as [0], Group as [1] 280 | "if_wx_msg_contains": /^[,,][??]$/g, 281 | "do_wx_reply_gif": "gif/ct-affirmative.gif", 282 | } 283 | }, 284 | c11n: { // customization 285 | 286 | // 🖇🧷💬 (Quoted "${content}" of ${from}) 287 | wxQuotedMsgSuffixLine: (from, content) => `(${from}💬${content})\``, 288 | // Define what prefix should be added to each merged msg item. 289 | // s=false, no title-changing; 290 | // s=, would be executed with parameter 'count' and taken return value 291 | titleForSameTalkerInMergedRoomMsg: c => `${c}|→ `, 292 | 293 | // For person chat, if wx-side msg have quoted msg, then we'll use these two nickname to replace the raw contact name. 294 | // Or, set this to null, to disable this feature. 295 | quotedMsgSuffixLineInPersonChat: ["YOU", "ta"], 296 | 297 | officialAccountParser: a => `[Official Account Card]${a.nickname} , from ${a.province} ${a.city}, operator ${a.certinfo || ""}`, 298 | personCardParser: a => `📇[Person Card]${a.nickname} , from ${a.province} ${a.city}, sex ${a.sex === 1 ? "Male" : (a.sex === 0 ? "(Female)?" : "")}`, 299 | 300 | // what nickname will system message use in group show up, like tickle message. 301 | systemMsgTitleInRoom: "(System)", 302 | // If a sticker with former delivery found, then run this func to get formatted text. 303 | stickerWithLink: (url_p, flib, md5) => flib.hint ? 304 | `🌁(${md5}) ${flib.hint}` : `🌁(${md5})`, 305 | stickerSkipped: md5 => `[Sticker](${md5})`, 306 | // What should display when new topic created automatically. 307 | newTopicCreated: () => `📌Topic Created.\nYour conversation starts here.`, 308 | 309 | // better keep an extra space at the end, if `add_identifier_to_merged_image` is on. 310 | C2C_group_mediaCaption: name => `from [${name}] `, 311 | 312 | tgTextQuoteAddition: (quoted, original) => `(回复「${quoted}」)\n${original}`, 313 | 314 | // If you want to override /help return text, change this to a function like common.js/TGBotHelpCmdText. 315 | // Please remind if you do so, then for new commands you must add them manually to /help text. 316 | override_help_text: false, 317 | 318 | // System notifications 319 | 320 | // When sending wx login QRCode to tg, this text is used: 321 | wxLoginQRCodeHint: "Please scan this QRCode to login WeChat.", 322 | 323 | // If you want to disable any of these replacements here, 324 | // please search for 'secret.misc.titles' in BotIndex.js and put corresponding 325 | // original text here (wrapped with []), to disable replacement here. 326 | unsupportedSticker: "{-🧩-}", 327 | recvCall: "{📞📲}", 328 | recvSplitBill: "{💰✂️📥, 👋}", 329 | recvTransfer: "{💰📥}", 330 | acceptTransfer: "{💰📥, ✅}", 331 | msgTypeNotSupported: "{📩❎, 👉📱}", 332 | }, 333 | txyun: { 334 | switch: "off", 335 | secretId: "---", 336 | secretKey: "---", 337 | }, 338 | upyun: { 339 | switch: "off", 340 | password: "----", 341 | webFilePathPrefix: "/Bucket____name/ctBotAsset/stickerTG", 342 | operatorName: "----", 343 | urlPrefix: "https://---.test.upcdn.net", 344 | urlPathPrefix: "/ctBotAsset/stickerTG" 345 | } 346 | }; -------------------------------------------------------------------------------- /src/tgProcessor.js: -------------------------------------------------------------------------------- 1 | // noinspection JSUnreachableSwitchBranches 2 | 3 | const dayjs = require("dayjs"); 4 | const {tgBotDo} = require("./init-tg"); 5 | const secret = require("../config/confLoader"); 6 | const nativeEmojiMap = require('../config/native_emoji_map.js'); 7 | const sharp = require("sharp"); 8 | const {PassThrough} = require("node:stream"); 9 | const fs = require("node:fs"); 10 | 11 | let env; 12 | 13 | // async function a() { 14 | // const {} = env; 15 | // } 16 | 17 | async function mergeToPrev_tgMsg(msg, isGroup, content, name = "", isText) { 18 | const {state, defLogger, tgBotDo, secret} = env; 19 | // Time-based identifier 20 | const timed_id = Date.now().toString(16).slice(-5, -1); 21 | if (!isText) { 22 | const DTypeName = ((value) => { 23 | const DTypes = {Image: 2, Audio: 3, File: 5, Push: 6}; 24 | for (const name in DTypes) if (DTypes[name] === value) return name; 25 | return "Media"; 26 | })(msg.DType); 27 | // Temporary override 'content' to inject into merged msg in this function 28 | if ((secret.misc.add_identifier_to_merged_image - !isGroup) && DTypeName === "Image") { 29 | content = `[${DTypeName}] %${timed_id}`; 30 | defLogger.trace(`[${DTypeName}] %${timed_id} is added to content.`); 31 | msg.media_identifier = timed_id; 32 | } else if (DTypeName === "File") content = `[${DTypeName}] ${msg.payload.filename}`; 33 | else content = `[${DTypeName}]`; 34 | } 35 | const word = isGroup ? "Room" : "Person"; 36 | const _ = isGroup ? state.preRoom : state.prePerson; 37 | // the 'newFirstTitle' is 0 when inside C2C 38 | const newFirstTitle = (msg.receiver.wx) ? 0 : (isGroup ? _.topic : msg.dname); 39 | const who = isGroup ? `${name}/${_.topic}` : name; 40 | const newItemTitle = (() => { 41 | const s = secret.c11n.titleForSameTalkerInMergedRoomMsg; 42 | if (s === false || (isGroup && _.lastTalker !== name)) { 43 | _.talkerCount = 0; 44 | _.lastTalker = name; 45 | const notDropTitle = secret.misc.PutStampBeforeFirstMergedMsg || isGroup; 46 | return notDropTitle ? `[${isGroup ? msg.dname : dayjs().format("H:mm:ss")}]` : ''; 47 | } 48 | _.talkerCount++; 49 | if (typeof s === "function") return s(_.talkerCount); 50 | defLogger.error(`Invalid configuration found for {settings.c11n.titleForSameTalkerInMergedRoomMsg}!`); 51 | return `|→ `; 52 | })(); 53 | msg[`pre${word}NeedUpdate`] = false; 54 | content = filterMsgText(content, {isGroup, peerName: name}); 55 | // from same talker check complete, ready to merge 56 | if (_.firstWord === "") { 57 | // Already merged, so just append newer to last 58 | const newString = `${_.msgText}\n${newItemTitle} ${content}`; 59 | _.msgText = newString; 60 | _.tgMsg = await tgBotDo.EditMessageText(newString, _.tgMsg, _.receiver); 61 | // defLogger.debug(`Merged msg from ${word}: ${who}, "${content}" into former.`); 62 | defLogger.debug(`(${who}) 🔗+ -->📂: "${content}"`); 63 | return isText; // !isText?false:true 64 | } else { 65 | // Ready to modify first msg, refactoring it. 66 | ///* newFirstTitle = 0 --> C2C msg, do not need header */ 67 | const newString = (newFirstTitle === 0 ? `` : `📨⛓️ [#${newFirstTitle}] - - - -\n`) + 68 | `${_.firstWord}\n${newItemTitle} ${content}`; 69 | _.msgText = newString; 70 | _.firstWord = ""; 71 | _.tgMsg = await tgBotDo.EditMessageText(newString, _.tgMsg, _.receiver); 72 | // Ref: wxLogger.debug(`📥WX(${tmplc})\t--[Text]-->TG, "${content}".`); 73 | defLogger.debug(`(${who}) 🔗---->📂: "${content}"`); 74 | //defLogger.debug(`Merged msg from ${word}: ${who}, "${content}" into first.`); 75 | return isText; 76 | } 77 | } 78 | 79 | async function replyWithTips(tipMode = "", target = null, timeout = 6, additional = null) { 80 | const {tgLogger, state, defLogger, tgBotDo} = env; 81 | let message = "", form = {}; 82 | switch (tipMode) { 83 | // cannot use this now! 84 | // case "needRelogin": 85 | // message = `Your WX credential expired, please refer to log or go with this [QRServer] link:\n${additional}`; 86 | // timeout = 180; 87 | // break; 88 | case "globalCmdToC2C": 89 | message = `You sent a global command to a C2C chat. The operation has been blocked and please check.`; 90 | break; 91 | case "replyCmdToNormal": 92 | message = `Invalid pointer! Are you missing target for this command? `; 93 | break; 94 | case "C2CNotFound": 95 | message = `Your C2C peer could not be found. Please Check!`; 96 | break; 97 | case "wrongMYSTAT_setter": 98 | message = `You sent a global command to a C2C chat. The operation has been blocked and please check.`; 99 | break; 100 | case "mystat_changed": 101 | message = `Changed myStat into ${additional}.`; 102 | break; 103 | case "lockStateChange": 104 | message = `Now conversation lock state is ${additional}.`; 105 | break; 106 | case "softReboot": 107 | message = `Soft Reboot Successful.\nReason: ${additional}`; 108 | form = {reply_markup: {}}; 109 | break; 110 | case "nothingToDo": 111 | message = `Nothing to do upon your message, ${target}`; 112 | break; 113 | case "dropCmdAutoOff": 114 | message = `The 'drop' lock has been on for ${secret.misc.keep_drop_on_x5s * 5}s, thus been switched off automatically.`; 115 | break; 116 | case "audioProcessFail": 117 | message = `Audio transcript request received, But error occurred when processing.`; 118 | break; 119 | case "alreadySetStickerHint": 120 | message = `Successfully set hint for Sticker (${additional})!`; 121 | break; 122 | case "notEnabledInConfig": 123 | message = `One or more action interrupted as something is not configured properly. See log for detail.`; 124 | break; 125 | case "setMediaSpoilerFail": 126 | message = `Error occurred while setting spoiler for former message :\n${additional} `; 127 | break; 128 | case "setAsLastAndLocked": 129 | message = `Already set '${additional}' as last talker and locked.`; 130 | break; 131 | case "autoCreateTopicFail": 132 | message = `Attempt of '/create_topic' failed.\t Reason: ${additional}.`; 133 | timeout = 60; 134 | break; 135 | case "autoCreateTopicSuccess": 136 | message = `Successfully created topic. \n${additional}`; 137 | break; 138 | case "genericFail": 139 | message = `Your action is not completed due to some errors. ${additional ?? ""}`; 140 | timeout = 60; 141 | break; 142 | default: 143 | tgLogger.error(`Wrong call of tg replyWithTips() with invalid 'tipMode'. Please check arguments.\n${tipMode}\t${target}`); 144 | return; 145 | } 146 | try { 147 | const tgMsg = await tgBotDo.SendMessage(target, message, true, "HTML", form); 148 | defLogger.info(`Sent out following tips: {${message}}`); 149 | if (timeout !== 0) { 150 | tgLogger.debug(`Added message #${tgMsg.message_id} to poolToDelete with timer (${timeout})sec.`); 151 | state.poolToDelete.push({tgMsg: tgMsg, toDelTs: (dayjs().unix()) + timeout, receiver: target}); 152 | } 153 | } catch (e) { 154 | defLogger.warn(`Sending Tip failed in post-check, please check!`); 155 | } 156 | // if (timeout !== 0) state.poolToDelete.add(tgMsg, timeout); 157 | 158 | } 159 | 160 | async function addSelfReplyTs(name = null) { 161 | const {processor, state, defLogger, secret} = env; 162 | if (name === null) name = state.last.name; 163 | if (isPreRoomValid(state.preRoom, name, false, secret.misc.mergeResetTimeout.forGroup) && state.preRoom.firstWord === "") { 164 | // preRoom valid and already merged (more than 2 msg) 165 | const _ = state.preRoom; 166 | const newString = `${_.msgText}\n← [${dayjs().format("H:mm:ss")}] {My Reply}`; 167 | if (secret.misc.addSelfReplyTimestampToRoomMergedMsg) { 168 | _.tgMsg = await tgBotDo.EditMessageText(newString, _.tgMsg, _.receiver); 169 | defLogger.debug(`Delivered myself reply stamp into Room:${_.topic} 's former message, and cleared its preRoom.`); 170 | } 171 | // at first this function is used to add reply timestamp on merged msg when user reply, but it became a resetter for merge 172 | // after user reply. now because of a neglect, the preRoom have no 'stat', which will cause a bug. 173 | state.preRoom = { 174 | firstWord: "", 175 | tgMsg: null, 176 | topic: "", 177 | msgText: "", 178 | lastTalker: "", 179 | stat: { 180 | "tsStarted": 0, 181 | "mediaCount": 0, 182 | "messageCount": 0, 183 | }, 184 | }; 185 | } else { 186 | if (secret.misc.addSelfReplyTimestampToRoomMergedMsg) defLogger.debug(`PreRoom not valid, skip delivering myself reply stamp into former message.`); 187 | } 188 | } 189 | 190 | async function filterPhoto(path) { 191 | const {defLogger} = env; 192 | const metadata = await sharp(path).metadata(); 193 | let {width, height} = metadata; 194 | let resizeOptions = null; 195 | 196 | // Check aspect ratio --- stat 2 197 | const ratio = width / height; 198 | if (ratio > 4 || ratio < 1 / 4) { 199 | 200 | defLogger.debug(`Photo [${width}x${height}, ${ratio.toFixed(2)}] exceeds ratio limit 3. Sending as file instead.`); 201 | return {stat: 2, stream: fs.createReadStream(path)}; 202 | } 203 | 204 | // Check if width + height exceeds 8000px --- stat 1 205 | if (width + height > 8000) { 206 | const scale = 8000 / (width + height); 207 | resizeOptions = { 208 | width: Math.round(width * scale), 209 | height: Math.round(height * scale), 210 | fit: "inside", 211 | }; 212 | 213 | defLogger.debug(`Shrinking photo [${width}x${height}] to [${resizeOptions.width}x${resizeOptions.height}]`); 214 | const passThrough = new PassThrough(); 215 | const pipeline = sharp(path); 216 | if (resizeOptions) { 217 | pipeline.resize(resizeOptions); 218 | } 219 | pipeline.pipe(passThrough); 220 | return {stat: 1, stream: passThrough}; 221 | } 222 | 223 | return {stat: 0, stream: fs.createReadStream(path)}; 224 | } 225 | 226 | function filterMsgText(inText, args = {}) { 227 | const {state, defLogger} = env; 228 | let txt = inText; 229 | let appender = ""; 230 | txt = txt.replaceAll("
", "\n"); 231 | 232 | { // Emoji dual processor 233 | 234 | // Process qqemoji (WeChat exclusive emoji) 235 | let qqemojiRegex = //g; 236 | txt = txt.replace(qqemojiRegex, (match, emojiId, text) => { 237 | text = text.replace('_web', ''); 238 | return text; 239 | }); 240 | 241 | // Process emoji (WeChat modified, native emoji) 242 | let flag = 0; 243 | let emojiRegex = //g; 244 | txt = txt.replace(emojiRegex, (match, emojiId, text) => { 245 | flag = 1; 246 | return `[emoji${emojiId}]`; // Replace with bracketed form 247 | }); 248 | 249 | // Iterate over nativeEmojiMap and replace bracketed emojis 250 | if (flag) { 251 | const timerLabel = `wx Emoji processor | #${process.uptime().toFixed(2)} used`; 252 | console.time(timerLabel); 253 | for (let key in nativeEmojiMap) { 254 | // Regexp is much slower than regular replacement! 255 | // In my test on a 10th gen-i5 machine, it takes 42s to complete a single check. 256 | // ####################let regex = new RegExp(key, 'g'); 257 | const val = nativeEmojiMap[key][0] 258 | txt = txt.replaceAll(key, val); 259 | // This logging below causes many useless logs in logfile! removing. 260 | // defLogger.trace(`[Verbose] replaced '${key}' to '${val}' in WX message.`); 261 | } 262 | console.timeEnd(timerLabel); 263 | } 264 | } // END: Emoji dual processor 265 | 266 | 267 | // process quoted message 268 | if (/「(.{1,20}):\n?([\s\S]*)」\n- - - - - - - - - - - - - - -\n/.test(txt)) { 269 | // Filter Wx ReplyTo / Quote Parameter: (quote-ee name must within [1,10]) 270 | const match = txt.match(/「(.{1,20}):\n?([\s\S]*)」\n- - - - - - - - - - - - - - -\n/); 271 | // 0 is all match, 1 is orig-msg sender, 2 is orig-msg 272 | const origMsgClip = (match[2].length > 8) ? match[2].substring(0, 8) : match[2]; 273 | // In clip, we do not need
to be revealed 274 | const origMsgClip2 = origMsgClip.replaceAll("\n", " "); 275 | txt = txt.replace(match[0], ``); 276 | // to let this not escaped by "Filter <> for recaller" 277 | 278 | if (args.peerName && !args.isGroup) { 279 | // P2P chat, not group, applying quote replacement 280 | const conf1 = secret.c11n.quotedMsgSuffixLineInPersonChat; 281 | if (secret.misc.debug_show_additional_log) defLogger.trace(`#23382 Quoted message name debug: ${match[1]} / ${state.s.selfName} / ${args.peerName}`); 282 | if (match[1] === state.s.selfName) match[1] = conf1 ? conf1[0] : match[1]; 283 | if (match[1] === args.peerName) match[1] = conf1 ? conf1[1] : match[1]; 284 | } 285 | 286 | appender += `\n` + secret.c11n.wxQuotedMsgSuffixLine(match[1], origMsgClip2); 287 | } 288 | // if (txt.includes("
")) { 289 | // // Telegram would not accept this tag in all mode! Must remind. 290 | // tgLogger.warn(`Unsupported
tag found and cleared. Check Raw Log for reason!`); 291 | // txt = txt.replaceAll("
", "\n"); 292 | // } 293 | 294 | // Filter <> for recaller! 295 | // txt = txt.replaceAll("<", "<").replaceAll(">", ">"); 296 | // This function helps reduce the possibility of mistaken substitution 297 | txt = (t => { 298 | // Improved regular expression to support Chinese characters. 299 | // noinspection RegExpUnnecessaryNonCapturingGroup 300 | const tagRegex = /<\/?([\w\u4e00-\u9fff]+)(?:\s+[\w\-.:]+\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))*\s*\/?>/g; 301 | // Replace all HTML entities with &__; except excluded tags. 302 | return t.replace(tagRegex, (match, tagName) => { 303 | const isExcludedTag = ['a', 'b', 'i', 'u', 's', 'code', 'blockquote'].includes(tagName.toLowerCase()); 304 | if (!isExcludedTag) { 305 | // Complete tag with non-excluded tag name, encode it. 306 | return match.replace(//g, '>'); 307 | } 308 | // Excluded tag, leave it unchanged. 309 | return match; 310 | }); 311 | })(txt); 312 | return txt + appender; 313 | } 314 | 315 | 316 | function isSameTGTarget(in1, in2) { 317 | const {secret} = env; 318 | const parser = in0 => { 319 | // in <-- / secret.class.def ( msg.receiver ) 320 | if (in0.tgid) { 321 | // if (in0.threadId) return [in0.tgid, 1, in0.threadId]; 322 | // else return [in0.tgid, 0]; 323 | return in0; 324 | } 325 | // in <-- tgMsg.matched <-- s=?, p={} 326 | if (typeof in0.s === "number") {/* s may be 0 so must do like this! */ 327 | if (in0.s === 1) return in0.p; 328 | else if (in0.s === 0) return secret.class.def; 329 | } 330 | }; 331 | const p1 = parser(in1), p2 = parser(in2); 332 | // thread verify maybe fixed here 333 | if (p1.tgid === p2.tgid) { 334 | if (!p1.threadId && !p2.threadId) return true; 335 | return p1.threadId === p2.threadId; 336 | 337 | } 338 | } 339 | 340 | function isPreRoomValid(preRoomState, targetTopic, forceMerge = false, timeout) { 341 | const {secret, tgLogger} = env; 342 | try { 343 | const _ = preRoomState; 344 | // noinspection JSUnresolvedVariable 345 | const lastDate = (_.tgMsg) ? (_.tgMsg.edit_date || _.tgMsg.date) : 0; 346 | const nowDate = dayjs().unix(); 347 | if (_.topic === targetTopic && (nowDate - lastDate < timeout || forceMerge)) { 348 | // Let's continue check for 'onceMergeCapacity' 349 | const exist = _.stat, limit = secret.misc.onceMergeCapacity; 350 | if (process.uptime() - exist.tsStarted > limit.timeSpan) { 351 | tgLogger.debug(`[Merge] time span reached limit, resetting merge...`); 352 | return false; 353 | } 354 | if (exist.mediaCount >= limit.mediaCount) { 355 | tgLogger.debug(`[Merge] mediaCount reached limit, resetting merge...`); 356 | return false; 357 | } 358 | if (exist.messageCount >= limit.messageCount) { 359 | tgLogger.debug(`[Merge] messageCount reached limit, resetting merge...`); 360 | return false; 361 | } 362 | return true; 363 | } else return false; 364 | } catch (e) { 365 | // console.error(`Maybe bug here!`); 366 | tgLogger.error(`Error occurred while validating preRoomState.\n\t${e.toString()}`); 367 | return false; 368 | } 369 | } 370 | 371 | module.exports = (incomingEnv) => { 372 | env = incomingEnv; 373 | return { 374 | addSelfReplyTs, 375 | replyWithTips, 376 | mergeToPrev_tgMsg, 377 | isSameTGTarget, 378 | filterMsgText, 379 | isPreRoomValid, 380 | filterPhoto 381 | }; 382 | }; 383 | -------------------------------------------------------------------------------- /wcferry-puppet-c/index.d.mts: -------------------------------------------------------------------------------- 1 | import * as _wechatferry_core from '@wechatferry/core'; 2 | import { WechatMessageType, WechatAppMessageType } from '@wechatferry/core'; 3 | import * as unstorage from 'unstorage'; 4 | import { Storage, StorageValue } from 'unstorage'; 5 | import { Room, RoomMember, Contact, Message } from 'wechaty-puppet/payloads'; 6 | import { WechatferryAgent, WechatferryAgentEventMessage, WechatferryAgentContact, WechatferryAgentChatRoomMember, WechatferryAgentChatRoom, WechatferryAgentDBMessage } from '@wechatferry/agent'; 7 | import * as PUPPET from 'wechaty-puppet'; 8 | import { FileBoxInterface } from 'file-box'; 9 | import { ParserOptions } from 'xml2js'; 10 | 11 | interface PuppetRoom extends Room { 12 | announce: string; 13 | members: RoomMember[]; 14 | } 15 | interface PuppetContact extends Contact { 16 | tags: string[]; 17 | } 18 | type PuppetMessage = Message & { 19 | isRefer: boolean; 20 | }; 21 | interface PuppetWcferryUserOptions { 22 | agent?: WechatferryAgent; 23 | /** 24 | * unstorage 实例,用于缓存数据 25 | */ 26 | storage?: Storage; 27 | } 28 | interface PuppetWcferryOptions extends Required { 29 | } 30 | 31 | declare function resolvePuppetWcferryOptions(userOptions: PuppetWcferryUserOptions): PuppetWcferryOptions; 32 | declare class WechatferryPuppet extends PUPPET.Puppet { 33 | static readonly VERSION: string; 34 | agent: WechatferryAgent; 35 | private cacheManager; 36 | private heartBeatTimer?; 37 | constructor(options?: PuppetWcferryUserOptions); 38 | name(): string; 39 | version(): string; 40 | onStart(): Promise; 41 | login(userId: string): void; 42 | onStop(): Promise; 43 | ding(data?: string): Promise; 44 | onMessage(message: WechatferryAgentEventMessage): Promise; 45 | private lastSelfMessageId; 46 | onSendMessage(timeout?: number): Promise; 47 | contactSelfQRCode(): Promise; 48 | contactSelfName(name: string): Promise; 49 | contactSelfSignature(signature: string): Promise; 50 | contactAlias(contactId: string): Promise; 51 | contactAlias(contactId: string, alias: string | null): Promise; 52 | contactPhone(contactId: string): Promise; 53 | contactPhone(contactId: string, phoneList: string[]): Promise; 54 | contactCorporationRemark(contactId: string, corporationRemark: string): Promise; 55 | contactDescription(contactId: string, description: string): Promise; 56 | contactList(): Promise; 57 | contactAvatar(contactId: string): Promise; 58 | contactAvatar(contactId: string, file: FileBoxInterface): Promise; 59 | contactRawPayloadParser(payload: WechatferryAgentContact): Promise; 60 | contactRawPayload(id: string): Promise; 61 | conversationReadMark(conversationId: string, hasRead?: boolean | undefined): Promise; 62 | messageContact(messageId: string): Promise; 63 | messageImage(messageId: string, imageType: PUPPET.types.Image): Promise; 64 | messageRecall(messageId: string): Promise; 65 | messageFile(messageId: string): Promise; 66 | messageUrl(messageId: string): Promise; 67 | messageLocation(messageId: string): Promise; 68 | messageMiniProgram(messageId: string): Promise; 69 | messageRawPayloadParser(payload: WechatferryAgentEventMessage): Promise; 70 | messageRawPayload(id: string): Promise; 71 | messageSendText(conversationId: string, text: string): Promise; 72 | messageSendFile(conversationId: string, file: FileBoxInterface): Promise; 73 | messageSendContact(conversationId: string, contactId: string): Promise; 74 | messageSendUrl(conversationId: string, urlLinkPayload: PUPPET.payloads.UrlLink): Promise; 75 | messageSendLocation(conversationId: string, locationPayload: PUPPET.payloads.Location): Promise; 76 | messageSendMiniProgram(conversationId: string, miniProgramPayload: PUPPET.payloads.MiniProgram): Promise; 77 | messageForward(conversationId: string, messageId: string): Promise; 78 | roomList(): Promise; 79 | roomCreate(contactIdList: string[], topic?: string | undefined): Promise; 80 | roomQuit(roomId: string): Promise; 81 | roomAdd(roomId: string, contactId: string): Promise; 82 | roomDel(roomId: string, contactId: string): Promise; 83 | roomAvatar(roomId: string): Promise; 84 | roomTopic(roomId: string): Promise; 85 | roomTopic(roomId: string, topic: string): Promise; 86 | roomQRCode(roomId: string): Promise; 87 | roomAnnounce(roomId: string): Promise; 88 | roomAnnounce(roomId: string, text: string): Promise; 89 | roomInvitationAccept(roomInvitationId: string): Promise; 90 | roomInvitationRawPayload(roomInvitationId: string): Promise; 91 | roomInvitationRawPayloadParser(rawPayload: any): Promise; 92 | roomMemberList(roomId: string): Promise; 93 | roomMemberRawPayloadParser(rawPayload: WechatferryAgentChatRoomMember): Promise; 94 | roomMemberRawPayload(roomId: string, contactId: string): Promise; 95 | roomRawPayloadParser(payload: WechatferryAgentChatRoom): Promise; 96 | roomRawPayload(id: string): Promise; 97 | friendshipSearchPhone(phone: string): Promise; 98 | friendshipSearchWeixin(weixin: string): Promise; 99 | friendshipAdd(contactId: string, hello: string): Promise; 100 | friendshipAccept(friendshipId: string): Promise; 101 | friendshipRawPayloadParser(rawPayload: any): Promise; 102 | friendshipRawPayload(id: string): Promise; 103 | tagContactAdd(tagId: string, contactId: string): Promise; 104 | tagContactRemove(tagId: string, contactId: string): Promise; 105 | tagContactDelete(tagId: string): Promise; 106 | tagContactList(contactId?: string): Promise; 107 | postPublish(payload: PUPPET.payloads.Post): Promise; 108 | postSearch(filter: PUPPET.filters.Post, pagination?: PUPPET.filters.PaginationRequest): Promise>; 109 | postRawPayloadParser(rawPayload: WechatferryAgentEventMessage): Promise; 110 | postRawPayload(postId: string): Promise<_wechatferry_core.WxMsg>; 111 | tap(postId: string, type?: PUPPET.types.Tap, tap?: boolean): Promise; 112 | tapSearch(postId: string, query?: PUPPET.filters.Tap, pagination?: PUPPET.filters.PaginationRequest): Promise>; 113 | private getRoomPayload; 114 | private getMessagePayload; 115 | private getContactPayload; 116 | updateContactCache(contactId: string, _contact?: PuppetContact): Promise; 117 | private updateRoomCache; 118 | /** 119 | * 更新群聊成员列表缓存 120 | * 121 | * @description 主要用于 room-join 事件前获取新加群的成员 122 | * @deprecated 尽可能避免使用,优先使用 updateRoomMemberCache 123 | * @param roomId 群聊 id 124 | */ 125 | updateRoomMemberListCache(roomId: string): Promise; 126 | private updateRoomMemberCache; 127 | private loadContactList; 128 | private loadRoomList; 129 | private startPuppetHeart; 130 | private stopPuppetHeart; 131 | } 132 | declare module 'wechaty-puppet/payloads' { 133 | interface UrlLink { 134 | /** 左下显示的名字 */ 135 | name?: string; 136 | /** 公众号 id 可以显示对应的头像(gh_ 开头的) */ 137 | account?: string; 138 | } 139 | } 140 | 141 | declare function xmlToJson>(xml: string, options?: ParserOptions): Promise; 142 | declare function jsonToXml(data: Record): Promise; 143 | 144 | declare function isRoomId(id?: string): boolean; 145 | declare function isContactOfficialId(id?: string): boolean; 146 | declare function isContactCorporationId(id?: string): boolean; 147 | declare function isIMRoomId(id?: string): boolean; 148 | declare function isRoomOps(type: WechatMessageType): type is WechatMessageType.SysNotice | WechatMessageType.Sys; 149 | declare function isContactId(id?: string): boolean; 150 | 151 | type PrefixStorage = ReturnType>; 152 | declare function createPrefixStorage(storage: Storage, base: string): { 153 | getItemsMap: (base?: string) => Promise<{ 154 | key: string; 155 | value: T; 156 | }[]>; 157 | getItemsList(base?: string): Promise; 158 | hasItem: (key: string, opts?: unstorage.TransactionOptions) => Promise; 159 | getItem: (key: string, opts?: unstorage.TransactionOptions) => Promise; 160 | getItems: (items: (string | { 161 | key: string; 162 | options?: unstorage.TransactionOptions; 163 | })[], commonOptions?: unstorage.TransactionOptions) => Promise<{ 164 | key: string; 165 | value: U; 166 | }[]>; 167 | getItemRaw: (key: string, opts?: unstorage.TransactionOptions) => Promise<(T_1 extends any ? T_1 : any) | null>; 168 | setItem: (key: string, value: U, opts?: unstorage.TransactionOptions) => Promise; 169 | setItems: (items: { 170 | key: string; 171 | value: U; 172 | options?: unstorage.TransactionOptions; 173 | }[], commonOptions?: unstorage.TransactionOptions) => Promise; 174 | setItemRaw: (key: string, value: T_1 extends any ? T_1 : any, opts?: unstorage.TransactionOptions) => Promise; 175 | removeItem: (key: string, opts?: (unstorage.TransactionOptions & { 176 | removeMeta?: boolean; 177 | }) | boolean) => Promise; 178 | getMeta: (key: string, opts?: (unstorage.TransactionOptions & { 179 | nativeOnly?: boolean; 180 | }) | boolean) => unstorage.StorageMeta | Promise; 181 | setMeta: (key: string, value: unstorage.StorageMeta, opts?: unstorage.TransactionOptions) => Promise; 182 | removeMeta: (key: string, opts?: unstorage.TransactionOptions) => Promise; 183 | getKeys: (base?: string, opts?: unstorage.TransactionOptions) => Promise; 184 | clear: (base?: string, opts?: unstorage.TransactionOptions) => Promise; 185 | dispose: () => Promise; 186 | watch: (callback: unstorage.WatchCallback) => Promise; 187 | unwatch: () => Promise; 188 | mount: (base: string, driver: unstorage.Driver) => Storage; 189 | unmount: (base: string, dispose?: boolean) => Promise; 190 | getMount: (key?: string) => { 191 | base: string; 192 | driver: unstorage.Driver; 193 | }; 194 | getMounts: (base?: string, options?: { 195 | parents?: boolean; 196 | }) => { 197 | base: string; 198 | driver: unstorage.Driver; 199 | }[]; 200 | }; 201 | 202 | declare function mentionTextParser(message: string): { 203 | mentions: string[]; 204 | message: string; 205 | }; 206 | declare function getMentionText(mentions?: string[], chatroomMembers?: WechatferryAgentChatRoomMember[]): string; 207 | 208 | type Runner = () => Promise; 209 | declare function executeRunners(runners: Runner[]): Promise; 210 | 211 | interface AppAttachPayload { 212 | totallen?: number; 213 | attachid?: string; 214 | emoticonmd5?: string; 215 | fileext?: string; 216 | cdnattachurl?: string; 217 | aeskey?: string; 218 | cdnthumbaeskey?: string; 219 | encryver?: number; 220 | islargefilemsg: number; 221 | } 222 | interface ReferMsgPayload { 223 | type: string; 224 | svrid: string; 225 | fromusr: string; 226 | chatusr: string; 227 | displayname: string; 228 | content: string; 229 | } 230 | interface ChannelsMsgPayload { 231 | objectId: string; 232 | feedType: string; 233 | nickname: string; 234 | avatar: string; 235 | desc: string; 236 | mediaCount: string; 237 | objectNonceId: string; 238 | liveId: string; 239 | username: string; 240 | authIconUrl: string; 241 | authIconType: string; 242 | mediaList?: { 243 | media?: { 244 | thumbUrl: string; 245 | fullCoverUrl: string; 246 | videoPlayDuration: string; 247 | url: string; 248 | height: string; 249 | mediaType: string; 250 | width: string; 251 | }; 252 | }; 253 | megaVideo?: object; 254 | bizAuthIconType?: string; 255 | } 256 | interface MiniAppMsgPayload { 257 | username: string; 258 | appid: string; 259 | pagepath: string; 260 | weappiconurl: string; 261 | shareId: string; 262 | } 263 | interface AppMessagePayload { 264 | des?: string; 265 | thumburl?: string; 266 | title: string; 267 | url: string; 268 | appattach?: AppAttachPayload; 269 | channel?: ChannelsMsgPayload; 270 | miniApp?: MiniAppMsgPayload; 271 | type: WechatAppMessageType; 272 | md5?: string; 273 | fromusername?: string; 274 | recorditem?: string; 275 | refermsg?: ReferMsgPayload; 276 | } 277 | declare function parseAppmsgMessagePayload(messageContent: string): Promise; 278 | 279 | interface EmojiMessagePayload { 280 | type: number; 281 | len: number; 282 | md5: string; 283 | cdnurl: string; 284 | width: number; 285 | height: number; 286 | gameext?: string; 287 | } 288 | declare function parseEmotionMessagePayload(message: PUPPET.payloads.Message): Promise; 289 | 290 | declare function parseMiniProgramMessagePayload(message: PUPPET.payloads.Message): Promise; 291 | 292 | interface ContactCardXmlSchema { 293 | msg: { 294 | $: { 295 | bigheadimgurl: string; 296 | smallheadimgurl: string; 297 | username: string; 298 | nickname: string; 299 | fullpy: string; 300 | shortpy: string; 301 | alias: string; 302 | imagestatus: string; 303 | scene: string; 304 | province: string; 305 | city: string; 306 | sign: string; 307 | sex: string; 308 | certflag: string; 309 | certinfo: string; 310 | brandIconUrl: string; 311 | brandHomeUrl: string; 312 | brandSubscriptConfigUrl: string; 313 | brandFlags: string; 314 | regionCode: string; 315 | biznamecardinfo: string; 316 | antispamticket: string; 317 | }; 318 | }; 319 | } 320 | declare function parseContactCardMessagePayload(messageContent: string): Promise; 321 | declare function buildContactCardXmlMessagePayload(contact: PUPPET.payloads.Contact): Promise; 322 | 323 | declare function parseTimelineMessagePayload(messageXml: string): Promise<{ 324 | messages: _wechatferry_core.WxMsg[]; 325 | payload: PUPPET.payloads.PostServer; 326 | }>; 327 | 328 | declare function roomTopicParser(puppet: PUPPET.Puppet, message: WechatferryAgentEventMessage): Promise; 329 | 330 | declare function messageParser(_puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 331 | 332 | declare function roomInviteParser(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 333 | 334 | declare function roomJoinParser(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage, retries?: number): Promise; 335 | 336 | declare function roomLeaveParser(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 337 | 338 | declare function friendShipParser(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 339 | 340 | declare function postParser(_puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 341 | 342 | declare enum EventType { 343 | Message = 0, 344 | Post = 1, 345 | Friendship = 2, 346 | RoomInvite = 3, 347 | RoomJoin = 4, 348 | RoomLeave = 5, 349 | RoomTopic = 6 350 | } 351 | interface EventPayloadSpec { 352 | [EventType.Message]: WechatferryAgentEventMessage; 353 | [EventType.Post]: PUPPET.payloads.EventPost; 354 | [EventType.Friendship]: PUPPET.payloads.Friendship; 355 | [EventType.RoomInvite]: PUPPET.payloads.RoomInvitation; 356 | [EventType.RoomJoin]: PUPPET.payloads.EventRoomJoin; 357 | [EventType.RoomLeave]: PUPPET.payloads.EventRoomLeave; 358 | [EventType.RoomTopic]: PUPPET.payloads.EventRoomTopic; 359 | } 360 | interface Event { 361 | type: T; 362 | payload: EventPayloadSpec[T]; 363 | } 364 | type EventPayload = EventPayloadSpec[keyof EventPayloadSpec] | null; 365 | type EventParserHandler = (puppet: WechatferryPuppet, message: WechatferryAgentEventMessage) => Promise; 366 | declare function addEventParser(eventType: EventType, parser: EventParserHandler): void; 367 | declare function parseEvent(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise>; 368 | 369 | declare function wechatferryContactToWechaty(contact: WechatferryAgentContact): PUPPET.payloads.Contact; 370 | declare function wechatyContactToWechatferry(contact: PUPPET.payloads.Contact): WechatferryAgentContact; 371 | declare module 'wechaty-puppet/payloads' { 372 | interface Contact { 373 | tags: string[]; 374 | } 375 | } 376 | 377 | declare function wechatferryMessageToWechaty(puppet: PUPPET.Puppet, message: WechatferryAgentEventMessage): Promise; 378 | declare function wechatferryDBMessageToWechaty(puppet: PUPPET.Puppet, message: WechatferryAgentDBMessage): Promise; 379 | declare function wechatferryDBMessageToEventMessage(message: WechatferryAgentDBMessage): WechatferryAgentEventMessage; 380 | 381 | declare function wechatferryRoomToWechaty(contact: WechatferryAgentChatRoom): PUPPET.payloads.Room; 382 | declare function wechatferryRoomMemberToWechaty(chatRoomMember: WechatferryAgentChatRoomMember): PUPPET.payloads.RoomMember; 383 | declare module 'wechaty-puppet/payloads' { 384 | interface Room { 385 | announce: string; 386 | } 387 | } 388 | 389 | export { type AppAttachPayload, type AppMessagePayload, type ChannelsMsgPayload, type EmojiMessagePayload, type Event, type EventParserHandler, type EventPayload, type EventPayloadSpec, EventType, type MiniAppMsgPayload, type PrefixStorage, type PuppetContact, type PuppetMessage, type PuppetRoom, type PuppetWcferryOptions, type PuppetWcferryUserOptions, type ReferMsgPayload, type Runner, WechatferryPuppet, addEventParser, buildContactCardXmlMessagePayload, createPrefixStorage, executeRunners, friendShipParser, getMentionText, isContactCorporationId, isContactId, isContactOfficialId, isIMRoomId, isRoomId, isRoomOps, jsonToXml, mentionTextParser, messageParser, parseAppmsgMessagePayload, parseContactCardMessagePayload, parseEmotionMessagePayload, parseEvent, parseMiniProgramMessagePayload, parseTimelineMessagePayload, postParser, resolvePuppetWcferryOptions, roomInviteParser, roomJoinParser, roomLeaveParser, roomTopicParser, wechatferryContactToWechaty, wechatferryDBMessageToEventMessage, wechatferryDBMessageToWechaty, wechatferryMessageToWechaty, wechatferryRoomMemberToWechaty, wechatferryRoomToWechaty, wechatyContactToWechatferry, xmlToJson }; 390 | -------------------------------------------------------------------------------- /wcferry-puppet-c/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as _wechatferry_core from '@wechatferry/core'; 2 | import { WechatMessageType, WechatAppMessageType } from '@wechatferry/core'; 3 | import * as unstorage from 'unstorage'; 4 | import { Storage, StorageValue } from 'unstorage'; 5 | import { Room, RoomMember, Contact, Message } from 'wechaty-puppet/payloads'; 6 | import { WechatferryAgent, WechatferryAgentEventMessage, WechatferryAgentContact, WechatferryAgentChatRoomMember, WechatferryAgentChatRoom, WechatferryAgentDBMessage } from '@wechatferry/agent'; 7 | import * as PUPPET from 'wechaty-puppet'; 8 | import { FileBoxInterface } from 'file-box'; 9 | import { ParserOptions } from 'xml2js'; 10 | 11 | interface PuppetRoom extends Room { 12 | announce: string; 13 | members: RoomMember[]; 14 | } 15 | interface PuppetContact extends Contact { 16 | tags: string[]; 17 | } 18 | type PuppetMessage = Message & { 19 | isRefer: boolean; 20 | }; 21 | interface PuppetWcferryUserOptions { 22 | agent?: WechatferryAgent; 23 | /** 24 | * unstorage 实例,用于缓存数据 25 | */ 26 | storage?: Storage; 27 | } 28 | interface PuppetWcferryOptions extends Required { 29 | } 30 | 31 | declare function resolvePuppetWcferryOptions(userOptions: PuppetWcferryUserOptions): PuppetWcferryOptions; 32 | declare class WechatferryPuppet extends PUPPET.Puppet { 33 | static readonly VERSION: string; 34 | agent: WechatferryAgent; 35 | private cacheManager; 36 | private heartBeatTimer?; 37 | constructor(options?: PuppetWcferryUserOptions); 38 | name(): string; 39 | version(): string; 40 | onStart(): Promise; 41 | login(userId: string): void; 42 | onStop(): Promise; 43 | ding(data?: string): Promise; 44 | onMessage(message: WechatferryAgentEventMessage): Promise; 45 | private lastSelfMessageId; 46 | onSendMessage(timeout?: number): Promise; 47 | contactSelfQRCode(): Promise; 48 | contactSelfName(name: string): Promise; 49 | contactSelfSignature(signature: string): Promise; 50 | contactAlias(contactId: string): Promise; 51 | contactAlias(contactId: string, alias: string | null): Promise; 52 | contactPhone(contactId: string): Promise; 53 | contactPhone(contactId: string, phoneList: string[]): Promise; 54 | contactCorporationRemark(contactId: string, corporationRemark: string): Promise; 55 | contactDescription(contactId: string, description: string): Promise; 56 | contactList(): Promise; 57 | contactAvatar(contactId: string): Promise; 58 | contactAvatar(contactId: string, file: FileBoxInterface): Promise; 59 | contactRawPayloadParser(payload: WechatferryAgentContact): Promise; 60 | contactRawPayload(id: string): Promise; 61 | conversationReadMark(conversationId: string, hasRead?: boolean | undefined): Promise; 62 | messageContact(messageId: string): Promise; 63 | messageImage(messageId: string, imageType: PUPPET.types.Image): Promise; 64 | messageRecall(messageId: string): Promise; 65 | messageFile(messageId: string): Promise; 66 | messageUrl(messageId: string): Promise; 67 | messageLocation(messageId: string): Promise; 68 | messageMiniProgram(messageId: string): Promise; 69 | messageRawPayloadParser(payload: WechatferryAgentEventMessage): Promise; 70 | messageRawPayload(id: string): Promise; 71 | messageSendText(conversationId: string, text: string): Promise; 72 | messageSendFile(conversationId: string, file: FileBoxInterface): Promise; 73 | messageSendContact(conversationId: string, contactId: string): Promise; 74 | messageSendUrl(conversationId: string, urlLinkPayload: PUPPET.payloads.UrlLink): Promise; 75 | messageSendLocation(conversationId: string, locationPayload: PUPPET.payloads.Location): Promise; 76 | messageSendMiniProgram(conversationId: string, miniProgramPayload: PUPPET.payloads.MiniProgram): Promise; 77 | messageForward(conversationId: string, messageId: string): Promise; 78 | roomList(): Promise; 79 | roomCreate(contactIdList: string[], topic?: string | undefined): Promise; 80 | roomQuit(roomId: string): Promise; 81 | roomAdd(roomId: string, contactId: string): Promise; 82 | roomDel(roomId: string, contactId: string): Promise; 83 | roomAvatar(roomId: string): Promise; 84 | roomTopic(roomId: string): Promise; 85 | roomTopic(roomId: string, topic: string): Promise; 86 | roomQRCode(roomId: string): Promise; 87 | roomAnnounce(roomId: string): Promise; 88 | roomAnnounce(roomId: string, text: string): Promise; 89 | roomInvitationAccept(roomInvitationId: string): Promise; 90 | roomInvitationRawPayload(roomInvitationId: string): Promise; 91 | roomInvitationRawPayloadParser(rawPayload: any): Promise; 92 | roomMemberList(roomId: string): Promise; 93 | roomMemberRawPayloadParser(rawPayload: WechatferryAgentChatRoomMember): Promise; 94 | roomMemberRawPayload(roomId: string, contactId: string): Promise; 95 | roomRawPayloadParser(payload: WechatferryAgentChatRoom): Promise; 96 | roomRawPayload(id: string): Promise; 97 | friendshipSearchPhone(phone: string): Promise; 98 | friendshipSearchWeixin(weixin: string): Promise; 99 | friendshipAdd(contactId: string, hello: string): Promise; 100 | friendshipAccept(friendshipId: string): Promise; 101 | friendshipRawPayloadParser(rawPayload: any): Promise; 102 | friendshipRawPayload(id: string): Promise; 103 | tagContactAdd(tagId: string, contactId: string): Promise; 104 | tagContactRemove(tagId: string, contactId: string): Promise; 105 | tagContactDelete(tagId: string): Promise; 106 | tagContactList(contactId?: string): Promise; 107 | postPublish(payload: PUPPET.payloads.Post): Promise; 108 | postSearch(filter: PUPPET.filters.Post, pagination?: PUPPET.filters.PaginationRequest): Promise>; 109 | postRawPayloadParser(rawPayload: WechatferryAgentEventMessage): Promise; 110 | postRawPayload(postId: string): Promise<_wechatferry_core.WxMsg>; 111 | tap(postId: string, type?: PUPPET.types.Tap, tap?: boolean): Promise; 112 | tapSearch(postId: string, query?: PUPPET.filters.Tap, pagination?: PUPPET.filters.PaginationRequest): Promise>; 113 | private getRoomPayload; 114 | private getMessagePayload; 115 | private getContactPayload; 116 | updateContactCache(contactId: string, _contact?: PuppetContact): Promise; 117 | private updateRoomCache; 118 | /** 119 | * 更新群聊成员列表缓存 120 | * 121 | * @description 主要用于 room-join 事件前获取新加群的成员 122 | * @deprecated 尽可能避免使用,优先使用 updateRoomMemberCache 123 | * @param roomId 群聊 id 124 | */ 125 | updateRoomMemberListCache(roomId: string): Promise; 126 | private updateRoomMemberCache; 127 | private loadContactList; 128 | private loadRoomList; 129 | private startPuppetHeart; 130 | private stopPuppetHeart; 131 | } 132 | declare module 'wechaty-puppet/payloads' { 133 | interface UrlLink { 134 | /** 左下显示的名字 */ 135 | name?: string; 136 | /** 公众号 id 可以显示对应的头像(gh_ 开头的) */ 137 | account?: string; 138 | } 139 | } 140 | 141 | declare function xmlToJson>(xml: string, options?: ParserOptions): Promise; 142 | declare function jsonToXml(data: Record): Promise; 143 | 144 | declare function isRoomId(id?: string): boolean; 145 | declare function isContactOfficialId(id?: string): boolean; 146 | declare function isContactCorporationId(id?: string): boolean; 147 | declare function isIMRoomId(id?: string): boolean; 148 | declare function isRoomOps(type: WechatMessageType): type is WechatMessageType.SysNotice | WechatMessageType.Sys; 149 | declare function isContactId(id?: string): boolean; 150 | 151 | type PrefixStorage = ReturnType>; 152 | declare function createPrefixStorage(storage: Storage, base: string): { 153 | getItemsMap: (base?: string) => Promise<{ 154 | key: string; 155 | value: T; 156 | }[]>; 157 | getItemsList(base?: string): Promise; 158 | hasItem: (key: string, opts?: unstorage.TransactionOptions) => Promise; 159 | getItem: (key: string, opts?: unstorage.TransactionOptions) => Promise; 160 | getItems: (items: (string | { 161 | key: string; 162 | options?: unstorage.TransactionOptions; 163 | })[], commonOptions?: unstorage.TransactionOptions) => Promise<{ 164 | key: string; 165 | value: U; 166 | }[]>; 167 | getItemRaw: (key: string, opts?: unstorage.TransactionOptions) => Promise<(T_1 extends any ? T_1 : any) | null>; 168 | setItem: (key: string, value: U, opts?: unstorage.TransactionOptions) => Promise; 169 | setItems: (items: { 170 | key: string; 171 | value: U; 172 | options?: unstorage.TransactionOptions; 173 | }[], commonOptions?: unstorage.TransactionOptions) => Promise; 174 | setItemRaw: (key: string, value: T_1 extends any ? T_1 : any, opts?: unstorage.TransactionOptions) => Promise; 175 | removeItem: (key: string, opts?: (unstorage.TransactionOptions & { 176 | removeMeta?: boolean; 177 | }) | boolean) => Promise; 178 | getMeta: (key: string, opts?: (unstorage.TransactionOptions & { 179 | nativeOnly?: boolean; 180 | }) | boolean) => unstorage.StorageMeta | Promise; 181 | setMeta: (key: string, value: unstorage.StorageMeta, opts?: unstorage.TransactionOptions) => Promise; 182 | removeMeta: (key: string, opts?: unstorage.TransactionOptions) => Promise; 183 | getKeys: (base?: string, opts?: unstorage.TransactionOptions) => Promise; 184 | clear: (base?: string, opts?: unstorage.TransactionOptions) => Promise; 185 | dispose: () => Promise; 186 | watch: (callback: unstorage.WatchCallback) => Promise; 187 | unwatch: () => Promise; 188 | mount: (base: string, driver: unstorage.Driver) => Storage; 189 | unmount: (base: string, dispose?: boolean) => Promise; 190 | getMount: (key?: string) => { 191 | base: string; 192 | driver: unstorage.Driver; 193 | }; 194 | getMounts: (base?: string, options?: { 195 | parents?: boolean; 196 | }) => { 197 | base: string; 198 | driver: unstorage.Driver; 199 | }[]; 200 | }; 201 | 202 | declare function mentionTextParser(message: string): { 203 | mentions: string[]; 204 | message: string; 205 | }; 206 | declare function getMentionText(mentions?: string[], chatroomMembers?: WechatferryAgentChatRoomMember[]): string; 207 | 208 | type Runner = () => Promise; 209 | declare function executeRunners(runners: Runner[]): Promise; 210 | 211 | interface AppAttachPayload { 212 | totallen?: number; 213 | attachid?: string; 214 | emoticonmd5?: string; 215 | fileext?: string; 216 | cdnattachurl?: string; 217 | aeskey?: string; 218 | cdnthumbaeskey?: string; 219 | encryver?: number; 220 | islargefilemsg: number; 221 | } 222 | interface ReferMsgPayload { 223 | type: string; 224 | svrid: string; 225 | fromusr: string; 226 | chatusr: string; 227 | displayname: string; 228 | content: string; 229 | } 230 | interface ChannelsMsgPayload { 231 | objectId: string; 232 | feedType: string; 233 | nickname: string; 234 | avatar: string; 235 | desc: string; 236 | mediaCount: string; 237 | objectNonceId: string; 238 | liveId: string; 239 | username: string; 240 | authIconUrl: string; 241 | authIconType: string; 242 | mediaList?: { 243 | media?: { 244 | thumbUrl: string; 245 | fullCoverUrl: string; 246 | videoPlayDuration: string; 247 | url: string; 248 | height: string; 249 | mediaType: string; 250 | width: string; 251 | }; 252 | }; 253 | megaVideo?: object; 254 | bizAuthIconType?: string; 255 | } 256 | interface MiniAppMsgPayload { 257 | username: string; 258 | appid: string; 259 | pagepath: string; 260 | weappiconurl: string; 261 | shareId: string; 262 | } 263 | interface AppMessagePayload { 264 | des?: string; 265 | thumburl?: string; 266 | title: string; 267 | url: string; 268 | appattach?: AppAttachPayload; 269 | channel?: ChannelsMsgPayload; 270 | miniApp?: MiniAppMsgPayload; 271 | type: WechatAppMessageType; 272 | md5?: string; 273 | fromusername?: string; 274 | recorditem?: string; 275 | refermsg?: ReferMsgPayload; 276 | } 277 | declare function parseAppmsgMessagePayload(messageContent: string): Promise; 278 | 279 | interface EmojiMessagePayload { 280 | type: number; 281 | len: number; 282 | md5: string; 283 | cdnurl: string; 284 | width: number; 285 | height: number; 286 | gameext?: string; 287 | } 288 | declare function parseEmotionMessagePayload(message: PUPPET.payloads.Message): Promise; 289 | 290 | declare function parseMiniProgramMessagePayload(message: PUPPET.payloads.Message): Promise; 291 | 292 | interface ContactCardXmlSchema { 293 | msg: { 294 | $: { 295 | bigheadimgurl: string; 296 | smallheadimgurl: string; 297 | username: string; 298 | nickname: string; 299 | fullpy: string; 300 | shortpy: string; 301 | alias: string; 302 | imagestatus: string; 303 | scene: string; 304 | province: string; 305 | city: string; 306 | sign: string; 307 | sex: string; 308 | certflag: string; 309 | certinfo: string; 310 | brandIconUrl: string; 311 | brandHomeUrl: string; 312 | brandSubscriptConfigUrl: string; 313 | brandFlags: string; 314 | regionCode: string; 315 | biznamecardinfo: string; 316 | antispamticket: string; 317 | }; 318 | }; 319 | } 320 | declare function parseContactCardMessagePayload(messageContent: string): Promise; 321 | declare function buildContactCardXmlMessagePayload(contact: PUPPET.payloads.Contact): Promise; 322 | 323 | declare function parseTimelineMessagePayload(messageXml: string): Promise<{ 324 | messages: _wechatferry_core.WxMsg[]; 325 | payload: PUPPET.payloads.PostServer; 326 | }>; 327 | 328 | declare function roomTopicParser(puppet: PUPPET.Puppet, message: WechatferryAgentEventMessage): Promise; 329 | 330 | declare function messageParser(_puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 331 | 332 | declare function roomInviteParser(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 333 | 334 | declare function roomJoinParser(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage, retries?: number): Promise; 335 | 336 | declare function roomLeaveParser(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 337 | 338 | declare function friendShipParser(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 339 | 340 | declare function postParser(_puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise; 341 | 342 | declare enum EventType { 343 | Message = 0, 344 | Post = 1, 345 | Friendship = 2, 346 | RoomInvite = 3, 347 | RoomJoin = 4, 348 | RoomLeave = 5, 349 | RoomTopic = 6 350 | } 351 | interface EventPayloadSpec { 352 | [EventType.Message]: WechatferryAgentEventMessage; 353 | [EventType.Post]: PUPPET.payloads.EventPost; 354 | [EventType.Friendship]: PUPPET.payloads.Friendship; 355 | [EventType.RoomInvite]: PUPPET.payloads.RoomInvitation; 356 | [EventType.RoomJoin]: PUPPET.payloads.EventRoomJoin; 357 | [EventType.RoomLeave]: PUPPET.payloads.EventRoomLeave; 358 | [EventType.RoomTopic]: PUPPET.payloads.EventRoomTopic; 359 | } 360 | interface Event { 361 | type: T; 362 | payload: EventPayloadSpec[T]; 363 | } 364 | type EventPayload = EventPayloadSpec[keyof EventPayloadSpec] | null; 365 | type EventParserHandler = (puppet: WechatferryPuppet, message: WechatferryAgentEventMessage) => Promise; 366 | declare function addEventParser(eventType: EventType, parser: EventParserHandler): void; 367 | declare function parseEvent(puppet: WechatferryPuppet, message: WechatferryAgentEventMessage): Promise>; 368 | 369 | declare function wechatferryContactToWechaty(contact: WechatferryAgentContact): PUPPET.payloads.Contact; 370 | declare function wechatyContactToWechatferry(contact: PUPPET.payloads.Contact): WechatferryAgentContact; 371 | declare module 'wechaty-puppet/payloads' { 372 | interface Contact { 373 | tags: string[]; 374 | } 375 | } 376 | 377 | declare function wechatferryMessageToWechaty(puppet: PUPPET.Puppet, message: WechatferryAgentEventMessage): Promise; 378 | declare function wechatferryDBMessageToWechaty(puppet: PUPPET.Puppet, message: WechatferryAgentDBMessage): Promise; 379 | declare function wechatferryDBMessageToEventMessage(message: WechatferryAgentDBMessage): WechatferryAgentEventMessage; 380 | 381 | declare function wechatferryRoomToWechaty(contact: WechatferryAgentChatRoom): PUPPET.payloads.Room; 382 | declare function wechatferryRoomMemberToWechaty(chatRoomMember: WechatferryAgentChatRoomMember): PUPPET.payloads.RoomMember; 383 | declare module 'wechaty-puppet/payloads' { 384 | interface Room { 385 | announce: string; 386 | } 387 | } 388 | 389 | export { type AppAttachPayload, type AppMessagePayload, type ChannelsMsgPayload, type EmojiMessagePayload, type Event, type EventParserHandler, type EventPayload, type EventPayloadSpec, EventType, type MiniAppMsgPayload, type PrefixStorage, type PuppetContact, type PuppetMessage, type PuppetRoom, type PuppetWcferryOptions, type PuppetWcferryUserOptions, type ReferMsgPayload, type Runner, WechatferryPuppet, addEventParser, buildContactCardXmlMessagePayload, createPrefixStorage, executeRunners, friendShipParser, getMentionText, isContactCorporationId, isContactId, isContactOfficialId, isIMRoomId, isRoomId, isRoomOps, jsonToXml, mentionTextParser, messageParser, parseAppmsgMessagePayload, parseContactCardMessagePayload, parseEmotionMessagePayload, parseEvent, parseMiniProgramMessagePayload, parseTimelineMessagePayload, postParser, resolvePuppetWcferryOptions, roomInviteParser, roomJoinParser, roomLeaveParser, roomTopicParser, wechatferryContactToWechaty, wechatferryDBMessageToEventMessage, wechatferryDBMessageToWechaty, wechatferryMessageToWechaty, wechatferryRoomMemberToWechaty, wechatferryRoomToWechaty, wechatyContactToWechatferry, xmlToJson }; 390 | -------------------------------------------------------------------------------- /config/native_emoji_map.js: -------------------------------------------------------------------------------- 1 | // noinspection JSNonASCIINames 2 | 3 | // Hello user, let me explain what WeChat does on their Web Client. 4 | // Perhaps its original goal was compatibility. It created a list of emojis and their corresponding random codes on their server. 5 | // The web client only receives a bundled image with all these emojis inside. Every time it receives an emoji, it replaces it with a random code. 6 | // This code is wrapped within an element. The Web Client then locates the position of the emoji in the bundled image, crops a small piece of it, 7 | // and uses this piece to display the emoji. 8 | // In simple terms, WeChat has hijacked all emojis through their server, creating a challenging situation for developers. 9 | // I need to find the correspondence between the random codes and the authentic emojis, and replace them only if the incoming message includes an emoji. 10 | // I tested 1742 emojis, and found that over 550 need to be replaced. I painstakingly collected this conversion map, which now covers most commonly used emojis in daily life. 11 | // If you discover more emojis that have been replaced by WeChat but yet not contained by this table, please let me know. 12 | 13 | 14 | module.exports = { 15 | "[emoji1f425]": ["🐥", 999], 16 | "[emoji1f63a]": ["😺", 124], 17 | "[emoji1f604]": ["😄", 3], 18 | "[emoji1f63c]": ["😼", 128], 19 | "[emoji1f604][emoji1f4a7]": ["😅", 5], 20 | "[emoji1f60c]": ["😌", 48], 21 | "[emoji1f639]": ["😹", 126], 22 | "[emoji1f609]": ["😉", 10], 23 | "[emoji1f60a]": ["😋", 26], 24 | "[emoji1f63b]": ["😻", 127], 25 | "[emoji1f63d]": ["😽", 129], 26 | "[emoji263a]": ["☺", 22], 27 | "[emoji1f61a]": ["😚", 23], 28 | "[emoji1f61c]": ["😜", 28], 29 | "[emoji1f61d]": ["👅", 224], 30 | "[emoji1f60f]": ["😏", 41], 31 | "[emoji1f612]": ["😒", 42], 32 | "[emoji1f62a]": ["😪", 46], 33 | "[emoji1f64d]": ["🙍", 250], 34 | "[emoji1f637]": ["😷", 51], 35 | "[emoji1f62b]": ["😫", 84], 36 | "[emoji1f632]": ["😲", 68], 37 | "[emoji1f633]": ["😳", 69], 38 | "[emoji1f628]": ["😨", 73], 39 | "[emoji1f630]": ["😰", 74], 40 | "[emoji1f625]": ["😥", 75], 41 | "[emoji1f63f]": ["😿", 131], 42 | "[emoji1f62d]": ["😭", 77], 43 | "[emoji1f631]": ["😱", 78], 44 | "[emoji1f4ab]": ["💫", 162], 45 | "[emoji1f61e]": ["😞", 81], 46 | "[emoji1f613]": ["😓", 82], 47 | "[emoji1f64e]": ["😾", 132], 48 | "[emoji1f620]": ["😠", 88], 49 | "[emoji1f47f]": ["👿", 91], 50 | "[emoji1f480]": ["💀", 92], 51 | "[emoji1f4a9]": ["💩", 94], 52 | "[emoji1f47b]": ["👻", 98], 53 | "[emoji1f47d]": ["👽", 99], 54 | "[emoji1f47e]": ["👾", 100], 55 | "😮‍[emoji1f4a8]": ["😮‍💨", 103], 56 | "[emoji1f62b]‍[emoji1f4ab]": ["😵‍💫", 104], 57 | "[emoji2764]️‍[emoji1f525]": ["❤️‍🔥", 148], 58 | "[emoji2764]️‍🩹": ["❤️‍🩹", 149], 59 | "[emoji1f468]‍🦰": ["👨‍🦰", 236], 60 | "[emoji1f468]‍🦱": ["👨‍🦱", 237], 61 | "[emoji1f468]‍🦳": ["👨‍🦳", 238], 62 | "[emoji1f468]‍🦲": ["👨‍🦲", 239], 63 | "[emoji1f469]‍🦰": ["👩‍🦰", 241], 64 | "[emoji1f469]‍🦱": ["👩‍🦱", 242], 65 | "[emoji1f469]‍🦳": ["👩‍🦳", 243], 66 | "[emoji1f469]‍🦲": ["👩‍🦲", 244], 67 | "[emoji1f471]‍♀️": ["👱‍♀️", 122], 68 | "[emoji1f471]‍♂️": ["👱‍♂️", 123], 69 | "[emoji1f48b]": ["💋", 136], 70 | "[emoji1f4e7][emoji1f48c]": ["💌", 137], 71 | "[emoji1f498]": ["💘", 138], 72 | "[emoji1f49d]": ["💝", 139], 73 | "[emoji1f49e]": ["💕", 144], 74 | "[emoji1f48c]": ["💗", 141], 75 | "[emoji1f49f]": ["💟", 145], 76 | "[emoji1f494]": ["💔", 147], 77 | "[emoji2764]": ["❤", 150], 78 | "[emoji1f49b]": ["💛", 152], 79 | "[emoji1f49a]": ["💚", 153], 80 | "[emoji1f499]": ["💙", 154], 81 | "[emoji1f49c]": ["💜", 155], 82 | "[emoji1f4a2]": ["💢", 160], 83 | "[emoji1f4a7]": ["💧", 922], 84 | "[emoji1f4a8]": ["💨", 164], 85 | "[emoji1f4a3]": ["💣", 166], 86 | "[emoji1f4a4]": ["💤", 172], 87 | "[emoji1f44b]": ["👋", 173], 88 | "[emoji270b]": ["✋", 176], 89 | "[emoji1f44c]": ["👌", 178], 90 | "[emoji270c]": ["✌", 181], 91 | "[emoji1f448]": ["👈", 186], 92 | "[emoji1f449]": ["👉", 187], 93 | "[emoji1f446]": ["👆", 188], 94 | "[emoji1f447]": ["👇", 189], 95 | "[emoji261d]": ["☝", 191], 96 | "[emoji1f44d]": ["👍", 193], 97 | "[emoji1f44e]": ["👎", 194], 98 | "[emoji270a]": ["✊", 195], 99 | "[emoji1f44a]": ["👊", 196], 100 | "[emoji1f44f]": ["👏", 199], 101 | "[emoji1f64c]": ["🙌", 200], 102 | "[emoji1f450]": ["👐", 201], 103 | "[emoji1f64f]": ["🙏", 204], 104 | "[emoji1f485]": ["💅", 206], 105 | "[emoji1f4aa]": ["💪", 208], 106 | "[emoji1f442]": ["👂", 213], 107 | "[emoji1f443]": ["👃", 215], 108 | "[emoji1f463]": ["🐾", 538], 109 | "[emoji1f440]": ["👀", 222], 110 | "[emoji1f444]": ["👄", 225], 111 | "[emoji1f476]": ["👶", 227], 112 | "[emoji1f466]": ["👦", 229], 113 | "[emoji1f467]": ["👧", 230], 114 | "[emoji1f471]": ["👱", 231], 115 | "[emoji1f468]": ["👨", 232], 116 | "[emoji1f469]": ["👩", 240], 117 | "[emoji1f471]‍♀‍": ["👱‍♀‍", 245], 118 | "[emoji1f471]‍♂‍": ["👱‍♂‍", 246], 119 | "[emoji1f474]": ["👴", 248], 120 | "[emoji1f475]": ["👵", 249], 121 | "[emoji1f64d]‍♂️": ["🙍‍♂️", 251], 122 | "[emoji1f64d]‍♀️": ["🙍‍♀️", 252], 123 | "[emoji1f64e]‍♂️": ["🙎‍♂️", 253], 124 | "[emoji1f64e]‍♀️": ["🙎‍♀️", 254], 125 | "[emoji1f645]‍♂️": ["🙅‍♂️", 255], 126 | "[emoji1f645]‍♀️": ["🙅‍♀️", 256], 127 | "[emoji1f646]‍♂️": ["🙆‍♂️", 257], 128 | "[emoji1f646]‍♀️": ["🙆‍♀️", 258], 129 | "[emoji1f481]‍♂️": ["💁‍♂️", 259], 130 | "[emoji1f481]‍♀️": ["💁‍♀️", 260], 131 | "[emoji270b]‍♂️": ["🙋‍♂️", 261], 132 | "[emoji270b]‍♀️": ["🙋‍♀️", 262], 133 | "[emoji1f647]‍♂️": ["🙇‍♂️", 265], 134 | "[emoji1f647]‍♀️": ["🙇‍♀️", 266], 135 | "[emoji1f468]‍⚕️": ["👨‍⚕️", 272], 136 | "[emoji1f469]‍⚕️": ["👩‍⚕️", 273], 137 | "🧑‍[emoji1f393]": ["🧑‍🎓", 274], 138 | "[emoji1f468]‍[emoji1f393]": ["👨‍🎓", 275], 139 | "[emoji1f469]‍[emoji1f393]": ["👩‍🎓", 276], 140 | "🧑‍[emoji1f3eb]": ["🧑‍🏫", 277], 141 | "[emoji1f468]‍[emoji1f3eb]": ["👨‍🏫", 278], 142 | "[emoji1f469]‍[emoji1f3eb]": ["👩‍🏫", 279], 143 | "[emoji1f469]‍⚖️": ["👩‍⚖️", 281], 144 | "🧑‍[emoji1f33e]": ["🧑‍🌾", 282], 145 | "[emoji1f468]‍[emoji1f33e]": ["👨‍🌾", 283], 146 | "[emoji1f469]‍[emoji1f33e]": ["👩‍🌾", 284], 147 | "🧑‍[emoji1f373]": ["🧑‍🍳", 285], 148 | "[emoji1f468]‍[emoji1f373]": ["👨‍🍳", 286], 149 | "[emoji1f469]‍[emoji1f373]": ["👩‍🍳", 287], 150 | "[emoji1f468]‍🔧": ["👨‍🔧", 289], 151 | "[emoji1f469]‍🔧": ["👩‍🔧", 290], 152 | "🧑‍[emoji1f3ed]": ["🧑‍🏭", 291], 153 | "[emoji1f468]‍[emoji1f3ed]": ["👨‍🏭", 292], 154 | "[emoji1f469]‍[emoji1f3ed]": ["👩‍🏭", 293], 155 | "🧑‍[emoji1f4bc]": ["🧑‍💼", 294], 156 | "[emoji1f468]‍[emoji1f4bc]": ["👨‍💼", 295], 157 | "[emoji1f469]‍[emoji1f4bc]": ["👩‍💼", 296], 158 | "[emoji1f468]‍🔬": ["👨‍🔬", 298], 159 | "[emoji1f469]‍🔬": ["👩‍🔬", 299], 160 | "🧑‍[emoji1f4bb]": ["🧑‍💻", 300], 161 | "[emoji1f468]‍[emoji1f4bb]": ["👨‍💻", 301], 162 | "[emoji1f469]‍[emoji1f4bb]": ["👩‍💻", 302], 163 | "🧑‍[emoji1f3a4]": ["🧑‍🎤", 303], 164 | "[emoji1f468]‍[emoji1f3a4]": ["👨‍🎤", 304], 165 | "[emoji1f469]‍[emoji1f3a4]": ["👩‍🎤", 305], 166 | "🧑‍[emoji1f3a8]": ["🧑‍🎨", 306], 167 | "[emoji1f468]‍[emoji1f3a8]": ["👨‍🎨", 307], 168 | "[emoji1f469]‍[emoji1f3a8]": ["👩‍🎨", 308], 169 | "🧑‍[emoji2708]️": ["🧑‍✈️", 309], 170 | "[emoji1f468]‍[emoji2708]️": ["👨‍✈️", 310], 171 | "[emoji1f469]‍[emoji2708]️": ["👩‍✈️", 311], 172 | "🧑‍[emoji1f680]": ["🧑‍🚀", 312], 173 | "[emoji1f468]‍[emoji1f680]": ["👨‍🚀", 313], 174 | "[emoji1f469]‍[emoji1f680]": ["👩‍🚀", 314], 175 | "🧑‍[emoji1f692]": ["🧑‍🚒", 315], 176 | "[emoji1f468]‍[emoji1f692]": ["👨‍🚒", 316], 177 | "[emoji1f469]‍[emoji1f692]": ["👩‍🚒", 317], 178 | "[emoji1f46e]‍♂️": ["👮‍♂️", 318], 179 | "[emoji1f46e]‍♀️": ["👮‍♀️", 319], 180 | "[emoji1f482]‍♂️": ["💂‍♂️", 322], 181 | "[emoji1f482]‍♀️": ["💂‍♀️", 323], 182 | "[emoji1f477]‍♂️": ["👷‍♂️", 324], 183 | "[emoji1f477]‍♀️": ["👷‍♀️", 325], 184 | "[emoji1f473]‍♂️": ["👳‍♂️", 326], 185 | "[emoji1f473]‍♀️": ["👳‍♀️", 327], 186 | "[emoji1f469]‍🍼": ["👩‍🍼", 333], 187 | "[emoji1f468]‍🍼": ["👨‍🍼", 334], 188 | "🧑‍[emoji1f384]": ["🧑‍🎄", 335], 189 | "[emoji1f486]‍♂️": ["💆‍♂️", 354], 190 | "[emoji1f486]‍♀️": ["💆‍♀️", 355], 191 | "[emoji1f487]‍♂️": ["💇‍♂️", 356], 192 | "[emoji1f487]‍♀️": ["💇‍♀️", 357], 193 | "[emoji1f6b6]‍♂️": ["🚶‍♂️", 358], 194 | "[emoji1f6b6]‍♀️": ["🚶‍♀️", 359], 195 | "[emoji1f468]‍🦯": ["👨‍🦯", 365], 196 | "[emoji1f469]‍🦯": ["👩‍🦯", 366], 197 | "[emoji1f468]‍🦼": ["👨‍🦼", 368], 198 | "[emoji1f469]‍🦼": ["👩‍🦼", 369], 199 | "[emoji1f468]‍🦽": ["👨‍🦽", 371], 200 | "[emoji1f469]‍🦽": ["👩‍🦽", 372], 201 | "[emoji1f3c3]‍♂️": ["🏃‍♂️", 373], 202 | "[emoji1f3c3]‍♀️": ["🏃‍♀️", 374], 203 | "[emoji1f46f]‍♂️": ["👯‍♂️", 375], 204 | "[emoji1f46f]‍♀️": ["👯‍♀️", 376], 205 | "[emoji1f3c4]": ["🏄", 393], 206 | "[emoji1f3c4]‍♂️": ["🏄‍♂️", 394], 207 | "[emoji1f3c4]‍♀️": ["🏄‍♀️", 395], 208 | "[emoji1f3ca]": ["🏊", 399], 209 | "[emoji1f3ca]‍♂️": ["🏊‍♂️", 400], 210 | "[emoji1f3ca]‍♀️": ["🏊‍♀️", 401], 211 | "[emoji1f6c0]": ["🛀", 432], 212 | "[emoji1f468]‍[emoji1f469]‍[emoji1f466]": ["👨‍👩‍👦", 435], 213 | "[emoji1f468]‍[emoji1f469]‍[emoji1f467]": ["👨‍👩‍👧", 436], 214 | "[emoji1f468]‍[emoji1f469]‍[emoji1f467]‍[emoji1f466]": ["👨‍👩‍👧‍👦", 437], 215 | "[emoji1f468]‍[emoji1f469]‍[emoji1f466]‍[emoji1f466]": ["👨‍👩‍👦‍👦", 438], 216 | "[emoji1f468]‍[emoji1f469]‍[emoji1f467]‍[emoji1f467]": ["👨‍👩‍👧‍👧", 439], 217 | "[emoji1f468]‍[emoji1f468]‍[emoji1f466]": ["👨‍👨‍👦", 440], 218 | "[emoji1f468]‍[emoji1f468]‍[emoji1f467]": ["👨‍👨‍👧", 441], 219 | "[emoji1f468]‍[emoji1f468]‍[emoji1f467]‍[emoji1f466]": ["👨‍👨‍👧‍👦", 442], 220 | "[emoji1f468]‍[emoji1f468]‍[emoji1f466]‍[emoji1f466]": ["👨‍👨‍👦‍👦", 443], 221 | "[emoji1f468]‍[emoji1f468]‍[emoji1f467]‍[emoji1f467]": ["👨‍👨‍👧‍👧", 444], 222 | "[emoji1f469]‍[emoji1f469]‍[emoji1f466]": ["👩‍👩‍👦", 445], 223 | "[emoji1f469]‍[emoji1f469]‍[emoji1f467]": ["👩‍👩‍👧", 446], 224 | "[emoji1f469]‍[emoji1f469]‍[emoji1f467]‍[emoji1f466]": ["👩‍👩‍👧‍👦", 447], 225 | "[emoji1f469]‍[emoji1f469]‍[emoji1f466]‍[emoji1f466]": ["👩‍👩‍👦‍👦", 448], 226 | "[emoji1f469]‍[emoji1f469]‍[emoji1f467]‍[emoji1f467]": ["👩‍👩‍👧‍👧", 449], 227 | "[emoji1f468]‍[emoji1f466]": ["👨‍👦", 450], 228 | "[emoji1f468]‍[emoji1f466]‍[emoji1f466]": ["👨‍👦‍👦", 451], 229 | "[emoji1f468]‍[emoji1f467]": ["👨‍👧", 452], 230 | "[emoji1f468]‍[emoji1f467]‍[emoji1f466]": ["👨‍👧‍👦", 453], 231 | "[emoji1f468]‍[emoji1f467]‍[emoji1f467]": ["👨‍👧‍👧", 454], 232 | "[emoji1f469]‍[emoji1f466]": ["👩‍👦", 455], 233 | "[emoji1f469]‍[emoji1f467]": ["👩‍👧", 456], 234 | "[emoji1f469]‍[emoji1f467]‍[emoji1f466]": ["👩‍👧‍👦", 457], 235 | "[emoji1f469]‍[emoji1f467]‍[emoji1f467]": ["👩‍👧‍👧", 458], 236 | "[emoji1f46b]": ["👫", 461], 237 | "[emoji1f48f]": ["💏", 463], 238 | "[emoji1f469]‍[emoji2764]️‍[emoji1f48b]‍[emoji1f468]": ["👩‍❤️‍💋‍👨", 464], 239 | "[emoji1f468]‍[emoji2764]️‍[emoji1f48b]‍[emoji1f468]": ["👨‍❤️‍💋‍👨", 465], 240 | "[emoji1f469]‍[emoji2764]️‍[emoji1f48b]‍[emoji1f469]": ["👩‍❤️‍💋‍👩", 466], 241 | "[emoji1f469]‍[emoji2764]️‍[emoji1f468]": ["👩‍❤️‍👨", 467], 242 | "[emoji1f468]‍[emoji2764]️‍[emoji1f468]": ["👨‍❤️‍👨", 468], 243 | "[emoji1f469]‍[emoji2764]️‍[emoji1f469]": ["👩‍❤️‍👩", 469], 244 | "[emoji1f491]": ["💑", 470], 245 | "[emoji1f435]": ["🐵", 475], 246 | "[emoji1f412]": ["🐒", 476], 247 | "[emoji1f436]": ["🐩", 483], 248 | "[emoji1f43a]": ["🐺", 484], 249 | "[emoji1f431]": ["🐱", 487], 250 | "🐈‍[emoji25fc]": ["🐈‍⬛", 489], 251 | "[emoji1f42f]": ["🐯", 491], 252 | "[emoji1f434]": ["🐴", 494], 253 | "[emoji1f40e]": ["🐎", 495], 254 | "[emoji1f42e]": ["🐮", 500], 255 | "[emoji1f43d]": ["🐽", 507], 256 | "[emoji1f417]": ["🐗", 506], 257 | "[emoji1f411]": ["🐑", 509], 258 | "[emoji1f42b]": ["🐫", 512], 259 | "[emoji1f418]": ["🐘", 515], 260 | "[emoji1f42d]": ["🐭", 519], 261 | "[emoji1f439]": ["🐹", 522], 262 | "[emoji1f430]": ["🐰", 523], 263 | "[emoji1f43b]": ["🐻", 529], 264 | "[emoji1f43b]‍❄️": ["🐻‍❄️", 530], 265 | "[emoji1f428]": ["🐨", 531], 266 | "[emoji1f414]": ["🐔", 540], 267 | "[emoji1f423]": ["🐥", 544], 268 | "[emoji1f426]": ["🐦", 545], 269 | "[emoji1f426]‍[emoji25fc]": ["🐦‍⬛", 546], 270 | "[emoji1f427]": ["🐧", 547], 271 | "[emoji1f438]": ["🐸", 557], 272 | "[emoji1f40d]": ["🐍", 561], 273 | "[emoji1f433]": ["🐳", 566], 274 | "[emoji1f42c]": ["🐬", 568], 275 | "[emoji1f3a3]": ["🎣", 971], 276 | "[emoji1f420]": ["🐠", 571], 277 | "[emoji1f419]": ["🐙", 574], 278 | "[emoji1f41a]": ["🐚", 575], 279 | "[emoji1f41b]": ["🐛", 578], 280 | "[emoji1f490]": ["💐", 592], 281 | "[emoji1f338]": ["🌸", 593], 282 | "[emoji1f339]": ["🌹", 596], 283 | "[emoji1f33a]": ["🌺", 598], 284 | "[emoji1f33c]": ["🌼", 600], 285 | "[emoji1f337]": ["🌷", 601], 286 | "[emoji1f33f]": ["🍀", 611], 287 | "[emoji1f334]": ["🌴", 606], 288 | "[emoji1f335]": ["🌵", 607], 289 | "[emoji1f33e]": ["🌾", 608], 290 | "[emoji1f341]": ["🍁", 612], 291 | "[emoji1f342]": ["🍂", 613], 292 | "[emoji1f343]": ["🍃", 614], 293 | "[emoji1f349]": ["🍉", 619], 294 | "[emoji1f34a]": ["🍊", 620], 295 | "[emoji1f34f]": ["🍏", 626], 296 | "[emoji1f353]": ["🍓", 630], 297 | "[emoji1f345]": ["🍅", 633], 298 | "[emoji1f346]": ["🍆", 637], 299 | "[emoji1f35e]": ["🍞", 652], 300 | "[emoji1f354]": ["🍔", 665], 301 | "[emoji1f35f]": ["🍟", 666], 302 | "[emoji1f373]": ["🍳", 676], 303 | "[emoji1f372]": ["🍲", 678], 304 | "[emoji1f371]": ["🍱", 686], 305 | "[emoji1f358]": ["🍘", 687], 306 | "[emoji1f359]": ["🍙", 688], 307 | "[emoji1f35a]": ["🍚", 689], 308 | "[emoji1f35b]": ["🍛", 690], 309 | "[emoji1f35c]": ["🍜", 691], 310 | "[emoji1f35d]": ["🍝", 692], 311 | "[emoji1f362]": ["🍢", 694], 312 | "[emoji1f363]": ["🍣", 695], 313 | "[emoji1f361]": ["🍡", 699], 314 | "[emoji1f367]": ["🍧", 709], 315 | "[emoji1f366]": ["🍦", 710], 316 | "[emoji1f382]": ["🎂", 713], 317 | "[emoji1f370]": ["🍰", 714], 318 | "[emoji2615]": ["☕", 724], 319 | "[emoji1f375]": ["🍵", 726], 320 | "[emoji1f376]": ["🏮", 1113], 321 | "[emoji1f379]": ["🍹", 731], 322 | "[emoji1f37a]": ["🍺", 732], 323 | "[emoji1f37b]": ["🍻", 733], 324 | "[emoji1f374]": ["🍴", 743], 325 | "[emoji1f5fb]": ["🗻", 757], 326 | "[emoji1f3e1]": ["🏡", 773], 327 | "[emoji1f3e2]": ["🏢", 774], 328 | "[emoji1f3e3]": ["🏣", 775], 329 | "[emoji1f3e5]": ["🏥", 777], 330 | "[emoji1f3e6]": ["🏦", 778], 331 | "[emoji1f3e8]": ["🏨", 779], 332 | "[emoji1f3e9]": ["🏩", 780], 333 | "[emoji1f3ea]": ["🏪", 781], 334 | "[emoji1f3eb]": ["🏫", 782], 335 | "[emoji1f3ec]": ["🏬", 783], 336 | "[emoji1f3ed]": ["🏭", 784], 337 | "[emoji1f3ef]": ["🏯", 785], 338 | "[emoji1f3f0]": ["🏰", 786], 339 | "[emoji1f492]": ["💒", 787], 340 | "[emoji1f5fc]": ["🗼", 788], 341 | "[emoji1f5fd]": ["🗽", 789], 342 | "[emoji26ea]": ["⛪", 790], 343 | "[emoji26f2]": ["⛲", 796], 344 | "[emoji26fa]": ["⛺", 797], 345 | "[emoji1f30c]": ["🌌", 897], 346 | "[emoji1f305]": ["🌅", 801], 347 | "[emoji1f304]": ["🌄", 802], 348 | "[emoji1f306]": ["🌆", 803], 349 | "[emoji1f307]": ["🌇", 804], 350 | "[emoji2668]": ["♨", 806], 351 | "[emoji1f3a1]": ["🎡", 808], 352 | "[emoji1f3a2]": ["🎢", 809], 353 | "[emoji1f488]": ["💈", 810], 354 | "[emoji1f683]": ["🚃", 813], 355 | "[emoji1f684]": ["🚄", 814], 356 | "[emoji1f685]": ["🚅", 815], 357 | "[emoji24c2]": ["Ⓜ", 1420], 358 | "[emoji1f689]": ["🚉", 819], 359 | "[emoji1f68c]": ["🚌", 824], 360 | "[emoji1f691]": ["🚑", 828], 361 | "[emoji1f692]": ["🚒", 829], 362 | "[emoji1f6a8]": ["🚓", 830], 363 | "[emoji1f695]": ["🚕", 832], 364 | "[emoji1f697]": ["🚗", 834], 365 | "[emoji1f699]": ["🚙", 836], 366 | "[emoji1f69a]": ["🚚", 838], 367 | "[emoji23f0]": ["🕙", 873], 368 | "[emoji1f55b]": ["🕛", 853], 369 | "[emoji1f550]": ["🕐", 855], 370 | "[emoji1f551]": ["🕑", 857], 371 | "[emoji1f552]": ["🕒", 859], 372 | "[emoji1f553]": ["🕓", 861], 373 | "[emoji1f554]": ["🕔", 863], 374 | "[emoji1f555]": ["🕕", 865], 375 | "[emoji1f556]": ["🕖", 867], 376 | "[emoji1f557]": ["🕗", 869], 377 | "[emoji1f558]": ["🕘", 871], 378 | "[emoji1f55a]": ["🕚", 875], 379 | "[emoji1f31b]": ["🌛", 887], 380 | "[emoji2600]": ["☀", 890], 381 | "[emoji2b50]": ["⭐", 894], 382 | "[emoji1f31f]": ["🌟", 895], 383 | "[emoji2601]": ["☁", 898], 384 | "[emoji2600][emoji2601]": ["⛅", 899], 385 | "[emoji1f300]": ["🌀", 910], 386 | "[emoji1f308]": ["🌈", 911], 387 | "[emoji1f302]": ["🌂", 912], 388 | "[emoji2614]": ["☔", 914], 389 | "[emoji26a1]": ["⚡", 916], 390 | "[emoji26c4]": ["⛄", 919], 391 | "[emoji1f525]": ["🔥", 921], 392 | "[emoji1f30a]": ["🌊", 923], 393 | "[emoji1f383]": ["🎃", 924], 394 | "[emoji1f384]": ["🎄", 925], 395 | "[emoji1f386]": ["🎆", 926], 396 | "[emoji1f387]": ["🎇", 927], 397 | "[emoji2747]": ["❇", 1389], 398 | "[emoji1f388]": ["🎈", 930], 399 | "[emoji1f389]": ["🎉", 931], 400 | "[emoji1f38d]": ["🎍", 934], 401 | "[emoji1f38e]": ["🎎", 935], 402 | "[emoji1f38f]": ["🎏", 936], 403 | "[emoji1f391]": ["🎑", 937], 404 | "[emoji1f380]": ["🎀", 939], 405 | "[emoji1f4e6]": ["📦", 1147], 406 | "[emoji1f3ab]": ["🎫", 943], 407 | "[emoji1f3c6]": ["🏆", 945], 408 | "[emoji26bd]": ["⚽", 950], 409 | "[emoji26be]": ["⚾", 951], 410 | "[emoji1f3c0]": ["🏀", 953], 411 | "[emoji1f3c8]": ["🏈", 955], 412 | "[emoji1f3be]": ["🎾", 957], 413 | "[emoji26f3]": ["⛳", 969], 414 | "[emoji1f3bf]": ["🎿", 974], 415 | "[emoji1f3af]": ["🎯", 977], 416 | "[emoji1f3b1]": ["🎱", 980], 417 | "[emoji1f52f]": ["🔯", 1316], 418 | "[emoji1f3b0]": ["🎰", 987], 419 | "[emoji2660]": ["♠", 993], 420 | "[emoji2665]": ["♥", 994], 421 | "[emoji2663]": ["♣", 995], 422 | "[emoji1f004]": ["🀄", 998], 423 | "[emoji1f3ad]": ["🎩", 1043], 424 | "[emoji1f3a8]": ["🎨", 1002], 425 | "[emoji1f454]": ["👔", 1012], 426 | "[emoji1f45a]": ["👚", 1026], 427 | "[emoji1f457]": ["👗", 1019], 428 | "[emoji1f458]": ["👘", 1020], 429 | "[emoji1f459]": ["👙", 1025], 430 | "[emoji1f45c]": ["👜", 1028], 431 | "[emoji1f392]": ["🎒", 1031], 432 | "[emoji1f45f]": ["👟", 1034], 433 | "[emoji1f460]": ["👠", 1037], 434 | "[emoji1f461]": ["👡", 1038], 435 | "[emoji1f462]": ["👢", 1040], 436 | "[emoji1f451]": ["👑", 1041], 437 | "[emoji1f452]": ["👒", 1042], 438 | "[emoji1f393]": ["🎓", 1044], 439 | "[emoji1f484]": ["💄", 1049], 440 | "[emoji1f48d]": ["💍", 1050], 441 | "[emoji1f48e]": ["💎", 1051], 442 | "[emoji1f50a]": ["🔊", 1055], 443 | "[emoji1f4e2]": ["📢", 1056], 444 | "[emoji1f4e3]": ["📣", 1057], 445 | "[emoji1f514]": ["🔔", 1059], 446 | "[emoji1f3bc]": ["🎶", 1063], 447 | "[emoji1f3b5]": ["🎵", 1062], 448 | "[emoji1f3a4]": ["🎤", 1067], 449 | "[emoji1f3a7]": ["🎧", 1068], 450 | "[emoji1f4fb]": ["📻", 1069], 451 | "[emoji1f3b7]": ["🎷", 1070], 452 | "[emoji1f3b8]": ["🎸", 1072], 453 | "[emoji1f3ba]": ["🎺", 1074], 454 | "[emoji1f4f1]": ["📱", 1079], 455 | "[emoji1f4f2]": ["📲", 1080], 456 | "[emoji1f4de]": ["📞", 1082], 457 | "[emoji1f4e0]": ["📠", 1084], 458 | "[emoji1f4bb]": ["💻", 1088], 459 | "[emoji1f4be]": ["💾", 1095], 460 | "[emoji1f4bf]": ["💿", 1096], 461 | "[emoji1f4c0]": ["📀", 1097], 462 | "[emoji1f4f9]": ["📹", 1106], 463 | "[emoji1f3ac]": ["🎬", 1102], 464 | "[emoji1f4fa]": ["📺", 1103], 465 | "[emoji1f4f7]": ["📷", 1104], 466 | "[emoji1f4fc]": ["📼", 1107], 467 | "[emoji1f50e]": ["🔎", 1109], 468 | "[emoji1f4a1]": ["💡", 1111], 469 | "[emoji1f4d2]": ["📇", 1167], 470 | "[emoji1f4d1]": ["📋", 1171], 471 | "[emoji1f4b5]": ["💲", 1371], 472 | "[emoji1f4c8]": ["📊", 1170], 473 | "[emoji1f4e7]": ["📩", 1144], 474 | "[emoji1f4eb]": ["📪", 1149], 475 | "[emoji1f4ee]": ["📮", 1152], 476 | "[emoji1f4bc]": ["💼", 1161], 477 | "[emoji2702]": ["✂", 1178], 478 | "[emoji1f510]": ["🔐", 1185], 479 | "[emoji1f513]": ["🔓", 1183], 480 | "[emoji1f511]": ["🔑", 1186], 481 | "[emoji1f528]": ["🔨", 1188], 482 | "[emoji1f52b]": ["🔫", 1195], 483 | "[emoji1f4e1]": ["📡", 1219], 484 | "[emoji1f489]": ["💉", 1220], 485 | "[emoji1f48a]": ["💊", 1222], 486 | "[emoji1f6bd]": ["🚽", 1233], 487 | "[emoji1f6ac]": ["🚬", 1251], 488 | "[emoji1f3e7]": ["🏧", 1258], 489 | "[emoji267f]": ["♿", 1261], 490 | "[emoji1f6b9]": ["🚹", 1262], 491 | "[emoji1f6ba]": ["🚺", 1263], 492 | "[emoji1f6bb]": ["🚻", 1264], 493 | "[emoji1f6bc]": ["🚼", 1265], 494 | "[emoji1f6be]": ["🚾", 1266], 495 | "[emoji26a0]": ["⚠", 1271], 496 | "[emoji26d4]": ["⛔", 1273], 497 | "[emoji1f6ad]": ["🚭", 1276], 498 | "[emoji1f51e]": ["🔞", 1281], 499 | "[emoji2b06]": ["⬆", 1284], 500 | "[emoji2934]": ["⤴", 1296], 501 | "[emoji27a1]": ["➡", 1286], 502 | "[emoji2935]": ["⤵", 1297], 503 | "[emoji2b07]": ["⬇", 1288], 504 | "[emoji2199]": ["↙", 1289], 505 | "[emoji1f519]": ["🔙", 1300], 506 | "[emoji2196]": ["↖", 1291], 507 | "[emoji1f51d]": ["🔝", 1304], 508 | "[emoji2648]": ["♈", 1317], 509 | "[emoji2649]": ["♉", 1318], 510 | "[emoji264a]": ["♊", 1319], 511 | "[emoji264b]": ["♋", 1320], 512 | "[emoji264c]": ["♌", 1321], 513 | "[emoji264d]": ["♍", 1322], 514 | "[emoji264e]": ["♎", 1323], 515 | "[emoji264f]": ["♏", 1324], 516 | "[emoji2650]": ["♐", 1325], 517 | "[emoji2651]": ["♑", 1326], 518 | "[emoji2652]": ["♒", 1327], 519 | "[emoji2653]": ["♓", 1328], 520 | "[emoji26ce]": ["⛎", 1329], 521 | "[emoji25b6]": ["▶", 1333], 522 | "[emoji23e9]": ["⏩", 1335], 523 | "[emoji25c0]": ["◀", 1338], 524 | "[emoji23ea]": ["⏪", 1339], 525 | "[emoji1f3a6]": ["🎦", 1348], 526 | "[emoji1f4f6]": ["📶", 1351], 527 | "[emoji1f4f3]": ["📳", 1352], 528 | "[emoji1f4f4]": ["📴", 1353], 529 | "[emoji2716]": ["❎", 1383], 530 | "[emoji2753]": ["❓", 1365], 531 | "[emoji2754]": ["❔", 1366], 532 | "[emoji2757]": ["❗", 1367], 533 | "[emoji2755]": ["❕", 1368], 534 | "[emoji1f4b1]": ["💱", 1370], 535 | "[emoji1f531]": ["🔱", 1375], 536 | "[emoji1f530]": ["🔰", 1377], 537 | "[emoji2b55]": ["⭕", 1378], 538 | "[emoji27bf]": ["➿", 1385], 539 | "[emoji303d]": ["〽", 1386], 540 | "[emoji2733]": ["✳", 1387], 541 | "[emoji2734]": ["✴", 1388], 542 | "[emojia9]": ["©", 1390], 543 | "[emojiae]": ["®", 1391], 544 | "[emoji2122]": ["™", 1392], 545 | "[emoji1f170]": ["🅰", 1411], 546 | "[emoji1f18e]": ["🆎", 1412], 547 | "[emoji1f171]": ["🅱", 1413], 548 | "[emoji1f17e]": ["🅾", 1414], 549 | "[emoji1f192]": ["🆒", 1416], 550 | "[emoji1f194]": ["🆔", 1419], 551 | "[emoji1f195]": ["🆕", 1421], 552 | "[emoji1f197]": ["🆗", 1423], 553 | "[emoji1f17f]": ["🅿", 1424], 554 | "[emoji1f199]": ["🆙", 1426], 555 | "[emoji1f19a]": ["🆚", 1427], 556 | "[emoji1f201]": ["🈁", 1428], 557 | "[emoji1f202]": ["🈂", 1429], 558 | "[emoji1f237]": ["🈷", 1430], 559 | "[emoji1f236]": ["🈶", 1431], 560 | "[emoji1f22f]": ["🈯", 1432], 561 | "[emoji1f250]": ["🉐", 1433], 562 | "[emoji1f239]": ["🈹", 1434], 563 | "[emoji1f21a]": ["🈚", 1435], 564 | "[emoji1f238]": ["🈸", 1438], 565 | "[emoji1f233]": ["🈳", 1440], 566 | "[emoji3297]": ["㊗", 1441], 567 | "[emoji3299]": ["㊙", 1442], 568 | "[emoji1f23a]": ["🈺", 1443], 569 | "[emoji1f235]": ["🈵", 1444], 570 | "[emoji1f534]": ["⚪", 1453], 571 | "[emoji25fc]": ["🔲", 1472], 572 | "[emoji1f539]": ["🔳", 1471], 573 | "[emoji1f3c1]": ["🏁", 1473], 574 | "[emoji1f38c]": ["🎌", 1475], 575 | "🏳️‍[emoji1f308]": ["🏳️‍🌈", 1478], 576 | "[emoji1f1e81f1f3]": ["🇨🇳", 1529], 577 | "[emoji1f1e91f1ea]": ["🇩🇪", 1539], 578 | "[emoji1f1ea1f1f8]": ["🇪🇸", 1551], 579 | "[emoji1f1eb1f1f7]": ["🇫🇷", 1559], 580 | "[emoji1f1ec1f1e7]": ["🇬🇧", 1561], 581 | "[emoji1f1ee1f1f9]": ["🇮🇹", 1595], 582 | "[emoji1f1ef1f1f5]": ["🇯🇵", 1599], 583 | "[emoji1f1f01f1f7]": ["🇰🇷", 1607], 584 | "[emoji1f1f71f1fa]": ["🇷🇺", 1676], 585 | "[emoji1f1fa1f1f8]": ["🇺🇸", 1720] 586 | }; -------------------------------------------------------------------------------- /static/CTBR_Docs1_zh.md: -------------------------------------------------------------------------------- 1 | # ctBridgeBot 使用文档 2 | 3 | 本程序助您桥接`Telegram`和`WeChat`、互通消息,且提供最大化的自定义性! 4 | 5 | _{提示:此处文档不定时更新,若需查看最新版文档,请移步至根目录README.md中指定的,在线版文档!}_ 6 | _上次更新日期:2024-06-01_ 7 | 8 | ## 一. 部署指南 9 | 10 | ### 1. 获取代码 11 | 12 | #### 1/1. Docker方式(一般推荐) 13 | 14 | > 在安装了GNU/Linux的机器上,我们推荐使用Docker以快速启动 (`bootstrap`) 本项目。对于该版本,您只需操作一个文件夹——data。您需要手动管理该文件夹,而其余文件将由Docker镜像管理。 15 | 16 | 1. 首先请在您的设备上寻找合适的地方,存放程序配置文件(程序将会频繁读取,但是数据量不大,推荐放在SSD上),此处假设存放在 `/opt/data_ct`下 ; 17 | 18 | 2. 运行下列命令 ,静待镜像下载完成后,容器即会立刻启动; 19 | 20 | ``` docker run -v /opt/data_ct:/bot/data --name ct cryan2409/ctbr``` 21 | > 意为:将宿主机 /opt/data_ct 文件夹映射到容器内的 /bot/data 下;将新容器命名为ct;采用Docker Hub的 cryan2409/ctbr 镜像。 22 | 23 | 3. 此后,由于data文件夹下找不到有效的配置文件,自检失败,容器将停止运行。请按照后续步骤,手动前往配置目录、修改模板文件,即可再次尝试启动容器。 24 | 4. 待容器成功启动后,请通过`docker logs ct`命令查看容器日志,以便扫描二维码并登录。 25 | 26 | #### 1/2. 手动部署(适合高级用户) 27 | 28 | > 本方法需要您的机器上安装有Node.js运行环境,若无可以前往 [Node.js 官网](https://nodejs.org/zh-cn) 手动下载安装,推荐选择LTS版。请注意,Windows 7 所能安装的最新Node版本12.16仍无法满足依赖项的要求,请谨慎部署,或考虑借助Linux虚拟机。 29 | 30 | 1. 从[GitHub Release](https://github.com/Eddy0644/ctBridgeBot/releases)下载最新版源码压缩包,解压到合适位置,记为项目根目录; 31 | 2. 在项目根目录打开终端/命令提示符,输入 `npm i`,安装依赖项;(若此步出现问题,请检查您的网络/科学上网情况;如果多次重试仍无法完成安装,请向我索要包含了`node_modules`的分发包) 32 | 3. 按照后续步骤,修改配置文件以及proxy后,在终端输入`npm run p` 或 `npm start`,运行代码,等待二维码出现后扫码登录。 33 | 34 | ​ 提示:Docker版中,程序将会自动创建需要的模板文件;所以为了节省您的时间,我们编写了两个脚本,`2.entry.sh`用于 *NIX系统,以及`init-project.bat`用于Windows系统,这两个文件可以在`static`文件夹内找到。运行它们时,会在`data`目录下创建程序所需的两个配置文件的模板 {`CHANGE_ME)user.conf.js`和`proxy.js`},稍后只需对它们做出修改即可。 35 | 36 | ### 2. 配置环境 37 | 38 | 本项目有以下两点需要用户注意,其一是代理地址,其二为用户级配置文件。 39 | 40 | 请进入data文件夹,首先编辑`proxy.js`,该文件指示程序如何连接到Telegram服务器。其已经内置部分HTTP代理地址和端口的示例,请据此修改字符串为合适的值;其中IP地址可以不做改动,端口视您所用的科学上网软件而定,需要http代理端口或mixed端口。 41 | 42 | 如果没找到该文件,可以手动将下述内容填入文件: 43 | 44 | ```javascript 45 | // This is a device-specific proxy setting, which is used to bypass the firewall. 46 | // When updating the code, you could make no change to this file. 47 | // ===================================================================== 48 | 49 | // Please write your HTTP proxy with patterns below, eg. NekoRay 50 | // module.exports = "http://127.0.0.1:7080" ; 51 | 52 | // Or, If you don't want to use proxy at all, just keep empty: 53 | module.exports = "" ; 54 | ``` 55 | 56 | 其次,`CHANGE_ME)user.conf.js`表示这是等待用户修改的配置文件,请首先依据 {3. 初始化配置文件} 编辑该文件(填入必要的凭据以及设置),然后将其改名为`user.conf.js`。 57 | 58 | ### 3. 初始化配置文件 59 | 60 | > 本程序的配置文件采用“叠加”制,即`config/def.conf.js`文件包含所有的配置项及其默认值,并在版本更新时被覆盖;而`data/user.conf.js`包含用户修改过的各配置项,因此只需为def配置的子集,无需包含前者中所有配置项;在启动时程序会先加载def配置文件,然后将user中的配置逐条覆盖,因此若需要修改任何配置项,请在复制到user配置文件后再做出修改。 61 | 62 | 对于想要bootstrap本项目的用户,您既可以从完整的配置文件开始,精简任何您未作出更改的配置项(`user.conf.js`中的内容可以基于`def.conf.js`,或是由初始化脚本创建的“CHANGE ME“文件);也可以从最少配置文件[`minimum_user.conf.js`](https://github.com/Eddy0644/ctBridgeBot/blob/main/config/minimum_user.conf.js)开始,添加您所需要的配置项。 63 | 64 | 以下提供了一些必需配置项的解释,大体按照重要程度排序,并且基于此版本(v3.5.0)所附带`def.conf.js`,仅供参考。若在后续版本中有增加的配置项但本文档未及时更新,请参考`def.conf.js`中所附的英文注释或耐心等待文档更新;在此版块未涉及的配置项,请前往 {配置文件参考} 专题查阅,谢谢。 65 | 66 | #接下来请按步骤操作,分别填充配置文件的各项。(1、2、3、6为必选项,其余为可选项) 67 | 68 | 1. `/tgbot/botToken` 改成您创建的Telegram Bot 的访问 token; 69 | 70 | `/tgbot/botName` 改成该Bot的用户名,此举是为了处理用户发送的带有机器人用户名的聊天指令。 71 | 72 | **提示**:`/tgbot/botToken`这一表示方法指,从`module.exports`开始,首先寻找名为tgbot的对象,再其内继续寻找botToken,而开头`/`的含义类似绝对路径与相对路径,下同。 73 | 74 | > 如果您未曾创建过bot,那么请按照以下步骤操作: 75 | > 在Telegram主页搜索`botfather`,点击Menu按钮选择`/newbot`,然后分别在聊天区输入Bot的显示名称、Bot用户名(以`_bot`结尾),即可在收到的回复的中间部分找到Bot Token. 76 | > 请注意,在运行本项目前,务必点击由`BotFather`发送的、您机器人的链接,打开与机器人的聊天窗口,再点击下方的Start按钮,才能允许机器人向您发送消息! 77 | > 格式如下:`6000000004:AA..(Skip 32 chars)..A` 78 | 79 | 2. `/tgbot/tgAllowList`规定机器人将对哪些用户的消息做出转发或回应,若不在此列表,发出的消息将会被忽略。请向该数组中添加您自己的Telegram ID,以数字形式、无需引号; 80 | 81 | 3. `/class/def`,即 **默认频道**的相关配置,请将`tgid`改为您的Telegram ID / 群id;(默认频道者,也就是不满足任何一条配置规则的其他所有消息都会被递送/转发到这里) 82 | 83 | {% notel primary 关于简称 %} 84 | 85 | 在下文中,可能会把 ”启用了Topic功能的某个超级群组中的某个Thread/topic“ 简写为”某个thread/线程/topic“,将这样的超级群简称为”话题群“; 86 | 也可能会混用”递送“与”转发“:这两个词均表示本程序把一个平台上的消息搬运到另一平台这一行为(一般指从wx到tg) 87 | 88 | {% endnotel %} 89 | 90 | {% note warning fa-bolt %} 91 | 92 | 提示:由于设计问题,本条目暂不支持设置为超级群组中的某个线程`Thread`,若强行设定到话题群的General线程,那么会出现异常行为,请注意! 93 | 94 | {% endnote %} 95 | 96 | > 个人Telegram ID获取方法: 97 | > 在主界面搜索`raw_info_bot`,点击开始,第一行的输出即为所求; 98 | > 群的创建及获取id的方法: 99 | > 首先新建群聊,名称随意填写,然后将您的Bot拉入此群(可能必须在搜索框输入bot的用户名才能看到bot),再在群管理设置中选择`Administrator`菜单,将Bot提升为管理员,并把倒数第二个开关`Remain Anonymous`打开,保存后等待1s,向群聊发送任意一条消息,对其右键即会出现`Copy Message Link`选项,点击复制并粘贴,其数据形似:`https://t.me/c/1954444465/2`,此时请提取中间部分的整数,再在其之前附加`-100`,得到类似`-1001954444465`这样的数,即为此群的id. 100 | 101 | 4. `/ctToken`:由于24年3月9日的[一项举措](https://t.me/c/1684881861/154),在本项目代码开源后,启用了这样一个配置项作为捐赠者识别凭证,如果您尚未成为捐助者,也可以前往 [此网站](https://ctbr.ryancc.top/startTrial) 注册试用版token,以字符串形式填到这里即可。 102 | 103 | {% folding blue::关于 ctToken 的相关说明 %} 104 | 105 | 获取方式:①若您此前已经支持过本人,那么请劳驾前往爱发电平台(传送门: [商品链接](https://afdian.com/item/b6b1c37a2d5011ee88eb52540025c377) )私信中查收我已为您生成的赞助版Token(其实也可以选择在tg上联络我,效果一样); 106 | 107 | ②若您只想试用本程序或者暂时不想支持,那么请前往 此网站 (https://ctbr.ryancc.top/startTrial) 自行生成试用版ctToken,生成时只需填写 您想要注册为token的字符串 和 联系方式(选填) 再提交即可,也无次数限制,因此可放心使用(因此希望各位切勿滥用该生成器,一人一次即可,网安高手们也不要尝试发掘我这破页面上的漏洞,我并没有使用任何SQL) 108 | 109 | 当您完成以上步骤得到token后,请将token填入您的 `user.conf.js` 首部对应区域即可。本程序在微信登录成功后将会把token,微信昵称(并非微信号,不具备定位到个人的效力)以及客户端版本号 发往服务器,仅用作统计与分析程序使用情况,因此请放心。在我看来并不会泄露使用者的隐私,若您不同意可自行删除相关代码。 110 | 111 | [ 同时这一代码可能会在适当的时间点,于控制台中输出捐赠提示文字,不会影响其他功能的正常使用,请忽略。] 112 | 113 | 此外程序中的一些 耗费作者大量精力的功能在未来也会调整为仅限捐助者使用,请谅解。(目前仅有 tg大会员emoji转wx原生表情 这一项为专有功能,其余功能均不受限) 114 | 115 | {% endfolding %} 116 | 117 | 5. `/misc/deliverPushMessage`,指示了订阅号推送以及服务消息应当如何被递送/转发。 118 | - 若设置为 false,则程序将忽略所有的公众号消息; 119 | - 若设置为 true,则这些消息将被发送到特定的聊天。可参考第3步操作,专门新建一个群并把id填入`/class/push/tgid`,但如果懒得建群的话,也可暂时性的设置为您的Telegram ID。 120 | 121 | 6. `/filtering/wxNameFilterStrategy`:您可以在此,让本程序选择性地忽略部分聊天对象发来的所有消息,比如不得不加但不关心其内容的某些wx群,减少程序运行压力。 122 | 123 | 欲使用该功能,请先决定您需要指定的是黑名单还是白名单,据此设置`./useBlackList`为true或false,同一时刻只有一个名单会生效; 124 | 然后请在下方对应名单对应的数组里,以字符串形式追加关键词,结果类似这样即可:`blackList: ["美团","饿了么"],` 125 | 126 | 7. `/notification`:Bark推送的相关设置项,本质是在程序出现问题(例如在连接tg服务器的过程中多次中断)需要用户介入时,向一个指定URL发送GET请求,后者会向用户个人手机发送推送通知以吸引您的注意力;后续正在考虑建立第三方bot,以期通过tg推送这样的消息;如不使用相关功能,请保持默认设置不变,理论上将不会再产生与此有关的报错。 127 | 128 | > `send_relogin_via_tg`指示在使用/relogin命令时,是否要把wx登录二维码发送到默认频道以供扫描。默认为开,若想关闭请将其设为0. 129 | 130 | 8. `/misc/deliverSticker`,决定了聊天消息中遇到wx用户发送的Sticker贴纸将如何递送。 131 | 132 | - 若设置为 false,则程序将忽略所有的sticker,仅在日志中输出提示信息,在tg侧将表现为对方未发送此消息; 133 | 134 | - 若设置为 true,则这些sticker将被发送到特定的聊天。建议另建一群,覆盖`tgid`值并删除`threadId`项,避免干扰正常聊天消息;当然也可指定到某thread中。 135 | - `./urlPrefix`项旨在让用户点击sticker文字链接后可以直接跳转到sticker频道的对应消息,以便查看对方发送了何种表情,但这一功能需要略为复杂的调试。示例为`https://t.me/c/1944700018/1411/"`,具体解析法请见第三步注释。若您未使用话题群,那么该prefix应当只包含一个数字,否则加上threadId应当如示例所示拥有两串数字。最后请以`/`结尾,因为程序还需要在该prefix的末尾追加sticker图片所在的消息id。 136 | 137 | > 在此,您可以考虑新建一个超级群并开启Topic功能,这样一个群可当多个群使用,从而节省您tg聊天列表的空间,具体方法如下: 138 | > 首先新建群,按照上文方法添加机器人,然后在群设置开启Topic功能,随后新建话题,名称随意起,在新话题中发送消息并复制链接,类似`https://t.me/c/1788888875/4/401`,取其中第二组数字`4`,即为`threadId`。 139 | 140 | 141 | 142 | {% notel success 恭喜🎉 %} 143 | 144 | 至此您已经完成了所有的必需设置,现在运行程序应当不会再出现问题。不过为了更好的体验,我们推荐继续按照下列步骤,完成一些影响体验的可选配置。) 145 | 146 | {% endnotel %} 147 | 148 | 149 | 150 | 9. `/class/C2C`,建议您启用C2C功能,并创建多个C2C Pair,这将会大幅提升您在tg侧的聊天体验,同时善用此功能也能让您创建更少的群,节省精力。对于每个C2C Pair,即`/class/C2C/*`: 151 | 152 | - `tgid`数值参考前述步骤,同时如果指定到话题群,那么请设置`threadId`为话题号; 153 | 154 | - `wx`数组的第一项是您期望聊天对象的名字(微信昵称/群名称/您自行设置的备注都可以,我们会按照此顺序依次查找),第二项是`isGroup`,请根据聊天对象是否为群聊自行填入`true`或`false`; 155 | 156 | `/class/C2C_generator`是为了进一步节省您精力而增设的,提供了为同一话题群中的不同话题添加对应的C2C Pair的快捷方法。(比如我个人就创建了一个话题群“From WX”,最常用联系人单独创建群,因为这样可以独立设置头像、显示名称;而次常用的联系人全部作为该话题群的子话题,方便管理。) 157 | 请先算出您话题群的tgid,即`-100`开头的形式,作为键,而值为一个空数组;然后在后者数组中添加数个长度为3或4的子数组,从左到右分别是,threadId,wx名称,wx类型,flag。并请保留`/* |autoCreateTopic Anchor| */`这一注释,其被后续的`/create_topic`指令需要。按照上文操作后,结果应为: 158 | 159 | > "C2C_generator": { 160 | "-1001888888888": [ 161 | /* |autoCreateTopic Anchor| */ 162 | [1, "name of group 1", "Group", "flags_here"], 163 | [4, "name of person 1", "Person", ""], 164 | ], 165 | }, 166 | 167 | 10. `/filtering/wxContentReplaceList`,如字面意思,wx中收到的消息会经此表过滤后再转发到tg。示例中的写法会将微信中的部分自带表情如发怒、捂脸等替换为(通用的)emoji,如果您不想在输出消息中看到emoji可以清空该数组; 168 | 169 | > 请注意如果您登录微信的手机语言为中文,可能还需要另外将 [Pout] [Facepalm] 等英文描述替换为 [发怒] [捂脸]等中文描述,此外还有[BadLuck]=[骷髅]等对应关系,我们暂未添加,未来将推出列表供用户自行选择; 170 | > *由于我们从上游服务器接收的就是中英文混搭的版本,因此此项的设置取决于您手机语言,后续会改进……* 171 | 172 | 173 | 174 | *[ This section is recently updated on 2024-05-16. ]* 175 | 176 | ## 二. 使用教程 177 | 178 | ### 1. 启动流程 179 | 180 | 启动程序后,稍等片刻,程序将检查`data`目录下是否有`ctbridgebot.memory-card.json`文件,若有则尝试使用上次的登录信息直接登录,一般能在数秒内完成;如果登录失效了或无登录会话信息,则会在控制台输出一幅二维码和二维码图片链接。手机扫码之后完成登录,日志输出形如`I/wx Contact已登录.` 181 | > a)复制"二维码图片链接"到浏览器,可以输出一张二维码图片在浏览器以供手机扫描; 182 | > b)请注意,由于微信官方的迷之限制,登录二维码在手机上无法使用**非摄像头以外的**任何形式识别。也就是说,无论是从相册选取二维码还是在聊天中长按图片识别二维码都无法完成登录(显示二维码失效)因此您可能必须使用两台设备才能完成登录。因此建议不要在不方便的场合通过远程手段重启本项目,以免登录失效却又无法重新登录。 183 | > c)由于需要在后台启动一个浏览器进程,所以启动阶段花费的时间在10s-30s不等。为了追踪启动过程所耗时间,启动浏览器所需时间将会输出在第一条日志旁,该时间介于2s-15s不等,视设备性能而定。 184 | > d)如果在微信登录消息之前输出了红色的如下内容(`Unhandled rejection RequestError: Error: Client network socket disconnected before secure TLS connection was established`),说明代理服务器连接出现问题,请查看代理服务器设置; 185 | > 若输出的错误信息如下(`Error: net::ERR_TIMED_OUT at https://wx2.qq.com?lang=zh_CN&target=t`)则说明您设备的网络出现问题,无法连接到微信服务器。请即刻停止运行,排除网络问题后重启,可首先检查设备DNS是否异常。 186 | > e)如果弹出微信登录二维码后您在30s内未处理,则会通过配置文件`/notification/prompt_relogin_required`项向手机发送重登录提示(内容可自定义),届时请及时处理,以免漏收消息。 187 | > f)如果程序运行中出现日志信息`[ERR] Polling - EFATAL: Client network socket disconnected before secure TLS connection was established`表明最近一次向tg服务器的poll操作(拉取用户发送的消息)失败,意味着您代理服务器出现网络波动;如果出现频率较低(如30s一次)则不会产生太大影响,但是还是建议切换到更好的代理/节点;如果在10s内连续出现两次,则程序会按照配置文件`prompt_network_problematic`项向手机发送断网提示,这种情况下若不及时处理可能会影响消息递送的及时性甚至漏收消息等。 188 | *(有些科学上网节点提供商限制同时在线设备数,也就是同时运行的客户端数量,所以当您网络出现异常时除了切换节点也可考虑排查 是否超出了在线设备限制)* 189 | 190 | ### 2. 接收wx消息 191 | 192 | 程序运行后,您微信上的所有消息即会按照配置文件中所定义的,通过本程序“递送”到Telegram。 193 | 194 | #### a.文字消息及合并 195 | 196 | 归属于默认频道(参考部署指南.3)的文字消息,根据是否来自群,有以下两种显示形式:`📨[#备注名] <消息内容>` / `📬[说话者微信昵称/#群名称] <消息内容>`。 197 | 198 | 请注意,本项目并不会查询某聊天对象在微信中是否已经设为免打扰状态,换言之,您**必须** 在配置文件中指定黑/白名单才能按您的需求屏蔽某些来源的消息。 199 | 200 | “合并消息“功能打开时(默认打开),依据配置文件中设定的时间间隔,当新消息与上一条消息的时间之差不超过设定值时,新消息将会被附加到旧消息的末尾,并且对旧消息的格式作出相应修改以提升观感。 201 | 例如聊天对象是某联系人A,间隔默认为15s,若他的第二条消息内容为2且在上一条消息15s内发出,则上一条消息从 `📨[#联系人A] 1` 变为 如下,也就是在保留首部标题与联系人信息的同时,将他的所有新消息前加上时间或序号(均可自定义)。 202 | 203 | > 📨⛓️ [#联系人A] - - - - 204 | > [13:35:35] 1 205 | > 2|→ 2nd msg 206 | > 3|→ 3rd msg 207 | 208 | 若聊天对象是群聊,程序会舍弃时间显示,改为在 `[ ]` 内显示说话者名称,并将首部标题内符号‘📨’替换为‘📬’。 209 | 210 | 此外,当`[ ]` 内容一致时(例如群内同一说话者连续三条消息且未被其他人打断,或某联系人在1s内发送多条消息),程序不会在新消息中显示`[ ]` ,反而是采用等宽字体显示其计数(如`2|→`)。这可以表明该说话者连发了多少条消息。此项可在配置文件中`/c11n/titleForSameTalkerInMergedRoomMsg`中自定义。 211 | 212 | > 请注意,此合并功能目前仍是全局共享,也就是说如果您有两个以上的群会经常输出消息,那么合并功能将经常被打断(因为第二个群合并的过程中第一个群的新消息会打断这一过程,并重置记忆,导致第二个群的新消息将无法再被合并)。我们正在尝试重写这部分代码以使“合并”功能的行为更加符合预期。 213 | 214 | #### b. 图片/文件/视频 多媒体消息 215 | 216 | - 图片消息将会先被下载到`/downloaded/photo`中,以`${消息发出者alias}-${wxdata.filename}`形式保存,再发送到其对应的接收对象中,并且这些图片的标题(`caption`,即图片下方出现的文字)显示为 ‘ from 发送者名称 ’(规则同上述a,根据是否为群/是否为C2C拥有不同行为) 217 | - 对于文件消息,将先向您发送一条提示,包含该文件的发送者、原始文件名和大小。形如:`📨[#联系人] 📎[文件.docx], 0.101MB.` 218 | - 若大小满足配置文件中`wxAutoDownloadThreshold`项设置的最小阈值,则会自动尝试下载并显示`Trying download as size is smaller than threshold.`,在下载完后将删除原先的提示信息。 219 | - 若大小大于上述阈值,则显示`Send a single OK to retrieve that.` 此时如果您仍希望接收此文件,则需对此提示消息点击回复,并发送`OK`(全大写或全小写均可),此后文件将会在后台下载并遵循上述步骤发送到tg。 220 | - 对于视频消息,由于微信某次更新取消了视频大小的显示,故软件只能自动下载。在`🎦(Downloading...)`提示出现后,请耐心等待,视频正在后台下载并发送到tg,其中大小大于49MB的将不会被发送;反之则会即时发送。(经测试,此过程主要取决于科学上网的速度。) 221 | 222 | #### c. 推送消息 223 | 224 | 所有的公众号文章消息(是否收到来自某公众号的推送消息,取决于是否在微信设置中选择“接收文章”/“Receive Articles”)都会被`handlePushMessage()`处理成富文本形式发送到配置文件中`/class/push`指定的push频道。具体格式包含订阅号名称(标题上带有#号以便快捷查找其所有文章),每篇文章的标题(点击标题则打开链接)及其简介(取决于公众号运营者的填写,或长或短,甚至没有;以斜体展示)。 225 | 226 | 点击链接后可在任意浏览器查看,但无法查看评论(微信侧限制,非登录用户无法查看评论) 227 | 228 | #### d. 语音消息 229 | 230 | 语音消息同样会被先下载到`/downloaded/audio`中,以`日期-时间-备注名.mp3`形式保存。 231 | 232 | 如果在配置文件中开启了语音识别功能`/txyun`并提供了有效的API Key,则 233 | 234 | - 每条语音在进一步发送前都会先发往腾讯云“一句话识别”系统(该系统能在数秒内给出语音转文字结果,支持多种方言且最大长度为60s,刚好满足最大长度为60s的语音这一情况),得到转录文字后附加到语音消息的`caption`部分,发送到tg。但请注意,由于tg app的限制,手机端在通知消息预览中无法看到已转出文字的内容,只显示`🎤 Voice Message`,此时您需要点击通知进入app才能看到转文字内容。 235 | - 否则,`caption`部分将只包含说话者信息。 236 | 237 | #### e. 文字中包含的引用消息 238 | 239 | 在UOS版本微信,如果对端给您发来带有引用消息的消息,那么默认会显示为如下;很明显观感很差且占用过多行。假如我们试图把这样的引用表现为tg消息间的互相回复的话,不仅与“合并”功能冲突,且技术难度较大,需要把所有历史消息都存入数据库才能做到,同时也造成很多不便。因此我们做了这样的特殊处理,也就是将此部分转换为消息末尾的一行斜体小字`(说话者💬"内容")`,这样就可以清晰地表示消息间引用关系。其中“内容”部分是被引用消息的内容的前8个字符,您可以根据该前缀向上搜索找到对应的原消息,至于“说话者”是该用户的昵称还是群名片,这一项由微信侧决定,一般情况下是后者。 240 | > "说话者A: 第一条消息" 241 | > — — — — — — — — — — 242 | > 第二条消息 243 | 244 | 245 | ### 3. 在tg上发送消息 246 | 247 | 不仅是wx侧,本项目在tg侧也拥有良好的操作性,支持多种消息格式与指令。 248 | 249 | {% notel info fa-info 提示 %} 250 | 本文档初次撰写时,C2C尚未发展完全,仍以def频道为主,而在后续开发中,各项功能均向利好C2C的方向发展,因此此处介绍的很多指令,比如查找和锁定功能,实际用处并不大。我们也推荐用户多使用C2C与`/create_topic`指令(将def频道中的聊天对象自动转换到C2C),这样便可以为每个聊天定义更加详细的规则,以增强聊天体验。 251 | {% endnotel %} 252 | 253 | #### a. 支持的消息种类 254 | 255 | (以`/class/def`频道举例) 256 | 257 | - 文字消息,将会经`tgContentReplaceList`过滤后,直接发送给“上一个聊天对象”,若无上次聊天的记录则会发出提醒,`Nothing to do upon your message`,这时这条消息将会被忽略。 258 | 259 | {% note default %} 260 | “上一个聊天对象”,代码中为`lastTalker`,指代def频道中上次收到消息的对象,若无已启用的`/lock`,则def频道中每收到一条消息,`lastTalker`都会被更新。 261 | {% endnote %} 262 | 263 | ​ 如果这条消息是对另一条消息的回复,则这条消息会被直接发送到“被回复的消息”的发送者处。 264 | 265 | ​ (例:我的“上一个聊天对象”是B,但我对“A”发给我的消息选择了“回复”,并且输入了我想说的话,那么这时这条消息会被发送给A而非B。) 266 | 267 | - 图片/文件/视频消息,将会被下载到本地后再发送给“上一个聊天对象”,所以如果需要准确地发送给某个人(以免在发送过程中有其他人的消息插入导致消息被错发),请利用c)节提到的lock功能确定您想要发送的对象。 268 | 269 | - 语音消息 和 Sticker(贴纸)消息,请详见d节介绍。 270 | 271 | 除了上述种类以外,其他所有的消息都会被忽略,包括但不限于tg侧的群名称/头像更改事件、投票、在移动端通过长按语音按钮拍摄的`VideoNote`……但会在日志中体现。 272 | 273 | #### b. 使用预定义的指令 274 | 275 | *(现有的博客系统的目录不支持h4标题,为了方便用户点击目录跳转,该部分已移动为单独的一小节,请前往下方 {tg侧指令一览} 查阅 )* 276 | 277 | 278 | #### c. 查找、行内查找与相关指令 279 | - 本程序支持交互式查找聊天对象,具体方法是直接发送`/find`指令,在得到程序反馈`Entering find mode; enter token to find it.`后,直接输入您想要查找的对象的微信昵称/群名称/您设置的备注,(由于UOS微信的限制,完成此步骤需要数秒)在查找成功后将会回复消息`Found Person: name=……`,此时若“聊天对象”锁未启用,则“上一个聊天对象”也会被设置成当前查找到的目标。 280 | 281 | (在未来的升级中,此项可能升级为模糊查找,即列出联系人列表并尝试在其中模糊查找,给可能匹配的每一位都分配上唯一id,并将后者返回到用户侧,这样做可以避免名称中出现非法字符等等) 282 | 283 | - “行内查找”指的是,当需要向一位新的聊天对象发送消息时,将查找和发送两步合并到一条消息内完成。具体操作方法是,首先输入查找对象的名称(“微信昵称/群名称/您设置的备注”三种情况均可接受,详见上文),紧随其后输入两个冒号,中英文均可;接下来请换行后直接输入您要发送的消息,最后发送即可。当您看到聊天界面顶部显示机器人的状态从“typing”变为“choosing sticker”则表示查找并发送成功。 284 | 285 | > 本功能的原理是先调用查找函数找到您欲发送消息的唯一对象,将其设为“上一个聊天对象”后按照正常消息的步骤再发送给目标。因此当“聊天对象”锁已启用时,本功能可能无法正常工作。(这也是一个问题,将在以后的更新中修复【TODO】) 286 | 287 | - 另外,您可以通过“@锁”方式,灵活地设置当前的聊天对象。例如,当您一时间收到来自两个联系人的多条消息时,为了避免互相干扰,您可以先选中其中一位的消息,回复它“@”,此时该联系人会被设置为“上一个聊天对象”且“聊天对象”锁也会被启用(并设为2状态),接下来您直接发送的所有消息都会被转发到该联系人。此后,只需选择另一位联系人的消息并同样回复“@”,即可专心回复ta的消息。并且通过这种方式设置的锁,在您“回复”其他消息时会自动解除。 288 | 289 | 290 | 291 | #### d. Sticker与语音识别 292 | 293 | - 语音消息,如果腾讯云语音识别已启用,则程序会输出您这段语音的识别结果并设置为`monospace`样式,此时您可以点击文字部分自动复制语音识别后的结果。但是,这条语音并不会被转发到wx。 294 | 295 | - 对于来自tg的Sticker(贴纸)消息,按照默认配置,此贴纸将会取缩略图后由`sharp`库 转换为gif,以图片形式发往wx。根据配置的不同,也可能通过upyun(又拍云)实现图片格式的转换。 296 | 297 | > a)由于tg的贴纸要求为webp格式,而wx不支持前者,所以需要设法转换之。已知又拍云提供简单的方案(指不需要通过事件id和查询请求确定转换状态)可以完成上述操作,因此需要您提供此upyun API Key。 经热心用户提醒,`sharp`库利用`libvips`可以高效地实现图片格式的转换,因此为了降低用户使用成本,我们已将默认配置改为sharp方案,若未出现问题则不需要调整此选项。 298 | > b)请注意,目前您的**所有媒体消息中携带的`caption`都不会被递送**。因此,请不要在媒体消息的`caption`中填写您想要传达的讯息,因为您可能需要复制后以纯文本形式单独发送。这一问题日后将会被解决。【TODO】 299 | > c)如果您购买了Telegram Premium并发送了第三方emoji表情,那么它将会被解析成普通emoji(这一行为取决于emoji上传者为其设定的替代emoji是哪一个)并发送。所以如果想要发送微信原生emoji,您将需要利用此项`tgContentReplaceList`达到目的。 作为大会员用户的替代方案,请添加 [这个Emoji Pack](https://t.me/addemoji/Wechat_Emoji_by_Andy) ,然后便可在聊天中随意使用其中的表情,均会被正确识别。 300 | 301 | 302 | 303 | #### e.机器人行为的指示 304 | 305 | ​ 本节将介绍,在您的特定行为下,机器人将会更新自己的`chatAction`到何种状态,以便您无需查看日志即可确定消息是否被成功转发,或者了解程序是否正在处理消息。 306 | 307 | {% note info %} 308 | Telegram Bot API中的 [`sendChatAction`](https://core.telegram.org/bots/api#sendchataction) 方法允许开发者把机器人端正在发生的事,以“正在输入状态”的形式告诉用户。 309 | {% endnote %} 310 | 311 | - 当您从tg侧发送文字消息时,机器人状态将在发送后变为`choose_sticker`。 312 | - 从tg发送多媒体及文件消息时,在从tg服务器下载资源到本地的阶段,状态为`upload_${文件类型} `;在把资源上传到wx服务器的阶段,状态为`record_video `;发送后状态变为`choose_sticker`。 313 | - 当正在执行查找联系人操作时,状态将变为`typing`,并在查找成功后消除。 314 | - 当您对待下载的wx文件消息做出确认并下载时,在下载期间状态将为`typing`;成功后状态变为`choose_sticker`。 315 | - 当接收到大部分的“预定义指令”时,状态都会变为`typing`。 316 | - 收到来自tg的语音时,状态将变为`record_voice`。 317 | - * 收到来自wx的语音消息时,状态将变为`record_voice`;对于wx视频消息同理。 318 | 319 | ​ 320 | 321 | ### 4. tg侧指令一览 322 | 323 | 以下指令可在聊天框中发送,也可打出`/`后点击命令列表选择目标指令。 324 | 325 | 请注意,绝大多数指令只能单独使用,在本消息“回复”了其他消息的情况下均不会生效,而是会原样发送到wx,因此发送前请务必检查。 326 | 327 | - `/clear`,即“软重启”,主要作用是调用`softReboot()`,清除大多数记忆数据,以减少bug以及提升性能。发送后程序会返回`Soft Reboot Successful. Reason: User triggered.`,表示软重启成功。这条提示消息会在数秒后自动消失。 328 | 329 | > 主要清除的数据:“上一个聊天对象”、“合并”功能作用对象、定时函数错误计数、“合并”错误致回退次数计数、全局网络错误次数计数 330 | 331 | - `/spoiler`(本消息只能在“回复”图片消息时起作用)为当前指向的图片消息添加遮罩,其实是借用tg的相关功能,为图片添加一层模糊(在点击后消除),很好地在聊天窗口和消息记录列表保护您的隐私。设置后永久有效。 332 | 333 | - `/lock`,可开关“聊天对象”锁的状态。当此锁: 334 | - 值为0时表示不启用锁; 335 | - 值为1时表示锁是通过本指令设置的,期间将屏蔽所有新来消息对“上一个聊天对象”这一变量的更改,不过您还是可以通过“引用消息”的方式向其他聊天对象发送消息; 336 | - 值为2时表示锁是通过c)节介绍的“@锁”方式设置的,此时同样会屏蔽“上一个聊天对象”的更改,不同的是,在这种情况下若您对任意消息发送了引用,则锁就会被解除。您也可以使用本`/lock`指令自行将“2状态”的锁重置回“0状态”。 337 | 338 | - `/slet`:在默认频道中使用“上一个显式指定的聊天对象”去覆盖“上一个聊天对象”。这么说可能有些抽象,举例表示,如果默认频道中存在分别来自A、B两位的数条消息,那么可先“回复”A的某条消息,然后输入该指令,这样A即为“上一个聊天对象”,后续直接发送的消息也将被递送到A处;此后再回复一次B的消息,再次使用此指令,即可让其后所有未显式指定回复的消息都发往B处。 339 | 340 | - `/help`,可在当前聊天界面输出一个带有更多指令的列表(这么做主要是为了让tg的指令列表不太累赘) 341 | 342 | 以上命令均可在Telegram的Bot命令列表中找到,而如下命令只能通过`/help`指令间接发起,当然您直接在聊天框输入也是允许的。 343 | 344 | - `/log`,可在当前聊天界面得到程序目前一定数量的日志。 345 | 346 | > 由于tg侧限制,消息不宜太长,因此默认输出的日志数量是末尾1000字符,您也可以通过`/log 2000`的形式手动覆盖输出的日志数量。 347 | > 本消息不会自动删除,查看完日志后请手动删除本消息。 348 | 349 | - `/placeholder`,可在当前聊天窗口输出一段空白消息,以将上方的其他消息“顶出”视野,从而保护隐私。此功能在需要将设备交由他人查看时非常有用。关于此功能的升级也正在规划中,届时将可以在任意消息之间插入空白消息。 350 | 351 | - `/drop_on & /drop_off`,可开启Drop模式,在此模式下程序将忽略来自tg侧的任何其他消息,启用此模式后(可以机器人正在typing为启用成功标志)您可以放心地在本程序所绑定的群聊里发送任何消息,均会被程序忽略,具体忽略了多少条消息可在日志中找到。请注意,为了防止忘记关闭此开关导致无法发送消息到wx,我们设定了默认为500s的自动关闭定时器`/misc/keep_drop_on_x5s`,达到时间后该模式将会被关闭,因此请时刻留意Drop模式是否仍在开启状态。 352 | 353 | - `/sync_on & /sync_off`,可开启Sync模式,在此模式下您在wx中发送的消息也将会被程序转发,就像是来自群内另一成员一样,启用此模式后,您通过手机wx发送的消息(如语音等)也会被程序记录甚至转成文字,有助于消息记录的完整性。 354 | 355 | - `/reloginWX_2`,旨在一键快速重启程序并刷新wx状态,发送指令后wx登录信息即被清空,然后程序终止。如果部署时使用了Docker等管理技术,那么程序会被自动重启,届时只需重新扫码即可完成重新启动过程。由于直接重新启动很有可能保留先前的登录信息,而先前处理过的聊天记录在启动时会被wx全部发送过来,虽然他们在校验后会被丢弃,但仍会造成初期卡顿的现象,因此该功能还是很有必要的。 356 | 357 | - `/reboot`:类似重登录指令,重新启动程序,但保留登录信息。可以每隔一段时间发送此指令,提升稳定性,因为基于浏览器的puppet方案在连续运行时间过长时可能会出现bug。 358 | 359 | {% note default %} 360 | 经测试,wx服务器中给每次UOS微信扫码登录后的session设定了为期7天的超时时间,也就是说,距离上次扫码登录的时间168小时以后,会话将立刻失效,但并不会有任何提示。在本程序内如果会话“失效”,那么将无法收到任何消息,也无法正常发出任何消息(但不会有报错)。因此,定时`/reboot`可以使程序稳定运行的时长达到7天(若不定期重启的话,每隔几天可能就会出现奇奇怪怪的浏览器错误,此时如果未及时发现错误并重启,导致程序与wx服务器之间的心跳包中断,那么几小时后会话将直接“过期”,再次启动时就必须重新扫码登录了),但必然无法超过这个时限。 361 | {% endnote %} 362 | 363 | - `/create_topic `,本功能旨在快速将联系人绑定到配置好的超级群的新话题,这样不用重启程序也能应用新的C2C设置,提升聊天体验。假如默认频道中收到了来自新联系人的消息,您想要将ta保存为C2C,这样便可以更愉快地聊天了,那么可直接发送本指令,稍后,在预设的话题群中将会出现一个新话题,以联系人名称命名;随后您只需切换到该话题并加以回复即可,以后的所有聊天行为均和其他C2C一样;与此同时,新C2C的相关信息也已写入用户配置文件的`/class/C2C_generator`项中,从而保证重启程序也不会丢失该条目。 364 | 365 | 为了能够正确使用此功能,您需要有一个用来承载新话题的话题群组,将其群id写入`/class/C2C_generator`中,然后在数组中括号内保留`/* |autoCreateTopic Anchor| */`注释,新创建的C2C条目会在编码为JSON后插入该注释之后,这样便实现了重启不丢失,同时方便用户进行更改。在部署阶段,您设置好该注释后,可以测试一次本指令,若新C2C的功能正常,且在重启后未出现配置文件无法解析的错误,那么以后便可放心使用此功能了。 366 | 367 | *关于本指令,以后将改进的内容:自动为所有新联系人创建C2C条目;在创建C2C后将先前def频道中的相关消息转发过去;。* 368 | 369 | 370 | 371 | - `/try_edit `:这是一个实验性功能,以如下结构“回复”某条来自机器人的消息:{`/try_edit 112233`},那么被回复的消息将会被直接覆盖为`112233`。若tg的相关规则限制了机器人对超出时限的历史消息的修改,则本指令会失效。 372 | 373 | - `/eval` *Debug指令,需预先在配置文件`debug_evalEnabled`启用本项:在该命令后追加js代码并发送,即可在`BotIndex.js`的上下文中执行该代码。若您并非对JavaScript语言有较好的理解,请不要启用该功能,以免引入远程代码执行漏洞。 374 | 375 | 376 | 377 | ## 三. 配置文件参考 378 | 379 | 本章将对模板配置文件`def.conf.js`中的各项进行尽可能详细的解释。标星号者表示作者认为您用到它的概率不大,可以忽略。 380 | 381 | {% note default %} 382 | 由于本程序仍在 快速发展,因此配置文件的变动可能会比较频繁,我将尽力做到至少在两个次版本号(如3.4.0->3.5.0)之间更新本章节内容,若下文有与dev分支最新源码不匹配者,请参考后者之英文注释,谢谢! 383 | {% endnote %} 384 | 385 | - `/ctToken`节请参阅 { 部署指南 --> 初始化配置文件 } ,并务必填写您自己的 ctToken。 386 | 387 | ### /tgbot 388 | 389 | - `botToken`:请填写从`@BotFather`处取得的Bot Token,以数字+冒号开头。 390 | 391 | - `botName`:与上述token对应的Bot的用户名。 392 | 393 | - `tgAllowList`数组:以数字形式存储有权向Bot发送消息的tg用户的id。对于此列表以外的tg用户,无论是对Bot私聊还是在有Bot的群组中发言,均会被忽略。 394 | 395 | - `webHookUrlPrefix`:使用`webhook`启动tg bot时,将需要此处的参数。 396 | 397 | {% note default %} 398 | (Incomplete Function) 当您想要以`webhook`而非`polling`模式启动tg bot时,将需要通过内网穿透等方案自行把本地的监听端口`8443`暴露到公网,并得到映射后的URL。例如,启动项目时使用的命令为`npm run h1`则webHook号为1,此时若`https://example.com/webHook1/`可以指向 `https://localhost:8443`,那么此处请填写`https://example.com/webHook`。此外,还需自行申请SSL证书并按照`src/init-tg.js`内的代码,将公私钥放置于特定位置。 因此不建议使用此功能,一是配置过程十分繁琐,二是将更难发现潜在的网络问题。如需要换用webHook模式,可联系作者。 399 | {% endnote %} 400 | 401 | - `statusReport`:该项定义了`/info`指令所生成的报告应存放在哪里。 402 | 403 | - `switch`定义整个项的开关状态; 404 | 405 | - `host`将要收到报告的目标服务器的域名; 406 | 407 | - `path`为接收脚本所放置的具体位置。 408 | 409 | {% note info %} 410 | 411 | 本功能开发于初期,现已不建议使用,作用是将当前运行环境中的部分变量信息以及部分日志 通过Web服务器的中转发往tg用户。 412 | 413 | 使用此功能需要您拥有任意安装有PHP的Web服务器,并把`/static/ctBotReport.php`放置到任一可执行的目录中。随后按照实际情况填写配置文件即可。 414 | 415 | {% endnote %} 416 | 417 | - `polling`: 418 | 419 | - `pollFailNoticeThres`:该项定义了将在多少次失败的telegram poll操作后发出网络问题警示。 420 | - `interval`:该项定义每两次poll之间应间隔的毫秒数,如果您希望机器人可以更快地接收来自tg的消息,则可调整此项,但过小的值可能造成网络异常出现频率加大。 421 | 422 | ### /class 423 | - `def`:默认频道的配置。类型为`connObj`,但不支持话题群,请单独为其创建群,或采用私聊。 424 | 425 | {% note info %} 426 | 本文档中会经常涉及到`connObj`及它的子类。最简单的形如这样:`{tgid:-100010203}`;若为话题群内的话题,则可以添加`threadId`属性,变成这样:`{tgid:-100010203, threadId: 5}`。 427 | {% endnote %} 428 | 429 | - `push`:推送频道的配置。类型为`connObj`。 430 | 431 | - `C2C`数组:包含各C2C配置资料。其中每个`C2C Pair`应如下所示: 432 | 433 | ``` 434 | { 435 | "tgid": -1001006, 436 | "wx": ["wx Contact 1's name", true], 437 | "flag": "", 438 | } 439 | ``` 440 | 441 | 也就是在`connObj`的基础上增加了wx数组和flag字符串。具体内容请参见前文 {初始化配置文件}。 442 | 443 | - `C2C_generator`对象:包含 启用了生成器的各话题群及其详细配置。在启动时,它们会被自动转换为C2C Pair格式,但在配置过程中可降低用户工作量。具体内容也请参见前文 {初始化配置文件}。 444 | 445 | ### /filtering 446 | - `wxFindNameReplaceList` :*定义了在使用查找联系人功能时,需要替换哪些字符串。原意是将需要频繁查找的对象名进行简写并存储短语,下次查找联系人只需输入短语即可。 447 | - `wxContentReplaceList`:定义了从wx接收的消息中,哪些字符串需要被替换。举例:将以文字形式显示的wx自带表情(如`[Pout]`)转换为自设emoji。 448 | - `tgContentReplaceList`:与上一项类似,但是替换对象是从tg接收的消息。举例:在未购买Telegram Premium的情况下,将一些emoji手动映射为wx自带emoji,这样便可在聊天中方便地发送wx表情了。 449 | - `wxNameFilterStrategy`:前文 {初始化配置文件} 中有提及,该功能可以屏蔽或只允许部分对象的消息经过转发,以提升程序效率。 450 | - `useBlackList`:true表示仅启用黑名单;false表示仅启用白名单。 451 | - `blackList & whiteList`:略 452 | - `wxMessageExcludeKeyword`数组:*当wx消息中包含任一关键词时,丢弃整个消息。 453 | - `wxPostOriginBlackList`数组:定义了屏蔽其推送消息的订阅号名称列表。 454 | 455 | ### /notification 456 | 457 | 本项中的大多数功能需要您先配置好Bark才能使用;如果您拥有自建的、以HTTP GET方式就能触发的推送服务,那么也可在此处设置。 458 | 459 | - `baseUrl`:Bark推送服务的URL。该URL应当可以在直接访问的情况下成功推送。可以包含通知标题。形如`https://example.com/BridgeBot_WARN/`。 460 | - `default_arg`每次执行推送时,应在URL末尾附加的参数。对于Bark,可以在此定义通知分组信息和推送消息头像,当然也可采用默认配置。 461 | - `prompt_network_problematic`:当网络出现问题(特指Polling)而又恢复了时,发送的推送通知的正文部分。 462 | - `prompt_relogin_required`:当需要重新扫码登录时,发送的通知内容。 463 | - `prompt_wx_stuck`:当wx puppet发生意外崩溃导致大概率无法正常工作时。 464 | - `prompt_network_issue_happened`:当网络出现问题且没有自动恢复时。 465 | - `incoming_call_webhook`函数:该项应当在被调用时返回一个URL,当收到来自wx的通话时,该URL将被访问。如果仍使用Bark作为推送服务的话,您需要将完整的URL填写至此。 466 | - `send_relogin_via_tg`:布尔型,为1表示将会在需要重新扫码登录时将二维码发送到默认频道;为0表示禁用该功能。 467 | 468 | ### /misc 469 | - `deliverPushMessage`定义是否转发订阅号推送消息:设为false表示不转发;true表示转发到`/class/push`频道;设为`connObj`会把消息转发到前者。 470 | 471 | - `deliverSticker`定义是否转发wx侧的动画表情:设为false表示完全不转发;也可设为`connObject`+`urlPrefix`,其中`urlPrefix`请将tg目标的相关参数填入URL中,以便在实际聊天中,点击Sticker字样可以跳转到由`deliverSticker`指定的动画表情所在地;具体用例请参照配置文件模板。 472 | 473 | - `deliverRoomRedPacketInAdvance`:是否监听所有群聊中的红包并通过def频道发送提醒;0表示不启用;1表示仅在“未被黑白名单过滤掉的群聊”中监听;2表示所有群聊。 474 | 475 | - `mergeResetTimeout`:该对象定义了对于私聊和群聊,当两条消息的时间间隔大于多少秒时,重置上次合并状态,直接发送消息。 476 | 477 | - `onceMergeCapacity`:该对象定义了单次合并中,满足何种条件时重置合并状态并重新开始。 478 | 479 | - `timeSpan`:当第一条消息距今多少秒时,重置合并。 480 | - `mediaCount`:当合并消息中包含多少个媒体消息时,重置合并。此项可避免聊天中包含过多图片时,文字与其所对应的图片距离太远。 481 | - `messageCount`:当合并消息中包含多少条消息时重置。此项同样是为了避免合并后的消息过长。 482 | 483 | - `debug_evalEnabled`(调试用途)启用`/eval`命令,使您可以在tg中远程执行代码。该功能可能引入极大的安全隐患,无必要请勿启用。 484 | 485 | - `display_ctToken_info`:控制是否在程序启动时,显示与您的`ctToken`相关的信息。设为0来阻止它们。 486 | 487 | - `debug_add_console_timers`(调试用途)是否在程序执行特定耗时操作(例如寻找wx联系人)时,在控制台显示该操作所耗时间。有助于您追踪耗时操作的用时以及设备性能。 488 | 489 | - `savePostRawDataInDetailedLog`:*是否在`msgDT`日志中保存收到的订阅号推送消息的原始版本。 490 | 491 | - `addHashCtLinkToMsg`:是否在wx侧发来网页链接时,在链接末尾附加`#ctLink`标志,因为一旦启用了`deliverSticker`,那么聊天中将充斥大量 t.me 链接,如果需要方便地查找wx好友发来的链接,那么可以启用此项。 492 | 493 | - `passUnrecognizedCmdNext`:是否把程序未能识别的tg聊天命令(例如,以/开头者)当成文本继续发送到聊天对象。若关闭,则在发送以`/`为开头的消息时可能会被误判为命令。(当然,在斜杠前加上空格即可避免) 494 | 495 | - `service_type_on_webp_conversion`:在tg sticker的格式转换过程中使用何种服务。2为采用本地模块`sharp`,推荐使用;1为又拍云在线检测,需要对应的API Key填写于`/upyun`中;0为不转换,直接把`.webp`格式的图像发往wx,可能会在他人设备中显示为无法预览的文件。 496 | 497 | - `add_identifier_to_merged_image`:是否在合并消息包含的图片消息的caption部分,添加四位随机数以标识图片和文字的对应关系,有助于查清对方发送图片的次序,因为转发过程中可能因网络等问题导致对方发来的媒体消息失序。设为0表示不启用;设为1表示仅在群聊中适用;设为2则适用于所有聊天。 498 | 499 | - `status_report_interval`*:控制每两次控制台`status Report`之间需要间隔至少多少秒。 500 | 501 | - `keep_drop_on_x5s`*:`/drop_on`打开时,若一直未手动关闭,则在多少个5秒后系统将自动关闭该功能。 502 | 503 | - `wxAutoDownloadSizeThreshold`:wx侧收到的文件中,大小大于多少字节的文件将不会被默认下载,而是发送提示给用户询问是否下载。默认值为`3 MiB`。 504 | 505 | - `tgCmdPlaceholder`:在`/placeholder`指令发出后,向当前聊天框输出什么样的占位符。保持默认即可。 506 | 507 | 508 | 509 | ### /chatOptions 510 | 511 | 本节定义了默认情况下每个C2C(以及默认频道)的特定选项值。 512 | 513 | - `mixed`:是否允许混合消息制,即以`*`开头的任何tg消息都将被忽略,适合在部分消息不希望让wx侧的聊天对象知晓的情况下指定。 514 | - `merge`:是否对此聊天启用合并功能。默认为开。 515 | - `skipSticker`:是否忽略该聊天中的所有 wx动画表情。 516 | - `nameType`:设定群聊中的群友们该以什么作为其显示名称。为0表示采用他们wx账户的名称;为1表示优先采用您对群友的个人名片中的备注;为2表示优先使用该联系人自行设置的群名片。 517 | - `onlyReceive`:表示该C2C是否仅转发来自wx的消息,而不转发来自tg的消息。适合您不想在其中误发言的群聊,例如通知群。 518 | 519 | ### /c11n 520 | 521 | 本节定义了一些“个性化”选项,以修改一些消息的表现形式。 522 | 523 | - `wxQuotedMsgSuffixLine`:来自wx的消息包含引用时,UOS版wx默认会显示为不美观的样式,本项会将其转换到消息末尾的斜体括号,尽力与wx客户端保持一致。 524 | 525 | - `titleForSameTalkerInMergedRoomMsg`:是否把合并消息里,来自同一对象的数条连续消息的标题隐藏(即不会重复显示说话者的名称)。在默认配置下,标题会被替换为数字,其值为说话者连续消息的条数。 526 | 527 | - `quotedMsgSuffixLineInPersonChat`:基于`wxQuotedMsgSuffixLine`,在私聊消息中出现引用消息,默认情况下后缀行会采用您和对方的wx名称,通过此项可将其替换为YOU和ta,避免自己和对方的昵称频繁出现。 528 | 529 | - `officialAccountParser`,`personCardParser`:这两项定义如何解析公众号名片和个人名片为字符串,可自定义显示哪些数据,不过建议采用默认配置。 530 | 531 | - `systemMsgTitleInRoom`:群聊内的系统消息(如红包、修改群名,拉入新成员,新成员与其他人都不是朋友等等)默认以群名作为“说话者:的显示昵称,此项会将其替换为`(System)`。 532 | 533 | - `stickerWithLink`:基于`deliverSticker`,经此功能分开递送动画表情之后,需要在当前聊天中输出一个链接以便用户点击后跳转到表情所属聊天,此项定义了这个链接应当如何显示。默认值足矣。 534 | 535 | - `stickerSkipped`:定义被忽略的动画表情应当如何显示。 536 | 537 | - `newTopicCreated`:使用`/create_topic`创建新C2C线程时,在新线程中显示的欢迎消息。(因tg限制,必须设置欢迎消息,否则无法创建线程。) 538 | 539 | - `C2C_group_mediaCaption`:C2C群聊消息中,群成员发送的媒体消息默认采用什么作为其caption。只需在输出字符串中包含`name`即可。 540 | 541 | - `override_help_text`:是否override默认的`/help`文本。默认设为false,即不覆盖,随版本更新而更新;若需override,请参考common.js中的`TGBotHelpCmdText`将合适的函数放置于此。 542 | 543 | 544 | 545 | 接下来几项,均为所收到的消息不被UOS版wx完全支持时的替代符(因为原版提示文字一般都会包含”升级版本“等字样,为避免碍眼因此使用emoji代替)。 546 | 547 | 548 | 549 | - `unsupportedSticker`:发出或收到不支持的表情(均为在wx客户端点开能看到文字的商城表情) 550 | 551 | - `recvCall`:收到通话邀请时(很遗憾,UOS版wx不支持视频通话与语音通话的区分,下同) 552 | 553 | - `recvSplitBill`:收到群内分付账单时 554 | 555 | - `recvTransfer`:收到转账消息时(并非均为向您的转账,也可能是群聊中指定收款对象的转账) 556 | 557 | - `acceptTransfer`:转账被接收时 558 | 559 | - `msgTypeNotSupported`:消息类型不支持时(主要包含所有小程序卡片、合并转发的消息卡片。很遗憾,UOS版wx……后面忘了) 560 | 561 | ### /txyun 562 | 563 | 本节定义了 腾讯云API的凭据信息,目前仍为语音转文字等功能的必需。 564 | 565 | 如需启用,请前往 https://console.cloud.tencent.com/cam/capi ,新建密钥即可。在此过程中可以遵循页面指引,新建子账号并为后者生成API Key,以降低Key泄露后被盗用的服务覆盖面。不过请注意所配置的子账户必须具有如下服务的访问权限: 566 | 567 | > 语音识别ASR。 568 | 569 | - `switch`:是否启用腾讯云API。 570 | 571 | - `secretId`,`secretKey`:请根据腾讯云网站提示分别填入AK与SK即可。 572 | 573 | 574 | 575 | /**upyun** 576 | 577 | 由于`sharp`的引入,此节用途暂缺,因此暂缓相关文档条目的完善。 578 | 579 | - `switch` 580 | - `password` 581 | - `webFilePathPrefix` 582 | - `operatorName` 583 | - `urlPrefix` 584 | - `urlPathPrefix` 585 | 586 | --------------------------------------------------------------------------------