koishi-plugin-dynamic-bot 0.0.0-dev
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/AGENTS.md +6 -0
- package/LICENSE +201 -0
- package/package.json +33 -0
- package/src/dbk/avatar.ts +58 -0
- package/src/dbk/bots.ts +261 -0
- package/src/dbk/codec.ts +55 -0
- package/src/dbk/error.ts +11 -0
- package/src/dbk/gateway.ts +155 -0
- package/src/dbk/get-target.ts +275 -0
- package/src/dbk/handlers.ts +29 -0
- package/src/dbk/list-targets.ts +459 -0
- package/src/dbk/protocol.ts +100 -0
- package/src/dbk/recall.ts +222 -0
- package/src/dbk/send.ts +398 -0
- package/src/dbk/session.ts +239 -0
- package/src/dbk/socket.ts +52 -0
- package/src/dbk/version.ts +65 -0
- package/src/gen/dbk/v1/common_pb.ts +238 -0
- package/src/gen/dbk/v1/frame_pb.ts +114 -0
- package/src/gen/dbk/v1/rpc_pb.ts +868 -0
- package/src/index.ts +104 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { create } from "@bufbuild/protobuf";
|
|
2
|
+
import type { Bot, Context } from "koishi";
|
|
3
|
+
import { ErrorCode, TargetKind } from "../gen/dbk/v1/common_pb";
|
|
4
|
+
import {
|
|
5
|
+
ListTargetsResponseSchema,
|
|
6
|
+
type ListTargetsRequest,
|
|
7
|
+
type ListTargetsResponse,
|
|
8
|
+
} from "../gen/dbk/v1/rpc_pb";
|
|
9
|
+
import { pickAvatar } from "./avatar";
|
|
10
|
+
import { botTargetKinds, canListDirectUsers, hasGuildSurface, hasNestedChannels } from "./bots";
|
|
11
|
+
import { DbkRpcError } from "./error";
|
|
12
|
+
|
|
13
|
+
/** TEXT is 0 in both Koishi and Satori. CATEGORY is 2 in both. DIRECT/VOICE swapped: old Koishi DIRECT=3 VOICE=1; current Satori DIRECT=1 VOICE=3. */
|
|
14
|
+
const CHANNEL_TEXT = 0;
|
|
15
|
+
const CHANNEL_CATEGORY = 2;
|
|
16
|
+
const CHANNEL_DIRECT_OR_VOICE = new Set([1, 3]);
|
|
17
|
+
/** Discord thread types if an adapter leaks native type numbers. */
|
|
18
|
+
const DISCORD_THREAD_TYPES = new Set([10, 11, 12]);
|
|
19
|
+
|
|
20
|
+
const MAX_LIST_PAGES = 64;
|
|
21
|
+
const MAX_LIST_ITEMS = 10_000;
|
|
22
|
+
|
|
23
|
+
interface GuildLike {
|
|
24
|
+
id?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
avatar?: string;
|
|
27
|
+
type?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ChannelLike {
|
|
31
|
+
id?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
type?: number;
|
|
34
|
+
parentId?: string;
|
|
35
|
+
parent_id?: string;
|
|
36
|
+
avatar?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ListedTarget {
|
|
40
|
+
kind: TargetKind;
|
|
41
|
+
id: string;
|
|
42
|
+
guildId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
guildName: string;
|
|
45
|
+
avatar: string;
|
|
46
|
+
botKeys: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PageResult<T> {
|
|
50
|
+
items: T[];
|
|
51
|
+
truncated: boolean;
|
|
52
|
+
unsupported: boolean;
|
|
53
|
+
failed: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function listTargets(ctx: Context, request: ListTargetsRequest): Promise<ListTargetsResponse> {
|
|
57
|
+
const bots = resolveBots(ctx, request.botKey);
|
|
58
|
+
const kindFilter = request.kind ?? TargetKind.UNSPECIFIED;
|
|
59
|
+
const merged = new Map<string, ListedTarget>();
|
|
60
|
+
let incomplete = false;
|
|
61
|
+
|
|
62
|
+
for (const bot of bots) {
|
|
63
|
+
if (isQq(bot)) continue;
|
|
64
|
+
const listed = await listTargetsForBot(ctx, bot, kindFilter);
|
|
65
|
+
incomplete = incomplete || listed.incomplete;
|
|
66
|
+
for (const target of listed.targets) {
|
|
67
|
+
mergeTarget(merged, target);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return create(ListTargetsResponseSchema, {
|
|
72
|
+
targets: [...merged.values()].map((target) => ({
|
|
73
|
+
target: { kind: target.kind, id: target.id, guildId: target.guildId },
|
|
74
|
+
name: target.name,
|
|
75
|
+
guildId: target.guildId,
|
|
76
|
+
guildName: target.guildName,
|
|
77
|
+
avatar: target.avatar,
|
|
78
|
+
botKeys: target.botKeys,
|
|
79
|
+
})),
|
|
80
|
+
incomplete,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveBots(ctx: Context, botKey: string | undefined): Bot[] {
|
|
85
|
+
const key = botKey?.trim() ?? "";
|
|
86
|
+
const visible = ctx.bots.filter((bot) => !bot.hidden);
|
|
87
|
+
if (!key) return visible;
|
|
88
|
+
const found = findBot(ctx, key);
|
|
89
|
+
if (!found) {
|
|
90
|
+
throw new DbkRpcError(ErrorCode.NOT_FOUND, `bot not found: ${key}`);
|
|
91
|
+
}
|
|
92
|
+
return [found];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findBot(ctx: Context, botKey: string): Bot | undefined {
|
|
96
|
+
return ctx.bots.find((bot) => !bot.hidden && botKeyOf(bot) === botKey);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function botKeyOf(bot: Bot): string {
|
|
100
|
+
const platform = bot.platform ?? "";
|
|
101
|
+
const selfId = bot.selfId ?? "";
|
|
102
|
+
return platform && selfId ? `${platform}:${selfId}` : "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isQq(bot: Bot): boolean {
|
|
106
|
+
return (bot.platform ?? "").toLowerCase() === "qq";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function wantsKind(filter: TargetKind, kind: TargetKind): boolean {
|
|
110
|
+
return filter === TargetKind.UNSPECIFIED || filter === kind;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function listTargetsForBot(
|
|
114
|
+
ctx: Context,
|
|
115
|
+
bot: Bot,
|
|
116
|
+
kindFilter: TargetKind,
|
|
117
|
+
): Promise<{ targets: ListedTarget[]; incomplete: boolean }> {
|
|
118
|
+
const botKey = botKeyOf(bot);
|
|
119
|
+
const targets: ListedTarget[] = [];
|
|
120
|
+
const nested = hasNestedChannels(bot);
|
|
121
|
+
const supported = botTargetKinds(bot);
|
|
122
|
+
if (kindFilter !== TargetKind.UNSPECIFIED && supported.length > 0 && !supported.includes(kindFilter)) {
|
|
123
|
+
return { targets, incomplete: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let incomplete = hasGuildSurface(bot) && !nested;
|
|
127
|
+
const wantGuilds =
|
|
128
|
+
wantsKind(kindFilter, TargetKind.GROUP) ||
|
|
129
|
+
wantsKind(kindFilter, TargetKind.CHANNEL) ||
|
|
130
|
+
wantsKind(kindFilter, TargetKind.THREAD);
|
|
131
|
+
const wantUsers = wantsKind(kindFilter, TargetKind.USER);
|
|
132
|
+
|
|
133
|
+
if (wantGuilds) {
|
|
134
|
+
const guilds = await paginate<GuildLike>((next) => callBotList(bot, "getGuildList", next));
|
|
135
|
+
if (guilds.unsupported || guilds.failed || guilds.truncated) incomplete = true;
|
|
136
|
+
if (guilds.failed && !guilds.unsupported) {
|
|
137
|
+
ctx.logger?.debug("targets.list: %s getGuildList failed", botKey);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let channelMode: "unknown" | "nested" | "flat" = nested ? "unknown" : "flat";
|
|
141
|
+
|
|
142
|
+
for (const guild of guilds.items) {
|
|
143
|
+
const guildId = String(guild.id ?? "").trim();
|
|
144
|
+
if (!guildId) continue;
|
|
145
|
+
const guildName = String(guild.name ?? "").trim();
|
|
146
|
+
|
|
147
|
+
if (channelMode === "flat") {
|
|
148
|
+
pushTarget(targets, kindFilter, {
|
|
149
|
+
kind: kindFromListedGuild(guild),
|
|
150
|
+
id: guildId,
|
|
151
|
+
guildId: "",
|
|
152
|
+
name: guildName || guildId,
|
|
153
|
+
guildName: "",
|
|
154
|
+
avatar: pickAvatar(guild),
|
|
155
|
+
botKeys: [botKey],
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const channels = await paginate<ChannelLike>((next) => callBotList(bot, "getChannelList", next, guildId));
|
|
161
|
+
if (channels.truncated) incomplete = true;
|
|
162
|
+
if (channels.unsupported) {
|
|
163
|
+
channelMode = "flat";
|
|
164
|
+
pushTarget(targets, kindFilter, {
|
|
165
|
+
kind: kindFromListedGuild(guild),
|
|
166
|
+
id: guildId,
|
|
167
|
+
guildId: "",
|
|
168
|
+
name: guildName || guildId,
|
|
169
|
+
guildName: "",
|
|
170
|
+
avatar: pickAvatar(guild),
|
|
171
|
+
botKeys: [botKey],
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (channels.failed) {
|
|
176
|
+
incomplete = true;
|
|
177
|
+
ctx.logger?.debug("targets.list: %s getChannelList failed guild=%s", botKey, guildId);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
channelMode = "nested";
|
|
181
|
+
const classified = classifyGuildChannels(channels.items, guildId, guildName, pickAvatar(guild), botKey);
|
|
182
|
+
for (const target of classified) {
|
|
183
|
+
pushTarget(targets, kindFilter, target);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (wantUsers) {
|
|
189
|
+
if (canListDirectUsers(bot)) {
|
|
190
|
+
const friends = await listDirectUsers(bot);
|
|
191
|
+
if (friends.truncated) incomplete = true;
|
|
192
|
+
if (friends.failed && !friends.unsupported) {
|
|
193
|
+
incomplete = true;
|
|
194
|
+
ctx.logger?.debug("targets.list: %s friend/user list failed", botKey);
|
|
195
|
+
}
|
|
196
|
+
for (const user of friends.items) {
|
|
197
|
+
pushTarget(targets, kindFilter, {
|
|
198
|
+
kind: TargetKind.USER,
|
|
199
|
+
id: user.id,
|
|
200
|
+
guildId: "",
|
|
201
|
+
name: user.name,
|
|
202
|
+
guildName: "",
|
|
203
|
+
avatar: user.avatar,
|
|
204
|
+
botKeys: [botKey],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
} else if (supported.includes(TargetKind.USER) || supported.length === 0) {
|
|
208
|
+
incomplete = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { targets, incomplete };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function classifyGuildChannels(
|
|
216
|
+
channels: ChannelLike[],
|
|
217
|
+
guildId: string,
|
|
218
|
+
guildName: string,
|
|
219
|
+
guildAvatar: string,
|
|
220
|
+
botKey: string,
|
|
221
|
+
): ListedTarget[] {
|
|
222
|
+
const byId = new Map<string, ChannelLike>();
|
|
223
|
+
for (const channel of channels) {
|
|
224
|
+
const id = String(channel.id ?? "").trim();
|
|
225
|
+
if (id) byId.set(id, channel);
|
|
226
|
+
}
|
|
227
|
+
const out: ListedTarget[] = [];
|
|
228
|
+
for (const channel of channels) {
|
|
229
|
+
const id = String(channel.id ?? "").trim();
|
|
230
|
+
if (!id) continue;
|
|
231
|
+
const kind = classifyChannel(channel, byId);
|
|
232
|
+
if (kind == null) continue;
|
|
233
|
+
const name = String(channel.name ?? "").trim() || id;
|
|
234
|
+
out.push({
|
|
235
|
+
kind,
|
|
236
|
+
id,
|
|
237
|
+
guildId,
|
|
238
|
+
name,
|
|
239
|
+
guildName,
|
|
240
|
+
avatar: pickAvatar(channel) || guildAvatar,
|
|
241
|
+
botKeys: [botKey],
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function classifyChannel(channel: ChannelLike, byId: Map<string, ChannelLike>): TargetKind | null {
|
|
248
|
+
const type = channel.type;
|
|
249
|
+
if (type === CHANNEL_CATEGORY || (type != null && CHANNEL_DIRECT_OR_VOICE.has(type))) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
if (type != null && DISCORD_THREAD_TYPES.has(type)) {
|
|
253
|
+
return TargetKind.THREAD;
|
|
254
|
+
}
|
|
255
|
+
if (type != null && type !== CHANNEL_TEXT) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const parentId = String(channel.parentId ?? channel.parent_id ?? "").trim();
|
|
259
|
+
if (parentId) {
|
|
260
|
+
const parent = byId.get(parentId);
|
|
261
|
+
if (parent && isTextLikeChannel(parent)) {
|
|
262
|
+
return TargetKind.THREAD;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return TargetKind.CHANNEL;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function kindFromListedGuild(guild: GuildLike): TargetKind {
|
|
269
|
+
const chatType = guild.type;
|
|
270
|
+
if (chatType === "private") return TargetKind.USER;
|
|
271
|
+
if (chatType === "channel") return TargetKind.CHANNEL;
|
|
272
|
+
if (chatType === "group" || chatType === "supergroup") return TargetKind.GROUP;
|
|
273
|
+
return TargetKind.GROUP;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isTextLikeChannel(channel: ChannelLike): boolean {
|
|
277
|
+
const type = channel.type;
|
|
278
|
+
if (type === CHANNEL_CATEGORY || (type != null && CHANNEL_DIRECT_OR_VOICE.has(type))) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
return type == null || type === CHANNEL_TEXT || DISCORD_THREAD_TYPES.has(type);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function listDirectUsers(bot: Bot): Promise<PageResult<{ id: string; name: string; avatar: string }>> {
|
|
285
|
+
const collected: { id: string; name: string; avatar: string }[] = [];
|
|
286
|
+
let truncated = false;
|
|
287
|
+
let sawSupported = false;
|
|
288
|
+
let sawFailure = false;
|
|
289
|
+
let sawSuccess = false;
|
|
290
|
+
|
|
291
|
+
for (const method of ["getFriendList", "getUserList"] as const) {
|
|
292
|
+
const page = await paginate<unknown>((next) => callBotList(bot, method, next));
|
|
293
|
+
if (page.unsupported) continue;
|
|
294
|
+
sawSupported = true;
|
|
295
|
+
truncated = truncated || page.truncated;
|
|
296
|
+
if (page.failed) sawFailure = true;
|
|
297
|
+
else sawSuccess = true;
|
|
298
|
+
for (const item of page.items) {
|
|
299
|
+
const user = userFromListItem(item);
|
|
300
|
+
if (user) collected.push(user);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
items: collected,
|
|
306
|
+
truncated,
|
|
307
|
+
unsupported: !sawSupported,
|
|
308
|
+
failed: sawSupported && sawFailure && !sawSuccess,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function userFromListItem(item: unknown): { id: string; name: string; avatar: string } | undefined {
|
|
313
|
+
if (!item || typeof item !== "object") return undefined;
|
|
314
|
+
const rec = item as Record<string, unknown>;
|
|
315
|
+
const nested = rec.user && typeof rec.user === "object" ? (rec.user as Record<string, unknown>) : rec;
|
|
316
|
+
const id = String(nested.id ?? rec.id ?? rec.userId ?? rec.user_id ?? "").trim();
|
|
317
|
+
if (!id) return undefined;
|
|
318
|
+
const name = String(nested.name ?? nested.nick ?? rec.nick ?? rec.name ?? "").trim();
|
|
319
|
+
return { id, name: name || id, avatar: pickAvatar(nested, rec) };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function pushTarget(targets: ListedTarget[], kindFilter: TargetKind, target: ListedTarget): void {
|
|
323
|
+
if (!wantsKind(kindFilter, target.kind)) return;
|
|
324
|
+
if (!target.botKeys[0]) return;
|
|
325
|
+
targets.push(target);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function mergeTarget(merged: Map<string, ListedTarget>, incoming: ListedTarget): void {
|
|
329
|
+
const key = `${incoming.kind}\0${incoming.id}\0${incoming.guildId}`;
|
|
330
|
+
const existing = merged.get(key);
|
|
331
|
+
if (!existing) {
|
|
332
|
+
merged.set(key, { ...incoming, botKeys: [...incoming.botKeys] });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
existing.botKeys = unionKeys(existing.botKeys, incoming.botKeys);
|
|
336
|
+
if (!existing.name || existing.name === existing.id) {
|
|
337
|
+
existing.name = incoming.name || existing.name;
|
|
338
|
+
}
|
|
339
|
+
if (!existing.guildName) existing.guildName = incoming.guildName;
|
|
340
|
+
if (!existing.avatar) existing.avatar = incoming.avatar;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function unionKeys(left: string[], right: string[]): string[] {
|
|
344
|
+
const seen = new Set(left);
|
|
345
|
+
const out = [...left];
|
|
346
|
+
for (const key of right) {
|
|
347
|
+
if (!key || seen.has(key)) continue;
|
|
348
|
+
seen.add(key);
|
|
349
|
+
out.push(key);
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function paginate<T>(
|
|
355
|
+
fetch: (next?: string) => Promise<unknown>,
|
|
356
|
+
): Promise<PageResult<T>> {
|
|
357
|
+
const items: T[] = [];
|
|
358
|
+
const seenTokens = new Set<string>();
|
|
359
|
+
let next: string | undefined;
|
|
360
|
+
let pages = 0;
|
|
361
|
+
|
|
362
|
+
while (true) {
|
|
363
|
+
pages += 1;
|
|
364
|
+
if (pages > MAX_LIST_PAGES || items.length >= MAX_LIST_ITEMS) {
|
|
365
|
+
return { items, truncated: true, unsupported: false, failed: false };
|
|
366
|
+
}
|
|
367
|
+
let raw: unknown;
|
|
368
|
+
try {
|
|
369
|
+
raw = await fetch(next);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (isUnsupported(error)) {
|
|
372
|
+
if (items.length > 0) {
|
|
373
|
+
return { items, truncated: true, unsupported: false, failed: false };
|
|
374
|
+
}
|
|
375
|
+
return { items, truncated: false, unsupported: true, failed: false };
|
|
376
|
+
}
|
|
377
|
+
return { items, truncated: items.length > 0, unsupported: false, failed: true };
|
|
378
|
+
}
|
|
379
|
+
if (raw == null) break;
|
|
380
|
+
if (typeof (raw as { then?: unknown }).then === "function") {
|
|
381
|
+
raw = await (raw as Promise<unknown>);
|
|
382
|
+
}
|
|
383
|
+
if (isAsyncIterableObject(raw) && !hasListData(raw)) {
|
|
384
|
+
try {
|
|
385
|
+
const collected = await collectFromIterable<T>(raw);
|
|
386
|
+
items.push(...collected.items);
|
|
387
|
+
return {
|
|
388
|
+
items,
|
|
389
|
+
truncated: collected.truncated,
|
|
390
|
+
unsupported: false,
|
|
391
|
+
failed: false,
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (isUnsupported(error) && items.length === 0) {
|
|
395
|
+
return { items, truncated: false, unsupported: true, failed: false };
|
|
396
|
+
}
|
|
397
|
+
return { items, truncated: items.length > 0, unsupported: false, failed: true };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const page = normalizePage<T>(raw);
|
|
401
|
+
items.push(...page.data);
|
|
402
|
+
const token = page.next;
|
|
403
|
+
if (!token || seenTokens.has(token) || page.data.length === 0) break;
|
|
404
|
+
seenTokens.add(token);
|
|
405
|
+
next = token;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { items, truncated: false, unsupported: false, failed: false };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function normalizePage<T>(raw: unknown): { data: T[]; next?: string } {
|
|
412
|
+
if (Array.isArray(raw)) {
|
|
413
|
+
return { data: raw as T[] };
|
|
414
|
+
}
|
|
415
|
+
if (!raw || typeof raw !== "object") {
|
|
416
|
+
return { data: [] };
|
|
417
|
+
}
|
|
418
|
+
const rec = raw as { data?: unknown; next?: unknown };
|
|
419
|
+
const data = Array.isArray(rec.data) ? (rec.data as T[]) : [];
|
|
420
|
+
const next = rec.next == null || rec.next === "" ? undefined : String(rec.next);
|
|
421
|
+
return { data, next };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function callBotList(bot: Bot, method: string, next?: string, guildId?: string): Promise<unknown> {
|
|
425
|
+
const fn = (bot as unknown as Record<string, unknown>)[method];
|
|
426
|
+
if (typeof fn !== "function") {
|
|
427
|
+
return Promise.reject(new Error(`${method} is not supported`));
|
|
428
|
+
}
|
|
429
|
+
if (method === "getChannelList") {
|
|
430
|
+
return Promise.resolve((fn as (guildId: string, next?: string) => unknown).call(bot, guildId ?? "", next));
|
|
431
|
+
}
|
|
432
|
+
return Promise.resolve((fn as (next?: string) => unknown).call(bot, next));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function hasListData(value: object): boolean {
|
|
436
|
+
return Array.isArray((value as { data?: unknown }).data);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isAsyncIterableObject(value: unknown): value is AsyncIterable<unknown> {
|
|
440
|
+
return typeof value === "object" && value != null && Symbol.asyncIterator in value && !Array.isArray(value);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function collectFromIterable<T>(iterable: AsyncIterable<unknown>): Promise<{ items: T[]; truncated: boolean }> {
|
|
444
|
+
const items: T[] = [];
|
|
445
|
+
for await (const item of iterable) {
|
|
446
|
+
items.push(item as T);
|
|
447
|
+
if (items.length >= MAX_LIST_ITEMS) return { items, truncated: true };
|
|
448
|
+
}
|
|
449
|
+
return { items, truncated: false };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function isUnsupported(error: unknown): boolean {
|
|
453
|
+
if (error && typeof error === "object" && "name" in error) {
|
|
454
|
+
const name = String((error as { name?: string }).name);
|
|
455
|
+
if (/unsupported/i.test(name)) return true;
|
|
456
|
+
}
|
|
457
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
458
|
+
return /not (implemented|supported)|unsupported|is not a function/i.test(message);
|
|
459
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { GenMessage } from "@bufbuild/protobuf/codegenv2";
|
|
2
|
+
import type { Message } from "@bufbuild/protobuf";
|
|
3
|
+
import { FrameSchema } from "../gen/dbk/v1/frame_pb";
|
|
4
|
+
import {
|
|
5
|
+
BotChangedEventSchema,
|
|
6
|
+
GetTargetRequestSchema,
|
|
7
|
+
GetTargetResponseSchema,
|
|
8
|
+
HelloRequestSchema,
|
|
9
|
+
HelloResponseSchema,
|
|
10
|
+
IncomingMessageSchema,
|
|
11
|
+
ListBotsRequestSchema,
|
|
12
|
+
ListBotsResponseSchema,
|
|
13
|
+
ListTargetsRequestSchema,
|
|
14
|
+
ListTargetsResponseSchema,
|
|
15
|
+
RecallParamsSchema,
|
|
16
|
+
RecallResultSchema,
|
|
17
|
+
SendParamsSchema,
|
|
18
|
+
SendResultSchema,
|
|
19
|
+
type BotChangedEvent,
|
|
20
|
+
type GetTargetRequest,
|
|
21
|
+
type GetTargetResponse,
|
|
22
|
+
type HelloRequest,
|
|
23
|
+
type HelloResponse,
|
|
24
|
+
type IncomingMessage,
|
|
25
|
+
type ListBotsRequest,
|
|
26
|
+
type ListBotsResponse,
|
|
27
|
+
type ListTargetsRequest,
|
|
28
|
+
type ListTargetsResponse,
|
|
29
|
+
type RecallParams,
|
|
30
|
+
type RecallResult,
|
|
31
|
+
type SendParams,
|
|
32
|
+
type SendResult,
|
|
33
|
+
} from "../gen/dbk/v1/rpc_pb";
|
|
34
|
+
|
|
35
|
+
export const PROTOCOL_VERSION = "1";
|
|
36
|
+
export { GATEWAY_VERSION } from "./version";
|
|
37
|
+
|
|
38
|
+
export const PING_INTERVAL_MS = 10_000;
|
|
39
|
+
export const PONG_TIMEOUT_MS = 20_000;
|
|
40
|
+
|
|
41
|
+
export const DbkMethod = {
|
|
42
|
+
SESSION_HELLO: "session.hello",
|
|
43
|
+
BOTS_LIST: "bots.list",
|
|
44
|
+
TARGETS_LIST: "targets.list",
|
|
45
|
+
TARGETS_GET: "targets.get",
|
|
46
|
+
MESSAGE_SEND: "message.send",
|
|
47
|
+
MESSAGE_RECALL: "message.recall",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
export const DbkEvent = {
|
|
51
|
+
BOT_CHANGED: "bot.changed",
|
|
52
|
+
MESSAGE_CREATED: "message.created",
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export type DbkMethodName = (typeof DbkMethod)[keyof typeof DbkMethod];
|
|
56
|
+
export type DbkEventName = (typeof DbkEvent)[keyof typeof DbkEvent];
|
|
57
|
+
export type DbkEncoding = "binary" | "json";
|
|
58
|
+
|
|
59
|
+
export interface DbkRpcMap {
|
|
60
|
+
[DbkMethod.SESSION_HELLO]: { request: HelloRequest; response: HelloResponse };
|
|
61
|
+
[DbkMethod.BOTS_LIST]: { request: ListBotsRequest; response: ListBotsResponse };
|
|
62
|
+
[DbkMethod.TARGETS_LIST]: { request: ListTargetsRequest; response: ListTargetsResponse };
|
|
63
|
+
[DbkMethod.TARGETS_GET]: { request: GetTargetRequest; response: GetTargetResponse };
|
|
64
|
+
[DbkMethod.MESSAGE_SEND]: { request: SendParams; response: SendResult };
|
|
65
|
+
[DbkMethod.MESSAGE_RECALL]: { request: RecallParams; response: RecallResult };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DbkEventMap {
|
|
69
|
+
[DbkEvent.BOT_CHANGED]: BotChangedEvent;
|
|
70
|
+
[DbkEvent.MESSAGE_CREATED]: IncomingMessage;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type DbkGatewayHandlers = {
|
|
74
|
+
[K in keyof DbkRpcMap]: (request: DbkRpcMap[K]["request"]) => Promise<DbkRpcMap[K]["response"]> | DbkRpcMap[K]["response"];
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
interface RpcCodec<Req extends Message, Res extends Message> {
|
|
78
|
+
request: GenMessage<Req>;
|
|
79
|
+
response: GenMessage<Res>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const DbkRpcCodecs: { [K in keyof DbkRpcMap]: RpcCodec<DbkRpcMap[K]["request"], DbkRpcMap[K]["response"]> } = {
|
|
83
|
+
[DbkMethod.SESSION_HELLO]: { request: HelloRequestSchema, response: HelloResponseSchema },
|
|
84
|
+
[DbkMethod.BOTS_LIST]: { request: ListBotsRequestSchema, response: ListBotsResponseSchema },
|
|
85
|
+
[DbkMethod.TARGETS_LIST]: { request: ListTargetsRequestSchema, response: ListTargetsResponseSchema },
|
|
86
|
+
[DbkMethod.TARGETS_GET]: { request: GetTargetRequestSchema, response: GetTargetResponseSchema },
|
|
87
|
+
[DbkMethod.MESSAGE_SEND]: { request: SendParamsSchema, response: SendResultSchema },
|
|
88
|
+
[DbkMethod.MESSAGE_RECALL]: { request: RecallParamsSchema, response: RecallResultSchema },
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const DbkEventCodecs: { [K in keyof DbkEventMap]: GenMessage<DbkEventMap[K]> } = {
|
|
92
|
+
[DbkEvent.BOT_CHANGED]: BotChangedEventSchema,
|
|
93
|
+
[DbkEvent.MESSAGE_CREATED]: IncomingMessageSchema,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export { FrameSchema };
|
|
97
|
+
export type { Frame } from "../gen/dbk/v1/frame_pb";
|
|
98
|
+
export { FrameOp } from "../gen/dbk/v1/frame_pb";
|
|
99
|
+
export { ErrorCode, RpcErrorSchema } from "../gen/dbk/v1/common_pb";
|
|
100
|
+
export type { RpcError } from "../gen/dbk/v1/common_pb";
|