pi-roundtable 0.9.3 → 0.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +4 -0
- package/README.zh-TW.md +4 -0
- package/docs/plugins.md +32 -6
- package/package.json +1 -1
- package/src/core/agents/agent-prompt.ts +1 -1
- package/src/core/builtin/discord-admin.ts +6 -2
- package/src/core/builtin/discord.ts +10 -0
- package/src/core/config/adapters.ts +47 -0
- package/src/core/config/config.ts +9 -34
- package/src/core/define-roundtable.ts +2 -0
- package/src/core/define.ts +8 -0
- package/src/core/discord/channel-files.ts +107 -0
- package/src/core/discord/channel-operations.ts +100 -33
- package/src/core/discord/discord-surface.ts +21 -4
- package/src/core/discord/dispatch-thread-host.ts +11 -0
- package/src/core/discord/dispatch-threads.ts +14 -0
- package/src/core/discord/inbound-message.ts +34 -0
- package/src/core/discord/owner-card-rows.ts +98 -0
- package/src/core/discord/owner-cards-fixture.ts +3 -0
- package/src/core/discord/owner-cards.ts +137 -105
- package/src/core/host.ts +9 -1
- package/src/core/i18n/cards.ts +6 -8
- package/src/core/i18n/conversation.ts +15 -0
- package/src/core/interactions/prompts.ts +36 -7
- package/src/core/modules/discord-admin/discord-admin.ts +16 -2
- package/src/core/plugin.ts +5 -0
- package/src/core/runtime/compaction-tiers.ts +61 -8
- package/src/core/runtime/conversation-sessions.ts +2 -0
- package/src/core/runtime/extensions/ask-user.ts +21 -4
- package/src/core/runtime/extensions/confirmation-gate.ts +73 -11
- package/src/core/runtime/prompt-slot.ts +6 -4
- package/src/core/runtime/runtime-types.ts +5 -0
- package/src/core/runtime/session-factory.ts +22 -2
- package/src/core/sessions.ts +6 -0
- package/src/core/shared/attach-file-tool.ts +72 -0
- package/src/core/shared/workspace-files.ts +76 -0
- package/src/core/testing/thread-host.ts +14 -0
- package/src/index.ts +2 -0
- package/src/kit/index.ts +2 -0
- package/src/kit/worker.ts +2 -0
- package/src/testing.ts +7 -1
|
@@ -38,7 +38,7 @@ const message = { channelId: id, messageId: id };
|
|
|
38
38
|
const strict = { additionalProperties: false } as const;
|
|
39
39
|
|
|
40
40
|
/** One upload may carry at most this much, in all files together. */
|
|
41
|
-
const CHANNEL_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
|
|
41
|
+
export const CHANNEL_UPLOAD_MAX_BYTES = 8 * 1024 * 1024;
|
|
42
42
|
|
|
43
43
|
const file = Type.Object(
|
|
44
44
|
{
|
|
@@ -70,12 +70,90 @@ const files = Type.Optional(
|
|
|
70
70
|
}),
|
|
71
71
|
);
|
|
72
72
|
|
|
73
|
+
/** A file a session sends: one on its disk by path, or small generated data inline. */
|
|
74
|
+
const localFile = Type.Object(
|
|
75
|
+
{
|
|
76
|
+
path: Type.Optional(
|
|
77
|
+
Type.String({
|
|
78
|
+
minLength: 1,
|
|
79
|
+
maxLength: 4096,
|
|
80
|
+
description:
|
|
81
|
+
"A file on disk inside the workspace or the scratch dir; prefer this for any file you have",
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
filename: Type.Optional(
|
|
85
|
+
Type.String({
|
|
86
|
+
...file.properties.filename,
|
|
87
|
+
description:
|
|
88
|
+
"The attachment file name, without a path; defaults to the path's base name, required with dataBase64",
|
|
89
|
+
}),
|
|
90
|
+
),
|
|
91
|
+
dataBase64: Type.Optional(
|
|
92
|
+
Type.String({
|
|
93
|
+
...file.properties.dataBase64,
|
|
94
|
+
description:
|
|
95
|
+
"Small generated content as standard base64, without a data URL prefix; never base64 a file you have on disk, give its path",
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
description: file.properties.description,
|
|
99
|
+
},
|
|
100
|
+
strict,
|
|
101
|
+
);
|
|
102
|
+
const localFiles = Type.Optional(
|
|
103
|
+
Type.Array(localFile, {
|
|
104
|
+
minItems: 1,
|
|
105
|
+
maxItems: 10,
|
|
106
|
+
description:
|
|
107
|
+
"Images or files to add, each with exactly one of path or dataBase64; at most 8 MiB in total for the whole call",
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
|
|
73
111
|
export interface ChannelTool {
|
|
74
112
|
readonly operation: ChannelOperation;
|
|
75
113
|
readonly schema: Readonly<TSchema>;
|
|
76
114
|
readonly description: string;
|
|
77
115
|
}
|
|
78
116
|
|
|
117
|
+
const sendMessage = (attached: typeof files | typeof localFiles) => ({
|
|
118
|
+
operation: "send" as const,
|
|
119
|
+
schema: Type.Object(
|
|
120
|
+
{
|
|
121
|
+
...channel,
|
|
122
|
+
content: Type.Optional(Type.String({ maxLength: 2000 })),
|
|
123
|
+
files: attached,
|
|
124
|
+
},
|
|
125
|
+
strict,
|
|
126
|
+
),
|
|
127
|
+
description:
|
|
128
|
+
"Send a message, optionally with images or files; it triggers no @ mentions.",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const editMessage = (attached: typeof files | typeof localFiles) => ({
|
|
132
|
+
operation: "edit" as const,
|
|
133
|
+
schema: Type.Object(
|
|
134
|
+
{
|
|
135
|
+
...message,
|
|
136
|
+
content: Type.Optional(Type.String({ maxLength: 2000 })),
|
|
137
|
+
files: attached,
|
|
138
|
+
keepAttachmentIds: Type.Optional(
|
|
139
|
+
Type.Array(id, {
|
|
140
|
+
maxItems: 10,
|
|
141
|
+
description:
|
|
142
|
+
"Omit to keep every old attachment; an empty array removes them all; listed ids keep only those attachments",
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
strict,
|
|
147
|
+
),
|
|
148
|
+
description: "Edit a message the bot itself sent.",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/** The tools that upload files; a session's files may name a path, which it reads before the call. */
|
|
152
|
+
export const FILE_CHANNEL_TOOLS: readonly string[] = Object.freeze([
|
|
153
|
+
"discord_send_message",
|
|
154
|
+
"discord_edit_message",
|
|
155
|
+
]);
|
|
156
|
+
|
|
79
157
|
/** Discord tool descriptions and schemas, each gated by one channel operation. */
|
|
80
158
|
export const CHANNEL_TOOLS: Readonly<Record<string, ChannelTool>> = freeze<
|
|
81
159
|
Record<string, ChannelTool>
|
|
@@ -164,38 +242,8 @@ export const CHANNEL_TOOLS: Readonly<Record<string, ChannelTool>> = freeze<
|
|
|
164
242
|
schema: Type.Object(channel, strict),
|
|
165
243
|
description: "Read the channel's pinned messages.",
|
|
166
244
|
},
|
|
167
|
-
discord_send_message:
|
|
168
|
-
|
|
169
|
-
schema: Type.Object(
|
|
170
|
-
{
|
|
171
|
-
...channel,
|
|
172
|
-
content: Type.Optional(Type.String({ maxLength: 2000 })),
|
|
173
|
-
files,
|
|
174
|
-
},
|
|
175
|
-
strict,
|
|
176
|
-
),
|
|
177
|
-
description:
|
|
178
|
-
"Send a message, optionally with images or files; it triggers no @ mentions.",
|
|
179
|
-
},
|
|
180
|
-
discord_edit_message: {
|
|
181
|
-
operation: "edit",
|
|
182
|
-
schema: Type.Object(
|
|
183
|
-
{
|
|
184
|
-
...message,
|
|
185
|
-
content: Type.Optional(Type.String({ maxLength: 2000 })),
|
|
186
|
-
files,
|
|
187
|
-
keepAttachmentIds: Type.Optional(
|
|
188
|
-
Type.Array(id, {
|
|
189
|
-
maxItems: 10,
|
|
190
|
-
description:
|
|
191
|
-
"Omit to keep every old attachment; an empty array removes them all; listed ids keep only those attachments",
|
|
192
|
-
}),
|
|
193
|
-
),
|
|
194
|
-
},
|
|
195
|
-
strict,
|
|
196
|
-
),
|
|
197
|
-
description: "Edit a message the bot itself sent.",
|
|
198
|
-
},
|
|
245
|
+
discord_send_message: sendMessage(files),
|
|
246
|
+
discord_edit_message: editMessage(files),
|
|
199
247
|
discord_delete_message: {
|
|
200
248
|
operation: "delete",
|
|
201
249
|
schema: Type.Object(message, strict),
|
|
@@ -251,6 +299,25 @@ export const CHANNEL_TOOLS: Readonly<Record<string, ChannelTool>> = freeze<
|
|
|
251
299
|
},
|
|
252
300
|
});
|
|
253
301
|
|
|
302
|
+
/**
|
|
303
|
+
* The channel tools as a session offers them: a file may name a path on the session's disk, which
|
|
304
|
+
* `resolveChannelFiles` reads into `CHANNEL_TOOLS`' inline form before the call runs.
|
|
305
|
+
*/
|
|
306
|
+
export const SESSION_CHANNEL_TOOLS: Readonly<Record<string, ChannelTool>> =
|
|
307
|
+
freeze<Record<string, ChannelTool>>({
|
|
308
|
+
...CHANNEL_TOOLS,
|
|
309
|
+
discord_send_message: {
|
|
310
|
+
...sendMessage(localFiles),
|
|
311
|
+
description:
|
|
312
|
+
"Send a message, optionally with images or files; it triggers no @ mentions. To send a file you have on disk, give its path; base64 is for small generated data only.",
|
|
313
|
+
},
|
|
314
|
+
discord_edit_message: {
|
|
315
|
+
...editMessage(localFiles),
|
|
316
|
+
description:
|
|
317
|
+
"Edit a message the bot itself sent. To add a file you have on disk, give its path; base64 is for small generated data only.",
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
254
321
|
/** A tool call that cannot be run as asked; the code is safe to show the caller. */
|
|
255
322
|
export class ChannelToolError extends Error {
|
|
256
323
|
constructor(readonly code: string) {
|
|
@@ -34,10 +34,9 @@ import type { ChannelExecutor } from "./channel-operations.ts";
|
|
|
34
34
|
import type { ComposedCommands } from "./compose-commands.ts";
|
|
35
35
|
import type { ChannelInfo, DiscordConnection } from "./connection.ts";
|
|
36
36
|
import { DiscordThreadHost } from "./dispatch-thread-host.ts";
|
|
37
|
-
import type { ThreadHost } from "./dispatch-threads.ts";
|
|
38
37
|
import { DEFAULT_FRESH_MARKER, postFreshMarker } from "./fresh-marker.ts";
|
|
39
|
-
import { toInbound } from "./inbound-message.ts";
|
|
40
|
-
import type { CardChannel } from "./owner-cards.ts";
|
|
38
|
+
import { lateAnswerMessage, toInbound } from "./inbound-message.ts";
|
|
39
|
+
import type { CardChannel, LateTurn } from "./owner-cards.ts";
|
|
41
40
|
import { DiscordOwnerOps } from "./owner-discord.ts";
|
|
42
41
|
import { stopPanel } from "./stop-button.ts";
|
|
43
42
|
|
|
@@ -86,6 +85,7 @@ export class DiscordSurface
|
|
|
86
85
|
});
|
|
87
86
|
|
|
88
87
|
#interactions: ComposedCommands = { commands: [], modules: [] };
|
|
88
|
+
#onMessage: ((message: InboundMessage) => void) | undefined;
|
|
89
89
|
|
|
90
90
|
constructor(options: DiscordSurfaceOptions) {
|
|
91
91
|
this.#options = options;
|
|
@@ -118,7 +118,7 @@ export class DiscordSurface
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/** Threads for background dispatches, in the channel that started each. */
|
|
121
|
-
threadHost():
|
|
121
|
+
threadHost(): DiscordThreadHost {
|
|
122
122
|
return new DiscordThreadHost(this.#client);
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -209,8 +209,25 @@ export class DiscordSurface
|
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Hands the router a card's late answer as a message from whoever answered, in the card's
|
|
214
|
+
* channel, so the conversation's claim answers it as theirs; dropped (logged) before start.
|
|
215
|
+
*/
|
|
216
|
+
resume(turn: LateTurn): void {
|
|
217
|
+
const onMessage = this.#onMessage;
|
|
218
|
+
if (!onMessage) {
|
|
219
|
+
this.#options.logger.warn(
|
|
220
|
+
{ channelId: turn.channelId },
|
|
221
|
+
"late card answer before the surface started; dropped",
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
onMessage(lateAnswerMessage(turn));
|
|
226
|
+
}
|
|
227
|
+
|
|
212
228
|
async start(onMessage: (message: InboundMessage) => void): Promise<void> {
|
|
213
229
|
const { logger, token } = this.#options;
|
|
230
|
+
this.#onMessage = onMessage;
|
|
214
231
|
this.#client.on(Events.MessageCreate, (message) => {
|
|
215
232
|
toInbound(message, {
|
|
216
233
|
botId: this.#client.user?.id,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type Message,
|
|
5
5
|
ThreadAutoArchiveDuration,
|
|
6
6
|
} from "discord.js";
|
|
7
|
+
import type { InterimMessage } from "../domain/interim.ts";
|
|
7
8
|
import { splitReply } from "../presentation/reply-splitter.ts";
|
|
8
9
|
import type { ThreadHost } from "./dispatch-threads.ts";
|
|
9
10
|
|
|
@@ -53,6 +54,16 @@ export class DiscordThreadHost implements ThreadHost {
|
|
|
53
54
|
for (const chunk of splitReply(text)) await thread.send({ content: chunk });
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
async send(threadId: string, text: string): Promise<InterimMessage> {
|
|
58
|
+
const thread = await this.#thread(threadId);
|
|
59
|
+
const message = await thread.send({ content: text });
|
|
60
|
+
return {
|
|
61
|
+
edit: async (change) => {
|
|
62
|
+
await message.edit({ content: change });
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
async close(threadId: string): Promise<void> {
|
|
57
68
|
const thread = await this.#thread(threadId);
|
|
58
69
|
// Locking needs Manage Threads; without it the thread is only archived.
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { dirname } from "node:path";
|
|
9
9
|
import type { ChannelKey } from "../domain/conversation.ts";
|
|
10
|
+
import type { InterimMessage, InterimPosts } from "../domain/interim.ts";
|
|
10
11
|
import { messages } from "../i18n/index.ts";
|
|
11
12
|
import type { Logger } from "../log.ts";
|
|
12
13
|
|
|
@@ -27,6 +28,11 @@ export interface ThreadHost {
|
|
|
27
28
|
line: (thread?: string) => string,
|
|
28
29
|
): Promise<string | undefined>;
|
|
29
30
|
post(threadId: string, text: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Posts one message of at most Discord's 2000 characters and resolves to it, so it can be
|
|
33
|
+
* edited in place; without it a thread has no `interim` posts.
|
|
34
|
+
*/
|
|
35
|
+
send?(threadId: string, text: string): Promise<InterimMessage>;
|
|
30
36
|
/** Archives the thread, and locks it when allowed. */
|
|
31
37
|
close(threadId: string): Promise<void>;
|
|
32
38
|
}
|
|
@@ -40,6 +46,12 @@ export interface DispatchThread {
|
|
|
40
46
|
readonly mention: string;
|
|
41
47
|
/** Posts in the thread; a failure is logged. */
|
|
42
48
|
post(text: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Where the dispatch posts its progress as it goes, under the same name as its report, such
|
|
51
|
+
* as a coding worker's interim text; absent when the host cannot edit a message it posted.
|
|
52
|
+
* Unlike `post`, a failure rejects.
|
|
53
|
+
*/
|
|
54
|
+
readonly interim?: InterimPosts;
|
|
43
55
|
/** Posts the final report, if any, then archives the thread. Runs once; never rejects. */
|
|
44
56
|
close(report?: string): Promise<void>;
|
|
45
57
|
}
|
|
@@ -132,6 +144,7 @@ export class DispatchThreads {
|
|
|
132
144
|
#thread(id: string): DispatchThread {
|
|
133
145
|
const { host, logger } = this.#options;
|
|
134
146
|
let closed = false;
|
|
147
|
+
const send = host.send?.bind(host);
|
|
135
148
|
const post = async (text: string) => {
|
|
136
149
|
try {
|
|
137
150
|
await host.post(id, text);
|
|
@@ -144,6 +157,7 @@ export class DispatchThreads {
|
|
|
144
157
|
channel: `${PREFIX}${id}`,
|
|
145
158
|
mention: `<#${id}>`,
|
|
146
159
|
post,
|
|
160
|
+
...(send ? { interim: { post: (text: string) => send(id, text) } } : {}),
|
|
147
161
|
close: async (report) => {
|
|
148
162
|
if (closed) return;
|
|
149
163
|
closed = true;
|
|
@@ -2,6 +2,7 @@ import { type Message, MessageReferenceType } from "discord.js";
|
|
|
2
2
|
import type { InboundMessage } from "../contract/channels.ts";
|
|
3
3
|
import { channelKey } from "../contract/surface.ts";
|
|
4
4
|
import type { AttachmentRef } from "../domain/attachment.ts";
|
|
5
|
+
import type { LateTurn } from "./owner-cards.ts";
|
|
5
6
|
|
|
6
7
|
/** Who the bot is on Discord, as far as the client knows it. */
|
|
7
8
|
export interface BotIdentity {
|
|
@@ -113,3 +114,36 @@ export async function toInbound(
|
|
|
113
114
|
...(referenced ? { reference: referenceOf(referenced, mention) } : {}),
|
|
114
115
|
};
|
|
115
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A card's late answer as a message from whoever answered, in the card's channel, standing in
|
|
120
|
+
* for the card's message: the router resolves its author as any other's, and the claim answers it.
|
|
121
|
+
*/
|
|
122
|
+
export function lateAnswerMessage(turn: LateTurn): InboundMessage {
|
|
123
|
+
const { user } = turn;
|
|
124
|
+
const roles = user.roleIds;
|
|
125
|
+
return {
|
|
126
|
+
channel: channelKey("discord", turn.channelId),
|
|
127
|
+
messageId: turn.messageId,
|
|
128
|
+
actor: {
|
|
129
|
+
provider: "discord",
|
|
130
|
+
subject: user.id,
|
|
131
|
+
name: user.name,
|
|
132
|
+
surface: "discord",
|
|
133
|
+
...(roles ? { roles: roles.map((role) => `discord:role:${role}`) } : {}),
|
|
134
|
+
...(turn.guildId ? { space: turn.guildId } : {}),
|
|
135
|
+
legacyId: user.id,
|
|
136
|
+
},
|
|
137
|
+
authorId: user.id,
|
|
138
|
+
authorName: user.name,
|
|
139
|
+
authorIsBot: false,
|
|
140
|
+
...(roles ? { authorRoleIds: roles } : {}),
|
|
141
|
+
...(turn.guildId ? { space: turn.guildId } : {}),
|
|
142
|
+
isDirect: turn.isDirect,
|
|
143
|
+
// Pressing the assistant's card speaks to it, as a mention does.
|
|
144
|
+
mentionsBot: true,
|
|
145
|
+
repliesToBot: true,
|
|
146
|
+
text: turn.text,
|
|
147
|
+
attachments: [],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionRowBuilder,
|
|
3
|
+
ButtonBuilder,
|
|
4
|
+
ButtonStyle,
|
|
5
|
+
LabelBuilder,
|
|
6
|
+
ModalBuilder,
|
|
7
|
+
StringSelectMenuBuilder,
|
|
8
|
+
TextInputBuilder,
|
|
9
|
+
TextInputStyle,
|
|
10
|
+
} from "discord.js";
|
|
11
|
+
import { messages } from "../i18n/index.ts";
|
|
12
|
+
import type { OwnerAnswer, OwnerQuestion } from "../interactions/prompts.ts";
|
|
13
|
+
import { type ownerPanel, plain } from "./owner-panel.ts";
|
|
14
|
+
|
|
15
|
+
/** The prefix of every card control's custom id. */
|
|
16
|
+
export const CARD_PREFIX = "roundtable:card:";
|
|
17
|
+
/** The menu value of "Other…". */
|
|
18
|
+
export const OTHER = "other";
|
|
19
|
+
/** The answer form's text field. */
|
|
20
|
+
export const TEXT_FIELD = "text";
|
|
21
|
+
|
|
22
|
+
export type Rows = NonNullable<Parameters<typeof ownerPanel>[0]["rows"]>;
|
|
23
|
+
|
|
24
|
+
/** A question's controls: a menu of its options, or a button that opens the answer form. */
|
|
25
|
+
export function askRows(
|
|
26
|
+
id: string,
|
|
27
|
+
question: OwnerQuestion,
|
|
28
|
+
disabled: boolean,
|
|
29
|
+
): Rows {
|
|
30
|
+
if (question.options.length === 0)
|
|
31
|
+
return [
|
|
32
|
+
new ActionRowBuilder<ButtonBuilder>().addComponents(
|
|
33
|
+
new ButtonBuilder()
|
|
34
|
+
.setCustomId(`${CARD_PREFIX}${id}:write`)
|
|
35
|
+
.setLabel(messages().cardAnswerLabel)
|
|
36
|
+
.setStyle(ButtonStyle.Primary)
|
|
37
|
+
.setDisabled(disabled),
|
|
38
|
+
),
|
|
39
|
+
];
|
|
40
|
+
// Discord menus take 25 options; "Other…" takes the last place when the options fill it.
|
|
41
|
+
const options = question.options
|
|
42
|
+
.slice(0, question.allowOther ? 24 : 25)
|
|
43
|
+
.map((option, i) => ({
|
|
44
|
+
label: option.label.slice(0, 100),
|
|
45
|
+
value: String(i),
|
|
46
|
+
...(option.description
|
|
47
|
+
? { description: option.description.slice(0, 100) }
|
|
48
|
+
: {}),
|
|
49
|
+
}));
|
|
50
|
+
if (question.allowOther)
|
|
51
|
+
options.push({
|
|
52
|
+
label: messages().cardOtherLabel,
|
|
53
|
+
value: OTHER,
|
|
54
|
+
description: messages().cardOtherDescription,
|
|
55
|
+
});
|
|
56
|
+
return [
|
|
57
|
+
new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
|
|
58
|
+
new StringSelectMenuBuilder()
|
|
59
|
+
.setCustomId(`${CARD_PREFIX}${id}:pick`)
|
|
60
|
+
.setPlaceholder(
|
|
61
|
+
question.multi
|
|
62
|
+
? messages().cardPickManyPlaceholder
|
|
63
|
+
: messages().cardPickOnePlaceholder,
|
|
64
|
+
)
|
|
65
|
+
.setMinValues(1)
|
|
66
|
+
.setMaxValues(question.multi ? options.length : 1)
|
|
67
|
+
.addOptions(options)
|
|
68
|
+
.setDisabled(disabled),
|
|
69
|
+
),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function answerModal(id: string): ModalBuilder {
|
|
74
|
+
return new ModalBuilder()
|
|
75
|
+
.setCustomId(`${CARD_PREFIX}${id}:text`)
|
|
76
|
+
.setTitle(messages().cardModalTitle)
|
|
77
|
+
.addLabelComponents(
|
|
78
|
+
new LabelBuilder()
|
|
79
|
+
.setLabel(messages().cardModalField)
|
|
80
|
+
.setTextInputComponent(
|
|
81
|
+
new TextInputBuilder()
|
|
82
|
+
.setCustomId(TEXT_FIELD)
|
|
83
|
+
.setStyle(TextInputStyle.Paragraph)
|
|
84
|
+
.setRequired(true)
|
|
85
|
+
.setMaxLength(1000),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function answeredLine(answer: OwnerAnswer): string {
|
|
91
|
+
const parts = [
|
|
92
|
+
...answer.choices.map(plain),
|
|
93
|
+
...(answer.text !== undefined
|
|
94
|
+
? [messages().cardQuote(plain(answer.text))]
|
|
95
|
+
: []),
|
|
96
|
+
];
|
|
97
|
+
return messages().cardAnswered(parts);
|
|
98
|
+
}
|
|
@@ -56,6 +56,9 @@ export function press(
|
|
|
56
56
|
isModalSubmit: () => kind === "modal",
|
|
57
57
|
isFromMessage: () => true,
|
|
58
58
|
customId,
|
|
59
|
+
inGuild: () => true,
|
|
60
|
+
guildId: "300000000000000003",
|
|
61
|
+
message: { id: "400000000000000004" },
|
|
59
62
|
user: { id: options.user ?? OWNER },
|
|
60
63
|
member: {
|
|
61
64
|
roles: { cache: new Map((options.roles ?? []).map((r) => [r, r])) },
|