@waifucave/discord-waifus 1.5.154
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/LICENSE +21 -0
- package/README.md +230 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/assets/ocr/eng.traineddata +0 -0
- package/bin/waifus.mjs +36 -0
- package/dist/api/errors.d.ts +9 -0
- package/dist/api/errors.js +23 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/server.d.ts +19 -0
- package/dist/api/server.js +1290 -0
- package/dist/api/server.js.map +1 -0
- package/dist/backend/logger.d.ts +20 -0
- package/dist/backend/logger.js +65 -0
- package/dist/backend/logger.js.map +1 -0
- package/dist/backend/migrations.d.ts +4 -0
- package/dist/backend/migrations.js +521 -0
- package/dist/backend/migrations.js.map +1 -0
- package/dist/backend/redaction.d.ts +1 -0
- package/dist/backend/redaction.js +22 -0
- package/dist/backend/redaction.js.map +1 -0
- package/dist/backend/runtime.d.ts +30 -0
- package/dist/backend/runtime.js +49 -0
- package/dist/backend/runtime.js.map +1 -0
- package/dist/backend/server.d.ts +18 -0
- package/dist/backend/server.js +581 -0
- package/dist/backend/server.js.map +1 -0
- package/dist/cli/commands.d.ts +35 -0
- package/dist/cli/commands.js +510 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +12 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/parser.d.ts +10 -0
- package/dist/cli/parser.js +64 -0
- package/dist/cli/parser.js.map +1 -0
- package/dist/config/appConfig.d.ts +3 -0
- package/dist/config/appConfig.js +27 -0
- package/dist/config/appConfig.js.map +1 -0
- package/dist/config/layout.d.ts +13 -0
- package/dist/config/layout.js +173 -0
- package/dist/config/layout.js.map +1 -0
- package/dist/config/paths.d.ts +5 -0
- package/dist/config/paths.js +20 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/prebuiltWaifus.d.ts +3 -0
- package/dist/config/prebuiltWaifus.js +106 -0
- package/dist/config/prebuiltWaifus.js.map +1 -0
- package/dist/discord/client.d.ts +394 -0
- package/dist/discord/client.js +1196 -0
- package/dist/discord/client.js.map +1 -0
- package/dist/discord/contextBuilder.d.ts +15 -0
- package/dist/discord/contextBuilder.js +18 -0
- package/dist/discord/contextBuilder.js.map +1 -0
- package/dist/discord/intents.d.ts +5 -0
- package/dist/discord/intents.js +21 -0
- package/dist/discord/intents.js.map +1 -0
- package/dist/discord/memberCache.d.ts +12 -0
- package/dist/discord/memberCache.js +55 -0
- package/dist/discord/memberCache.js.map +1 -0
- package/dist/discord/normalization.d.ts +34 -0
- package/dist/discord/normalization.js +330 -0
- package/dist/discord/normalization.js.map +1 -0
- package/dist/discord/rateLimit.d.ts +9 -0
- package/dist/discord/rateLimit.js +11 -0
- package/dist/discord/rateLimit.js.map +1 -0
- package/dist/orchestration/context.d.ts +85 -0
- package/dist/orchestration/context.js +200 -0
- package/dist/orchestration/context.js.map +1 -0
- package/dist/orchestration/decisions.d.ts +52 -0
- package/dist/orchestration/decisions.js +61 -0
- package/dist/orchestration/decisions.js.map +1 -0
- package/dist/orchestration/messageSplit.d.ts +6 -0
- package/dist/orchestration/messageSplit.js +118 -0
- package/dist/orchestration/messageSplit.js.map +1 -0
- package/dist/orchestration/ocr.d.ts +43 -0
- package/dist/orchestration/ocr.js +504 -0
- package/dist/orchestration/ocr.js.map +1 -0
- package/dist/orchestration/ocrPackages.d.ts +24 -0
- package/dist/orchestration/ocrPackages.js +85 -0
- package/dist/orchestration/ocrPackages.js.map +1 -0
- package/dist/orchestration/promptBlocks.d.ts +28 -0
- package/dist/orchestration/promptBlocks.js +232 -0
- package/dist/orchestration/promptBlocks.js.map +1 -0
- package/dist/orchestration/replyQuote.d.ts +7 -0
- package/dist/orchestration/replyQuote.js +198 -0
- package/dist/orchestration/replyQuote.js.map +1 -0
- package/dist/orchestration/reviewer.d.ts +5 -0
- package/dist/orchestration/reviewer.js +5 -0
- package/dist/orchestration/reviewer.js.map +1 -0
- package/dist/orchestration/runtime.d.ts +152 -0
- package/dist/orchestration/runtime.js +3535 -0
- package/dist/orchestration/runtime.js.map +1 -0
- package/dist/orchestration/session.d.ts +30 -0
- package/dist/orchestration/session.js +41 -0
- package/dist/orchestration/session.js.map +1 -0
- package/dist/orchestration/stageManager.d.ts +42 -0
- package/dist/orchestration/stageManager.js +50 -0
- package/dist/orchestration/stageManager.js.map +1 -0
- package/dist/providers/catalog.d.ts +6 -0
- package/dist/providers/catalog.js +212 -0
- package/dist/providers/catalog.js.map +1 -0
- package/dist/providers/pipelines.d.ts +13 -0
- package/dist/providers/pipelines.js +2388 -0
- package/dist/providers/pipelines.js.map +1 -0
- package/dist/providers/types.d.ts +91 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/shared/queryLog.d.ts +39 -0
- package/dist/shared/queryLog.js +80 -0
- package/dist/shared/queryLog.js.map +1 -0
- package/dist/shared/schemas/common.d.ts +21 -0
- package/dist/shared/schemas/common.js +27 -0
- package/dist/shared/schemas/common.js.map +1 -0
- package/dist/shared/schemas/config.d.ts +57 -0
- package/dist/shared/schemas/config.js +49 -0
- package/dist/shared/schemas/config.js.map +1 -0
- package/dist/shared/schemas/domain.d.ts +825 -0
- package/dist/shared/schemas/domain.js +444 -0
- package/dist/shared/schemas/domain.js.map +1 -0
- package/dist/storage/atomic.d.ts +6 -0
- package/dist/storage/atomic.js +60 -0
- package/dist/storage/atomic.js.map +1 -0
- package/dist/storage/errors.d.ts +12 -0
- package/dist/storage/errors.js +23 -0
- package/dist/storage/errors.js.map +1 -0
- package/dist/storage/locks.d.ts +9 -0
- package/dist/storage/locks.js +42 -0
- package/dist/storage/locks.js.map +1 -0
- package/dist/storage/storageService.d.ts +25 -0
- package/dist/storage/storageService.js +66 -0
- package/dist/storage/storageService.js.map +1 -0
- package/dist-frontend/assets/index-B2bB3_2r.js +39 -0
- package/dist-frontend/assets/index-B2bB3_2r.js.map +1 -0
- package/dist-frontend/assets/index-Dj1L8Px-.css +1 -0
- package/dist-frontend/index.html +14 -0
- package/docs/api.md +162 -0
- package/docs/old-orchestrator-prompt.md +123 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
import { ApplicationCommandOptionType, Client, Events, GatewayIntentBits, MessageFlags, Partials, PermissionFlagsBits } from "discord.js";
|
|
2
|
+
import { buildNormalizedDiscordContext } from "./contextBuilder.js";
|
|
3
|
+
import { unresolvedMentionIdsByType } from "./memberCache.js";
|
|
4
|
+
import { denormalizeModelContentForDiscord } from "./normalization.js";
|
|
5
|
+
export class DiscordJsGateway {
|
|
6
|
+
options;
|
|
7
|
+
clients = new Map();
|
|
8
|
+
listeners = new Set();
|
|
9
|
+
reviewListeners = new Set();
|
|
10
|
+
clearListeners = new Set();
|
|
11
|
+
runListeners = new Set();
|
|
12
|
+
runWaifuAutocompleteListeners = new Set();
|
|
13
|
+
stopListeners = new Set();
|
|
14
|
+
memoriesListeners = new Set();
|
|
15
|
+
printListeners = new Set();
|
|
16
|
+
printWaifuAutocompleteListeners = new Set();
|
|
17
|
+
debugListeners = new Set();
|
|
18
|
+
recentMentionRefreshes = new Map();
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
}
|
|
22
|
+
async connect() {
|
|
23
|
+
const bots = [this.options.orchestrator, ...(this.options.waifus ?? [])].filter((bot) => Boolean(bot.token));
|
|
24
|
+
await Promise.all(bots.map(async (bot) => {
|
|
25
|
+
const client = new Client({
|
|
26
|
+
intents: [
|
|
27
|
+
GatewayIntentBits.Guilds,
|
|
28
|
+
GatewayIntentBits.GuildMessages,
|
|
29
|
+
GatewayIntentBits.GuildMessageReactions,
|
|
30
|
+
GatewayIntentBits.MessageContent
|
|
31
|
+
],
|
|
32
|
+
partials: [Partials.Channel, Partials.Message, Partials.Reaction]
|
|
33
|
+
});
|
|
34
|
+
this.attachClientRuntimeHandlers(client, bot.id);
|
|
35
|
+
if (bot.id === this.options.orchestrator.id) {
|
|
36
|
+
client.on("messageCreate", (message) => {
|
|
37
|
+
if (!message.guildId)
|
|
38
|
+
return;
|
|
39
|
+
const event = {
|
|
40
|
+
guildId: message.guildId,
|
|
41
|
+
channelId: message.channelId,
|
|
42
|
+
messageId: message.id,
|
|
43
|
+
authorId: message.author.id,
|
|
44
|
+
authorDisplayName: message.member?.displayName ?? message.author.globalName ?? message.author.username,
|
|
45
|
+
authorBot: message.author.bot
|
|
46
|
+
};
|
|
47
|
+
this.options.logger?.info("Discord gateway received message", {
|
|
48
|
+
guildId: event.guildId,
|
|
49
|
+
channelId: event.channelId,
|
|
50
|
+
messageId: event.messageId,
|
|
51
|
+
authorBot: event.authorBot
|
|
52
|
+
});
|
|
53
|
+
for (const listener of this.listeners) {
|
|
54
|
+
this.dispatchListener("Discord message listener failed", listener, event, {
|
|
55
|
+
guildId: event.guildId,
|
|
56
|
+
channelId: event.channelId,
|
|
57
|
+
messageId: event.messageId
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
client.on(Events.InteractionCreate, (interaction) => {
|
|
62
|
+
if (interaction.isAutocomplete()) {
|
|
63
|
+
if (interaction.commandName === RUN_COMMAND_NAME) {
|
|
64
|
+
this.runBackground("Discord run autocomplete interaction failed", {
|
|
65
|
+
commandName: interaction.commandName,
|
|
66
|
+
guildId: interaction.guildId,
|
|
67
|
+
channelId: interaction.channelId
|
|
68
|
+
}, () => this.handleRunAutocompleteInteraction(interaction));
|
|
69
|
+
}
|
|
70
|
+
else if (interaction.commandName === PRINT_COMMAND_NAME) {
|
|
71
|
+
this.runBackground("Discord print autocomplete interaction failed", {
|
|
72
|
+
commandName: interaction.commandName,
|
|
73
|
+
guildId: interaction.guildId,
|
|
74
|
+
channelId: interaction.channelId
|
|
75
|
+
}, () => this.handlePrintAutocompleteInteraction(interaction));
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!interaction.isChatInputCommand())
|
|
80
|
+
return;
|
|
81
|
+
if (interaction.commandName === REVIEW_COMMAND_NAME) {
|
|
82
|
+
this.runBackground("Discord review interaction failed", interactionLogContext(interaction), () => this.handleReviewInteraction(interaction));
|
|
83
|
+
}
|
|
84
|
+
else if (interaction.commandName === CLEAR_COMMAND_NAME) {
|
|
85
|
+
this.runBackground("Discord clear interaction failed", interactionLogContext(interaction), () => this.handleClearInteraction(interaction));
|
|
86
|
+
}
|
|
87
|
+
else if (interaction.commandName === RUN_COMMAND_NAME) {
|
|
88
|
+
this.runBackground("Discord run interaction failed", interactionLogContext(interaction), () => this.handleRunInteraction(interaction));
|
|
89
|
+
}
|
|
90
|
+
else if (interaction.commandName === STOP_COMMAND_NAME) {
|
|
91
|
+
this.runBackground("Discord stop interaction failed", interactionLogContext(interaction), () => this.handleStopInteraction(interaction));
|
|
92
|
+
}
|
|
93
|
+
else if (interaction.commandName === MEMORIES_COMMAND_NAME) {
|
|
94
|
+
this.runBackground("Discord memories interaction failed", interactionLogContext(interaction), () => this.handleMemoriesInteraction(interaction));
|
|
95
|
+
}
|
|
96
|
+
else if (interaction.commandName === PRINT_COMMAND_NAME) {
|
|
97
|
+
this.runBackground("Discord print interaction failed", interactionLogContext(interaction), () => this.handlePrintInteraction(interaction));
|
|
98
|
+
}
|
|
99
|
+
else if (interaction.commandName === DEBUG_COMMAND_NAME) {
|
|
100
|
+
this.runBackground("Discord debug interaction failed", interactionLogContext(interaction), () => this.handleDebugInteraction(interaction));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
await loginAndWaitUntilReady(client, bot.token);
|
|
105
|
+
this.clients.set(bot.id, client);
|
|
106
|
+
if (bot.id === this.options.orchestrator.id) {
|
|
107
|
+
await registerOrchestratorCommands(client, this.options.logger);
|
|
108
|
+
}
|
|
109
|
+
this.options.logger?.info("Discord bot connected", {
|
|
110
|
+
botId: bot.id,
|
|
111
|
+
displayName: bot.displayName,
|
|
112
|
+
kind: bot.id === this.options.orchestrator.id ? "orchestrator" : "waifu"
|
|
113
|
+
});
|
|
114
|
+
}));
|
|
115
|
+
return {
|
|
116
|
+
connected: this.clients.size > 0,
|
|
117
|
+
orchestratorConnected: this.clients.has(this.options.orchestrator.id),
|
|
118
|
+
waifuBotCount: (this.options.waifus ?? []).filter((bot) => this.clients.has(bot.id)).length,
|
|
119
|
+
warnings: this.clients.size > 0
|
|
120
|
+
? []
|
|
121
|
+
: ["No enabled Discord bot tokens are configured."]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async disconnect() {
|
|
125
|
+
await Promise.all([...this.clients.values()].map((client) => client.destroy()));
|
|
126
|
+
this.clients.clear();
|
|
127
|
+
}
|
|
128
|
+
onMessage(listener) {
|
|
129
|
+
this.listeners.add(listener);
|
|
130
|
+
return () => this.listeners.delete(listener);
|
|
131
|
+
}
|
|
132
|
+
onReviewCommand(listener) {
|
|
133
|
+
this.reviewListeners.add(listener);
|
|
134
|
+
return () => this.reviewListeners.delete(listener);
|
|
135
|
+
}
|
|
136
|
+
onClearCommand(listener) {
|
|
137
|
+
this.clearListeners.add(listener);
|
|
138
|
+
return () => this.clearListeners.delete(listener);
|
|
139
|
+
}
|
|
140
|
+
onRunCommand(listener) {
|
|
141
|
+
this.runListeners.add(listener);
|
|
142
|
+
return () => this.runListeners.delete(listener);
|
|
143
|
+
}
|
|
144
|
+
onRunWaifuAutocomplete(listener) {
|
|
145
|
+
this.runWaifuAutocompleteListeners.add(listener);
|
|
146
|
+
return () => this.runWaifuAutocompleteListeners.delete(listener);
|
|
147
|
+
}
|
|
148
|
+
onStopCommand(listener) {
|
|
149
|
+
this.stopListeners.add(listener);
|
|
150
|
+
return () => this.stopListeners.delete(listener);
|
|
151
|
+
}
|
|
152
|
+
onMemoriesCommand(listener) {
|
|
153
|
+
this.memoriesListeners.add(listener);
|
|
154
|
+
return () => this.memoriesListeners.delete(listener);
|
|
155
|
+
}
|
|
156
|
+
onPrintCommand(listener) {
|
|
157
|
+
this.printListeners.add(listener);
|
|
158
|
+
return () => this.printListeners.delete(listener);
|
|
159
|
+
}
|
|
160
|
+
onPrintWaifuAutocomplete(listener) {
|
|
161
|
+
this.printWaifuAutocompleteListeners.add(listener);
|
|
162
|
+
return () => this.printWaifuAutocompleteListeners.delete(listener);
|
|
163
|
+
}
|
|
164
|
+
onDebugCommand(listener) {
|
|
165
|
+
this.debugListeners.add(listener);
|
|
166
|
+
return () => this.debugListeners.delete(listener);
|
|
167
|
+
}
|
|
168
|
+
async listGuilds() {
|
|
169
|
+
const client = this.orchestratorClient();
|
|
170
|
+
await client.guilds.fetch();
|
|
171
|
+
return client.guilds.cache.map((guild) => ({
|
|
172
|
+
guildId: guild.id,
|
|
173
|
+
name: guild.name
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
async fetchChannelMetadata(input) {
|
|
177
|
+
const client = this.orchestratorClient();
|
|
178
|
+
const [guild, channel] = await Promise.all([
|
|
179
|
+
client.guilds.fetch(input.guildId),
|
|
180
|
+
client.channels.fetch(input.channelId)
|
|
181
|
+
]);
|
|
182
|
+
if (!channel) {
|
|
183
|
+
throw new Error(`Discord channel ${input.channelId} is not accessible to the orchestrator bot.`);
|
|
184
|
+
}
|
|
185
|
+
const channelGuildId = "guildId" in channel && typeof channel.guildId === "string" ? channel.guildId : undefined;
|
|
186
|
+
if (channelGuildId && channelGuildId !== input.guildId) {
|
|
187
|
+
throw new Error(`Discord channel ${input.channelId} does not belong to guild ${input.guildId}.`);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
guildId: guild.id,
|
|
191
|
+
guildName: guild.name,
|
|
192
|
+
channelId: channel.id,
|
|
193
|
+
channelName: "name" in channel && typeof channel.name === "string" ? channel.name : undefined
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
async fetchFreshContext(input) {
|
|
197
|
+
throwIfAborted(input.signal);
|
|
198
|
+
const client = this.orchestratorClient();
|
|
199
|
+
let cache = await this.cacheForGuild(input.guildId);
|
|
200
|
+
const channel = await client.channels.fetch(input.channelId);
|
|
201
|
+
if (!channel || !("messages" in channel)) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
const collection = await channel.messages.fetch({ limit: input.limit });
|
|
205
|
+
const rawMessages = [...collection.values()].sort((a, b) => a.createdTimestamp - b.createdTimestamp);
|
|
206
|
+
const mentionedRoles = uniqueMentionedRoles(rawMessages, cache.roles ?? []);
|
|
207
|
+
const roleCacheForResolution = mergeRoleCaches(cache.roles ?? [], mentionedRoles);
|
|
208
|
+
const missingMentionIds = unresolvedMentionIdsByType(rawMessages.map((message) => message.content), cache.members ?? [], roleCacheForResolution);
|
|
209
|
+
if ((missingMentionIds.userIds.length > 0 || missingMentionIds.roleIds.length > 0) && this.shouldRefreshMentions(input.guildId)) {
|
|
210
|
+
this.options.logger?.info("Refreshing Discord mention caches for unresolved mentions", {
|
|
211
|
+
guildId: input.guildId,
|
|
212
|
+
channelId: input.channelId,
|
|
213
|
+
userMentionCount: missingMentionIds.userIds.length,
|
|
214
|
+
roleMentionCount: missingMentionIds.roleIds.length,
|
|
215
|
+
mentionIds: [...missingMentionIds.userIds, ...missingMentionIds.roleIds].slice(0, 5)
|
|
216
|
+
});
|
|
217
|
+
try {
|
|
218
|
+
const [refreshedMembers, refreshedRoles] = await Promise.all([
|
|
219
|
+
missingMentionIds.userIds.length > 0 ? this.options.refreshMembersForGuild?.(input.guildId) : undefined,
|
|
220
|
+
missingMentionIds.roleIds.length > 0 ? this.options.refreshRolesForGuild?.(input.guildId) : undefined
|
|
221
|
+
]);
|
|
222
|
+
cache = {
|
|
223
|
+
...cache,
|
|
224
|
+
...refreshedMembers,
|
|
225
|
+
...refreshedRoles
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.options.logger?.warn("Discord mention cache refresh for unresolved mentions failed", {
|
|
230
|
+
guildId: input.guildId,
|
|
231
|
+
channelId: input.channelId,
|
|
232
|
+
message: error instanceof Error ? error.message : String(error)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const messages = await Promise.all(rawMessages.map(async (message) => {
|
|
237
|
+
const [replyTo, reactions] = await Promise.all([
|
|
238
|
+
message.reference?.messageId
|
|
239
|
+
? resolveReplyReference(message).catch(() => ({ messageId: message.reference?.messageId ?? "" }))
|
|
240
|
+
: undefined,
|
|
241
|
+
summarizeReactions(message.reactions.cache.values(), cache)
|
|
242
|
+
]);
|
|
243
|
+
return {
|
|
244
|
+
id: message.id,
|
|
245
|
+
channelId: message.channelId,
|
|
246
|
+
guildId: message.guildId ?? input.guildId,
|
|
247
|
+
authorId: message.author.id,
|
|
248
|
+
authorName: message.author.username,
|
|
249
|
+
authorDisplayName: message.member?.displayName ?? message.author.globalName ?? message.author.username,
|
|
250
|
+
authorBot: message.author.bot,
|
|
251
|
+
content: message.content,
|
|
252
|
+
createdAt: message.createdAt,
|
|
253
|
+
images: collectImageAttachments(message),
|
|
254
|
+
replyTo,
|
|
255
|
+
reactions
|
|
256
|
+
};
|
|
257
|
+
}));
|
|
258
|
+
return buildNormalizedDiscordContext(messages, { ...cache, roles: mergeRoleCaches(cache.roles ?? [], mentionedRoles) }, {
|
|
259
|
+
orchestratorAuthorIds: botAuthorIds([this.options.orchestrator]),
|
|
260
|
+
waifuAuthorIds: botAuthorIds(this.options.waifus ?? [])
|
|
261
|
+
}).messages;
|
|
262
|
+
}
|
|
263
|
+
async sendWaifuMessage(input) {
|
|
264
|
+
const client = input.senderBotId ? this.clients.get(input.senderBotId) : this.orchestratorClient();
|
|
265
|
+
if (!client) {
|
|
266
|
+
throw new Error(input.senderBotId
|
|
267
|
+
? `Waifu Discord bot ${input.senderBotId} is not connected.`
|
|
268
|
+
: "Orchestrator Discord client is not connected.");
|
|
269
|
+
}
|
|
270
|
+
const cache = await this.cacheForGuild(input.guildId);
|
|
271
|
+
const channel = await client.channels.fetch(input.channelId);
|
|
272
|
+
if (!channel || !("send" in channel)) {
|
|
273
|
+
throw new Error(`Discord channel ${input.channelId} is not sendable.`);
|
|
274
|
+
}
|
|
275
|
+
const denormalized = denormalizeModelContentForDiscord(input.content, {
|
|
276
|
+
...cache,
|
|
277
|
+
activeAuthorIds: input.allowedUserMentionIds
|
|
278
|
+
});
|
|
279
|
+
const replyToMessageId = sanitizeReplyTarget(input.replyToMessageId);
|
|
280
|
+
if (input.replyToMessageId && !replyToMessageId) {
|
|
281
|
+
this.options.logger?.warn("Dropping non-snowflake replyToMessageId", {
|
|
282
|
+
guildId: input.guildId,
|
|
283
|
+
channelId: input.channelId,
|
|
284
|
+
value: input.replyToMessageId
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const sent = await channel.send({
|
|
288
|
+
content: denormalized.content,
|
|
289
|
+
allowedMentions: denormalized.allowedMentions,
|
|
290
|
+
reply: replyToMessageId ? { messageReference: replyToMessageId } : undefined
|
|
291
|
+
});
|
|
292
|
+
return { messageId: sent.id };
|
|
293
|
+
}
|
|
294
|
+
async sendTyping(input) {
|
|
295
|
+
const client = input.senderBotId ? this.clients.get(input.senderBotId) : this.orchestratorClient();
|
|
296
|
+
if (!client)
|
|
297
|
+
return;
|
|
298
|
+
try {
|
|
299
|
+
const channel = await client.channels.fetch(input.channelId);
|
|
300
|
+
if (channel && "sendTyping" in channel) {
|
|
301
|
+
await channel.sendTyping();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
this.options.logger?.warn("Failed to send typing indicator", {
|
|
306
|
+
guildId: input.guildId,
|
|
307
|
+
channelId: input.channelId,
|
|
308
|
+
senderBotId: input.senderBotId,
|
|
309
|
+
message: error instanceof Error ? error.message : String(error)
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async validateDebugChannel(input) {
|
|
314
|
+
const channel = await this.orchestratorClient().channels.fetch(input.channelId);
|
|
315
|
+
if (!channel) {
|
|
316
|
+
throw new Error(`Discord channel ${input.channelId} is not accessible to the orchestrator bot.`);
|
|
317
|
+
}
|
|
318
|
+
if (input.requireMessages && !("messages" in channel)) {
|
|
319
|
+
throw new Error(`Discord channel ${input.channelId} is not readable by the orchestrator bot.`);
|
|
320
|
+
}
|
|
321
|
+
if (input.requireSend && !("send" in channel)) {
|
|
322
|
+
throw new Error(`Discord channel ${input.channelId} is not sendable by the orchestrator bot.`);
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
channelId: input.channelId,
|
|
326
|
+
guildId: "guildId" in channel && typeof channel.guildId === "string" ? channel.guildId : undefined,
|
|
327
|
+
name: "name" in channel && typeof channel.name === "string" ? channel.name : undefined
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
async sendDebugMessage(input) {
|
|
331
|
+
const channel = await this.orchestratorClient().channels.fetch(input.channelId);
|
|
332
|
+
if (!channel || !("send" in channel)) {
|
|
333
|
+
throw new Error(`Discord channel ${input.channelId} is not sendable by the orchestrator bot.`);
|
|
334
|
+
}
|
|
335
|
+
const sent = await channel.send({
|
|
336
|
+
content: input.content,
|
|
337
|
+
allowedMentions: { parse: [], users: [], roles: [], repliedUser: false }
|
|
338
|
+
});
|
|
339
|
+
return { messageId: sent.id };
|
|
340
|
+
}
|
|
341
|
+
async deleteMessages(input) {
|
|
342
|
+
const result = {
|
|
343
|
+
deletedMessageIds: [],
|
|
344
|
+
failedMessageIds: []
|
|
345
|
+
};
|
|
346
|
+
if (input.messageIds.length === 0)
|
|
347
|
+
return result;
|
|
348
|
+
const orchestrator = this.orchestratorClient();
|
|
349
|
+
const orchestratorChannel = await orchestrator.channels.fetch(input.channelId);
|
|
350
|
+
if (!orchestratorChannel || !("messages" in orchestratorChannel)) {
|
|
351
|
+
throw new Error(`Discord channel ${input.channelId} is not message-manageable.`);
|
|
352
|
+
}
|
|
353
|
+
const remaining = new Set(input.messageIds);
|
|
354
|
+
const supportsBulkDelete = "bulkDelete" in orchestratorChannel &&
|
|
355
|
+
typeof orchestratorChannel.bulkDelete === "function";
|
|
356
|
+
if (input.messageIds.length >= 2 && supportsBulkDelete) {
|
|
357
|
+
try {
|
|
358
|
+
const bulkChannel = orchestratorChannel;
|
|
359
|
+
const bulkDeleted = await bulkChannel.bulkDelete(input.messageIds, true);
|
|
360
|
+
for (const id of bulkDeleted.keys()) {
|
|
361
|
+
if (remaining.has(id)) {
|
|
362
|
+
result.deletedMessageIds.push(id);
|
|
363
|
+
remaining.delete(id);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
369
|
+
this.options.logger?.warn("Discord bulk delete failed, falling back to per-message deletes", {
|
|
370
|
+
guildId: input.guildId,
|
|
371
|
+
channelId: input.channelId,
|
|
372
|
+
messageCount: input.messageIds.length,
|
|
373
|
+
message
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (remaining.size === 0)
|
|
378
|
+
return result;
|
|
379
|
+
const channelByBotId = new Map();
|
|
380
|
+
channelByBotId.set(this.options.orchestrator.id, orchestratorChannel);
|
|
381
|
+
for (const messageId of input.messageIds) {
|
|
382
|
+
if (!remaining.has(messageId))
|
|
383
|
+
continue;
|
|
384
|
+
const messageAuthorId = input.authorIdByMessageId?.[messageId] ?? input.authorId;
|
|
385
|
+
const client = this.clientForAuthor(messageAuthorId);
|
|
386
|
+
const botId = this.botIdForClient(client);
|
|
387
|
+
let channel = botId ? channelByBotId.get(botId) : undefined;
|
|
388
|
+
try {
|
|
389
|
+
if (!channel) {
|
|
390
|
+
const fetched = await client.channels.fetch(input.channelId);
|
|
391
|
+
if (!fetched || !("messages" in fetched)) {
|
|
392
|
+
throw new Error(`Discord channel ${input.channelId} is not message-manageable from this bot.`);
|
|
393
|
+
}
|
|
394
|
+
channel = fetched;
|
|
395
|
+
if (botId)
|
|
396
|
+
channelByBotId.set(botId, fetched);
|
|
397
|
+
}
|
|
398
|
+
await channel.messages.delete(messageId);
|
|
399
|
+
result.deletedMessageIds.push(messageId);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
403
|
+
result.failedMessageIds.push({ messageId, message });
|
|
404
|
+
this.options.logger?.warn("Failed to delete Discord message", {
|
|
405
|
+
guildId: input.guildId,
|
|
406
|
+
channelId: input.channelId,
|
|
407
|
+
messageId,
|
|
408
|
+
authorId: messageAuthorId,
|
|
409
|
+
deletingBotId: botId,
|
|
410
|
+
message
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
async deleteAllMessages(input) {
|
|
417
|
+
const result = {
|
|
418
|
+
scannedMessageCount: 0,
|
|
419
|
+
deletedCount: 0,
|
|
420
|
+
failedCount: 0,
|
|
421
|
+
failedMessageIds: []
|
|
422
|
+
};
|
|
423
|
+
const orchestrator = this.orchestratorClient();
|
|
424
|
+
const fetchedChannel = await orchestrator.channels.fetch(input.channelId);
|
|
425
|
+
if (!fetchedChannel || !("messages" in fetchedChannel)) {
|
|
426
|
+
throw new Error(`Discord channel ${input.channelId} is not message-manageable.`);
|
|
427
|
+
}
|
|
428
|
+
const channel = fetchedChannel;
|
|
429
|
+
let before;
|
|
430
|
+
while (true) {
|
|
431
|
+
const page = await channel.messages.fetch(before ? { limit: 100, before } : { limit: 100 });
|
|
432
|
+
const messages = [...page.values()];
|
|
433
|
+
if (messages.length === 0)
|
|
434
|
+
break;
|
|
435
|
+
result.scannedMessageCount += messages.length;
|
|
436
|
+
await this.deleteFetchedMessages({
|
|
437
|
+
guildId: input.guildId,
|
|
438
|
+
channelId: input.channelId,
|
|
439
|
+
channel,
|
|
440
|
+
messages,
|
|
441
|
+
result
|
|
442
|
+
});
|
|
443
|
+
const nextBefore = oldestMessageId(messages);
|
|
444
|
+
if (!nextBefore || nextBefore === before)
|
|
445
|
+
break;
|
|
446
|
+
before = nextBefore;
|
|
447
|
+
}
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
async deleteFetchedMessages(input) {
|
|
451
|
+
const remaining = new Set(input.messages.map((message) => message.id));
|
|
452
|
+
const bulkIds = input.messages
|
|
453
|
+
.filter((message) => isBulkDeletableMessage(message))
|
|
454
|
+
.map((message) => message.id);
|
|
455
|
+
if (bulkIds.length >= 2 && input.channel.bulkDelete) {
|
|
456
|
+
for (const chunk of chunks(bulkIds, DISCORD_BULK_DELETE_MAX_MESSAGES)) {
|
|
457
|
+
if (chunk.length < 2)
|
|
458
|
+
continue;
|
|
459
|
+
try {
|
|
460
|
+
const bulkDeleted = await input.channel.bulkDelete(chunk, true);
|
|
461
|
+
for (const id of bulkDeleted.keys()) {
|
|
462
|
+
if (remaining.delete(id)) {
|
|
463
|
+
input.result.deletedCount += 1;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
this.options.logger?.warn("Discord bulk delete failed during full channel clear", {
|
|
469
|
+
guildId: input.guildId,
|
|
470
|
+
channelId: input.channelId,
|
|
471
|
+
messageCount: chunk.length,
|
|
472
|
+
message: error instanceof Error ? error.message : String(error)
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for (const message of input.messages) {
|
|
478
|
+
if (!remaining.has(message.id))
|
|
479
|
+
continue;
|
|
480
|
+
try {
|
|
481
|
+
await input.channel.messages.delete(message.id);
|
|
482
|
+
remaining.delete(message.id);
|
|
483
|
+
input.result.deletedCount += 1;
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
487
|
+
remaining.delete(message.id);
|
|
488
|
+
input.result.failedCount += 1;
|
|
489
|
+
input.result.failedMessageIds.push({ messageId: message.id, message: errorMessage });
|
|
490
|
+
this.options.logger?.warn("Failed to delete Discord message during full channel clear", {
|
|
491
|
+
guildId: input.guildId,
|
|
492
|
+
channelId: input.channelId,
|
|
493
|
+
messageId: message.id,
|
|
494
|
+
message: errorMessage
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async cacheForGuild(guildId) {
|
|
500
|
+
return this.options.cacheForGuild?.(guildId) ?? {
|
|
501
|
+
members: this.options.members,
|
|
502
|
+
emojis: this.options.emojis
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
orchestratorClient() {
|
|
506
|
+
const client = this.clients.get(this.options.orchestrator.id);
|
|
507
|
+
if (!client) {
|
|
508
|
+
throw new Error("Orchestrator Discord client is not connected.");
|
|
509
|
+
}
|
|
510
|
+
return client;
|
|
511
|
+
}
|
|
512
|
+
clientForAuthor(authorId) {
|
|
513
|
+
if (authorId) {
|
|
514
|
+
const bots = [this.options.orchestrator, ...(this.options.waifus ?? [])];
|
|
515
|
+
const bot = bots.find((candidate) => candidate.id === authorId || candidate.applicationId === authorId);
|
|
516
|
+
const client = bot ? this.clients.get(bot.id) : undefined;
|
|
517
|
+
if (client) {
|
|
518
|
+
return client;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return this.orchestratorClient();
|
|
522
|
+
}
|
|
523
|
+
botIdForClient(client) {
|
|
524
|
+
for (const [botId, candidate] of this.clients) {
|
|
525
|
+
if (candidate === client)
|
|
526
|
+
return botId;
|
|
527
|
+
}
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
shouldRefreshMentions(guildId) {
|
|
531
|
+
if (!this.options.refreshMembersForGuild)
|
|
532
|
+
return false;
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
const previous = this.recentMentionRefreshes.get(guildId) ?? 0;
|
|
535
|
+
if (now - previous < MENTION_REFRESH_COOLDOWN_MS) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
this.recentMentionRefreshes.set(guildId, now);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
attachClientRuntimeHandlers(client, botId) {
|
|
542
|
+
client.on(Events.Error, (error) => {
|
|
543
|
+
this.reportRuntimeIssue({
|
|
544
|
+
source: "client-error",
|
|
545
|
+
botId,
|
|
546
|
+
message: errorMessage(error),
|
|
547
|
+
error
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
client.on(Events.ShardError, (error, shardId) => {
|
|
551
|
+
this.reportRuntimeIssue({
|
|
552
|
+
source: "shard-error",
|
|
553
|
+
botId,
|
|
554
|
+
shardId,
|
|
555
|
+
message: errorMessage(error),
|
|
556
|
+
error
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
client.on(Events.ShardDisconnect, (event, shardId) => {
|
|
560
|
+
const code = closeEventCode(event);
|
|
561
|
+
const reason = closeEventReason(event);
|
|
562
|
+
this.reportRuntimeIssue({
|
|
563
|
+
source: "shard-disconnect",
|
|
564
|
+
botId,
|
|
565
|
+
shardId,
|
|
566
|
+
code,
|
|
567
|
+
wasClean: closeEventWasClean(event),
|
|
568
|
+
message: `Discord shard disconnected${code === undefined ? "" : ` with code ${code}`}${reason ? `: ${reason}` : "."}`
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
client.on(Events.Invalidated, () => {
|
|
572
|
+
this.reportRuntimeIssue({
|
|
573
|
+
source: "invalidated",
|
|
574
|
+
botId,
|
|
575
|
+
message: "Discord gateway session was invalidated."
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
client.on(Events.Warn, (message) => {
|
|
579
|
+
this.reportRuntimeIssue({
|
|
580
|
+
source: "warn",
|
|
581
|
+
botId,
|
|
582
|
+
message
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
reportRuntimeIssue(issue) {
|
|
587
|
+
const context = runtimeIssueLogContext(issue);
|
|
588
|
+
if (issue.source === "warn") {
|
|
589
|
+
this.options.logger?.warn("Discord gateway warning", context);
|
|
590
|
+
}
|
|
591
|
+
else {
|
|
592
|
+
this.options.logger?.error("Discord gateway runtime issue", context);
|
|
593
|
+
}
|
|
594
|
+
if (!this.options.onRuntimeIssue)
|
|
595
|
+
return;
|
|
596
|
+
this.runBackground("Discord runtime issue callback failed", context, () => this.options.onRuntimeIssue?.(issue));
|
|
597
|
+
}
|
|
598
|
+
async callListener(label, listener, event, context) {
|
|
599
|
+
try {
|
|
600
|
+
await listener(event);
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
this.options.logger?.error(label, {
|
|
604
|
+
...context,
|
|
605
|
+
message: error instanceof Error ? error.message : String(error)
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
dispatchListener(label, listener, event, context) {
|
|
610
|
+
void this.callListener(label, listener, event, context);
|
|
611
|
+
}
|
|
612
|
+
runBackground(label, context, task) {
|
|
613
|
+
void Promise.resolve()
|
|
614
|
+
.then(task)
|
|
615
|
+
.catch((error) => {
|
|
616
|
+
this.options.logger?.error(label, {
|
|
617
|
+
...context,
|
|
618
|
+
message: error instanceof Error ? error.message : String(error)
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
async handleReviewInteraction(interaction) {
|
|
623
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
624
|
+
if (!interaction.guildId || !interaction.channelId) {
|
|
625
|
+
await interaction.editReply("/review can only be used in a server channel.");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const event = {
|
|
629
|
+
guildId: interaction.guildId,
|
|
630
|
+
channelId: interaction.channelId,
|
|
631
|
+
userId: interaction.user.id,
|
|
632
|
+
respond: async (content) => {
|
|
633
|
+
await interaction.editReply(content);
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
for (const listener of this.reviewListeners) {
|
|
637
|
+
this.dispatchListener("Discord review command listener failed", listener, event, slashCommandLogContext(event));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async handleClearInteraction(interaction) {
|
|
641
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
642
|
+
if (!interaction.guildId || !interaction.channelId) {
|
|
643
|
+
await interaction.editReply("/clear can only be used in a server channel.");
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const event = {
|
|
647
|
+
guildId: interaction.guildId,
|
|
648
|
+
channelId: interaction.channelId,
|
|
649
|
+
userId: interaction.user.id,
|
|
650
|
+
count: interaction.options.getInteger(CLEAR_COUNT_OPTION_NAME) ?? undefined,
|
|
651
|
+
type: parseClearType(interaction.options.getString(CLEAR_TYPE_OPTION_NAME)),
|
|
652
|
+
respond: async (content) => {
|
|
653
|
+
await interaction.editReply(content);
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
for (const listener of this.clearListeners) {
|
|
657
|
+
this.dispatchListener("Discord clear command listener failed", listener, event, slashCommandLogContext(event));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async handleRunInteraction(interaction) {
|
|
661
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
662
|
+
if (!interaction.guildId || !interaction.channelId) {
|
|
663
|
+
await interaction.editReply("/run can only be used in a server channel.");
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const event = {
|
|
667
|
+
guildId: interaction.guildId,
|
|
668
|
+
channelId: interaction.channelId,
|
|
669
|
+
userId: interaction.user.id,
|
|
670
|
+
waifuId: sanitizeRunString(interaction.options.getString(RUN_WAIFU_OPTION_NAME)),
|
|
671
|
+
sceneDirection: sanitizeRunString(interaction.options.getString(RUN_SCENE_DIRECTION_OPTION_NAME)),
|
|
672
|
+
respond: async (content) => {
|
|
673
|
+
await interaction.editReply(content);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
for (const listener of this.runListeners) {
|
|
677
|
+
this.dispatchListener("Discord run command listener failed", listener, event, slashCommandLogContext(event));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async handleRunAutocompleteInteraction(interaction) {
|
|
681
|
+
const focused = interaction.options.getFocused(true);
|
|
682
|
+
if (focused.name !== RUN_WAIFU_OPTION_NAME) {
|
|
683
|
+
await interaction.respond([]);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const event = {
|
|
687
|
+
guildId: interaction.guildId ?? undefined,
|
|
688
|
+
channelId: interaction.channelId ?? undefined,
|
|
689
|
+
focusedValue: typeof focused.value === "string" ? focused.value : String(focused.value ?? ""),
|
|
690
|
+
respond: async (choices) => {
|
|
691
|
+
await interaction.respond(choices.slice(0, MAX_AUTOCOMPLETE_CHOICES));
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
if (this.runWaifuAutocompleteListeners.size === 0) {
|
|
695
|
+
await interaction.respond([]);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
await Promise.all([...this.runWaifuAutocompleteListeners].map((listener) => this.callListener("Discord run autocomplete listener failed", listener, event, {
|
|
699
|
+
guildId: event.guildId,
|
|
700
|
+
channelId: event.channelId,
|
|
701
|
+
focusedValue: event.focusedValue
|
|
702
|
+
})));
|
|
703
|
+
}
|
|
704
|
+
async handleStopInteraction(interaction) {
|
|
705
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
706
|
+
if (!interaction.guildId || !interaction.channelId) {
|
|
707
|
+
await interaction.editReply("/stop can only be used in a server channel.");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const event = {
|
|
711
|
+
guildId: interaction.guildId,
|
|
712
|
+
channelId: interaction.channelId,
|
|
713
|
+
userId: interaction.user.id,
|
|
714
|
+
respond: async (content) => {
|
|
715
|
+
await interaction.editReply(content);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
for (const listener of this.stopListeners) {
|
|
719
|
+
this.dispatchListener("Discord stop command listener failed", listener, event, slashCommandLogContext(event));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async handleMemoriesInteraction(interaction) {
|
|
723
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
724
|
+
if (!interaction.guildId || !interaction.channelId) {
|
|
725
|
+
await interaction.editReply("/memories can only be used in a server channel.");
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const event = {
|
|
729
|
+
guildId: interaction.guildId,
|
|
730
|
+
channelId: interaction.channelId,
|
|
731
|
+
userId: interaction.user.id,
|
|
732
|
+
respond: async (content) => {
|
|
733
|
+
await interaction.editReply(content);
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
for (const listener of this.memoriesListeners) {
|
|
737
|
+
this.dispatchListener("Discord memories command listener failed", listener, event, slashCommandLogContext(event));
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async handlePrintInteraction(interaction) {
|
|
741
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
742
|
+
if (!interaction.guildId || !interaction.channelId) {
|
|
743
|
+
await interaction.editReply("/print can only be used in a server channel.");
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const event = {
|
|
747
|
+
guildId: interaction.guildId,
|
|
748
|
+
channelId: interaction.channelId,
|
|
749
|
+
userId: interaction.user.id,
|
|
750
|
+
type: parsePrintCommandType(interaction.options.getString(PRINT_TYPE_OPTION_NAME)),
|
|
751
|
+
waifuId: sanitizeRunString(interaction.options.getString(PRINT_WAIFU_OPTION_NAME)),
|
|
752
|
+
respond: async (content) => {
|
|
753
|
+
await interaction.editReply(content);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
for (const listener of this.printListeners) {
|
|
757
|
+
this.dispatchListener("Discord print command listener failed", listener, event, {
|
|
758
|
+
...slashCommandLogContext(event),
|
|
759
|
+
type: event.type,
|
|
760
|
+
waifuId: event.waifuId
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
async handlePrintAutocompleteInteraction(interaction) {
|
|
765
|
+
const focused = interaction.options.getFocused(true);
|
|
766
|
+
if (focused.name !== PRINT_WAIFU_OPTION_NAME) {
|
|
767
|
+
await interaction.respond([]);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const event = {
|
|
771
|
+
guildId: interaction.guildId ?? undefined,
|
|
772
|
+
channelId: interaction.channelId ?? undefined,
|
|
773
|
+
focusedValue: typeof focused.value === "string" ? focused.value : String(focused.value ?? ""),
|
|
774
|
+
respond: async (choices) => {
|
|
775
|
+
await interaction.respond(choices.slice(0, MAX_AUTOCOMPLETE_CHOICES));
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
if (this.printWaifuAutocompleteListeners.size === 0) {
|
|
779
|
+
await interaction.respond([]);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
await Promise.all([...this.printWaifuAutocompleteListeners].map((listener) => this.callListener("Discord print autocomplete listener failed", listener, event, {
|
|
783
|
+
guildId: event.guildId,
|
|
784
|
+
channelId: event.channelId,
|
|
785
|
+
focusedValue: event.focusedValue
|
|
786
|
+
})));
|
|
787
|
+
}
|
|
788
|
+
async handleDebugInteraction(interaction) {
|
|
789
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
790
|
+
if (!interaction.guildId || !interaction.channelId) {
|
|
791
|
+
await interaction.editReply("/console can only be used in a server channel.");
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const event = {
|
|
795
|
+
guildId: interaction.guildId,
|
|
796
|
+
channelId: interaction.channelId,
|
|
797
|
+
userId: interaction.user.id,
|
|
798
|
+
type: parseDebugCommandType(interaction.options.getString(DEBUG_TYPE_OPTION_NAME)),
|
|
799
|
+
sourceChannelId: sanitizeRunString(interaction.options.getString(DEBUG_CHANNEL_ID_OPTION_NAME)),
|
|
800
|
+
respond: async (content) => {
|
|
801
|
+
await interaction.editReply(content);
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
for (const listener of this.debugListeners) {
|
|
805
|
+
this.dispatchListener("Discord debug command listener failed", listener, event, {
|
|
806
|
+
...slashCommandLogContext(event),
|
|
807
|
+
type: event.type,
|
|
808
|
+
sourceChannelId: event.sourceChannelId
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
export class DiscordGatewayNotConfigured {
|
|
814
|
+
async connect() {
|
|
815
|
+
return {
|
|
816
|
+
connected: false,
|
|
817
|
+
orchestratorConnected: false,
|
|
818
|
+
waifuBotCount: 0,
|
|
819
|
+
warnings: ["Discord tokens are not configured yet."]
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
async disconnect() { }
|
|
823
|
+
async fetchFreshContext() {
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
async sendWaifuMessage() {
|
|
827
|
+
throw new Error("Discord sending is not configured yet.");
|
|
828
|
+
}
|
|
829
|
+
async sendTyping() { }
|
|
830
|
+
}
|
|
831
|
+
async function loginAndWaitUntilReady(client, token) {
|
|
832
|
+
if (client.isReady())
|
|
833
|
+
return;
|
|
834
|
+
await Promise.all([
|
|
835
|
+
new Promise((resolve) => {
|
|
836
|
+
client.once(Events.ClientReady, () => resolve());
|
|
837
|
+
}),
|
|
838
|
+
client.login(token)
|
|
839
|
+
]);
|
|
840
|
+
}
|
|
841
|
+
function botAuthorIds(bots) {
|
|
842
|
+
return bots.flatMap((bot) => [bot.id, bot.applicationId].filter((id) => Boolean(id)));
|
|
843
|
+
}
|
|
844
|
+
function collectImageAttachments(message) {
|
|
845
|
+
if (!message.attachments)
|
|
846
|
+
return undefined;
|
|
847
|
+
const images = [];
|
|
848
|
+
for (const attachment of message.attachments.values()) {
|
|
849
|
+
if (!attachment.url)
|
|
850
|
+
continue;
|
|
851
|
+
const contentType = attachment.contentType ?? undefined;
|
|
852
|
+
if (isImageAttachment(contentType, attachment.name ?? undefined)) {
|
|
853
|
+
images.push({
|
|
854
|
+
url: attachment.url,
|
|
855
|
+
contentType,
|
|
856
|
+
contentLengthBytes: attachment.size ?? undefined
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return images.length ? images : undefined;
|
|
861
|
+
}
|
|
862
|
+
function isImageAttachment(contentType, name) {
|
|
863
|
+
if (contentType)
|
|
864
|
+
return contentType.startsWith("image/");
|
|
865
|
+
return /\.(png|jpe?g|webp|gif)$/i.test(name ?? "");
|
|
866
|
+
}
|
|
867
|
+
async function resolveReplyReference(message) {
|
|
868
|
+
if (!message.reference?.messageId)
|
|
869
|
+
return undefined;
|
|
870
|
+
if (!message.fetchReference) {
|
|
871
|
+
return { messageId: message.reference.messageId };
|
|
872
|
+
}
|
|
873
|
+
const referenced = await message.fetchReference();
|
|
874
|
+
return {
|
|
875
|
+
messageId: message.reference.messageId,
|
|
876
|
+
authorName: referenced.member?.displayName ?? referenced.author.globalName ?? referenced.author.username,
|
|
877
|
+
contentPreview: referenced.content.slice(0, 160)
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
async function summarizeReactions(reactions, cache) {
|
|
881
|
+
const membersById = new Map((cache.members ?? []).map((member) => [member.userId, member]));
|
|
882
|
+
const emojisById = new Map((cache.emojis ?? []).map((emoji) => [emoji.id, emoji]));
|
|
883
|
+
return Promise.all([...reactions].map(async (reaction) => {
|
|
884
|
+
let users;
|
|
885
|
+
if (reaction.users) {
|
|
886
|
+
try {
|
|
887
|
+
const fetched = await reaction.users.fetch({ limit: 20 });
|
|
888
|
+
users = [...fetched.values()].map((user) => {
|
|
889
|
+
const member = membersById.get(user.id);
|
|
890
|
+
return member?.guildDisplayName ?? member?.globalDisplayName ?? user.globalName ?? user.username;
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
users = undefined;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
emoji: modelVisibleReactionEmoji(reaction.emoji, emojisById),
|
|
899
|
+
count: reaction.count ?? 0,
|
|
900
|
+
users
|
|
901
|
+
};
|
|
902
|
+
}));
|
|
903
|
+
}
|
|
904
|
+
function modelVisibleReactionEmoji(emoji, emojisById) {
|
|
905
|
+
if (emoji.id) {
|
|
906
|
+
const cached = emojisById.get(emoji.id);
|
|
907
|
+
if (cached) {
|
|
908
|
+
return `<${cached.animated ? "a" : ""}:${cached.name}:>`;
|
|
909
|
+
}
|
|
910
|
+
return emoji.name ? `:${emoji.name}:` : emoji.identifier;
|
|
911
|
+
}
|
|
912
|
+
return emoji.name ?? emoji.identifier;
|
|
913
|
+
}
|
|
914
|
+
function uniqueMentionedRoles(messages, cachedRoles) {
|
|
915
|
+
const cachedById = new Map(cachedRoles.map((role) => [role.id, role]));
|
|
916
|
+
const rolesById = new Map();
|
|
917
|
+
for (const message of messages) {
|
|
918
|
+
for (const role of message.mentions?.roles?.values() ?? []) {
|
|
919
|
+
rolesById.set(role.id, cachedById.get(role.id) ?? {
|
|
920
|
+
id: role.id,
|
|
921
|
+
name: role.name,
|
|
922
|
+
color: 0,
|
|
923
|
+
hoist: false,
|
|
924
|
+
mentionable: false,
|
|
925
|
+
managed: false,
|
|
926
|
+
fetchedAt: new Date().toISOString()
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return [...rolesById.values()];
|
|
931
|
+
}
|
|
932
|
+
function mergeRoleCaches(primary, fallback) {
|
|
933
|
+
const rolesById = new Map(fallback.map((role) => [role.id, role]));
|
|
934
|
+
for (const role of primary) {
|
|
935
|
+
rolesById.set(role.id, role);
|
|
936
|
+
}
|
|
937
|
+
return [...rolesById.values()];
|
|
938
|
+
}
|
|
939
|
+
function throwIfAborted(signal) {
|
|
940
|
+
if (signal?.aborted) {
|
|
941
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Discord request aborted.");
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const SNOWFLAKE_PATTERN = /^\d{17,20}$/;
|
|
945
|
+
const MENTION_REFRESH_COOLDOWN_MS = 5 * 60 * 1000;
|
|
946
|
+
const REVIEW_COMMAND_NAME = "review";
|
|
947
|
+
const CLEAR_COMMAND_NAME = "clear";
|
|
948
|
+
const RUN_COMMAND_NAME = "run";
|
|
949
|
+
const STOP_COMMAND_NAME = "stop";
|
|
950
|
+
const MEMORIES_COMMAND_NAME = "memories";
|
|
951
|
+
const PRINT_COMMAND_NAME = "print";
|
|
952
|
+
const DEBUG_COMMAND_NAME = "console";
|
|
953
|
+
const LEGACY_DEBUG_COMMAND_NAMES = ["debug"];
|
|
954
|
+
const CLEAR_COUNT_OPTION_NAME = "count";
|
|
955
|
+
const CLEAR_TYPE_OPTION_NAME = "type";
|
|
956
|
+
const PRINT_TYPE_OPTION_NAME = "type";
|
|
957
|
+
const PRINT_WAIFU_OPTION_NAME = "waifu";
|
|
958
|
+
const DEBUG_TYPE_OPTION_NAME = "type";
|
|
959
|
+
const DEBUG_CHANNEL_ID_OPTION_NAME = "channel_id";
|
|
960
|
+
const RUN_WAIFU_OPTION_NAME = "waifu";
|
|
961
|
+
const RUN_SCENE_DIRECTION_OPTION_NAME = "scene_direction";
|
|
962
|
+
const MAX_CLEAR_COUNT = 100;
|
|
963
|
+
const MAX_AUTOCOMPLETE_CHOICES = 25;
|
|
964
|
+
const DISCORD_BULK_DELETE_MAX_MESSAGES = 100;
|
|
965
|
+
const DISCORD_BULK_DELETE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
966
|
+
const ADMIN_COMMAND_PERMISSIONS = PermissionFlagsBits.Administrator;
|
|
967
|
+
function sanitizeReplyTarget(value) {
|
|
968
|
+
return value && SNOWFLAKE_PATTERN.test(value) ? value : undefined;
|
|
969
|
+
}
|
|
970
|
+
function sanitizeRunString(value) {
|
|
971
|
+
const trimmed = value?.trim();
|
|
972
|
+
return trimmed || undefined;
|
|
973
|
+
}
|
|
974
|
+
function parseDebugCommandType(value) {
|
|
975
|
+
return value === "set" || value === "unset" || value === "print" ? value : undefined;
|
|
976
|
+
}
|
|
977
|
+
function parsePrintCommandType(value) {
|
|
978
|
+
return value === "system_prompt" || value === "memories" || value === "personality" ? value : undefined;
|
|
979
|
+
}
|
|
980
|
+
function errorMessage(error) {
|
|
981
|
+
return error instanceof Error ? error.message : String(error);
|
|
982
|
+
}
|
|
983
|
+
function closeEventCode(event) {
|
|
984
|
+
return typeof event.code === "string" || typeof event.code === "number" ? event.code : undefined;
|
|
985
|
+
}
|
|
986
|
+
function closeEventReason(event) {
|
|
987
|
+
return typeof event.reason === "string" && event.reason.trim() ? event.reason.trim() : undefined;
|
|
988
|
+
}
|
|
989
|
+
function closeEventWasClean(event) {
|
|
990
|
+
return typeof event.wasClean === "boolean" ? event.wasClean : undefined;
|
|
991
|
+
}
|
|
992
|
+
function runtimeIssueLogContext(issue) {
|
|
993
|
+
return {
|
|
994
|
+
source: issue.source,
|
|
995
|
+
botId: issue.botId,
|
|
996
|
+
shardId: issue.shardId,
|
|
997
|
+
code: issue.code,
|
|
998
|
+
wasClean: issue.wasClean,
|
|
999
|
+
message: issue.message
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function interactionLogContext(interaction) {
|
|
1003
|
+
return {
|
|
1004
|
+
commandName: interaction.commandName,
|
|
1005
|
+
guildId: interaction.guildId,
|
|
1006
|
+
channelId: interaction.channelId,
|
|
1007
|
+
userId: interaction.user.id
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
function slashCommandLogContext(event) {
|
|
1011
|
+
return {
|
|
1012
|
+
guildId: event.guildId,
|
|
1013
|
+
channelId: event.channelId,
|
|
1014
|
+
userId: event.userId,
|
|
1015
|
+
commandMessageId: event.commandMessageId
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function isBulkDeletableMessage(message) {
|
|
1019
|
+
return message.createdTimestamp === undefined || Date.now() - message.createdTimestamp < DISCORD_BULK_DELETE_MAX_AGE_MS;
|
|
1020
|
+
}
|
|
1021
|
+
function oldestMessageId(messages) {
|
|
1022
|
+
if (messages.length === 0)
|
|
1023
|
+
return undefined;
|
|
1024
|
+
if (messages.some((message) => typeof message.createdTimestamp !== "number")) {
|
|
1025
|
+
return messages[messages.length - 1]?.id;
|
|
1026
|
+
}
|
|
1027
|
+
let oldest = messages[0];
|
|
1028
|
+
for (const message of messages.slice(1)) {
|
|
1029
|
+
if ((message.createdTimestamp ?? 0) < (oldest.createdTimestamp ?? 0)) {
|
|
1030
|
+
oldest = message;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return oldest.id;
|
|
1034
|
+
}
|
|
1035
|
+
function chunks(values, size) {
|
|
1036
|
+
const result = [];
|
|
1037
|
+
for (let index = 0; index < values.length; index += size) {
|
|
1038
|
+
result.push(values.slice(index, index + size));
|
|
1039
|
+
}
|
|
1040
|
+
return result;
|
|
1041
|
+
}
|
|
1042
|
+
async function registerOrchestratorCommands(client, logger) {
|
|
1043
|
+
try {
|
|
1044
|
+
const manager = client.application?.commands;
|
|
1045
|
+
if (!manager)
|
|
1046
|
+
return;
|
|
1047
|
+
const payloads = buildOrchestratorCommandPayloads();
|
|
1048
|
+
const commands = await manager.fetch();
|
|
1049
|
+
for (const payload of payloads) {
|
|
1050
|
+
const existing = commands.find((command) => command.name === payload.name);
|
|
1051
|
+
if (existing) {
|
|
1052
|
+
await manager.edit(existing.id, payload);
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
await manager.create(payload);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const payloadNames = new Set(payloads.map((payload) => payload.name));
|
|
1059
|
+
for (const legacyName of LEGACY_DEBUG_COMMAND_NAMES) {
|
|
1060
|
+
if (payloadNames.has(legacyName))
|
|
1061
|
+
continue;
|
|
1062
|
+
const existing = commands.find((command) => command.name === legacyName);
|
|
1063
|
+
if (existing) {
|
|
1064
|
+
await manager.delete(existing.id);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
logger?.warn("Failed to register orchestrator slash commands", {
|
|
1070
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
export function buildOrchestratorCommandPayloads() {
|
|
1075
|
+
return [
|
|
1076
|
+
{
|
|
1077
|
+
name: REVIEW_COMMAND_NAME,
|
|
1078
|
+
description: "Review and remove the latest waifu message if it leaked hallucinated internals.",
|
|
1079
|
+
defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
name: CLEAR_COMMAND_NAME,
|
|
1083
|
+
description: "Delete channel messages without running reviewer judgment.",
|
|
1084
|
+
defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS,
|
|
1085
|
+
options: [
|
|
1086
|
+
{
|
|
1087
|
+
type: ApplicationCommandOptionType.String,
|
|
1088
|
+
name: CLEAR_TYPE_OPTION_NAME,
|
|
1089
|
+
description: "Which messages to clear.",
|
|
1090
|
+
required: true,
|
|
1091
|
+
choices: [
|
|
1092
|
+
{ name: "waifus", value: "waifus" },
|
|
1093
|
+
{ name: "users", value: "users" },
|
|
1094
|
+
{ name: "both", value: "both" },
|
|
1095
|
+
{ name: "everything", value: "everything" }
|
|
1096
|
+
]
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
type: ApplicationCommandOptionType.Integer,
|
|
1100
|
+
name: CLEAR_COUNT_OPTION_NAME,
|
|
1101
|
+
description: "How many latest messages to delete for waifus, users, or both.",
|
|
1102
|
+
required: false,
|
|
1103
|
+
min_value: 1,
|
|
1104
|
+
max_value: MAX_CLEAR_COUNT
|
|
1105
|
+
}
|
|
1106
|
+
]
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
name: RUN_COMMAND_NAME,
|
|
1110
|
+
description: "Run the orchestrator in this channel immediately if the runtime is idle.",
|
|
1111
|
+
options: [
|
|
1112
|
+
{
|
|
1113
|
+
type: ApplicationCommandOptionType.String,
|
|
1114
|
+
name: RUN_WAIFU_OPTION_NAME,
|
|
1115
|
+
description: "Optional waifu id or display name to speak first.",
|
|
1116
|
+
required: false,
|
|
1117
|
+
autocomplete: true
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
type: ApplicationCommandOptionType.String,
|
|
1121
|
+
name: RUN_SCENE_DIRECTION_OPTION_NAME,
|
|
1122
|
+
description: "Optional private scene direction for the selected waifu.",
|
|
1123
|
+
required: false
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
name: STOP_COMMAND_NAME,
|
|
1129
|
+
description: "Stop the current orchestrator and waifu work in this channel.",
|
|
1130
|
+
defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
name: MEMORIES_COMMAND_NAME,
|
|
1134
|
+
description: "Run the stage manager for this channel and update guild memories.",
|
|
1135
|
+
defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
name: PRINT_COMMAND_NAME,
|
|
1139
|
+
description: "Print a configured waifu field for this server.",
|
|
1140
|
+
defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS,
|
|
1141
|
+
options: [
|
|
1142
|
+
{
|
|
1143
|
+
type: ApplicationCommandOptionType.String,
|
|
1144
|
+
name: PRINT_TYPE_OPTION_NAME,
|
|
1145
|
+
description: "Which waifu field to print.",
|
|
1146
|
+
required: true,
|
|
1147
|
+
choices: [
|
|
1148
|
+
{ name: "system prompt", value: "system_prompt" },
|
|
1149
|
+
{ name: "memories", value: "memories" },
|
|
1150
|
+
{ name: "personality", value: "personality" }
|
|
1151
|
+
]
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
type: ApplicationCommandOptionType.String,
|
|
1155
|
+
name: PRINT_WAIFU_OPTION_NAME,
|
|
1156
|
+
description: "Waifu to inspect from this server.",
|
|
1157
|
+
required: true,
|
|
1158
|
+
autocomplete: true
|
|
1159
|
+
}
|
|
1160
|
+
]
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
name: DEBUG_COMMAND_NAME,
|
|
1164
|
+
description: "Route debug logs for a source channel.",
|
|
1165
|
+
defaultMemberPermissions: ADMIN_COMMAND_PERMISSIONS,
|
|
1166
|
+
options: [
|
|
1167
|
+
{
|
|
1168
|
+
type: ApplicationCommandOptionType.String,
|
|
1169
|
+
name: DEBUG_TYPE_OPTION_NAME,
|
|
1170
|
+
description: "Whether to set or unset debug output.",
|
|
1171
|
+
required: true,
|
|
1172
|
+
choices: [
|
|
1173
|
+
{ name: "set", value: "set" },
|
|
1174
|
+
{ name: "unset", value: "unset" }
|
|
1175
|
+
]
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
type: ApplicationCommandOptionType.String,
|
|
1179
|
+
name: DEBUG_CHANNEL_ID_OPTION_NAME,
|
|
1180
|
+
description: "Source channel ID for set/unset.",
|
|
1181
|
+
required: false
|
|
1182
|
+
}
|
|
1183
|
+
]
|
|
1184
|
+
}
|
|
1185
|
+
];
|
|
1186
|
+
}
|
|
1187
|
+
function parseClearType(value) {
|
|
1188
|
+
if (value === "waifus" || value === "users" || value === "both" || value === "everything") {
|
|
1189
|
+
return value;
|
|
1190
|
+
}
|
|
1191
|
+
if (value === "all") {
|
|
1192
|
+
return "both";
|
|
1193
|
+
}
|
|
1194
|
+
return undefined;
|
|
1195
|
+
}
|
|
1196
|
+
//# sourceMappingURL=client.js.map
|