napcat-sdk 0.1.0 → 0.1.1

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/src/napcat.ts DELETED
@@ -1,497 +0,0 @@
1
- import crypto from 'node:crypto'
2
- import mitt from 'mitt'
3
- import pkg from '../package.json' with { type: 'json' }
4
- import { segment } from './segment'
5
- import { CONSOLE_LOGGER, ABSTRACT_LOGGER } from './logger'
6
- import { NAPCAT_NOTICE_EVENT_MAP, NAPCAT_NOTICE_NOTIFY_MAP } from './onebot'
7
-
8
- import type { Emitter } from 'mitt'
9
- import type { Logger } from './logger'
10
- import type { EventMap, MiokiOptions, OptionalProps } from './types'
11
- import type {
12
- API,
13
- Friend,
14
- FriendWithInfo,
15
- Group,
16
- GroupMessageEvent,
17
- GroupWithInfo,
18
- NormalizedElementToSend,
19
- PrivateMessageEvent,
20
- Sendable,
21
- } from './onebot'
22
-
23
- export const name = pkg.name
24
- export const version = pkg.version
25
-
26
- export { CONSOLE_LOGGER, ABSTRACT_LOGGER, pkg as PKG }
27
-
28
- export const DEFAULT_NAPCAT_OPTIONS = {
29
- protocol: 'ws',
30
- host: 'localhost',
31
- port: 3333,
32
- logger: ABSTRACT_LOGGER,
33
- } satisfies Required<OptionalProps<MiokiOptions>>
34
-
35
- export class NapCat {
36
- /** WebSocket 实例 */
37
- #ws: WebSocket | null = null
38
- /** 事件发射器 */
39
- #event: Emitter<EventMap & Record<string | symbol, unknown>> = mitt()
40
- /** Echo 事件发射器 */
41
- #echoEvent: Emitter<Record<string, unknown>> = mitt()
42
-
43
- constructor(private readonly options: MiokiOptions) {}
44
-
45
- /** 配置项 */
46
- get #config(): Required<MiokiOptions> {
47
- return {
48
- protocol: this.options.protocol || DEFAULT_NAPCAT_OPTIONS.protocol,
49
- host: this.options.host || DEFAULT_NAPCAT_OPTIONS.host,
50
- port: this.options.port || DEFAULT_NAPCAT_OPTIONS.port,
51
- logger: this.options.logger || DEFAULT_NAPCAT_OPTIONS.logger,
52
- token: this.options.token,
53
- }
54
- }
55
-
56
- /** WebSocket 实例 */
57
- get ws(): WebSocket {
58
- if (!this.#ws) {
59
- this.logger.error('WebSocket is not connected.')
60
- throw new Error('WebSocket is not connected.')
61
- }
62
-
63
- return this.#ws
64
- }
65
-
66
- /** 日志记录器 */
67
- get logger(): Logger {
68
- return this.#config.logger
69
- }
70
-
71
- /** 消息段构建器 */
72
- get segment(): typeof segment {
73
- return segment
74
- }
75
-
76
- /** 生成唯一的 echo ID */
77
- #echoId() {
78
- return crypto.randomBytes(16).toString('hex')
79
- }
80
-
81
- /** 构建 WebSocket 连接地址 */
82
- #buildWsUrl(): string {
83
- return `${this.#config.protocol}://${this.#config.host}:${this.#config.port}?access_token=${this.#config.token}`
84
- }
85
-
86
- /** 包装回复消息 */
87
- #wrapReply(sendable: Sendable | Sendable[], message_id?: number, reply?: boolean): Sendable[] {
88
- const sendableList = typeof sendable === 'string' ? [sendable] : [sendable].flat()
89
-
90
- if (reply && message_id) {
91
- return [segment.reply(String(message_id)), ...sendableList]
92
- }
93
-
94
- return sendableList
95
- }
96
-
97
- /** 确保 WebSocket 已连接 */
98
- #ensureWsConnection(ws: WebSocket | null): asserts ws is WebSocket {
99
- if (!ws) {
100
- this.logger.error('WebSocket is not connected.')
101
- throw new Error('WebSocket is not connected.')
102
- }
103
-
104
- if (ws.readyState !== WebSocket.OPEN) {
105
- this.logger.error('WebSocket is not open.')
106
- throw new Error('WebSocket is not open.')
107
- }
108
- }
109
-
110
- /** 标准化可发送消息元素 */
111
- #normalizeSendable(msg: Sendable | Sendable[]): NormalizedElementToSend[] {
112
- return [msg].flat(2).map((item) => {
113
- if (typeof item === 'string') {
114
- return { type: 'text', data: { text: item } }
115
- }
116
- if (item.type === 'at') {
117
- return { type: 'at', data: { qq: String(item.qq) } }
118
- }
119
- const { type, ...data } = item
120
- return { type, data } as NormalizedElementToSend
121
- })
122
- }
123
-
124
- /** 等待服务器响应操作 */
125
- #waitForAction<T extends any>(echoId: string) {
126
- const eventName = `echo#${echoId}`
127
-
128
- return new Promise<T>((resolve, reject) => {
129
- const handle = (data: any) => {
130
- if (!data || data.echo !== echoId) return
131
-
132
- this.#echoEvent.off(eventName, handle)
133
-
134
- if (data.retcode === 0) {
135
- resolve(data.data as T)
136
- } else {
137
- reject(`Server Error: ${data.message}`)
138
- }
139
- }
140
-
141
- this.#echoEvent.on(eventName, handle)
142
- })
143
- }
144
-
145
- /** 构建群对象 */
146
- #buildGroup<T extends object>(group_id: number, group_name: string = '', extraInfo: T = {} as T): Group & T {
147
- return {
148
- ...extraInfo,
149
- group_id,
150
- group_name,
151
- doSign: () => this.api('set_group_sign', { group_id }),
152
- getInfo: () => this.api('get_group_info', { group_id }),
153
- getMemberList: async () => this.api('get_group_member_list', { group_id }),
154
- getMemberInfo: (user_id: number) => this.api('get_group_member_info', { group_id, user_id }),
155
- setTitle: (title: string) => this.api('set_group_special_title', { group_id, title }),
156
- setCard: (user_id: number, card: string) => this.api('set_group_card', { group_id, user_id, card }),
157
- addEssence: (message_id: string) => this.api('set_essence_msg', { message_id }),
158
- delEssence: (message_id: string) => this.api('delete_essence_msg', { message_id }),
159
- recall: (message_id: number) => this.api('delete_msg', { message_id }),
160
- banMember: (user_id: number, duration: number) => this.api('set_group_ban', { group_id, user_id, duration }),
161
- sendMsg: (sendable: Sendable | Sendable[]) => this.sendGroupMsg(group_id, sendable),
162
- }
163
- }
164
-
165
- /** 构建好友对象 */
166
- #buildFriend<T extends object>(user_id: number, nickname: string = '', extraInfo: T = {} as T): Friend & T {
167
- return {
168
- ...extraInfo,
169
- user_id,
170
- nickname,
171
- delete: (block?: boolean, both?: boolean) =>
172
- this.api('delete_friend', { user_id, temp_block: block, temp_both_del: both }),
173
- sendMsg: (sendable: Sendable | Sendable[]) => this.sendPrivateMsg(user_id, sendable),
174
- getInfo: () => this.api('get_stranger_info', { user_id }),
175
- }
176
- }
177
-
178
- /** 构建群消息事件 */
179
- #buildPrivateMessageEvent(event: PrivateMessageEvent) {
180
- return {
181
- ...event,
182
- message: (event.message || []).map((el: any) => ({ type: el.type, ...el.data })),
183
- friend: this.#buildFriend(event.user_id, event.sender?.nickname || ''),
184
- recall: () => this.api('delete_msg', { message_id: event.message_id }),
185
- reply: (sendable: Sendable | Sendable[], reply = false) =>
186
- this.sendPrivateMsg(event.user_id, this.#wrapReply(sendable, event.message_id, reply)),
187
- }
188
- }
189
-
190
- /** 构建群消息事件对象 */
191
- #buildGroupMessageEvent(event: GroupMessageEvent) {
192
- return {
193
- ...event,
194
- message: (event.message || []).map((el: any) => ({ type: el.type, ...el.data })),
195
- group: this.#buildGroup(event.group_id, event.group?.group_name || ''),
196
- recall: () => this.api('delete_msg', { message_id: event.message_id }),
197
- addReaction: (id: string) =>
198
- this.api('set_msg_emoji_like', { message_id: event.message_id, emoji_id: id, set: true }),
199
- delReaction: (id: string) =>
200
- this.api('set_msg_emoji_like', { message_id: event.message_id, emoji_id: id, set: false }),
201
- addEssence: () => this.api('set_essence_msg', { message_id: event.message_id }),
202
- delEssence: () => this.api('delete_essence_msg', { message_id: event.message_id }),
203
- reply: (sendable: Sendable | Sendable[], reply = false) =>
204
- this.sendGroupMsg(event.group_id, this.#wrapReply(sendable, event.message_id, reply)),
205
- }
206
- }
207
-
208
- /** 绑定内部事件处理器 */
209
- #bindInternalEvents(data: any) {
210
- if (data.echo) {
211
- this.#echoEvent.emit(`echo#${data.echo}`, data)
212
- return
213
- }
214
-
215
- if (data.post_type) {
216
- switch (data.post_type) {
217
- case 'meta_event': {
218
- this.logger.trace(`received meta_event: ${JSON.stringify(data)}`)
219
- this.#event.emit('meta_event', data)
220
-
221
- if (data.meta_event_type) {
222
- this.#event.emit(`meta_event.${data.meta_event_type}`, data)
223
- if (data.sub_type) {
224
- this.#event.emit(`meta_event.${data.meta_event_type}.${data.sub_type}`, data)
225
- }
226
- }
227
-
228
- break
229
- }
230
-
231
- case 'message': {
232
- if (data.message_type === 'private') {
233
- data = this.#buildPrivateMessageEvent(data)
234
- } else {
235
- data = this.#buildGroupMessageEvent(data)
236
- }
237
-
238
- this.#event.emit('message', data)
239
-
240
- switch (data.message_type) {
241
- case 'private': {
242
- this.logger.trace(`received private message: ${JSON.stringify(data)}`)
243
- this.#event.emit('message.private', data)
244
- this.#event.emit(`message.private.${data.sub_type}`, data)
245
-
246
- break
247
- }
248
-
249
- case 'group': {
250
- this.logger.trace(`received group message: ${JSON.stringify(data)}`)
251
- this.#event.emit('message.group', data)
252
- this.#event.emit(`message.group.${data.sub_type}`, data)
253
-
254
- break
255
- }
256
-
257
- default: {
258
- this.logger.debug(`received unknown message type: ${JSON.stringify(data)}`)
259
-
260
- break
261
- }
262
- }
263
-
264
- break
265
- }
266
-
267
- case 'message_sent': {
268
- this.logger.trace(`received message_sent: ${JSON.stringify(data)}`)
269
- this.#event.emit('message_sent', data)
270
-
271
- if (data.message_type) {
272
- this.#event.emit(`message_sent.${data.message_type}`, data)
273
- if (data.sub_type) {
274
- this.#event.emit(`message_sent.${data.message_type}.${data.sub_type}`, data)
275
- }
276
- }
277
-
278
- break
279
- }
280
-
281
- case 'notice': {
282
- this.logger.trace(`received notice: ${JSON.stringify(data)}`)
283
-
284
- if (!data.notice_type) {
285
- this.logger.debug(`received unknown notice type: ${JSON.stringify(data)}`)
286
- break
287
- }
288
-
289
- const isNotify = data.notice_type === 'notify'
290
- const isPoke = data.sub_type === 'poke'
291
- const isGroup = !!data.group_id
292
-
293
- const { notice_type, sub_type } = isNotify
294
- ? isPoke
295
- ? { notice_type: isGroup ? 'group' : 'friend', sub_type: 'poke' }
296
- : NAPCAT_NOTICE_NOTIFY_MAP[data.sub_type] || {}
297
- : NAPCAT_NOTICE_EVENT_MAP[data.notice_type] || {}
298
-
299
- data.original_notice_type = data.notice_type
300
- data.notice_type = notice_type || data.notice_type
301
-
302
- if (data.sub_type && data.sub_type !== sub_type) {
303
- data.action_type = data.sub_type
304
- }
305
-
306
- data.sub_type = sub_type || data.sub_type
307
-
308
- if (isGroup) {
309
- data.group = this.#buildGroup(data.group_id, data.group_name || '')
310
- } else {
311
- data.friend = this.#buildFriend(data.user_id, data.nickname || '')
312
- }
313
-
314
- this.#event.emit('notice', data)
315
-
316
- if (notice_type) {
317
- this.#event.emit(`notice.${notice_type}`, data)
318
- if (sub_type) {
319
- this.#event.emit(`notice.${notice_type}.${sub_type}`, data)
320
- }
321
- }
322
-
323
- break
324
- }
325
-
326
- case 'request': {
327
- this.logger.trace(`received request: ${JSON.stringify(data)}`)
328
-
329
- if (data.request_type === 'friend') {
330
- data.reject = () => this.api('set_friend_request', { flag: data.flag, approve: false })
331
- data.approve = () => this.api('set_friend_request', { flag: data.flag, approve: true })
332
- }
333
-
334
- if (data.request_type === 'group') {
335
- data.reject = (reason?: string) =>
336
- this.api('set_group_add_request', { flag: data.flag, approve: false, reason })
337
- data.approve = () => this.api('set_group_add_request', { flag: data.flag, approve: true })
338
- }
339
-
340
- this.#event.emit('request', data)
341
-
342
- if (data.request_type) {
343
- this.#event.emit(`request.${data.request_type}`, data)
344
- if (data.sub_type) {
345
- this.#event.emit(`request.${data.request_type}.${data.sub_type}`, data)
346
- }
347
- }
348
-
349
- break
350
- }
351
-
352
- default: {
353
- this.logger.debug(`received: ${JSON.stringify(data)}`)
354
- this.#event.emit(data.post_type, data)
355
- return
356
- }
357
- }
358
-
359
- return
360
- }
361
- }
362
-
363
- /** 获取一个群的信息,可以用于发送群消息等操作 */
364
- async pickGroup(group_id: number): Promise<GroupWithInfo> {
365
- const groupInfo = await this.api<ReturnType<Group['getInfo']>>('get_group_info', { group_id })
366
- return this.#buildGroup(group_id, groupInfo.group_name, groupInfo)
367
- }
368
-
369
- /** 获取一个好友的信息,可以用于发送私聊消息等操作 */
370
- async pickFriend(user_id: number): Promise<FriendWithInfo> {
371
- const friendInfo = await this.api<ReturnType<Friend['getInfo']>>('get_stranger_info', { user_id })
372
- return this.#buildFriend(user_id, friendInfo.nickname, friendInfo)
373
- }
374
-
375
- /**
376
- * 注册一次性事件监听器
377
- */
378
- once<T extends keyof EventMap>(type: T, handler: (event: EventMap[NoInfer<T>]) => void) {
379
- const onceHandler = (event: EventMap[NoInfer<T>]) => {
380
- handler(event)
381
- this.#event.off(type, onceHandler)
382
- }
383
-
384
- this.logger.debug(`registering once: ${String(type)}`)
385
- this.#event.on(type, onceHandler)
386
- }
387
-
388
- /**
389
- * 注册事件监听器,支持主类型或者点分子类型
390
- *
391
- * 如: `notice`、`message.private`、`request.group.invite` 等
392
- *
393
- * 如果需要移除监听器,请调用 `off` 方法
394
- */
395
- on<T extends keyof EventMap>(type: T, handler: (event: EventMap[NoInfer<T>]) => void) {
396
- this.logger.debug(`registering: ${String(type)}`)
397
- this.#event.on(type, handler)
398
- }
399
-
400
- /**
401
- * 移除事件监听器
402
- */
403
- off<T extends keyof EventMap>(type: T, handler: (event: EventMap[NoInfer<T>]) => void) {
404
- this.logger.debug(`unregistering: ${String(type)}`)
405
- this.#event.off(type, handler)
406
- }
407
-
408
- api<T extends any>(action: API | (string & {}), params: Record<string, any> = {}): Promise<T> {
409
- this.#ensureWsConnection(this.#ws)
410
- this.logger.debug(`calling api action: ${action} with params: ${JSON.stringify(params)}`)
411
- const echo = this.#echoId()
412
- this.#ws.send(JSON.stringify({ echo, action, params }))
413
- return this.#waitForAction<T>(echo)
414
- }
415
-
416
- /**
417
- * 发送私聊消息
418
- */
419
- sendPrivateMsg(user_id: number, sendable: Sendable | Sendable[]) {
420
- return this.api<{ message_id: number }>('send_private_msg', {
421
- user_id,
422
- message: this.#normalizeSendable(sendable),
423
- })
424
- }
425
-
426
- /**
427
- * 发送群消息
428
- */
429
- sendGroupMsg(group_id: number, sendable: Sendable | Sendable[]) {
430
- return this.api<{ message_id: number }>('send_group_msg', {
431
- group_id,
432
- message: this.#normalizeSendable(sendable),
433
- })
434
- }
435
-
436
- /** 启动 NapCat SDK 实例,建立 WebSocket 连接 */
437
- async bootstrap() {
438
- const { logger: _, ...config } = this.#config
439
-
440
- this.logger.info(`bootstrap with config: ${JSON.stringify(config)}`)
441
-
442
- return new Promise<void>((resolve, reject) => {
443
- const ws = new WebSocket(this.#buildWsUrl())
444
-
445
- ws.onmessage = (event) => {
446
- const data = (() => {
447
- try {
448
- return JSON.parse(event.data)
449
- } catch {
450
- return null
451
- }
452
- })() as any
453
-
454
- if (!data) {
455
- this.logger.warn(`received non-json message: ${event.data}`)
456
- return
457
- }
458
-
459
- this.#event.emit('ws.message', data)
460
- this.#bindInternalEvents(data)
461
- }
462
-
463
- ws.onclose = () => {
464
- this.logger.info('closed')
465
- this.#event.emit('ws.close')
466
- }
467
-
468
- ws.onerror = (error) => {
469
- this.logger.error(`error: ${error}`)
470
- this.#event.emit('ws.error', error)
471
- reject(error)
472
- }
473
-
474
- ws.onopen = () => {
475
- this.logger.info('connected')
476
- this.#event.emit('ws.open')
477
- resolve()
478
- }
479
-
480
- this.#ws = ws
481
-
482
- this.logger.trace(`WebSocket instance created: ${this.#ws}`)
483
- })
484
- }
485
-
486
- /** 销毁 NapCat SDK 实例,关闭 WebSocket 连接 */
487
- async destroy() {
488
- if (this.#ws) {
489
- this.logger.info('destroying NapCat SDK instance...')
490
- this.#ws.close()
491
- this.#ws = null
492
- this.logger.info('NapCat SDK instance destroyed.')
493
- } else {
494
- this.logger.warn('NapCat SDK instance is not initialized.')
495
- }
496
- }
497
- }