feat(commands): unify chat commands (#275)
* Chat commands: registry, access groups, Carbon * Chat commands: clear native commands on disable * fix(commands): align command surface typing * docs(changelog): note commands registry (PR #275) --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { Guild } from "@buape/carbon";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
allowListMatches,
|
||||
@@ -12,8 +13,7 @@ import {
|
||||
shouldEmitDiscordReactionNotification,
|
||||
} from "./monitor.js";
|
||||
|
||||
const fakeGuild = (id: string, name: string) =>
|
||||
({ id, name }) as unknown as import("discord.js").Guild;
|
||||
const fakeGuild = (id: string, name: string) => ({ id, name }) as Guild;
|
||||
|
||||
const makeEntries = (
|
||||
entries: Record<string, Partial<DiscordGuildEntryResolved>>,
|
||||
|
||||
@@ -1,267 +1,170 @@
|
||||
import type { Client } from "@buape/carbon";
|
||||
import { ChannelType, MessageType } from "@buape/carbon";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { monitorDiscordProvider } from "./monitor.js";
|
||||
|
||||
const sendMock = vi.fn();
|
||||
const replyMock = vi.fn();
|
||||
const updateLastRouteMock = vi.fn();
|
||||
let config: Record<string, unknown> = {};
|
||||
const readAllowFromStoreMock = vi.fn();
|
||||
const upsertPairingRequestMock = vi.fn();
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => config,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../auto-reply/reply.js", () => ({
|
||||
getReplyFromConfig: (...args: unknown[]) => replyMock(...args),
|
||||
}));
|
||||
const dispatchMock = vi.fn();
|
||||
|
||||
vi.mock("./send.js", () => ({
|
||||
sendMessageDiscord: (...args: unknown[]) => sendMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readProviderAllowFromStore: (...args: unknown[]) =>
|
||||
readAllowFromStoreMock(...args),
|
||||
upsertProviderPairingRequest: (...args: unknown[]) =>
|
||||
upsertPairingRequestMock(...args),
|
||||
vi.mock("../auto-reply/reply/dispatch-from-config.js", () => ({
|
||||
dispatchReplyFromConfig: (...args: unknown[]) => dispatchMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../config/sessions.js", () => ({
|
||||
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
|
||||
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
|
||||
resolveSessionKey: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("discord.js", () => {
|
||||
const handlers = new Map<string, Set<(...args: unknown[]) => void>>();
|
||||
class Client {
|
||||
static lastClient: Client | null = null;
|
||||
user = { id: "bot-id", tag: "bot#1" };
|
||||
constructor() {
|
||||
Client.lastClient = this;
|
||||
}
|
||||
on(event: string, handler: (...args: unknown[]) => void) {
|
||||
if (!handlers.has(event)) handlers.set(event, new Set());
|
||||
handlers.get(event)?.add(handler);
|
||||
}
|
||||
once(event: string, handler: (...args: unknown[]) => void) {
|
||||
this.on(event, handler);
|
||||
}
|
||||
off(event: string, handler: (...args: unknown[]) => void) {
|
||||
handlers.get(event)?.delete(handler);
|
||||
}
|
||||
emit(event: string, ...args: unknown[]) {
|
||||
for (const handler of handlers.get(event) ?? []) {
|
||||
Promise.resolve(handler(...args)).catch(() => {});
|
||||
}
|
||||
}
|
||||
login = vi.fn().mockResolvedValue(undefined);
|
||||
destroy = vi.fn().mockImplementation(async () => {
|
||||
handlers.clear();
|
||||
Client.lastClient = null;
|
||||
});
|
||||
}
|
||||
|
||||
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||
return {
|
||||
Client,
|
||||
__getLastClient: () => Client.lastClient,
|
||||
Events: {
|
||||
ClientReady: "ready",
|
||||
Error: "error",
|
||||
MessageCreate: "messageCreate",
|
||||
MessageReactionAdd: "reactionAdd",
|
||||
MessageReactionRemove: "reactionRemove",
|
||||
},
|
||||
ChannelType: {
|
||||
DM: "dm",
|
||||
GroupDM: "group_dm",
|
||||
GuildText: "guild_text",
|
||||
},
|
||||
MessageType: {
|
||||
Default: "default",
|
||||
ChatInputCommand: "chat_command",
|
||||
ContextMenuCommand: "context_command",
|
||||
},
|
||||
GatewayIntentBits: {},
|
||||
Partials: {},
|
||||
...actual,
|
||||
resolveStorePath: vi.fn(() => "/tmp/clawdbot-sessions.json"),
|
||||
updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args),
|
||||
resolveSessionKey: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
async function waitForClient() {
|
||||
const discord = (await import("discord.js")) as unknown as {
|
||||
__getLastClient: () => { emit: (...args: unknown[]) => void } | null;
|
||||
};
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
const client = discord.__getLastClient();
|
||||
if (client) return client;
|
||||
await flush();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
config = {
|
||||
messages: { responsePrefix: "PFX" },
|
||||
discord: { dm: { enabled: true, policy: "open", allowFrom: ["*"] } },
|
||||
routing: { allowFrom: [] },
|
||||
};
|
||||
sendMock.mockReset().mockResolvedValue(undefined);
|
||||
replyMock.mockReset();
|
||||
updateLastRouteMock.mockReset();
|
||||
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
||||
upsertPairingRequestMock
|
||||
.mockReset()
|
||||
.mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
dispatchMock.mockReset().mockImplementation(async ({ dispatcher }) => {
|
||||
dispatcher.sendFinalReply({ text: "hi" });
|
||||
return { queuedFinal: true, counts: { final: 1 } };
|
||||
});
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
describe("monitorDiscordProvider tool results", () => {
|
||||
it("sends tool summaries with responsePrefix", async () => {
|
||||
replyMock.mockImplementation(async (_ctx, opts) => {
|
||||
await opts?.onToolResult?.({ text: "tool update" });
|
||||
return { text: "final reply" };
|
||||
});
|
||||
describe("discord tool result dispatch", () => {
|
||||
it("sends status replies with responsePrefix", async () => {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
const cfg = {
|
||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
messages: { responsePrefix: "PFX" },
|
||||
discord: { dm: { enabled: true, policy: "open" } },
|
||||
routing: { allowFrom: [] },
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const controller = new AbortController();
|
||||
const run = monitorDiscordProvider({
|
||||
const runtimeError = vi.fn();
|
||||
const handler = createDiscordMessageHandler({
|
||||
cfg,
|
||||
token: "token",
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
|
||||
const discord = await import("discord.js");
|
||||
const client = await waitForClient();
|
||||
if (!client) throw new Error("Discord client not created");
|
||||
|
||||
client.emit(discord.Events.MessageCreate, {
|
||||
id: "m1",
|
||||
content: "hello",
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
channelId: "c1",
|
||||
channel: {
|
||||
type: discord.ChannelType.DM,
|
||||
isSendable: () => false,
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: runtimeError,
|
||||
exit: (code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
},
|
||||
guild: undefined,
|
||||
mentions: { has: () => false },
|
||||
attachments: { first: () => undefined },
|
||||
type: discord.MessageType.Default,
|
||||
createdTimestamp: Date.now(),
|
||||
botUserId: "bot-id",
|
||||
guildHistories: new Map(),
|
||||
historyLimit: 0,
|
||||
mediaMaxBytes: 10_000,
|
||||
textLimit: 2000,
|
||||
replyToMode: "off",
|
||||
dmEnabled: true,
|
||||
groupDmEnabled: false,
|
||||
});
|
||||
|
||||
await flush();
|
||||
controller.abort();
|
||||
await run;
|
||||
const client = {
|
||||
fetchChannel: vi.fn().mockResolvedValue({
|
||||
type: ChannelType.DM,
|
||||
name: "dm",
|
||||
}),
|
||||
} as unknown as Client;
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(2);
|
||||
expect(sendMock.mock.calls[0][1]).toBe("PFX tool update");
|
||||
expect(sendMock.mock.calls[1][1]).toBe("PFX final reply");
|
||||
});
|
||||
await handler(
|
||||
{
|
||||
message: {
|
||||
id: "m1",
|
||||
content: "/status",
|
||||
channelId: "c1",
|
||||
timestamp: new Date().toISOString(),
|
||||
type: MessageType.Default,
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
mentionedEveryone: false,
|
||||
mentionedUsers: [],
|
||||
mentionedRoles: [],
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
},
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
guild_id: null,
|
||||
},
|
||||
client,
|
||||
);
|
||||
|
||||
expect(runtimeError).not.toHaveBeenCalled();
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0]?.[1]).toMatch(/^PFX /);
|
||||
}, 10000);
|
||||
|
||||
it("accepts guild messages when mentionPatterns match", async () => {
|
||||
config = {
|
||||
const { createDiscordMessageHandler } = await import("./monitor.js");
|
||||
const cfg = {
|
||||
agent: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/clawd" },
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
messages: { responsePrefix: "PFX" },
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
dm: { enabled: true, policy: "open" },
|
||||
guilds: { "*": { requireMention: true } },
|
||||
},
|
||||
routing: {
|
||||
allowFrom: [],
|
||||
groupChat: { mentionPatterns: ["\\bclawd\\b"] },
|
||||
},
|
||||
};
|
||||
replyMock.mockResolvedValue({ text: "hi" });
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const controller = new AbortController();
|
||||
const run = monitorDiscordProvider({
|
||||
const handler = createDiscordMessageHandler({
|
||||
cfg,
|
||||
token: "token",
|
||||
abortSignal: controller.signal,
|
||||
runtime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: (code: number): never => {
|
||||
throw new Error(`exit ${code}`);
|
||||
},
|
||||
},
|
||||
botUserId: "bot-id",
|
||||
guildHistories: new Map(),
|
||||
historyLimit: 0,
|
||||
mediaMaxBytes: 10_000,
|
||||
textLimit: 2000,
|
||||
replyToMode: "off",
|
||||
dmEnabled: true,
|
||||
groupDmEnabled: false,
|
||||
guildEntries: { "*": { requireMention: true } },
|
||||
});
|
||||
|
||||
const discord = await import("discord.js");
|
||||
const client = await waitForClient();
|
||||
if (!client) throw new Error("Discord client not created");
|
||||
|
||||
client.emit(discord.Events.MessageCreate, {
|
||||
id: "m2",
|
||||
content: "clawd: hello",
|
||||
author: { id: "u1", bot: false, username: "Ada", tag: "Ada#1" },
|
||||
member: { displayName: "Ada" },
|
||||
channelId: "c1",
|
||||
channel: {
|
||||
type: discord.ChannelType.GuildText,
|
||||
const client = {
|
||||
fetchChannel: vi.fn().mockResolvedValue({
|
||||
type: ChannelType.GuildText,
|
||||
name: "general",
|
||||
isSendable: () => false,
|
||||
}),
|
||||
} as unknown as Client;
|
||||
|
||||
await handler(
|
||||
{
|
||||
message: {
|
||||
id: "m2",
|
||||
content: "clawd: hello",
|
||||
channelId: "c1",
|
||||
timestamp: new Date().toISOString(),
|
||||
type: MessageType.Default,
|
||||
attachments: [],
|
||||
embeds: [],
|
||||
mentionedEveryone: false,
|
||||
mentionedUsers: [],
|
||||
mentionedRoles: [],
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
},
|
||||
author: { id: "u1", bot: false, username: "Ada" },
|
||||
member: { nickname: "Ada" },
|
||||
guild: { id: "g1", name: "Guild" },
|
||||
guild_id: "g1",
|
||||
},
|
||||
guild: { id: "g1", name: "Guild" },
|
||||
mentions: {
|
||||
has: () => false,
|
||||
everyone: false,
|
||||
users: { size: 0 },
|
||||
roles: { size: 0 },
|
||||
},
|
||||
attachments: { first: () => undefined },
|
||||
type: discord.MessageType.Default,
|
||||
createdTimestamp: Date.now(),
|
||||
});
|
||||
|
||||
await flush();
|
||||
controller.abort();
|
||||
await run;
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||
expect(replyMock.mock.calls[0][0].WasMentioned).toBe(true);
|
||||
});
|
||||
|
||||
it("replies with pairing code when dmPolicy is pairing and no allowFrom is set", async () => {
|
||||
config = {
|
||||
...config,
|
||||
discord: { dm: { enabled: true, policy: "pairing", allowFrom: [] } },
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const run = monitorDiscordProvider({
|
||||
token: "token",
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
|
||||
const discord = await import("discord.js");
|
||||
const client = await waitForClient();
|
||||
if (!client) throw new Error("Discord client not created");
|
||||
|
||||
const reply = vi.fn().mockResolvedValue(undefined);
|
||||
client.emit(discord.Events.MessageCreate, {
|
||||
id: "m3",
|
||||
content: "hello",
|
||||
author: { id: "u1", bot: false, username: "Ada", tag: "Ada#1" },
|
||||
channelId: "c1",
|
||||
channel: {
|
||||
type: discord.ChannelType.DM,
|
||||
isSendable: () => false,
|
||||
},
|
||||
guild: undefined,
|
||||
mentions: { has: () => false },
|
||||
attachments: { first: () => undefined },
|
||||
type: discord.MessageType.Default,
|
||||
createdTimestamp: Date.now(),
|
||||
reply,
|
||||
});
|
||||
|
||||
await flush();
|
||||
controller.abort();
|
||||
await run;
|
||||
|
||||
expect(replyMock).not.toHaveBeenCalled();
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalled();
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(String(reply.mock.calls[0]?.[0] ?? "")).toContain(
|
||||
"Pairing code: PAIRCODE",
|
||||
client,
|
||||
);
|
||||
});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,3 +74,27 @@ export async function probeDiscord(
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDiscordApplicationId(
|
||||
token: string,
|
||||
timeoutMs: number,
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<string | undefined> {
|
||||
const normalized = normalizeDiscordToken(token);
|
||||
if (!normalized) return undefined;
|
||||
try {
|
||||
const res = await fetchWithTimeout(
|
||||
`${DISCORD_API_BASE}/oauth2/applications/@me`,
|
||||
timeoutMs,
|
||||
fetcher,
|
||||
{
|
||||
Authorization: `Bot ${normalized}`,
|
||||
},
|
||||
);
|
||||
if (!res.ok) return undefined;
|
||||
const json = (await res.json()) as { id?: string };
|
||||
return json.id ?? undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PermissionsBitField, Routes } from "discord.js";
|
||||
import { PermissionFlagsBits, Routes } from "discord-api-types/v10";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
@@ -53,7 +53,7 @@ const makeRest = () => {
|
||||
get: getMock,
|
||||
patch: patchMock,
|
||||
delete: deleteMock,
|
||||
} as unknown as import("discord.js").REST,
|
||||
} as unknown as import("@buape/carbon").RequestClient,
|
||||
postMock,
|
||||
putMock,
|
||||
getMock,
|
||||
@@ -108,9 +108,7 @@ describe("sendMessageDiscord", () => {
|
||||
|
||||
it("adds missing permission hints on 50013", async () => {
|
||||
const { rest, postMock, getMock } = makeRest();
|
||||
const perms = new PermissionsBitField([
|
||||
PermissionsBitField.Flags.ViewChannel,
|
||||
]);
|
||||
const perms = PermissionFlagsBits.ViewChannel;
|
||||
const apiError = Object.assign(new Error("Missing Permissions"), {
|
||||
code: 50013,
|
||||
status: 403,
|
||||
@@ -126,7 +124,7 @@ describe("sendMessageDiscord", () => {
|
||||
.mockResolvedValueOnce({ id: "bot1" })
|
||||
.mockResolvedValueOnce({
|
||||
id: "guild1",
|
||||
roles: [{ id: "guild1", permissions: perms.bitfield.toString() }],
|
||||
roles: [{ id: "guild1", permissions: perms.toString() }],
|
||||
})
|
||||
.mockResolvedValueOnce({ roles: [] });
|
||||
|
||||
@@ -152,7 +150,9 @@ describe("sendMessageDiscord", () => {
|
||||
expect(postMock).toHaveBeenCalledWith(
|
||||
Routes.channelMessages("789"),
|
||||
expect.objectContaining({
|
||||
files: [expect.objectContaining({ name: "photo.jpg" })],
|
||||
body: expect.objectContaining({
|
||||
files: [expect.objectContaining({ name: "photo.jpg" })],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -268,10 +268,8 @@ describe("fetchChannelPermissionsDiscord", () => {
|
||||
|
||||
it("calculates permissions from guild roles", async () => {
|
||||
const { rest, getMock } = makeRest();
|
||||
const perms = new PermissionsBitField([
|
||||
PermissionsBitField.Flags.ViewChannel,
|
||||
PermissionsBitField.Flags.SendMessages,
|
||||
]);
|
||||
const perms =
|
||||
PermissionFlagsBits.ViewChannel | PermissionFlagsBits.SendMessages;
|
||||
getMock
|
||||
.mockResolvedValueOnce({
|
||||
id: "chan1",
|
||||
@@ -282,7 +280,7 @@ describe("fetchChannelPermissionsDiscord", () => {
|
||||
.mockResolvedValueOnce({
|
||||
id: "guild1",
|
||||
roles: [
|
||||
{ id: "guild1", permissions: perms.bitfield.toString() },
|
||||
{ id: "guild1", permissions: perms.toString() },
|
||||
{ id: "role2", permissions: "0" },
|
||||
],
|
||||
})
|
||||
@@ -303,7 +301,7 @@ describe("readMessagesDiscord", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("passes query params as URLSearchParams", async () => {
|
||||
it("passes query params as an object", async () => {
|
||||
const { rest, getMock } = makeRest();
|
||||
getMock.mockResolvedValue([]);
|
||||
await readMessagesDiscord(
|
||||
@@ -312,8 +310,8 @@ describe("readMessagesDiscord", () => {
|
||||
{ rest, token: "t" },
|
||||
);
|
||||
const call = getMock.mock.calls[0];
|
||||
const options = call?.[1] as { query?: URLSearchParams };
|
||||
expect(options.query?.toString()).toBe("limit=5&before=10");
|
||||
const options = call?.[1] as Record<string, unknown>;
|
||||
expect(options).toEqual({ limit: 5, before: "10" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -376,8 +374,7 @@ describe("searchMessagesDiscord", () => {
|
||||
{ rest, token: "t" },
|
||||
);
|
||||
const call = getMock.mock.calls[0];
|
||||
const options = call?.[1] as { query?: URLSearchParams };
|
||||
expect(options.query?.toString()).toBe("content=hello&limit=5");
|
||||
expect(call?.[0]).toBe("/guilds/g1/messages/search?content=hello&limit=5");
|
||||
});
|
||||
|
||||
it("supports channel/author arrays and clamps limit", async () => {
|
||||
@@ -394,9 +391,8 @@ describe("searchMessagesDiscord", () => {
|
||||
{ rest, token: "t" },
|
||||
);
|
||||
const call = getMock.mock.calls[0];
|
||||
const options = call?.[1] as { query?: URLSearchParams };
|
||||
expect(options.query?.toString()).toBe(
|
||||
"content=hello&channel_id=c1&channel_id=c2&author_id=u1&limit=25",
|
||||
expect(call?.[0]).toBe(
|
||||
"/guilds/g1/messages/search?content=hello&channel_id=c1&channel_id=c2&author_id=u1&limit=25",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -546,13 +542,13 @@ describe("uploadStickerDiscord", () => {
|
||||
name: "clawdbot_wave",
|
||||
description: "Clawdbot waving",
|
||||
tags: "👋",
|
||||
files: [
|
||||
expect.objectContaining({
|
||||
name: "asset.png",
|
||||
contentType: "image/png",
|
||||
}),
|
||||
],
|
||||
},
|
||||
files: [
|
||||
expect.objectContaining({
|
||||
name: "asset.png",
|
||||
contentType: "image/png",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChannelType, PermissionsBitField, REST, Routes } from "discord.js";
|
||||
import { RequestClient } from "@buape/carbon";
|
||||
import { PollLayoutType } from "discord-api-types/payloads/v10";
|
||||
import type { RESTAPIPoll } from "discord-api-types/rest/v10";
|
||||
import type {
|
||||
@@ -11,6 +11,11 @@ import type {
|
||||
APIVoiceState,
|
||||
RESTPostAPIGuildScheduledEventJSONBody,
|
||||
} from "discord-api-types/v10";
|
||||
import {
|
||||
ChannelType,
|
||||
PermissionFlagsBits,
|
||||
Routes,
|
||||
} from "discord-api-types/v10";
|
||||
|
||||
import { chunkMarkdownText } from "../auto-reply/chunk.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
@@ -47,6 +52,10 @@ export class DiscordSendError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const PERMISSION_ENTRIES = Object.entries(PermissionFlagsBits).filter(
|
||||
([, value]) => typeof value === "bigint",
|
||||
) as Array<[string, bigint]>;
|
||||
|
||||
type DiscordRecipient =
|
||||
| {
|
||||
kind: "user";
|
||||
@@ -61,7 +70,7 @@ type DiscordSendOpts = {
|
||||
token?: string;
|
||||
mediaUrl?: string;
|
||||
verbose?: boolean;
|
||||
rest?: REST;
|
||||
rest?: RequestClient;
|
||||
replyTo?: string;
|
||||
};
|
||||
|
||||
@@ -72,7 +81,7 @@ export type DiscordSendResult = {
|
||||
|
||||
export type DiscordReactOpts = {
|
||||
token?: string;
|
||||
rest?: REST;
|
||||
rest?: RequestClient;
|
||||
};
|
||||
|
||||
export type DiscordReactionUser = {
|
||||
@@ -174,6 +183,10 @@ function resolveToken(explicit?: string) {
|
||||
return token;
|
||||
}
|
||||
|
||||
function resolveRest(token: string, rest?: RequestClient) {
|
||||
return rest ?? new RequestClient(token);
|
||||
}
|
||||
|
||||
function normalizeReactionEmoji(raw: string) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
@@ -252,6 +265,22 @@ function normalizeDiscordPollInput(input: PollInput): RESTAPIPoll {
|
||||
};
|
||||
}
|
||||
|
||||
function addPermissionBits(base: bigint, add?: string) {
|
||||
if (!add) return base;
|
||||
return base | BigInt(add);
|
||||
}
|
||||
|
||||
function removePermissionBits(base: bigint, deny?: string) {
|
||||
if (!deny) return base;
|
||||
return base & ~BigInt(deny);
|
||||
}
|
||||
|
||||
function bitfieldToPermissions(bitfield: bigint) {
|
||||
return PERMISSION_ENTRIES.filter(([, value]) => (bitfield & value) === value)
|
||||
.map(([name]) => name)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function getDiscordErrorCode(err: unknown) {
|
||||
if (!err || typeof err !== "object") return undefined;
|
||||
const candidate =
|
||||
@@ -279,7 +308,7 @@ async function buildDiscordSendError(
|
||||
err: unknown,
|
||||
ctx: {
|
||||
channelId: string;
|
||||
rest: REST;
|
||||
rest: RequestClient;
|
||||
token: string;
|
||||
hasMedia: boolean;
|
||||
},
|
||||
@@ -327,7 +356,7 @@ async function buildDiscordSendError(
|
||||
}
|
||||
|
||||
async function resolveChannelId(
|
||||
rest: REST,
|
||||
rest: RequestClient,
|
||||
recipient: DiscordRecipient,
|
||||
): Promise<{ channelId: string; dm?: boolean }> {
|
||||
if (recipient.kind === "channel") {
|
||||
@@ -343,7 +372,7 @@ async function resolveChannelId(
|
||||
}
|
||||
|
||||
async function sendDiscordText(
|
||||
rest: REST,
|
||||
rest: RequestClient,
|
||||
channelId: string,
|
||||
text: string,
|
||||
replyTo?: string,
|
||||
@@ -379,7 +408,7 @@ async function sendDiscordText(
|
||||
}
|
||||
|
||||
async function sendDiscordMedia(
|
||||
rest: REST,
|
||||
rest: RequestClient,
|
||||
channelId: string,
|
||||
text: string,
|
||||
mediaUrl: string,
|
||||
@@ -395,13 +424,13 @@ async function sendDiscordMedia(
|
||||
body: {
|
||||
content: caption || undefined,
|
||||
message_reference: messageReference,
|
||||
files: [
|
||||
{
|
||||
data: media.buffer,
|
||||
name: media.fileName ?? "upload",
|
||||
},
|
||||
],
|
||||
},
|
||||
files: [
|
||||
{
|
||||
data: media.buffer,
|
||||
name: media.fileName ?? "upload",
|
||||
},
|
||||
],
|
||||
})) as { id: string; channel_id: string };
|
||||
if (text.length > DISCORD_TEXT_LIMIT) {
|
||||
const remaining = text.slice(DISCORD_TEXT_LIMIT).trim();
|
||||
@@ -429,7 +458,7 @@ function formatReactionEmoji(emoji: {
|
||||
return buildReactionIdentifier(emoji);
|
||||
}
|
||||
|
||||
async function fetchBotUserId(rest: REST) {
|
||||
async function fetchBotUserId(rest: RequestClient) {
|
||||
const me = (await rest.get(Routes.user("@me"))) as { id?: string };
|
||||
if (!me?.id) {
|
||||
throw new Error("Failed to resolve bot user id");
|
||||
@@ -443,7 +472,7 @@ export async function sendMessageDiscord(
|
||||
opts: DiscordSendOpts = {},
|
||||
): Promise<DiscordSendResult> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const recipient = parseRecipient(to);
|
||||
const { channelId } = await resolveChannelId(rest, recipient);
|
||||
let result:
|
||||
@@ -482,7 +511,7 @@ export async function sendStickerDiscord(
|
||||
opts: DiscordSendOpts & { content?: string } = {},
|
||||
): Promise<DiscordSendResult> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const recipient = parseRecipient(to);
|
||||
const { channelId } = await resolveChannelId(rest, recipient);
|
||||
const content = opts.content?.trim();
|
||||
@@ -505,7 +534,7 @@ export async function sendPollDiscord(
|
||||
opts: DiscordSendOpts & { content?: string } = {},
|
||||
): Promise<DiscordSendResult> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const recipient = parseRecipient(to);
|
||||
const { channelId } = await resolveChannelId(rest, recipient);
|
||||
const content = opts.content?.trim();
|
||||
@@ -529,7 +558,7 @@ export async function reactMessageDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const encoded = normalizeReactionEmoji(emoji);
|
||||
await rest.put(
|
||||
Routes.channelMessageOwnReaction(channelId, messageId, encoded),
|
||||
@@ -543,7 +572,7 @@ export async function fetchReactionsDiscord(
|
||||
opts: DiscordReactOpts & { limit?: number } = {},
|
||||
): Promise<DiscordReactionSummary[]> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const message = (await rest.get(
|
||||
Routes.channelMessage(channelId, messageId),
|
||||
)) as {
|
||||
@@ -566,7 +595,7 @@ export async function fetchReactionsDiscord(
|
||||
const encoded = encodeURIComponent(identifier);
|
||||
const users = (await rest.get(
|
||||
Routes.channelMessageReaction(channelId, messageId, encoded),
|
||||
{ query: new URLSearchParams({ limit: String(limit) }) },
|
||||
{ limit },
|
||||
)) as Array<{ id: string; username?: string; discriminator?: string }>;
|
||||
summaries.push({
|
||||
emoji: {
|
||||
@@ -593,7 +622,7 @@ export async function fetchChannelPermissionsDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<DiscordPermissionsSummary> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const channel = (await rest.get(Routes.channel(channelId))) as APIChannel;
|
||||
const channelType = "type" in channel ? channel.type : undefined;
|
||||
const guildId = "guild_id" in channel ? channel.guild_id : undefined;
|
||||
@@ -616,47 +645,47 @@ export async function fetchChannelPermissionsDiscord(
|
||||
const rolesById = new Map<string, APIRole>(
|
||||
(guild.roles ?? []).map((role) => [role.id, role]),
|
||||
);
|
||||
const base = new PermissionsBitField();
|
||||
const everyoneRole = rolesById.get(guildId);
|
||||
let base = 0n;
|
||||
if (everyoneRole?.permissions) {
|
||||
base.add(BigInt(everyoneRole.permissions));
|
||||
base = addPermissionBits(base, everyoneRole.permissions);
|
||||
}
|
||||
for (const roleId of member.roles ?? []) {
|
||||
const role = rolesById.get(roleId);
|
||||
if (role?.permissions) {
|
||||
base.add(BigInt(role.permissions));
|
||||
base = addPermissionBits(base, role.permissions);
|
||||
}
|
||||
}
|
||||
|
||||
const permissions = new PermissionsBitField(base);
|
||||
let permissions = base;
|
||||
const overwrites =
|
||||
"permission_overwrites" in channel
|
||||
? (channel.permission_overwrites ?? [])
|
||||
: [];
|
||||
for (const overwrite of overwrites) {
|
||||
if (overwrite.id === guildId) {
|
||||
permissions.remove(BigInt(overwrite.deny ?? "0"));
|
||||
permissions.add(BigInt(overwrite.allow ?? "0"));
|
||||
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
|
||||
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
|
||||
}
|
||||
}
|
||||
for (const overwrite of overwrites) {
|
||||
if (member.roles?.includes(overwrite.id)) {
|
||||
permissions.remove(BigInt(overwrite.deny ?? "0"));
|
||||
permissions.add(BigInt(overwrite.allow ?? "0"));
|
||||
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
|
||||
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
|
||||
}
|
||||
}
|
||||
for (const overwrite of overwrites) {
|
||||
if (overwrite.id === botId) {
|
||||
permissions.remove(BigInt(overwrite.deny ?? "0"));
|
||||
permissions.add(BigInt(overwrite.allow ?? "0"));
|
||||
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
|
||||
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
channelId,
|
||||
guildId,
|
||||
permissions: permissions.toArray(),
|
||||
raw: permissions.bitfield.toString(),
|
||||
permissions: bitfieldToPermissions(permissions),
|
||||
raw: permissions.toString(),
|
||||
isDm: false,
|
||||
channelType,
|
||||
};
|
||||
@@ -668,19 +697,20 @@ export async function readMessagesDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIMessage[]> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const limit =
|
||||
typeof query.limit === "number" && Number.isFinite(query.limit)
|
||||
? Math.min(Math.max(Math.floor(query.limit), 1), 100)
|
||||
: undefined;
|
||||
const params = new URLSearchParams();
|
||||
if (limit) params.set("limit", String(limit));
|
||||
if (query.before) params.set("before", query.before);
|
||||
if (query.after) params.set("after", query.after);
|
||||
if (query.around) params.set("around", query.around);
|
||||
return (await rest.get(Routes.channelMessages(channelId), {
|
||||
query: params,
|
||||
})) as APIMessage[];
|
||||
const params: Record<string, string | number> = {};
|
||||
if (limit) params.limit = limit;
|
||||
if (query.before) params.before = query.before;
|
||||
if (query.after) params.after = query.after;
|
||||
if (query.around) params.around = query.around;
|
||||
return (await rest.get(
|
||||
Routes.channelMessages(channelId),
|
||||
params,
|
||||
)) as APIMessage[];
|
||||
}
|
||||
|
||||
export async function editMessageDiscord(
|
||||
@@ -690,7 +720,7 @@ export async function editMessageDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIMessage> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.patch(Routes.channelMessage(channelId, messageId), {
|
||||
body: { content: payload.content },
|
||||
})) as APIMessage;
|
||||
@@ -702,7 +732,7 @@ export async function deleteMessageDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
await rest.delete(Routes.channelMessage(channelId, messageId));
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -713,7 +743,7 @@ export async function pinMessageDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
await rest.put(Routes.channelPin(channelId, messageId));
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -724,7 +754,7 @@ export async function unpinMessageDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
await rest.delete(Routes.channelPin(channelId, messageId));
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -734,7 +764,7 @@ export async function listPinsDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIMessage[]> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.get(Routes.channelPins(channelId))) as APIMessage[];
|
||||
}
|
||||
|
||||
@@ -744,7 +774,7 @@ export async function createThreadDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const body: Record<string, unknown> = { name: payload.name };
|
||||
if (payload.autoArchiveMinutes) {
|
||||
body.auto_archive_duration = payload.autoArchiveMinutes;
|
||||
@@ -758,17 +788,18 @@ export async function listThreadsDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
if (payload.includeArchived) {
|
||||
if (!payload.channelId) {
|
||||
throw new Error("channelId required to list archived threads");
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
if (payload.before) params.set("before", payload.before);
|
||||
if (payload.limit) params.set("limit", String(payload.limit));
|
||||
return await rest.get(Routes.channelThreads(payload.channelId, "public"), {
|
||||
query: params,
|
||||
});
|
||||
const params: Record<string, string | number> = {};
|
||||
if (payload.before) params.before = payload.before;
|
||||
if (payload.limit) params.limit = payload.limit;
|
||||
return await rest.get(
|
||||
Routes.channelThreads(payload.channelId, "public"),
|
||||
params,
|
||||
);
|
||||
}
|
||||
return await rest.get(Routes.guildActiveThreads(payload.guildId));
|
||||
}
|
||||
@@ -778,7 +809,7 @@ export async function searchMessagesDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const params = new URLSearchParams();
|
||||
params.set("content", query.content);
|
||||
if (query.channelIds?.length) {
|
||||
@@ -795,9 +826,9 @@ export async function searchMessagesDiscord(
|
||||
const limit = Math.min(Math.max(Math.floor(query.limit), 1), 25);
|
||||
params.set("limit", String(limit));
|
||||
}
|
||||
return await rest.get(`/guilds/${query.guildId}/messages/search`, {
|
||||
query: params,
|
||||
});
|
||||
return await rest.get(
|
||||
`/guilds/${query.guildId}/messages/search?${params.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function listGuildEmojisDiscord(
|
||||
@@ -805,7 +836,7 @@ export async function listGuildEmojisDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return await rest.get(Routes.guildEmojis(guildId));
|
||||
}
|
||||
|
||||
@@ -814,7 +845,7 @@ export async function uploadEmojiDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const media = await loadWebMediaRaw(
|
||||
payload.mediaUrl,
|
||||
DISCORD_MAX_EMOJI_BYTES,
|
||||
@@ -844,7 +875,7 @@ export async function uploadStickerDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const media = await loadWebMediaRaw(
|
||||
payload.mediaUrl,
|
||||
DISCORD_MAX_STICKER_BYTES,
|
||||
@@ -866,14 +897,14 @@ export async function uploadStickerDiscord(
|
||||
"Sticker description",
|
||||
),
|
||||
tags: normalizeEmojiName(payload.tags, "Sticker tags"),
|
||||
files: [
|
||||
{
|
||||
data: media.buffer,
|
||||
name: media.fileName ?? "sticker",
|
||||
contentType,
|
||||
},
|
||||
],
|
||||
},
|
||||
files: [
|
||||
{
|
||||
data: media.buffer,
|
||||
name: media.fileName ?? "sticker",
|
||||
contentType,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -883,7 +914,7 @@ export async function fetchMemberInfoDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIGuildMember> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.get(
|
||||
Routes.guildMember(guildId, userId),
|
||||
)) as APIGuildMember;
|
||||
@@ -894,7 +925,7 @@ export async function fetchRoleInfoDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIRole[]> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.get(Routes.guildRoles(guildId))) as APIRole[];
|
||||
}
|
||||
|
||||
@@ -903,7 +934,7 @@ export async function addRoleDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
await rest.put(
|
||||
Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId),
|
||||
);
|
||||
@@ -915,7 +946,7 @@ export async function removeRoleDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
await rest.delete(
|
||||
Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId),
|
||||
);
|
||||
@@ -927,7 +958,7 @@ export async function fetchChannelInfoDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIChannel> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.get(Routes.channel(channelId))) as APIChannel;
|
||||
}
|
||||
|
||||
@@ -936,7 +967,7 @@ export async function listGuildChannelsDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIChannel[]> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.get(Routes.guildChannels(guildId))) as APIChannel[];
|
||||
}
|
||||
|
||||
@@ -946,7 +977,7 @@ export async function fetchVoiceStatusDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIVoiceState> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.get(
|
||||
Routes.guildVoiceState(guildId, userId),
|
||||
)) as APIVoiceState;
|
||||
@@ -957,7 +988,7 @@ export async function listScheduledEventsDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIGuildScheduledEvent[]> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.get(
|
||||
Routes.guildScheduledEvents(guildId),
|
||||
)) as APIGuildScheduledEvent[];
|
||||
@@ -969,7 +1000,7 @@ export async function createScheduledEventDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIGuildScheduledEvent> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
return (await rest.post(Routes.guildScheduledEvents(guildId), {
|
||||
body: payload,
|
||||
})) as APIGuildScheduledEvent;
|
||||
@@ -980,7 +1011,7 @@ export async function timeoutMemberDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIGuildMember> {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
let until = payload.until;
|
||||
if (!until && payload.durationMinutes) {
|
||||
const ms = payload.durationMinutes * 60 * 1000;
|
||||
@@ -990,7 +1021,9 @@ export async function timeoutMemberDiscord(
|
||||
Routes.guildMember(payload.guildId, payload.userId),
|
||||
{
|
||||
body: { communication_disabled_until: until ?? null },
|
||||
reason: payload.reason,
|
||||
headers: payload.reason
|
||||
? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
|
||||
: undefined,
|
||||
},
|
||||
)) as APIGuildMember;
|
||||
}
|
||||
@@ -1000,9 +1033,11 @@ export async function kickMemberDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
await rest.delete(Routes.guildMember(payload.guildId, payload.userId), {
|
||||
reason: payload.reason,
|
||||
headers: payload.reason
|
||||
? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
|
||||
: undefined,
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -1012,7 +1047,7 @@ export async function banMemberDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const token = resolveToken(opts.token);
|
||||
const rest = opts.rest ?? new REST({ version: "10" }).setToken(token);
|
||||
const rest = resolveRest(token, opts.rest);
|
||||
const deleteMessageDays =
|
||||
typeof payload.deleteMessageDays === "number" &&
|
||||
Number.isFinite(payload.deleteMessageDays)
|
||||
@@ -1023,7 +1058,9 @@ export async function banMemberDiscord(
|
||||
deleteMessageDays !== undefined
|
||||
? { delete_message_days: deleteMessageDays }
|
||||
: undefined,
|
||||
reason: payload.reason,
|
||||
headers: payload.reason
|
||||
? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
|
||||
: undefined,
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user