Skip to content
This repository was archived by the owner on Jan 10, 2025. It is now read-only.

Commit

Permalink
feat: 🎸 支持掉线或者异常时的通知机制
Browse files Browse the repository at this point in the history
Closes: #9
  • Loading branch information
chentianyu committed Oct 8, 2023
1 parent 1b64d1e commit 6008271
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 46 deletions.
4 changes: 2 additions & 2 deletions .env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
PORT=3001
# 如果想自己处理收到消息的逻辑,在下面填上你的API地址, 默认为空
LOCAL_RECVD_MSG_API=
LOCAL_RECVD_MSG_API=https://home.danielcoding.me:888/webhook-test/53f51927-9cec-447b-899a-d4a212000808
# 登录地址Token访问地址: http://localhost:3001/loginCheck?token=[LOCAL_LOGIN_API_TOKEN]
# 生成规则:src/utils/index.js
# 生成规则:src/utils/index.js -> generateToken
LOCAL_LOGIN_API_TOKEN=
55 changes: 49 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

[view this project on docker hub :)](https://hub.docker.com/repository/docker/dannicool/docker-wechatbot-webhook/general)

## News (2023.10.8)
> 目前已知的是登录2天左右会掉,应该是网页微信风控的问题(长时间无消息),目前解决方案是触发了掉线或者异常通知后,通知你配置的 `RECVD_MSG_API`,去处理扫码登录逻辑,比如访问暴露到外网的登录 api http://localhost:3001/loginCheck?token=[your token]
如果有更好的方案可以和我交流 : )


## 一、启动

### 1. 本地调试
Expand All @@ -33,18 +38,31 @@ LOCAL_RECVD_MSG_API=https://example.com/your/url
docker pull dannicool/docker-wechatbot-webhook
```

#### 启动容器(后台常驻)
#### 启动容器

该方法会在后台启动一个 **只能给微信推消息** 的容器

```bash
docker run -d \
--name wxBotWebhook \
-p 3001:3001 \
-e RECVD_MSG_API="https://example.com/your/url" \
dannicool/docker-wechatbot-webhook
```
收消息钩子
> -e RECVD_MSG_API 如果想自己处理收到消息的逻辑,比如根据消息联动,填上你的处理逻辑url,该行可以省略

#### 容器参数(可选)

我还想收消息
> 如果想自己处理收到消息的逻辑,比如根据消息联动,填上你的处理逻辑 url,该行可以省略
```
-e RECVD_MSG_API="https://example.com/your/url" \
```

我想自定义登录 API 令牌
> 容器启动后支持通过api 形式获得 登录状态 / 扫码登录 url,你也可以自定义一个自己的令牌,不配置的话,默认会生成一个
```
-e LOGIN_API_TOKEN="<YOUR PERSONAL TOKEN>" \
```
## 二、登录wx

以下只展示 docker 启动,本地调试可以直接在控制台找到链接
Expand All @@ -55,7 +73,7 @@ docker logs -f wxBotWebhook

找到二维码登录地址,图下 url 部分,浏览器访问,扫码登录wx

![](https://cdn.jsdelivr.net/gh/danni-cool/danni-cool@cdn/image/docker-wechat-login-demo.png)
![](https://cdn.jsdelivr.net/gh/danni-cool/danni-cool@cdn/image/wechatlogindemo.png)

## 三、API

Expand Down Expand Up @@ -88,8 +106,9 @@ docker logs -f wxBotWebhook
| formData | 说明 | 数据类型 | 可选值 |
|--|--|--|--|
| type | 表单类型 | `String` | `text` / `img` |
| content | 传输的内容,文件也放在这个字段,如果是图片收到的就是二进制buffer | `String` / `Binary` | |
| content | 传输的内容,文件也放在这个字段,如果是图片收到的就是二进制buffer, 如果 `isSystemEvent` 为 '1', 将收到 `JSON String` | `String` / `Binary` | |
| source | 消息的相关发送方数据, JSON String | `String` | |
| isSystemEvent | 是否是来自系统消息事件(比如 掉线、异常事件)| `String` | 1 / 0

source 字段示例

Expand Down Expand Up @@ -160,6 +179,30 @@ source 字段示例
}
```

### 3. 通过 API 获得登录状态

example: 访问登录shell 的 `http://localhost:3001/loginCheck?token=YOUR_PERSONAL_TOKEN`, 你将得到当前的登录态

token 是必填项,无需配置,初次启动项目会自动生成一个,当然你也可以配置一个简单好记的个人 token, 有两种方式

1. docker 启动,参数为 -e LOGIN_API_TOKEN="YOUR_PERSONAL_TOKEN"
2. `.env` 文件中,配置 LOCAL_LOGIN_API_TOKEN=YOUR_PERSONAL_TOKEN

> 如果都配置,docker 配置将覆盖本地配置
**请求体**

- Methods: `GET`
- URL: http://localhost:3001/loginCheck?token=YOUR_PERSONAL_TOKEN

**返回体**



| Json | 说明 | 数据类型 | 可选值 |
|--|--|--|--|
| success | 登录成功与否 | `Boolean` | `true` / `false` |
| content | | `String` / `Binary` | |


## 四、更新日志
Expand Down
2 changes: 2 additions & 0 deletions dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ COPY . .

# 如果收消息想接入webhook
ENV RECVD_MSG_API=
# 默认登录API接口访问token
ENV LOGIN_API_TOKEN=

# 暴露端口(你的 Express 应用程序监听的端口)
EXPOSE 3001
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "给微信里加个 webhook 机器人,支持docker部署",
"main": "index.js",
"scripts": {
"prestart": "node ./scripts/writeLoginApiToken",
"start": "node main",
"release": "standard-version"
},
Expand Down
4 changes: 0 additions & 4 deletions scripts/shellToken.js

This file was deleted.

43 changes: 43 additions & 0 deletions scripts/writeLoginApiToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 给 shell 调用使用
const fs = require('fs');
const dotenv = require('dotenv');
const { generateToken } = require('../src/utils/index')

// 读取 .env 文件内容
const envContent = fs.readFileSync('.env', 'utf-8').split('\n');

// 解析 .env 文件内容
const envConfig = dotenv.parse(envContent.join('\n'));

// 无配置token,会默认生成一个token
if(envConfig.LOCAL_LOGIN_API_TOKEN) return
const token = generateToken()
console.log(`检测未配置 LOGIN_API_TOKEN, 写入初始化值 LOCAL_LOGIN_API_TOKEN=${token} => .env \n`)

envConfig.LOCAL_LOGIN_API_TOKEN = token // 添加或修改键值对

// 生成新的 .env 文件内容,同时保留注释
const newEnv = envContent.map(line => {
if (line.startsWith('#')) {
// 保留注释
return line;
}

const [key] = line.split('=');
if (envConfig[key] !== undefined) {
// 更新已存在的键值对
const updatedLine = `${key}=${envConfig[key]}`;
delete envConfig[key]; // 从 envConfig 中移除已处理的键
return updatedLine;
}

return line;
}).join('\n');

// 将未在原始 .env 文件中的新键值对添加到文件末尾
for (const [key, value] of Object.entries(envConfig)) {
newEnv += `\n${key}=${value}`;
}

// 写入新的 .env 文件内容
fs.writeFileSync('.env', newEnv);
15 changes: 15 additions & 0 deletions src/route/loginCheck.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const { sendMsg2RecvdApi } = require('../service/webhook')
const { TextMsg } = require('../utils/msg')

// 登录
module.exports = function registerLoginCheck({ app, bot }) {
let message,
Expand All @@ -15,6 +18,18 @@ module.exports = function registerLoginCheck({ app, bot }) {
.on('logout', user => {
message = ''
success = false
// 登出时给接收消息api发送特殊文本
sendMsg2RecvdApi(new TextMsg({
text: JSON.stringify({ event: 'logout', user }),
isSystemEvent: true
}))
})
.on('error', error => {
// 报错时接收特殊文本
sendMsg2RecvdApi(new TextMsg({
text: JSON.stringify({ event: 'error', error }),
isSystemEvent: true
}))
})

// 处理 POST 请求
Expand Down
48 changes: 23 additions & 25 deletions src/service/webhook.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
const fetch = require('node-fetch-commonjs')
const FormData = require('form-data')
const chalk = require('chalk')
const { generateToken } = require('../utils/index')
const {
LOCAL_RECVD_MSG_API,
RECVD_MSG_API,
LOGIN_API_TOKEN,
LOCAL_LOGIN_API_TOKEN
} = process.env

const sendMsg2RecvdApi = async function (msg, webhookUrl) {
const sendMsg2RecvdApi = async function (msg) {
// 检测是否配置了webhookurl
let webhookUrl
let errorText = (key, value) => console.error(chalk.red(`配置参数 ${key}: ${chalk.cyan(value)} <- 不符合 URL 规范, 该 API 将不会收到请求\n`))

// 外部传入了以外部为准
if (!['', undefined].includes(RECVD_MSG_API)) {
webhookUrl = ('' + RECVD_MSG_API).startsWith('http') ? RECVD_MSG_API : ''
!webhookUrl && errorText('RECVD_MSG_API', RECVD_MSG_API)
// 无外部则用本地
} else if (!['', undefined].includes(LOCAL_RECVD_MSG_API)) {
webhookUrl = ('' + LOCAL_RECVD_MSG_API).startsWith('http') ? LOCAL_RECVD_MSG_API : ''
!webhookUrl && errorText('LOCAL_RECVD_MSG_API', LOCAL_RECVD_MSG_API)
}
// 有webhookurl才发送
if (!webhookUrl) return

const source = {
room: msg.room() || '',
to: msg.to() || '',
from: msg.talker() || '',
}

const formData = new FormData();
let passed = true
const formData = new FormData();

formData.append('source', JSON.stringify(source))
formData.append('isSystemEvent', msg.isSystemEvent ? '1' : '0')

switch (msg.type()) {
// 图片
Expand Down Expand Up @@ -57,37 +74,18 @@ const sendMsg2RecvdApi = async function (msg, webhookUrl) {
})
}

// 得到收消息api,并做格式检查
const getValidRecvdApi = () => {
let webhookUrl = ''
let errorText = (key, value) => console.error(chalk.red(`配置参数 ${key}: ${chalk.cyan(value)} <- 不符合 URL 规范, 该 API 将不会收到请求\n`))

// 外部传入了以外部为准
if (!['', undefined].includes(RECVD_MSG_API)) {
webhookUrl = ('' + RECVD_MSG_API).startsWith('http') ? RECVD_MSG_API : ''
!webhookUrl && errorText('RECVD_MSG_API', RECVD_MSG_API)
// 无外部则用本地
} else if (!['', undefined].includes(LOCAL_RECVD_MSG_API)) {
webhookUrl = ('' + LOCAL_RECVD_MSG_API).startsWith('http') ? LOCAL_RECVD_MSG_API : ''
!webhookUrl && errorText('LOCAL_RECVD_MSG_API', LOCAL_RECVD_MSG_API)
}

return webhookUrl
}

//得到 loginAPIToken
const getLoginApiToken = () => {
if(!process.env.globalLoginToken) {
process.env.globalLoginToken = LOGIN_API_TOKEN || LOCAL_LOGIN_API_TOKEN || generateToken()
if (!process.env.globalLoginToken) {
process.env.globalLoginToken = LOGIN_API_TOKEN || LOCAL_LOGIN_API_TOKEN
}

return process.env.globalLoginToken
}



module.exports = {
sendMsg2RecvdApi,
getValidRecvdApi,
getLoginApiToken
}
30 changes: 30 additions & 0 deletions src/utils/msg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module.exports.TextMsg = class TextMsg {
constructor({ text, isSystemEvent = false }) {
this.payload = text
this.isSystemEvent = isSystemEvent
}

type() {
return 7
}

text() {
return this.payload
}

self() {
return false
}

room() {
return ''
}

to() {
return ''
}

talker() {
return ''
}
}
16 changes: 7 additions & 9 deletions src/wechaty/init.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
const { WechatyBuilder } = require('wechaty')
const { sendMsg2RecvdApi, getValidRecvdApi, getLoginApiToken } = require('../service/webhook')
const { sendMsg2RecvdApi, getLoginApiToken } = require('../service/webhook')
const bot = WechatyBuilder.build() // get a Wechaty instance
const chalk = require('chalk')
const { PORT } = process.env

module.exports = function init() {
const webhookUrl = getValidRecvdApi()

// 启动 Wechaty 机器人
bot
.on('scan', (qrcode) =>
console.log([
`\nAccess the URL to login: ${chalk.cyan('https://wechaty.js.org/qrcode/' + encodeURIComponent(qrcode))}`,
'You can also check login by API: '+ chalk.cyan(`http://localhost:${PORT}/loginCheck?token=${getLoginApiToken()}`)
].join('\n')))
.on('scan', (qrcode) =>
console.log([
`\nAccess the URL to login: ${chalk.cyan('https://wechaty.js.org/qrcode/' + encodeURIComponent(qrcode))}`,
'You can also check login by API: ' + chalk.cyan(`http://localhost:${PORT}/loginCheck?token=${getLoginApiToken()}`)
].join('\n')))
.on('login', async user => console.log(chalk.green(`User ${user} logged in`)))
.on('logout', async user => console.log(chalk.red(`User ${user} logout`)))
// .on('room-topic', async (room, topic, oldTopic, changer) => {
Expand All @@ -22,7 +20,7 @@ module.exports = function init() {
.on('message', async message => {
console.log(`Message: ${message}`)
//收到消息二次转发特殊处理
webhookUrl && await sendMsg2RecvdApi(message, webhookUrl)
sendMsg2RecvdApi(message)

})
.on('error', (error) => {
Expand Down

0 comments on commit 6008271

Please sign in to comment.