diff --git a/.env.example b/.env.example index d46f187..3981fdd 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,9 @@ LOG_LEVEL=info # 如果不希望登录一次后就记住当前账号,想每次都扫码登陆,填 true DISABLE_AUTO_LOGIN= +# RECVD_MSG_API 是否接收来自自己发的消息 +ACCEPT_RECVD_MSG_MYSELF=false + # 如果想自己处理收到消息的逻辑,在下面填上你的API地址, 默认为空 LOCAL_RECVD_MSG_API= diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 806e37a..66a84a9 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -19,8 +19,15 @@ jobs: with: username: dannicool password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 - name: Build and Push Docker Image - run: | - docker build . -t dannicool/docker-wechatbot-webhook:nightly - docker push dannicool/docker-wechatbot-webhook:nightly + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + push: true + tags: dannicool/docker-wechatbot-webhook:nightly + platforms: linux/amd64,linux/arm64 diff --git a/.husky/commit-msg b/.husky/commit-msg index d82f374..b5a5eb0 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npx -y commitlint --edit "$1" +npx commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit index ef2afbd..d24fdfc 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -npx -y lint-staged +npx lint-staged diff --git a/README.md b/README.md index e80e1d3..79fbe98 100644 --- a/README.md +++ b/README.md @@ -125,11 +125,13 @@ docker logs -f wxBotWebhook > Tips:需要增加参数使用 -e,多行用 \ 隔开,例如 -e RECVD_MSG_API="" \ -| 功能 | 环境变量 | 案例 | 备注 | -|--|--|--|--| -| 收消息 | RECVD_MSG_API | RECVD_MSG_API= | 如果想自己处理收到消息的逻辑,比如根据消息联动,填上你的处理逻辑 url,该行可以省略 | -| 禁用自动登录 | DISABLE_AUTO_LOGIN | DISABLE_AUTO_LOGIN=true | 非微信踢下线账号,可以依靠session免登, 如果想每次都扫码登陆,则增加该条配置 | -| 自定义登录 API token | LOGIN_API_TOKEN | LOGIN_API_TOKEN=abcdefg123 | 你也可以自定义一个自己的登录令牌,不配置的话,默认会生成一个 | +| 功能 | 变量 | 备注 | +|--|--|--| +| 日志级别 | LOG_LEVEL=info | 日志级别,默认 info,只影响当前日志输出,详细输出考虑使用 debug。无论该值如何变化,日志文件总是记录debug级别的日志 | +| 收消息 API | RECVD_MSG_API= | 如果想自己处理收到消息的逻辑,比如根据消息联动,填上你的处理逻辑 url | +| 收消息 API 接受自己发的消息 | ACCEPT_RECVD_MSG_MYSELF=false | RECVD_MSG_API 是否接收来自自己发的消息(设置为true,即接收, 默认false) | +| 自定义登录 API token | LOGIN_API_TOKEN=abcdefg123 | 你也可以自定义一个自己的登录令牌,不配置的话,默认会生成一个 | +| 禁用自动登录 | DISABLE_AUTO_LOGIN=true | **非微信踢下线账号,可以依靠当前登录的session免登**, 如果想每次都扫码登陆,则增加该条配置 | ## 🛠️ API @@ -309,11 +311,11 @@ curl --location --request POST 'http://localhost:3001/webhook/msg?token=[YOUR_PE | formData | 说明 | 数据类型 | 可选值 | 示例 | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------- | ----------------------- | ------------------------------------------------ | -| type |
支持的类型
  • ✅ 文字(text)
  • ✅ 链接卡片(urlLink)
  • ✅ 图片(file)
  • ✅ 视频(file)
  • ✅ 附件(file)
  • ✅ 语音(file)
  • ✅ 添加好友邀请(friendship)
refer: [wechaty类型支持列表](https://wechaty.js.org/docs/api/message#messagetype--messagetype) | `String` | `text` `file` `urlLink` `friendship` | - | +| type |
功能类型
  • ✅ 文字(text)
  • ✅ 链接卡片(urlLink)
  • ✅ 图片(file)
  • ✅ 视频(file)
  • ✅ 附件(file)
  • ✅ 语音(file)
  • ✅ 添加好友邀请(friendship)
其他类型
  • 未实现的消息类型(unknown)
系统类型
  • ✅ 登录(system_event_login)
  • ✅ 登出(system_event_logout)
  • ✅ 异常报错(system_event_error)
  • ✅ 快捷回复后消息推送状态通知(system_event_push_notify)
| `String` | `text` `file` `urlLink` `friendship` `unknown` `system_event_login` `system_event_logout` `system_event_error` `system_event_push_notify`| - | | content | 传输的内容, 文本或传输的文件共用这个字段,结构映射请看示例 | `String` `Binary` | | [示例](docs/recvdApi.example.md#formdatacontent) | | source | 消息的相关发送方数据, JSON String | `String` | | [示例](docs/recvdApi.example.md#formdatasource) | -| isMentioned | 该消息是@我的消息[#38](https://github.com/danni-cool/wechatbot-webhook/issues/38) | `String` | `1` `0` | - | -| isSystemEvent | 是否是来自系统消息事件(上线,掉线、异常事件、快捷回复后的通知) | `String` | `1` `0` | - | +| isMentioned | 该消息是@我的消息 [#38](https://github.com/danni-cool/wechatbot-webhook/issues/38) | `String` | `1` `0` | - | +| isMsgFromSelf | 是否是来自自己的消息 [#159](https://github.com/danni-cool/wechatbot-webhook/issues/159) | `String` | `1` `0` | - | **服务端处理 formData 一般需要对应的处理程序,假设你已经完成这一步,你将得到以下 request** @@ -323,7 +325,8 @@ curl --location --request POST 'http://localhost:3001/webhook/msg?token=[YOUR_PE "content": "你好", "source": "{\"room\":\"\",\"to\":{\"_events\":{},\"_eventsCount\":0,\"id\":\"@f387910fa45\",\"payload\":{\"alias\":\"\",\"avatar\":\"/cgi-bin/mmwebwx-bin/webwxgeticon?seq=1302335654&username=@f38bfd1e0567910fa45&skey=@crypaafc30\",\"friend\":false,\"gender\":1,\"id\":\"@f38bfd1e10fa45\",\"name\":\"ch.\",\"phone\":[],\"star\":false,\"type\":1}},\"from\":{\"_events\":{},\"_eventsCount\":0,\"id\":\"@6b5111dcc269b6901fbb58\",\"payload\":{\"address\":\"\",\"alias\":\"\",\"avatar\":\"/cgi-bin/mmwebwx-bin/webwxgeticon?seq=123234564&username=@6b5dbb58&skey=@crypt_ec356afc30\",\"city\":\"Mars\",\"friend\":false,\"gender\":1,\"id\":\"@6b5dbd3facb58\",\"name\":\"Daniel\",\"phone\":[],\"province\":\"Earth\",\"signature\":\"\",\"star\":false,\"weixin\":\"\",\"type\":1}}}", "isMentioned": "0", - "isSystemEvent": "0" + "isMsgFromSelf": "0", + "isSystemEvent": "0" // 考虑废弃,请使用type类型判断系统消息 } ``` @@ -334,8 +337,7 @@ curl --location 'https://your.recvdapi.com' \ --form 'type="file"' \ --form 'content=@"/Users/Downloads/13482835.jpeg"' \ --form 'source="{\\\"room\\\":\\\"\\\",\\\"to\\\":{\\\"_events\\\":{},\\\"_eventsCount\\\":0,\\\"id\\\":\\\"@f387910fa45\\\",\\\"payload\\\":{\\\"alias\\\":\\\"\\\",\\\"avatar\\\":\\\"/cgi-bin/mmwebwx-bin/webwxgeticon?seq=1302335654&username=@f38bfd1e0567910fa45&skey=@crypaafc30\\\",\\\"friend\\\":false,\\\"gender\\\":1,\\\"id\\\":\\\"@f38bfd1e10fa45\\\",\\\"name\\\":\\\"ch.\\\",\\\"phone\\\":[],\\\"star\\\":false,\\\"type\\\":1}},\\\"from\\\":{\\\"_events\\\":{},\\\"_eventsCount\\\":0,\\\"id\\\":\\\"@6b5111dcc269b6901fbb58\\\",\\\"payload\\\":{\\\"address\\\":\\\"\\\",\\\"alias\\\":\\\"\\\",\\\"avatar\\\":\\\"/cgi-bin/mmwebwx-bin/webwxgeticon?seq=123234564&username=@6b5dbb58&skey=@crypt_ec356afc30\\\",\\\"city\\\":\\\"Mars\\\",\\\"friend\\\":false,\\\"gender\\\":1,\\\"id\\\":\\\"@6b5dbd3facb58\\\",\\\"name\\\":\\\"Daniel\\\",\\\"phone\\\":[],\\\"province\\\":\\\"Earth\\\",\\\"signature\\\":\\\"\\\",\\\"star\\\":false,\\\"weixin\\\":\\\"\\\",\\\"type\\\":1}}}"' \ ---form 'isMentioned="0"' \ ---form 'isSystemEvent="0"' +--form 'isMentioned="0"' ``` @@ -399,14 +401,18 @@ curl --location 'https://your.recvdapi.com' \ - **query**: token **status**: `200` -登录成功,返回 json 包含当前用户 + +##### 登录成功 + +返回 json 包含当前用户 ```json {"success":true,"message":"Contactis already login"} ``` -**status**: `302` -登录态掉了,跳转最新的登录二维码 +##### 登录失败 + +展示微信登录扫码页面 #### `/healthz?token=[YOUR_PERSONAL_TOKEN]` diff --git a/docker-compose.yml b/docker-compose.yml index 80ea758..815cca3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,9 @@ services: ports: - "3001:3001" environment: - - LOG_LEVEL=info # 调整容器输出级别(不影响日志文件输出等级)运行时提示的消息等级(默认info,想有更详细的日志,等级同log4js) + - LOG_LEVEL=info # 调整容器输出级别(不影响日志文件输出等级)运行时提示的消息等级(默认info,debug级别会输出详细的日志) # - DISABLE_AUTO_LOGIN=true # 如果不希望登录一次后就记住当前账号,想每次都扫码登陆,填 true - # - LOCAL_RECVD_MSG_API= # 如果想自己处理收到消息的逻辑,在下面填上你的API地址, 默认为空 - # - LOCAL_LOGIN_API_TOKEN= # 登录地址Token访问地址: http://localhost:3001/login?token=[LOCAL_LOGIN_API_TOKEN] + # - ACCEPT_RECVD_MSG_MYSELF=true # 如果希望机器人可以自己接收消息,填 true + # - RECVD_MSG_API= # 如果想自己处理收到消息的逻辑,在下面填上你的API地址, 默认为空 + # - LOGIN_API_TOKEN= # 登录地址Token访问地址: http://localhost:3001/login?token=[LOCAL_LOGIN_API_TOKEN] restart: unless-stopped diff --git a/dockerfile b/dockerfile index d56d485..ec59575 100644 --- a/dockerfile +++ b/dockerfile @@ -22,6 +22,10 @@ ENV RECVD_MSG_API= ENV LOGIN_API_TOKEN= # 是否禁用默认登录 ENV DISABLE_AUTO_LOGIN= +# 运行时提示的消息等级(默认info,想有更详细的日志,可以指定为debug) +ENV LOG_LEVEL=info +# RECVD_MSG_API 是否接收来自自己发的消息(设置为true,即接收) +ENV ACCEPT_RECVD_MSG_MYSELF=false # 暴露端口(你的 Express 应用程序监听的端口) EXPOSE 3001 diff --git a/docs/recvdApi.example.md b/docs/recvdApi.example.md index 1cd4d76..64ff03e 100644 --- a/docs/recvdApi.example.md +++ b/docs/recvdApi.example.md @@ -1,18 +1,20 @@ # RECVD_MSG_API JSON 示例 -## formData 请求体不同情况说明 +## 1. `formData.type` 不同情况说明 -### 文字消息 `formData.type === text` +### 1.1 功能消息类型 + +#### 文字消息 `text` - 是否支持快捷回复:✅ - `formData.content`: `String` -### 文件消息 `formData.type === file` +#### 文件消息 `file` - 是否支持快捷回复:✅ - `formData.content`: `binary` -### 公众号推文 `formData.type === urlLink` +#### 公众号推文 `urlLink` - 是否支持快捷回复:❌ - `formData.content`:`json` @@ -27,7 +29,7 @@ } ``` -### 加好友请求 `formData.type === friendship` +#### 加好友请求 `friendship` - 是否支持快捷回复:✅ - `formData.content`:`json` @@ -41,16 +43,31 @@ > 通过好友请求,需要通过接口返回 `{ "success": true }` 字段 -### 4. 系统消息 `formData.isSystemEvent === '1'` +### 1.2 其他消息类型 + +#### 不支持的消息类型 `unknown` + +- 是否支持快捷回复:✅ + +没法在当前版本微信中展示的消息,如果能展示值,会以**文本形式**展示,否则为空 + +例如: +- [unknown 类型里拍一拍消息提示](https://github.com/danni-cool/wechatbot-webhook/pull/121) + + +### 1.3 系统通知消息类型 - 是否支持快捷回复:❌ + +#### 微信已登录/登出 `system_event_login` | `system_event_logout` + - `formData.content`: `json` -示例 + ```js { - "event": "login", // login | logout | error | notifyOfRecvdApiPushMsg + "event": "login", // login | logout - "user": { // 当前的用户信息,没有则为null + "user": { // 当前的用户信息 "_events": {}, "_eventsCount": 0, "id": "@xxxasdfsf", @@ -65,11 +82,61 @@ "star": false, "type": 1 } - - "error": ''// js 报错的错误栈信息 } +} +``` + +#### 系统运行出错 `system_event_error` +- `formData.content`: `json` +```js +{ + "event": "error", //notifyOfRecvdApiPushMsg + + "user": { // 当前的用户信息 + "_events": {}, + "_eventsCount": 0, + "id": "@xxxasdfsf", + "payload": { + "alias": "", + "avatar": "", + "friend": false, + "gender": 1, + "id": "@xxx", + "name": "somebody", + "phone": [], + "star": false, + "type": 1 + } + }, + + "error": {} // 具体出错信息 js error stack +} +``` + +#### 快捷回复后通知 `system_event_push_notify` +- `formData.content`: `json` +```js +{ + "event": "error", //notifyOfRecvdApiPushMsg + + "user": { // 当前的用户信息 + "_events": {}, + "_eventsCount": 0, + "id": "@xxxasdfsf", + "payload": { + "alias": "", + "avatar": "", + "friend": false, + "gender": 1, + "id": "@xxx", + "name": "somebody", + "phone": [], + "star": false, + "type": 1 + } + }, - //仅当 event: "notifyOfRecvdApiPushMsg" 快捷回复后触发才返回次结构, 如果有部分消息推送失败也在此结构能拿到所有信息, 结构同推消息的api结构 + // 快捷回复后触发才返回此结构,如果有部分消息推送失败也在此结构能拿到所有信息, 结构同推消息的api结构 "recvdApiReplyNotify": { "success": true, "message": "Message sent successfully", @@ -86,7 +153,7 @@ ``` -## formData.source `String` +## 2. formData.source `String` ```js { @@ -99,7 +166,7 @@ "adminIdList": [], "avatar": "xxxx", // 相对路径,应该要配合解密 "memberList": [ - {id: '@xxxx', name:'昵称', alias: '备注名' } + {id: '@xxxx', name:'昵称', alias: '备注名'/** 个人备注名,非群备注名 */ } ] }, //以下暂不清楚什么用途,如有兴趣,请查阅 wechaty 官网文档 diff --git a/main.js b/main.js index 3c0b301..e899ae5 100644 --- a/main.js +++ b/main.js @@ -1,10 +1,7 @@ require('dotenv').config({ path: process.env.homeEnvCfg /** 兼容cli调用 */ ?? './.env' }) -/** log 在 prestart 阶段初始化了,后续需要手动改level才能同步env配置 */ -require('./src/utils/index').proxyConsole({ - logLevel: process.env.LOG_LEVEL -}) +require('./src/utils/index').proxyConsole() const { PORT } = process.env const { Hono } = require('hono') const { serve } = require('@hono/node-server') diff --git a/package.json b/package.json index 23528fa..eca5606 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dotenv": "^16.3.1", "file-box": "1.4.15", "form-data": "^4.0.0", + "gerror": "^1.0.16", "hono": "^3.11.11", "log4js": "^6.9.1", "mime": "^3.0.0", @@ -70,7 +71,9 @@ }, "pnpm": { "patchedDependencies": { - "wechat4u@0.7.14": "patches/wechat4u@0.7.14.patch" + "wechat4u@0.7.14": "patches/wechat4u@0.7.14.patch", + "wechaty-puppet-wechat4u@1.14.13": "patches/wechaty-puppet-wechat4u@1.14.13.patch", + "wechaty-puppet@1.20.2": "patches/wechaty-puppet@1.20.2.patch" } } } diff --git a/packages/cli/.env.example b/packages/cli/.env.example index d46f187..3981fdd 100644 --- a/packages/cli/.env.example +++ b/packages/cli/.env.example @@ -7,6 +7,9 @@ LOG_LEVEL=info # 如果不希望登录一次后就记住当前账号,想每次都扫码登陆,填 true DISABLE_AUTO_LOGIN= +# RECVD_MSG_API 是否接收来自自己发的消息 +ACCEPT_RECVD_MSG_MYSELF=false + # 如果想自己处理收到消息的逻辑,在下面填上你的API地址, 默认为空 LOCAL_RECVD_MSG_API= diff --git a/packages/cli/README.md b/packages/cli/README.md index ce977f7..101d2e2 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -31,10 +31,8 @@ ### 1. 安装 -> 目前使用 pnpm 管理包,以支持临时包修补(patches)和加速依赖安装,如果你还不了解 pnpm,可以先了解 [pnpm](https://pnpm.io/zh/motivation) - ```bash -pnpm i wechatbot-webhook -g +npm i wechatbot-webhook -g ``` ### 2. 运行 & 扫码 diff --git a/packages/cli/index.js b/packages/cli/index.js index 72638a0..a47a939 100755 --- a/packages/cli/index.js +++ b/packages/cli/index.js @@ -15,15 +15,6 @@ const memoryCardFile = (process.env.homeMemoryCardPath = path.join( './loginSession.memory-card.json' )) -console.log( - [ - '╔════════════════════════════╗', - '║ wechatbot-webhook ║', - `║ v${version} ║`, - `╚════════════════════════════╝` - ].join('\n') -) - program .name('wechatbot-webhook') .description( diff --git a/patches/wechaty-puppet-wechat4u@1.14.13.patch b/patches/wechaty-puppet-wechat4u@1.14.13.patch new file mode 100644 index 0000000..37c7ef0 --- /dev/null +++ b/patches/wechaty-puppet-wechat4u@1.14.13.patch @@ -0,0 +1,13 @@ +diff --git a/dist/cjs/src/puppet-wechat4u.js b/dist/cjs/src/puppet-wechat4u.js +index 68a52e6eb60c6efa0436984ef4399b1cf38d03af..6d1ae5aa166f61c288b66ea9a8e0273777d22687 100644 +--- a/dist/cjs/src/puppet-wechat4u.js ++++ b/dist/cjs/src/puppet-wechat4u.js +@@ -353,6 +353,8 @@ class PuppetWechat4u extends PUPPET.Puppet { + if (!this.getContactInterval) { + this.getContactsInfo(); + this.getContactInterval = setInterval(() => { ++ //fix: 修复登出了还一直请求 ++ this.isLoggedIn && + this.getContactsInfo(); + }, 2000); + } diff --git a/patches/wechaty-puppet@1.20.2.patch b/patches/wechaty-puppet@1.20.2.patch new file mode 100644 index 0000000..56f9822 --- /dev/null +++ b/patches/wechaty-puppet@1.20.2.patch @@ -0,0 +1,13 @@ +diff --git a/dist/cjs/src/mixins/login-mixin.js b/dist/cjs/src/mixins/login-mixin.js +index 01c9a9caea23816ebdd36398bb2cd1f4f0e85559..d273c203e0d41262559cc3c85543485b4affb4ea 100644 +--- a/dist/cjs/src/mixins/login-mixin.js ++++ b/dist/cjs/src/mixins/login-mixin.js +@@ -110,6 +110,8 @@ const loginMixin = (mixinBase) => { + this.__currentUserId = undefined; + resolve(); + })); ++ // bugfix: 修复wechat4u并未真正登出的问题 ++ this.wechat4u?.emit('logout'); + } + /** + * @deprecated use `currentUserId` instead. (will be removed in v2.0) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f9f121..9538783 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ patchedDependencies: wechat4u@0.7.14: hash: mmirz2kximouh53sl2jk76abxe path: patches/wechat4u@0.7.14.patch + wechaty-puppet-wechat4u@1.14.13: + hash: nq4vns7vah46g6vxziyzqg5jey + path: patches/wechaty-puppet-wechat4u@1.14.13.patch + wechaty-puppet@1.20.2: + hash: wwr5xvwazgbyb26ud6vwdruala + path: patches/wechaty-puppet@1.20.2.patch importers: @@ -28,6 +34,9 @@ importers: form-data: specifier: ^4.0.0 version: 4.0.0 + gerror: + specifier: ^1.0.16 + version: 1.0.16 hono: specifier: ^3.11.11 version: 3.11.11 @@ -3334,7 +3343,7 @@ packages: faker: 5.5.3 file-box: 1.4.15 typed-emitter: 1.5.0-from-event - wechaty-puppet: 1.20.2(rxjs@7.8.1) + wechaty-puppet: 1.20.2(patch_hash=wwr5xvwazgbyb26ud6vwdruala)(rxjs@7.8.1) dev: true /wechaty-puppet-service@1.19.9(brolog@1.14.2)(redux@4.2.1)(wechaty-puppet@1.20.2)(wechaty@1.20.2): @@ -3354,7 +3363,7 @@ packages: stronger-typed-streams: 0.2.0 uuid: 8.3.2 wechaty-grpc: 1.5.2 - wechaty-puppet: 1.20.2(rxjs@7.8.1) + wechaty-puppet: 1.20.2(patch_hash=wwr5xvwazgbyb26ud6vwdruala)(rxjs@7.8.1) wechaty-redux: 1.20.2(brolog@1.14.2)(wechaty-puppet@1.20.2)(wechaty@1.20.2) wechaty-token: 1.1.2 transitivePeerDependencies: @@ -3364,7 +3373,7 @@ packages: - wechaty dev: false - /wechaty-puppet-wechat4u@1.14.13(@swc/core@1.3.101)(wechaty-puppet@1.20.2): + /wechaty-puppet-wechat4u@1.14.13(patch_hash=nq4vns7vah46g6vxziyzqg5jey)(@swc/core@1.3.101)(wechaty-puppet@1.20.2): resolution: {integrity: sha512-aLm0Hp0VrmQB4POoFTzBKRQMmtdv4gMQUSUocfqL0JWAutsVwzWkm4Utbt8rnN7Jg2qxT5ooH4hkWDUlUKnKyw==} engines: {node: '>=16', npm: '>=7'} peerDependencies: @@ -3376,13 +3385,14 @@ packages: fast-xml-parser: 3.21.1 promise-retry: 2.0.1 wechat4u: 0.7.14(patch_hash=mmirz2kximouh53sl2jk76abxe) - wechaty-puppet: 1.20.2(rxjs@7.8.1) + wechaty-puppet: 1.20.2(patch_hash=wwr5xvwazgbyb26ud6vwdruala)(rxjs@7.8.1) xml2js: 0.4.23 transitivePeerDependencies: - supports-color dev: false + patched: true - /wechaty-puppet@1.20.2(rxjs@7.8.1): + /wechaty-puppet@1.20.2(patch_hash=wwr5xvwazgbyb26ud6vwdruala)(rxjs@7.8.1): resolution: {integrity: sha512-IXcnUc9A3hGIT+9CEF8KLkbdld+AQPfVlWLeUXmmSvm3I9NbSbVfU43FZvKMjvLCexvN5DZq2+JZdASESohI5Q==} engines: {node: '>=16', npm: '>=7'} dependencies: @@ -3400,6 +3410,7 @@ packages: watchdog: 0.9.2 transitivePeerDependencies: - rxjs + patched: true /wechaty-redux@1.20.2(brolog@1.14.2)(wechaty-puppet@1.20.2)(wechaty@1.20.2): resolution: {integrity: sha512-XAhSmdCDa1yaAMRYPiYqbX3BL0T1Owyf+77HxLY+450AFAyjM4xak5ZLStLWIA3zEktgzn6KwEthtu7mmZ7n3g==} @@ -3419,7 +3430,7 @@ packages: utility-types: 3.10.0 uuid: 8.3.2 wechaty: 1.20.2(@swc/core@1.3.101)(brolog@1.14.2)(redux@4.2.1)(rxjs@7.8.1) - wechaty-puppet: 1.20.2(rxjs@7.8.1) + wechaty-puppet: 1.20.2(patch_hash=wwr5xvwazgbyb26ud6vwdruala)(rxjs@7.8.1) transitivePeerDependencies: - brolog dev: false @@ -3457,9 +3468,9 @@ packages: rx-queue: 1.0.5 state-switch: 1.6.3(brolog@1.14.2)(gerror@1.0.16)(rxjs@7.8.1) uuid: 8.3.2 - wechaty-puppet: 1.20.2(rxjs@7.8.1) + wechaty-puppet: 1.20.2(patch_hash=wwr5xvwazgbyb26ud6vwdruala)(rxjs@7.8.1) wechaty-puppet-service: 1.19.9(brolog@1.14.2)(redux@4.2.1)(wechaty-puppet@1.20.2)(wechaty@1.20.2) - wechaty-puppet-wechat4u: 1.14.13(@swc/core@1.3.101)(wechaty-puppet@1.20.2) + wechaty-puppet-wechat4u: 1.14.13(patch_hash=nq4vns7vah46g6vxziyzqg5jey)(@swc/core@1.3.101)(wechaty-puppet@1.20.2) wechaty-token: 1.1.2 ws: 8.15.1 transitivePeerDependencies: diff --git a/src/config/const.js b/src/config/const.js index 76974f6..e0ec497 100644 --- a/src/config/const.js +++ b/src/config/const.js @@ -1,8 +1,27 @@ +const path = require('path') + +const config = { + /** + * 上报消息的api群成员缓存多久(单位:ms) + * @type {number} + */ + roomCachedTime: 1000 * 60 * 5 +} + +const { homeEnvCfg, homeMemoryCardPath } = process.env +const isCliEnv = Boolean(homeEnvCfg) +const memoryCardName = isCliEnv ? homeMemoryCardPath : 'loginSession' +const memoryCardPath = isCliEnv + ? homeMemoryCardPath + : path.join(__dirname, '../../', 'loginSession.memory-card.json') + /** * Enum for msg type * @readonly * @enum {number} */ const MSG_TYPE_ENUM = { + /** 未知 */ + UNKNOWN: 0, /** 各种文件 */ ATTACHMENT: 1, /** 语音 */ @@ -18,15 +37,56 @@ const MSG_TYPE_ENUM = { /** 视频 */ VIDEO: 15, /** 好友邀请 or 好友通过消息(自定义类型) */ - CUSTOM_FRIENDSHIP: 99 + CUSTOM_FRIENDSHIP: 99, + /** 系统消息类型 */ + /** 登录事件 */ + SYSTEM_EVENT_LOGIN: 1000, + /** 登出事件 */ + SYSTEM_EVENT_LOGOUT: 1001, + /** 错误事件 */ + SYSTEM_EVENT_ERROR: 1002, + /** 推送通知事件 */ + SYSTEM_EVENT_PUSH_NOTIFY: 1003 } -const config = { - /** - * 上报消息的api群成员缓存多久(单位:ms) - * @type {number} - */ - roomCachedTime: 1000 * 60 * 5 +/** + * Enum for system msg type (legacy) + * @readonly + * @enum {number} */ +const legacySystemMsgStrMap = { + login: MSG_TYPE_ENUM.SYSTEM_EVENT_LOGIN, + logout: MSG_TYPE_ENUM.SYSTEM_EVENT_LOGOUT, + error: MSG_TYPE_ENUM.SYSTEM_EVENT_ERROR, + notifyOfRecvdApiPushMsg: MSG_TYPE_ENUM.SYSTEM_EVENT_PUSH_NOTIFY +} + +/** + * 系统消息类型映射表(外部) + * @enum {string} */ +const systemMsgEnumMap = { + [MSG_TYPE_ENUM.SYSTEM_EVENT_LOGIN]: 'system_event_login', + [MSG_TYPE_ENUM.SYSTEM_EVENT_LOGOUT]: 'system_event_logout', + [MSG_TYPE_ENUM.SYSTEM_EVENT_ERROR]: 'system_event_error', + [MSG_TYPE_ENUM.SYSTEM_EVENT_PUSH_NOTIFY]: 'system_event_push_notify' } -module.exports = { MSG_TYPE_ENUM, config } +const logOutUnofficialCodeList = [ + '400 != 400', + '1101 == 0', + "'1101' == 0", + '1205 == 0', + '3 == 0', + "'1102' == 0" /** 场景:没法发消息了 */, + '-1 == 0' /** 场景:没法发消息 */, + "'-1' == 0" /** 不确定,暂时两种都加上 */ +] + +module.exports = { + MSG_TYPE_ENUM, + config, + legacySystemMsgStrMap, + systemMsgEnumMap, + memoryCardName, + memoryCardPath, + logOutUnofficialCodeList +} diff --git a/src/config/log4jsFilter.js b/src/config/log4jsFilter.js new file mode 100644 index 0000000..972f7da --- /dev/null +++ b/src/config/log4jsFilter.js @@ -0,0 +1,14 @@ +/** 只想纯console.log输出,但是不记录到日志文件的白名单 */ +const logOnlyOutputWhiteList = [ + '▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄\n' //扫二维码登录 +] + +/** 不想输出的一些错误信息 */ +const logDontOutpuptBlackList = [ + '[https://github.com/node-fetch/node-fetch/issues/1167]' // form-data 提示的DepracationWarning,会被认为是错误提issue +] + +module.exports = { + logOnlyOutputWhiteList, + logDontOutpuptBlackList +} diff --git a/src/route/login.js b/src/route/login.js index 053a561..66ba9c0 100644 --- a/src/route/login.js +++ b/src/route/login.js @@ -1,7 +1,3 @@ -const Service = require('../service') -const Utils = require('../utils') -const { TextMsg } = require('../utils/msg.js') - /** * 注册login路由和处理上报逻辑 * @param {Object} param @@ -10,9 +6,6 @@ const { TextMsg } = require('../utils/msg.js') */ module.exports = function registerLoginCheck({ app, bot }) { let message = '' - /** @type {import('wechaty').ContactSelf | null} */ - let currentUser = null - let logOutWhenError = false let success = false bot @@ -21,66 +14,17 @@ module.exports = function registerLoginCheck({ app, bot }) { success = false }) .on('login', async (user) => { - message = user.toString() + 'is already login' + message = user + 'is already login' success = true - currentUser = user - logOutWhenError = false - - try { - await Service.sendMsg2RecvdApi( - new TextMsg({ - text: JSON.stringify({ event: 'login', user }), - isSystemEvent: true - }) - ) - } catch (e) { - Utils.logger.error('上报login事件给 RECVD_MSG_API 出错', e) - } }) - .on('logout', (user) => { + .on('logout', () => { message = '' - currentUser = null success = false - // 登出时给接收消息api发送特殊文本 - Service.sendMsg2RecvdApi( - new TextMsg({ - text: JSON.stringify({ event: 'logout', user }), - isSystemEvent: true - }) - ).catch((e) => { - Utils.logger.error('上报 logout 事件给 RECVD_MSG_API 出错:', e) - }) }) - .on('error', (error) => { - // 登出后的错误没有必要重复上报 - !logOutWhenError && - Service.sendMsg2RecvdApi( - new TextMsg({ - text: JSON.stringify({ event: 'error', error, user: currentUser }), - isSystemEvent: true - }) - ).catch((e) => { - Utils.logger.error('上报 error 事件给 RECVD_MSG_API 出错:', e) - }) - - // 处理异常错误后的登出上报,每次登录成功后掉线只上报一次 - if (!logOutWhenError && !bot.isLoggedIn) { - Service.sendMsg2RecvdApi( - new TextMsg({ - text: JSON.stringify({ event: 'logout', user: currentUser }), - isSystemEvent: true - }) - ).catch((e) => { - Utils.logger.error( - '上报 error 事件中的 logout 给 RECVD_MSG_API 出错:', - e - ) - }) - + .on('error', async () => { + if (!bot.isLoggedIn) { success = false message = '' - logOutWhenError = true - currentUser = null } }) @@ -95,7 +39,29 @@ module.exports = function registerLoginCheck({ app, bot }) { message }) } else { - return c.redirect(message, 302) + // 构建带有iframe的HTML字符串 + const html = ` + + + + + + 扫码登录 + + + + + + + ` + return c.html(html) } } ) diff --git a/src/route/msg.js b/src/route/msg.js index c3b1f4c..00100ea 100644 --- a/src/route/msg.js +++ b/src/route/msg.js @@ -150,6 +150,8 @@ function registerPushHook({ app, bot }) { if (msgReceiver !== undefined) { const { success, error } = await Service.formatAndSendMsg({ + isRoom, + bot, type, content, msgInstance: msgReceiver diff --git a/src/service/friendship.js b/src/service/friendship.js index 634797f..7342b42 100644 --- a/src/service/friendship.js +++ b/src/service/friendship.js @@ -5,10 +5,10 @@ const { FriendshipMsg } = require('../utils/msg.js') const { MSG_TYPE_ENUM } = require('../config/const') /** * @param {import('wechaty').Friendship} friendship - * @param {import('wechaty/impls').WechatyInterface} botInstance + * @param {import('wechaty/impls').WechatyInterface} bot */ -const onRecvdFriendship = async (friendship, botInstance) => { - const { Friendship } = botInstance +const onRecvdFriendship = async (friendship, bot) => { + const { Friendship } = bot let logMsg = 'received `friend` event from ' + friendship.contact().name() @@ -28,6 +28,7 @@ const onRecvdFriendship = async (friendship, botInstance) => { await handleResSendMsg({ res, type: MSG_TYPE_ENUM.CUSTOM_FRIENDSHIP, + bot, friendship }) } catch (error) { diff --git a/src/service/msgSender.js b/src/service/msgSender.js index 8231a30..682e34d 100644 --- a/src/service/msgSender.js +++ b/src/service/msgSender.js @@ -1,4 +1,5 @@ const Utils = require('../utils/index.js') +const chalk = require('chalk') const { sendMsg2RecvdApi } = require('./msgUploader.js') const { MSG_TYPE_ENUM } = require('../config/const.js') const rules = require('../config/valid.js') @@ -6,7 +7,7 @@ const rules = require('../config/valid.js') /** * 根据v2群发逻辑整合归并状态方便调用和处理回调 * @param {*} body - * @param {{bot?:import('wechaty/impls').WechatyInterface, skipReceiverCheck?:boolean, messageReceiver?:msgInstanceType}} bot + * @param {{bot:import('wechaty/impls').WechatyInterface, skipReceiverCheck?:boolean, messageReceiver?:msgInstanceType}} bot * */ const handleSendV2Msg = async function ( body, @@ -64,7 +65,7 @@ const handleSendV2Msg = async function ( /** * 处理v2版本推消息(群发or个人),返回状态 * @param {*} body - * @param {{bot?:import('wechaty/impls').WechatyInterface, skipReceiverCheck?:boolean, messageReceiver?:msgInstanceType}} opt + * @param {{bot:import('wechaty/impls').WechatyInterface, skipReceiverCheck?:boolean, messageReceiver?:msgInstanceType}} opt * @returns {Promise<{state:'pass' | 'reject' | '', task:msgV2taskType}>} */ const handleSendV2MsgCollectInfo = async function ( @@ -340,7 +341,7 @@ const hadnleMsgV2PreCheck = function (body, opt) { /** * 处理消息发给个人逻辑(单条/多条)(不校验参数) * @param {*} body - * @param {{bot?:import('wechaty/impls').WechatyInterface, messageReceiver?:msgInstanceType }} opt + * @param {{bot:import('wechaty/impls').WechatyInterface, messageReceiver?:msgInstanceType }} opt * @returns {Promise<{status: msg2SingleStatus, notFoundObj: msg2SingleRejectReason | null, rejectReasonObj: msg2SingleRejectReason|null, sendingTaskObj: sendingTaskType | null}>} */ const handleMsg2Single = async function (body, { bot, messageReceiver }) { @@ -368,7 +369,7 @@ const handleMsg2Single = async function (body, { bot, messageReceiver }) { let msgReceiver = messageReceiver // msgReceiver 可以由外部提供 - if (!msgReceiver && bot) { + if (!msgReceiver) { if (isRoom === true && typeof to === 'string') { msgReceiver = await bot.Room.find({ topic: to }) } else { @@ -379,12 +380,14 @@ const handleMsg2Single = async function (body, { bot, messageReceiver }) { } } - if (msgReceiver !== undefined) { + if (msgReceiver) { if (Array.isArray(payload.data) && payload.data.length) { /**@type {(pushMsgUnitTypeOpt & {success?:boolean, error?:string})[]} */ let msgArr = payload.data for (let i = 0; i < msgArr.length; i++) { const { success, error } = await formatAndSendMsg({ + bot, + isRoom, type: msgArr[i].type || 'text', // @ts-ignore content: msgArr[i].content, @@ -416,6 +419,8 @@ const handleMsg2Single = async function (body, { bot, messageReceiver }) { } } else if (!Array.isArray(payload.data)) { const { success } = await formatAndSendMsg({ + isRoom, + bot, type: payload.data.type ?? 'text', // @ts-ignore content: payload.data.content, @@ -471,12 +476,37 @@ const getPushMsgUnitUnvalidStr = function ({ type, content }) { /** * 发送消息核心。这个处理程序将数据转换为标准格式,然后使用 wechaty 发送消息。 * @type {{ - * (payload:{ type: 'text' | 'fileUrl'|'file', content: string| payloadFormFile, msgInstance: msgInstanceType }) : Promise<{success:boolean, error:any}>; + * (payload:{ isRoom?: boolean,bot:import('wechaty/impls').WechatyInterface, type: 'text' | 'fileUrl'|'file', content: string| payloadFormFile, msgInstance: msgInstanceType }) : Promise<{success:boolean, error:any}>; * }} */ -const formatAndSendMsg = async function ({ type, content, msgInstance }) { +const formatAndSendMsg = async function ({ + isRoom = false, + bot, + type, + content, + msgInstance +}) { let success = false let error + /** @type {msgStructurePayload} */ + const emitPayload = { + content: '', + type: { + text: MSG_TYPE_ENUM.TEXT, + fileUrl: MSG_TYPE_ENUM.ATTACHMENT, + file: MSG_TYPE_ENUM.ATTACHMENT + }[type], + type_display: { + text: '消息', + fileUrl: '文件', + file: '文件' + }[type], + self: true, + from: bot.currentUser, + to: msgInstance, + // @ts-ignore 此处一定是 roomInstance + room: isRoom ? msgInstance : '' + } try { switch (type) { @@ -484,23 +514,33 @@ const formatAndSendMsg = async function ({ type, content, msgInstance }) { case 'text': //@ts-expect-errors 重载不是很好使,手动判断 await msgInstance.say(content) + //@ts-expect-errors 重载不是很好使,手动判断 + emitPayload.content = content + msgSenderCallback(emitPayload) break case 'fileUrl': { //@ts-expect-errors 重载不是很好使,手动判断 const fileUrlArr = content.split(',') + // 单文件 if (fileUrlArr.length === 1) { //@ts-expect-errors 重载不是很好使,手动判断 const file = await Utils.getMediaFromUrl(content) + //@ts-expect-errors 重载不是很好使,手动判断 + emitPayload.content = file await msgInstance.say(file) + msgSenderCallback(emitPayload) break } // 多个文件的情况 for (let i = 0; i < fileUrlArr.length; i++) { const file = await Utils.getMediaFromUrl(fileUrlArr[i]) + //@ts-expect-errors 重载不是很好使,手动判断 + emitPayload.content = file await msgInstance.say(file) + msgSenderCallback(emitPayload) } break } @@ -508,7 +548,11 @@ const formatAndSendMsg = async function ({ type, content, msgInstance }) { case 'file': { //@ts-expect-errors 重载不是很好使,手动判断 - await msgInstance.say(await Utils.getBufferFile(content)) + const file = await Utils.getBufferFile(content) + await msgInstance.say(file) + //@ts-expect-errors 重载不是很好使,手动判断 + emitPayload.content = file + msgSenderCallback(emitPayload) } break default: @@ -523,15 +567,37 @@ const formatAndSendMsg = async function ({ type, content, msgInstance }) { return { success, error } } +/** 推消息api发送后 + * @param {msgStructurePayload} payload + */ +const msgSenderCallback = async (payload) => { + Utils.logger.info( + `调用 bot api 发送 ${payload.type_display} 给 ${chalk.blue(payload.to)}:`, + typeof payload.content === 'object' + ? payload.content._name ?? 'unknown file' + : payload.content + ) + + if (process.env.ACCEPT_RECVD_MSG_MYSELF !== 'true') return + sendMsg2RecvdApi(new Utils.ApiMsg(payload)) +} + /** * 接受 Service.sendMsg2RecvdApi 的response 回调以便回复或作出其他动作 * @param {Object} payload * @param {Response} [payload.res] + * @param {import('wechaty/impls').WechatyInterface} payload.bot * @param {import('@src/config/const.js').MSG_TYPE_ENUM} payload.type * @param {import('wechaty').Friendship} [payload.friendship] * @param {msgInstanceType} [payload.msgInstance] */ -const handleResSendMsg = async ({ res, type, friendship, msgInstance }) => { +const handleResSendMsg = async ({ + res, + bot, + type, + friendship, + msgInstance +}) => { // to 的逻辑 // 个人 // msgInstance.payload.name @@ -565,8 +631,12 @@ const handleResSendMsg = async ({ res, type, friendship, msgInstance }) => { // 同意且包含回复信息 if (success === true && data !== undefined) { await Utils.sleep(1000) - //@ts-expect-errors 重载不是很好使,手动判断 - recvdApiReplyHandler(data, { msgInstance: friendship.contact(), to }) + recvdApiReplyHandler(data, { + //@ts-expect-errors 重载不是很好使,手动判断 + msgInstance: friendship.contact(), + bot, + to + }) } break @@ -579,7 +649,7 @@ const handleResSendMsg = async ({ res, type, friendship, msgInstance }) => { isRoom = !!msgInstance.payload?.topic //@ts-ignore to = isRoom ? msgInstance.payload?.topic : msgInstance.payload.name - recvdApiReplyHandler(data, { msgInstance, to, isRoom }) + recvdApiReplyHandler(data, { msgInstance, bot, to, isRoom }) } break } @@ -588,28 +658,25 @@ const handleResSendMsg = async ({ res, type, friendship, msgInstance }) => { /** * 处理消息回复api和加好友请求后的回复 * @param {pushMsgUnitTypeOpt | pushMsgUnitTypeOpt[]} data - * @param {{msgInstance:msgInstanceType, to:string, isRoom?:boolean}} opt + * @param {{msgInstance:msgInstanceType, to?:string, isRoom?:boolean, bot:import('wechaty/impls').WechatyInterface}} opt */ -const recvdApiReplyHandler = async (data, { msgInstance, to, isRoom }) => { +const recvdApiReplyHandler = async (data, { msgInstance, bot, to, isRoom }) => { // 组装标准的请求结构 const { success, task, message, status } = await handleSendV2Msg( { to, isRoom, data }, - { skipReceiverCheck: true, messageReceiver: msgInstance } + { skipReceiverCheck: true, bot, messageReceiver: msgInstance } ) sendMsg2RecvdApi( - new Utils.TextMsg({ - text: JSON.stringify({ - event: 'notifyOfRecvdApiPushMsg', - recvdApiReplyNotify: { - success, - task, - message, - status - } - }), - isSystemEvent: true + new Utils.SystemEvent({ + event: 'notifyOfRecvdApiPushMsg', + recvdApiReplyNotify: { + success, + task, + message, + status + } }) ) } @@ -617,13 +684,15 @@ const recvdApiReplyHandler = async (data, { msgInstance, to, isRoom }) => { /** * 收消息钩子 * @param {import('wechaty').Message} msg + * @param {import('wechaty/impls').WechatyInterface} bot */ -const onRecvdMessage = async (msg) => { +const onRecvdMessage = async (msg, bot) => { // 自己发的消息没有必要处理 - if (msg.self()) return + if (process.env.ACCEPT_RECVD_MSG_MYSELF !== 'true' && msg.self()) return handleResSendMsg({ res: await sendMsg2RecvdApi(msg), + bot, type: msg.type(), msgInstance: msg }) diff --git a/src/service/msgUploader.js b/src/service/msgUploader.js index 132a088..41bf9a7 100644 --- a/src/service/msgUploader.js +++ b/src/service/msgUploader.js @@ -1,6 +1,6 @@ const Utils = require('../utils/index') const fetch = require('node-fetch-commonjs') -const { config } = require('../config/const') +const { config, systemMsgEnumMap } = require('../config/const') const FormData = require('form-data') const { LOCAL_RECVD_MSG_API, RECVD_MSG_API } = process.env const { MSG_TYPE_ENUM } = require('../config/const') @@ -13,8 +13,6 @@ const cacheTool = require('../service/cache') * @returns {Promise} recvdApiReponse */ async function sendMsg2RecvdApi(msg) { - // 自己发的消息没有必要转发(外部已经处理) - // if (msg.self()) return // 检测是否配置了webhookurl let webhookUrl /** @@ -79,10 +77,11 @@ async function sendMsg2RecvdApi(msg) { /** room的话解析群成员信息,原始信息不会带 */ room: roomInfo ?? '', to: msg.to() ?? '', + // @ts-ignore from: msg.talker() ?? '' } - let passed = true + // let passed = true /** @type {import('form-data')} */ const formData = new FormData() @@ -96,6 +95,9 @@ async function sendMsg2RecvdApi(msg) { (await msg.mentionSelf()) /** 原版@我,wechaty web版应该都是false */ formData.append('isMentioned', someoneMentionMe ? '1' : '0') + // 判断是否是自己发送的消息 + formData.append('isMsgFromSelf', msg.self() ? '1' : '0') + switch (msg.type()) { case MSG_TYPE_ENUM.ATTACHMENT: case MSG_TYPE_ENUM.VOICE: @@ -105,7 +107,7 @@ async function sendMsg2RecvdApi(msg) { formData.append('type', 'file') /**@type {import('file-box').FileBox} */ //@ts-expect-errors 这里msg一定是wechaty的msg - const steamFile = await msg.toFileBox() + const steamFile = msg.toFileBox ? await msg.toFileBox() : msg.content() let fileInfo = { // @ts-ignore @@ -148,14 +150,26 @@ async function sendMsg2RecvdApi(msg) { formData.append('type', 'friendship') formData.append('content', msg.text()) break - // 其他统一暂不处理 + + // 系统消息(用于上报状态) + case MSG_TYPE_ENUM.SYSTEM_EVENT_LOGIN: + case MSG_TYPE_ENUM.SYSTEM_EVENT_LOGOUT: + case MSG_TYPE_ENUM.SYSTEM_EVENT_PUSH_NOTIFY: + case MSG_TYPE_ENUM.SYSTEM_EVENT_ERROR: + formData.append('type', systemMsgEnumMap[msg.type()]) + formData.append('content', msg.text()) + break + + // 其他统一当unknown处理 + case MSG_TYPE_ENUM.UNKNOWN: case MSG_TYPE_ENUM.EMOTION: // 自定义表情 default: - passed = false + formData.append('type', 'unknown') + formData.append('content', msg.text()) break } - if (!passed) return + // if (!passed) return Utils.logger.info('starting fetching api: ' + webhookUrl) //@ts-expect-errors form-data 未定义的私有属性 diff --git a/src/utils/index.js b/src/utils/index.js index eba3bca..7cf4fe5 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -171,6 +171,17 @@ const sleep = async (ms) => { return await new Promise((resolve) => setTimeout(resolve, ms)) } +/** + * 删除登录缓存文件 + */ +// const deleteMemoryCard = () => { +// //@ts-expect-errors 必定是 pathlike +// if (fs.existsSync(memoryCardPath)) { +// //@ts-expect-errors 必定是 pathlike +// fs.unlinkSync(memoryCardPath) +// } +// } + module.exports = { ...require('./msg.js'), ...require('./nextTick.js'), diff --git a/src/utils/log.js b/src/utils/log.js index 44c6dff..7afdbb6 100644 --- a/src/utils/log.js +++ b/src/utils/log.js @@ -1,3 +1,8 @@ +const { + logOnlyOutputWhiteList, + logDontOutpuptBlackList +} = require('../config/log4jsFilter') + if (!process.env.homeEnvCfg) { const log4js = require('log4js') @@ -42,12 +47,9 @@ if (!process.env.homeEnvCfg) { const originalConsoleLog = console.log const originalConsoleWarn = console.warn const originalConsoleErr = console.error - /** - * - * @param {{logLevel?:string}} param0 - */ - const proxyConsole = ({ logLevel = 'info' } = {}) => { - logger.level = logLevel + + const proxyConsole = () => { + // logger.level = logLevel /** * 希望排除在log4js里的console输出,即不希望打到日志里去或者显示异常 * @param {any[]} args @@ -55,10 +57,9 @@ if (!process.env.homeEnvCfg) { const whiteListConditionLog = (args) => { const arg0 = args?.[0] - return [ - typeof arg0 === 'string' && - arg0.startsWith('▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄\n') /** 二维码扫码 */ - ].some(Boolean) + return logOnlyOutputWhiteList + .map((str) => typeof arg0 === 'string' && arg0.includes(str)) + .some(Boolean) } /** @@ -69,10 +70,7 @@ if (!process.env.homeEnvCfg) { const blackListConditionError = (args) => { const arg0 = args?.[0] - return [ - // form-data 提示的DepracationWarning,会被认为是错误提issue - '[https://github.com/node-fetch/node-fetch/issues/1167]' - ].some((str) => arg0.includes(str)) + return logDontOutpuptBlackList.some((str) => arg0.includes(str)) } console.log = function (...args) { diff --git a/src/utils/msg.js b/src/utils/msg.js index f96ba0b..ad54b0f 100644 --- a/src/utils/msg.js +++ b/src/utils/msg.js @@ -1,13 +1,26 @@ -const { MSG_TYPE_ENUM } = require('../config/const') +const { MSG_TYPE_ENUM, legacySystemMsgStrMap } = require('../config/const') class CommonMsg { /** - * @param {string} text - * @param {import("@src/config/const").MSG_TYPE_ENUM} type - * @param {boolean} [isSystemEvent] + * @param {commonMsgPayload} payload */ - constructor(text, type, isSystemEvent = false) { + constructor({ + text, + type, + isSystemEvent = false, + self = false, + room = '', + to, + from = '', + file = '' + }) { this.t = type + this.isSelf = self + this.toInfo = to + this.fromInfo = from + this.fileInfo = file + this.roomInfo = room this.payload = text + /** @deprecated 已经废弃,但保留其旧版本逻辑的兼容性 */ this.isSystemEvent = isSystemEvent } @@ -30,30 +43,51 @@ class CommonMsg { } self() { - return false + return this.isSelf } room() { - return '' + return this.roomInfo + } + + content() { + return this.fileInfo } to() { - return '' + return this.toInfo } talker() { - return '' + return this.fromInfo + } +} + +class ApiMsg extends CommonMsg { + /** @param {msgStructurePayload } payload*/ + constructor({ from, to, room = '', content = '', type, self = false }) { + if (type === MSG_TYPE_ENUM.TEXT) { + super({ + from, + to, + room, + // @ts-expect-error 此处一定是string + text: content, + type, + self + }) + } else { + super({ from, to, room, type, file: content, self }) + } } } class TextMsg extends CommonMsg { /** - * @param {Object} option - * @param {string} option.text - * @param {boolean} option.isSystemEvent + * @param {string} text */ - constructor({ text, isSystemEvent = false }) { - super(text, MSG_TYPE_ENUM.TEXT, isSystemEvent) + constructor(text) { + super({ text, type: MSG_TYPE_ENUM.TEXT }) } } @@ -62,12 +96,30 @@ class FriendshipMsg extends CommonMsg { * @param {Record} payload */ constructor(payload) { - super(JSON.stringify(payload), MSG_TYPE_ENUM.CUSTOM_FRIENDSHIP) + super({ + text: JSON.stringify(payload), + type: MSG_TYPE_ENUM.CUSTOM_FRIENDSHIP + }) + } +} + +class SystemEvent extends CommonMsg { + /** + * @param {systemEventPayload} payload + */ + constructor(payload) { + super({ + text: JSON.stringify(payload), + type: legacySystemMsgStrMap[payload.event], + isSystemEvent: true + }) } } module.exports = { CommonMsg, + ApiMsg, TextMsg, - FriendshipMsg + FriendshipMsg, + SystemEvent } diff --git a/src/wechaty/init.js b/src/wechaty/init.js index 028b6d7..1d090dc 100644 --- a/src/wechaty/init.js +++ b/src/wechaty/init.js @@ -1,19 +1,27 @@ +const { version } = require('../../package.json') const { WechatyBuilder } = require('wechaty') +const { SystemEvent } = require('../utils/msg.js') const Service = require('../service') const Utils = require('../utils/index') const chalk = require('chalk') -const { PORT, homeEnvCfg, homeMemoryCardPath } = process.env -const isCliEnv = Boolean(homeEnvCfg) +const { PORT } = process.env +const { memoryCardName, logOutUnofficialCodeList } = require('../config/const') const token = Service.initLoginApiToken() const cacheTool = require('../service/cache') const bot = process.env.DISABLE_AUTO_LOGIN === 'true' ? WechatyBuilder.build() : WechatyBuilder.build({ - name: isCliEnv ? homeMemoryCardPath : 'loginSession' + name: memoryCardName }) module.exports = function init() { + /** @type {import('wechaty').Contact} */ + let currentUser + let botLoginSuccessLastTime = false + + console.log(chalk.blue(`🤖 wechatbot-webhook v${version} 🤖`)) + // 启动 Wechaty 机器人 bot // 扫码登陆事件 @@ -43,11 +51,32 @@ module.exports = function init() { 'https://github.com/danni-cool/wechatbot-webhook?tab=readme-ov-file#%EF%B8%8F-api' )}\n` ) + + currentUser = user + botLoginSuccessLastTime = true + + Service.sendMsg2RecvdApi(new SystemEvent({ event: 'login', user })).catch( + (e) => { + Utils.logger.error('上报login事件给 RECVD_MSG_API 出错', e) + } + ) }) // 登出事件 .on('logout', async (user) => { + /** bugfix: 重置登录会触发多次logout,但是上报只需要登录成功后登出那一次 */ + if (!botLoginSuccessLastTime) return + + botLoginSuccessLastTime = false + Utils.logger.info(chalk.red(`User ${user.toString()} logout`)) + + // 登出时给接收消息api发送特殊文本 + Service.sendMsg2RecvdApi( + new SystemEvent({ event: 'logout', user }) + ).catch((e) => { + Utils.logger.error('上报 logout 事件给 RECVD_MSG_API 出错:', e) + }) }) .on('room-topic', async (room, topic, oldTopic, changer) => { @@ -75,7 +104,7 @@ module.exports = function init() { // 收到消息事件 .on('message', async (message) => { Utils.logger.info(`Message: ${message.toString()}`) - Service.onRecvdMessage(message).catch((e) => { + Service.onRecvdMessage(message, bot).catch((e) => { Utils.logger.error('向 RECVD_MSG_API 上报 message 事件出错:', e) }) }) @@ -86,8 +115,24 @@ module.exports = function init() { }) // 各种出错事件 - .on('error', (error) => { + .on('error', async (error) => { Utils.logger.error(`\n${chalk.red(error)}\n`) + + if (!bot.isLoggedIn) return + + // wechaty 未知的登出状态,处理异常错误后的登出上报 + if ( + logOutUnofficialCodeList.some((item) => error.message.includes(item)) + ) { + await bot.logout() + } + + // 发送error事件给接收消息api + Service.sendMsg2RecvdApi( + new SystemEvent({ event: 'error', error, user: currentUser }) + ).catch((e) => { + Utils.logger.error('上报 error 事件给 RECVD_MSG_API 出错:', e) + }) }) bot.start().catch((e) => { diff --git a/tsconfig.json b/tsconfig.json index 47e3beb..b0a7050 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "noEmit": true + "noEmit": true, + "resolveJsonModule": true, } } diff --git a/typings/global.d.ts b/typings/global.d.ts index 9bed2f8..96bc4be 100644 --- a/typings/global.d.ts +++ b/typings/global.d.ts @@ -115,3 +115,38 @@ type standardV2Payload = { data: pushMsgUnitTypeOpt | pushMsgUnitTypeOpt[] unValidParamsStr: string } + +type systemEventPayload = { + event: keyof typeof import('@src/config/const').legacySystemMsgStrMap + user?: import('wechaty').Contact + recvdApiReplyNotify?: { + success: boolean + task: msgV2taskType + message: string + status: number + } + error?: import('gerror').GError +} + +type msgStructurePayload = { + content: string | (import('file-box').FileBoxInterface & { _name: string }) + type: number + type_display: string + isSystemEvent?: boolean + self: boolean + from: import('wechaty').Contact | '' + to: msgInstanceType + room: import('wechaty').Room | '' + file?: '' | File +} + +type commonMsgPayload = { + text?: string + type: import('@src/config/const').MSG_TYPE_ENUM + isSystemEvent?: boolean + self?: boolean + from?: import('wechaty').Contact | '' + to?: msgInstanceType + room?: import('wechaty').Room | '' + file?: string | (import('file-box').FileBoxInterface & { _name: string }) +}