@spinabot/brigade 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/agent-loop.d.ts.map +1 -1
- package/dist/agents/agent-loop.js +51 -1
- package/dist/agents/agent-loop.js.map +1 -1
- package/dist/agents/channels/bundled-channel-metas.d.ts +2 -0
- package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
- package/dist/agents/channels/bundled-channel-metas.js +11 -0
- package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
- package/dist/agents/channels/discord/account-config.d.ts +177 -0
- package/dist/agents/channels/discord/account-config.d.ts.map +1 -0
- package/dist/agents/channels/discord/account-config.js +349 -0
- package/dist/agents/channels/discord/account-config.js.map +1 -0
- package/dist/agents/channels/discord/adapter.d.ts +79 -0
- package/dist/agents/channels/discord/adapter.d.ts.map +1 -0
- package/dist/agents/channels/discord/adapter.js +693 -0
- package/dist/agents/channels/discord/adapter.js.map +1 -0
- package/dist/agents/channels/discord/approval-authorize.d.ts +43 -0
- package/dist/agents/channels/discord/approval-authorize.d.ts.map +1 -0
- package/dist/agents/channels/discord/approval-authorize.js +71 -0
- package/dist/agents/channels/discord/approval-authorize.js.map +1 -0
- package/dist/agents/channels/discord/approval-native.d.ts +68 -0
- package/dist/agents/channels/discord/approval-native.d.ts.map +1 -0
- package/dist/agents/channels/discord/approval-native.js +81 -0
- package/dist/agents/channels/discord/approval-native.js.map +1 -0
- package/dist/agents/channels/discord/command-menu.d.ts +49 -0
- package/dist/agents/channels/discord/command-menu.d.ts.map +1 -0
- package/dist/agents/channels/discord/command-menu.js +73 -0
- package/dist/agents/channels/discord/command-menu.js.map +1 -0
- package/dist/agents/channels/discord/component-blocks.d.ts +108 -0
- package/dist/agents/channels/discord/component-blocks.d.ts.map +1 -0
- package/dist/agents/channels/discord/component-blocks.js +113 -0
- package/dist/agents/channels/discord/component-blocks.js.map +1 -0
- package/dist/agents/channels/discord/components.d.ts +175 -0
- package/dist/agents/channels/discord/components.d.ts.map +1 -0
- package/dist/agents/channels/discord/components.js +220 -0
- package/dist/agents/channels/discord/components.js.map +1 -0
- package/dist/agents/channels/discord/connection.d.ts +570 -0
- package/dist/agents/channels/discord/connection.d.ts.map +1 -0
- package/dist/agents/channels/discord/connection.js +1600 -0
- package/dist/agents/channels/discord/connection.js.map +1 -0
- package/dist/agents/channels/discord/directory-cache.d.ts +47 -0
- package/dist/agents/channels/discord/directory-cache.d.ts.map +1 -0
- package/dist/agents/channels/discord/directory-cache.js +131 -0
- package/dist/agents/channels/discord/directory-cache.js.map +1 -0
- package/dist/agents/channels/discord/directory-live.d.ts +61 -0
- package/dist/agents/channels/discord/directory-live.d.ts.map +1 -0
- package/dist/agents/channels/discord/directory-live.js +140 -0
- package/dist/agents/channels/discord/directory-live.js.map +1 -0
- package/dist/agents/channels/discord/draft-stream.d.ts +92 -0
- package/dist/agents/channels/discord/draft-stream.d.ts.map +1 -0
- package/dist/agents/channels/discord/draft-stream.js +213 -0
- package/dist/agents/channels/discord/draft-stream.js.map +1 -0
- package/dist/agents/channels/discord/format.d.ts +70 -0
- package/dist/agents/channels/discord/format.d.ts.map +1 -0
- package/dist/agents/channels/discord/format.js +303 -0
- package/dist/agents/channels/discord/format.js.map +1 -0
- package/dist/agents/channels/discord/guilds.d.ts +25 -0
- package/dist/agents/channels/discord/guilds.d.ts.map +1 -0
- package/dist/agents/channels/discord/guilds.js +46 -0
- package/dist/agents/channels/discord/guilds.js.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts +377 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.js +589 -0
- package/dist/agents/channels/discord/inbound-extras.js.map +1 -0
- package/dist/agents/channels/discord/index.d.ts +21 -0
- package/dist/agents/channels/discord/index.d.ts.map +1 -0
- package/dist/agents/channels/discord/index.js +21 -0
- package/dist/agents/channels/discord/index.js.map +1 -0
- package/dist/agents/channels/discord/media.d.ts +85 -0
- package/dist/agents/channels/discord/media.d.ts.map +1 -0
- package/dist/agents/channels/discord/media.js +242 -0
- package/dist/agents/channels/discord/media.js.map +1 -0
- package/dist/agents/channels/discord/modal-registry.d.ts +89 -0
- package/dist/agents/channels/discord/modal-registry.d.ts.map +1 -0
- package/dist/agents/channels/discord/modal-registry.js +104 -0
- package/dist/agents/channels/discord/modal-registry.js.map +1 -0
- package/dist/agents/channels/discord/modals.d.ts +100 -0
- package/dist/agents/channels/discord/modals.d.ts.map +1 -0
- package/dist/agents/channels/discord/modals.js +124 -0
- package/dist/agents/channels/discord/modals.js.map +1 -0
- package/dist/agents/channels/discord/module.d.ts +15 -0
- package/dist/agents/channels/discord/module.d.ts.map +1 -0
- package/dist/agents/channels/discord/module.js +22 -0
- package/dist/agents/channels/discord/module.js.map +1 -0
- package/dist/agents/channels/discord/permission-audit.d.ts +43 -0
- package/dist/agents/channels/discord/permission-audit.d.ts.map +1 -0
- package/dist/agents/channels/discord/permission-audit.js +192 -0
- package/dist/agents/channels/discord/permission-audit.js.map +1 -0
- package/dist/agents/channels/discord/plugin.d.ts +89 -0
- package/dist/agents/channels/discord/plugin.d.ts.map +1 -0
- package/dist/agents/channels/discord/plugin.js +372 -0
- package/dist/agents/channels/discord/plugin.js.map +1 -0
- package/dist/agents/channels/discord/probe.d.ts +115 -0
- package/dist/agents/channels/discord/probe.d.ts.map +1 -0
- package/dist/agents/channels/discord/probe.js +193 -0
- package/dist/agents/channels/discord/probe.js.map +1 -0
- package/dist/agents/channels/discord/reasoning-lane.d.ts +42 -0
- package/dist/agents/channels/discord/reasoning-lane.d.ts.map +1 -0
- package/dist/agents/channels/discord/reasoning-lane.js +68 -0
- package/dist/agents/channels/discord/reasoning-lane.js.map +1 -0
- package/dist/agents/channels/discord/rest-actions.d.ts +346 -0
- package/dist/agents/channels/discord/rest-actions.d.ts.map +1 -0
- package/dist/agents/channels/discord/rest-actions.js +559 -0
- package/dist/agents/channels/discord/rest-actions.js.map +1 -0
- package/dist/agents/channels/discord/rest-components.d.ts +122 -0
- package/dist/agents/channels/discord/rest-components.d.ts.map +1 -0
- package/dist/agents/channels/discord/rest-components.js +243 -0
- package/dist/agents/channels/discord/rest-components.js.map +1 -0
- package/dist/agents/channels/discord/security-audit.d.ts +29 -0
- package/dist/agents/channels/discord/security-audit.d.ts.map +1 -0
- package/dist/agents/channels/discord/security-audit.js +94 -0
- package/dist/agents/channels/discord/security-audit.js.map +1 -0
- package/dist/agents/channels/discord/security-doctor.d.ts +43 -0
- package/dist/agents/channels/discord/security-doctor.d.ts.map +1 -0
- package/dist/agents/channels/discord/security-doctor.js +83 -0
- package/dist/agents/channels/discord/security-doctor.js.map +1 -0
- package/dist/agents/channels/discord/status-issues.d.ts +37 -0
- package/dist/agents/channels/discord/status-issues.d.ts.map +1 -0
- package/dist/agents/channels/discord/status-issues.js +66 -0
- package/dist/agents/channels/discord/status-issues.js.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts +57 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.js +98 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.js.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding.d.ts +95 -0
- package/dist/agents/channels/discord/subagent-thread-binding.d.ts.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding.js +208 -0
- package/dist/agents/channels/discord/subagent-thread-binding.js.map +1 -0
- package/dist/agents/channels/discord/system-events.d.ts +31 -0
- package/dist/agents/channels/discord/system-events.d.ts.map +1 -0
- package/dist/agents/channels/discord/system-events.js +74 -0
- package/dist/agents/channels/discord/system-events.js.map +1 -0
- package/dist/agents/channels/general-callback.d.ts +12 -0
- package/dist/agents/channels/general-callback.d.ts.map +1 -1
- package/dist/agents/channels/general-callback.js +18 -0
- package/dist/agents/channels/general-callback.js.map +1 -1
- package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
- package/dist/agents/channels/inbound-pipeline.js +70 -10
- package/dist/agents/channels/inbound-pipeline.js.map +1 -1
- package/dist/agents/channels/sdk.d.ts +2 -0
- package/dist/agents/channels/sdk.d.ts.map +1 -1
- package/dist/agents/channels/sdk.js +2 -0
- package/dist/agents/channels/sdk.js.map +1 -1
- package/dist/agents/extensions/modules/index.d.ts.map +1 -1
- package/dist/agents/extensions/modules/index.js +5 -0
- package/dist/agents/extensions/modules/index.js.map +1 -1
- package/dist/agents/extensions/types.d.ts +7 -0
- package/dist/agents/extensions/types.d.ts.map +1 -1
- package/dist/agents/extensions/types.js.map +1 -1
- package/dist/agents/subagent-announce-delivery.d.ts +10 -0
- package/dist/agents/subagent-announce-delivery.d.ts.map +1 -1
- package/dist/agents/subagent-announce-delivery.js +1 -0
- package/dist/agents/subagent-announce-delivery.js.map +1 -1
- package/dist/agents/subagent-completion-bridge.d.ts.map +1 -1
- package/dist/agents/subagent-completion-bridge.js +81 -0
- package/dist/agents/subagent-completion-bridge.js.map +1 -1
- package/dist/agents/subagent-spawn.d.ts.map +1 -1
- package/dist/agents/subagent-spawn.js +57 -4
- package/dist/agents/subagent-spawn.js.map +1 -1
- package/dist/agents/tools/cron-tool.d.ts.map +1 -1
- package/dist/agents/tools/cron-tool.js +4 -1
- package/dist/agents/tools/cron-tool.js.map +1 -1
- package/dist/agents/tools/discord-action-tool.d.ts +224 -0
- package/dist/agents/tools/discord-action-tool.d.ts.map +1 -0
- package/dist/agents/tools/discord-action-tool.js +848 -0
- package/dist/agents/tools/discord-action-tool.js.map +1 -0
- package/dist/agents/tools/registry.d.ts.map +1 -1
- package/dist/agents/tools/registry.js +21 -0
- package/dist/agents/tools/registry.js.map +1 -1
- package/dist/agents/tools/sessions/index.d.ts +8 -0
- package/dist/agents/tools/sessions/index.d.ts.map +1 -1
- package/dist/agents/tools/sessions/index.js +15 -3
- package/dist/agents/tools/sessions/index.js.map +1 -1
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/channels.d.ts +2 -0
- package/dist/cli/commands/channels.d.ts.map +1 -1
- package/dist/cli/commands/channels.js +58 -1
- package/dist/cli/commands/channels.js.map +1 -1
- package/dist/core/auth-bridge.d.ts +1 -0
- package/dist/core/auth-bridge.d.ts.map +1 -1
- package/dist/core/auth-bridge.js +46 -1
- package/dist/core/auth-bridge.js.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +18 -2
- package/dist/core/server.js.map +1 -1
- package/dist/cron/isolated-agent/run-executor.d.ts +11 -0
- package/dist/cron/isolated-agent/run-executor.d.ts.map +1 -1
- package/dist/cron/isolated-agent/run-executor.js +20 -4
- package/dist/cron/isolated-agent/run-executor.js.map +1 -1
- package/dist/cron/types.d.ts +8 -0
- package/dist/cron/types.d.ts.map +1 -1
- package/dist/system-prompt/assembler.d.ts.map +1 -1
- package/dist/system-prompt/assembler.js +4 -2
- package/dist/system-prompt/assembler.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `discord_action` — owner-only Discord guild action surface (Phase 4).
|
|
3
|
+
*
|
|
4
|
+
* Brigade's live Discord adapter handles inbound + the everyday send/edit/react
|
|
5
|
+
* path, but it does NOT expose the wide guild-management surface a full Discord
|
|
6
|
+
* bot needs: creating/editing channels + categories, role + member management, emoji /
|
|
7
|
+
* sticker / scheduled-event admin, rich embeds + polls + stickers + threads +
|
|
8
|
+
* search, and moderation (ban / unban / kick / timeout). This tool fills that
|
|
9
|
+
* gap with a SELF-CONTAINED Discord REST v10 client — exactly like `probe.ts`
|
|
10
|
+
* and Slack's `directory-live.ts`: it resolves the bot token via
|
|
11
|
+
* `resolveDiscordBotToken` and talks straight to `https://discord.com/api/v10`
|
|
12
|
+
* with `Authorization: Bot <token>`. It never reaches through the live
|
|
13
|
+
* adapter/connection, so it works the same whether or not the Gateway socket is
|
|
14
|
+
* up.
|
|
15
|
+
*
|
|
16
|
+
* One meta-tool with an `action` discriminator (keeps the prompt small) dispatches
|
|
17
|
+
* to the matching helper in `discord/rest-actions.ts`. Owner-only: the standard
|
|
18
|
+
* `wrapOwnerOnlyToolExecution` gate (applied at session-wiring) already refuses
|
|
19
|
+
* non-owner senders and unattended cron turns, so this file does not re-implement
|
|
20
|
+
* auth. The registry only ASSEMBLES this tool when the Discord channel is
|
|
21
|
+
* configured (`channels.discord.enabled`), so a non-Discord install never sees it.
|
|
22
|
+
*
|
|
23
|
+
* Each action validates its required params, calls the REST helper, and returns a
|
|
24
|
+
* compact `jsonResult({ action, ok, … })`. REST failures decode (permissions /
|
|
25
|
+
* 404 / rate-limit / unknown-resource) into an operator-readable message via the
|
|
26
|
+
* `DiscordRestError` carried up from the helper.
|
|
27
|
+
*
|
|
28
|
+
* `set-presence` (Phase 5) is the ONE action that is NOT a REST call: presence
|
|
29
|
+
* is a Gateway (websocket) operation, and this self-contained REST tool holds no
|
|
30
|
+
* live discord.js client handle. The clean path chosen here is CONFIG-WRITE — it
|
|
31
|
+
* persists `channels.discord.presence` via `mutateConfigAtomic`, and the live
|
|
32
|
+
* connection applies that presence on its next (re)connect (it re-reads the
|
|
33
|
+
* resolved presence on every start). This keeps the tool stateless + air-gap-
|
|
34
|
+
* safe and avoids reaching across module boundaries into the running gateway.
|
|
35
|
+
*/
|
|
36
|
+
import { Type } from "typebox";
|
|
37
|
+
import { loadConfig } from "../../core/config.js";
|
|
38
|
+
import { mutateConfigAtomic } from "../../config/io.js";
|
|
39
|
+
import { DISCORD_DEFAULT_ACCOUNT_ID, resolveDiscordBotToken, } from "../channels/discord/account-config.js";
|
|
40
|
+
import { DiscordRestError, ban, categoryCreate, categoryDelete, categoryEdit, channelCreate, channelDelete, channelEdit, channelMove, emojiList, emojiUpload, eventCreate, eventList, kick, listReactions, listThreads, memberInfo, readMessages, removeReaction, roleAdd, roleInfo, roleList, roleRemove, searchMessages, sendEmbed, sendMessage, sendPoll, sendSticker, stickerUpload, threadCreate, timeout, unban, untimeout, } from "../channels/discord/rest-actions.js";
|
|
41
|
+
import { serializeDiscordModalTrigger, serializeDiscordSelectRow, serializeDiscordV2Message, } from "../channels/discord/rest-components.js";
|
|
42
|
+
import { jsonResult } from "./common.js";
|
|
43
|
+
/* ───────────────────────────── params ───────────────────────────── */
|
|
44
|
+
const EmbedSpecSchema = Type.Object({
|
|
45
|
+
title: Type.Optional(Type.String({ maxLength: 256 })),
|
|
46
|
+
description: Type.Optional(Type.String({ maxLength: 4096 })),
|
|
47
|
+
color: Type.Optional(Type.Number({ description: "Decimal color int, e.g. 5793266 (0x5865F2)." })),
|
|
48
|
+
url: Type.Optional(Type.String({ maxLength: 2048 })),
|
|
49
|
+
footer: Type.Optional(Type.String({ maxLength: 2048 })),
|
|
50
|
+
image: Type.Optional(Type.String({ description: "Image URL.", maxLength: 2048 })),
|
|
51
|
+
thumbnail: Type.Optional(Type.String({ description: "Thumbnail URL.", maxLength: 2048 })),
|
|
52
|
+
fields: Type.Optional(Type.Array(Type.Object({
|
|
53
|
+
name: Type.String({ maxLength: 256 }),
|
|
54
|
+
value: Type.String({ maxLength: 1024 }),
|
|
55
|
+
inline: Type.Optional(Type.Boolean()),
|
|
56
|
+
}), { maxItems: 25 })),
|
|
57
|
+
}, { additionalProperties: false });
|
|
58
|
+
/* ── typed interactive-component specs (Fix A1) ── */
|
|
59
|
+
/** A structured string-select option. */
|
|
60
|
+
const SelectOptionSchema = Type.Object({
|
|
61
|
+
label: Type.String({ maxLength: 100 }),
|
|
62
|
+
value: Type.String({ maxLength: 100 }),
|
|
63
|
+
description: Type.Optional(Type.String({ maxLength: 100 })),
|
|
64
|
+
}, { additionalProperties: false });
|
|
65
|
+
/**
|
|
66
|
+
* A structured select-menu spec. The tool serializes it into a Discord
|
|
67
|
+
* action-row whose `custom_id` carries the general-callback marker, so a press
|
|
68
|
+
* routes through the existing select branch and surfaces the chosen values.
|
|
69
|
+
*/
|
|
70
|
+
const SelectSpecSchema = Type.Object({
|
|
71
|
+
kind: Type.Union([Type.Literal("string"), Type.Literal("user"), Type.Literal("role"), Type.Literal("channel"), Type.Literal("mentionable")], { description: "Select kind. `string` needs `options`; the entity kinds (user/role/channel/mentionable) don't." }),
|
|
72
|
+
customId: Type.String({ description: "App-defined token a press routes back to the agent (general-prefixed automatically).", maxLength: 80 }),
|
|
73
|
+
placeholder: Type.Optional(Type.String({ maxLength: 150 })),
|
|
74
|
+
minValues: Type.Optional(Type.Number({ description: "Min selections (default 1)." })),
|
|
75
|
+
maxValues: Type.Optional(Type.Number({ description: "Max selections (default 1)." })),
|
|
76
|
+
options: Type.Optional(Type.Array(SelectOptionSchema, { maxItems: 25, description: "string-select options (required for kind:string, ≤25)." })),
|
|
77
|
+
}, { additionalProperties: false });
|
|
78
|
+
/** A single modal text-input field. */
|
|
79
|
+
const ModalFieldSchema = Type.Object({
|
|
80
|
+
id: Type.String({ description: "Field id — echoed back keying the submitted value.", maxLength: 100 }),
|
|
81
|
+
label: Type.String({ maxLength: 45 }),
|
|
82
|
+
style: Type.Optional(Type.Union([Type.Literal("short"), Type.Literal("paragraph")])),
|
|
83
|
+
required: Type.Optional(Type.Boolean()),
|
|
84
|
+
placeholder: Type.Optional(Type.String({ maxLength: 100 })),
|
|
85
|
+
}, { additionalProperties: false });
|
|
86
|
+
/**
|
|
87
|
+
* A structured modal spec. The tool registers the form in the TTL modal registry
|
|
88
|
+
* and emits a trigger button whose `custom_id` is the `modal:<id>` marker the
|
|
89
|
+
* press-router opens via `showModal`; submitting the form routes back as a turn.
|
|
90
|
+
*/
|
|
91
|
+
const ModalSpecSchema = Type.Object({
|
|
92
|
+
buttonLabel: Type.String({ description: "Label of the button that opens the form.", maxLength: 80 }),
|
|
93
|
+
title: Type.Optional(Type.String({ description: "Modal heading.", maxLength: 45 })),
|
|
94
|
+
fields: Type.Array(ModalFieldSchema, { minItems: 1, maxItems: 5, description: "1..5 text-input fields." }),
|
|
95
|
+
buttonStyle: Type.Optional(Type.Number({ description: "Trigger button style (1=primary, 2=secondary, 3=success, 4=danger)." })),
|
|
96
|
+
}, { additionalProperties: false });
|
|
97
|
+
/** A Components-V2 layout block (discriminated by `type`). */
|
|
98
|
+
const BlockSpecSchema = Type.Union([
|
|
99
|
+
Type.Object({ type: Type.Literal("text"), text: Type.String({ maxLength: 4000 }) }, { additionalProperties: false }),
|
|
100
|
+
Type.Object({
|
|
101
|
+
type: Type.Literal("section"),
|
|
102
|
+
texts: Type.Array(Type.String({ maxLength: 4000 }), { minItems: 1, maxItems: 3 }),
|
|
103
|
+
accessory: Type.Optional(Type.Union([
|
|
104
|
+
Type.Object({ kind: Type.Literal("thumbnail"), url: Type.String({ maxLength: 2048 }) }, { additionalProperties: false }),
|
|
105
|
+
Type.Object({
|
|
106
|
+
kind: Type.Literal("button"),
|
|
107
|
+
button: Type.Object({
|
|
108
|
+
label: Type.String({ maxLength: 80 }),
|
|
109
|
+
url: Type.Optional(Type.String({ maxLength: 2048 })),
|
|
110
|
+
customId: Type.Optional(Type.String({ maxLength: 100 })),
|
|
111
|
+
style: Type.Optional(Type.Number()),
|
|
112
|
+
}, { additionalProperties: false }),
|
|
113
|
+
}, { additionalProperties: false }),
|
|
114
|
+
])),
|
|
115
|
+
}, { additionalProperties: false }),
|
|
116
|
+
Type.Object({
|
|
117
|
+
type: Type.Literal("separator"),
|
|
118
|
+
divider: Type.Optional(Type.Boolean()),
|
|
119
|
+
spacing: Type.Optional(Type.Union([Type.Literal("small"), Type.Literal("large")])),
|
|
120
|
+
}, { additionalProperties: false }),
|
|
121
|
+
Type.Object({
|
|
122
|
+
type: Type.Literal("actions"),
|
|
123
|
+
buttons: Type.Array(Type.Object({
|
|
124
|
+
label: Type.String({ maxLength: 80 }),
|
|
125
|
+
url: Type.Optional(Type.String({ maxLength: 2048 })),
|
|
126
|
+
customId: Type.Optional(Type.String({ maxLength: 100 })),
|
|
127
|
+
style: Type.Optional(Type.Number()),
|
|
128
|
+
}, { additionalProperties: false }), { maxItems: 5 }),
|
|
129
|
+
}, { additionalProperties: false }),
|
|
130
|
+
Type.Object({
|
|
131
|
+
type: Type.Literal("media-gallery"),
|
|
132
|
+
items: Type.Array(Type.Object({ url: Type.String({ maxLength: 2048 }), description: Type.Optional(Type.String({ maxLength: 1024 })), spoiler: Type.Optional(Type.Boolean()) }, { additionalProperties: false }), { maxItems: 10 }),
|
|
133
|
+
}, { additionalProperties: false }),
|
|
134
|
+
Type.Object({ type: Type.Literal("file"), url: Type.String({ description: "An attachment:// ref.", maxLength: 2048 }), spoiler: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
135
|
+
], { description: "A Components-V2 layout block." });
|
|
136
|
+
/** A structured Components-V2 message spec (container of blocks). */
|
|
137
|
+
const BlocksSpecSchema = Type.Object({
|
|
138
|
+
blocks: Type.Array(BlockSpecSchema, { minItems: 1, maxItems: 40, description: "Ordered V2 layout blocks." }),
|
|
139
|
+
accentColor: Type.Optional(Type.Number({ description: "Container accent color (decimal int)." })),
|
|
140
|
+
}, { additionalProperties: false });
|
|
141
|
+
const DiscordActionParams = Type.Object({
|
|
142
|
+
action: Type.Union([
|
|
143
|
+
// messaging / content
|
|
144
|
+
Type.Literal("send"),
|
|
145
|
+
Type.Literal("send-embed"),
|
|
146
|
+
Type.Literal("poll"),
|
|
147
|
+
Type.Literal("sticker"),
|
|
148
|
+
Type.Literal("read-messages"),
|
|
149
|
+
Type.Literal("list-reactions"),
|
|
150
|
+
Type.Literal("remove-reaction"),
|
|
151
|
+
Type.Literal("thread-create"),
|
|
152
|
+
Type.Literal("list-threads"),
|
|
153
|
+
Type.Literal("search-messages"),
|
|
154
|
+
// guild-admin
|
|
155
|
+
Type.Literal("channel-create"),
|
|
156
|
+
Type.Literal("channel-edit"),
|
|
157
|
+
Type.Literal("channel-delete"),
|
|
158
|
+
Type.Literal("channel-move"),
|
|
159
|
+
Type.Literal("category-create"),
|
|
160
|
+
Type.Literal("category-edit"),
|
|
161
|
+
Type.Literal("category-delete"),
|
|
162
|
+
Type.Literal("role-list"),
|
|
163
|
+
Type.Literal("role-add"),
|
|
164
|
+
Type.Literal("role-remove"),
|
|
165
|
+
Type.Literal("role-info"),
|
|
166
|
+
Type.Literal("member-info"),
|
|
167
|
+
Type.Literal("emoji-list"),
|
|
168
|
+
Type.Literal("emoji-upload"),
|
|
169
|
+
Type.Literal("sticker-upload"),
|
|
170
|
+
Type.Literal("event-list"),
|
|
171
|
+
Type.Literal("event-create"),
|
|
172
|
+
// moderation
|
|
173
|
+
Type.Literal("ban"),
|
|
174
|
+
Type.Literal("unban"),
|
|
175
|
+
Type.Literal("kick"),
|
|
176
|
+
Type.Literal("timeout"),
|
|
177
|
+
Type.Literal("untimeout"),
|
|
178
|
+
// presence (Gateway op — persisted to config, applied on (re)connect)
|
|
179
|
+
Type.Literal("set-presence"),
|
|
180
|
+
], {
|
|
181
|
+
description: "The Discord guild action to run. Messaging: send, send-embed, poll, sticker, read-messages, list-reactions, remove-reaction, thread-create, list-threads, search-messages. Guild-admin: channel-create/edit/delete/move, category-create/edit/delete, role-list/add/remove/info, member-info, emoji-list/upload, sticker-upload, event-list/create. Moderation: ban, unban, kick, timeout, untimeout. Presence: set-presence (persists channels.discord.presence; applied on next (re)connect).",
|
|
182
|
+
}),
|
|
183
|
+
accountId: Type.Optional(Type.String({ description: "Which Discord bot account to act as (default: the configured default).", maxLength: 64 })),
|
|
184
|
+
// common targets
|
|
185
|
+
to: Type.Optional(Type.String({ description: "send/send-embed/poll/sticker target: a channel id, `channel:<id>`, or `user:<id>` to DM a user.", maxLength: 64 })),
|
|
186
|
+
channelId: Type.Optional(Type.String({ description: "Channel id for read/reaction/thread/channel-edit/delete actions.", maxLength: 64 })),
|
|
187
|
+
guildId: Type.Optional(Type.String({ description: "Guild (server) id for guild-admin + moderation + search actions.", maxLength: 64 })),
|
|
188
|
+
userId: Type.Optional(Type.String({ description: "Target user id (member-info / role add-remove / moderation).", maxLength: 64 })),
|
|
189
|
+
messageId: Type.Optional(Type.String({ description: "Target message id (reactions / thread-from-message).", maxLength: 64 })),
|
|
190
|
+
roleId: Type.Optional(Type.String({ description: "Role id (role-add / role-remove).", maxLength: 64 })),
|
|
191
|
+
categoryId: Type.Optional(Type.String({ description: "Category (type-4 channel) id (category-edit / category-delete).", maxLength: 64 })),
|
|
192
|
+
// content
|
|
193
|
+
content: Type.Optional(Type.String({ description: "Message text for send/poll/sticker.", maxLength: 4000 })),
|
|
194
|
+
embed: Type.Optional(EmbedSpecSchema),
|
|
195
|
+
embeds: Type.Optional(Type.Array(EmbedSpecSchema, { maxItems: 10, description: "Rich embeds for send." })),
|
|
196
|
+
components: Type.Optional(Type.Array(Type.Unknown(), { description: "send (power-user): raw Discord component-row JSON, passed through verbatim. Prefer the typed `select` / `modal` / `blocks` params." })),
|
|
197
|
+
select: Type.Optional(SelectSpecSchema),
|
|
198
|
+
modal: Type.Optional(ModalSpecSchema),
|
|
199
|
+
blocks: Type.Optional(BlocksSpecSchema),
|
|
200
|
+
replyTo: Type.Optional(Type.String({ description: "send: message id to reply to.", maxLength: 64 })),
|
|
201
|
+
silent: Type.Optional(Type.Boolean({ description: "send: suppress the @-notification ping." })),
|
|
202
|
+
// poll
|
|
203
|
+
question: Type.Optional(Type.String({ description: "poll question.", maxLength: 300 })),
|
|
204
|
+
answers: Type.Optional(Type.Array(Type.String({ maxLength: 55 }), { maxItems: 10, description: "poll answers (≤10)." })),
|
|
205
|
+
durationHours: Type.Optional(Type.Number({ description: "poll duration in hours (default 24)." })),
|
|
206
|
+
allowMultiselect: Type.Optional(Type.Boolean({ description: "poll: allow selecting multiple answers." })),
|
|
207
|
+
// sticker / emoji
|
|
208
|
+
stickerIds: Type.Optional(Type.Array(Type.String({ maxLength: 32 }), { maxItems: 3, description: "sticker: sticker ids to send (≤3)." })),
|
|
209
|
+
emoji: Type.Optional(Type.String({ description: "Reaction emoji — raw unicode, or `name:id` for a custom emoji.", maxLength: 128 })),
|
|
210
|
+
emojiName: Type.Optional(Type.String({ description: "emoji-upload: the new emoji's name.", maxLength: 64 })),
|
|
211
|
+
emojiImage: Type.Optional(Type.String({ description: "emoji-upload: a data URI (data:image/png;base64,…) for the emoji image.", maxLength: 600_000 })),
|
|
212
|
+
stickerName: Type.Optional(Type.String({ description: "sticker-upload: the new sticker name.", maxLength: 30 })),
|
|
213
|
+
stickerDescription: Type.Optional(Type.String({ description: "sticker-upload: the sticker description.", maxLength: 100 })),
|
|
214
|
+
stickerTags: Type.Optional(Type.String({ description: "sticker-upload: autocomplete/suggestion tags (a related unicode emoji or short keywords).", maxLength: 200 })),
|
|
215
|
+
stickerImage: Type.Optional(Type.String({ description: "sticker-upload: a data URI (data:image/png;base64,... PNG/APNG, or application/json for Lottie) for the sticker file.", maxLength: 600_000 })),
|
|
216
|
+
// reads / search
|
|
217
|
+
limit: Type.Optional(Type.Number({ description: "read-messages (≤50) / list-reactions (≤50) / search-messages (≤25) cap." })),
|
|
218
|
+
before: Type.Optional(Type.String({ description: "read-messages: fetch messages before this id.", maxLength: 64 })),
|
|
219
|
+
after: Type.Optional(Type.String({ description: "read-messages: fetch messages after this id.", maxLength: 64 })),
|
|
220
|
+
around: Type.Optional(Type.String({ description: "read-messages: fetch messages around this id.", maxLength: 64 })),
|
|
221
|
+
query: Type.Optional(Type.String({ description: "search-messages: the text to search for.", maxLength: 400 })),
|
|
222
|
+
authorId: Type.Optional(Type.String({ description: "search-messages: restrict to this author.", maxLength: 64 })),
|
|
223
|
+
// thread
|
|
224
|
+
name: Type.Optional(Type.String({ description: "Name for channel/category/role-less create + thread-create.", maxLength: 100 })),
|
|
225
|
+
threadType: Type.Optional(Type.Number({ description: "thread-create (standalone): channel type (11=public, 12=private)." })),
|
|
226
|
+
autoArchiveMinutes: Type.Optional(Type.Number({ description: "thread-create: idle minutes before auto-archive (60/1440/4320/10080)." })),
|
|
227
|
+
// channel-create / edit
|
|
228
|
+
channelType: Type.Optional(Type.Number({ description: "channel-create: Discord channel type (0=text, 2=voice, 5=announcement, 15=forum)." })),
|
|
229
|
+
parentId: Type.Optional(Type.String({ description: "channel-create/edit/move: parent category id.", maxLength: 64 })),
|
|
230
|
+
topic: Type.Optional(Type.String({ description: "channel-create/edit: channel topic.", maxLength: 1024 })),
|
|
231
|
+
position: Type.Optional(Type.Number({ description: "Sort position for channel/category create/edit/move." })),
|
|
232
|
+
nsfw: Type.Optional(Type.Boolean({ description: "channel-create/edit: mark NSFW." })),
|
|
233
|
+
rateLimitPerUser: Type.Optional(Type.Number({ description: "channel-edit: slow-mode seconds (0 disables)." })),
|
|
234
|
+
archived: Type.Optional(Type.Boolean({ description: "channel-edit (thread): set archived." })),
|
|
235
|
+
locked: Type.Optional(Type.Boolean({ description: "channel-edit (thread): set locked." })),
|
|
236
|
+
// events
|
|
237
|
+
eventName: Type.Optional(Type.String({ description: "event-create: the event name.", maxLength: 100 })),
|
|
238
|
+
startTime: Type.Optional(Type.String({ description: "event-create: ISO-8601 start time.", maxLength: 40 })),
|
|
239
|
+
endTime: Type.Optional(Type.String({ description: "event-create: ISO-8601 end time (required for external events).", maxLength: 40 })),
|
|
240
|
+
description: Type.Optional(Type.String({ description: "event-create: description.", maxLength: 1000 })),
|
|
241
|
+
location: Type.Optional(Type.String({ description: "event-create (external): physical/virtual location.", maxLength: 100 })),
|
|
242
|
+
entityType: Type.Optional(Type.Union([Type.Literal("stage"), Type.Literal("voice"), Type.Literal("external")], {
|
|
243
|
+
description: "event-create: stage / voice (needs channelId) / external (needs location + endTime). Default voice.",
|
|
244
|
+
})),
|
|
245
|
+
// moderation
|
|
246
|
+
reason: Type.Optional(Type.String({ description: "Audit-log reason for the moderation action.", maxLength: 512 })),
|
|
247
|
+
deleteMessageDays: Type.Optional(Type.Number({ description: "ban: purge the user's messages from the last N days (0–7)." })),
|
|
248
|
+
durationMinutes: Type.Optional(Type.Number({ description: "timeout: minutes to silence the member (1–40320 = 28 days)." })),
|
|
249
|
+
// presence (set-presence)
|
|
250
|
+
status: Type.Optional(Type.Union([Type.Literal("online"), Type.Literal("idle"), Type.Literal("dnd"), Type.Literal("invisible")], { description: "set-presence: the bot's online dot." })),
|
|
251
|
+
activityType: Type.Optional(Type.Union([
|
|
252
|
+
Type.Literal("playing"),
|
|
253
|
+
Type.Literal("streaming"),
|
|
254
|
+
Type.Literal("listening"),
|
|
255
|
+
Type.Literal("watching"),
|
|
256
|
+
Type.Literal("custom"),
|
|
257
|
+
Type.Literal("competing"),
|
|
258
|
+
], { description: "set-presence: the activity row kind (custom uses the status state line; streaming uses activityUrl)." })),
|
|
259
|
+
activityText: Type.Optional(Type.String({ description: "set-presence: the activity text (the 'Playing …' / status line).", maxLength: 128 })),
|
|
260
|
+
activityUrl: Type.Optional(Type.String({ description: "set-presence (streaming): the Twitch/YouTube stream URL.", maxLength: 512 })),
|
|
261
|
+
});
|
|
262
|
+
const MAX_DATA_CHARS = 12_000;
|
|
263
|
+
/** Discord allows at most 5 component (action) rows on a classic message. */
|
|
264
|
+
const DISCORD_MAX_COMPONENT_ROWS = 5;
|
|
265
|
+
/** Compact a payload so a large list result can't flood the model's context. Pure. */
|
|
266
|
+
export function capDiscordData(value, maxChars = MAX_DATA_CHARS) {
|
|
267
|
+
let s;
|
|
268
|
+
try {
|
|
269
|
+
s = JSON.stringify(value);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
if (!s || s.length <= maxChars)
|
|
275
|
+
return value;
|
|
276
|
+
return {
|
|
277
|
+
truncated: true,
|
|
278
|
+
bytes: s.length,
|
|
279
|
+
note: `Result truncated at ${maxChars} chars — narrow the request (a lower limit) and try again.`,
|
|
280
|
+
preview: s.slice(0, maxChars),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Decode a `data:<mime>;base64,<payload>` URI into raw bytes + the MIME type, for
|
|
285
|
+
* the multipart sticker upload (which needs the file bytes, not a JSON image
|
|
286
|
+
* string like emoji-upload). Returns null when the input isn't a base64 data URI.
|
|
287
|
+
*/
|
|
288
|
+
export function decodeStickerDataUri(input) {
|
|
289
|
+
const m = /^data:([^;,]+);base64,(.+)$/s.exec((input ?? "").trim());
|
|
290
|
+
if (!m || !m[1] || !m[2])
|
|
291
|
+
return null;
|
|
292
|
+
try {
|
|
293
|
+
const bytes = new Uint8Array(Buffer.from(m[2], "base64"));
|
|
294
|
+
if (bytes.length === 0)
|
|
295
|
+
return null;
|
|
296
|
+
return { bytes, contentType: m[1].trim() };
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Persist a presence block under `channels.discord.presence` (single-account) or
|
|
304
|
+
* `channels.discord.accounts[id].presence` (when the account exists in the
|
|
305
|
+
* accounts list). Returns the written presence object. Pure config edit — the
|
|
306
|
+
* live connection re-reads + applies it on next (re)connect.
|
|
307
|
+
*/
|
|
308
|
+
function writeDiscordPresence(current, accountId, presence) {
|
|
309
|
+
const cfg = current;
|
|
310
|
+
const channels = { ...(cfg.channels ?? {}) };
|
|
311
|
+
const discord = { ...(channels.discord ?? {}) };
|
|
312
|
+
const accounts = Array.isArray(discord.accounts) ? discord.accounts : undefined;
|
|
313
|
+
const accountEntry = accounts?.find((a) => typeof a?.id === "string" && a.id.trim() === accountId);
|
|
314
|
+
if (accountEntry) {
|
|
315
|
+
// Per-account presence.
|
|
316
|
+
discord.accounts = accounts.map((a) => a === accountEntry ? { ...a, presence } : a);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Single-account / top-level presence.
|
|
320
|
+
discord.presence = presence;
|
|
321
|
+
}
|
|
322
|
+
channels.discord = discord;
|
|
323
|
+
return { ...current, channels };
|
|
324
|
+
}
|
|
325
|
+
export function makeDiscordActionTool(opts = {}) {
|
|
326
|
+
const resolveToken = opts.resolveToken ??
|
|
327
|
+
((accountId) => {
|
|
328
|
+
try {
|
|
329
|
+
return resolveDiscordBotToken(loadConfig(), accountId);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return "";
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
name: "discord_action",
|
|
337
|
+
label: "Discord action",
|
|
338
|
+
displaySummary: "managing Discord",
|
|
339
|
+
ownerOnly: true,
|
|
340
|
+
description: [
|
|
341
|
+
"Manage a Discord server over the Discord REST API: post/edit content and run guild administration the everyday chat reply can't.",
|
|
342
|
+
"Messaging: action:send (to a channel id or user:<id> DM — content + optional embeds; interactive `select` / `modal` / `blocks` (Components-V2) specs that the user can press/submit — a press routes back to you as a turn), send-embed (a rich embed), poll, sticker, read-messages, list-reactions, remove-reaction, thread-create, list-threads, search-messages.",
|
|
343
|
+
"Guild-admin: channel-create/edit/delete/move, category-create/edit/delete, role-list/add/remove/info, member-info, emoji-list/upload, event-list/create (scheduled events).",
|
|
344
|
+
"Moderation: ban, unban, kick, timeout, untimeout — Discord enforces the bot's own permissions (a missing-permission error is decoded into a clear hint).",
|
|
345
|
+
"Owner-only. Most actions need ids (guildId / channelId / userId / messageId); the tool reports clearly when one is missing.",
|
|
346
|
+
].join(" "),
|
|
347
|
+
parameters: DiscordActionParams,
|
|
348
|
+
execute: async (_toolCallId, args) => {
|
|
349
|
+
const action = args.action;
|
|
350
|
+
const ok = (message, data) => jsonResult({ action, ok: true, message, ...(data !== undefined ? { data: capDiscordData(data) } : {}) });
|
|
351
|
+
const fail = (message) => jsonResult({ action, ok: false, message });
|
|
352
|
+
const accountId = (args.accountId ?? "").trim() || DISCORD_DEFAULT_ACCOUNT_ID;
|
|
353
|
+
// set-presence is a CONFIG write (Gateway op applied on next (re)connect),
|
|
354
|
+
// not a REST call — handle it before the live-token guard since it never
|
|
355
|
+
// touches Discord directly.
|
|
356
|
+
if (action === "set-presence") {
|
|
357
|
+
const status = args.status;
|
|
358
|
+
const activityType = args.activityType;
|
|
359
|
+
const activityText = (args.activityText ?? "").trim();
|
|
360
|
+
const activityUrl = (args.activityUrl ?? "").trim();
|
|
361
|
+
if (!status && !activityType && !activityText) {
|
|
362
|
+
return fail("set-presence requires at least a `status` or an `activityType` + `activityText`.");
|
|
363
|
+
}
|
|
364
|
+
const presence = {};
|
|
365
|
+
if (status)
|
|
366
|
+
presence.status = status;
|
|
367
|
+
if (activityType)
|
|
368
|
+
presence.activityType = activityType;
|
|
369
|
+
if (activityText)
|
|
370
|
+
presence.activityText = activityText;
|
|
371
|
+
if (activityType === "streaming" && activityUrl)
|
|
372
|
+
presence.activityUrl = activityUrl;
|
|
373
|
+
const mutate = opts.mutateConfig ?? ((m) => mutateConfigAtomic(m));
|
|
374
|
+
try {
|
|
375
|
+
await mutate((current) => writeDiscordPresence(current, accountId, presence));
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
return fail(`set-presence failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
379
|
+
}
|
|
380
|
+
return ok("Saved Discord presence — it applies on the bot's next (re)connect.", presence);
|
|
381
|
+
}
|
|
382
|
+
const token = resolveToken(accountId);
|
|
383
|
+
if (!token) {
|
|
384
|
+
return fail("No Discord bot token is configured — connect the Discord channel first (the token seals via connect_channel / channels.discord.botToken), then retry.");
|
|
385
|
+
}
|
|
386
|
+
const rest = {
|
|
387
|
+
token,
|
|
388
|
+
...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
|
|
389
|
+
};
|
|
390
|
+
// Tiny required-param guard → a clean, actionable refusal (no REST round-trip).
|
|
391
|
+
const need = (value, label) => {
|
|
392
|
+
const v = (value ?? "").trim();
|
|
393
|
+
return v ? v : null;
|
|
394
|
+
};
|
|
395
|
+
const missing = (label) => fail(`${action} requires \`${label}\`.`);
|
|
396
|
+
try {
|
|
397
|
+
switch (action) {
|
|
398
|
+
/* ── messaging / content ── */
|
|
399
|
+
case "send": {
|
|
400
|
+
const to = need(args.to, "to");
|
|
401
|
+
if (!to)
|
|
402
|
+
return missing("to");
|
|
403
|
+
// Typed interactive components (Fix A1). Each structured spec is
|
|
404
|
+
// serialized into raw Discord component-row JSON carrying the SAME
|
|
405
|
+
// custom_id codecs the press-routing expects; a `blocks` (V2) spec
|
|
406
|
+
// also sets the IsComponentsV2 message flag (text must live in
|
|
407
|
+
// TextDisplay blocks, not plain content). The raw `components`
|
|
408
|
+
// passthrough still works for power users.
|
|
409
|
+
const componentRows = Array.isArray(args.components) ? [...args.components] : [];
|
|
410
|
+
let v2Flags = 0;
|
|
411
|
+
let isV2 = false;
|
|
412
|
+
if (args.modal) {
|
|
413
|
+
const built = serializeDiscordModalTrigger({
|
|
414
|
+
...args.modal,
|
|
415
|
+
...(accountId ? { accountId } : {}),
|
|
416
|
+
});
|
|
417
|
+
if (!built.ok)
|
|
418
|
+
return fail(built.error);
|
|
419
|
+
componentRows.push(built.row);
|
|
420
|
+
}
|
|
421
|
+
if (args.select) {
|
|
422
|
+
const built = serializeDiscordSelectRow(args.select);
|
|
423
|
+
if (!built.ok)
|
|
424
|
+
return fail(built.error);
|
|
425
|
+
componentRows.push(built.row);
|
|
426
|
+
}
|
|
427
|
+
if (args.blocks) {
|
|
428
|
+
const built = serializeDiscordV2Message(args.blocks);
|
|
429
|
+
if (!built.ok)
|
|
430
|
+
return fail(built.error);
|
|
431
|
+
// A V2 message is its OWN components array + flag — it cannot mix
|
|
432
|
+
// with classic content/embeds/action-rows.
|
|
433
|
+
if (args.content || (Array.isArray(args.embeds) && args.embeds.length > 0) || componentRows.length > 0) {
|
|
434
|
+
return fail("blocks (Components-V2) cannot be combined with content, embeds, or select/modal in one send — send them separately.");
|
|
435
|
+
}
|
|
436
|
+
componentRows.push(...built.components);
|
|
437
|
+
v2Flags = built.flags;
|
|
438
|
+
isV2 = true;
|
|
439
|
+
}
|
|
440
|
+
if (componentRows.length > DISCORD_MAX_COMPONENT_ROWS && !isV2) {
|
|
441
|
+
return fail(`send accepts at most ${DISCORD_MAX_COMPONENT_ROWS} component rows.`);
|
|
442
|
+
}
|
|
443
|
+
const data = await sendMessage({
|
|
444
|
+
to,
|
|
445
|
+
...(args.content && !isV2 ? { content: args.content } : {}),
|
|
446
|
+
...(args.embeds && !isV2 ? { embeds: args.embeds } : {}),
|
|
447
|
+
...(componentRows.length > 0 ? { components: componentRows } : {}),
|
|
448
|
+
...(v2Flags ? { flags: v2Flags } : {}),
|
|
449
|
+
...(args.replyTo ? { replyTo: args.replyTo } : {}),
|
|
450
|
+
...(args.silent ? { silent: true } : {}),
|
|
451
|
+
}, rest);
|
|
452
|
+
return ok(`Sent to ${to}.`, data);
|
|
453
|
+
}
|
|
454
|
+
case "send-embed": {
|
|
455
|
+
const to = need(args.to, "to");
|
|
456
|
+
if (!to)
|
|
457
|
+
return missing("to");
|
|
458
|
+
if (!args.embed)
|
|
459
|
+
return missing("embed");
|
|
460
|
+
const data = await sendEmbed({ to, embed: args.embed, ...(args.content ? { content: args.content } : {}) }, rest);
|
|
461
|
+
return ok(`Sent embed to ${to}.`, data);
|
|
462
|
+
}
|
|
463
|
+
case "poll": {
|
|
464
|
+
const to = need(args.to, "to");
|
|
465
|
+
if (!to)
|
|
466
|
+
return missing("to");
|
|
467
|
+
const question = need(args.question, "question");
|
|
468
|
+
if (!question)
|
|
469
|
+
return missing("question");
|
|
470
|
+
if (!Array.isArray(args.answers) || args.answers.length < 1)
|
|
471
|
+
return missing("answers");
|
|
472
|
+
const data = await sendPoll({
|
|
473
|
+
to,
|
|
474
|
+
question,
|
|
475
|
+
answers: args.answers,
|
|
476
|
+
...(typeof args.durationHours === "number" ? { durationHours: args.durationHours } : {}),
|
|
477
|
+
...(args.allowMultiselect ? { allowMultiselect: true } : {}),
|
|
478
|
+
}, rest);
|
|
479
|
+
return ok(`Posted a poll to ${to}.`, data);
|
|
480
|
+
}
|
|
481
|
+
case "sticker": {
|
|
482
|
+
const to = need(args.to, "to");
|
|
483
|
+
if (!to)
|
|
484
|
+
return missing("to");
|
|
485
|
+
if (!Array.isArray(args.stickerIds) || args.stickerIds.length < 1)
|
|
486
|
+
return missing("stickerIds");
|
|
487
|
+
const data = await sendSticker({ to, stickerIds: args.stickerIds, ...(args.content ? { content: args.content } : {}) }, rest);
|
|
488
|
+
return ok(`Sent sticker(s) to ${to}.`, data);
|
|
489
|
+
}
|
|
490
|
+
case "read-messages": {
|
|
491
|
+
const channelId = need(args.channelId, "channelId");
|
|
492
|
+
if (!channelId)
|
|
493
|
+
return missing("channelId");
|
|
494
|
+
const data = await readMessages({
|
|
495
|
+
channelId,
|
|
496
|
+
...(typeof args.limit === "number" ? { limit: args.limit } : {}),
|
|
497
|
+
...(args.before ? { before: args.before } : {}),
|
|
498
|
+
...(args.after ? { after: args.after } : {}),
|
|
499
|
+
...(args.around ? { around: args.around } : {}),
|
|
500
|
+
}, rest);
|
|
501
|
+
const count = Array.isArray(data) ? data.length : undefined;
|
|
502
|
+
return ok(count !== undefined ? `Fetched ${count} message(s).` : "Fetched messages.", data);
|
|
503
|
+
}
|
|
504
|
+
case "list-reactions": {
|
|
505
|
+
const channelId = need(args.channelId, "channelId");
|
|
506
|
+
if (!channelId)
|
|
507
|
+
return missing("channelId");
|
|
508
|
+
const messageId = need(args.messageId, "messageId");
|
|
509
|
+
if (!messageId)
|
|
510
|
+
return missing("messageId");
|
|
511
|
+
const emoji = need(args.emoji, "emoji");
|
|
512
|
+
if (!emoji)
|
|
513
|
+
return missing("emoji");
|
|
514
|
+
const data = await listReactions({ channelId, messageId, emoji, ...(typeof args.limit === "number" ? { limit: args.limit } : {}) }, rest);
|
|
515
|
+
return ok("Listed reactions.", data);
|
|
516
|
+
}
|
|
517
|
+
case "remove-reaction": {
|
|
518
|
+
const channelId = need(args.channelId, "channelId");
|
|
519
|
+
if (!channelId)
|
|
520
|
+
return missing("channelId");
|
|
521
|
+
const messageId = need(args.messageId, "messageId");
|
|
522
|
+
if (!messageId)
|
|
523
|
+
return missing("messageId");
|
|
524
|
+
const emoji = need(args.emoji, "emoji");
|
|
525
|
+
if (!emoji)
|
|
526
|
+
return missing("emoji");
|
|
527
|
+
await removeReaction({ channelId, messageId, emoji, ...(args.userId ? { userId: args.userId } : {}) }, rest);
|
|
528
|
+
return ok("Removed reaction.");
|
|
529
|
+
}
|
|
530
|
+
case "thread-create": {
|
|
531
|
+
const channelId = need(args.channelId, "channelId");
|
|
532
|
+
if (!channelId)
|
|
533
|
+
return missing("channelId");
|
|
534
|
+
const name = need(args.name, "name");
|
|
535
|
+
if (!name)
|
|
536
|
+
return missing("name");
|
|
537
|
+
const data = await threadCreate({
|
|
538
|
+
channelId,
|
|
539
|
+
name,
|
|
540
|
+
...(args.messageId ? { messageId: args.messageId } : {}),
|
|
541
|
+
...(typeof args.autoArchiveMinutes === "number" ? { autoArchiveMinutes: args.autoArchiveMinutes } : {}),
|
|
542
|
+
...(typeof args.threadType === "number" ? { type: args.threadType } : {}),
|
|
543
|
+
...(args.content ? { content: args.content } : {}),
|
|
544
|
+
}, rest);
|
|
545
|
+
return ok(`Created thread "${name}".`, data);
|
|
546
|
+
}
|
|
547
|
+
case "list-threads": {
|
|
548
|
+
const guildId = need(args.guildId, "guildId");
|
|
549
|
+
if (!guildId)
|
|
550
|
+
return missing("guildId");
|
|
551
|
+
const data = await listThreads({ guildId }, rest);
|
|
552
|
+
return ok("Listed active threads.", data);
|
|
553
|
+
}
|
|
554
|
+
case "search-messages": {
|
|
555
|
+
const guildId = need(args.guildId, "guildId");
|
|
556
|
+
if (!guildId)
|
|
557
|
+
return missing("guildId");
|
|
558
|
+
const query = need(args.query, "query");
|
|
559
|
+
if (!query)
|
|
560
|
+
return missing("query");
|
|
561
|
+
const data = await searchMessages({
|
|
562
|
+
guildId,
|
|
563
|
+
query,
|
|
564
|
+
...(args.authorId ? { authorId: args.authorId } : {}),
|
|
565
|
+
...(args.channelId ? { channelId: args.channelId } : {}),
|
|
566
|
+
...(typeof args.limit === "number" ? { limit: args.limit } : {}),
|
|
567
|
+
}, rest);
|
|
568
|
+
return ok("Searched messages.", data);
|
|
569
|
+
}
|
|
570
|
+
/* ── guild-admin ── */
|
|
571
|
+
case "channel-create": {
|
|
572
|
+
const guildId = need(args.guildId, "guildId");
|
|
573
|
+
if (!guildId)
|
|
574
|
+
return missing("guildId");
|
|
575
|
+
const name = need(args.name, "name");
|
|
576
|
+
if (!name)
|
|
577
|
+
return missing("name");
|
|
578
|
+
const data = await channelCreate({
|
|
579
|
+
guildId,
|
|
580
|
+
name,
|
|
581
|
+
...(typeof args.channelType === "number" ? { type: args.channelType } : {}),
|
|
582
|
+
...(args.parentId ? { parentId: args.parentId } : {}),
|
|
583
|
+
...(args.topic ? { topic: args.topic } : {}),
|
|
584
|
+
...(typeof args.position === "number" ? { position: args.position } : {}),
|
|
585
|
+
...(typeof args.nsfw === "boolean" ? { nsfw: args.nsfw } : {}),
|
|
586
|
+
}, rest);
|
|
587
|
+
return ok(`Created channel "${name}".`, data);
|
|
588
|
+
}
|
|
589
|
+
case "channel-edit": {
|
|
590
|
+
const channelId = need(args.channelId, "channelId");
|
|
591
|
+
if (!channelId)
|
|
592
|
+
return missing("channelId");
|
|
593
|
+
const data = await channelEdit({
|
|
594
|
+
channelId,
|
|
595
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
596
|
+
...(args.topic !== undefined ? { topic: args.topic } : {}),
|
|
597
|
+
...(typeof args.position === "number" ? { position: args.position } : {}),
|
|
598
|
+
...(args.parentId !== undefined ? { parentId: args.parentId } : {}),
|
|
599
|
+
...(typeof args.nsfw === "boolean" ? { nsfw: args.nsfw } : {}),
|
|
600
|
+
...(typeof args.rateLimitPerUser === "number" ? { rateLimitPerUser: args.rateLimitPerUser } : {}),
|
|
601
|
+
...(typeof args.archived === "boolean" ? { archived: args.archived } : {}),
|
|
602
|
+
...(typeof args.locked === "boolean" ? { locked: args.locked } : {}),
|
|
603
|
+
}, rest);
|
|
604
|
+
return ok("Edited channel.", data);
|
|
605
|
+
}
|
|
606
|
+
case "channel-delete": {
|
|
607
|
+
const channelId = need(args.channelId, "channelId");
|
|
608
|
+
if (!channelId)
|
|
609
|
+
return missing("channelId");
|
|
610
|
+
const data = await channelDelete({ channelId }, rest);
|
|
611
|
+
return ok("Deleted channel.", data);
|
|
612
|
+
}
|
|
613
|
+
case "channel-move": {
|
|
614
|
+
const guildId = need(args.guildId, "guildId");
|
|
615
|
+
if (!guildId)
|
|
616
|
+
return missing("guildId");
|
|
617
|
+
const channelId = need(args.channelId, "channelId");
|
|
618
|
+
if (!channelId)
|
|
619
|
+
return missing("channelId");
|
|
620
|
+
await channelMove({
|
|
621
|
+
guildId,
|
|
622
|
+
channelId,
|
|
623
|
+
...(typeof args.position === "number" ? { position: args.position } : {}),
|
|
624
|
+
...(args.parentId !== undefined ? { parentId: args.parentId } : {}),
|
|
625
|
+
}, rest);
|
|
626
|
+
return ok("Moved channel.");
|
|
627
|
+
}
|
|
628
|
+
case "category-create": {
|
|
629
|
+
const guildId = need(args.guildId, "guildId");
|
|
630
|
+
if (!guildId)
|
|
631
|
+
return missing("guildId");
|
|
632
|
+
const name = need(args.name, "name");
|
|
633
|
+
if (!name)
|
|
634
|
+
return missing("name");
|
|
635
|
+
const data = await categoryCreate({ guildId, name, ...(typeof args.position === "number" ? { position: args.position } : {}) }, rest);
|
|
636
|
+
return ok(`Created category "${name}".`, data);
|
|
637
|
+
}
|
|
638
|
+
case "category-edit": {
|
|
639
|
+
const categoryId = need(args.categoryId, "categoryId");
|
|
640
|
+
if (!categoryId)
|
|
641
|
+
return missing("categoryId");
|
|
642
|
+
const data = await categoryEdit({
|
|
643
|
+
categoryId,
|
|
644
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
645
|
+
...(typeof args.position === "number" ? { position: args.position } : {}),
|
|
646
|
+
}, rest);
|
|
647
|
+
return ok("Edited category.", data);
|
|
648
|
+
}
|
|
649
|
+
case "category-delete": {
|
|
650
|
+
const categoryId = need(args.categoryId, "categoryId");
|
|
651
|
+
if (!categoryId)
|
|
652
|
+
return missing("categoryId");
|
|
653
|
+
const data = await categoryDelete({ categoryId }, rest);
|
|
654
|
+
return ok("Deleted category.", data);
|
|
655
|
+
}
|
|
656
|
+
case "role-list": {
|
|
657
|
+
const guildId = need(args.guildId, "guildId");
|
|
658
|
+
if (!guildId)
|
|
659
|
+
return missing("guildId");
|
|
660
|
+
const data = await roleList({ guildId }, rest);
|
|
661
|
+
return ok("Listed roles.", data);
|
|
662
|
+
}
|
|
663
|
+
case "role-info": {
|
|
664
|
+
const guildId = need(args.guildId, "guildId");
|
|
665
|
+
if (!guildId)
|
|
666
|
+
return missing("guildId");
|
|
667
|
+
const data = await roleInfo({ guildId }, rest);
|
|
668
|
+
return ok("Fetched role info.", data);
|
|
669
|
+
}
|
|
670
|
+
case "role-add": {
|
|
671
|
+
const guildId = need(args.guildId, "guildId");
|
|
672
|
+
if (!guildId)
|
|
673
|
+
return missing("guildId");
|
|
674
|
+
const userId = need(args.userId, "userId");
|
|
675
|
+
if (!userId)
|
|
676
|
+
return missing("userId");
|
|
677
|
+
const roleId = need(args.roleId, "roleId");
|
|
678
|
+
if (!roleId)
|
|
679
|
+
return missing("roleId");
|
|
680
|
+
await roleAdd({ guildId, userId, roleId, ...(args.reason ? { reason: args.reason } : {}) }, rest);
|
|
681
|
+
return ok(`Added role ${roleId} to ${userId}.`);
|
|
682
|
+
}
|
|
683
|
+
case "role-remove": {
|
|
684
|
+
const guildId = need(args.guildId, "guildId");
|
|
685
|
+
if (!guildId)
|
|
686
|
+
return missing("guildId");
|
|
687
|
+
const userId = need(args.userId, "userId");
|
|
688
|
+
if (!userId)
|
|
689
|
+
return missing("userId");
|
|
690
|
+
const roleId = need(args.roleId, "roleId");
|
|
691
|
+
if (!roleId)
|
|
692
|
+
return missing("roleId");
|
|
693
|
+
await roleRemove({ guildId, userId, roleId, ...(args.reason ? { reason: args.reason } : {}) }, rest);
|
|
694
|
+
return ok(`Removed role ${roleId} from ${userId}.`);
|
|
695
|
+
}
|
|
696
|
+
case "member-info": {
|
|
697
|
+
const guildId = need(args.guildId, "guildId");
|
|
698
|
+
if (!guildId)
|
|
699
|
+
return missing("guildId");
|
|
700
|
+
const userId = need(args.userId, "userId");
|
|
701
|
+
if (!userId)
|
|
702
|
+
return missing("userId");
|
|
703
|
+
const data = await memberInfo({ guildId, userId }, rest);
|
|
704
|
+
return ok("Fetched member info.", data);
|
|
705
|
+
}
|
|
706
|
+
case "emoji-list": {
|
|
707
|
+
const guildId = need(args.guildId, "guildId");
|
|
708
|
+
if (!guildId)
|
|
709
|
+
return missing("guildId");
|
|
710
|
+
const data = await emojiList({ guildId }, rest);
|
|
711
|
+
return ok("Listed emojis.", data);
|
|
712
|
+
}
|
|
713
|
+
case "emoji-upload": {
|
|
714
|
+
const guildId = need(args.guildId, "guildId");
|
|
715
|
+
if (!guildId)
|
|
716
|
+
return missing("guildId");
|
|
717
|
+
const name = need(args.emojiName, "emojiName");
|
|
718
|
+
if (!name)
|
|
719
|
+
return missing("emojiName");
|
|
720
|
+
const image = need(args.emojiImage, "emojiImage");
|
|
721
|
+
if (!image)
|
|
722
|
+
return missing("emojiImage");
|
|
723
|
+
const data = await emojiUpload({ guildId, name, image }, rest);
|
|
724
|
+
return ok(`Uploaded emoji "${name}".`, data);
|
|
725
|
+
}
|
|
726
|
+
case "sticker-upload": {
|
|
727
|
+
const guildId = need(args.guildId, "guildId");
|
|
728
|
+
if (!guildId)
|
|
729
|
+
return missing("guildId");
|
|
730
|
+
const name = need(args.stickerName, "stickerName");
|
|
731
|
+
if (!name)
|
|
732
|
+
return missing("stickerName");
|
|
733
|
+
const description = (args.stickerDescription ?? "").trim();
|
|
734
|
+
const tags = need(args.stickerTags, "stickerTags");
|
|
735
|
+
if (!tags)
|
|
736
|
+
return missing("stickerTags");
|
|
737
|
+
const image = need(args.stickerImage, "stickerImage");
|
|
738
|
+
if (!image)
|
|
739
|
+
return missing("stickerImage");
|
|
740
|
+
const decoded = decodeStickerDataUri(image);
|
|
741
|
+
if (!decoded) {
|
|
742
|
+
return fail("sticker-upload `stickerImage` must be a base64 data URI (data:image/png;base64,…).");
|
|
743
|
+
}
|
|
744
|
+
const data = await stickerUpload({ guildId, name, description, tags, file: decoded.bytes, contentType: decoded.contentType }, rest);
|
|
745
|
+
return ok(`Uploaded sticker "${name}".`, data);
|
|
746
|
+
}
|
|
747
|
+
case "event-list": {
|
|
748
|
+
const guildId = need(args.guildId, "guildId");
|
|
749
|
+
if (!guildId)
|
|
750
|
+
return missing("guildId");
|
|
751
|
+
const data = await eventList({ guildId }, rest);
|
|
752
|
+
return ok("Listed scheduled events.", data);
|
|
753
|
+
}
|
|
754
|
+
case "event-create": {
|
|
755
|
+
const guildId = need(args.guildId, "guildId");
|
|
756
|
+
if (!guildId)
|
|
757
|
+
return missing("guildId");
|
|
758
|
+
const name = need(args.eventName, "eventName");
|
|
759
|
+
if (!name)
|
|
760
|
+
return missing("eventName");
|
|
761
|
+
const startTime = need(args.startTime, "startTime");
|
|
762
|
+
if (!startTime)
|
|
763
|
+
return missing("startTime");
|
|
764
|
+
const data = await eventCreate({
|
|
765
|
+
guildId,
|
|
766
|
+
name,
|
|
767
|
+
startTime,
|
|
768
|
+
...(args.endTime ? { endTime: args.endTime } : {}),
|
|
769
|
+
...(args.description ? { description: args.description } : {}),
|
|
770
|
+
...(args.channelId ? { channelId: args.channelId } : {}),
|
|
771
|
+
...(args.location ? { location: args.location } : {}),
|
|
772
|
+
...(args.entityType ? { entityType: args.entityType } : {}),
|
|
773
|
+
}, rest);
|
|
774
|
+
return ok(`Created scheduled event "${name}".`, data);
|
|
775
|
+
}
|
|
776
|
+
/* ── moderation ── */
|
|
777
|
+
case "ban": {
|
|
778
|
+
const guildId = need(args.guildId, "guildId");
|
|
779
|
+
if (!guildId)
|
|
780
|
+
return missing("guildId");
|
|
781
|
+
const userId = need(args.userId, "userId");
|
|
782
|
+
if (!userId)
|
|
783
|
+
return missing("userId");
|
|
784
|
+
await ban({
|
|
785
|
+
guildId,
|
|
786
|
+
userId,
|
|
787
|
+
...(args.reason ? { reason: args.reason } : {}),
|
|
788
|
+
...(typeof args.deleteMessageDays === "number" ? { deleteMessageDays: args.deleteMessageDays } : {}),
|
|
789
|
+
}, rest);
|
|
790
|
+
return ok(`Banned ${userId}.`);
|
|
791
|
+
}
|
|
792
|
+
case "unban": {
|
|
793
|
+
const guildId = need(args.guildId, "guildId");
|
|
794
|
+
if (!guildId)
|
|
795
|
+
return missing("guildId");
|
|
796
|
+
const userId = need(args.userId, "userId");
|
|
797
|
+
if (!userId)
|
|
798
|
+
return missing("userId");
|
|
799
|
+
await unban({ guildId, userId, ...(args.reason ? { reason: args.reason } : {}) }, rest);
|
|
800
|
+
return ok(`Unbanned ${userId}.`);
|
|
801
|
+
}
|
|
802
|
+
case "kick": {
|
|
803
|
+
const guildId = need(args.guildId, "guildId");
|
|
804
|
+
if (!guildId)
|
|
805
|
+
return missing("guildId");
|
|
806
|
+
const userId = need(args.userId, "userId");
|
|
807
|
+
if (!userId)
|
|
808
|
+
return missing("userId");
|
|
809
|
+
await kick({ guildId, userId, ...(args.reason ? { reason: args.reason } : {}) }, rest);
|
|
810
|
+
return ok(`Kicked ${userId}.`);
|
|
811
|
+
}
|
|
812
|
+
case "timeout": {
|
|
813
|
+
const guildId = need(args.guildId, "guildId");
|
|
814
|
+
if (!guildId)
|
|
815
|
+
return missing("guildId");
|
|
816
|
+
const userId = need(args.userId, "userId");
|
|
817
|
+
if (!userId)
|
|
818
|
+
return missing("userId");
|
|
819
|
+
if (typeof args.durationMinutes !== "number" || args.durationMinutes <= 0) {
|
|
820
|
+
return fail("timeout requires `durationMinutes` (> 0).");
|
|
821
|
+
}
|
|
822
|
+
await timeout({ guildId, userId, durationMinutes: args.durationMinutes, ...(args.reason ? { reason: args.reason } : {}) }, rest);
|
|
823
|
+
return ok(`Timed out ${userId} for ${args.durationMinutes} minute(s).`);
|
|
824
|
+
}
|
|
825
|
+
case "untimeout": {
|
|
826
|
+
const guildId = need(args.guildId, "guildId");
|
|
827
|
+
if (!guildId)
|
|
828
|
+
return missing("guildId");
|
|
829
|
+
const userId = need(args.userId, "userId");
|
|
830
|
+
if (!userId)
|
|
831
|
+
return missing("userId");
|
|
832
|
+
await untimeout({ guildId, userId, ...(args.reason ? { reason: args.reason } : {}) }, rest);
|
|
833
|
+
return ok(`Cleared timeout for ${userId}.`);
|
|
834
|
+
}
|
|
835
|
+
default:
|
|
836
|
+
return fail(`Unknown action "${String(action)}".`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
if (err instanceof DiscordRestError) {
|
|
841
|
+
return fail(`Discord ${action} failed: ${err.message}`);
|
|
842
|
+
}
|
|
843
|
+
return fail(`Discord ${action} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
//# sourceMappingURL=discord-action-tool.js.map
|