@alemonjs/qq-bot 0.0.12 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/dist/assets/index.css +1 -1
- package/dist/assets/index.js +1 -1
- package/lib/api.d.ts +971 -843
- package/lib/api.js +1172 -1156
- package/lib/client.d.ts +22 -22
- package/lib/client.js +201 -217
- package/lib/config.js +2 -2
- package/lib/desktop.js +3 -1
- package/lib/from.js +27 -34
- package/lib/index.d.ts +3 -3
- package/lib/index.group.js +238 -0
- package/lib/index.guild.js +338 -0
- package/lib/index.js +43 -2
- package/lib/message/AT_MESSAGE_CREATE.d.ts +35 -35
- package/lib/message/C2C_MESSAGE_CREATE.d.ts +9 -9
- package/lib/message/CHANNEL_CREATE.d.ts +15 -15
- package/lib/message/CHANNEL_DELETE.d.ts +15 -15
- package/lib/message/CHANNEL_UPDATE.d.ts +15 -15
- package/lib/message/DIRECT_MESSAGE_CREATE.d.ts +29 -29
- package/lib/message/DIRECT_MESSAGE_DELETE.d.ts +17 -17
- package/lib/message/ERROR.d.ts +2 -2
- package/lib/message/GROUP_AT_MESSAGE_CREATE.d.ts +10 -10
- package/lib/message/GUILD_CREATE.d.ts +15 -15
- package/lib/message/GUILD_DELETE.d.ts +15 -15
- package/lib/message/GUILD_MEMBER_ADD.d.ts +14 -14
- package/lib/message/GUILD_MEMBER_REMOVE.d.ts +14 -14
- package/lib/message/GUILD_MEMBER_UPDATE.d.ts +14 -14
- package/lib/message/GUILD_UPDATE.d.ts +15 -15
- package/lib/message/INTERACTION_CREATE.d.ts +2 -2
- package/lib/message/MESSAGE_CREATE.d.ts +2 -2
- package/lib/message/MESSAGE_DELETE.d.ts +2 -2
- package/lib/message/MESSAGE_REACTION_ADD.d.ts +13 -13
- package/lib/message/MESSAGE_REACTION_REMOVE.d.ts +13 -13
- package/lib/message/PUBLIC_MESSAGE_DELETE.d.ts +15 -15
- package/lib/message/READY.d.ts +6 -6
- package/lib/message.d.ts +46 -46
- package/lib/sdk/api.d.ts +847 -0
- package/lib/sdk/api.js +1183 -0
- package/lib/sdk/client.js +228 -0
- package/lib/sdk/config.js +3 -0
- package/lib/sdk/counter.js +19 -0
- package/lib/sdk/from.js +44 -0
- package/lib/sdk/intents.js +102 -0
- package/lib/sdk/typing.d.ts +56 -0
- package/lib/sdk/webhook.secret.js +53 -0
- package/lib/sdk/websoket.group.js +221 -0
- package/lib/sdk/websoket.guild.js +203 -0
- package/lib/send/index.js +87 -21
- package/lib/typing.d.ts +62 -54
- package/lib/utils.js +14 -0
- package/lib/webhook.js +46 -48
- package/package.json +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { QQBotAPI } from './api.js';
|
|
2
|
+
import bodyParser from 'koa-bodyparser';
|
|
3
|
+
import Router from 'koa-router';
|
|
4
|
+
import { WebhookAPI } from './webhook.secret.js';
|
|
5
|
+
import Koa from 'koa';
|
|
6
|
+
import { config } from './config.js';
|
|
7
|
+
import { v4 } from 'uuid';
|
|
8
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
9
|
+
|
|
10
|
+
class QQBotClient extends QQBotAPI {
|
|
11
|
+
#events = {};
|
|
12
|
+
#app = null;
|
|
13
|
+
#count = 0;
|
|
14
|
+
#client = [];
|
|
15
|
+
#ws = null;
|
|
16
|
+
/**
|
|
17
|
+
* 设置配置
|
|
18
|
+
* @param opstion
|
|
19
|
+
*/
|
|
20
|
+
constructor(opstion) {
|
|
21
|
+
super();
|
|
22
|
+
if (opstion.secret)
|
|
23
|
+
config.set('secret', opstion.secret);
|
|
24
|
+
if (opstion.app_id)
|
|
25
|
+
config.set('app_id', opstion.app_id);
|
|
26
|
+
if (opstion.token)
|
|
27
|
+
config.set('token', opstion.token);
|
|
28
|
+
if (opstion.port)
|
|
29
|
+
config.set('port', opstion.port);
|
|
30
|
+
if (opstion.ws)
|
|
31
|
+
config.set('ws', opstion.ws);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 注册事件处理程序
|
|
35
|
+
* @param key 事件名称
|
|
36
|
+
* @param val 事件处理函数
|
|
37
|
+
*/
|
|
38
|
+
on(key, val) {
|
|
39
|
+
if (!this.#events[key]) {
|
|
40
|
+
this.#events[key] = [];
|
|
41
|
+
}
|
|
42
|
+
this.#events[key].push(val);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 定时鉴权
|
|
47
|
+
* @param cfg
|
|
48
|
+
* @returns
|
|
49
|
+
*/
|
|
50
|
+
async #setTimeoutBotConfig() {
|
|
51
|
+
const callBack = async () => {
|
|
52
|
+
const app_id = config.get('app_id');
|
|
53
|
+
if (!app_id)
|
|
54
|
+
return;
|
|
55
|
+
const secret = config.get('secret');
|
|
56
|
+
if (!secret)
|
|
57
|
+
return;
|
|
58
|
+
// 发送请求
|
|
59
|
+
const data = await this.getAuthentication(app_id, secret).then(res => res.data);
|
|
60
|
+
config.set('access_token', data.access_token);
|
|
61
|
+
console.info('refresh', data.expires_in, 's');
|
|
62
|
+
setTimeout(callBack, data.expires_in * 1000);
|
|
63
|
+
};
|
|
64
|
+
await callBack();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
*
|
|
68
|
+
* @param cfg
|
|
69
|
+
* @param conversation
|
|
70
|
+
*/
|
|
71
|
+
connect() {
|
|
72
|
+
try {
|
|
73
|
+
const ws = config.get('ws');
|
|
74
|
+
if (!ws) {
|
|
75
|
+
this.#setTimeoutBotConfig();
|
|
76
|
+
this.#app = new Koa();
|
|
77
|
+
this.#app.use(bodyParser());
|
|
78
|
+
const router = new Router();
|
|
79
|
+
const port = config.get('port');
|
|
80
|
+
const secret = config.get('secret');
|
|
81
|
+
const route = config.get('route') ?? '/webhook';
|
|
82
|
+
const cfg = {
|
|
83
|
+
secret: secret ?? '',
|
|
84
|
+
port: port ? Number(port) : 17157
|
|
85
|
+
};
|
|
86
|
+
const ntqqWebhook = new WebhookAPI({
|
|
87
|
+
secret: cfg.secret
|
|
88
|
+
});
|
|
89
|
+
this.#app.use(async (ctx, next) => {
|
|
90
|
+
let rawData = '';
|
|
91
|
+
ctx.req.on('data', chunk => (rawData += chunk));
|
|
92
|
+
ctx.req.on('end', () => (ctx.request.rawBody = rawData));
|
|
93
|
+
await next();
|
|
94
|
+
});
|
|
95
|
+
// 启动服务
|
|
96
|
+
router.post(route, async (ctx) => {
|
|
97
|
+
const sign = ctx.req.headers['x-signature-ed25519'];
|
|
98
|
+
const timestamp = ctx.req.headers['x-signature-timestamp'];
|
|
99
|
+
const rawBody = ctx.request.rawBody;
|
|
100
|
+
const isValid = ntqqWebhook.validSign(timestamp, rawBody, String(sign));
|
|
101
|
+
if (!isValid) {
|
|
102
|
+
ctx.status = 400;
|
|
103
|
+
ctx.body = { msg: 'invalid signature' };
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const body = ctx.request.body;
|
|
107
|
+
if (body.op == 13) {
|
|
108
|
+
ctx.status = 200;
|
|
109
|
+
ctx.body = {
|
|
110
|
+
// 返回明文 token
|
|
111
|
+
plain_token: body.d.plain_token,
|
|
112
|
+
// 生成签名
|
|
113
|
+
signature: ntqqWebhook.getSign(body.d.event_ts, body.d.plain_token)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
else if (body.op == 0) {
|
|
117
|
+
ctx.status = 204;
|
|
118
|
+
// 根据事件类型,处理事件
|
|
119
|
+
for (const event of this.#events[body.t] || []) {
|
|
120
|
+
event(body.d);
|
|
121
|
+
}
|
|
122
|
+
const access_token = config.get('access_token');
|
|
123
|
+
// 也可以分法到客户端。 发送失败需要处理 或清理调
|
|
124
|
+
for (const client of this.#client) {
|
|
125
|
+
try {
|
|
126
|
+
if (access_token)
|
|
127
|
+
body['access_token'] = access_token;
|
|
128
|
+
client.ws.send(JSON.stringify(body));
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
this.#error(e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
this.#app.use(router.routes());
|
|
137
|
+
this.#app.use(router.allowedMethods());
|
|
138
|
+
// 启动服务
|
|
139
|
+
const server = this.#app.listen(cfg.port, () => {
|
|
140
|
+
console.log('Server running at http://localhost:' + cfg.port + route);
|
|
141
|
+
});
|
|
142
|
+
// 创建 WebSocketServer 并监听同一个端口
|
|
143
|
+
const wss = new WebSocketServer({ server: server });
|
|
144
|
+
console.log('Server running at wss://localhost:' + cfg.port + '/');
|
|
145
|
+
// 处理客户端连接
|
|
146
|
+
wss.on('connection', ws => {
|
|
147
|
+
const clientId = v4();
|
|
148
|
+
ws['clientId'] = clientId;
|
|
149
|
+
console.log(clientId, 'connection');
|
|
150
|
+
this.#client.push({ id: clientId, ws });
|
|
151
|
+
// 处理消息事件
|
|
152
|
+
ws.on('message', (message) => {
|
|
153
|
+
// 拿到消息
|
|
154
|
+
try {
|
|
155
|
+
const body = JSON.parse(message.toString());
|
|
156
|
+
for (const event of this.#events[body.t] || []) {
|
|
157
|
+
event(body.d);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
this.#error(e);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// 处理关闭事件
|
|
165
|
+
ws.on('close', () => {
|
|
166
|
+
console.log(`${clientId} disconnected`);
|
|
167
|
+
this.#client = this.#client.filter(client => client.id !== clientId);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const reconnect = () => {
|
|
173
|
+
// 使用了ws服务器
|
|
174
|
+
this.#ws = new WebSocket(ws);
|
|
175
|
+
this.#ws.on('open', () => {
|
|
176
|
+
this.#count = 0;
|
|
177
|
+
console.log('ws connected');
|
|
178
|
+
});
|
|
179
|
+
this.#ws.on('message', data => {
|
|
180
|
+
try {
|
|
181
|
+
// 拿到消息
|
|
182
|
+
const body = JSON.parse(data.toString());
|
|
183
|
+
const access_token = body['access_token'];
|
|
184
|
+
if (access_token)
|
|
185
|
+
config.set('access_token', access_token);
|
|
186
|
+
for (const event of this.#events[body.t] || []) {
|
|
187
|
+
event(body.d);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (e) {
|
|
191
|
+
this.#error(e);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
this.#ws.on('close', () => {
|
|
195
|
+
console.log('ws closed');
|
|
196
|
+
// 重连5次,超过5次不再重连
|
|
197
|
+
if (this.#count > 5)
|
|
198
|
+
return;
|
|
199
|
+
// 1.3s 后重连
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
reconnect();
|
|
202
|
+
}, 1300);
|
|
203
|
+
});
|
|
204
|
+
this.#ws.on('error', e => {
|
|
205
|
+
this.#error(e);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
reconnect();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
this.#error(e);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
*
|
|
217
|
+
* @param error
|
|
218
|
+
*/
|
|
219
|
+
#error(error) {
|
|
220
|
+
if (this.#events['ERROR']) {
|
|
221
|
+
for (const event of this.#events['ERROR'] || []) {
|
|
222
|
+
event(error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { QQBotClient };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class Counter {
|
|
2
|
+
#counter = 1;
|
|
3
|
+
#val = 0;
|
|
4
|
+
constructor(initialValue) {
|
|
5
|
+
this.#counter = initialValue;
|
|
6
|
+
this.#val = initialValue;
|
|
7
|
+
}
|
|
8
|
+
getNextId() {
|
|
9
|
+
return ++this.#counter;
|
|
10
|
+
}
|
|
11
|
+
get() {
|
|
12
|
+
return this.#counter;
|
|
13
|
+
}
|
|
14
|
+
reStart() {
|
|
15
|
+
this.#counter = this.#val;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Counter };
|
package/lib/sdk/from.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, createReadStream } from 'fs';
|
|
2
|
+
import { Readable, isReadable } from 'stream';
|
|
3
|
+
import { basename } from 'path';
|
|
4
|
+
import { fileTypeFromBuffer, fileTypeFromStream } from 'file-type';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 创建form
|
|
8
|
+
* @param image
|
|
9
|
+
* @param name
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
async function createPicFrom(image, name = 'image.jpg') {
|
|
13
|
+
let picData;
|
|
14
|
+
// 是 string
|
|
15
|
+
if (typeof image === 'string') {
|
|
16
|
+
if (!existsSync(image))
|
|
17
|
+
return false;
|
|
18
|
+
if (!name)
|
|
19
|
+
name = basename(image);
|
|
20
|
+
picData = createReadStream(image);
|
|
21
|
+
// 是 buffer
|
|
22
|
+
}
|
|
23
|
+
else if (Buffer.isBuffer(image)) {
|
|
24
|
+
const file = await fileTypeFromBuffer(image);
|
|
25
|
+
if (!name)
|
|
26
|
+
name = 'file.' + file?.ext;
|
|
27
|
+
picData = new Readable();
|
|
28
|
+
picData.push(image);
|
|
29
|
+
picData.push(null);
|
|
30
|
+
// 是 文件流
|
|
31
|
+
}
|
|
32
|
+
else if (isReadable(image)) {
|
|
33
|
+
const file = await fileTypeFromStream(image);
|
|
34
|
+
if (!name)
|
|
35
|
+
name = 'file.' + file?.ext;
|
|
36
|
+
picData = image;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return { picData, image, name };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { createPicFrom };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GUILDS (1 << 0)
|
|
3
|
+
- GUILD_CREATE // 当机器人加入新guild时
|
|
4
|
+
- GUILD_UPDATE // 当guild资料发生变更时
|
|
5
|
+
- GUILD_DELETE // 当机器人退出guild时
|
|
6
|
+
- CHANNEL_CREATE // 当channel被创建时
|
|
7
|
+
- CHANNEL_UPDATE // 当channel被更新时
|
|
8
|
+
- CHANNEL_DELETE // 当channel被删除时
|
|
9
|
+
|
|
10
|
+
GUILD_MEMBERS (1 << 1)
|
|
11
|
+
- GUILD_MEMBER_ADD // 当成员加入时
|
|
12
|
+
- GUILD_MEMBER_UPDATE // 当成员资料变更时
|
|
13
|
+
- GUILD_MEMBER_REMOVE // 当成员被移除时
|
|
14
|
+
|
|
15
|
+
GUILD_MESSAGES (1 << 9) // 消息事件,仅 *私域* 机器人能够设置此 intents。
|
|
16
|
+
- MESSAGE_CREATE // 发送消息事件,代表频道内的全部消息,而不只是 at 机器人的消息。内容与 AT_MESSAGE_CREATE 相同
|
|
17
|
+
- MESSAGE_DELETE // 删除(撤回)消息事件
|
|
18
|
+
|
|
19
|
+
GUILD_MESSAGE_REACTIONS (1 << 10)
|
|
20
|
+
- MESSAGE_REACTION_ADD // 为消息添加表情表态
|
|
21
|
+
- MESSAGE_REACTION_REMOVE // 为消息删除表情表态
|
|
22
|
+
|
|
23
|
+
DIRECT_MESSAGE (1 << 12)
|
|
24
|
+
- DIRECT_MESSAGE_CREATE // 当收到用户发给机器人的私信消息时
|
|
25
|
+
- DIRECT_MESSAGE_DELETE // 删除(撤回)消息事件
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
GROUP_AND_C2C_EVENT (1 << 25)
|
|
29
|
+
- C2C_MESSAGE_CREATE // 用户单聊发消息给机器人时候
|
|
30
|
+
- FRIEND_ADD // 用户添加使用机器人
|
|
31
|
+
- FRIEND_DEL // 用户删除机器人
|
|
32
|
+
- C2C_MSG_REJECT // 用户在机器人资料卡手动关闭"主动消息"推送
|
|
33
|
+
- C2C_MSG_RECEIVE // 用户在机器人资料卡手动开启"主动消息"推送开关
|
|
34
|
+
- GROUP_AT_MESSAGE_CREATE // 用户在群里@机器人时收到的消息
|
|
35
|
+
- GROUP_ADD_ROBOT // 机器人被添加到群聊
|
|
36
|
+
- GROUP_DEL_ROBOT // 机器人被移出群聊
|
|
37
|
+
- GROUP_MSG_REJECT // 群管理员主动在机器人资料页操作关闭通知
|
|
38
|
+
- GROUP_MSG_RECEIVE // 群管理员主动在机器人资料页操作开启通知
|
|
39
|
+
|
|
40
|
+
INTERACTION (1 << 26)
|
|
41
|
+
- INTERACTION_CREATE // 互动事件创建时
|
|
42
|
+
|
|
43
|
+
MESSAGE_AUDIT (1 << 27)
|
|
44
|
+
- MESSAGE_AUDIT_PASS // 消息审核通过
|
|
45
|
+
- MESSAGE_AUDIT_REJECT // 消息审核不通过
|
|
46
|
+
|
|
47
|
+
FORUMS_EVENT (1 << 28) // 论坛事件,仅 *私域* 机器人能够设置此 intents。
|
|
48
|
+
- FORUM_THREAD_CREATE // 当用户创建主题时
|
|
49
|
+
- FORUM_THREAD_UPDATE // 当用户更新主题时
|
|
50
|
+
- FORUM_THREAD_DELETE // 当用户删除主题时
|
|
51
|
+
- FORUM_POST_CREATE // 当用户创建帖子时
|
|
52
|
+
- FORUM_POST_DELETE // 当用户删除帖子时
|
|
53
|
+
- FORUM_REPLY_CREATE // 当用户回复评论时
|
|
54
|
+
- FORUM_REPLY_DELETE // 当用户回复评论时
|
|
55
|
+
- FORUM_PUBLISH_AUDIT_RESULT // 当用户发表审核通过时
|
|
56
|
+
|
|
57
|
+
AUDIO_ACTION (1 << 29)
|
|
58
|
+
- AUDIO_START // 音频开始播放时
|
|
59
|
+
- AUDIO_FINISH // 音频播放结束时
|
|
60
|
+
- AUDIO_ON_MIC // 上麦时
|
|
61
|
+
- AUDIO_OFF_MIC // 下麦时
|
|
62
|
+
|
|
63
|
+
PUBLIC_GUILD_MESSAGES (1 << 30) // 消息事件,此为公域的消息事件
|
|
64
|
+
- AT_MESSAGE_CREATE // 当收到@机器人的消息时
|
|
65
|
+
- PUBLIC_MESSAGE_DELETE // 当频道的消息被删除时
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* 订阅枚举
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* 订阅事件集合
|
|
72
|
+
*/
|
|
73
|
+
const intentsMap = {
|
|
74
|
+
GUILDS: 1 << 0,
|
|
75
|
+
GUILD_MEMBERS: 1 << 1,
|
|
76
|
+
GUILD_MESSAGES: 1 << 9,
|
|
77
|
+
GUILD_MESSAGE_REACTIONS: 1 << 10,
|
|
78
|
+
DIRECT_MESSAGE: 1 << 12,
|
|
79
|
+
GROUP_AND_C2C_EVENT: 1 << 25,
|
|
80
|
+
INTERACTION: 1 << 26,
|
|
81
|
+
MESSAGE_AUDIT: 1 << 27,
|
|
82
|
+
FORUMS_EVENT: 1 << 28,
|
|
83
|
+
AUDIO_ACTION: 1 << 29,
|
|
84
|
+
PUBLIC_GUILD_MESSAGES: 1 << 30
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
*
|
|
88
|
+
* @param intents
|
|
89
|
+
* @returns
|
|
90
|
+
*/
|
|
91
|
+
function getIntentsMask(intents) {
|
|
92
|
+
let intentsMask = 0;
|
|
93
|
+
for (const item of intents) {
|
|
94
|
+
const mask = intentsMap[item];
|
|
95
|
+
if (mask) {
|
|
96
|
+
intentsMask |= mask;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return intentsMask;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { getIntentsMask };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
type MessageType = 0 | 1 | 2 | 3 | 4 | 7;
|
|
2
|
+
type FileType = 1 | 2 | 3 | 4;
|
|
3
|
+
interface ButtonType {
|
|
4
|
+
id: string;
|
|
5
|
+
render_data: {
|
|
6
|
+
label: string;
|
|
7
|
+
visited_label: string;
|
|
8
|
+
style: number;
|
|
9
|
+
};
|
|
10
|
+
action: {
|
|
11
|
+
type: number;
|
|
12
|
+
permission: {
|
|
13
|
+
type: number;
|
|
14
|
+
};
|
|
15
|
+
reply?: boolean;
|
|
16
|
+
enter?: boolean;
|
|
17
|
+
unsupport_tips: string;
|
|
18
|
+
data: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
interface KeyboardType {
|
|
22
|
+
id?: string;
|
|
23
|
+
content?: {
|
|
24
|
+
rows: {
|
|
25
|
+
buttons: ButtonType[];
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
interface MarkdownType {
|
|
30
|
+
/** markdown 模版id,申请模版后获得 */
|
|
31
|
+
custom_template_id: string;
|
|
32
|
+
/** 原生 markdown 文本内容(内邀使用) */
|
|
33
|
+
content?: string;
|
|
34
|
+
/** 模版内变量与填充值的kv映射 */
|
|
35
|
+
params?: Array<{
|
|
36
|
+
key: string;
|
|
37
|
+
values: string[];
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
interface ApiRequestData {
|
|
41
|
+
content?: string;
|
|
42
|
+
msg_type: MessageType;
|
|
43
|
+
markdown?: MarkdownType;
|
|
44
|
+
keyboard?: KeyboardType;
|
|
45
|
+
media?: {
|
|
46
|
+
file_info: string;
|
|
47
|
+
};
|
|
48
|
+
ark?: any;
|
|
49
|
+
image?: any;
|
|
50
|
+
message_reference?: any;
|
|
51
|
+
event_id?: any;
|
|
52
|
+
msg_id?: string;
|
|
53
|
+
msg_seq?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type { ApiRequestData, ButtonType, FileType, KeyboardType, MarkdownType, MessageType };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ed25519 } from '@noble/curves/ed25519';
|
|
2
|
+
|
|
3
|
+
class WebhookAPI {
|
|
4
|
+
config;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 验证签名
|
|
10
|
+
* @param ts
|
|
11
|
+
* @param body
|
|
12
|
+
* @param sign
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
validSign(ts, body, sign) {
|
|
16
|
+
const { publicKey } = this.getKey();
|
|
17
|
+
const sig = Buffer.isBuffer(sign) ? sign : Buffer.from(sign, 'hex');
|
|
18
|
+
const httpBody = Buffer.from(body);
|
|
19
|
+
const msg = Buffer.from(ts + httpBody);
|
|
20
|
+
return ed25519.verify(sig, msg, publicKey);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 生成签名
|
|
24
|
+
* @param eventTs
|
|
25
|
+
* @param plainToken
|
|
26
|
+
* @returns
|
|
27
|
+
*/
|
|
28
|
+
getSign(eventTs, plainToken) {
|
|
29
|
+
const { privateKey } = this.getKey();
|
|
30
|
+
const msg = Buffer.from(eventTs + plainToken);
|
|
31
|
+
const signature = Buffer.from(ed25519.sign(msg, privateKey)).toString('hex');
|
|
32
|
+
return signature;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 获取 key
|
|
36
|
+
* @returns
|
|
37
|
+
*/
|
|
38
|
+
getKey() {
|
|
39
|
+
let seed = this.config.secret;
|
|
40
|
+
if (!seed)
|
|
41
|
+
throw new Error("secret not set, can't calc ed25519 key");
|
|
42
|
+
while (seed.length < 32)
|
|
43
|
+
seed = seed.repeat(2); // Ed25519 的种子大小是 32 字节
|
|
44
|
+
seed = seed.slice(0, 32); // 修剪到 32 字节
|
|
45
|
+
const privateKey = Buffer.from(seed);
|
|
46
|
+
return {
|
|
47
|
+
privateKey,
|
|
48
|
+
publicKey: ed25519.getPublicKey(privateKey)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { WebhookAPI };
|