@fraqjs/fraq 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.mts +5648 -0
- package/dist/index.mjs +777 -0
- package/package.json +28 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,777 @@
|
|
|
1
|
+
import mitt from "mitt";
|
|
2
|
+
//#region src/protocol/client.ts
|
|
3
|
+
var MilkyClientBase = class {
|
|
4
|
+
baseUrl;
|
|
5
|
+
wsBaseUrl;
|
|
6
|
+
accessToken;
|
|
7
|
+
baseHeaders;
|
|
8
|
+
constructor(baseUrl, options) {
|
|
9
|
+
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
10
|
+
this.wsBaseUrl = this.baseUrl.replace(/^http/, "ws");
|
|
11
|
+
this.accessToken = options?.accessToken;
|
|
12
|
+
this.baseHeaders = { "Content-Type": "application/json" };
|
|
13
|
+
if (options?.accessToken) this.baseHeaders.Authorization = `Bearer ${options.accessToken}`;
|
|
14
|
+
}
|
|
15
|
+
async callApi(endpoint, params) {
|
|
16
|
+
const response = await fetch(`${this.baseUrl}/api/${endpoint}`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: JSON.stringify(params ?? {}),
|
|
19
|
+
headers: this.baseHeaders
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) throw new Error(`API call failed with status ${response.status}`);
|
|
22
|
+
const json = await response.json();
|
|
23
|
+
if (json.status === "failed") throw new Error(`API call failed with retcode ${json.retcode}: ${json.message}`);
|
|
24
|
+
return json.data;
|
|
25
|
+
}
|
|
26
|
+
async openEventWebSocket(onEvent) {
|
|
27
|
+
const ws = new WebSocket(`${this.wsBaseUrl}/event${this.accessToken ? `?access_token=${this.accessToken}` : ""}`);
|
|
28
|
+
ws.addEventListener("message", onEvent);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
ws.addEventListener("open", () => resolve(ws));
|
|
31
|
+
ws.addEventListener("error", (event) => reject(/* @__PURE__ */ new Error(`WebSocket error: ${event}`)));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function createMilkyClient(...params) {
|
|
36
|
+
const base = new MilkyClientBase(...params);
|
|
37
|
+
return new Proxy(base, { get(target, endpoint) {
|
|
38
|
+
if (typeof endpoint === "string" && endpoint.includes("_")) return (params) => target.callApi(endpoint, params);
|
|
39
|
+
else return target[endpoint];
|
|
40
|
+
} });
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/routing/tokenizer.ts
|
|
44
|
+
var Tokenizer = class {
|
|
45
|
+
segments;
|
|
46
|
+
offset = 0;
|
|
47
|
+
subOffset = void 0;
|
|
48
|
+
constructor(segments) {
|
|
49
|
+
this.segments = segments;
|
|
50
|
+
}
|
|
51
|
+
getState() {
|
|
52
|
+
return {
|
|
53
|
+
offset: this.offset,
|
|
54
|
+
subOffset: this.subOffset
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
setState(state) {
|
|
58
|
+
this.offset = state.offset;
|
|
59
|
+
this.subOffset = state.subOffset;
|
|
60
|
+
}
|
|
61
|
+
hasNext() {
|
|
62
|
+
return this.findNextPosition() !== void 0;
|
|
63
|
+
}
|
|
64
|
+
peek() {
|
|
65
|
+
const position = this.findNextPosition();
|
|
66
|
+
if (position === void 0) return;
|
|
67
|
+
const segment = this.segments[position.offset];
|
|
68
|
+
if (segment.type === "text") return this.readTextToken(segment.data.text, position.subOffset ?? 0);
|
|
69
|
+
return segment;
|
|
70
|
+
}
|
|
71
|
+
next() {
|
|
72
|
+
const position = this.findNextPosition();
|
|
73
|
+
if (position === void 0) return;
|
|
74
|
+
const segment = this.segments[position.offset];
|
|
75
|
+
if (segment.type !== "text") {
|
|
76
|
+
this.offset = position.offset + 1;
|
|
77
|
+
this.subOffset = void 0;
|
|
78
|
+
return segment;
|
|
79
|
+
}
|
|
80
|
+
const subOffset = position.subOffset ?? 0;
|
|
81
|
+
const token = this.readTextToken(segment.data.text, subOffset);
|
|
82
|
+
const tokenEnd = subOffset + token.length;
|
|
83
|
+
const nextSubOffset = this.findTextTokenStart(segment.data.text, tokenEnd);
|
|
84
|
+
if (nextSubOffset < segment.data.text.length) {
|
|
85
|
+
this.offset = position.offset;
|
|
86
|
+
this.subOffset = nextSubOffset;
|
|
87
|
+
} else {
|
|
88
|
+
this.offset = position.offset + 1;
|
|
89
|
+
this.subOffset = void 0;
|
|
90
|
+
}
|
|
91
|
+
return token;
|
|
92
|
+
}
|
|
93
|
+
isGreedyAvailable() {
|
|
94
|
+
if (this.offset !== this.segments.length - 1) return false;
|
|
95
|
+
const segment = this.segments[this.offset];
|
|
96
|
+
if (segment?.type !== "text") return false;
|
|
97
|
+
return this.findTextTokenStart(segment.data.text, this.subOffset ?? 0) < segment.data.text.length;
|
|
98
|
+
}
|
|
99
|
+
greedy() {
|
|
100
|
+
if (!this.isGreedyAvailable()) throw new Error("Greedy token is not available");
|
|
101
|
+
const segment = this.segments[this.offset];
|
|
102
|
+
if (segment.type !== "text") throw new Error("Greedy token is not available");
|
|
103
|
+
const tokenStart = this.findTextTokenStart(segment.data.text, this.subOffset ?? 0);
|
|
104
|
+
const token = segment.data.text.slice(tokenStart);
|
|
105
|
+
this.offset = this.segments.length;
|
|
106
|
+
this.subOffset = void 0;
|
|
107
|
+
return token;
|
|
108
|
+
}
|
|
109
|
+
findNextPosition() {
|
|
110
|
+
let offset = this.offset;
|
|
111
|
+
let subOffset = this.subOffset;
|
|
112
|
+
while (offset < this.segments.length) {
|
|
113
|
+
const segment = this.segments[offset];
|
|
114
|
+
if (segment.type !== "text") return {
|
|
115
|
+
offset,
|
|
116
|
+
subOffset: void 0
|
|
117
|
+
};
|
|
118
|
+
const tokenStart = this.findTextTokenStart(segment.data.text, subOffset ?? 0);
|
|
119
|
+
if (tokenStart < segment.data.text.length) return {
|
|
120
|
+
offset,
|
|
121
|
+
subOffset: tokenStart
|
|
122
|
+
};
|
|
123
|
+
offset += 1;
|
|
124
|
+
subOffset = void 0;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
findTextTokenStart(text, from) {
|
|
128
|
+
let offset = from;
|
|
129
|
+
while (offset < text.length && text[offset] === " ") offset += 1;
|
|
130
|
+
return offset;
|
|
131
|
+
}
|
|
132
|
+
readTextToken(text, from) {
|
|
133
|
+
let offset = from;
|
|
134
|
+
while (offset < text.length && text[offset] !== " ") offset += 1;
|
|
135
|
+
return text.slice(from, offset);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region src/routing/router.ts
|
|
140
|
+
var Router = class Router {
|
|
141
|
+
entries = [];
|
|
142
|
+
groups = /* @__PURE__ */ new Map();
|
|
143
|
+
command(name, pattern, handler) {
|
|
144
|
+
const command = {
|
|
145
|
+
name,
|
|
146
|
+
pattern,
|
|
147
|
+
handler
|
|
148
|
+
};
|
|
149
|
+
this.entries.push({
|
|
150
|
+
type: "command",
|
|
151
|
+
command
|
|
152
|
+
});
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
rawPattern(pattern, handler) {
|
|
156
|
+
if (Object.keys(pattern).length === 0) throw new Error("Raw pattern must have at least one parameter.");
|
|
157
|
+
if (pattern[Object.keys(pattern)[0]].capturer.typeInstruction.type === "literal") throw new Error("The first parameter of a raw pattern cannot be a literal, as it would conflict with command patterns.");
|
|
158
|
+
const rawPattern = {
|
|
159
|
+
pattern,
|
|
160
|
+
handler
|
|
161
|
+
};
|
|
162
|
+
this.entries.push({
|
|
163
|
+
type: "rawPattern",
|
|
164
|
+
rawPattern
|
|
165
|
+
});
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
group(name) {
|
|
169
|
+
let router = this.groups.get(name);
|
|
170
|
+
if (!router) {
|
|
171
|
+
router = new Router();
|
|
172
|
+
this.groups.set(name, router);
|
|
173
|
+
this.entries.push({
|
|
174
|
+
type: "group",
|
|
175
|
+
name,
|
|
176
|
+
router
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return router;
|
|
180
|
+
}
|
|
181
|
+
filter(predicate) {
|
|
182
|
+
const router = new Router();
|
|
183
|
+
this.entries.push({
|
|
184
|
+
type: "filter",
|
|
185
|
+
predicate,
|
|
186
|
+
router
|
|
187
|
+
});
|
|
188
|
+
return router;
|
|
189
|
+
}
|
|
190
|
+
async dispatch(session, message) {
|
|
191
|
+
const tokenizer = new Tokenizer(message.segments);
|
|
192
|
+
return await this.process(session, tokenizer);
|
|
193
|
+
}
|
|
194
|
+
async process(session, tokenizer) {
|
|
195
|
+
const initialState = tokenizer.getState();
|
|
196
|
+
for (const entry of this.entries) {
|
|
197
|
+
tokenizer.setState(initialState);
|
|
198
|
+
if (await this.processEntry(entry, session, tokenizer)) return true;
|
|
199
|
+
}
|
|
200
|
+
tokenizer.setState(initialState);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
async processEntry(entry, session, tokenizer) {
|
|
204
|
+
switch (entry.type) {
|
|
205
|
+
case "command": return await this.processCommand(entry.command, session, tokenizer);
|
|
206
|
+
case "group": return await this.processGroup(entry.name, entry.router, session, tokenizer);
|
|
207
|
+
case "filter":
|
|
208
|
+
if (entry.predicate(session) !== true) return false;
|
|
209
|
+
return await entry.router.process(session, tokenizer);
|
|
210
|
+
case "rawPattern": return await this.processRawPattern(entry.rawPattern, session, tokenizer);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async processCommand(command, session, tokenizer) {
|
|
214
|
+
const token = tokenizer.peek();
|
|
215
|
+
if (typeof token !== "string" || token !== command.name) return false;
|
|
216
|
+
tokenizer.next();
|
|
217
|
+
const params = this.capturePattern(command.pattern, tokenizer);
|
|
218
|
+
if (params === void 0 || tokenizer.hasNext()) return false;
|
|
219
|
+
await command.handler(session, params);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
async processGroup(name, router, session, tokenizer) {
|
|
223
|
+
const token = tokenizer.peek();
|
|
224
|
+
if (typeof token !== "string" || token !== name) return false;
|
|
225
|
+
tokenizer.next();
|
|
226
|
+
return await router.process(session, tokenizer);
|
|
227
|
+
}
|
|
228
|
+
async processRawPattern(rawPattern, session, tokenizer) {
|
|
229
|
+
const params = this.capturePattern(rawPattern.pattern, tokenizer);
|
|
230
|
+
if (params === void 0 || tokenizer.hasNext()) return false;
|
|
231
|
+
await rawPattern.handler(session, params);
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
capturePattern(pattern, tokenizer) {
|
|
235
|
+
const initialState = tokenizer.getState();
|
|
236
|
+
const params = {};
|
|
237
|
+
for (const [name, parameter] of Object.entries(pattern)) {
|
|
238
|
+
const value = parameter.capturer.capture(tokenizer);
|
|
239
|
+
if (value === void 0) {
|
|
240
|
+
tokenizer.setState(initialState);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
params[name] = value;
|
|
244
|
+
}
|
|
245
|
+
return params;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/core/service.ts
|
|
250
|
+
function getServiceName(service) {
|
|
251
|
+
return service.name || "<anonymous service>";
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/core/context.ts
|
|
255
|
+
const INITIAL_RECONNECT_DELAY_MS = 1e3;
|
|
256
|
+
const MAX_RECONNECT_DELAY_MS = 3e4;
|
|
257
|
+
var Context = class Context {
|
|
258
|
+
client;
|
|
259
|
+
parent;
|
|
260
|
+
router = new Router();
|
|
261
|
+
eventBus = mitt();
|
|
262
|
+
plugins = [];
|
|
263
|
+
services = /* @__PURE__ */ new Map();
|
|
264
|
+
subContexts = [];
|
|
265
|
+
isStarted = false;
|
|
266
|
+
constructor(client, parent, filter) {
|
|
267
|
+
this.client = client;
|
|
268
|
+
this.parent = parent;
|
|
269
|
+
parent?.eventBus.on("*", (type, event) => {
|
|
270
|
+
if (filter) {
|
|
271
|
+
const predicate = filter[type];
|
|
272
|
+
if (predicate && !predicate(event)) return;
|
|
273
|
+
}
|
|
274
|
+
this.eventBus.emit(type, event);
|
|
275
|
+
});
|
|
276
|
+
this.eventBus.on("message_receive", async ({ data: message }) => {
|
|
277
|
+
try {
|
|
278
|
+
this.router.dispatch(this.createSession(message), message);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error("Error routing command:", error);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
on(type, handler) {
|
|
285
|
+
this.eventBus.on(type, async (event) => {
|
|
286
|
+
try {
|
|
287
|
+
await handler(event);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(`Error handling event ${type}:`, error);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
install(plugin, ...args) {
|
|
294
|
+
this.plugins.push({
|
|
295
|
+
plugin,
|
|
296
|
+
args
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
provide(service, instance) {
|
|
300
|
+
if (this.services.has(service)) throw new Error(`Service ${getServiceName(service)} has already been provided in this context.`);
|
|
301
|
+
this.services.set(service, instance);
|
|
302
|
+
}
|
|
303
|
+
resolve(service) {
|
|
304
|
+
const instance = this.tryResolve(service);
|
|
305
|
+
if (instance === void 0) throw new Error(`Service ${getServiceName(service)} has not been provided.`);
|
|
306
|
+
return instance;
|
|
307
|
+
}
|
|
308
|
+
tryResolve(service) {
|
|
309
|
+
if (this.services.has(service)) return this.services.get(service);
|
|
310
|
+
return this.parent?.tryResolve(service);
|
|
311
|
+
}
|
|
312
|
+
isProvided(service) {
|
|
313
|
+
return this.tryResolve(service) !== void 0;
|
|
314
|
+
}
|
|
315
|
+
fork(filter) {
|
|
316
|
+
const subContext = new Context(this.client, this, filter);
|
|
317
|
+
this.subContexts.push(subContext);
|
|
318
|
+
return subContext;
|
|
319
|
+
}
|
|
320
|
+
async start() {
|
|
321
|
+
if (this.isStarted) return;
|
|
322
|
+
await this.applyPlugins();
|
|
323
|
+
for (const subContext of this.subContexts) await subContext.start();
|
|
324
|
+
this.isStarted = true;
|
|
325
|
+
if (!this.parent) this.connectEventWebSocket();
|
|
326
|
+
}
|
|
327
|
+
async applyPlugins() {
|
|
328
|
+
for (const { plugin, args } of this.sortPlugins()) {
|
|
329
|
+
const providedBeforeApply = new Set(this.services.keys());
|
|
330
|
+
await plugin.apply(this, ...args);
|
|
331
|
+
for (const service of plugin.provides ?? []) if (!this.services.has(service) || providedBeforeApply.has(service)) throw new Error(`${this.describePlugin(plugin)} declares service ${getServiceName(service)} but did not provide it.`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
sortPlugins() {
|
|
335
|
+
this.validateUniqueProvidedServices();
|
|
336
|
+
const pending = [...this.plugins];
|
|
337
|
+
const sorted = [];
|
|
338
|
+
const available = /* @__PURE__ */ new Set();
|
|
339
|
+
for (const service of this.collectAvailableServices()) available.add(service);
|
|
340
|
+
while (pending.length > 0) {
|
|
341
|
+
const nextIndex = pending.findIndex(({ plugin }) => this.areRequirementsAvailable(plugin, available));
|
|
342
|
+
if (nextIndex === -1) throw this.createUnresolvablePluginError(pending, available);
|
|
343
|
+
const [next] = pending.splice(nextIndex, 1);
|
|
344
|
+
sorted.push(next);
|
|
345
|
+
for (const service of next.plugin.provides ?? []) available.add(service);
|
|
346
|
+
}
|
|
347
|
+
return sorted;
|
|
348
|
+
}
|
|
349
|
+
validateUniqueProvidedServices() {
|
|
350
|
+
const providers = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const { plugin } of this.plugins) for (const service of plugin.provides ?? []) {
|
|
352
|
+
const existingProvider = providers.get(service);
|
|
353
|
+
if (existingProvider) throw new Error(`Service ${getServiceName(service)} is declared by multiple plugins: ${this.describePlugin(existingProvider)} and ${this.describePlugin(plugin)}.`);
|
|
354
|
+
providers.set(service, plugin);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
collectAvailableServices() {
|
|
358
|
+
const services = [...this.services.keys()];
|
|
359
|
+
if (this.parent) services.push(...this.parent.collectAvailableServices());
|
|
360
|
+
return services;
|
|
361
|
+
}
|
|
362
|
+
areRequirementsAvailable(plugin, available) {
|
|
363
|
+
return (plugin.requires ?? []).every((service) => available.has(service));
|
|
364
|
+
}
|
|
365
|
+
createUnresolvablePluginError(pending, available) {
|
|
366
|
+
const missingRequirements = /* @__PURE__ */ new Map();
|
|
367
|
+
const pendingProviders = /* @__PURE__ */ new Set();
|
|
368
|
+
for (const { plugin } of pending) for (const service of plugin.provides ?? []) pendingProviders.add(service);
|
|
369
|
+
for (const { plugin } of pending) for (const service of plugin.requires ?? []) if (!available.has(service)) {
|
|
370
|
+
const plugins = missingRequirements.get(service) ?? [];
|
|
371
|
+
plugins.push(plugin);
|
|
372
|
+
missingRequirements.set(service, plugins);
|
|
373
|
+
}
|
|
374
|
+
const lines = [...missingRequirements].map(([service, plugins]) => {
|
|
375
|
+
const dependents = plugins.map((plugin) => this.describePlugin(plugin)).join(", ");
|
|
376
|
+
const reason = pendingProviders.has(service) ? "blocked by a dependency cycle" : "no installed plugin provides it";
|
|
377
|
+
return `${getServiceName(service)} required by ${dependents} (${reason})`;
|
|
378
|
+
});
|
|
379
|
+
return /* @__PURE__ */ new Error(`Unable to resolve plugin service dependencies: ${lines.join("; ")}.`);
|
|
380
|
+
}
|
|
381
|
+
describePlugin(plugin) {
|
|
382
|
+
const pluginIndex = this.plugins.findIndex((installed) => installed.plugin === plugin);
|
|
383
|
+
return plugin.constructor.name && plugin.constructor.name !== "Object" ? plugin.constructor.name : `plugin #${pluginIndex + 1}`;
|
|
384
|
+
}
|
|
385
|
+
async connectEventWebSocket() {
|
|
386
|
+
let reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
387
|
+
while (this.isStarted) {
|
|
388
|
+
try {
|
|
389
|
+
const ws = await this.client.openEventWebSocket((event) => this.handleWebSocketMessage(event));
|
|
390
|
+
reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
391
|
+
await new Promise((resolve) => {
|
|
392
|
+
ws.addEventListener("close", () => resolve(), { once: true });
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.error("Error connecting event WebSocket:", error);
|
|
396
|
+
}
|
|
397
|
+
await this.delay(reconnectDelay);
|
|
398
|
+
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
handleWebSocketMessage(event) {
|
|
402
|
+
try {
|
|
403
|
+
if (typeof event.data !== "string") throw new Error(`Expected text frame, got ${typeof event.data}`);
|
|
404
|
+
const payload = JSON.parse(event.data);
|
|
405
|
+
this.eventBus.emit(payload.event_type, payload);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.error("Error handling WebSocket event:", error);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
delay(ms) {
|
|
411
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
412
|
+
}
|
|
413
|
+
createSession(message) {
|
|
414
|
+
return {
|
|
415
|
+
raw: message,
|
|
416
|
+
reply: async (segments) => {
|
|
417
|
+
try {
|
|
418
|
+
switch (message.message_scene) {
|
|
419
|
+
case "friend":
|
|
420
|
+
await this.client.send_private_message({
|
|
421
|
+
user_id: message.peer_id,
|
|
422
|
+
message: segments
|
|
423
|
+
});
|
|
424
|
+
break;
|
|
425
|
+
case "group":
|
|
426
|
+
await this.client.send_group_message({
|
|
427
|
+
group_id: message.peer_id,
|
|
428
|
+
message: segments
|
|
429
|
+
});
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.error("Error sending reply:", error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
static create(options) {
|
|
439
|
+
return new Context(createMilkyClient(options.connect.baseUrl, { accessToken: options.connect.accessToken }));
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/core/filter.ts
|
|
444
|
+
const eventKeys = [
|
|
445
|
+
"bot_offline",
|
|
446
|
+
"message_receive",
|
|
447
|
+
"message_recall",
|
|
448
|
+
"peer_pin_change",
|
|
449
|
+
"friend_request",
|
|
450
|
+
"group_join_request",
|
|
451
|
+
"group_invited_join_request",
|
|
452
|
+
"group_invitation",
|
|
453
|
+
"friend_nudge",
|
|
454
|
+
"friend_file_upload",
|
|
455
|
+
"group_admin_change",
|
|
456
|
+
"group_essence_message_change",
|
|
457
|
+
"group_member_increase",
|
|
458
|
+
"group_member_decrease",
|
|
459
|
+
"group_disband",
|
|
460
|
+
"group_name_change",
|
|
461
|
+
"group_message_reaction",
|
|
462
|
+
"group_mute",
|
|
463
|
+
"group_whole_mute",
|
|
464
|
+
"group_nudge",
|
|
465
|
+
"group_file_upload"
|
|
466
|
+
];
|
|
467
|
+
let filter;
|
|
468
|
+
(function(_filter) {
|
|
469
|
+
function define(filter) {
|
|
470
|
+
return filter;
|
|
471
|
+
}
|
|
472
|
+
_filter.define = define;
|
|
473
|
+
function allPass() {
|
|
474
|
+
const result = {};
|
|
475
|
+
for (const key of eventKeys) result[key] = () => true;
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
_filter.allPass = allPass;
|
|
479
|
+
function friend(uinList) {
|
|
480
|
+
const uinSet = new Set(uinList);
|
|
481
|
+
return {
|
|
482
|
+
message_receive: ({ data }) => data.message_scene === "friend" && uinSet.has(data.peer_id),
|
|
483
|
+
message_recall: ({ data }) => data.message_scene === "friend" && uinSet.has(data.peer_id),
|
|
484
|
+
peer_pin_change: ({ data }) => data.message_scene === "friend" && uinSet.has(data.peer_id),
|
|
485
|
+
group_invitation: ({ data }) => uinSet.has(data.initiator_id),
|
|
486
|
+
friend_nudge: ({ data }) => uinSet.has(data.user_id),
|
|
487
|
+
friend_file_upload: ({ data }) => uinSet.has(data.user_id)
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
_filter.friend = friend;
|
|
491
|
+
function group(uinList) {
|
|
492
|
+
const uinSet = new Set(uinList);
|
|
493
|
+
return {
|
|
494
|
+
message_receive: ({ data }) => data.message_scene === "group" && uinSet.has(data.peer_id),
|
|
495
|
+
message_recall: ({ data }) => data.message_scene === "group" && uinSet.has(data.peer_id),
|
|
496
|
+
peer_pin_change: ({ data }) => data.message_scene === "group" && uinSet.has(data.peer_id),
|
|
497
|
+
group_join_request: ({ data }) => uinSet.has(data.group_id),
|
|
498
|
+
group_invited_join_request: ({ data }) => uinSet.has(data.group_id),
|
|
499
|
+
group_admin_change: ({ data }) => uinSet.has(data.group_id),
|
|
500
|
+
group_essence_message_change: ({ data }) => uinSet.has(data.group_id),
|
|
501
|
+
group_member_increase: ({ data }) => uinSet.has(data.group_id),
|
|
502
|
+
group_member_decrease: ({ data }) => uinSet.has(data.group_id),
|
|
503
|
+
group_name_change: ({ data }) => uinSet.has(data.group_id),
|
|
504
|
+
group_message_reaction: ({ data }) => uinSet.has(data.group_id),
|
|
505
|
+
group_mute: ({ data }) => uinSet.has(data.group_id),
|
|
506
|
+
group_whole_mute: ({ data }) => uinSet.has(data.group_id),
|
|
507
|
+
group_nudge: ({ data }) => uinSet.has(data.group_id),
|
|
508
|
+
group_file_upload: ({ data }) => uinSet.has(data.group_id)
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
_filter.group = group;
|
|
512
|
+
function admin() {
|
|
513
|
+
return { message_receive: ({ data }) => data.message_scene === "group" && data.group_member.role !== "member" };
|
|
514
|
+
}
|
|
515
|
+
_filter.admin = admin;
|
|
516
|
+
function or(...filters) {
|
|
517
|
+
const result = {};
|
|
518
|
+
for (const key of eventKeys) result[key] = (event) => {
|
|
519
|
+
for (const filter of filters) {
|
|
520
|
+
const predicate = filter[key];
|
|
521
|
+
if (predicate?.(event)) return true;
|
|
522
|
+
}
|
|
523
|
+
return false;
|
|
524
|
+
};
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
527
|
+
_filter.or = or;
|
|
528
|
+
function and(...filters) {
|
|
529
|
+
const result = {};
|
|
530
|
+
for (const key of eventKeys) result[key] = (event) => {
|
|
531
|
+
for (const filter of filters) {
|
|
532
|
+
const predicate = filter[key];
|
|
533
|
+
if (!predicate?.(event)) return false;
|
|
534
|
+
}
|
|
535
|
+
return true;
|
|
536
|
+
};
|
|
537
|
+
return result;
|
|
538
|
+
}
|
|
539
|
+
_filter.and = and;
|
|
540
|
+
})(filter || (filter = {}));
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/core/plugin.ts
|
|
543
|
+
function definePlugin(plugin) {
|
|
544
|
+
return plugin;
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/protocol/segment.ts
|
|
548
|
+
function msg(strings, ...values) {
|
|
549
|
+
let buffer = "";
|
|
550
|
+
const segments = [];
|
|
551
|
+
for (let i = 0; i < strings.length; i++) {
|
|
552
|
+
buffer += strings[i];
|
|
553
|
+
if (i < values.length) {
|
|
554
|
+
const value = values[i];
|
|
555
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") buffer += value.toString();
|
|
556
|
+
else {
|
|
557
|
+
if (buffer) {
|
|
558
|
+
segments.push({
|
|
559
|
+
type: "text",
|
|
560
|
+
data: { text: buffer }
|
|
561
|
+
});
|
|
562
|
+
buffer = "";
|
|
563
|
+
}
|
|
564
|
+
segments.push(value);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (buffer) segments.push({
|
|
569
|
+
type: "text",
|
|
570
|
+
data: { text: buffer }
|
|
571
|
+
});
|
|
572
|
+
return segments;
|
|
573
|
+
}
|
|
574
|
+
let seg;
|
|
575
|
+
(function(_seg) {
|
|
576
|
+
function mention(userId) {
|
|
577
|
+
return {
|
|
578
|
+
type: "mention",
|
|
579
|
+
data: { user_id: userId }
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
_seg.mention = mention;
|
|
583
|
+
function mentionAll() {
|
|
584
|
+
return {
|
|
585
|
+
type: "mention_all",
|
|
586
|
+
data: {}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
_seg.mentionAll = mentionAll;
|
|
590
|
+
function face(faceId, options) {
|
|
591
|
+
return {
|
|
592
|
+
type: "face",
|
|
593
|
+
data: {
|
|
594
|
+
face_id: faceId.toString(),
|
|
595
|
+
is_large: options?.isLarge ?? false
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
_seg.face = face;
|
|
600
|
+
function reply(messageSeq) {
|
|
601
|
+
return {
|
|
602
|
+
type: "reply",
|
|
603
|
+
data: { message_seq: messageSeq }
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
_seg.reply = reply;
|
|
607
|
+
function image(uri, options) {
|
|
608
|
+
return {
|
|
609
|
+
type: "image",
|
|
610
|
+
data: {
|
|
611
|
+
uri,
|
|
612
|
+
sub_type: options?.subType,
|
|
613
|
+
summary: options?.summary
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
_seg.image = image;
|
|
618
|
+
function record(uri) {
|
|
619
|
+
return {
|
|
620
|
+
type: "record",
|
|
621
|
+
data: { uri }
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
_seg.record = record;
|
|
625
|
+
function video(uri, options) {
|
|
626
|
+
return {
|
|
627
|
+
type: "video",
|
|
628
|
+
data: {
|
|
629
|
+
uri,
|
|
630
|
+
thumb_uri: options?.thumbUri
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
_seg.video = video;
|
|
635
|
+
function forward(messages, options) {
|
|
636
|
+
return {
|
|
637
|
+
type: "forward",
|
|
638
|
+
data: {
|
|
639
|
+
messages,
|
|
640
|
+
title: options?.title,
|
|
641
|
+
preview: options?.preview,
|
|
642
|
+
summary: options?.summary,
|
|
643
|
+
prompt: options?.prompt
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
_seg.forward = forward;
|
|
648
|
+
})(seg || (seg = {}));
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/protocol/types.ts
|
|
651
|
+
const milkyVersion = "1.3";
|
|
652
|
+
const milkyPackageVersion = "1.3.0-rc.1";
|
|
653
|
+
//#endregion
|
|
654
|
+
//#region src/routing/capturer.ts
|
|
655
|
+
let capturer;
|
|
656
|
+
(function(_capturer) {
|
|
657
|
+
function literal(literal) {
|
|
658
|
+
return {
|
|
659
|
+
typeInstruction: {
|
|
660
|
+
type: "literal",
|
|
661
|
+
literal
|
|
662
|
+
},
|
|
663
|
+
capture(tokenizer) {
|
|
664
|
+
const token = tokenizer.peek();
|
|
665
|
+
if (typeof token === "string" && token === literal) {
|
|
666
|
+
tokenizer.next();
|
|
667
|
+
return token;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
_capturer.literal = literal;
|
|
673
|
+
_capturer.num = {
|
|
674
|
+
typeInstruction: { type: "number" },
|
|
675
|
+
capture(tokenizer) {
|
|
676
|
+
const token = tokenizer.peek();
|
|
677
|
+
if (typeof token === "string") {
|
|
678
|
+
const parsed = Number(token);
|
|
679
|
+
if (!Number.isNaN(parsed)) {
|
|
680
|
+
tokenizer.next();
|
|
681
|
+
return parsed;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
_capturer.str = {
|
|
687
|
+
typeInstruction: { type: "string" },
|
|
688
|
+
capture(tokenizer) {
|
|
689
|
+
const token = tokenizer.peek();
|
|
690
|
+
if (typeof token === "string") {
|
|
691
|
+
tokenizer.next();
|
|
692
|
+
return token;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
_capturer.greedy = {
|
|
697
|
+
typeInstruction: { type: "greedy" },
|
|
698
|
+
capture(tokenizer) {
|
|
699
|
+
if (tokenizer.isGreedyAvailable()) return tokenizer.greedy();
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
function union(...members) {
|
|
703
|
+
const literalSet = new Set(members);
|
|
704
|
+
return {
|
|
705
|
+
typeInstruction: {
|
|
706
|
+
type: "union",
|
|
707
|
+
members
|
|
708
|
+
},
|
|
709
|
+
capture(tokenizer) {
|
|
710
|
+
const token = tokenizer.peek();
|
|
711
|
+
if (typeof token === "string" && literalSet.has(token)) {
|
|
712
|
+
tokenizer.next();
|
|
713
|
+
return token;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
_capturer.union = union;
|
|
719
|
+
function segment(type) {
|
|
720
|
+
return {
|
|
721
|
+
typeInstruction: {
|
|
722
|
+
type: "segment",
|
|
723
|
+
segmentType: type
|
|
724
|
+
},
|
|
725
|
+
capture(tokenizer) {
|
|
726
|
+
const token = tokenizer.peek();
|
|
727
|
+
if (typeof token === "object" && token !== null && token.type === type) {
|
|
728
|
+
tokenizer.next();
|
|
729
|
+
return token;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
_capturer.segment = segment;
|
|
735
|
+
})(capturer || (capturer = {}));
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region src/routing/parameter.ts
|
|
738
|
+
var Parameter = class {
|
|
739
|
+
capturer;
|
|
740
|
+
description;
|
|
741
|
+
constructor(capturer) {
|
|
742
|
+
this.capturer = capturer;
|
|
743
|
+
}
|
|
744
|
+
describe(description) {
|
|
745
|
+
this.description = description;
|
|
746
|
+
return this;
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
let param;
|
|
750
|
+
(function(_param) {
|
|
751
|
+
function literal(literal) {
|
|
752
|
+
return new Parameter(capturer.literal(literal));
|
|
753
|
+
}
|
|
754
|
+
_param.literal = literal;
|
|
755
|
+
function num() {
|
|
756
|
+
return new Parameter(capturer.num);
|
|
757
|
+
}
|
|
758
|
+
_param.num = num;
|
|
759
|
+
function str() {
|
|
760
|
+
return new Parameter(capturer.str);
|
|
761
|
+
}
|
|
762
|
+
_param.str = str;
|
|
763
|
+
function greedy() {
|
|
764
|
+
return new Parameter(capturer.greedy);
|
|
765
|
+
}
|
|
766
|
+
_param.greedy = greedy;
|
|
767
|
+
function union(...literals) {
|
|
768
|
+
return new Parameter(capturer.union(...literals));
|
|
769
|
+
}
|
|
770
|
+
_param.union = union;
|
|
771
|
+
function segment(type) {
|
|
772
|
+
return new Parameter(capturer.segment(type));
|
|
773
|
+
}
|
|
774
|
+
_param.segment = segment;
|
|
775
|
+
})(param || (param = {}));
|
|
776
|
+
//#endregion
|
|
777
|
+
export { Context, Parameter, Router, createMilkyClient, definePlugin, filter, milkyPackageVersion, milkyVersion, msg, param, seg };
|