Merge pull request #870 from JDIVE/fix/discord-actions-schema

fix(config): allow discord action flags in schema
This commit is contained in:
Peter Steinberger
2026-01-16 06:50:50 +00:00
committed by GitHub
7 changed files with 61 additions and 18 deletions

View File

@@ -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 `<final>` tags across reply pipelines. (#975) — thanks @ThomsenDrake.
- Fix: normalize pairing CLI aliases, allow extension channels, and harden Zalo webhook payload parsing. (#991) — thanks @longmaba.

View File

@@ -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.<id>.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.<id>.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 |

View File

@@ -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", {

View File

@@ -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");
});
});

View File

@@ -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");

View File

@@ -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);
});

View File

@@ -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(),