@zhin.js/adapter-satori 1.1.26 → 1.1.28

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/lib/webhook.js CHANGED
@@ -8,43 +8,29 @@ const logger = getLogger('satori');
8
8
  export function registerSatoriWebhookRoutes(http, handler) {
9
9
  const path = handler.config.path;
10
10
  return [
11
- http.route('POST', path, async (request, response) => {
12
- await handleSatoriWebhookRequest(request, response, handler);
13
- }, { summary: 'Satori webhook callback', tags: ['satori'] }),
11
+ http.route('POST', path, (request) => handleSatoriWebhookRequest(request, handler), { summary: 'Satori webhook callback', tags: ['satori'] }),
14
12
  ];
15
13
  }
16
- export async function handleSatoriWebhookRequest(request, response, handler) {
14
+ export async function handleSatoriWebhookRequest(request, handler) {
17
15
  try {
18
16
  if (!verifySatoriToken(handler.config.token, request)) {
19
- response.writeHead(403, { 'Content-Type': 'application/json' });
20
- response.end(JSON.stringify({ message: 'Unauthorized' }));
21
- return;
17
+ return new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
22
18
  }
23
19
  const opcode = resolveSatoriOpcode(request);
24
20
  if (opcode !== SatoriOpcode.EVENT && opcode !== SatoriOpcode.META) {
25
- response.writeHead(200, { 'Content-Type': 'application/json' });
26
- response.end(JSON.stringify({ message: 'OK' }));
27
- return;
28
- }
29
- if (handler.isOpen) {
30
- await handler.acceptHttp(request, response);
31
- }
32
- else {
33
- response.writeHead(200, { 'Content-Type': 'application/json' });
34
- response.end(JSON.stringify({ status: 'ok' }));
21
+ return new Response(JSON.stringify({ message: 'OK' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
35
22
  }
23
+ if (handler.isOpen)
24
+ return await handler.acceptHttp(request);
25
+ return new Response(JSON.stringify({ status: 'ok' }), { status: 200, headers: { 'Content-Type': 'application/json' } });
36
26
  }
37
27
  catch (error) {
38
28
  logger.error('Satori webhook error:', error);
39
- if (!response.headersSent) {
40
- response.writeHead(500, { 'Content-Type': 'application/json' });
41
- response.end(JSON.stringify({ message: 'Internal Server Error' }));
42
- }
29
+ return new Response(JSON.stringify({ message: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' } });
43
30
  }
44
31
  }
45
32
  export function resolveSatoriOpcode(request) {
46
- const raw = request.headers['satori-opcode'] ?? request.headers['Satori-Opcode'];
47
- const value = Array.isArray(raw) ? raw[0] : raw;
33
+ const value = request.headers.get('satori-opcode');
48
34
  if (value == null || value === '')
49
35
  return undefined;
50
36
  const parsed = Number.parseInt(String(value), 10);
@@ -53,7 +39,7 @@ export function resolveSatoriOpcode(request) {
53
39
  export function verifySatoriToken(token, request) {
54
40
  if (!token)
55
41
  return true;
56
- const auth = request.headers.authorization ?? '';
42
+ const auth = request.headers.get('authorization') ?? '';
57
43
  const expected = Buffer.from(`Bearer ${token}`, 'utf8');
58
44
  const actual = Buffer.from(auth, 'utf8');
59
45
  return actual.length === expected.length && timingSafeEqual(actual, expected);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-satori",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
4
4
  "description": "Zhin.js Satori adapter for Plugin Runtime (WebSocket client)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -19,7 +19,6 @@
19
19
  "schema.json",
20
20
  "src",
21
21
  "lib",
22
- "skills",
23
22
  "README.md",
24
23
  "CHANGELOG.md"
25
24
  ],
@@ -34,36 +33,35 @@
34
33
  "license": "MIT",
35
34
  "repository": {
36
35
  "type": "git",
37
- "url": "https://github.com/zhinjs/zhin.git",
36
+ "url": "git+https://github.com/zhinjs/zhin.git",
38
37
  "directory": "plugins/adapters/satori"
39
38
  },
40
39
  "dependencies": {
41
40
  "@imhelper/satori-v1": "1.0.7",
42
41
  "imhelper": "1.0.7",
43
42
  "ws": "^8.21.1",
44
- "@zhin.js/adapter": "1.1.32",
45
- "@zhin.js/console-contract": "1.1.5",
46
- "@zhin.js/core": "1.1.60",
47
- "@zhin.js/feature-kit": "1.1.15",
48
- "@zhin.js/host-http": "1.1.17",
49
- "@zhin.js/im-contract": "1.1.13",
50
- "@zhin.js/logger": "1.1.2",
51
- "@zhin.js/page": "1.1.17",
52
- "@zhin.js/skill": "1.1.16"
43
+ "@zhin.js/adapter": "1.1.33",
44
+ "@zhin.js/console-contract": "1.1.6",
45
+ "@zhin.js/core": "1.2.0",
46
+ "@zhin.js/feature-kit": "1.1.16",
47
+ "@zhin.js/host-http": "1.2.0",
48
+ "@zhin.js/im-contract": "1.1.14",
49
+ "@zhin.js/logger": "1.1.3",
50
+ "@zhin.js/page": "1.1.18"
53
51
  },
54
52
  "devDependencies": {
55
53
  "@types/node": "^26.1.2",
56
54
  "@types/ws": "^8.18.1",
57
55
  "typescript": "^6.0.3",
58
56
  "vitest": "^4.1.10",
59
- "@zhin.js/host-http": "1.1.17",
60
- "zhin.js": "1.1.25"
57
+ "@zhin.js/host-http": "1.2.0",
58
+ "zhin.js": "1.1.26"
61
59
  },
62
60
  "peerDependencies": {
63
- "@zhin.js/adapter": "^1.1.32",
64
- "@zhin.js/command": "^1.1.17",
65
- "@zhin.js/core": "^1.1.60",
66
- "zhin.js": "^1.1.25"
61
+ "@zhin.js/adapter": "^1.1.33",
62
+ "@zhin.js/command": "^1.2.0",
63
+ "@zhin.js/core": "^1.2.0",
64
+ "zhin.js": "^1.1.26"
67
65
  },
68
66
  "peerDependenciesMeta": {
69
67
  "@zhin.js/command": {
@@ -100,10 +98,6 @@
100
98
  "package": "@zhin.js/command",
101
99
  "api": "^1.0.0"
102
100
  },
103
- {
104
- "package": "@zhin.js/skill",
105
- "api": "^1.0.0"
106
- },
107
101
  {
108
102
  "package": "@zhin.js/page",
109
103
  "api": "^1.0.0"
@@ -114,6 +108,6 @@
114
108
  "scripts": {
115
109
  "build": "tsc",
116
110
  "clean": "rimraf lib",
117
- "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/adapters/satori/tests"
111
+ "test": "vitest run --root ../../.. plugins/adapters/satori/tests"
118
112
  }
119
113
  }
package/schema.json CHANGED
@@ -17,34 +17,6 @@
17
17
  "default": 10000,
18
18
  "description": "WS PING interval in milliseconds"
19
19
  },
20
- "master": {
21
- "type": [
22
- "string",
23
- "number"
24
- ],
25
- "description": "框架 master(platform user id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
26
- "x-i18n": {
27
- "en": {
28
- "description": "Framework masters (platform user id; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
29
- }
30
- }
31
- },
32
- "trusted": {
33
- "type": "array",
34
- "items": {
35
- "type": [
36
- "string",
37
- "number"
38
- ],
39
- "description": "Trusted platform user id"
40
- },
41
- "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
42
- "x-i18n": {
43
- "en": {
44
- "description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
45
- }
46
- }
47
- },
48
20
  "endpoints": {
49
21
  "type": "array",
50
22
  "minItems": 1,
@@ -53,34 +25,6 @@
53
25
  "type": "object",
54
26
  "additionalProperties": false,
55
27
  "properties": {
56
- "master": {
57
- "type": [
58
- "string",
59
- "number"
60
- ],
61
- "description": "本 endpoint 的框架 master(platform user id);覆盖顶层 master",
62
- "x-i18n": {
63
- "en": {
64
- "description": "Framework masters for this endpoint (platform user id), overriding the top-level master list."
65
- }
66
- }
67
- },
68
- "trusted": {
69
- "type": "array",
70
- "items": {
71
- "type": [
72
- "string",
73
- "number"
74
- ],
75
- "description": "Trusted platform user id"
76
- },
77
- "description": "本 endpoint 的 trusted 列表",
78
- "x-i18n": {
79
- "en": {
80
- "description": "Trusted users for this endpoint."
81
- }
82
- }
83
- },
84
28
  "baseUrl": {
85
29
  "type": "string",
86
30
  "description": "Satori SDK HTTP/WS base URL (e.g. http://127.0.0.1:5140)"
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) {
@@ -402,11 +427,12 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
402
427
  this.#routeReleases.push(...registerSatoriWebhookRoutes(this.#options.http, {
403
428
  config: this.#options.config,
404
429
  get isOpen() { return isOpen(); },
405
- acceptHttp: async (request, response) => {
406
- await acceptHttpIngress<SatoriEventBody>(request, response, body => {
430
+ acceptHttp: async (request) => {
431
+ const result = await acceptHttpIngress<SatoriEventBody>(request, undefined, body => {
407
432
  void this[endpointSpec].events.run(() => this.#receive(body))
408
433
  .catch(error => this.#warnPlatformEvent('event', error));
409
434
  });
435
+ return new Response(JSON.stringify(result.body), { status: result.status, headers: result.headers });
410
436
  },
411
437
  }));
412
438
  this.#logger.info(formatCompact({
@@ -428,10 +454,7 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
428
454
 
429
455
  async #send({ conversation, payload, signal }: EndpointSendRequest): Promise<string> {
430
456
  const content = formatSatoriOutbound(payload);
431
- const result = await this.#call({ resource: 'message', method: 'create', signal, params: {
432
- channel_id: conversation.id,
433
- content,
434
- } });
457
+ const result = await this.#call({ resource: 'message', method: 'create', signal, params: satoriMessageCreateParams(conversation.id, content) });
435
458
  const msgId = extractCreatedMessageId(result);
436
459
  this.#logger.debug(formatCompact({
437
460
  op: 'satori_send',
@@ -444,34 +467,33 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
444
467
 
445
468
  #admitRaw(body: SatoriEventBody): void {
446
469
  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,
470
+ if (emitSatoriFriendRequest(body, {
460
471
  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,
472
+ call: this.#call,
473
+ emit: request => {
474
+ void this[endpointSpec].events.emit('request.receive', request).catch(error => {
475
+ this.#warnPlatformEvent('request.receive', error);
476
+ });
477
+ },
478
+ })) return;
479
+ const deliver = () => deliverSatoriInbound(body, {
480
+ endpointId: this.#options.config.id,
481
+ endpointKey: String(this.#options.id),
482
+ identity: this[endpointSpec].identity,
483
+ selfId: this.#options.config.userId,
466
484
  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
- }));
485
+ events: this[endpointSpec].events,
486
+ warnReceive: (channel, error) => {
487
+ this.#logger.warn(formatCompact({
488
+ op: 'satori_gateway_receive_failed',
489
+ channel,
490
+ error: error instanceof Error ? error.message : String(error),
491
+ }));
492
+ },
493
+ warnEvent: (event, error) => this.#warnPlatformEvent(event, error),
474
494
  });
495
+ if (body.referrer !== undefined) withSatoriReferrer(body.referrer, deliver);
496
+ else deliver();
475
497
  }
476
498
 
477
499
  async #recall({ message, signal }: EndpointRecallRequest): Promise<void> {
@@ -482,6 +504,14 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
482
504
  } });
483
505
  }
484
506
 
507
+ #addReaction(request: EndpointReactionRequest): Promise<string | null> {
508
+ return addSatoriReaction(this.#call, request);
509
+ }
510
+
511
+ #removeReaction(request: EndpointRemoveReactionRequest): Promise<void> {
512
+ return removeSatoriReaction(this.#call, request);
513
+ }
514
+
485
515
  #warnPlatformEvent(name: string, error: unknown): void {
486
516
  this.#logger.warn(formatCompact({
487
517
  op: 'satori_platform_event_failed',
@@ -498,6 +528,47 @@ export class SatoriWebhookEndpoint extends SatoriClient implements Endpoint {
498
528
 
499
529
  }
500
530
 
531
+ function deliverSatoriInbound(body: SatoriEventBody, input: {
532
+ readonly endpointId: string;
533
+ readonly endpointKey: string;
534
+ readonly identity: EndpointIdentity;
535
+ readonly selfId: string;
536
+ readonly platform: string;
537
+ readonly events: EndpointEventSink;
538
+ readonly warnReceive: (channel: string, error: unknown) => void;
539
+ readonly warnEvent: (event: string, error: unknown) => void;
540
+ }): Promise<unknown> {
541
+ const notice = mapGuildEmojiNotice(body, input.endpointId);
542
+ if (notice) {
543
+ return input.events.emit('notice.receive', notice).catch(error => input.warnEvent('notice.receive', error));
544
+ }
545
+ if (!isMessageEvent(body)) return Promise.resolve();
546
+ const conversation = { ...satoriInboundConversation(input.endpointKey, body), endpoint: input.identity };
547
+ const content = formatInboundContent(body);
548
+ const segments = typeof body.message.content === 'string'
549
+ ? inboundSatoriSegments(body.message.content)
550
+ : undefined;
551
+ const sender = resolveInboundSender(body);
552
+ const mentioned = isSelfMentioned(body, input.selfId);
553
+ return input.events.message({
554
+ event: body,
555
+ ...(typeof body.timestamp === 'number' ? { platformSentAt: body.timestamp } : {}),
556
+ conversation,
557
+ message: { conversation, id: body.message.id },
558
+ content,
559
+ ...(segments ? { segments } : {}),
560
+ sender,
561
+ endpointId: input.endpointId,
562
+ ...(mentioned ? { mentioned: true } : {}),
563
+ metadata: Object.freeze({
564
+ type: body.type,
565
+ channelType: isPrivateChannelType(body) ? 'private' : 'group',
566
+ sn: body.sn,
567
+ platform: input.platform,
568
+ }),
569
+ }).catch(error => input.warnReceive(conversation.id, error));
570
+ }
571
+
501
572
  function isPrivateChannelType(body: SatoriEventBody): boolean {
502
573
  const channel = body.channel ?? body.message?.channel;
503
574
  return channel?.type === 1;
@@ -545,7 +616,8 @@ function asRecord(value: unknown): Record<string, unknown> {
545
616
 
546
617
  /**
547
618
  * Satori endpoint 的 EndpointManagement 语义端口(ws / webhook 共用),
548
- * 数据走协议 API:guild.list / channel.list / guild-member.list。
619
+ * 数据走协议 API:guild.list / channel.list / guild.member.list。
620
+ * @see https://satori.chat/zh-CN/resources/member.html
549
621
  */
550
622
  export function createSatoriEndpointManagement(api: SatoriManagementApi): EndpointManagement {
551
623
  return Object.freeze<EndpointManagement>({
@@ -583,14 +655,100 @@ export function createSatoriEndpointManagement(api: SatoriManagementApi): Endpoi
583
655
  return channels;
584
656
  },
585
657
  async listGroupMembers({ groupId, signal }: EndpointGroupRequest): Promise<readonly unknown[]> {
586
- // 平台形状(GuildMember[])原样返回
587
- return listSatoriPages(api, { resource: 'guild-member', params: { guild_id: groupId }, signal });
658
+ // 平台形状(GuildMember[])原样返回,再规范化 1.3 的 GuildRole 角色
659
+ const members = await listSatoriPages(api, { resource: 'guild.member', params: { guild_id: groupId }, signal });
660
+ return members.map(member => withSatoriGuildRoles(member));
661
+ },
662
+ async listFriends(request: EndpointManagementRequest = {}): Promise<readonly EndpointFriend[]> {
663
+ const friends: EndpointFriend[] = [];
664
+ for (const value of await listSatoriPages(api, { resource: 'friend', signal: request.signal })) {
665
+ const friend = readSatoriFriend(value);
666
+ if (!friend?.user?.id) continue;
667
+ const nick = friend.nick ?? '';
668
+ const name = friend.user.name ?? '';
669
+ friends.push({
670
+ user_id: friend.user.id,
671
+ nickname: name || nick || friend.user.id,
672
+ remark: nick,
673
+ });
674
+ }
675
+ return friends;
676
+ },
677
+ async deleteFriend({ userId, signal }: EndpointUserRequest): Promise<void> {
678
+ await api({ resource: 'friend', method: 'delete', params: { user_id: userId }, signal });
679
+ },
680
+ async approveRequest({ requestId, remark, signal }: EndpointApproveRequest): Promise<void> {
681
+ await api({
682
+ resource: 'friend',
683
+ method: 'approve',
684
+ signal,
685
+ params: satoriFriendApproveParams(requestId, true, remark),
686
+ });
687
+ },
688
+ async rejectRequest({ requestId, reason, signal }: EndpointRejectRequest): Promise<void> {
689
+ await api({
690
+ resource: 'friend',
691
+ method: 'approve',
692
+ signal,
693
+ params: satoriFriendApproveParams(requestId, false, reason),
694
+ });
588
695
  },
589
696
  });
590
697
  }
591
698
 
592
699
  export type { CreateSatoriWebSocket, SatoriWsSocket } from './ws.js';
593
700
 
701
+ function emitSatoriFriendRequest(body: SatoriEventBody, input: {
702
+ readonly endpointId: string;
703
+ readonly call: SatoriActionCaller;
704
+ readonly emit: (request: ReturnType<typeof mapSatoriFriendRequest>) => void;
705
+ }): boolean {
706
+ if (body.type !== 'friend-request') return false;
707
+ const request = mapSatoriFriendRequest(body, input.endpointId, {
708
+ approve: (messageId, comment) => input.call({
709
+ resource: 'friend',
710
+ method: 'approve',
711
+ params: satoriFriendApproveParams(messageId, true, comment),
712
+ }).then(() => undefined),
713
+ reject: (messageId, reason) => input.call({
714
+ resource: 'friend',
715
+ method: 'approve',
716
+ params: satoriFriendApproveParams(messageId, false, reason),
717
+ }).then(() => undefined),
718
+ });
719
+ if (request) input.emit(request);
720
+ return true;
721
+ }
722
+
723
+ async function addSatoriReaction(call: SatoriActionCaller, request: EndpointReactionRequest): Promise<string | null> {
724
+ const { channelId, messageId } = parseMessageRef(request.message.id);
725
+ await call({
726
+ resource: 'reaction',
727
+ method: 'create',
728
+ signal: request.signal,
729
+ params: satoriReactionParams('create', {
730
+ channelId: channelId || request.message.conversation.id,
731
+ messageId,
732
+ emojiId: request.emoji,
733
+ }),
734
+ });
735
+ return request.emoji;
736
+ }
737
+
738
+ async function removeSatoriReaction(call: SatoriActionCaller, request: EndpointRemoveReactionRequest): Promise<void> {
739
+ const { channelId, messageId } = parseMessageRef(request.message.id);
740
+ await call({
741
+ resource: 'reaction',
742
+ method: 'delete',
743
+ signal: request.signal,
744
+ params: satoriReactionParams('delete', {
745
+ channelId: channelId || request.message.conversation.id,
746
+ messageId,
747
+ emojiId: request.reactionId,
748
+ }),
749
+ });
750
+ }
751
+
594
752
  /** A transport connection may multiplex logins; this Endpoint admits only its selected account. */
595
753
  function matchesSatoriLogin(config: { readonly platform: string; readonly userId: string }, login: SatoriLogin | undefined): boolean {
596
754
  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,