napcat-sdk 0.1.0 → 0.1.2

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 ADDED
@@ -0,0 +1,70 @@
1
+ # NapCat SDK for TypeScript
2
+
3
+ ## Getting Started
4
+
5
+ The NapCat SDK for TypeScript allows developers to easily integrate NapCat's functionalities into their TypeScript applications. This SDK provides a set of tools and utilities to interact with NapCat services seamlessly.
6
+
7
+ ## Installation
8
+
9
+ You can install the NapCat SDK via npm. Run the following command in your terminal:
10
+
11
+ ```bash
12
+ pnpm install napcat-sdk
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ To connect to NapCat, you need to create an instance of the NapCat client. Here's a simple example:
18
+
19
+ ```typescript
20
+ import { NapCat, segment } from 'napcat-sdk'
21
+
22
+ // 1. Create a new NapCat client instance
23
+ const napcat = new NapCat({
24
+ // protocol: 'ws', // Optional: specify the protocol (default is 'ws')
25
+ // host: 'localhost', // Optional: specify a custom host
26
+ // port: 3333, // Optional: specify a custom port
27
+ token: 'here-your-auth-token', // Required: your authentication token
28
+ })
29
+
30
+ // 2. Subscribe to events
31
+ napcat.on('message', (event) => {
32
+ // replay is a method to send a message quickly, optional with reply mark
33
+ event.reply('Hello from NapCat SDK!', true) // true is for reply mark
34
+
35
+ // you can call all the NapCat api through `napcat.api()` method
36
+ const { value } = await napcat.api<{ value: unknown }>('awesome-function')
37
+ })
38
+
39
+ // you can also listen to specific message sub-types
40
+ napcat.on('message.group', async (event) => {
41
+ // all methods of a message event are available
42
+ await event.addEssence(event.message_id)
43
+ await event.recall()
44
+
45
+ // You can also interact with group instance to do some operations
46
+ await event.group.setTitle(114514, 'Special Title')
47
+
48
+ // message to send is allowed to be an array of segments
49
+ await event.reply(['Hi! ', napcat.segment.face(66)])
50
+
51
+ // or just use napcat to send messages
52
+ await napcat.sendGroupMsg(event.group_id, 'Hello Group!')
53
+ })
54
+
55
+ // and more events...
56
+ napcat.on('notice', (event) => {})
57
+ napcat.on('notice.group', (event) => {})
58
+ napcat.on('request', (event) => {})
59
+ napcat.on('request.group.invite', (event) => {
60
+ // approve the group invite request, or event.reject() to reject
61
+ event.approve()
62
+ })
63
+
64
+ // close the connection when needed
65
+ napcat.close()
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT License © 2025-PRESENT Viki
package/.node-version DELETED
@@ -1 +0,0 @@
1
- 24.11.1
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from './napcat'
2
- export * from './logger'
3
- export * from './types'
4
- export * from './segment'
package/src/logger.ts DELETED
@@ -1,27 +0,0 @@
1
- export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace'
2
-
3
- export type Logger = Record<LogLevel, (...args: unknown[]) => void>
4
-
5
- const LOG_LEVELS: Record<LogLevel, number> = {
6
- fatal: 0,
7
- error: 1,
8
- warn: 2,
9
- info: 3,
10
- debug: 4,
11
- trace: 5,
12
- }
13
-
14
- const LOG_LEVEL_KEYS = Object.keys(LOG_LEVELS) as LogLevel[]
15
-
16
- const noop = () => {}
17
-
18
- export const ABSTRACT_LOGGER: Logger = Object.fromEntries(LOG_LEVEL_KEYS.map((level) => [level, noop])) as Logger
19
-
20
- export const CONSOLE_LOGGER: Logger = Object.fromEntries(
21
- LOG_LEVEL_KEYS.map((level) => [
22
- level,
23
- (...args: unknown[]) => {
24
- console[level === 'fatal' ? 'error' : level === 'trace' ? 'debug' : level](...args)
25
- },
26
- ]),
27
- ) as Logger
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
- }