@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/src/endpoint.ts CHANGED
@@ -7,7 +7,15 @@ import {
7
7
  createEndpointSpecification,
8
8
  endpointSpec,
9
9
  type Endpoint,
10
+ type EndpointApproveRequest,
11
+ type EndpointFriend,
12
+ type EndpointReactionRequest,
13
+ type EndpointRejectRequest,
14
+ type EndpointRemoveReactionRequest,
15
+ type EndpointEventSink,
16
+ type EndpointIdentity,
10
17
  type EndpointSpecification,
18
+ type EndpointUserRequest,
11
19
  createEndpointLifecycle,
12
20
  type EndpointChannel,
13
21
  type EndpointConnectHandle,
@@ -31,11 +39,20 @@ import {
31
39
  formatInboundContent,
32
40
  formatMessageId,
33
41
  formatSatoriOutbound,
42
+ inboundSatoriSegments,
34
43
  isMessageEvent,
44
+ mapSatoriFriendRequest,
45
+ readSatoriFriend,
46
+ satoriFriendApproveParams,
47
+ satoriReactionParams,
48
+ withSatoriGuildRoles,
35
49
  isSelfMentioned,
50
+ mapGuildEmojiNotice,
36
51
  parseMessageRef,
37
52
  resolveInboundSender,
38
53
  satoriInboundConversation,
54
+ satoriMessageCreateParams,
55
+ withSatoriReferrer,
39
56
  type ResolvedSatoriWebhookConfig,
40
57
  type ResolvedSatoriWsConfig,
41
58
  type SatoriEventBody,
@@ -74,6 +91,8 @@ export class SatoriWsEndpoint extends SatoriClient implements Endpoint {
74
91
  readonly #call: SatoriActionCaller;
75
92
  readonly #control: EndpointControl = Object.freeze<EndpointControl>({
76
93
  recall: request => this.#recall(request),
94
+ addReaction: request => this.#addReaction(request),
95
+ removeReaction: request => this.#removeReaction(request),
77
96
  });
78
97
 
79
98
  constructor(options: SatoriWsEndpointOptions) {
@@ -143,10 +162,7 @@ export class SatoriWsEndpoint extends SatoriClient implements Endpoint {
143
162
 
144
163
  async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
145
164
  const content = formatSatoriOutbound(payload);
146
- const result = await this.#call({ resource: 'message', method: 'create', signal, params: {
147
- channel_id: conversation.id,
148
- content,
149
- } });
165
+ const result = await this.#call({ resource: 'message', method: 'create', signal, params: satoriMessageCreateParams(conversation.id, content) });
150
166
  const msgId = extractCreatedMessageId(result);
151
167
  this.#logger.debug(formatCompact({
152
168
  op: 'satori_send',
@@ -159,34 +175,33 @@ export class SatoriWsEndpoint extends SatoriClient implements Endpoint {
159
175
 
160
176
  #admitRaw(body: SatoriEventBody): void {
161
177
  if (!matchesSatoriLogin(this.#options.config, body.login)) return;
162
- if (!isMessageEvent(body)) return;
163
- const conversation = { ...satoriInboundConversation(String(this.#options.id), body), endpoint: this[endpointSpec].identity };
164
- const content = formatInboundContent(body);
165
- const sender = resolveInboundSender(body);
166
- const selfId = this.#options.config.userId;
167
- const mentioned = isSelfMentioned(body, selfId);
168
- void this[endpointSpec].events.message({
169
- event: body,
170
- ...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
171
- conversation,
172
- message: { conversation, id: body.message.id },
173
- content,
174
- sender,
178
+ if (emitSatoriFriendRequest(body, {
175
179
  endpointId: this.#options.config.id,
176
- ...(mentioned ? { mentioned: true } : {}),
177
- metadata: Object.freeze({
178
- type: body.type,
179
- channelType: isPrivateChannelType(body) ? 'private' : 'group',
180
- sn: body.sn,
180
+ call: this.#call,
181
+ emit: request => {
182
+ void this[endpointSpec].events.emit('request.receive', request).catch(error => {
183
+ this.#warnPlatformEvent('request.receive', error);
184
+ });
185
+ },
186
+ })) return;
187
+ const deliver = () => deliverSatoriInbound(body, {
188
+ endpointId: this.#options.config.id,
189
+ endpointKey: String(this.#options.id),
190
+ identity: this[endpointSpec].identity,
191
+ selfId: this.#options.config.userId,
181
192
  platform: this.#options.config.platform,
182
- }),
183
- }).catch((err) => {
184
- this.#logger.warn(formatCompact({
185
- op: 'satori_gateway_receive_failed',
186
- channel: conversation.id,
187
- error: err instanceof Error ? err.message : String(err),
188
- }));
193
+ events: this[endpointSpec].events,
194
+ warnReceive: (channel, error) => {
195
+ this.#logger.warn(formatCompact({
196
+ op: 'satori_gateway_receive_failed',
197
+ channel,
198
+ error: error instanceof Error ? error.message : String(error),
199
+ }));
200
+ },
201
+ warnEvent: (event, error) => this.#warnPlatformEvent(event, error),
189
202
  });
203
+ if (body.referrer !== undefined) withSatoriReferrer(body.referrer, deliver);
204
+ else deliver();
190
205
  }
191
206
 
192
207
  async #recall({ message, signal }: EndpointRecallRequest): Promise<void> {
@@ -197,6 +212,14 @@ export class SatoriWsEndpoint extends SatoriClient implements Endpoint {
197
212
  } });
198
213
  }
199
214
 
215
+ #addReaction(request: EndpointReactionRequest): Promise<string | null> {
216
+ return addSatoriReaction(this.#call, request);
217
+ }
218
+
219
+ #removeReaction(request: EndpointRemoveReactionRequest): Promise<void> {
220
+ return removeSatoriReaction(this.#call, request);
221
+ }
222
+
200
223
  #warnPlatformEvent(name: string, error: unknown): void {
201
224
  this.#logger.warn(formatCompact({
202
225
  op: 'satori_platform_event_failed',
@@ -359,6 +382,8 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
359
382
  readonly #call: SatoriActionCaller;
360
383
  readonly #control: EndpointControl = Object.freeze<EndpointControl>({
361
384
  recall: request => this.#recall(request),
385
+ addReaction: request => this.#addReaction(request),
386
+ removeReaction: request => this.#removeReaction(request),
362
387
  });
363
388
 
364
389
  constructor(options: SatoriWebhookEndpointOptions) {
@@ -428,10 +453,7 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
428
453
 
429
454
  async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
430
455
  const content = formatSatoriOutbound(payload);
431
- const result = await this.#call({ resource: 'message', method: 'create', signal, params: {
432
- channel_id: conversation.id,
433
- content,
434
- } });
456
+ const result = await this.#call({ resource: 'message', method: 'create', signal, params: satoriMessageCreateParams(conversation.id, content) });
435
457
  const msgId = extractCreatedMessageId(result);
436
458
  this.#logger.debug(formatCompact({
437
459
  op: 'satori_send',
@@ -444,34 +466,33 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
444
466
 
445
467
  #admitRaw(body: SatoriEventBody): void {
446
468
  if (!matchesSatoriLogin(this.#options.config, body.login)) return;
447
- if (!isMessageEvent(body)) return;
448
- const conversation = { ...satoriInboundConversation(String(this.#options.id), body), endpoint: this[endpointSpec].identity };
449
- const content = formatInboundContent(body);
450
- const sender = resolveInboundSender(body);
451
- const selfId = this.#options.config.userId;
452
- const mentioned = isSelfMentioned(body, selfId);
453
- void this[endpointSpec].events.message({
454
- event: body,
455
- ...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
456
- conversation,
457
- message: { conversation, id: body.message.id },
458
- content,
459
- sender,
469
+ if (emitSatoriFriendRequest(body, {
460
470
  endpointId: this.#options.config.id,
461
- ...(mentioned ? { mentioned: true } : {}),
462
- metadata: Object.freeze({
463
- type: body.type,
464
- channelType: isPrivateChannelType(body) ? 'private' : 'group',
465
- sn: body.sn,
471
+ call: this.#call,
472
+ emit: request => {
473
+ void this[endpointSpec].events.emit('request.receive', request).catch(error => {
474
+ this.#warnPlatformEvent('request.receive', error);
475
+ });
476
+ },
477
+ })) return;
478
+ const deliver = () => deliverSatoriInbound(body, {
479
+ endpointId: this.#options.config.id,
480
+ endpointKey: String(this.#options.id),
481
+ identity: this[endpointSpec].identity,
482
+ selfId: this.#options.config.userId,
466
483
  platform: this.#options.config.platform,
467
- }),
468
- }).catch((err) => {
469
- this.#logger.warn(formatCompact({
470
- op: 'satori_gateway_receive_failed',
471
- channel: conversation.id,
472
- error: err instanceof Error ? err.message : String(err),
473
- }));
484
+ events: this[endpointSpec].events,
485
+ warnReceive: (channel, error) => {
486
+ this.#logger.warn(formatCompact({
487
+ op: 'satori_gateway_receive_failed',
488
+ channel,
489
+ error: error instanceof Error ? error.message : String(error),
490
+ }));
491
+ },
492
+ warnEvent: (event, error) => this.#warnPlatformEvent(event, error),
474
493
  });
494
+ if (body.referrer !== undefined) withSatoriReferrer(body.referrer, deliver);
495
+ else deliver();
475
496
  }
476
497
 
477
498
  async #recall({ message, signal }: EndpointRecallRequest): Promise<void> {
@@ -482,6 +503,14 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
482
503
  } });
483
504
  }
484
505
 
506
+ #addReaction(request: EndpointReactionRequest): Promise<string | null> {
507
+ return addSatoriReaction(this.#call, request);
508
+ }
509
+
510
+ #removeReaction(request: EndpointRemoveReactionRequest): Promise<void> {
511
+ return removeSatoriReaction(this.#call, request);
512
+ }
513
+
485
514
  #warnPlatformEvent(name: string, error: unknown): void {
486
515
  this.#logger.warn(formatCompact({
487
516
  op: 'satori_platform_event_failed',
@@ -498,6 +527,47 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
498
527
 
499
528
  }
500
529
 
530
+ function deliverSatoriInbound(body: SatoriEventBody, input: {
531
+ readonly endpointId: string;
532
+ readonly endpointKey: string;
533
+ readonly identity: EndpointIdentity;
534
+ readonly selfId: string;
535
+ readonly platform: string;
536
+ readonly events: EndpointEventSink;
537
+ readonly warnReceive: (channel: string, error: unknown) => void;
538
+ readonly warnEvent: (event: string, error: unknown) => void;
539
+ }): Promise<unknown> {
540
+ const notice = mapGuildEmojiNotice(body, input.endpointId);
541
+ if (notice) {
542
+ return input.events.emit('notice.receive', notice).catch(error => input.warnEvent('notice.receive', error));
543
+ }
544
+ if (!isMessageEvent(body)) return Promise.resolve();
545
+ const conversation = { ...satoriInboundConversation(input.endpointKey, body), endpoint: input.identity };
546
+ const content = formatInboundContent(body);
547
+ const segments = typeof body.message.content === 'string'
548
+ ? inboundSatoriSegments(body.message.content)
549
+ : undefined;
550
+ const sender = resolveInboundSender(body);
551
+ const mentioned = isSelfMentioned(body, input.selfId);
552
+ return input.events.message({
553
+ event: body,
554
+ ...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
555
+ conversation,
556
+ message: { conversation, id: body.message.id },
557
+ content,
558
+ ...(segments ? { segments } : {}),
559
+ sender,
560
+ endpointId: input.endpointId,
561
+ ...(mentioned ? { mentioned: true } : {}),
562
+ metadata: Object.freeze({
563
+ type: body.type,
564
+ channelType: isPrivateChannelType(body) ? 'private' : 'group',
565
+ sn: body.sn,
566
+ platform: input.platform,
567
+ }),
568
+ }).catch(error => input.warnReceive(conversation.id, error));
569
+ }
570
+
501
571
  function isPrivateChannelType(body: SatoriEventBody): boolean {
502
572
  const channel = body.channel ?? body.message?.channel;
503
573
  return channel?.type === 1;
@@ -545,7 +615,8 @@ function asRecord(value: unknown): Record<string, unknown> {
545
615
 
546
616
  /**
547
617
  * Satori endpoint 的 EndpointManagement 语义端口(ws / webhook 共用),
548
- * 数据走协议 API:guild.list / channel.list / guild-member.list。
618
+ * 数据走协议 API:guild.list / channel.list / guild.member.list。
619
+ * @see https://satori.chat/zh-CN/resources/member.html
549
620
  */
550
621
  export function createSatoriEndpointManagement(api: SatoriManagementApi): EndpointManagement {
551
622
  return Object.freeze<EndpointManagement>({
@@ -583,14 +654,100 @@ export function createSatoriEndpointManagement(api: SatoriManagementApi): Endpoi
583
654
  return channels;
584
655
  },
585
656
  async listGroupMembers({ groupId, signal }: EndpointGroupRequest): Promise<readonly unknown[]> {
586
- // 平台形状(GuildMember[])原样返回
587
- return listSatoriPages(api, { resource: 'guild-member', params: { guild_id: groupId }, signal });
657
+ // 平台形状(GuildMember[])原样返回,再规范化 1.3 的 GuildRole 角色
658
+ const members = await listSatoriPages(api, { resource: 'guild.member', params: { guild_id: groupId }, signal });
659
+ return members.map(member => withSatoriGuildRoles(member));
660
+ },
661
+ async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
662
+ const friends: EndpointFriend[] = [];
663
+ for (const value of await listSatoriPages(api, { resource: 'friend', signal: request.signal })) {
664
+ const friend = readSatoriFriend(value);
665
+ if (!friend?.user?.id) continue;
666
+ const nick = friend.nick ?? '';
667
+ const name = friend.user.name ?? '';
668
+ friends.push({
669
+ user_id: friend.user.id,
670
+ nickname: name || nick || friend.user.id,
671
+ remark: nick,
672
+ });
673
+ }
674
+ return friends;
675
+ },
676
+ async deleteFriend({ userId, signal }: EndpointUserRequest): Promise<void> {
677
+ await api({ resource: 'friend', method: 'delete', params: { user_id: userId }, signal });
678
+ },
679
+ async approveRequest({ requestId, remark, signal }: EndpointApproveRequest): Promise<void> {
680
+ await api({
681
+ resource: 'friend',
682
+ method: 'approve',
683
+ signal,
684
+ params: satoriFriendApproveParams(requestId, true, remark),
685
+ });
686
+ },
687
+ async rejectRequest({ requestId, reason, signal }: EndpointRejectRequest): Promise<void> {
688
+ await api({
689
+ resource: 'friend',
690
+ method: 'approve',
691
+ signal,
692
+ params: satoriFriendApproveParams(requestId, false, reason),
693
+ });
588
694
  },
589
695
  });
590
696
  }
591
697
 
592
698
  export type { CreateSatoriWebSocket, SatoriWsSocket } from './ws.js';
593
699
 
700
+ function emitSatoriFriendRequest(body: SatoriEventBody, input: {
701
+ readonly endpointId: string;
702
+ readonly call: SatoriActionCaller;
703
+ readonly emit: (request: ReturnType<typeof mapSatoriFriendRequest>) => void;
704
+ }): boolean {
705
+ if (body.type !== 'friend-request') return false;
706
+ const request = mapSatoriFriendRequest(body, input.endpointId, {
707
+ approve: (messageId, comment) => input.call({
708
+ resource: 'friend',
709
+ method: 'approve',
710
+ params: satoriFriendApproveParams(messageId, true, comment),
711
+ }).then(() => undefined),
712
+ reject: (messageId, reason) => input.call({
713
+ resource: 'friend',
714
+ method: 'approve',
715
+ params: satoriFriendApproveParams(messageId, false, reason),
716
+ }).then(() => undefined),
717
+ });
718
+ if (request) input.emit(request);
719
+ return true;
720
+ }
721
+
722
+ async function addSatoriReaction(call: SatoriActionCaller, request: EndpointReactionRequest): Promise<string | null> {
723
+ const { channelId, messageId } = parseMessageRef(request.message.id);
724
+ await call({
725
+ resource: 'reaction',
726
+ method: 'create',
727
+ signal: request.signal,
728
+ params: satoriReactionParams('create', {
729
+ channelId: channelId || request.message.conversation.id,
730
+ messageId,
731
+ emojiId: request.emoji,
732
+ }),
733
+ });
734
+ return request.emoji;
735
+ }
736
+
737
+ async function removeSatoriReaction(call: SatoriActionCaller, request: EndpointRemoveReactionRequest): Promise<void> {
738
+ const { channelId, messageId } = parseMessageRef(request.message.id);
739
+ await call({
740
+ resource: 'reaction',
741
+ method: 'delete',
742
+ signal: request.signal,
743
+ params: satoriReactionParams('delete', {
744
+ channelId: channelId || request.message.conversation.id,
745
+ messageId,
746
+ emojiId: request.reactionId,
747
+ }),
748
+ });
749
+ }
750
+
594
751
  /** A transport connection may multiplex logins; this Endpoint admits only its selected account. */
595
752
  function matchesSatoriLogin(config: { readonly platform: string; readonly userId: string }, login: SatoriLogin | undefined): boolean {
596
753
  return login?.platform === config.platform && login.user?.id === config.userId;
package/src/index.ts CHANGED
@@ -3,9 +3,19 @@ export {
3
3
  buildWsUrl,
4
4
  callSatoriApi,
5
5
  extractCreatedMessageId,
6
+ decodeSatoriContent,
6
7
  formatInboundContent,
7
8
  formatSatoriOutbound,
9
+ inboundSatoriSegments,
8
10
  isMessageEvent,
11
+ mapSatoriFriendRequest,
12
+ normalizeSatoriGuildRoles,
13
+ readSatoriFriend,
14
+ satoriFriendApproveParams,
15
+ satoriReactionParams,
16
+ mapGuildEmojiNotice,
17
+ satoriMessageCreateParams,
18
+ withSatoriReferrer,
9
19
  isPrivateChannel,
10
20
  resolveInboundSender,
11
21
  resolveSatoriConfig,
@@ -16,7 +26,13 @@ export {
16
26
  type SatoriApiOptions,
17
27
  type SatoriApiRequest,
18
28
  type SatoriChannel,
29
+ type SatoriEmoji,
19
30
  type SatoriEventBody,
31
+ type SatoriFriend,
32
+ type SatoriFriendRequestActions,
33
+ type SatoriGuildMember,
34
+ type SatoriGuildRole,
35
+ type SatoriReactionMethod,
20
36
  type SatoriLogin,
21
37
  type SatoriMessage,
22
38
  type SatoriSignal,