diff --git a/CHANGELOG.md b/CHANGELOG.md index 34876ceed..9a1f13109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ - Telegram: add bidirectional reaction support with configurable notifications and agent guidance. (#964) — thanks @bohdanpodvirnyi. - Telegram: skip `message_thread_id=1` for General topic sends while keeping typing indicators. (#848) — thanks @azade-c. - Discord: allow allowlisted guilds without channel lists to receive messages when `groupPolicy="allowlist"`. — thanks @thewilloftheshadow. +- Discord: allow emoji/sticker uploads + channel actions in config defaults. (#870) — thanks @JDIVE. - Fix: sanitize user-facing error text + strip `` tags across reply pipelines. (#975) — thanks @ThomsenDrake. - Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba. diff --git a/docs/channels/discord.md b/docs/channels/discord.md index 3282279fb..3ded9f374 100644 --- a/docs/channels/discord.md +++ b/docs/channels/discord.md @@ -227,6 +227,8 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after actions: { reactions: true, stickers: true, + emojiUploads: true, + stickerUploads: true, polls: true, permissions: true, messages: true, @@ -237,6 +239,7 @@ Outbound Discord API calls retry on rate limits (429) using Discord `retry_after roleInfo: true, roles: false, channelInfo: true, + channels: true, voiceStatus: true, events: true, moderation: false @@ -304,8 +307,9 @@ ack reaction after the bot replies. - `retry`: retry policy for outbound Discord API calls (attempts, minDelayMs, maxDelayMs, jitter). - `actions`: per-action tool gates; omit to allow all (set `false` to disable). - `reactions` (covers react + read reactions) - - `stickers`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search` + - `stickers`, `emojiUploads`, `stickerUploads`, `polls`, `permissions`, `messages`, `threads`, `pins`, `search` - `memberInfo`, `roleInfo`, `channelInfo`, `voiceStatus`, `events` + - `channels` (create/edit/delete channels + categories + permissions) - `roles` (role add/remove, default `false`) - `moderation` (timeout/kick/ban, default `false`) @@ -321,6 +325,8 @@ Reaction notifications use `guilds..reactionNotifications`: | --- | --- | --- | | reactions | enabled | React + list reactions + emojiList | | stickers | enabled | Send stickers | +| emojiUploads | enabled | Upload emojis | +| stickerUploads | enabled | Upload stickers | | polls | enabled | Create polls | | permissions | enabled | Channel permission snapshot | | messages | enabled | Read/send/edit/delete | @@ -330,6 +336,7 @@ Reaction notifications use `guilds..reactionNotifications`: | memberInfo | enabled | Member info | | roleInfo | enabled | Role list | | channelInfo | enabled | Channel info + list | +| channels | enabled | Channel/category management | | voiceStatus | enabled | Voice state lookup | | events | enabled | List/create scheduled events | | roles | disabled | Role add/remove | diff --git a/src/agents/tools/discord-actions-guild.ts b/src/agents/tools/discord-actions-guild.ts index 87778143a..cf43f90af 100644 --- a/src/agents/tools/discord-actions-guild.ts +++ b/src/agents/tools/discord-actions-guild.ts @@ -219,7 +219,7 @@ export async function handleDiscordGuildAction( return jsonResult({ ok: true, event }); } case "channelCreate": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const guildId = readStringParam(params, "guildId", { required: true }); @@ -241,7 +241,7 @@ export async function handleDiscordGuildAction( return jsonResult({ ok: true, channel }); } case "channelEdit": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const channelId = readStringParam(params, "channelId", { @@ -267,7 +267,7 @@ export async function handleDiscordGuildAction( return jsonResult({ ok: true, channel }); } case "channelDelete": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const channelId = readStringParam(params, "channelId", { @@ -277,7 +277,7 @@ export async function handleDiscordGuildAction( return jsonResult(result); } case "channelMove": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const guildId = readStringParam(params, "guildId", { required: true }); @@ -295,7 +295,7 @@ export async function handleDiscordGuildAction( return jsonResult({ ok: true }); } case "categoryCreate": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const guildId = readStringParam(params, "guildId", { required: true }); @@ -310,7 +310,7 @@ export async function handleDiscordGuildAction( return jsonResult({ ok: true, category: channel }); } case "categoryEdit": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const categoryId = readStringParam(params, "categoryId", { @@ -326,7 +326,7 @@ export async function handleDiscordGuildAction( return jsonResult({ ok: true, category: channel }); } case "categoryDelete": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const categoryId = readStringParam(params, "categoryId", { @@ -336,7 +336,7 @@ export async function handleDiscordGuildAction( return jsonResult(result); } case "channelPermissionSet": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const channelId = readStringParam(params, "channelId", { @@ -359,7 +359,7 @@ export async function handleDiscordGuildAction( return jsonResult({ ok: true }); } case "channelPermissionRemove": { - if (!isActionEnabled("channels", false)) { + if (!isActionEnabled("channels")) { throw new Error("Discord channel management is disabled."); } const channelId = readStringParam(params, "channelId", { diff --git a/src/channels/plugins/actions/discord.test.ts b/src/channels/plugins/actions/discord.test.ts new file mode 100644 index 000000000..d68aba74b --- /dev/null +++ b/src/channels/plugins/actions/discord.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; + +import type { ClawdbotConfig } from "../../../config/config.js"; +import { discordMessageActions } from "./discord.js"; + +describe("discord message actions", () => { + it("lists channel and upload actions by default", () => { + const cfg = { channels: { discord: { token: "d0" } } } as ClawdbotConfig; + const actions = discordMessageActions.listActions?.({ cfg }) ?? []; + + expect(actions).toContain("emoji-upload"); + expect(actions).toContain("sticker-upload"); + expect(actions).toContain("channel-create"); + }); + + it("respects disabled channel actions", () => { + const cfg = { + channels: { discord: { token: "d0", actions: { channels: false } } }, + } as ClawdbotConfig; + const actions = discordMessageActions.listActions?.({ cfg }) ?? []; + + expect(actions).not.toContain("channel-create"); + }); +}); diff --git a/src/channels/plugins/actions/discord.ts b/src/channels/plugins/actions/discord.ts index b844eaee9..fcae08633 100644 --- a/src/channels/plugins/actions/discord.ts +++ b/src/channels/plugins/actions/discord.ts @@ -47,7 +47,7 @@ export const discordMessageActions: ChannelMessageActionAdapter = { actions.add("channel-info"); actions.add("channel-list"); } - if (gate("channels", false)) { + if (gate("channels")) { actions.add("channel-create"); actions.add("channel-edit"); actions.add("channel-delete"); diff --git a/src/config/config.discord.test.ts b/src/config/config.discord.test.ts index 958ee74c7..2656e95c5 100644 --- a/src/config/config.discord.test.ts +++ b/src/config/config.discord.test.ts @@ -28,13 +28,18 @@ describe("config discord", () => { dm: { enabled: true, allowFrom: ["steipete"], - groupEnabled: true, - groupChannels: ["clawd-dm"], - }, - guilds: { - "123": { - slug: "friends-of-clawd", - requireMention: false, + groupEnabled: true, + groupChannels: ["clawd-dm"], + }, + actions: { + emojiUploads: true, + stickerUploads: false, + channels: true, + }, + guilds: { + "123": { + slug: "friends-of-clawd", + requireMention: false, users: ["steipete"], channels: { general: { allow: true }, @@ -57,6 +62,9 @@ describe("config discord", () => { expect(cfg.channels?.discord?.enabled).toBe(true); expect(cfg.channels?.discord?.dm?.groupEnabled).toBe(true); expect(cfg.channels?.discord?.dm?.groupChannels).toEqual(["clawd-dm"]); + expect(cfg.channels?.discord?.actions?.emojiUploads).toBe(true); + expect(cfg.channels?.discord?.actions?.stickerUploads).toBe(false); + expect(cfg.channels?.discord?.actions?.channels).toBe(true); expect(cfg.channels?.discord?.guilds?.["123"]?.slug).toBe("friends-of-clawd"); expect(cfg.channels?.discord?.guilds?.["123"]?.channels?.general?.allow).toBe(true); }); diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 3ff045ef1..18851d557 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -154,6 +154,8 @@ export const DiscordAccountSchema = z.object({ .object({ reactions: z.boolean().optional(), stickers: z.boolean().optional(), + emojiUploads: z.boolean().optional(), + stickerUploads: z.boolean().optional(), polls: z.boolean().optional(), permissions: z.boolean().optional(), messages: z.boolean().optional(), @@ -167,6 +169,7 @@ export const DiscordAccountSchema = z.object({ voiceStatus: z.boolean().optional(), events: z.boolean().optional(), moderation: z.boolean().optional(), + channels: z.boolean().optional(), }) .optional(), replyToMode: ReplyToModeSchema.optional(),