@zhin.js/adapter-satori 1.1.26 → 1.1.27
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/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/adapters/satori/index.js +5 -0
- package/adapters/satori/index.ts +5 -0
- package/lib/endpoint.d.ts +2 -1
- package/lib/endpoint.js +191 -64
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/protocol.d.ts +121 -9
- package/lib/protocol.js +561 -6
- package/package.json +2 -2
- package/src/endpoint.ts +218 -61
- package/src/index.ts +16 -0
- package/src/protocol.ts +671 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @zhin.js/adapter-satori
|
|
2
2
|
|
|
3
|
+
## 1.1.27
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 11306b0: 对齐 Satori 出站元素和被动回复的 `referrer`。
|
|
8
|
+
|
|
9
|
+
**破坏性变更:** `at` / `mention` 不再编成 `@name`,图片、音频、视频和文件不再编成 `[image:src]` 这类纯文本,改为标准元素 `<at>`、`<img>`、`<audio>`、`<video>`、`<file>`,并支持 `<emoji>`。入站 `content` 仍是协议原文,这些元素会另外解成段。
|
|
10
|
+
|
|
11
|
+
收到带 `referrer` 的事件时,同一次处理里的 `message.create` 会原样带回该对象;之后的主动发送不带。`guild-emoji-added` / `guild-emoji-updated` / `guild-emoji-deleted` 映射为 `notice.group.emoji_added` / `emoji_updated` / `emoji_deleted`。
|
|
12
|
+
|
|
13
|
+
`@imhelper/satori-v1` 还没有这些类型(上游 `feat/satori-1.3-alignment` 未发布)。适配器使用本地类型,上游发版后可以换成官方类型。
|
|
14
|
+
|
|
15
|
+
- 54b034f: 对齐 Satori 的 Friend、reaction 和群成员角色。
|
|
16
|
+
|
|
17
|
+
**破坏性变更:** `listGroupMembers` 返回的 `roles` 改为 `GuildRole[]`(`{ id, name? }`)。旧 SDK 若仍下发 `string[]`,适配器会收成 `{ id }`。
|
|
18
|
+
|
|
19
|
+
新增 `friend.list` / `friend.delete` / `friend.approve`(管理端口的 `listFriends`、`deleteFriend`、`approveRequest`、`rejectRequest`)。`friend-request` 进入 `request.receive`,名称是 `request.friend.add`。表态改为 `reaction.create` / `reaction.delete`,参数名是 `emoji_id`。
|
|
20
|
+
|
|
21
|
+
`@imhelper/satori-v1` 1.0.10 仍把好友列表当成 User,reaction 参数仍叫 `emoji`。这些类型是本地声明。上游 `feat/satori-1.3-alignment` 发版后可以换成官方类型。
|
|
22
|
+
|
|
23
|
+
- 79395bb: 群成员列表改为请求 `POST /v1/guild.member.list`。原先资源名写成 `guild-member`,实际打到 `/v1/guild-member.list`,与 Satori 文档不一致。
|
|
24
|
+
|
|
3
25
|
## 1.1.26
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ pnpm add @zhin.js/adapter-satori
|
|
|
29
29
|
|
|
30
30
|
入站:`endpoint[endpointSpec].events.emit('message.receive', { conversation, message, content, sender, metadata })`(`conversation` 为 ConversationRef:DIRECT 频道 → kind `private`,其余 → kind `group`、所属 guild 进 `parent`)
|
|
31
31
|
|
|
32
|
-
出站:`send({ conversation, payload })` → Satori `message.create`(`channel_id = conversation.id
|
|
32
|
+
出站:`send({ conversation, payload })` → Satori `message.create`(`channel_id = conversation.id`)。同一次入站处理结束前的被动回复会原样带回 `referrer`。段里的 mention 编成 `<at>`,媒体编成 `<img>` / `<audio>` / `<video>` / `<file>`,`emoji` 和规范 `face` 编成 `<emoji>`。
|
|
33
33
|
|
|
34
34
|
入站 `metadata.mentioned`:消息 content 中 `<at id="…"/>` 元素的 id 等于登录 selfId(配置的 `userId`)时置 `true`。
|
|
35
35
|
|
package/adapters/satori/index.js
CHANGED
|
@@ -17,9 +17,14 @@ export default defineAdapter({
|
|
|
17
17
|
interactive: 'text',
|
|
18
18
|
},
|
|
19
19
|
recall: {},
|
|
20
|
+
reaction: {},
|
|
20
21
|
},
|
|
21
22
|
actions: {
|
|
22
23
|
recall: { scopes: ['private', 'group', 'channel'] },
|
|
24
|
+
reaction: {
|
|
25
|
+
scopes: ['private', 'group', 'channel'],
|
|
26
|
+
params: { cancellable: true },
|
|
27
|
+
},
|
|
23
28
|
},
|
|
24
29
|
delivery: { send: true, spool: true },
|
|
25
30
|
},
|
package/adapters/satori/index.ts
CHANGED
|
@@ -27,9 +27,14 @@ export default defineAdapter<SatoriEndpointConfig>({
|
|
|
27
27
|
interactive: 'text',
|
|
28
28
|
},
|
|
29
29
|
recall: {},
|
|
30
|
+
reaction: {},
|
|
30
31
|
},
|
|
31
32
|
actions: {
|
|
32
33
|
recall: { scopes: ['private', 'group', 'channel'] },
|
|
34
|
+
reaction: {
|
|
35
|
+
scopes: ['private', 'group', 'channel'],
|
|
36
|
+
params: { cancellable: true },
|
|
37
|
+
},
|
|
33
38
|
},
|
|
34
39
|
delivery: { send: true, spool: true },
|
|
35
40
|
},
|
package/lib/endpoint.d.ts
CHANGED
|
@@ -41,7 +41,8 @@ export declare class SatoriWebhookEndpoint extends SatoriClient implements Endpo
|
|
|
41
41
|
export type SatoriManagementApi = SatoriActionCaller;
|
|
42
42
|
/**
|
|
43
43
|
* Satori endpoint 的 EndpointManagement 语义端口(ws / webhook 共用),
|
|
44
|
-
* 数据走协议 API:guild.list / channel.list / guild
|
|
44
|
+
* 数据走协议 API:guild.list / channel.list / guild.member.list。
|
|
45
|
+
* @see https://satori.chat/zh-CN/resources/member.html
|
|
45
46
|
*/
|
|
46
47
|
export declare function createSatoriEndpointManagement(api: SatoriManagementApi): EndpointManagement;
|
|
47
48
|
export type { CreateSatoriWebSocket, SatoriWsSocket } from './ws.js';
|
package/lib/endpoint.js
CHANGED
|
@@ -5,7 +5,7 @@ import { acceptHttpIngress } from 'imhelper';
|
|
|
5
5
|
*/
|
|
6
6
|
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, } from 'zhin.js/adapter';
|
|
7
7
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
8
|
-
import { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatMessageId, formatSatoriOutbound, isMessageEvent, isSelfMentioned, parseMessageRef, resolveInboundSender, satoriInboundConversation, } from './protocol.js';
|
|
8
|
+
import { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatMessageId, formatSatoriOutbound, inboundSatoriSegments, isMessageEvent, mapSatoriFriendRequest, readSatoriFriend, satoriFriendApproveParams, satoriReactionParams, withSatoriGuildRoles, isSelfMentioned, mapGuildEmojiNotice, parseMessageRef, resolveInboundSender, satoriInboundConversation, satoriMessageCreateParams, withSatoriReferrer, } from './protocol.js';
|
|
9
9
|
import { registerSatoriWebhookRoutes } from './webhook.js';
|
|
10
10
|
import { satoriClientOptions, createSatoriActionCaller, forwardSatoriClientEvents, SatoriClient } from './client.js';
|
|
11
11
|
import { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
|
@@ -21,6 +21,8 @@ export class SatoriWsEndpoint extends SatoriClient {
|
|
|
21
21
|
#call;
|
|
22
22
|
#control = Object.freeze({
|
|
23
23
|
recall: request => this.#recall(request),
|
|
24
|
+
addReaction: request => this.#addReaction(request),
|
|
25
|
+
removeReaction: request => this.#removeReaction(request),
|
|
24
26
|
});
|
|
25
27
|
constructor(options) {
|
|
26
28
|
const call = createSatoriActionCaller({ config: options.config, request: options.callApi ?? callSatoriApi, signal: options.signal });
|
|
@@ -90,10 +92,7 @@ export class SatoriWsEndpoint extends SatoriClient {
|
|
|
90
92
|
}
|
|
91
93
|
async #send({ conversation, payload, signal }) {
|
|
92
94
|
const content = formatSatoriOutbound(payload);
|
|
93
|
-
const result = await this.#call({ resource: 'message', method: 'create', signal, params:
|
|
94
|
-
channel_id: conversation.id,
|
|
95
|
-
content,
|
|
96
|
-
} });
|
|
95
|
+
const result = await this.#call({ resource: 'message', method: 'create', signal, params: satoriMessageCreateParams(conversation.id, content) });
|
|
97
96
|
const msgId = extractCreatedMessageId(result);
|
|
98
97
|
this.#logger.debug(formatCompact({
|
|
99
98
|
op: 'satori_send',
|
|
@@ -106,35 +105,36 @@ export class SatoriWsEndpoint extends SatoriClient {
|
|
|
106
105
|
#admitRaw(body) {
|
|
107
106
|
if (!matchesSatoriLogin(this.#options.config, body.login))
|
|
108
107
|
return;
|
|
109
|
-
if (
|
|
108
|
+
if (emitSatoriFriendRequest(body, {
|
|
109
|
+
endpointId: this.#options.config.id,
|
|
110
|
+
call: this.#call,
|
|
111
|
+
emit: request => {
|
|
112
|
+
void this[endpointSpec].events.emit('request.receive', request).catch(error => {
|
|
113
|
+
this.#warnPlatformEvent('request.receive', error);
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
}))
|
|
110
117
|
return;
|
|
111
|
-
const
|
|
112
|
-
const content = formatInboundContent(body);
|
|
113
|
-
const sender = resolveInboundSender(body);
|
|
114
|
-
const selfId = this.#options.config.userId;
|
|
115
|
-
const mentioned = isSelfMentioned(body, selfId);
|
|
116
|
-
void this[endpointSpec].events.message({
|
|
117
|
-
event: body,
|
|
118
|
-
...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
|
|
119
|
-
conversation,
|
|
120
|
-
message: { conversation, id: body.message.id },
|
|
121
|
-
content,
|
|
122
|
-
sender,
|
|
118
|
+
const deliver = () => deliverSatoriInbound(body, {
|
|
123
119
|
endpointId: this.#options.config.id,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
120
|
+
endpointKey: String(this.#options.id),
|
|
121
|
+
identity: this[endpointSpec].identity,
|
|
122
|
+
selfId: this.#options.config.userId,
|
|
123
|
+
platform: this.#options.config.platform,
|
|
124
|
+
events: this[endpointSpec].events,
|
|
125
|
+
warnReceive: (channel, error) => {
|
|
126
|
+
this.#logger.warn(formatCompact({
|
|
127
|
+
op: 'satori_gateway_receive_failed',
|
|
128
|
+
channel,
|
|
129
|
+
error: error instanceof Error ? error.message : String(error),
|
|
130
|
+
}));
|
|
131
|
+
},
|
|
132
|
+
warnEvent: (event, error) => this.#warnPlatformEvent(event, error),
|
|
137
133
|
});
|
|
134
|
+
if (body.referrer !== undefined)
|
|
135
|
+
withSatoriReferrer(body.referrer, deliver);
|
|
136
|
+
else
|
|
137
|
+
deliver();
|
|
138
138
|
}
|
|
139
139
|
async #recall({ message, signal }) {
|
|
140
140
|
const { channelId, messageId } = parseMessageRef(message.id);
|
|
@@ -143,6 +143,12 @@ export class SatoriWsEndpoint extends SatoriClient {
|
|
|
143
143
|
message_id: messageId,
|
|
144
144
|
} });
|
|
145
145
|
}
|
|
146
|
+
#addReaction(request) {
|
|
147
|
+
return addSatoriReaction(this.#call, request);
|
|
148
|
+
}
|
|
149
|
+
#removeReaction(request) {
|
|
150
|
+
return removeSatoriReaction(this.#call, request);
|
|
151
|
+
}
|
|
146
152
|
#warnPlatformEvent(name, error) {
|
|
147
153
|
this.#logger.warn(formatCompact({
|
|
148
154
|
op: 'satori_platform_event_failed',
|
|
@@ -294,6 +300,8 @@ export class SatoriWebhookEndpoint extends SatoriClient {
|
|
|
294
300
|
#call;
|
|
295
301
|
#control = Object.freeze({
|
|
296
302
|
recall: request => this.#recall(request),
|
|
303
|
+
addReaction: request => this.#addReaction(request),
|
|
304
|
+
removeReaction: request => this.#removeReaction(request),
|
|
297
305
|
});
|
|
298
306
|
constructor(options) {
|
|
299
307
|
const call = createSatoriActionCaller({ config: options.config, request: options.callApi ?? callSatoriApi, signal: options.signal });
|
|
@@ -363,10 +371,7 @@ export class SatoriWebhookEndpoint extends SatoriClient {
|
|
|
363
371
|
}
|
|
364
372
|
async #send({ conversation, payload, signal }) {
|
|
365
373
|
const content = formatSatoriOutbound(payload);
|
|
366
|
-
const result = await this.#call({ resource: 'message', method: 'create', signal, params:
|
|
367
|
-
channel_id: conversation.id,
|
|
368
|
-
content,
|
|
369
|
-
} });
|
|
374
|
+
const result = await this.#call({ resource: 'message', method: 'create', signal, params: satoriMessageCreateParams(conversation.id, content) });
|
|
370
375
|
const msgId = extractCreatedMessageId(result);
|
|
371
376
|
this.#logger.debug(formatCompact({
|
|
372
377
|
op: 'satori_send',
|
|
@@ -379,35 +384,36 @@ export class SatoriWebhookEndpoint extends SatoriClient {
|
|
|
379
384
|
#admitRaw(body) {
|
|
380
385
|
if (!matchesSatoriLogin(this.#options.config, body.login))
|
|
381
386
|
return;
|
|
382
|
-
if (
|
|
387
|
+
if (emitSatoriFriendRequest(body, {
|
|
388
|
+
endpointId: this.#options.config.id,
|
|
389
|
+
call: this.#call,
|
|
390
|
+
emit: request => {
|
|
391
|
+
void this[endpointSpec].events.emit('request.receive', request).catch(error => {
|
|
392
|
+
this.#warnPlatformEvent('request.receive', error);
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
}))
|
|
383
396
|
return;
|
|
384
|
-
const
|
|
385
|
-
const content = formatInboundContent(body);
|
|
386
|
-
const sender = resolveInboundSender(body);
|
|
387
|
-
const selfId = this.#options.config.userId;
|
|
388
|
-
const mentioned = isSelfMentioned(body, selfId);
|
|
389
|
-
void this[endpointSpec].events.message({
|
|
390
|
-
event: body,
|
|
391
|
-
...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
|
|
392
|
-
conversation,
|
|
393
|
-
message: { conversation, id: body.message.id },
|
|
394
|
-
content,
|
|
395
|
-
sender,
|
|
397
|
+
const deliver = () => deliverSatoriInbound(body, {
|
|
396
398
|
endpointId: this.#options.config.id,
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
399
|
+
endpointKey: String(this.#options.id),
|
|
400
|
+
identity: this[endpointSpec].identity,
|
|
401
|
+
selfId: this.#options.config.userId,
|
|
402
|
+
platform: this.#options.config.platform,
|
|
403
|
+
events: this[endpointSpec].events,
|
|
404
|
+
warnReceive: (channel, error) => {
|
|
405
|
+
this.#logger.warn(formatCompact({
|
|
406
|
+
op: 'satori_gateway_receive_failed',
|
|
407
|
+
channel,
|
|
408
|
+
error: error instanceof Error ? error.message : String(error),
|
|
409
|
+
}));
|
|
410
|
+
},
|
|
411
|
+
warnEvent: (event, error) => this.#warnPlatformEvent(event, error),
|
|
410
412
|
});
|
|
413
|
+
if (body.referrer !== undefined)
|
|
414
|
+
withSatoriReferrer(body.referrer, deliver);
|
|
415
|
+
else
|
|
416
|
+
deliver();
|
|
411
417
|
}
|
|
412
418
|
async #recall({ message, signal }) {
|
|
413
419
|
const { channelId, messageId } = parseMessageRef(message.id);
|
|
@@ -416,6 +422,12 @@ export class SatoriWebhookEndpoint extends SatoriClient {
|
|
|
416
422
|
message_id: messageId,
|
|
417
423
|
} });
|
|
418
424
|
}
|
|
425
|
+
#addReaction(request) {
|
|
426
|
+
return addSatoriReaction(this.#call, request);
|
|
427
|
+
}
|
|
428
|
+
#removeReaction(request) {
|
|
429
|
+
return removeSatoriReaction(this.#call, request);
|
|
430
|
+
}
|
|
419
431
|
#warnPlatformEvent(name, error) {
|
|
420
432
|
this.#logger.warn(formatCompact({
|
|
421
433
|
op: 'satori_platform_event_failed',
|
|
@@ -430,6 +442,38 @@ export class SatoriWebhookEndpoint extends SatoriClient {
|
|
|
430
442
|
this.ingest(body);
|
|
431
443
|
}
|
|
432
444
|
}
|
|
445
|
+
function deliverSatoriInbound(body, input) {
|
|
446
|
+
const notice = mapGuildEmojiNotice(body, input.endpointId);
|
|
447
|
+
if (notice) {
|
|
448
|
+
return input.events.emit('notice.receive', notice).catch(error => input.warnEvent('notice.receive', error));
|
|
449
|
+
}
|
|
450
|
+
if (!isMessageEvent(body))
|
|
451
|
+
return Promise.resolve();
|
|
452
|
+
const conversation = { ...satoriInboundConversation(input.endpointKey, body), endpoint: input.identity };
|
|
453
|
+
const content = formatInboundContent(body);
|
|
454
|
+
const segments = typeof body.message.content === 'string'
|
|
455
|
+
? inboundSatoriSegments(body.message.content)
|
|
456
|
+
: undefined;
|
|
457
|
+
const sender = resolveInboundSender(body);
|
|
458
|
+
const mentioned = isSelfMentioned(body, input.selfId);
|
|
459
|
+
return input.events.message({
|
|
460
|
+
event: body,
|
|
461
|
+
...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
|
|
462
|
+
conversation,
|
|
463
|
+
message: { conversation, id: body.message.id },
|
|
464
|
+
content,
|
|
465
|
+
...(segments ? { segments } : {}),
|
|
466
|
+
sender,
|
|
467
|
+
endpointId: input.endpointId,
|
|
468
|
+
...(mentioned ? { mentioned: true } : {}),
|
|
469
|
+
metadata: Object.freeze({
|
|
470
|
+
type: body.type,
|
|
471
|
+
channelType: isPrivateChannelType(body) ? 'private' : 'group',
|
|
472
|
+
sn: body.sn,
|
|
473
|
+
platform: input.platform,
|
|
474
|
+
}),
|
|
475
|
+
}).catch(error => input.warnReceive(conversation.id, error));
|
|
476
|
+
}
|
|
433
477
|
function isPrivateChannelType(body) {
|
|
434
478
|
const channel = body.channel ?? body.message?.channel;
|
|
435
479
|
return channel?.type === 1;
|
|
@@ -458,7 +502,8 @@ function asRecord(value) {
|
|
|
458
502
|
}
|
|
459
503
|
/**
|
|
460
504
|
* Satori endpoint 的 EndpointManagement 语义端口(ws / webhook 共用),
|
|
461
|
-
* 数据走协议 API:guild.list / channel.list / guild
|
|
505
|
+
* 数据走协议 API:guild.list / channel.list / guild.member.list。
|
|
506
|
+
* @see https://satori.chat/zh-CN/resources/member.html
|
|
462
507
|
*/
|
|
463
508
|
export function createSatoriEndpointManagement(api) {
|
|
464
509
|
return Object.freeze({
|
|
@@ -500,9 +545,91 @@ export function createSatoriEndpointManagement(api) {
|
|
|
500
545
|
return channels;
|
|
501
546
|
},
|
|
502
547
|
async listGroupMembers({ groupId, signal }) {
|
|
503
|
-
// 平台形状(GuildMember[]
|
|
504
|
-
|
|
548
|
+
// 平台形状(GuildMember[])原样返回,再规范化 1.3 的 GuildRole 角色
|
|
549
|
+
const members = await listSatoriPages(api, { resource: 'guild.member', params: { guild_id: groupId }, signal });
|
|
550
|
+
return members.map(member => withSatoriGuildRoles(member));
|
|
551
|
+
},
|
|
552
|
+
async listFriends(request = {}) {
|
|
553
|
+
const friends = [];
|
|
554
|
+
for (const value of await listSatoriPages(api, { resource: 'friend', signal: request.signal })) {
|
|
555
|
+
const friend = readSatoriFriend(value);
|
|
556
|
+
if (!friend?.user?.id)
|
|
557
|
+
continue;
|
|
558
|
+
const nick = friend.nick ?? '';
|
|
559
|
+
const name = friend.user.name ?? '';
|
|
560
|
+
friends.push({
|
|
561
|
+
user_id: friend.user.id,
|
|
562
|
+
nickname: name || nick || friend.user.id,
|
|
563
|
+
remark: nick,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
return friends;
|
|
567
|
+
},
|
|
568
|
+
async deleteFriend({ userId, signal }) {
|
|
569
|
+
await api({ resource: 'friend', method: 'delete', params: { user_id: userId }, signal });
|
|
505
570
|
},
|
|
571
|
+
async approveRequest({ requestId, remark, signal }) {
|
|
572
|
+
await api({
|
|
573
|
+
resource: 'friend',
|
|
574
|
+
method: 'approve',
|
|
575
|
+
signal,
|
|
576
|
+
params: satoriFriendApproveParams(requestId, true, remark),
|
|
577
|
+
});
|
|
578
|
+
},
|
|
579
|
+
async rejectRequest({ requestId, reason, signal }) {
|
|
580
|
+
await api({
|
|
581
|
+
resource: 'friend',
|
|
582
|
+
method: 'approve',
|
|
583
|
+
signal,
|
|
584
|
+
params: satoriFriendApproveParams(requestId, false, reason),
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function emitSatoriFriendRequest(body, input) {
|
|
590
|
+
if (body.type !== 'friend-request')
|
|
591
|
+
return false;
|
|
592
|
+
const request = mapSatoriFriendRequest(body, input.endpointId, {
|
|
593
|
+
approve: (messageId, comment) => input.call({
|
|
594
|
+
resource: 'friend',
|
|
595
|
+
method: 'approve',
|
|
596
|
+
params: satoriFriendApproveParams(messageId, true, comment),
|
|
597
|
+
}).then(() => undefined),
|
|
598
|
+
reject: (messageId, reason) => input.call({
|
|
599
|
+
resource: 'friend',
|
|
600
|
+
method: 'approve',
|
|
601
|
+
params: satoriFriendApproveParams(messageId, false, reason),
|
|
602
|
+
}).then(() => undefined),
|
|
603
|
+
});
|
|
604
|
+
if (request)
|
|
605
|
+
input.emit(request);
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
async function addSatoriReaction(call, request) {
|
|
609
|
+
const { channelId, messageId } = parseMessageRef(request.message.id);
|
|
610
|
+
await call({
|
|
611
|
+
resource: 'reaction',
|
|
612
|
+
method: 'create',
|
|
613
|
+
signal: request.signal,
|
|
614
|
+
params: satoriReactionParams('create', {
|
|
615
|
+
channelId: channelId || request.message.conversation.id,
|
|
616
|
+
messageId,
|
|
617
|
+
emojiId: request.emoji,
|
|
618
|
+
}),
|
|
619
|
+
});
|
|
620
|
+
return request.emoji;
|
|
621
|
+
}
|
|
622
|
+
async function removeSatoriReaction(call, request) {
|
|
623
|
+
const { channelId, messageId } = parseMessageRef(request.message.id);
|
|
624
|
+
await call({
|
|
625
|
+
resource: 'reaction',
|
|
626
|
+
method: 'delete',
|
|
627
|
+
signal: request.signal,
|
|
628
|
+
params: satoriReactionParams('delete', {
|
|
629
|
+
channelId: channelId || request.message.conversation.id,
|
|
630
|
+
messageId,
|
|
631
|
+
emojiId: request.reactionId,
|
|
632
|
+
}),
|
|
506
633
|
});
|
|
507
634
|
}
|
|
508
635
|
/** A transport connection may multiplex logins; this Endpoint admits only its selected account. */
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig, type SatoriEndpointConfig, type SatoriApiOptions, type SatoriApiRequest, type SatoriChannel, type SatoriEventBody, type SatoriLogin, type SatoriMessage, type SatoriSignal, type SatoriUser, type SatoriWireSegment, } from './protocol.js';
|
|
1
|
+
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, decodeSatoriContent, formatInboundContent, formatSatoriOutbound, inboundSatoriSegments, isMessageEvent, mapSatoriFriendRequest, normalizeSatoriGuildRoles, readSatoriFriend, satoriFriendApproveParams, satoriReactionParams, mapGuildEmojiNotice, satoriMessageCreateParams, withSatoriReferrer, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, type ResolvedSatoriWebhookConfig, type ResolvedSatoriWsConfig, type SatoriEndpointConfig, type SatoriApiOptions, type SatoriApiRequest, type SatoriChannel, type SatoriEmoji, type SatoriEventBody, type SatoriFriend, type SatoriFriendRequestActions, type SatoriGuildMember, type SatoriGuildRole, type SatoriReactionMethod, type SatoriLogin, type SatoriMessage, type SatoriSignal, type SatoriUser, type SatoriWireSegment, } from './protocol.js';
|
|
2
2
|
export { SatoriWebhookEndpoint, SatoriWsEndpoint, type CreateSatoriWebSocket, type SatoriApiCaller, type SatoriWebhookEndpointOptions, type SatoriWsEndpointOptions, type SatoriWsSocket, } from './endpoint.js';
|
|
3
3
|
export { handleSatoriWebhookRequest, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, type SatoriWebhookHandler, } from './webhook.js';
|
|
4
4
|
export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, formatInboundContent, formatSatoriOutbound, isMessageEvent, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, } from './protocol.js';
|
|
1
|
+
export { SatoriOpcode, buildWsUrl, callSatoriApi, extractCreatedMessageId, decodeSatoriContent, formatInboundContent, formatSatoriOutbound, inboundSatoriSegments, isMessageEvent, mapSatoriFriendRequest, normalizeSatoriGuildRoles, readSatoriFriend, satoriFriendApproveParams, satoriReactionParams, mapGuildEmojiNotice, satoriMessageCreateParams, withSatoriReferrer, isPrivateChannel, resolveInboundSender, resolveSatoriConfig, satoriInboundConversation, } from './protocol.js';
|
|
2
2
|
export { SatoriWebhookEndpoint, SatoriWsEndpoint, } from './endpoint.js';
|
|
3
3
|
export { handleSatoriWebhookRequest, registerSatoriWebhookRoutes, resolveSatoriOpcode, verifySatoriToken, } from './webhook.js';
|
|
4
4
|
export { WS_OPEN, defaultCreateWebSocket, } from './ws.js';
|
package/lib/protocol.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EndpointProfileConfiguration } from 'zhin.js/adapter';
|
|
2
|
-
|
|
3
|
-
import { type ConversationRef } from '@zhin.js/im-contract';
|
|
2
|
+
import { type IncomingNotice, type IncomingRequest } from '@zhin.js/core';
|
|
3
|
+
import { type ConversationRef, type Segment } from '@zhin.js/im-contract';
|
|
4
4
|
/** Opcode:EVENT=0, PING=1, PONG=2, IDENTIFY=3, READY=4, META=5 */
|
|
5
5
|
export declare const SatoriOpcode: {
|
|
6
6
|
readonly EVENT: 0;
|
|
@@ -19,6 +19,48 @@ export interface SatoriUser {
|
|
|
19
19
|
readonly name?: string;
|
|
20
20
|
readonly avatar?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Satori Emoji。字段对齐文档 https://satori.chat/zh-CN/resources/emoji.html (`id`、`name?`)。
|
|
24
|
+
* `@imhelper/satori-v1` 1.0.7 / 1.0.10 没有这个类型。上游 `feat/satori-1.3-alignment`
|
|
25
|
+
* 会补上,但这次不发布 npm,适配器先用本地声明。上游发版后可以换成 SDK 导出的官方类型。
|
|
26
|
+
*/
|
|
27
|
+
export interface SatoriEmoji {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly name?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 群组角色。文档只声明 `id` 和 `name?`。
|
|
33
|
+
* 协议源码里还有 color、position 等字段,这里以文档为准,不建模它们。
|
|
34
|
+
* `@imhelper/satori-v1` 没有这个类型。上游 `feat/satori-1.3-alignment` 会补,但未发 npm。
|
|
35
|
+
* 上游发版后可以换成官方类型。
|
|
36
|
+
* @see https://satori.chat/zh-CN/resources/role.html
|
|
37
|
+
*/
|
|
38
|
+
export interface SatoriGuildRole {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
readonly name?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 好友。文档形状是 `{ user?, nick? }`,不是带顶层 `id` / `name` / `avatar` 的 User。
|
|
44
|
+
* 1.0.10 的 `directory-api.getFriendList` 仍按 User 读取 `friend.id`。本地按文档解析。
|
|
45
|
+
* 上游发版后可以换成官方类型。
|
|
46
|
+
* @see https://satori.chat/zh-CN/resources/friend.html
|
|
47
|
+
*/
|
|
48
|
+
export interface SatoriFriend {
|
|
49
|
+
readonly user?: SatoriUser;
|
|
50
|
+
readonly nick?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 群组成员。`roles` 在 1.3 起是 `GuildRole[]`,不再是 `string[]`。
|
|
54
|
+
* 本地类型。上游发版后可以换成官方类型。
|
|
55
|
+
* @see https://satori.chat/zh-CN/resources/member.html
|
|
56
|
+
*/
|
|
57
|
+
export interface SatoriGuildMember {
|
|
58
|
+
readonly user?: SatoriUser;
|
|
59
|
+
readonly nick?: string;
|
|
60
|
+
readonly avatar?: string;
|
|
61
|
+
readonly joined_at?: number;
|
|
62
|
+
readonly roles?: readonly SatoriGuildRole[];
|
|
63
|
+
}
|
|
22
64
|
export interface SatoriChannel {
|
|
23
65
|
readonly id: string;
|
|
24
66
|
/** 0=TEXT, 1=DIRECT, 2=CATEGORY, 3=VOICE */
|
|
@@ -56,11 +98,26 @@ export interface SatoriEventBody {
|
|
|
56
98
|
readonly id: string;
|
|
57
99
|
readonly name?: string;
|
|
58
100
|
};
|
|
59
|
-
readonly member?:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
101
|
+
readonly member?: SatoriGuildMember;
|
|
102
|
+
/**
|
|
103
|
+
* 事件里的好友。本地字段,原因见 {@link SatoriFriend}。
|
|
104
|
+
* @see https://satori.chat/zh-CN/protocol/events.html
|
|
105
|
+
*/
|
|
106
|
+
readonly friend?: SatoriFriend;
|
|
107
|
+
readonly operator?: SatoriUser;
|
|
108
|
+
/**
|
|
109
|
+
* 事件表情。本地字段,原因见 {@link SatoriEmoji}。
|
|
110
|
+
* @see https://satori.chat/zh-CN/protocol/events.html
|
|
111
|
+
*/
|
|
112
|
+
readonly emoji?: SatoriEmoji;
|
|
113
|
+
/**
|
|
114
|
+
* 被动请求的来源上下文。协议不约束内部结构,发送时必须原样回传。
|
|
115
|
+
* 资源页 `message.create` 表格目前没列出该字段,但被动请求页要求传入,
|
|
116
|
+
* 协议方法表也包含 `referrer`。这里以被动请求页为准。
|
|
117
|
+
* 本地字段:`SatoriV1Event` 尚未声明它。上游发版后可以换成官方类型。
|
|
118
|
+
* @see https://satori.chat/zh-CN/advanced/passive.html
|
|
119
|
+
*/
|
|
120
|
+
readonly referrer?: unknown;
|
|
64
121
|
readonly [key: string]: unknown;
|
|
65
122
|
}
|
|
66
123
|
export interface SatoriApiOptions {
|
|
@@ -73,6 +130,7 @@ export interface SatoriApiOptions {
|
|
|
73
130
|
export interface SatoriWireSegment {
|
|
74
131
|
readonly type: string;
|
|
75
132
|
readonly data?: Record<string, unknown>;
|
|
133
|
+
readonly platform?: Record<string, unknown>;
|
|
76
134
|
}
|
|
77
135
|
/** One endpoint config after AdapterIndex expands `plugins.<instanceKey>.endpoints`. */
|
|
78
136
|
export interface SatoriEndpointConfig extends EndpointProfileConfiguration {
|
|
@@ -129,6 +187,19 @@ export declare function callSatoriApi<T = unknown>({ resource, method, params, .
|
|
|
129
187
|
export declare function formatInboundContent(body: SatoriEventBody & {
|
|
130
188
|
message: SatoriMessage;
|
|
131
189
|
}): string;
|
|
190
|
+
export declare function decodeSatoriContent(content: string, depth?: number): Segment[];
|
|
191
|
+
/** 仅在出现非文本元素时把解码结果送进消息管道,纯文本保持原来的 content-only 形状。 */
|
|
192
|
+
export declare function inboundSatoriSegments(content: string): readonly Segment[] | undefined;
|
|
193
|
+
/**
|
|
194
|
+
* `guild-emoji-*` → `notice.group.emoji_*`。
|
|
195
|
+
* 文档只要求资源 `emoji`,没有可回复频道,因此不填 conversation。
|
|
196
|
+
* 上游 SDK 目前只把这些事件原样 emit("event"),映射放在适配器内。
|
|
197
|
+
* @see https://satori.chat/zh-CN/resources/emoji.html
|
|
198
|
+
*/
|
|
199
|
+
export declare function mapGuildEmojiNotice(body: SatoriEventBody, endpointId: string): IncomingNotice | undefined;
|
|
200
|
+
export declare function withSatoriReferrer<T>(referrer: unknown, run: () => T): T;
|
|
201
|
+
/** `message.create` 参数。处理尚未结束时原样附带 referrer,不挑字段、不改名。 */
|
|
202
|
+
export declare function satoriMessageCreateParams(channelId: string, content: string): Record<string, unknown>;
|
|
132
203
|
/**
|
|
133
204
|
* 入站归一化 → ConversationRef:Channel.type 1 (DIRECT) → kind 'private';
|
|
134
205
|
* 其余频道消息 → kind 'group',所属 guild 容器进 `parent`(kind 'channel')。
|
|
@@ -155,8 +226,49 @@ export declare function parseMessageRef(id: string): {
|
|
|
155
226
|
messageId: string;
|
|
156
227
|
};
|
|
157
228
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
229
|
+
* 把已渲染的出站载荷编成 Satori 消息字符串。
|
|
230
|
+
* at / mention → `<at>`,image → `<img>`,其余媒体用同名标准元素,emoji → `<emoji>`。
|
|
231
|
+
* 整段字符串原样发送(调用方可能已经写好标记);段数组里的文本会转义。
|
|
232
|
+
* 上游 message-codec 把段编成 `{type, attrs}` 对象,而且不会把 mention/image 改成 at/img。
|
|
233
|
+
* 这里在适配器内按文档元素编码。上游发版后如果 SDK 直接产出这些元素,可以改走 SDK。
|
|
234
|
+
* @see https://satori.chat/zh-CN/protocol/elements.html
|
|
160
235
|
*/
|
|
161
236
|
export declare function formatSatoriOutbound(payload: unknown): string;
|
|
162
237
|
export declare function extractCreatedMessageId(result: unknown): string;
|
|
238
|
+
/**
|
|
239
|
+
* 把 `guild-member.list` 的 `roles` 收成 `GuildRole[]`。
|
|
240
|
+
* 旧 SDK 可能仍下发 `string[]`,这里收成 `{ id }`。上游发版并保证对象数组后,字符串分支可以去掉。
|
|
241
|
+
*/
|
|
242
|
+
export declare function normalizeSatoriGuildRoles(value: unknown): readonly SatoriGuildRole[] | undefined;
|
|
243
|
+
/** 成员列表原样保留其余字段,只把 roles 收成 GuildRole[]。 */
|
|
244
|
+
export declare function withSatoriGuildRoles<T>(member: T): T;
|
|
245
|
+
/**
|
|
246
|
+
* 按 `{ user, nick }` 读取好友。顶层 `id` / `name` 是 1.0.10 误用的 User 形状,这里不认。
|
|
247
|
+
*/
|
|
248
|
+
export declare function readSatoriFriend(value: unknown): SatoriFriend | undefined;
|
|
249
|
+
export type SatoriReactionMethod = 'create' | 'delete' | 'clear' | 'list';
|
|
250
|
+
/**
|
|
251
|
+
* reaction.* 的参数名是 `emoji_id`,不是 1.0.10 `addMessageReactionIn` 用的 `emoji`。
|
|
252
|
+
* clear 不传 emoji_id 表示清除全部表态。
|
|
253
|
+
* @see https://satori.chat/zh-CN/resources/reaction.html
|
|
254
|
+
*/
|
|
255
|
+
export declare function satoriReactionParams(method: SatoriReactionMethod, input: {
|
|
256
|
+
readonly channelId: string;
|
|
257
|
+
readonly messageId: string;
|
|
258
|
+
readonly emojiId?: string;
|
|
259
|
+
readonly userId?: string;
|
|
260
|
+
readonly next?: string;
|
|
261
|
+
}): Record<string, unknown>;
|
|
262
|
+
/** `friend.approve` 同时表达通过和拒绝。 */
|
|
263
|
+
export declare function satoriFriendApproveParams(messageId: string, approve: boolean, comment?: string): Record<string, unknown>;
|
|
264
|
+
export interface SatoriFriendRequestActions {
|
|
265
|
+
approve(messageId: string, comment?: string): Promise<void>;
|
|
266
|
+
reject(messageId: string, reason?: string): Promise<void>;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* `friend-request` → `request.friend.add`,让它进入 request.receive 管道。
|
|
270
|
+
* 文档的必需资源是 `user`。`friend.approve` 的请求 ID 是 `message_id`;
|
|
271
|
+
* 事件没有 `message.id` 时仍然投递,但批准/拒绝会说明缺少该字段。
|
|
272
|
+
* @see https://satori.chat/zh-CN/resources/friend.html
|
|
273
|
+
*/
|
|
274
|
+
export declare function mapSatoriFriendRequest(body: SatoriEventBody, endpointId: string, actions: SatoriFriendRequestActions): IncomingRequest | undefined;
|