@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/CHANGELOG.md +154 -0
- package/LICENSE +21 -0
- package/README.md +6 -6
- 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 +194 -66
- 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/lib/webhook.d.ts +8 -5
- package/lib/webhook.js +10 -24
- package/package.json +17 -23
- package/schema.json +0 -56
- package/src/endpoint.ts +221 -63
- package/src/index.ts +16 -0
- package/src/protocol.ts +671 -8
- package/src/webhook.ts +13 -29
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,
|
|
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,
|
|
14
|
+
export async function handleSatoriWebhookRequest(request, handler) {
|
|
17
15
|
try {
|
|
18
16
|
if (!verifySatoriToken(handler.config.token, request)) {
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
45
|
-
"@zhin.js/console-contract": "1.1.
|
|
46
|
-
"@zhin.js/core": "1.
|
|
47
|
-
"@zhin.js/feature-kit": "1.1.
|
|
48
|
-
"@zhin.js/host-http": "1.
|
|
49
|
-
"@zhin.js/im-contract": "1.1.
|
|
50
|
-
"@zhin.js/logger": "1.1.
|
|
51
|
-
"@zhin.js/page": "1.1.
|
|
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.
|
|
60
|
-
"zhin.js": "1.1.
|
|
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.
|
|
64
|
-
"@zhin.js/command": "^1.
|
|
65
|
-
"@zhin.js/core": "^1.
|
|
66
|
-
"zhin.js": "^1.1.
|
|
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": "
|
|
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 (
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
406
|
-
await acceptHttpIngress<SatoriEventBody>(request,
|
|
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 (
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
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
|
-
|
|
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,
|