feat(discord): add channel/category management actions (#487)
Co-authored-by: Shadow <shadow@clawd.bot>
This commit is contained in:
@@ -120,6 +120,7 @@
|
|||||||
- Gateway/CLI: allow dev profile (`clawdbot --dev`) to auto-create the dev config + workspace. — thanks @steipete
|
- Gateway/CLI: allow dev profile (`clawdbot --dev`) to auto-create the dev config + workspace. — thanks @steipete
|
||||||
- Dev templates: ship C-3PO dev workspace defaults as docs templates and use them for dev bootstrap. — thanks @steipete
|
- Dev templates: ship C-3PO dev workspace defaults as docs templates and use them for dev bootstrap. — thanks @steipete
|
||||||
- Config: fix Minimax hosted onboarding to write `agents.defaults` and allow `msteams` as a heartbeat target. — thanks @steipete
|
- Config: fix Minimax hosted onboarding to write `agents.defaults` and allow `msteams` as a heartbeat target. — thanks @steipete
|
||||||
|
- Discord: add channel/category management actions (create/edit/move/delete + category removal). (#487) - thanks @NicholasSpisak
|
||||||
|
|
||||||
## 2026.1.8
|
## 2026.1.8
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|||||||
import type { DiscordActionConfig } from "../../config/config.js";
|
import type { DiscordActionConfig } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
addRoleDiscord,
|
addRoleDiscord,
|
||||||
|
createChannelDiscord,
|
||||||
createScheduledEventDiscord,
|
createScheduledEventDiscord,
|
||||||
|
deleteChannelDiscord,
|
||||||
|
editChannelDiscord,
|
||||||
fetchChannelInfoDiscord,
|
fetchChannelInfoDiscord,
|
||||||
fetchMemberInfoDiscord,
|
fetchMemberInfoDiscord,
|
||||||
fetchRoleInfoDiscord,
|
fetchRoleInfoDiscord,
|
||||||
@@ -10,17 +13,28 @@ import {
|
|||||||
listGuildChannelsDiscord,
|
listGuildChannelsDiscord,
|
||||||
listGuildEmojisDiscord,
|
listGuildEmojisDiscord,
|
||||||
listScheduledEventsDiscord,
|
listScheduledEventsDiscord,
|
||||||
|
moveChannelDiscord,
|
||||||
|
removeChannelPermissionDiscord,
|
||||||
removeRoleDiscord,
|
removeRoleDiscord,
|
||||||
|
setChannelPermissionDiscord,
|
||||||
uploadEmojiDiscord,
|
uploadEmojiDiscord,
|
||||||
uploadStickerDiscord,
|
uploadStickerDiscord,
|
||||||
} from "../../discord/send.js";
|
} from "../../discord/send.js";
|
||||||
import {
|
import {
|
||||||
type ActionGate,
|
type ActionGate,
|
||||||
jsonResult,
|
jsonResult,
|
||||||
|
readNumberParam,
|
||||||
readStringArrayParam,
|
readStringArrayParam,
|
||||||
readStringParam,
|
readStringParam,
|
||||||
} from "./common.js";
|
} from "./common.js";
|
||||||
|
|
||||||
|
function readParentIdParam(
|
||||||
|
params: Record<string, unknown>,
|
||||||
|
): string | null | undefined {
|
||||||
|
if (params.parentId === null) return null;
|
||||||
|
return readStringParam(params, "parentId");
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleDiscordGuildAction(
|
export async function handleDiscordGuildAction(
|
||||||
action: string,
|
action: string,
|
||||||
params: Record<string, unknown>,
|
params: Record<string, unknown>,
|
||||||
@@ -207,6 +221,157 @@ export async function handleDiscordGuildAction(
|
|||||||
const event = await createScheduledEventDiscord(guildId, payload);
|
const event = await createScheduledEventDiscord(guildId, payload);
|
||||||
return jsonResult({ ok: true, event });
|
return jsonResult({ ok: true, event });
|
||||||
}
|
}
|
||||||
|
case "channelCreate": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const name = readStringParam(params, "name", { required: true });
|
||||||
|
const type = readNumberParam(params, "type", { integer: true });
|
||||||
|
const parentId = readParentIdParam(params);
|
||||||
|
const topic = readStringParam(params, "topic");
|
||||||
|
const position = readNumberParam(params, "position", { integer: true });
|
||||||
|
const nsfw = params.nsfw as boolean | undefined;
|
||||||
|
const channel = await createChannelDiscord({
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
type: type ?? undefined,
|
||||||
|
parentId: parentId ?? undefined,
|
||||||
|
topic: topic ?? undefined,
|
||||||
|
position: position ?? undefined,
|
||||||
|
nsfw,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, channel });
|
||||||
|
}
|
||||||
|
case "channelEdit": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const channelId = readStringParam(params, "channelId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const name = readStringParam(params, "name");
|
||||||
|
const topic = readStringParam(params, "topic");
|
||||||
|
const position = readNumberParam(params, "position", { integer: true });
|
||||||
|
const parentId = readParentIdParam(params);
|
||||||
|
const nsfw = params.nsfw as boolean | undefined;
|
||||||
|
const rateLimitPerUser = readNumberParam(params, "rateLimitPerUser", {
|
||||||
|
integer: true,
|
||||||
|
});
|
||||||
|
const channel = await editChannelDiscord({
|
||||||
|
channelId,
|
||||||
|
name: name ?? undefined,
|
||||||
|
topic: topic ?? undefined,
|
||||||
|
position: position ?? undefined,
|
||||||
|
parentId: parentId === undefined ? undefined : parentId,
|
||||||
|
nsfw,
|
||||||
|
rateLimitPerUser: rateLimitPerUser ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, channel });
|
||||||
|
}
|
||||||
|
case "channelDelete": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const channelId = readStringParam(params, "channelId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const result = await deleteChannelDiscord(channelId);
|
||||||
|
return jsonResult(result);
|
||||||
|
}
|
||||||
|
case "channelMove": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const channelId = readStringParam(params, "channelId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const parentId = readParentIdParam(params);
|
||||||
|
const position = readNumberParam(params, "position", { integer: true });
|
||||||
|
await moveChannelDiscord({
|
||||||
|
guildId,
|
||||||
|
channelId,
|
||||||
|
parentId: parentId === undefined ? undefined : parentId,
|
||||||
|
position: position ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true });
|
||||||
|
}
|
||||||
|
case "categoryCreate": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const guildId = readStringParam(params, "guildId", { required: true });
|
||||||
|
const name = readStringParam(params, "name", { required: true });
|
||||||
|
const position = readNumberParam(params, "position", { integer: true });
|
||||||
|
const channel = await createChannelDiscord({
|
||||||
|
guildId,
|
||||||
|
name,
|
||||||
|
type: 4,
|
||||||
|
position: position ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, category: channel });
|
||||||
|
}
|
||||||
|
case "categoryEdit": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const categoryId = readStringParam(params, "categoryId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const name = readStringParam(params, "name");
|
||||||
|
const position = readNumberParam(params, "position", { integer: true });
|
||||||
|
const channel = await editChannelDiscord({
|
||||||
|
channelId: categoryId,
|
||||||
|
name: name ?? undefined,
|
||||||
|
position: position ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true, category: channel });
|
||||||
|
}
|
||||||
|
case "categoryDelete": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const categoryId = readStringParam(params, "categoryId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const result = await deleteChannelDiscord(categoryId);
|
||||||
|
return jsonResult(result);
|
||||||
|
}
|
||||||
|
case "channelPermissionSet": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const channelId = readStringParam(params, "channelId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const targetId = readStringParam(params, "targetId", { required: true });
|
||||||
|
const targetTypeRaw = readStringParam(params, "targetType", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const targetType = targetTypeRaw === "member" ? 1 : 0;
|
||||||
|
const allow = readStringParam(params, "allow");
|
||||||
|
const deny = readStringParam(params, "deny");
|
||||||
|
await setChannelPermissionDiscord({
|
||||||
|
channelId,
|
||||||
|
targetId,
|
||||||
|
targetType,
|
||||||
|
allow: allow ?? undefined,
|
||||||
|
deny: deny ?? undefined,
|
||||||
|
});
|
||||||
|
return jsonResult({ ok: true });
|
||||||
|
}
|
||||||
|
case "channelPermissionRemove": {
|
||||||
|
if (!isActionEnabled("channels", false)) {
|
||||||
|
throw new Error("Discord channel management is disabled.");
|
||||||
|
}
|
||||||
|
const channelId = readStringParam(params, "channelId", {
|
||||||
|
required: true,
|
||||||
|
});
|
||||||
|
const targetId = readStringParam(params, "targetId", { required: true });
|
||||||
|
await removeChannelPermissionDiscord(channelId, targetId);
|
||||||
|
return jsonResult({ ok: true });
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown action: ${action}`);
|
throw new Error(`Unknown action: ${action}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,58 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import type { DiscordActionConfig } from "../../config/config.js";
|
import type { DiscordActionConfig } from "../../config/config.js";
|
||||||
|
import { handleDiscordGuildAction } from "./discord-actions-guild.js";
|
||||||
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
|
import { handleDiscordMessagingAction } from "./discord-actions-messaging.js";
|
||||||
|
|
||||||
|
const createChannelDiscord = vi.fn(async () => ({
|
||||||
|
id: "new-channel",
|
||||||
|
name: "test",
|
||||||
|
type: 0,
|
||||||
|
}));
|
||||||
const createThreadDiscord = vi.fn(async () => ({}));
|
const createThreadDiscord = vi.fn(async () => ({}));
|
||||||
|
const deleteChannelDiscord = vi.fn(async () => ({ ok: true, channelId: "C1" }));
|
||||||
const deleteMessageDiscord = vi.fn(async () => ({}));
|
const deleteMessageDiscord = vi.fn(async () => ({}));
|
||||||
|
const editChannelDiscord = vi.fn(async () => ({
|
||||||
|
id: "C1",
|
||||||
|
name: "edited",
|
||||||
|
}));
|
||||||
const editMessageDiscord = vi.fn(async () => ({}));
|
const editMessageDiscord = vi.fn(async () => ({}));
|
||||||
const fetchChannelPermissionsDiscord = vi.fn(async () => ({}));
|
const fetchChannelPermissionsDiscord = vi.fn(async () => ({}));
|
||||||
const fetchReactionsDiscord = vi.fn(async () => ({}));
|
const fetchReactionsDiscord = vi.fn(async () => ({}));
|
||||||
const listPinsDiscord = vi.fn(async () => ({}));
|
const listPinsDiscord = vi.fn(async () => ({}));
|
||||||
const listThreadsDiscord = vi.fn(async () => ({}));
|
const listThreadsDiscord = vi.fn(async () => ({}));
|
||||||
|
const moveChannelDiscord = vi.fn(async () => ({ ok: true }));
|
||||||
const pinMessageDiscord = vi.fn(async () => ({}));
|
const pinMessageDiscord = vi.fn(async () => ({}));
|
||||||
const reactMessageDiscord = vi.fn(async () => ({}));
|
const reactMessageDiscord = vi.fn(async () => ({}));
|
||||||
const readMessagesDiscord = vi.fn(async () => []);
|
const readMessagesDiscord = vi.fn(async () => []);
|
||||||
|
const removeChannelPermissionDiscord = vi.fn(async () => ({ ok: true }));
|
||||||
const removeOwnReactionsDiscord = vi.fn(async () => ({ removed: ["👍"] }));
|
const removeOwnReactionsDiscord = vi.fn(async () => ({ removed: ["👍"] }));
|
||||||
const removeReactionDiscord = vi.fn(async () => ({}));
|
const removeReactionDiscord = vi.fn(async () => ({}));
|
||||||
const searchMessagesDiscord = vi.fn(async () => ({}));
|
const searchMessagesDiscord = vi.fn(async () => ({}));
|
||||||
const sendMessageDiscord = vi.fn(async () => ({}));
|
const sendMessageDiscord = vi.fn(async () => ({}));
|
||||||
const sendPollDiscord = vi.fn(async () => ({}));
|
const sendPollDiscord = vi.fn(async () => ({}));
|
||||||
const sendStickerDiscord = vi.fn(async () => ({}));
|
const sendStickerDiscord = vi.fn(async () => ({}));
|
||||||
|
const setChannelPermissionDiscord = vi.fn(async () => ({ ok: true }));
|
||||||
const unpinMessageDiscord = vi.fn(async () => ({}));
|
const unpinMessageDiscord = vi.fn(async () => ({}));
|
||||||
|
|
||||||
vi.mock("../../discord/send.js", () => ({
|
vi.mock("../../discord/send.js", () => ({
|
||||||
|
createChannelDiscord: (...args: unknown[]) => createChannelDiscord(...args),
|
||||||
createThreadDiscord: (...args: unknown[]) => createThreadDiscord(...args),
|
createThreadDiscord: (...args: unknown[]) => createThreadDiscord(...args),
|
||||||
|
deleteChannelDiscord: (...args: unknown[]) => deleteChannelDiscord(...args),
|
||||||
deleteMessageDiscord: (...args: unknown[]) => deleteMessageDiscord(...args),
|
deleteMessageDiscord: (...args: unknown[]) => deleteMessageDiscord(...args),
|
||||||
|
editChannelDiscord: (...args: unknown[]) => editChannelDiscord(...args),
|
||||||
editMessageDiscord: (...args: unknown[]) => editMessageDiscord(...args),
|
editMessageDiscord: (...args: unknown[]) => editMessageDiscord(...args),
|
||||||
fetchChannelPermissionsDiscord: (...args: unknown[]) =>
|
fetchChannelPermissionsDiscord: (...args: unknown[]) =>
|
||||||
fetchChannelPermissionsDiscord(...args),
|
fetchChannelPermissionsDiscord(...args),
|
||||||
fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args),
|
fetchReactionsDiscord: (...args: unknown[]) => fetchReactionsDiscord(...args),
|
||||||
listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args),
|
listPinsDiscord: (...args: unknown[]) => listPinsDiscord(...args),
|
||||||
listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args),
|
listThreadsDiscord: (...args: unknown[]) => listThreadsDiscord(...args),
|
||||||
|
moveChannelDiscord: (...args: unknown[]) => moveChannelDiscord(...args),
|
||||||
pinMessageDiscord: (...args: unknown[]) => pinMessageDiscord(...args),
|
pinMessageDiscord: (...args: unknown[]) => pinMessageDiscord(...args),
|
||||||
reactMessageDiscord: (...args: unknown[]) => reactMessageDiscord(...args),
|
reactMessageDiscord: (...args: unknown[]) => reactMessageDiscord(...args),
|
||||||
readMessagesDiscord: (...args: unknown[]) => readMessagesDiscord(...args),
|
readMessagesDiscord: (...args: unknown[]) => readMessagesDiscord(...args),
|
||||||
|
removeChannelPermissionDiscord: (...args: unknown[]) =>
|
||||||
|
removeChannelPermissionDiscord(...args),
|
||||||
removeOwnReactionsDiscord: (...args: unknown[]) =>
|
removeOwnReactionsDiscord: (...args: unknown[]) =>
|
||||||
removeOwnReactionsDiscord(...args),
|
removeOwnReactionsDiscord(...args),
|
||||||
removeReactionDiscord: (...args: unknown[]) => removeReactionDiscord(...args),
|
removeReactionDiscord: (...args: unknown[]) => removeReactionDiscord(...args),
|
||||||
@@ -40,6 +60,8 @@ vi.mock("../../discord/send.js", () => ({
|
|||||||
sendMessageDiscord: (...args: unknown[]) => sendMessageDiscord(...args),
|
sendMessageDiscord: (...args: unknown[]) => sendMessageDiscord(...args),
|
||||||
sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args),
|
sendPollDiscord: (...args: unknown[]) => sendPollDiscord(...args),
|
||||||
sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args),
|
sendStickerDiscord: (...args: unknown[]) => sendStickerDiscord(...args),
|
||||||
|
setChannelPermissionDiscord: (...args: unknown[]) =>
|
||||||
|
setChannelPermissionDiscord(...args),
|
||||||
unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args),
|
unpinMessageDiscord: (...args: unknown[]) => unpinMessageDiscord(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -117,3 +139,214 @@ describe("handleDiscordMessagingAction", () => {
|
|||||||
).rejects.toThrow(/Discord reactions are disabled/);
|
).rejects.toThrow(/Discord reactions are disabled/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const channelsEnabled = (key: keyof DiscordActionConfig) => key === "channels";
|
||||||
|
const channelsDisabled = () => false;
|
||||||
|
|
||||||
|
describe("handleDiscordGuildAction - channel management", () => {
|
||||||
|
it("creates a channel", async () => {
|
||||||
|
const result = await handleDiscordGuildAction(
|
||||||
|
"channelCreate",
|
||||||
|
{
|
||||||
|
guildId: "G1",
|
||||||
|
name: "test-channel",
|
||||||
|
type: 0,
|
||||||
|
topic: "Test topic",
|
||||||
|
},
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(createChannelDiscord).toHaveBeenCalledWith({
|
||||||
|
guildId: "G1",
|
||||||
|
name: "test-channel",
|
||||||
|
type: 0,
|
||||||
|
parentId: undefined,
|
||||||
|
topic: "Test topic",
|
||||||
|
position: undefined,
|
||||||
|
nsfw: undefined,
|
||||||
|
});
|
||||||
|
expect(result.details).toMatchObject({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects channel gating for channelCreate", async () => {
|
||||||
|
await expect(
|
||||||
|
handleDiscordGuildAction(
|
||||||
|
"channelCreate",
|
||||||
|
{ guildId: "G1", name: "test" },
|
||||||
|
channelsDisabled,
|
||||||
|
),
|
||||||
|
).rejects.toThrow(/Discord channel management is disabled/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("edits a channel", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"channelEdit",
|
||||||
|
{
|
||||||
|
channelId: "C1",
|
||||||
|
name: "new-name",
|
||||||
|
topic: "new topic",
|
||||||
|
},
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(editChannelDiscord).toHaveBeenCalledWith({
|
||||||
|
channelId: "C1",
|
||||||
|
name: "new-name",
|
||||||
|
topic: "new topic",
|
||||||
|
position: undefined,
|
||||||
|
parentId: undefined,
|
||||||
|
nsfw: undefined,
|
||||||
|
rateLimitPerUser: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the channel parent when parentId is null", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"channelEdit",
|
||||||
|
{
|
||||||
|
channelId: "C1",
|
||||||
|
parentId: null,
|
||||||
|
},
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(editChannelDiscord).toHaveBeenCalledWith({
|
||||||
|
channelId: "C1",
|
||||||
|
name: undefined,
|
||||||
|
topic: undefined,
|
||||||
|
position: undefined,
|
||||||
|
parentId: null,
|
||||||
|
nsfw: undefined,
|
||||||
|
rateLimitPerUser: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes a channel", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"channelDelete",
|
||||||
|
{ channelId: "C1" },
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(deleteChannelDiscord).toHaveBeenCalledWith("C1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moves a channel", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"channelMove",
|
||||||
|
{
|
||||||
|
guildId: "G1",
|
||||||
|
channelId: "C1",
|
||||||
|
parentId: "P1",
|
||||||
|
position: 5,
|
||||||
|
},
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(moveChannelDiscord).toHaveBeenCalledWith({
|
||||||
|
guildId: "G1",
|
||||||
|
channelId: "C1",
|
||||||
|
parentId: "P1",
|
||||||
|
position: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the channel parent on move when parentId is null", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"channelMove",
|
||||||
|
{
|
||||||
|
guildId: "G1",
|
||||||
|
channelId: "C1",
|
||||||
|
parentId: null,
|
||||||
|
},
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(moveChannelDiscord).toHaveBeenCalledWith({
|
||||||
|
guildId: "G1",
|
||||||
|
channelId: "C1",
|
||||||
|
parentId: null,
|
||||||
|
position: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a category with type=4", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"categoryCreate",
|
||||||
|
{ guildId: "G1", name: "My Category" },
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(createChannelDiscord).toHaveBeenCalledWith({
|
||||||
|
guildId: "G1",
|
||||||
|
name: "My Category",
|
||||||
|
type: 4,
|
||||||
|
position: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("edits a category", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"categoryEdit",
|
||||||
|
{ categoryId: "CAT1", name: "Renamed Category" },
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(editChannelDiscord).toHaveBeenCalledWith({
|
||||||
|
channelId: "CAT1",
|
||||||
|
name: "Renamed Category",
|
||||||
|
position: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes a category", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"categoryDelete",
|
||||||
|
{ categoryId: "CAT1" },
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(deleteChannelDiscord).toHaveBeenCalledWith("CAT1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets channel permissions for role", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"channelPermissionSet",
|
||||||
|
{
|
||||||
|
channelId: "C1",
|
||||||
|
targetId: "R1",
|
||||||
|
targetType: "role",
|
||||||
|
allow: "1024",
|
||||||
|
deny: "2048",
|
||||||
|
},
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(setChannelPermissionDiscord).toHaveBeenCalledWith({
|
||||||
|
channelId: "C1",
|
||||||
|
targetId: "R1",
|
||||||
|
targetType: 0,
|
||||||
|
allow: "1024",
|
||||||
|
deny: "2048",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets channel permissions for member", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"channelPermissionSet",
|
||||||
|
{
|
||||||
|
channelId: "C1",
|
||||||
|
targetId: "U1",
|
||||||
|
targetType: "member",
|
||||||
|
allow: "1024",
|
||||||
|
},
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(setChannelPermissionDiscord).toHaveBeenCalledWith({
|
||||||
|
channelId: "C1",
|
||||||
|
targetId: "U1",
|
||||||
|
targetType: 1,
|
||||||
|
allow: "1024",
|
||||||
|
deny: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes channel permissions", async () => {
|
||||||
|
await handleDiscordGuildAction(
|
||||||
|
"channelPermissionRemove",
|
||||||
|
{ channelId: "C1", targetId: "R1" },
|
||||||
|
channelsEnabled,
|
||||||
|
);
|
||||||
|
expect(removeChannelPermissionDiscord).toHaveBeenCalledWith("C1", "R1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ const guildActions = new Set([
|
|||||||
"voiceStatus",
|
"voiceStatus",
|
||||||
"eventList",
|
"eventList",
|
||||||
"eventCreate",
|
"eventCreate",
|
||||||
|
"channelCreate",
|
||||||
|
"channelEdit",
|
||||||
|
"channelDelete",
|
||||||
|
"channelMove",
|
||||||
|
"categoryCreate",
|
||||||
|
"categoryEdit",
|
||||||
|
"categoryDelete",
|
||||||
|
"channelPermissionSet",
|
||||||
|
"channelPermissionRemove",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const moderationActions = new Set(["timeout", "kick", "ban"]);
|
const moderationActions = new Set(["timeout", "kick", "ban"]);
|
||||||
|
|||||||
@@ -202,4 +202,67 @@ export const DiscordToolSchema = Type.Union([
|
|||||||
reason: Type.Optional(Type.String()),
|
reason: Type.Optional(Type.String()),
|
||||||
deleteMessageDays: Type.Optional(Type.Number()),
|
deleteMessageDays: Type.Optional(Type.Number()),
|
||||||
}),
|
}),
|
||||||
|
// Channel management actions
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("channelCreate"),
|
||||||
|
guildId: Type.String(),
|
||||||
|
name: Type.String(),
|
||||||
|
type: Type.Optional(Type.Number()),
|
||||||
|
parentId: Type.Optional(Type.String()),
|
||||||
|
topic: Type.Optional(Type.String()),
|
||||||
|
position: Type.Optional(Type.Number()),
|
||||||
|
nsfw: Type.Optional(Type.Boolean()),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("channelEdit"),
|
||||||
|
channelId: Type.String(),
|
||||||
|
name: Type.Optional(Type.String()),
|
||||||
|
topic: Type.Optional(Type.String()),
|
||||||
|
position: Type.Optional(Type.Number()),
|
||||||
|
parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||||
|
nsfw: Type.Optional(Type.Boolean()),
|
||||||
|
rateLimitPerUser: Type.Optional(Type.Number()),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("channelDelete"),
|
||||||
|
channelId: Type.String(),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("channelMove"),
|
||||||
|
guildId: Type.String(),
|
||||||
|
channelId: Type.String(),
|
||||||
|
parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
||||||
|
position: Type.Optional(Type.Number()),
|
||||||
|
}),
|
||||||
|
// Category management actions (convenience aliases)
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("categoryCreate"),
|
||||||
|
guildId: Type.String(),
|
||||||
|
name: Type.String(),
|
||||||
|
position: Type.Optional(Type.Number()),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("categoryEdit"),
|
||||||
|
categoryId: Type.String(),
|
||||||
|
name: Type.Optional(Type.String()),
|
||||||
|
position: Type.Optional(Type.Number()),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("categoryDelete"),
|
||||||
|
categoryId: Type.String(),
|
||||||
|
}),
|
||||||
|
// Permission overwrite actions
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("channelPermissionSet"),
|
||||||
|
channelId: Type.String(),
|
||||||
|
targetId: Type.String(),
|
||||||
|
targetType: Type.Union([Type.Literal("role"), Type.Literal("member")]),
|
||||||
|
allow: Type.Optional(Type.String()),
|
||||||
|
deny: Type.Optional(Type.String()),
|
||||||
|
}),
|
||||||
|
Type.Object({
|
||||||
|
action: Type.Literal("channelPermissionRemove"),
|
||||||
|
channelId: Type.String(),
|
||||||
|
targetId: Type.String(),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -404,6 +404,7 @@ export type DiscordActionConfig = {
|
|||||||
moderation?: boolean;
|
moderation?: boolean;
|
||||||
emojiUploads?: boolean;
|
emojiUploads?: boolean;
|
||||||
stickerUploads?: boolean;
|
stickerUploads?: boolean;
|
||||||
|
channels?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiscordAccountConfig = {
|
export type DiscordAccountConfig = {
|
||||||
|
|||||||
@@ -183,6 +183,41 @@ export type DiscordStickerUpload = {
|
|||||||
mediaUrl: string;
|
mediaUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DiscordChannelCreate = {
|
||||||
|
guildId: string;
|
||||||
|
name: string;
|
||||||
|
type?: number;
|
||||||
|
parentId?: string;
|
||||||
|
topic?: string;
|
||||||
|
position?: number;
|
||||||
|
nsfw?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiscordChannelEdit = {
|
||||||
|
channelId: string;
|
||||||
|
name?: string;
|
||||||
|
topic?: string;
|
||||||
|
position?: number;
|
||||||
|
parentId?: string | null;
|
||||||
|
nsfw?: boolean;
|
||||||
|
rateLimitPerUser?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiscordChannelMove = {
|
||||||
|
guildId: string;
|
||||||
|
channelId: string;
|
||||||
|
parentId?: string | null;
|
||||||
|
position?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiscordChannelPermissionSet = {
|
||||||
|
channelId: string;
|
||||||
|
targetId: string;
|
||||||
|
targetType: 0 | 1;
|
||||||
|
allow?: string;
|
||||||
|
deny?: string;
|
||||||
|
};
|
||||||
|
|
||||||
function resolveToken(params: {
|
function resolveToken(params: {
|
||||||
explicit?: string;
|
explicit?: string;
|
||||||
accountId: string;
|
accountId: string;
|
||||||
@@ -1191,3 +1226,93 @@ export async function banMemberDiscord(
|
|||||||
});
|
});
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Channel management functions
|
||||||
|
|
||||||
|
export async function createChannelDiscord(
|
||||||
|
payload: DiscordChannelCreate,
|
||||||
|
opts: DiscordReactOpts = {},
|
||||||
|
): Promise<APIChannel> {
|
||||||
|
const rest = resolveDiscordRest(opts);
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
name: payload.name,
|
||||||
|
};
|
||||||
|
if (payload.type !== undefined) body.type = payload.type;
|
||||||
|
if (payload.parentId) body.parent_id = payload.parentId;
|
||||||
|
if (payload.topic) body.topic = payload.topic;
|
||||||
|
if (payload.position !== undefined) body.position = payload.position;
|
||||||
|
if (payload.nsfw !== undefined) body.nsfw = payload.nsfw;
|
||||||
|
return (await rest.post(Routes.guildChannels(payload.guildId), {
|
||||||
|
body,
|
||||||
|
})) as APIChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editChannelDiscord(
|
||||||
|
payload: DiscordChannelEdit,
|
||||||
|
opts: DiscordReactOpts = {},
|
||||||
|
): Promise<APIChannel> {
|
||||||
|
const rest = resolveDiscordRest(opts);
|
||||||
|
const body: Record<string, unknown> = {};
|
||||||
|
if (payload.name !== undefined) body.name = payload.name;
|
||||||
|
if (payload.topic !== undefined) body.topic = payload.topic;
|
||||||
|
if (payload.position !== undefined) body.position = payload.position;
|
||||||
|
if (payload.parentId !== undefined) body.parent_id = payload.parentId;
|
||||||
|
if (payload.nsfw !== undefined) body.nsfw = payload.nsfw;
|
||||||
|
if (payload.rateLimitPerUser !== undefined)
|
||||||
|
body.rate_limit_per_user = payload.rateLimitPerUser;
|
||||||
|
return (await rest.patch(Routes.channel(payload.channelId), {
|
||||||
|
body,
|
||||||
|
})) as APIChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteChannelDiscord(
|
||||||
|
channelId: string,
|
||||||
|
opts: DiscordReactOpts = {},
|
||||||
|
) {
|
||||||
|
const rest = resolveDiscordRest(opts);
|
||||||
|
await rest.delete(Routes.channel(channelId));
|
||||||
|
return { ok: true, channelId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveChannelDiscord(
|
||||||
|
payload: DiscordChannelMove,
|
||||||
|
opts: DiscordReactOpts = {},
|
||||||
|
) {
|
||||||
|
const rest = resolveDiscordRest(opts);
|
||||||
|
const body: Array<Record<string, unknown>> = [
|
||||||
|
{
|
||||||
|
id: payload.channelId,
|
||||||
|
...(payload.parentId !== undefined && { parent_id: payload.parentId }),
|
||||||
|
...(payload.position !== undefined && { position: payload.position }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await rest.patch(Routes.guildChannels(payload.guildId), { body });
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setChannelPermissionDiscord(
|
||||||
|
payload: DiscordChannelPermissionSet,
|
||||||
|
opts: DiscordReactOpts = {},
|
||||||
|
) {
|
||||||
|
const rest = resolveDiscordRest(opts);
|
||||||
|
const body: Record<string, unknown> = {
|
||||||
|
type: payload.targetType,
|
||||||
|
};
|
||||||
|
if (payload.allow !== undefined) body.allow = payload.allow;
|
||||||
|
if (payload.deny !== undefined) body.deny = payload.deny;
|
||||||
|
await rest.put(
|
||||||
|
`/channels/${payload.channelId}/permissions/${payload.targetId}`,
|
||||||
|
{ body },
|
||||||
|
);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeChannelPermissionDiscord(
|
||||||
|
channelId: string,
|
||||||
|
targetId: string,
|
||||||
|
opts: DiscordReactOpts = {},
|
||||||
|
) {
|
||||||
|
const rest = resolveDiscordRest(opts);
|
||||||
|
await rest.delete(`/channels/${channelId}/permissions/${targetId}`);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user