chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type { DiscordAccountConfig } from "../config/types.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
import { resolveDiscordToken } from "./token.js";
|
||||
|
||||
export type ResolvedDiscordAccount = {
|
||||
@@ -42,12 +39,10 @@ function resolveAccountConfig(
|
||||
return accounts[accountId] as DiscordAccountConfig | undefined;
|
||||
}
|
||||
|
||||
function mergeDiscordAccountConfig(
|
||||
cfg: ClawdbotConfig,
|
||||
accountId: string,
|
||||
): DiscordAccountConfig {
|
||||
const { accounts: _ignored, ...base } = (cfg.channels?.discord ??
|
||||
{}) as DiscordAccountConfig & { accounts?: unknown };
|
||||
function mergeDiscordAccountConfig(cfg: ClawdbotConfig, accountId: string): DiscordAccountConfig {
|
||||
const { accounts: _ignored, ...base } = (cfg.channels?.discord ?? {}) as DiscordAccountConfig & {
|
||||
accounts?: unknown;
|
||||
};
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
return { ...base, ...account };
|
||||
}
|
||||
@@ -72,9 +67,7 @@ export function resolveDiscordAccount(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function listEnabledDiscordAccounts(
|
||||
cfg: ClawdbotConfig,
|
||||
): ResolvedDiscordAccount[] {
|
||||
export function listEnabledDiscordAccounts(cfg: ClawdbotConfig): ResolvedDiscordAccount[] {
|
||||
return listDiscordAccountIds(cfg)
|
||||
.map((accountId) => resolveDiscordAccount({ cfg, accountId }))
|
||||
.filter((account) => account.enabled);
|
||||
|
||||
@@ -36,9 +36,7 @@ describe("discord audit", () => {
|
||||
expect(collected.channelIds).toEqual(["111"]);
|
||||
expect(collected.unresolvedChannels).toBe(1);
|
||||
|
||||
(
|
||||
fetchChannelPermissionsDiscord as unknown as ReturnType<typeof vi.fn>
|
||||
).mockResolvedValueOnce({
|
||||
(fetchChannelPermissionsDiscord as unknown as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
|
||||
channelId: "111",
|
||||
permissions: ["ViewChannel"],
|
||||
raw: "0",
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import type {
|
||||
DiscordGuildChannelConfig,
|
||||
DiscordGuildEntry,
|
||||
} from "../config/types.js";
|
||||
import type { DiscordGuildChannelConfig, DiscordGuildEntry } from "../config/types.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { fetchChannelPermissionsDiscord } from "./send.js";
|
||||
|
||||
@@ -27,9 +24,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function shouldAuditChannelConfig(
|
||||
config: DiscordGuildChannelConfig | undefined,
|
||||
) {
|
||||
function shouldAuditChannelConfig(config: DiscordGuildChannelConfig | undefined) {
|
||||
if (!config) return true;
|
||||
if (config.allow === false) return false;
|
||||
if (config.enabled === false) return false;
|
||||
@@ -48,12 +43,7 @@ function listConfiguredGuildChannelKeys(
|
||||
for (const [key, value] of Object.entries(channelsRaw)) {
|
||||
const channelId = String(key).trim();
|
||||
if (!channelId) continue;
|
||||
if (
|
||||
!shouldAuditChannelConfig(
|
||||
value as DiscordGuildChannelConfig | undefined,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
if (!shouldAuditChannelConfig(value as DiscordGuildChannelConfig | undefined)) continue;
|
||||
ids.add(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,9 +25,7 @@ function hasBalancedFences(chunk: string) {
|
||||
|
||||
describe("chunkDiscordText", () => {
|
||||
it("splits tall messages even when under 2000 chars", () => {
|
||||
const text = Array.from({ length: 45 }, (_, i) => `line-${i + 1}`).join(
|
||||
"\n",
|
||||
);
|
||||
const text = Array.from({ length: 45 }, (_, i) => `line-${i + 1}`).join("\n");
|
||||
expect(text.length).toBeLessThan(2000);
|
||||
|
||||
const chunks = chunkDiscordText(text, { maxChars: 2000, maxLines: 20 });
|
||||
@@ -38,10 +36,7 @@ describe("chunkDiscordText", () => {
|
||||
});
|
||||
|
||||
it("keeps fenced code blocks balanced across chunks", () => {
|
||||
const body = Array.from(
|
||||
{ length: 30 },
|
||||
(_, i) => `console.log(${i});`,
|
||||
).join("\n");
|
||||
const body = Array.from({ length: 30 }, (_, i) => `console.log(${i});`).join("\n");
|
||||
const text = `Here is code:\n\n\`\`\`js\n${body}\n\`\`\`\n\nDone.`;
|
||||
|
||||
const chunks = chunkDiscordText(text, { maxChars: 2000, maxLines: 10 });
|
||||
@@ -69,9 +64,7 @@ describe("chunkDiscordText", () => {
|
||||
});
|
||||
|
||||
it("keeps reasoning italics balanced across chunks", () => {
|
||||
const body = Array.from({ length: 25 }, (_, i) => `${i + 1}. line`).join(
|
||||
"\n",
|
||||
);
|
||||
const body = Array.from({ length: 25 }, (_, i) => `${i + 1}. line`).join("\n");
|
||||
const text = `Reasoning:\n_${body}_`;
|
||||
|
||||
const chunks = chunkDiscordText(text, { maxLines: 10, maxChars: 2000 });
|
||||
@@ -90,8 +83,7 @@ describe("chunkDiscordText", () => {
|
||||
});
|
||||
|
||||
it("keeps reasoning italics balanced when chunks split by char limit", () => {
|
||||
const longLine =
|
||||
"This is a very long reasoning line that forces char splits.";
|
||||
const longLine = "This is a very long reasoning line that forces char splits.";
|
||||
const body = Array.from({ length: 5 }, () => longLine).join("\n");
|
||||
const text = `Reasoning:\n_${body}_`;
|
||||
|
||||
|
||||
@@ -40,9 +40,7 @@ function parseFenceLine(line: string): OpenFence | null {
|
||||
}
|
||||
|
||||
function closeFenceLine(openFence: OpenFence) {
|
||||
return `${openFence.indent}${openFence.markerChar.repeat(
|
||||
openFence.markerLen,
|
||||
)}`;
|
||||
return `${openFence.indent}${openFence.markerChar.repeat(openFence.markerLen)}`;
|
||||
}
|
||||
|
||||
function closeFenceIfNeeded(text: string, openFence: OpenFence | null) {
|
||||
@@ -78,8 +76,7 @@ function splitLongLine(
|
||||
}
|
||||
if (breakIdx <= 0) breakIdx = limit;
|
||||
out.push(remaining.slice(0, breakIdx));
|
||||
const brokeOnSeparator =
|
||||
breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
|
||||
const brokeOnSeparator = breakIdx < remaining.length && /\s/.test(remaining[breakIdx]);
|
||||
remaining = remaining.slice(breakIdx + (brokeOnSeparator ? 1 : 0));
|
||||
}
|
||||
if (remaining.length) out.push(remaining);
|
||||
@@ -90,10 +87,7 @@ function splitLongLine(
|
||||
* Chunks outbound Discord text by both character count and (soft) line count,
|
||||
* while keeping fenced code blocks balanced across chunks.
|
||||
*/
|
||||
export function chunkDiscordText(
|
||||
text: string,
|
||||
opts: ChunkDiscordTextOpts = {},
|
||||
): string[] {
|
||||
export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}): string[] {
|
||||
const maxChars = Math.max(1, Math.floor(opts.maxChars ?? DEFAULT_MAX_CHARS));
|
||||
const maxLines = Math.max(1, Math.floor(opts.maxLines ?? DEFAULT_MAX_LINES));
|
||||
|
||||
@@ -137,9 +131,7 @@ export function chunkDiscordText(
|
||||
}
|
||||
}
|
||||
|
||||
const reserveChars = nextOpenFence
|
||||
? closeFenceLine(nextOpenFence).length + 1
|
||||
: 0;
|
||||
const reserveChars = nextOpenFence ? closeFenceLine(nextOpenFence).length + 1 : 0;
|
||||
const reserveLines = nextOpenFence ? 1 : 0;
|
||||
const effectiveMaxChars = maxChars - reserveChars;
|
||||
const effectiveMaxLines = maxLines - reserveLines;
|
||||
@@ -154,11 +146,7 @@ export function chunkDiscordText(
|
||||
for (let segIndex = 0; segIndex < segments.length; segIndex++) {
|
||||
const segment = segments[segIndex];
|
||||
const isLineContinuation = segIndex > 0;
|
||||
const delimiter = isLineContinuation
|
||||
? ""
|
||||
: current.length > 0
|
||||
? "\n"
|
||||
: "";
|
||||
const delimiter = isLineContinuation ? "" : current.length > 0 ? "\n" : "";
|
||||
const addition = `${delimiter}${segment}`;
|
||||
const nextLen = current.length + addition.length;
|
||||
const nextLines = currentLines + (isLineContinuation ? 0 : 1);
|
||||
|
||||
@@ -17,11 +17,7 @@ const shouldPromoteGatewayDebug = (message: string) =>
|
||||
const formatGatewayMetrics = (metrics: unknown) => {
|
||||
if (metrics === null || metrics === undefined) return String(metrics);
|
||||
if (typeof metrics === "string") return metrics;
|
||||
if (
|
||||
typeof metrics === "number" ||
|
||||
typeof metrics === "boolean" ||
|
||||
typeof metrics === "bigint"
|
||||
) {
|
||||
if (typeof metrics === "number" || typeof metrics === "boolean" || typeof metrics === "bigint") {
|
||||
return String(metrics);
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -5,9 +5,7 @@ export type DiscordGatewayHandle = {
|
||||
disconnect?: () => void;
|
||||
};
|
||||
|
||||
export function getDiscordGatewayEmitter(
|
||||
gateway?: unknown,
|
||||
): EventEmitter | undefined {
|
||||
export function getDiscordGatewayEmitter(gateway?: unknown): EventEmitter | undefined {
|
||||
return (gateway as { emitter?: EventEmitter } | undefined)?.emitter;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,56 +29,52 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
describe("discord native commands", () => {
|
||||
it(
|
||||
"streams tool results for native slash commands",
|
||||
{ timeout: 30_000 },
|
||||
async () => {
|
||||
const { ChannelType } = await import("@buape/carbon");
|
||||
const { createDiscordNativeCommand } = await import("./monitor.js");
|
||||
it("streams tool results for native slash commands", { timeout: 30_000 }, async () => {
|
||||
const { ChannelType } = await import("@buape/carbon");
|
||||
const { createDiscordNativeCommand } = await import("./monitor.js");
|
||||
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
humanDelay: { mode: "off" },
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "anthropic/claude-opus-4-5",
|
||||
humanDelay: { mode: "off" },
|
||||
workspace: "/tmp/clawd",
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
discord: { dm: { enabled: true, policy: "open" } },
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
},
|
||||
session: { store: "/tmp/clawdbot-sessions.json" },
|
||||
discord: { dm: { enabled: true, policy: "open" } },
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const command = createDiscordNativeCommand({
|
||||
command: {
|
||||
name: "verbose",
|
||||
description: "Toggle verbose mode.",
|
||||
acceptsArgs: true,
|
||||
},
|
||||
cfg,
|
||||
discordConfig: cfg.discord,
|
||||
accountId: "default",
|
||||
sessionPrefix: "discord:slash",
|
||||
ephemeralDefault: true,
|
||||
});
|
||||
const command = createDiscordNativeCommand({
|
||||
command: {
|
||||
name: "verbose",
|
||||
description: "Toggle verbose mode.",
|
||||
acceptsArgs: true,
|
||||
},
|
||||
cfg,
|
||||
discordConfig: cfg.discord,
|
||||
accountId: "default",
|
||||
sessionPrefix: "discord:slash",
|
||||
ephemeralDefault: true,
|
||||
});
|
||||
|
||||
const reply = vi.fn().mockResolvedValue(undefined);
|
||||
const followUp = vi.fn().mockResolvedValue(undefined);
|
||||
const reply = vi.fn().mockResolvedValue(undefined);
|
||||
const followUp = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
await command.run({
|
||||
user: { id: "u1", username: "Ada", globalName: "Ada" },
|
||||
channel: { type: ChannelType.DM },
|
||||
guild: null,
|
||||
rawData: { id: "i1" },
|
||||
options: { getString: vi.fn().mockReturnValue("on") },
|
||||
reply,
|
||||
followUp,
|
||||
});
|
||||
await command.run({
|
||||
user: { id: "u1", username: "Ada", globalName: "Ada" },
|
||||
channel: { type: ChannelType.DM },
|
||||
guild: null,
|
||||
rawData: { id: "i1" },
|
||||
options: { getString: vi.fn().mockReturnValue("on") },
|
||||
reply,
|
||||
followUp,
|
||||
});
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(followUp).toHaveBeenCalledTimes(1);
|
||||
expect(reply.mock.calls[0]?.[0]?.content).toContain("tool");
|
||||
expect(followUp.mock.calls[0]?.[0]?.content).toContain("final");
|
||||
},
|
||||
);
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(1);
|
||||
expect(reply).toHaveBeenCalledTimes(1);
|
||||
expect(followUp).toHaveBeenCalledTimes(1);
|
||||
expect(reply.mock.calls[0]?.[0]?.content).toContain("tool");
|
||||
expect(followUp.mock.calls[0]?.[0]?.content).toContain("final");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -342,10 +342,7 @@ describe("discord reply target selection", () => {
|
||||
|
||||
describe("discord autoThread name sanitization", () => {
|
||||
it("strips mentions and collapses whitespace", () => {
|
||||
const name = sanitizeDiscordThreadName(
|
||||
" <@123> <@&456> <#789> Help here ",
|
||||
"msg-1",
|
||||
);
|
||||
const name = sanitizeDiscordThreadName(" <@123> <@&456> <#789> Help here ", "msg-1");
|
||||
expect(name).toBe("Help here");
|
||||
});
|
||||
|
||||
@@ -449,15 +446,7 @@ describe("discord media payload", () => {
|
||||
expect(payload.MediaPath).toBe("/tmp/a.png");
|
||||
expect(payload.MediaUrl).toBe("/tmp/a.png");
|
||||
expect(payload.MediaType).toBe("image/png");
|
||||
expect(payload.MediaPaths).toEqual([
|
||||
"/tmp/a.png",
|
||||
"/tmp/b.png",
|
||||
"/tmp/c.png",
|
||||
]);
|
||||
expect(payload.MediaUrls).toEqual([
|
||||
"/tmp/a.png",
|
||||
"/tmp/b.png",
|
||||
"/tmp/c.png",
|
||||
]);
|
||||
expect(payload.MediaPaths).toEqual(["/tmp/a.png", "/tmp/b.png", "/tmp/c.png"]);
|
||||
expect(payload.MediaUrls).toEqual(["/tmp/a.png", "/tmp/b.png", "/tmp/c.png"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,10 +20,8 @@ vi.mock("../auto-reply/reply/dispatch-from-config.js", () => ({
|
||||
dispatchReplyFromConfig: (...args: unknown[]) => dispatchMock(...args),
|
||||
}));
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: (...args: unknown[]) =>
|
||||
readAllowFromStoreMock(...args),
|
||||
upsertChannelPairingRequest: (...args: unknown[]) =>
|
||||
upsertPairingRequestMock(...args),
|
||||
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
|
||||
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
||||
}));
|
||||
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||
@@ -43,9 +41,7 @@ beforeEach(() => {
|
||||
return { queuedFinal: true, counts: { final: 1 } };
|
||||
});
|
||||
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
||||
upsertPairingRequestMock
|
||||
.mockReset()
|
||||
.mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
@@ -343,9 +339,7 @@ describe("discord tool result dispatch", () => {
|
||||
);
|
||||
|
||||
expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:t1");
|
||||
expect(capturedCtx?.ParentSessionKey).toBe(
|
||||
"agent:main:discord:channel:forum-1",
|
||||
);
|
||||
expect(capturedCtx?.ParentSessionKey).toBe("agent:main:discord:channel:forum-1");
|
||||
expect(capturedCtx?.ThreadStarterBody).toContain("starter message");
|
||||
expect(capturedCtx?.ThreadLabel).toContain("Discord thread #support");
|
||||
expect(restGet).toHaveBeenCalledWith(Routes.channelMessage("t1", "t1"));
|
||||
@@ -382,9 +376,7 @@ describe("discord tool result dispatch", () => {
|
||||
guilds: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
bindings: [
|
||||
{ agentId: "support", match: { channel: "discord", guildId: "g1" } },
|
||||
],
|
||||
bindings: [{ agentId: "support", match: { channel: "discord", guildId: "g1" } }],
|
||||
} as ReturnType<typeof import("../config/config.js").loadConfig>;
|
||||
|
||||
const handler = createDiscordMessageHandler({
|
||||
@@ -457,8 +449,6 @@ describe("discord tool result dispatch", () => {
|
||||
);
|
||||
|
||||
expect(capturedCtx?.SessionKey).toBe("agent:support:discord:channel:t1");
|
||||
expect(capturedCtx?.ParentSessionKey).toBe(
|
||||
"agent:support:discord:channel:p1",
|
||||
);
|
||||
expect(capturedCtx?.ParentSessionKey).toBe("agent:support:discord:channel:p1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,10 +19,8 @@ vi.mock("../auto-reply/reply/dispatch-from-config.js", () => ({
|
||||
dispatchReplyFromConfig: (...args: unknown[]) => dispatchMock(...args),
|
||||
}));
|
||||
vi.mock("../pairing/pairing-store.js", () => ({
|
||||
readChannelAllowFromStore: (...args: unknown[]) =>
|
||||
readAllowFromStoreMock(...args),
|
||||
upsertChannelPairingRequest: (...args: unknown[]) =>
|
||||
upsertPairingRequestMock(...args),
|
||||
readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args),
|
||||
upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args),
|
||||
}));
|
||||
vi.mock("../config/sessions.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../config/sessions.js")>();
|
||||
@@ -42,9 +40,7 @@ beforeEach(() => {
|
||||
return { queuedFinal: true, counts: { final: 1 } };
|
||||
});
|
||||
readAllowFromStoreMock.mockReset().mockResolvedValue([]);
|
||||
upsertPairingRequestMock
|
||||
.mockReset()
|
||||
.mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
@@ -441,11 +437,7 @@ describe("discord tool result dispatch", () => {
|
||||
expect(dispatchMock).not.toHaveBeenCalled();
|
||||
expect(upsertPairingRequestMock).toHaveBeenCalled();
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(String(sendMock.mock.calls[0]?.[1] ?? "")).toContain(
|
||||
"Your Discord user id: u2",
|
||||
);
|
||||
expect(String(sendMock.mock.calls[0]?.[1] ?? "")).toContain(
|
||||
"Pairing code: PAIRCODE",
|
||||
);
|
||||
expect(String(sendMock.mock.calls[0]?.[1] ?? "")).toContain("Your Discord user id: u2");
|
||||
expect(String(sendMock.mock.calls[0]?.[1] ?? "")).toContain("Pairing code: PAIRCODE");
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
@@ -15,10 +15,7 @@ export {
|
||||
resolveGroupDmAllow,
|
||||
shouldEmitDiscordReactionNotification,
|
||||
} from "./monitor/allow-list.js";
|
||||
export type {
|
||||
DiscordMessageEvent,
|
||||
DiscordMessageHandler,
|
||||
} from "./monitor/listeners.js";
|
||||
export type { DiscordMessageEvent, DiscordMessageHandler } from "./monitor/listeners.js";
|
||||
export { registerDiscordListener } from "./monitor/listeners.js";
|
||||
|
||||
export { createDiscordMessageHandler } from "./monitor/message-handler.js";
|
||||
@@ -27,7 +24,4 @@ export { createDiscordNativeCommand } from "./monitor/native-command.js";
|
||||
export type { MonitorDiscordOpts } from "./monitor/provider.js";
|
||||
export { monitorDiscordProvider } from "./monitor/provider.js";
|
||||
|
||||
export {
|
||||
resolveDiscordReplyTarget,
|
||||
sanitizeDiscordThreadName,
|
||||
} from "./monitor/threading.js";
|
||||
export { resolveDiscordReplyTarget, sanitizeDiscordThreadName } from "./monitor/threading.js";
|
||||
|
||||
@@ -85,8 +85,7 @@ export function allowListMatches(
|
||||
if (candidate.id && list.ids.has(candidate.id)) return true;
|
||||
const slug = candidate.name ? normalizeDiscordSlug(candidate.name) : "";
|
||||
if (slug && list.names.has(slug)) return true;
|
||||
if (candidate.tag && list.names.has(normalizeDiscordSlug(candidate.tag)))
|
||||
return true;
|
||||
if (candidate.tag && list.names.has(normalizeDiscordSlug(candidate.tag))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -96,10 +95,7 @@ export function resolveDiscordUserAllowed(params: {
|
||||
userName?: string;
|
||||
userTag?: string;
|
||||
}) {
|
||||
const allowList = normalizeDiscordAllowList(params.allowList, [
|
||||
"discord:",
|
||||
"user:",
|
||||
]);
|
||||
const allowList = normalizeDiscordAllowList(params.allowList, ["discord:", "user:"]);
|
||||
if (!allowList) return true;
|
||||
return allowListMatches(allowList, {
|
||||
id: params.userId,
|
||||
@@ -115,10 +111,7 @@ export function resolveDiscordCommandAuthorized(params: {
|
||||
author: User;
|
||||
}) {
|
||||
if (!params.isDirectMessage) return true;
|
||||
const allowList = normalizeDiscordAllowList(params.allowFrom, [
|
||||
"discord:",
|
||||
"user:",
|
||||
]);
|
||||
const allowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:"]);
|
||||
if (!allowList) return true;
|
||||
return allowListMatches(allowList, {
|
||||
id: params.author.id,
|
||||
@@ -140,8 +133,7 @@ export function resolveDiscordGuildEntry(params: {
|
||||
const bySlug = entries[slug];
|
||||
if (bySlug) return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
|
||||
const wildcard = entries["*"];
|
||||
if (wildcard)
|
||||
return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
|
||||
if (wildcard) return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -200,11 +192,7 @@ export function resolveDiscordShouldRequireMention(params: {
|
||||
}): boolean {
|
||||
if (!params.isGuildMessage) return false;
|
||||
if (params.isThread && params.channelConfig?.autoThread) return false;
|
||||
return (
|
||||
params.channelConfig?.requireMention ??
|
||||
params.guildInfo?.requireMention ??
|
||||
true
|
||||
);
|
||||
return params.channelConfig?.requireMention ?? params.guildInfo?.requireMention ?? true;
|
||||
}
|
||||
|
||||
export function isDiscordGroupAllowedByPolicy(params: {
|
||||
@@ -227,18 +215,13 @@ export function resolveGroupDmAllow(params: {
|
||||
}) {
|
||||
const { channels, channelId, channelName, channelSlug } = params;
|
||||
if (!channels || channels.length === 0) return true;
|
||||
const allowList = channels.map((entry) =>
|
||||
normalizeDiscordSlug(String(entry)),
|
||||
);
|
||||
const allowList = channels.map((entry) => normalizeDiscordSlug(String(entry)));
|
||||
const candidates = [
|
||||
normalizeDiscordSlug(channelId),
|
||||
channelSlug,
|
||||
channelName ? normalizeDiscordSlug(channelName) : "",
|
||||
].filter(Boolean);
|
||||
return (
|
||||
allowList.includes("*") ||
|
||||
candidates.some((candidate) => allowList.includes(candidate))
|
||||
);
|
||||
return allowList.includes("*") || candidates.some((candidate) => allowList.includes(candidate));
|
||||
}
|
||||
|
||||
export function shouldEmitDiscordReactionNotification(params: {
|
||||
@@ -257,10 +240,7 @@ export function shouldEmitDiscordReactionNotification(params: {
|
||||
return Boolean(params.botId && params.messageAuthorId === params.botId);
|
||||
}
|
||||
if (mode === "allowlist") {
|
||||
const list = normalizeDiscordAllowList(params.allowlist, [
|
||||
"discord:",
|
||||
"user:",
|
||||
]);
|
||||
const list = normalizeDiscordAllowList(params.allowlist, ["discord:", "user:"]);
|
||||
if (!list) return false;
|
||||
return allowListMatches(list, {
|
||||
id: params.userId,
|
||||
|
||||
@@ -12,10 +12,7 @@ export function resolveDiscordSystemLocation(params: {
|
||||
return guild?.name ? `${guild.name} #${channelName}` : `#${channelName}`;
|
||||
}
|
||||
|
||||
export function formatDiscordReactionEmoji(emoji: {
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
}) {
|
||||
export function formatDiscordReactionEmoji(emoji: { id?: string | null; name?: string | null }) {
|
||||
if (emoji.id && emoji.name) {
|
||||
return `${emoji.name}:${emoji.id}`;
|
||||
}
|
||||
|
||||
@@ -17,20 +17,13 @@ import {
|
||||
} from "./allow-list.js";
|
||||
import { formatDiscordReactionEmoji, formatDiscordUserTag } from "./format.js";
|
||||
|
||||
type LoadedConfig = ReturnType<
|
||||
typeof import("../../config/config.js").loadConfig
|
||||
>;
|
||||
type LoadedConfig = ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
type RuntimeEnv = import("../../runtime.js").RuntimeEnv;
|
||||
type Logger = ReturnType<typeof import("../../logging.js").getChildLogger>;
|
||||
|
||||
export type DiscordMessageEvent = Parameters<
|
||||
MessageCreateListener["handle"]
|
||||
>[0];
|
||||
export type DiscordMessageEvent = Parameters<MessageCreateListener["handle"]>[0];
|
||||
|
||||
export type DiscordMessageHandler = (
|
||||
data: DiscordMessageEvent,
|
||||
client: Client,
|
||||
) => Promise<void>;
|
||||
export type DiscordMessageHandler = (data: DiscordMessageEvent, client: Client) => Promise<void>;
|
||||
|
||||
type DiscordReactionEvent = Parameters<MessageReactionAddListener["handle"]>[0];
|
||||
|
||||
@@ -55,13 +48,8 @@ function logSlowDiscordListener(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export function registerDiscordListener(
|
||||
listeners: Array<object>,
|
||||
listener: object,
|
||||
) {
|
||||
if (
|
||||
listeners.some((existing) => existing.constructor === listener.constructor)
|
||||
) {
|
||||
export function registerDiscordListener(listeners: Array<object>, listener: object) {
|
||||
if (listeners.some((existing) => existing.constructor === listener.constructor)) {
|
||||
return false;
|
||||
}
|
||||
listeners.push(listener);
|
||||
@@ -98,10 +86,7 @@ export class DiscordReactionListener extends MessageReactionAddListener {
|
||||
accountId: string;
|
||||
runtime: RuntimeEnv;
|
||||
botUserId?: string;
|
||||
guildEntries?: Record<
|
||||
string,
|
||||
import("./allow-list.js").DiscordGuildEntryResolved
|
||||
>;
|
||||
guildEntries?: Record<string, import("./allow-list.js").DiscordGuildEntryResolved>;
|
||||
logger: Logger;
|
||||
},
|
||||
) {
|
||||
@@ -139,10 +124,7 @@ export class DiscordReactionRemoveListener extends MessageReactionRemoveListener
|
||||
accountId: string;
|
||||
runtime: RuntimeEnv;
|
||||
botUserId?: string;
|
||||
guildEntries?: Record<
|
||||
string,
|
||||
import("./allow-list.js").DiscordGuildEntryResolved
|
||||
>;
|
||||
guildEntries?: Record<string, import("./allow-list.js").DiscordGuildEntryResolved>;
|
||||
logger: Logger;
|
||||
},
|
||||
) {
|
||||
@@ -180,10 +162,7 @@ async function handleDiscordReactionEvent(params: {
|
||||
cfg: LoadedConfig;
|
||||
accountId: string;
|
||||
botUserId?: string;
|
||||
guildEntries?: Record<
|
||||
string,
|
||||
import("./allow-list.js").DiscordGuildEntryResolved
|
||||
>;
|
||||
guildEntries?: Record<string, import("./allow-list.js").DiscordGuildEntryResolved>;
|
||||
logger: Logger;
|
||||
}) {
|
||||
try {
|
||||
@@ -203,8 +182,7 @@ async function handleDiscordReactionEvent(params: {
|
||||
|
||||
const channel = await client.fetchChannel(data.channel_id);
|
||||
if (!channel) return;
|
||||
const channelName =
|
||||
"name" in channel ? (channel.name ?? undefined) : undefined;
|
||||
const channelName = "name" in channel ? (channel.name ?? undefined) : undefined;
|
||||
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
||||
const channelConfig = resolveDiscordChannelConfig({
|
||||
guildInfo,
|
||||
@@ -233,18 +211,13 @@ async function handleDiscordReactionEvent(params: {
|
||||
const emojiLabel = formatDiscordReactionEmoji(data.emoji);
|
||||
const actorLabel = formatDiscordUserTag(user);
|
||||
const guildSlug =
|
||||
guildInfo?.slug ||
|
||||
(data.guild?.name
|
||||
? normalizeDiscordSlug(data.guild.name)
|
||||
: data.guild_id);
|
||||
guildInfo?.slug || (data.guild?.name ? normalizeDiscordSlug(data.guild.name) : data.guild_id);
|
||||
const channelLabel = channelSlug
|
||||
? `#${channelSlug}`
|
||||
: channelName
|
||||
? `#${normalizeDiscordSlug(channelName)}`
|
||||
: `#${data.channel_id}`;
|
||||
const authorLabel = message?.author
|
||||
? formatDiscordUserTag(message.author)
|
||||
: undefined;
|
||||
const authorLabel = message?.author ? formatDiscordUserTag(message.author) : undefined;
|
||||
const baseText = `Discord reaction ${action}: ${emojiLabel} by ${actorLabel} on ${guildSlug} ${channelLabel} msg ${data.message_id}`;
|
||||
const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
|
||||
const route = resolveAgentRoute({
|
||||
@@ -259,8 +232,6 @@ async function handleDiscordReactionEvent(params: {
|
||||
contextKey: `discord:reaction:${action}:${data.message_id}:${user.id}:${emojiLabel}`,
|
||||
});
|
||||
} catch (err) {
|
||||
params.logger.error(
|
||||
danger(`discord reaction handler failed: ${String(err)}`),
|
||||
);
|
||||
params.logger.error(danger(`discord reaction handler failed: ${String(err)}`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import { ChannelType, MessageType, type User } from "@buape/carbon";
|
||||
import { hasControlCommand } from "../../auto-reply/command-detection.js";
|
||||
import { shouldHandleTextCommands } from "../../auto-reply/commands-registry.js";
|
||||
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
||||
import {
|
||||
buildMentionRegexes,
|
||||
matchesMentionPatterns,
|
||||
} from "../../auto-reply/reply/mentions.js";
|
||||
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
|
||||
import { logVerbose, shouldLogVerbose } from "../../globals.js";
|
||||
import { recordChannelActivity } from "../../infra/channel-activity.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
@@ -39,15 +36,9 @@ import type {
|
||||
DiscordMessagePreflightContext,
|
||||
DiscordMessagePreflightParams,
|
||||
} from "./message-handler.preflight.types.js";
|
||||
import {
|
||||
resolveDiscordChannelInfo,
|
||||
resolveDiscordMessageText,
|
||||
} from "./message-utils.js";
|
||||
import { resolveDiscordChannelInfo, resolveDiscordMessageText } from "./message-utils.js";
|
||||
import { resolveDiscordSystemEvent } from "./system-events.js";
|
||||
import {
|
||||
resolveDiscordThreadChannel,
|
||||
resolveDiscordThreadParentInfo,
|
||||
} from "./threading.js";
|
||||
import { resolveDiscordThreadChannel, resolveDiscordThreadParentInfo } from "./threading.js";
|
||||
|
||||
export type {
|
||||
DiscordMessagePreflightContext,
|
||||
@@ -73,10 +64,7 @@ export async function preflightDiscordMessage(
|
||||
}
|
||||
|
||||
const isGuildMessage = Boolean(params.data.guild_id);
|
||||
const channelInfo = await resolveDiscordChannelInfo(
|
||||
params.client,
|
||||
message.channelId,
|
||||
);
|
||||
const channelInfo = await resolveDiscordChannelInfo(params.client, message.channelId);
|
||||
const isDirectMessage = channelInfo?.type === ChannelType.DM;
|
||||
const isGroupDm = channelInfo?.type === ChannelType.GroupDM;
|
||||
|
||||
@@ -97,17 +85,9 @@ export async function preflightDiscordMessage(
|
||||
return null;
|
||||
}
|
||||
if (dmPolicy !== "open") {
|
||||
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(
|
||||
() => [],
|
||||
);
|
||||
const effectiveAllowFrom = [
|
||||
...(params.allowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
];
|
||||
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, [
|
||||
"discord:",
|
||||
"user:",
|
||||
]);
|
||||
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
|
||||
const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom];
|
||||
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
|
||||
const permitted = allowList
|
||||
? allowListMatches(allowList, {
|
||||
id: author.id,
|
||||
@@ -145,15 +125,11 @@ export async function preflightDiscordMessage(
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`discord pairing reply failed for ${author.id}: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`discord pairing reply failed for ${author.id}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logVerbose(
|
||||
`Blocked unauthorized discord sender ${author.id} (dmPolicy=${dmPolicy})`,
|
||||
);
|
||||
logVerbose(`Blocked unauthorized discord sender ${author.id} (dmPolicy=${dmPolicy})`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -186,9 +162,7 @@ export async function preflightDiscordMessage(
|
||||
const mentionRegexes = buildMentionRegexes(params.cfg, route.agentId);
|
||||
const wasMentioned =
|
||||
!isDirectMessage &&
|
||||
(Boolean(
|
||||
botId && message.mentionedUsers?.some((user: User) => user.id === botId),
|
||||
) ||
|
||||
(Boolean(botId && message.mentionedUsers?.some((user: User) => user.id === botId)) ||
|
||||
matchesMentionPatterns(baseText, mentionRegexes));
|
||||
if (shouldLogVerbose()) {
|
||||
logVerbose(
|
||||
@@ -225,9 +199,7 @@ export async function preflightDiscordMessage(
|
||||
|
||||
const channelName =
|
||||
channelInfo?.name ??
|
||||
((isGuildMessage || isGroupDm) &&
|
||||
message.channel &&
|
||||
"name" in message.channel
|
||||
((isGuildMessage || isGroupDm) && message.channel && "name" in message.channel
|
||||
? message.channel.name
|
||||
: undefined);
|
||||
const threadChannel = resolveDiscordThreadChannel({
|
||||
@@ -250,18 +222,12 @@ export async function preflightDiscordMessage(
|
||||
}
|
||||
const threadName = threadChannel?.name;
|
||||
const configChannelName = threadParentName ?? channelName;
|
||||
const configChannelSlug = configChannelName
|
||||
? normalizeDiscordSlug(configChannelName)
|
||||
: "";
|
||||
const configChannelSlug = configChannelName ? normalizeDiscordSlug(configChannelName) : "";
|
||||
const displayChannelName = threadName ?? channelName;
|
||||
const displayChannelSlug = displayChannelName
|
||||
? normalizeDiscordSlug(displayChannelName)
|
||||
: "";
|
||||
const displayChannelSlug = displayChannelName ? normalizeDiscordSlug(displayChannelName) : "";
|
||||
const guildSlug =
|
||||
guildInfo?.slug ||
|
||||
(params.data.guild?.name
|
||||
? normalizeDiscordSlug(params.data.guild.name)
|
||||
: "");
|
||||
(params.data.guild?.name ? normalizeDiscordSlug(params.data.guild.name) : "");
|
||||
|
||||
const baseSessionKey = route.sessionKey;
|
||||
const channelConfig = isGuildMessage
|
||||
@@ -273,9 +239,7 @@ export async function preflightDiscordMessage(
|
||||
})
|
||||
: null;
|
||||
if (isGuildMessage && channelConfig?.enabled === false) {
|
||||
logVerbose(
|
||||
`Blocked discord channel ${message.channelId} (channel disabled)`,
|
||||
);
|
||||
logVerbose(`Blocked discord channel ${message.channelId} (channel disabled)`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -290,8 +254,7 @@ export async function preflightDiscordMessage(
|
||||
if (isGroupDm && !groupDmAllowed) return null;
|
||||
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(guildInfo?.channels) &&
|
||||
Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||
const channelAllowed = channelConfig?.allowed !== false;
|
||||
if (
|
||||
isGuildMessage &&
|
||||
@@ -304,9 +267,7 @@ export async function preflightDiscordMessage(
|
||||
if (params.groupPolicy === "disabled") {
|
||||
logVerbose("discord: drop guild message (groupPolicy: disabled)");
|
||||
} else if (!channelAllowlistConfigured) {
|
||||
logVerbose(
|
||||
"discord: drop guild message (groupPolicy: allowlist, no channel allowlist)",
|
||||
);
|
||||
logVerbose("discord: drop guild message (groupPolicy: allowlist, no channel allowlist)");
|
||||
} else {
|
||||
logVerbose(
|
||||
`Blocked discord channel ${message.channelId} not in guild channel allowlist (groupPolicy: allowlist)`,
|
||||
@@ -316,9 +277,7 @@ export async function preflightDiscordMessage(
|
||||
}
|
||||
|
||||
if (isGuildMessage && channelConfig?.allowed === false) {
|
||||
logVerbose(
|
||||
`Blocked discord channel ${message.channelId} not in guild channel allowlist`,
|
||||
);
|
||||
logVerbose(`Blocked discord channel ${message.channelId} not in guild channel allowlist`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -328,11 +287,7 @@ export async function preflightDiscordMessage(
|
||||
const historyEntry =
|
||||
isGuildMessage && params.historyLimit > 0 && textForHistory
|
||||
? ({
|
||||
sender:
|
||||
params.data.member?.nickname ??
|
||||
author.globalName ??
|
||||
author.username ??
|
||||
author.id,
|
||||
sender: params.data.member?.nickname ?? author.globalName ?? author.username ?? author.id,
|
||||
body: textForHistory,
|
||||
timestamp: resolveTimestampMs(message.timestamp),
|
||||
messageId: message.id,
|
||||
@@ -347,9 +302,9 @@ export async function preflightDiscordMessage(
|
||||
});
|
||||
const hasAnyMention = Boolean(
|
||||
!isDirectMessage &&
|
||||
(message.mentionedEveryone ||
|
||||
(message.mentionedUsers?.length ?? 0) > 0 ||
|
||||
(message.mentionedRoles?.length ?? 0) > 0),
|
||||
(message.mentionedEveryone ||
|
||||
(message.mentionedUsers?.length ?? 0) > 0 ||
|
||||
(message.mentionedRoles?.length ?? 0) > 0),
|
||||
);
|
||||
if (!isDirectMessage) {
|
||||
commandAuthorized = resolveDiscordCommandAuthorized({
|
||||
@@ -375,9 +330,7 @@ export async function preflightDiscordMessage(
|
||||
const canDetectMention = Boolean(botId) || mentionRegexes.length > 0;
|
||||
if (isGuildMessage && shouldRequireMention) {
|
||||
if (botId && !wasMentioned && !shouldBypassMention) {
|
||||
logVerbose(
|
||||
`discord: drop guild message (mention required, botId=${botId})`,
|
||||
);
|
||||
logVerbose(`discord: drop guild message (mention required, botId=${botId})`);
|
||||
logger.info(
|
||||
{
|
||||
channelId: message.channelId,
|
||||
@@ -399,9 +352,7 @@ export async function preflightDiscordMessage(
|
||||
userTag: formatDiscordUserTag(author),
|
||||
});
|
||||
if (!userOk) {
|
||||
logVerbose(
|
||||
`Blocked discord guild sender ${author.id} (not in channel users allowlist)`,
|
||||
);
|
||||
logVerbose(`Blocked discord guild sender ${author.id} (not in channel users allowlist)`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,11 @@ import type { ChannelType, Client, User } from "@buape/carbon";
|
||||
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
|
||||
import type { ReplyToMode } from "../../config/config.js";
|
||||
import type { resolveAgentRoute } from "../../routing/resolve-route.js";
|
||||
import type {
|
||||
DiscordChannelConfigResolved,
|
||||
DiscordGuildEntryResolved,
|
||||
} from "./allow-list.js";
|
||||
import type { DiscordChannelConfigResolved, DiscordGuildEntryResolved } from "./allow-list.js";
|
||||
import type { DiscordChannelInfo } from "./message-utils.js";
|
||||
import type { DiscordThreadChannel } from "./threading.js";
|
||||
|
||||
export type LoadedConfig = ReturnType<
|
||||
typeof import("../../config/config.js").loadConfig
|
||||
>;
|
||||
export type LoadedConfig = ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
export type RuntimeEnv = import("../../runtime.js").RuntimeEnv;
|
||||
|
||||
export type DiscordMessageEvent = import("./listeners.js").DiscordMessageEvent;
|
||||
|
||||
@@ -3,15 +3,9 @@ import {
|
||||
resolveEffectiveMessagesConfig,
|
||||
resolveHumanDelayConfig,
|
||||
} from "../../agents/identity.js";
|
||||
import {
|
||||
formatAgentEnvelope,
|
||||
formatThreadStarterEnvelope,
|
||||
} from "../../auto-reply/envelope.js";
|
||||
import { formatAgentEnvelope, formatThreadStarterEnvelope } from "../../auto-reply/envelope.js";
|
||||
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
|
||||
import {
|
||||
buildHistoryContextFromMap,
|
||||
clearHistoryEntries,
|
||||
} from "../../auto-reply/reply/history.js";
|
||||
import { buildHistoryContextFromMap, clearHistoryEntries } from "../../auto-reply/reply/history.js";
|
||||
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
|
||||
import type { ReplyPayload } from "../../auto-reply/types.js";
|
||||
import { resolveStorePath, updateLastRoute } from "../../config/sessions.js";
|
||||
@@ -28,11 +22,7 @@ import {
|
||||
resolveDiscordMessageText,
|
||||
resolveMediaList,
|
||||
} from "./message-utils.js";
|
||||
import {
|
||||
buildDirectLabel,
|
||||
buildGuildLabel,
|
||||
resolveReplyContext,
|
||||
} from "./reply-context.js";
|
||||
import { buildDirectLabel, buildGuildLabel, resolveReplyContext } from "./reply-context.js";
|
||||
import { deliverDiscordReply } from "./reply-delivery.js";
|
||||
import {
|
||||
maybeCreateDiscordAutoThread,
|
||||
@@ -41,9 +31,7 @@ import {
|
||||
} from "./threading.js";
|
||||
import { sendTyping } from "./typing.js";
|
||||
|
||||
export async function processDiscordMessage(
|
||||
ctx: DiscordMessagePreflightContext,
|
||||
) {
|
||||
export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) {
|
||||
const {
|
||||
cfg,
|
||||
discordConfig,
|
||||
@@ -115,9 +103,7 @@ export async function processDiscordMessage(
|
||||
}).then(
|
||||
() => true,
|
||||
(err) => {
|
||||
logVerbose(
|
||||
`discord react failed for channel ${message.channelId}: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`discord react failed for channel ${message.channelId}: ${String(err)}`);
|
||||
return false;
|
||||
},
|
||||
)
|
||||
@@ -130,8 +116,7 @@ export async function processDiscordMessage(
|
||||
channelName: channelName ?? message.channelId,
|
||||
channelId: message.channelId,
|
||||
});
|
||||
const groupRoom =
|
||||
isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
|
||||
const groupRoom = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
|
||||
const groupSubject = isDirectMessage ? undefined : groupRoom;
|
||||
const channelDescription = channelInfo?.topic?.trim();
|
||||
const systemPromptParts = [
|
||||
@@ -216,9 +201,7 @@ export async function processDiscordMessage(
|
||||
Body: combinedBody,
|
||||
RawBody: baseText,
|
||||
CommandBody: baseText,
|
||||
From: isDirectMessage
|
||||
? `discord:${author.id}`
|
||||
: `group:${message.channelId}`,
|
||||
From: isDirectMessage ? `discord:${author.id}` : `group:${message.channelId}`,
|
||||
To: discordTo,
|
||||
SessionKey: threadKeys.sessionKey,
|
||||
AccountId: route.accountId,
|
||||
@@ -230,9 +213,7 @@ export async function processDiscordMessage(
|
||||
GroupSubject: groupSubject,
|
||||
GroupRoom: groupRoom,
|
||||
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
||||
GroupSpace: isGuildMessage
|
||||
? (guildInfo?.id ?? guildSlug) || undefined
|
||||
: undefined,
|
||||
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
|
||||
Provider: "discord" as const,
|
||||
Surface: "discord" as const,
|
||||
WasMentioned: effectiveWasMentioned,
|
||||
@@ -295,34 +276,30 @@ export async function processDiscordMessage(
|
||||
}
|
||||
|
||||
let didSendReply = false;
|
||||
const { dispatcher, replyOptions, markDispatchIdle } =
|
||||
createReplyDispatcherWithTyping({
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
.responsePrefix,
|
||||
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
||||
deliver: async (payload: ReplyPayload) => {
|
||||
const replyToId = replyReference.use();
|
||||
await deliverDiscordReply({
|
||||
replies: [payload],
|
||||
target: deliverTarget,
|
||||
token,
|
||||
accountId,
|
||||
rest: client.rest,
|
||||
runtime,
|
||||
replyToId,
|
||||
textLimit,
|
||||
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
||||
});
|
||||
didSendReply = true;
|
||||
replyReference.markSent();
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(
|
||||
danger(`discord ${info.kind} reply failed: ${String(err)}`),
|
||||
);
|
||||
},
|
||||
onReplyStart: () => sendTyping({ client, channelId: message.channelId }),
|
||||
});
|
||||
const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
||||
deliver: async (payload: ReplyPayload) => {
|
||||
const replyToId = replyReference.use();
|
||||
await deliverDiscordReply({
|
||||
replies: [payload],
|
||||
target: deliverTarget,
|
||||
token,
|
||||
accountId,
|
||||
rest: client.rest,
|
||||
runtime,
|
||||
replyToId,
|
||||
textLimit,
|
||||
maxLinesPerMessage: discordConfig?.maxLinesPerMessage,
|
||||
});
|
||||
didSendReply = true;
|
||||
replyReference.markSent();
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(danger(`discord ${info.kind} reply failed: ${String(err)}`));
|
||||
},
|
||||
onReplyStart: () => sendTyping({ client, channelId: message.channelId }),
|
||||
});
|
||||
|
||||
const { queuedFinal, counts } = await dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
@@ -339,12 +316,7 @@ export async function processDiscordMessage(
|
||||
});
|
||||
markDispatchIdle();
|
||||
if (!queuedFinal) {
|
||||
if (
|
||||
isGuildMessage &&
|
||||
shouldClearHistory &&
|
||||
historyLimit > 0 &&
|
||||
didSendReply
|
||||
) {
|
||||
if (isGuildMessage && shouldClearHistory && historyLimit > 0 && didSendReply) {
|
||||
clearHistoryEntries({
|
||||
historyMap: guildHistories,
|
||||
historyKey: message.channelId,
|
||||
@@ -372,12 +344,7 @@ export async function processDiscordMessage(
|
||||
});
|
||||
});
|
||||
}
|
||||
if (
|
||||
isGuildMessage &&
|
||||
shouldClearHistory &&
|
||||
historyLimit > 0 &&
|
||||
didSendReply
|
||||
) {
|
||||
if (isGuildMessage && shouldClearHistory && historyLimit > 0 && didSendReply) {
|
||||
clearHistoryEntries({
|
||||
historyMap: guildHistories,
|
||||
historyKey: message.channelId,
|
||||
|
||||
@@ -7,9 +7,7 @@ import type { DiscordMessageHandler } from "./listeners.js";
|
||||
import { preflightDiscordMessage } from "./message-handler.preflight.js";
|
||||
import { processDiscordMessage } from "./message-handler.process.js";
|
||||
|
||||
type LoadedConfig = ReturnType<
|
||||
typeof import("../../config/config.js").loadConfig
|
||||
>;
|
||||
type LoadedConfig = ReturnType<typeof import("../../config/config.js").loadConfig>;
|
||||
type DiscordConfig = NonNullable<
|
||||
import("../../config/config.js").ClawdbotConfig["channels"]
|
||||
>["discord"];
|
||||
@@ -33,8 +31,7 @@ export function createDiscordMessageHandler(params: {
|
||||
guildEntries?: Record<string, DiscordGuildEntryResolved>;
|
||||
}): DiscordMessageHandler {
|
||||
const groupPolicy = params.discordConfig?.groupPolicy ?? "open";
|
||||
const ackReactionScope =
|
||||
params.cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
const ackReactionScope = params.cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
|
||||
return async (data, client) => {
|
||||
try {
|
||||
|
||||
@@ -64,8 +64,7 @@ export async function resolveDiscordChannelInfo(
|
||||
}
|
||||
const name = "name" in channel ? (channel.name ?? undefined) : undefined;
|
||||
const topic = "topic" in channel ? (channel.topic ?? undefined) : undefined;
|
||||
const parentId =
|
||||
"parentId" in channel ? (channel.parentId ?? undefined) : undefined;
|
||||
const parentId = "parentId" in channel ? (channel.parentId ?? undefined) : undefined;
|
||||
const payload: DiscordChannelInfo = {
|
||||
type: channel.type,
|
||||
name,
|
||||
@@ -113,9 +112,7 @@ export async function resolveMediaList(
|
||||
});
|
||||
} catch (err) {
|
||||
const id = attachment.id ?? attachment.url;
|
||||
logVerbose(
|
||||
`discord: failed to download attachment ${id}: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`discord: failed to download attachment ${id}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
@@ -137,9 +134,7 @@ function isImageAttachment(attachment: APIAttachment): boolean {
|
||||
return /\.(avif|bmp|gif|heic|heif|jpe?g|png|tiff?|webp)$/.test(name);
|
||||
}
|
||||
|
||||
function buildDiscordAttachmentPlaceholder(
|
||||
attachments?: APIAttachment[],
|
||||
): string {
|
||||
function buildDiscordAttachmentPlaceholder(attachments?: APIAttachment[]): string {
|
||||
if (!attachments || attachments.length === 0) return "";
|
||||
const count = attachments.length;
|
||||
const allImages = attachments.every(isImageAttachment);
|
||||
@@ -186,29 +181,21 @@ function resolveDiscordForwardedMessagesText(message: Message): string {
|
||||
return forwardedBlocks.join("\n\n");
|
||||
}
|
||||
|
||||
function resolveDiscordMessageSnapshots(
|
||||
message: Message,
|
||||
): DiscordMessageSnapshot[] {
|
||||
const rawData = (message as { rawData?: { message_snapshots?: unknown } })
|
||||
.rawData;
|
||||
function resolveDiscordMessageSnapshots(message: Message): DiscordMessageSnapshot[] {
|
||||
const rawData = (message as { rawData?: { message_snapshots?: unknown } }).rawData;
|
||||
const snapshots =
|
||||
rawData?.message_snapshots ??
|
||||
(message as { message_snapshots?: unknown }).message_snapshots ??
|
||||
(message as { messageSnapshots?: unknown }).messageSnapshots;
|
||||
if (!Array.isArray(snapshots)) return [];
|
||||
return snapshots.filter(
|
||||
(entry): entry is DiscordMessageSnapshot =>
|
||||
Boolean(entry) && typeof entry === "object",
|
||||
(entry): entry is DiscordMessageSnapshot => Boolean(entry) && typeof entry === "object",
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDiscordSnapshotMessageText(
|
||||
snapshot: DiscordSnapshotMessage,
|
||||
): string {
|
||||
function resolveDiscordSnapshotMessageText(snapshot: DiscordSnapshotMessage): string {
|
||||
const content = snapshot.content?.trim() ?? "";
|
||||
const attachmentText = buildDiscordAttachmentPlaceholder(
|
||||
snapshot.attachments ?? undefined,
|
||||
);
|
||||
const attachmentText = buildDiscordAttachmentPlaceholder(snapshot.attachments ?? undefined);
|
||||
const embed = snapshot.embeds?.[0];
|
||||
const embedText = embed?.description?.trim() || embed?.title?.trim() || "";
|
||||
return content || attachmentText || embedText || "";
|
||||
@@ -243,9 +230,7 @@ export function buildDiscordMediaPayload(
|
||||
} {
|
||||
const first = mediaList[0];
|
||||
const mediaPaths = mediaList.map((media) => media.path);
|
||||
const mediaTypes = mediaList
|
||||
.map((media) => media.contentType)
|
||||
.filter(Boolean) as string[];
|
||||
const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean) as string[];
|
||||
return {
|
||||
MediaPath: first?.path,
|
||||
MediaType: first?.contentType,
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import {
|
||||
ChannelType,
|
||||
Command,
|
||||
type CommandInteraction,
|
||||
type CommandOptions,
|
||||
} from "@buape/carbon";
|
||||
import { ChannelType, Command, type CommandInteraction, type CommandOptions } from "@buape/carbon";
|
||||
import { ApplicationCommandOptionType } from "discord-api-types/v10";
|
||||
|
||||
import {
|
||||
resolveEffectiveMessagesConfig,
|
||||
resolveHumanDelayConfig,
|
||||
} from "../../agents/identity.js";
|
||||
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
|
||||
import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
|
||||
import { buildCommandText } from "../../auto-reply/commands-registry.js";
|
||||
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
|
||||
@@ -48,14 +40,7 @@ export function createDiscordNativeCommand(params: {
|
||||
sessionPrefix: string;
|
||||
ephemeralDefault: boolean;
|
||||
}) {
|
||||
const {
|
||||
command,
|
||||
cfg,
|
||||
discordConfig,
|
||||
accountId,
|
||||
sessionPrefix,
|
||||
ephemeralDefault,
|
||||
} = params;
|
||||
const { command, cfg, discordConfig, accountId, sessionPrefix, ephemeralDefault } = params;
|
||||
return new (class extends Command {
|
||||
name = command.name;
|
||||
description = command.description;
|
||||
@@ -80,14 +65,11 @@ export function createDiscordNativeCommand(params: {
|
||||
const channelType = channel?.type;
|
||||
const isDirectMessage = channelType === ChannelType.DM;
|
||||
const isGroupDm = channelType === ChannelType.GroupDM;
|
||||
const channelName =
|
||||
channel && "name" in channel ? (channel.name as string) : undefined;
|
||||
const channelName = channel && "name" in channel ? (channel.name as string) : undefined;
|
||||
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
||||
const prompt = buildCommandText(
|
||||
this.name,
|
||||
command.acceptsArgs
|
||||
? interaction.options.getString("input")
|
||||
: undefined,
|
||||
command.acceptsArgs ? interaction.options.getString("input") : undefined,
|
||||
);
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: interaction.guild ?? undefined,
|
||||
@@ -115,8 +97,7 @@ export function createDiscordNativeCommand(params: {
|
||||
}
|
||||
if (useAccessGroups && interaction.guild) {
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(guildInfo?.channels) &&
|
||||
Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||
const channelAllowed = channelConfig?.allowed !== false;
|
||||
const allowByPolicy = isDiscordGroupAllowedByPolicy({
|
||||
groupPolicy: discordConfig?.groupPolicy ?? "open",
|
||||
@@ -139,17 +120,9 @@ export function createDiscordNativeCommand(params: {
|
||||
return;
|
||||
}
|
||||
if (dmPolicy !== "open") {
|
||||
const storeAllowFrom = await readChannelAllowFromStore(
|
||||
"discord",
|
||||
).catch(() => []);
|
||||
const effectiveAllowFrom = [
|
||||
...(discordConfig?.dm?.allowFrom ?? []),
|
||||
...storeAllowFrom,
|
||||
];
|
||||
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, [
|
||||
"discord:",
|
||||
"user:",
|
||||
]);
|
||||
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
|
||||
const effectiveAllowFrom = [...(discordConfig?.dm?.allowFrom ?? []), ...storeAllowFrom];
|
||||
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
|
||||
const permitted = allowList
|
||||
? allowListMatches(allowList, {
|
||||
id: user.id,
|
||||
@@ -237,19 +210,13 @@ export function createDiscordNativeCommand(params: {
|
||||
GroupSystemPrompt: isGuild
|
||||
? (() => {
|
||||
const channelTopic =
|
||||
channel && "topic" in channel
|
||||
? (channel.topic ?? undefined)
|
||||
: undefined;
|
||||
channel && "topic" in channel ? (channel.topic ?? undefined) : undefined;
|
||||
const channelDescription = channelTopic?.trim();
|
||||
const systemPromptParts = [
|
||||
channelDescription
|
||||
? `Channel topic: ${channelDescription}`
|
||||
: null,
|
||||
channelDescription ? `Channel topic: ${channelDescription}` : null,
|
||||
channelConfig?.systemPrompt?.trim() || null,
|
||||
].filter((entry): entry is string => Boolean(entry));
|
||||
return systemPromptParts.length > 0
|
||||
? systemPromptParts.join("\n\n")
|
||||
: undefined;
|
||||
return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||
})()
|
||||
: undefined,
|
||||
SenderName: user.globalName ?? user.username,
|
||||
@@ -270,8 +237,7 @@ export function createDiscordNativeCommand(params: {
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcherOptions: {
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId)
|
||||
.responsePrefix,
|
||||
responsePrefix: resolveEffectiveMessagesConfig(cfg, route.agentId).responsePrefix,
|
||||
humanDelay: resolveHumanDelayConfig(cfg, route.agentId),
|
||||
deliver: async (payload) => {
|
||||
await deliverDiscordInteractionReply({
|
||||
@@ -308,22 +274,12 @@ async function deliverDiscordInteractionReply(params: {
|
||||
maxLinesPerMessage?: number;
|
||||
preferFollowUp: boolean;
|
||||
}) {
|
||||
const {
|
||||
interaction,
|
||||
payload,
|
||||
textLimit,
|
||||
maxLinesPerMessage,
|
||||
preferFollowUp,
|
||||
} = params;
|
||||
const mediaList =
|
||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const { interaction, payload, textLimit, maxLinesPerMessage, preferFollowUp } = params;
|
||||
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const text = payload.text ?? "";
|
||||
|
||||
let hasReplied = false;
|
||||
const sendMessage = async (
|
||||
content: string,
|
||||
files?: { name: string; data: Buffer }[],
|
||||
) => {
|
||||
const sendMessage = async (content: string, files?: { name: string; data: Buffer }[]) => {
|
||||
const payload =
|
||||
files && files.length > 0
|
||||
? {
|
||||
|
||||
@@ -15,10 +15,7 @@ import { getChildLogger } from "../../logging.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import { resolveDiscordAccount } from "../accounts.js";
|
||||
import { attachDiscordGatewayLogging } from "../gateway-logging.js";
|
||||
import {
|
||||
getDiscordGatewayEmitter,
|
||||
waitForDiscordGatewayStop,
|
||||
} from "../monitor.gateway.js";
|
||||
import { getDiscordGatewayEmitter, waitForDiscordGatewayStop } from "../monitor.gateway.js";
|
||||
import { fetchDiscordApplicationId } from "../probe.js";
|
||||
import { normalizeDiscordToken } from "../token.js";
|
||||
import {
|
||||
@@ -44,8 +41,7 @@ export type MonitorDiscordOpts = {
|
||||
function summarizeAllowList(list?: Array<string | number>) {
|
||||
if (!list || list.length === 0) return "any";
|
||||
const sample = list.slice(0, 4).map((entry) => String(entry));
|
||||
const suffix =
|
||||
list.length > sample.length ? ` (+${list.length - sample.length})` : "";
|
||||
const suffix = list.length > sample.length ? ` (+${list.length - sample.length})` : "";
|
||||
return `${sample.join(", ")}${suffix}`;
|
||||
}
|
||||
|
||||
@@ -53,8 +49,7 @@ function summarizeGuilds(entries?: Record<string, unknown>) {
|
||||
if (!entries || Object.keys(entries).length === 0) return "any";
|
||||
const keys = Object.keys(entries);
|
||||
const sample = keys.slice(0, 4);
|
||||
const suffix =
|
||||
keys.length > sample.length ? ` (+${keys.length - sample.length})` : "";
|
||||
const suffix = keys.length > sample.length ? ` (+${keys.length - sample.length})` : "";
|
||||
return `${sample.join(", ")}${suffix}`;
|
||||
}
|
||||
|
||||
@@ -84,17 +79,13 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const guildEntries = discordCfg.guilds;
|
||||
const groupPolicy = discordCfg.groupPolicy ?? "open";
|
||||
const allowFrom = dmConfig?.allowFrom;
|
||||
const mediaMaxBytes =
|
||||
(opts.mediaMaxMb ?? discordCfg.mediaMaxMb ?? 8) * 1024 * 1024;
|
||||
const mediaMaxBytes = (opts.mediaMaxMb ?? discordCfg.mediaMaxMb ?? 8) * 1024 * 1024;
|
||||
const textLimit = resolveTextChunkLimit(cfg, "discord", account.accountId, {
|
||||
fallbackLimit: 2000,
|
||||
});
|
||||
const historyLimit = Math.max(
|
||||
0,
|
||||
opts.historyLimit ??
|
||||
discordCfg.historyLimit ??
|
||||
cfg.messages?.groupChat?.historyLimit ??
|
||||
20,
|
||||
opts.historyLimit ?? discordCfg.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 20,
|
||||
);
|
||||
const replyToMode = opts.replyToMode ?? discordCfg.replyToMode ?? "off";
|
||||
const dmEnabled = dmConfig?.enabled ?? true;
|
||||
@@ -125,9 +116,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
throw new Error("Failed to resolve Discord application id");
|
||||
}
|
||||
|
||||
const commandSpecs = nativeEnabled
|
||||
? listNativeCommandSpecsForConfig(cfg)
|
||||
: [];
|
||||
const commandSpecs = nativeEnabled ? listNativeCommandSpecsForConfig(cfg) : [];
|
||||
const commands = commandSpecs.map((spec) =>
|
||||
createDiscordNativeCommand({
|
||||
command: spec,
|
||||
@@ -190,9 +179,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const botUser = await client.fetchUser("@me");
|
||||
botUserId = botUser?.id;
|
||||
} catch (err) {
|
||||
runtime.error?.(
|
||||
danger(`discord: failed to fetch bot identity: ${String(err)}`),
|
||||
);
|
||||
runtime.error?.(danger(`discord: failed to fetch bot identity: ${String(err)}`));
|
||||
}
|
||||
|
||||
const messageHandler = createDiscordMessageHandler({
|
||||
@@ -214,10 +201,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
guildEntries,
|
||||
});
|
||||
|
||||
registerDiscordListener(
|
||||
client.listeners,
|
||||
new DiscordMessageListener(messageHandler, logger),
|
||||
);
|
||||
registerDiscordListener(client.listeners, new DiscordMessageListener(messageHandler, logger));
|
||||
registerDiscordListener(
|
||||
client.listeners,
|
||||
new DiscordReactionListener({
|
||||
@@ -285,8 +269,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
shouldStopOnError: (err) => {
|
||||
const message = String(err);
|
||||
return (
|
||||
message.includes("Max reconnect attempts") ||
|
||||
message.includes("Fatal Gateway error")
|
||||
message.includes("Max reconnect attempts") || message.includes("Fatal Gateway error")
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -303,16 +286,11 @@ async function clearDiscordNativeCommands(params: {
|
||||
runtime: RuntimeEnv;
|
||||
}) {
|
||||
try {
|
||||
await params.client.rest.put(
|
||||
Routes.applicationCommands(params.applicationId),
|
||||
{
|
||||
body: [],
|
||||
},
|
||||
);
|
||||
await params.client.rest.put(Routes.applicationCommands(params.applicationId), {
|
||||
body: [],
|
||||
});
|
||||
logVerbose("discord: cleared native commands (commands.native=false)");
|
||||
} catch (err) {
|
||||
params.runtime.error?.(
|
||||
danger(`discord: failed to clear native commands: ${String(err)}`),
|
||||
);
|
||||
params.runtime.error?.(danger(`discord: failed to clear native commands: ${String(err)}`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ import { formatDiscordUserTag, resolveTimestampMs } from "./format.js";
|
||||
|
||||
export function resolveReplyContext(
|
||||
message: Message,
|
||||
resolveDiscordMessageText: (
|
||||
message: Message,
|
||||
options?: { includeForwarded?: boolean },
|
||||
) => string,
|
||||
resolveDiscordMessageText: (message: Message, options?: { includeForwarded?: boolean }) => string,
|
||||
): string | null {
|
||||
const referenced = message.referencedMessage;
|
||||
if (!referenced?.author) return null;
|
||||
@@ -16,9 +13,7 @@ export function resolveReplyContext(
|
||||
includeForwarded: true,
|
||||
});
|
||||
if (!referencedText) return null;
|
||||
const fromLabel = referenced.author
|
||||
? buildDirectLabel(referenced.author)
|
||||
: "Unknown";
|
||||
const fromLabel = referenced.author ? buildDirectLabel(referenced.author) : "Unknown";
|
||||
const body = `${referencedText}\n[discord message id: ${referenced.id} channel: ${referenced.channelId} from: ${formatDiscordUserTag(referenced.author)} user id:${referenced.author?.id ?? "unknown"}]`;
|
||||
return formatAgentEnvelope({
|
||||
channel: "Discord",
|
||||
@@ -33,11 +28,7 @@ export function buildDirectLabel(author: User) {
|
||||
return `${username} user id:${author.id}`;
|
||||
}
|
||||
|
||||
export function buildGuildLabel(params: {
|
||||
guild?: Guild;
|
||||
channelName: string;
|
||||
channelId: string;
|
||||
}) {
|
||||
export function buildGuildLabel(params: { guild?: Guild; channelName: string; channelId: string }) {
|
||||
const { guild, channelName, channelId } = params;
|
||||
return `${guild?.name ?? "Guild"} #${channelName} channel id:${channelId}`;
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ export async function deliverDiscordReply(params: {
|
||||
}) {
|
||||
const chunkLimit = Math.min(params.textLimit, 2000);
|
||||
for (const payload of params.replies) {
|
||||
const mediaList =
|
||||
payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const mediaList = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
||||
const text = payload.text ?? "";
|
||||
if (!text && mediaList.length === 0) continue;
|
||||
const replyTo = params.replyToId?.trim() || undefined;
|
||||
|
||||
@@ -2,10 +2,7 @@ import { type Message, MessageType } from "@buape/carbon";
|
||||
|
||||
import { formatDiscordUserTag } from "./format.js";
|
||||
|
||||
export function resolveDiscordSystemEvent(
|
||||
message: Message,
|
||||
location: string,
|
||||
): string | null {
|
||||
export function resolveDiscordSystemEvent(message: Message, location: string): string | null {
|
||||
switch (message.type) {
|
||||
case MessageType.ChannelPinnedMessage:
|
||||
return buildDiscordSystemEvent(message, location, "pinned a message");
|
||||
@@ -18,84 +15,42 @@ export function resolveDiscordSystemEvent(
|
||||
case MessageType.GuildBoost:
|
||||
return buildDiscordSystemEvent(message, location, "boosted the server");
|
||||
case MessageType.GuildBoostTier1:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"boosted the server (Tier 1 reached)",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "boosted the server (Tier 1 reached)");
|
||||
case MessageType.GuildBoostTier2:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"boosted the server (Tier 2 reached)",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "boosted the server (Tier 2 reached)");
|
||||
case MessageType.GuildBoostTier3:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"boosted the server (Tier 3 reached)",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "boosted the server (Tier 3 reached)");
|
||||
case MessageType.ThreadCreated:
|
||||
return buildDiscordSystemEvent(message, location, "created a thread");
|
||||
case MessageType.AutoModerationAction:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"auto moderation action",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "auto moderation action");
|
||||
case MessageType.GuildIncidentAlertModeEnabled:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"raid protection enabled",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "raid protection enabled");
|
||||
case MessageType.GuildIncidentAlertModeDisabled:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"raid protection disabled",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "raid protection disabled");
|
||||
case MessageType.GuildIncidentReportRaid:
|
||||
return buildDiscordSystemEvent(message, location, "raid reported");
|
||||
case MessageType.GuildIncidentReportFalseAlarm:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"raid report marked false alarm",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "raid report marked false alarm");
|
||||
case MessageType.StageStart:
|
||||
return buildDiscordSystemEvent(message, location, "stage started");
|
||||
case MessageType.StageEnd:
|
||||
return buildDiscordSystemEvent(message, location, "stage ended");
|
||||
case MessageType.StageSpeaker:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"stage speaker updated",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "stage speaker updated");
|
||||
case MessageType.StageTopic:
|
||||
return buildDiscordSystemEvent(message, location, "stage topic updated");
|
||||
case MessageType.PollResult:
|
||||
return buildDiscordSystemEvent(message, location, "poll results posted");
|
||||
case MessageType.PurchaseNotification:
|
||||
return buildDiscordSystemEvent(
|
||||
message,
|
||||
location,
|
||||
"purchase notification",
|
||||
);
|
||||
return buildDiscordSystemEvent(message, location, "purchase notification");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildDiscordSystemEvent(
|
||||
message: Message,
|
||||
location: string,
|
||||
action: string,
|
||||
) {
|
||||
const authorLabel = message.author
|
||||
? formatDiscordUserTag(message.author)
|
||||
: "";
|
||||
function buildDiscordSystemEvent(message: Message, location: string, action: string) {
|
||||
const authorLabel = message.author ? formatDiscordUserTag(message.author) : "";
|
||||
const actor = authorLabel ? `${authorLabel} ` : "";
|
||||
return `Discord system: ${actor}${action} in ${location}`;
|
||||
}
|
||||
|
||||
@@ -44,10 +44,7 @@ export function resolveDiscordThreadChannel(params: {
|
||||
}): DiscordThreadChannel | null {
|
||||
if (!params.isGuildMessage) return null;
|
||||
const { message, channelInfo } = params;
|
||||
const channel =
|
||||
"channel" in message
|
||||
? (message as { channel?: unknown }).channel
|
||||
: undefined;
|
||||
const channel = "channel" in message ? (message as { channel?: unknown }).channel : undefined;
|
||||
const isThreadChannel =
|
||||
channel &&
|
||||
typeof channel === "object" &&
|
||||
@@ -71,10 +68,7 @@ export async function resolveDiscordThreadParentInfo(params: {
|
||||
}): Promise<DiscordThreadParentInfo> {
|
||||
const { threadChannel, channelInfo, client } = params;
|
||||
const parentId =
|
||||
threadChannel.parentId ??
|
||||
threadChannel.parent?.id ??
|
||||
channelInfo?.parentId ??
|
||||
undefined;
|
||||
threadChannel.parentId ?? threadChannel.parent?.id ?? channelInfo?.parentId ?? undefined;
|
||||
if (!parentId) return {};
|
||||
let parentName = threadChannel.parent?.name;
|
||||
const parentInfo = await resolveDiscordChannelInfo(client, parentId);
|
||||
@@ -96,11 +90,8 @@ export async function resolveDiscordThreadStarter(params: {
|
||||
try {
|
||||
const parentType = params.parentType;
|
||||
const isForumParent =
|
||||
parentType === ChannelType.GuildForum ||
|
||||
parentType === ChannelType.GuildMedia;
|
||||
const messageChannelId = isForumParent
|
||||
? params.channel.id
|
||||
: params.parentId;
|
||||
parentType === ChannelType.GuildForum || parentType === ChannelType.GuildMedia;
|
||||
const messageChannelId = isForumParent ? params.channel.id : params.parentId;
|
||||
if (!messageChannelId) return null;
|
||||
const starter = (await params.client.rest.get(
|
||||
Routes.channelMessage(messageChannelId, params.channel.id),
|
||||
@@ -116,8 +107,7 @@ export async function resolveDiscordThreadStarter(params: {
|
||||
timestamp?: string | null;
|
||||
};
|
||||
if (!starter) return null;
|
||||
const text =
|
||||
starter.content?.trim() ?? starter.embeds?.[0]?.description?.trim() ?? "";
|
||||
const text = starter.content?.trim() ?? starter.embeds?.[0]?.description?.trim() ?? "";
|
||||
if (!text) return null;
|
||||
const author =
|
||||
starter.member?.nick ??
|
||||
@@ -152,10 +142,7 @@ export function resolveDiscordReplyTarget(opts: {
|
||||
return opts.hasReplied ? undefined : replyToId;
|
||||
}
|
||||
|
||||
export function sanitizeDiscordThreadName(
|
||||
rawName: string,
|
||||
fallbackId: string,
|
||||
): string {
|
||||
export function sanitizeDiscordThreadName(rawName: string, fallbackId: string): string {
|
||||
const cleanedName = rawName
|
||||
.replace(/<@!?\d+>/g, "") // user mentions
|
||||
.replace(/<@&\d+>/g, "") // role mentions
|
||||
|
||||
@@ -2,22 +2,14 @@ import type { Client } from "@buape/carbon";
|
||||
|
||||
import { logVerbose } from "../../globals.js";
|
||||
|
||||
export async function sendTyping(params: {
|
||||
client: Client;
|
||||
channelId: string;
|
||||
}) {
|
||||
export async function sendTyping(params: { client: Client; channelId: string }) {
|
||||
try {
|
||||
const channel = await params.client.fetchChannel(params.channelId);
|
||||
if (!channel) return;
|
||||
if (
|
||||
"triggerTyping" in channel &&
|
||||
typeof channel.triggerTyping === "function"
|
||||
) {
|
||||
if ("triggerTyping" in channel && typeof channel.triggerTyping === "function") {
|
||||
await channel.triggerTyping();
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`discord typing cue failed for channel ${params.channelId}: ${String(err)}`,
|
||||
);
|
||||
logVerbose(`discord typing cue failed for channel ${params.channelId}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ describe("resolveDiscordPrivilegedIntentsFromFlags", () => {
|
||||
});
|
||||
|
||||
it("prefers enabled over limited when both set", () => {
|
||||
const flags =
|
||||
(1 << 12) | (1 << 13) | (1 << 14) | (1 << 15) | (1 << 18) | (1 << 19);
|
||||
const flags = (1 << 12) | (1 << 13) | (1 << 14) | (1 << 15) | (1 << 18) | (1 << 19);
|
||||
expect(resolveDiscordPrivilegedIntentsFromFlags(flags)).toEqual({
|
||||
presence: "enabled",
|
||||
guildMembers: "enabled",
|
||||
|
||||
@@ -41,10 +41,7 @@ export function resolveDiscordPrivilegedIntentsFromFlags(
|
||||
return "disabled";
|
||||
};
|
||||
return {
|
||||
presence: resolve(
|
||||
DISCORD_APP_FLAG_GATEWAY_PRESENCE,
|
||||
DISCORD_APP_FLAG_GATEWAY_PRESENCE_LIMITED,
|
||||
),
|
||||
presence: resolve(DISCORD_APP_FLAG_GATEWAY_PRESENCE, DISCORD_APP_FLAG_GATEWAY_PRESENCE_LIMITED),
|
||||
guildMembers: resolve(
|
||||
DISCORD_APP_FLAG_GATEWAY_GUILD_MEMBERS,
|
||||
DISCORD_APP_FLAG_GATEWAY_GUILD_MEMBERS_LIMITED,
|
||||
@@ -75,16 +72,12 @@ export async function fetchDiscordApplicationSummary(
|
||||
if (!res.ok) return undefined;
|
||||
const json = (await res.json()) as { id?: string; flags?: number };
|
||||
const flags =
|
||||
typeof json.flags === "number" && Number.isFinite(json.flags)
|
||||
? json.flags
|
||||
: undefined;
|
||||
typeof json.flags === "number" && Number.isFinite(json.flags) ? json.flags : undefined;
|
||||
return {
|
||||
id: json.id ?? null,
|
||||
flags: flags ?? null,
|
||||
intents:
|
||||
typeof flags === "number"
|
||||
? resolveDiscordPrivilegedIntentsFromFlags(flags)
|
||||
: undefined,
|
||||
typeof flags === "number" ? resolveDiscordPrivilegedIntentsFromFlags(flags) : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
@@ -129,14 +122,9 @@ export async function probeDiscord(
|
||||
};
|
||||
}
|
||||
try {
|
||||
const res = await fetchWithTimeout(
|
||||
`${DISCORD_API_BASE}/users/@me`,
|
||||
timeoutMs,
|
||||
fetcher,
|
||||
{
|
||||
Authorization: `Bot ${normalized}`,
|
||||
},
|
||||
);
|
||||
const res = await fetchWithTimeout(`${DISCORD_API_BASE}/users/@me`, timeoutMs, fetcher, {
|
||||
Authorization: `Bot ${normalized}`,
|
||||
});
|
||||
if (!res.ok) {
|
||||
result.status = res.status;
|
||||
result.error = `getMe failed (${res.status})`;
|
||||
@@ -150,11 +138,7 @@ export async function probeDiscord(
|
||||
};
|
||||
if (includeApplication) {
|
||||
result.application =
|
||||
(await fetchDiscordApplicationSummary(
|
||||
normalized,
|
||||
timeoutMs,
|
||||
fetcher,
|
||||
)) ?? undefined;
|
||||
(await fetchDiscordApplicationSummary(normalized, timeoutMs, fetcher)) ?? undefined;
|
||||
}
|
||||
return { ...result, elapsedMs: Date.now() - started };
|
||||
} catch (err) {
|
||||
|
||||
@@ -38,26 +38,19 @@ export async function editChannelDiscord(
|
||||
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;
|
||||
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 = {},
|
||||
) {
|
||||
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 = {},
|
||||
) {
|
||||
export async function moveChannelDiscord(payload: DiscordChannelMove, opts: DiscordReactOpts = {}) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const body: Array<Record<string, unknown>> = [
|
||||
{
|
||||
@@ -80,10 +73,7 @@ export async function setChannelPermissionDiscord(
|
||||
};
|
||||
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 },
|
||||
);
|
||||
await rest.put(`/channels/${payload.channelId}/permissions/${payload.targetId}`, { body });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
|
||||
@@ -63,11 +63,7 @@ describe("sendMessageDiscord", () => {
|
||||
it("creates a thread", async () => {
|
||||
const { rest, postMock } = makeRest();
|
||||
postMock.mockResolvedValue({ id: "t1" });
|
||||
await createThreadDiscord(
|
||||
"chan1",
|
||||
{ name: "thread", messageId: "m1" },
|
||||
{ rest, token: "t" },
|
||||
);
|
||||
await createThreadDiscord("chan1", { name: "thread", messageId: "m1" }, { rest, token: "t" });
|
||||
expect(postMock).toHaveBeenCalledWith(
|
||||
Routes.threads("chan1", "m1"),
|
||||
expect.objectContaining({ body: { name: "thread" } }),
|
||||
@@ -102,20 +98,10 @@ describe("sendMessageDiscord", () => {
|
||||
const { rest, putMock, deleteMock } = makeRest();
|
||||
putMock.mockResolvedValue({});
|
||||
deleteMock.mockResolvedValue({});
|
||||
await addRoleDiscord(
|
||||
{ guildId: "g1", userId: "u1", roleId: "r1" },
|
||||
{ rest, token: "t" },
|
||||
);
|
||||
await removeRoleDiscord(
|
||||
{ guildId: "g1", userId: "u1", roleId: "r1" },
|
||||
{ rest, token: "t" },
|
||||
);
|
||||
expect(putMock).toHaveBeenCalledWith(
|
||||
Routes.guildMemberRole("g1", "u1", "r1"),
|
||||
);
|
||||
expect(deleteMock).toHaveBeenCalledWith(
|
||||
Routes.guildMemberRole("g1", "u1", "r1"),
|
||||
);
|
||||
await addRoleDiscord({ guildId: "g1", userId: "u1", roleId: "r1" }, { rest, token: "t" });
|
||||
await removeRoleDiscord({ guildId: "g1", userId: "u1", roleId: "r1" }, { rest, token: "t" });
|
||||
expect(putMock).toHaveBeenCalledWith(Routes.guildMemberRole("g1", "u1", "r1"));
|
||||
expect(deleteMock).toHaveBeenCalledWith(Routes.guildMemberRole("g1", "u1", "r1"));
|
||||
});
|
||||
|
||||
it("bans a member", async () => {
|
||||
@@ -264,10 +250,7 @@ describe("sendPollDiscord", () => {
|
||||
body: expect.objectContaining({
|
||||
poll: {
|
||||
question: { text: "Lunch?" },
|
||||
answers: [
|
||||
{ poll_media: { text: "Pizza" } },
|
||||
{ poll_media: { text: "Sushi" } },
|
||||
],
|
||||
answers: [{ poll_media: { text: "Pizza" } }, { poll_media: { text: "Sushi" } }],
|
||||
duration: 24,
|
||||
allow_multiselect: false,
|
||||
layout_type: 1,
|
||||
@@ -362,9 +345,9 @@ describe("retry rate limits", () => {
|
||||
const { rest, postMock } = makeRest();
|
||||
postMock.mockRejectedValueOnce(new Error("network error"));
|
||||
|
||||
await expect(
|
||||
sendMessageDiscord("channel:789", "hello", { rest, token: "t" }),
|
||||
).rejects.toThrow("network error");
|
||||
await expect(sendMessageDiscord("channel:789", "hello", { rest, token: "t" })).rejects.toThrow(
|
||||
"network error",
|
||||
);
|
||||
expect(postMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -372,9 +355,7 @@ describe("retry rate limits", () => {
|
||||
const { rest, putMock } = makeRest();
|
||||
const rateLimitError = createMockRateLimitError(0);
|
||||
|
||||
putMock
|
||||
.mockRejectedValueOnce(rateLimitError)
|
||||
.mockResolvedValueOnce(undefined);
|
||||
putMock.mockRejectedValueOnce(rateLimitError).mockResolvedValueOnce(undefined);
|
||||
|
||||
const res = await reactMessageDiscord("chan1", "msg1", "ok", {
|
||||
rest,
|
||||
|
||||
@@ -2,33 +2,17 @@ import { Routes } from "discord-api-types/v10";
|
||||
|
||||
import { loadWebMediaRaw } from "../web/media.js";
|
||||
import { normalizeEmojiName, resolveDiscordRest } from "./send.shared.js";
|
||||
import type {
|
||||
DiscordEmojiUpload,
|
||||
DiscordReactOpts,
|
||||
DiscordStickerUpload,
|
||||
} from "./send.types.js";
|
||||
import {
|
||||
DISCORD_MAX_EMOJI_BYTES,
|
||||
DISCORD_MAX_STICKER_BYTES,
|
||||
} from "./send.types.js";
|
||||
import type { DiscordEmojiUpload, DiscordReactOpts, DiscordStickerUpload } from "./send.types.js";
|
||||
import { DISCORD_MAX_EMOJI_BYTES, DISCORD_MAX_STICKER_BYTES } from "./send.types.js";
|
||||
|
||||
export async function listGuildEmojisDiscord(
|
||||
guildId: string,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
export async function listGuildEmojisDiscord(guildId: string, opts: DiscordReactOpts = {}) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return await rest.get(Routes.guildEmojis(guildId));
|
||||
}
|
||||
|
||||
export async function uploadEmojiDiscord(
|
||||
payload: DiscordEmojiUpload,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
export async function uploadEmojiDiscord(payload: DiscordEmojiUpload, opts: DiscordReactOpts = {}) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const media = await loadWebMediaRaw(
|
||||
payload.mediaUrl,
|
||||
DISCORD_MAX_EMOJI_BYTES,
|
||||
);
|
||||
const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_EMOJI_BYTES);
|
||||
const contentType = media.contentType?.toLowerCase();
|
||||
if (
|
||||
!contentType ||
|
||||
@@ -37,9 +21,7 @@ export async function uploadEmojiDiscord(
|
||||
throw new Error("Discord emoji uploads require a PNG, JPG, or GIF image");
|
||||
}
|
||||
const image = `data:${contentType};base64,${media.buffer.toString("base64")}`;
|
||||
const roleIds = (payload.roleIds ?? [])
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean);
|
||||
const roleIds = (payload.roleIds ?? []).map((id) => id.trim()).filter(Boolean);
|
||||
return await rest.post(Routes.guildEmojis(payload.guildId), {
|
||||
body: {
|
||||
name: normalizeEmojiName(payload.name, "Emoji name"),
|
||||
@@ -54,26 +36,15 @@ export async function uploadStickerDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const media = await loadWebMediaRaw(
|
||||
payload.mediaUrl,
|
||||
DISCORD_MAX_STICKER_BYTES,
|
||||
);
|
||||
const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_STICKER_BYTES);
|
||||
const contentType = media.contentType?.toLowerCase();
|
||||
if (
|
||||
!contentType ||
|
||||
!["image/png", "image/apng", "application/json"].includes(contentType)
|
||||
) {
|
||||
throw new Error(
|
||||
"Discord sticker uploads require a PNG, APNG, or Lottie JSON file",
|
||||
);
|
||||
if (!contentType || !["image/png", "image/apng", "application/json"].includes(contentType)) {
|
||||
throw new Error("Discord sticker uploads require a PNG, APNG, or Lottie JSON file");
|
||||
}
|
||||
return await rest.post(Routes.guildStickers(payload.guildId), {
|
||||
body: {
|
||||
name: normalizeEmojiName(payload.name, "Sticker name"),
|
||||
description: normalizeEmojiName(
|
||||
payload.description,
|
||||
"Sticker description",
|
||||
),
|
||||
description: normalizeEmojiName(payload.description, "Sticker description"),
|
||||
tags: normalizeEmojiName(payload.tags, "Sticker tags"),
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -21,9 +21,7 @@ export async function fetchMemberInfoDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIGuildMember> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(
|
||||
Routes.guildMember(guildId, userId),
|
||||
)) as APIGuildMember;
|
||||
return (await rest.get(Routes.guildMember(guildId, userId))) as APIGuildMember;
|
||||
}
|
||||
|
||||
export async function fetchRoleInfoDiscord(
|
||||
@@ -34,25 +32,15 @@ export async function fetchRoleInfoDiscord(
|
||||
return (await rest.get(Routes.guildRoles(guildId))) as APIRole[];
|
||||
}
|
||||
|
||||
export async function addRoleDiscord(
|
||||
payload: DiscordRoleChange,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
export async function addRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts = {}) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.put(
|
||||
Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId),
|
||||
);
|
||||
await rest.put(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function removeRoleDiscord(
|
||||
payload: DiscordRoleChange,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
export async function removeRoleDiscord(payload: DiscordRoleChange, opts: DiscordReactOpts = {}) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
await rest.delete(
|
||||
Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId),
|
||||
);
|
||||
await rest.delete(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -78,9 +66,7 @@ export async function fetchVoiceStatusDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIVoiceState> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(
|
||||
Routes.guildVoiceState(guildId, userId),
|
||||
)) as APIVoiceState;
|
||||
return (await rest.get(Routes.guildVoiceState(guildId, userId))) as APIVoiceState;
|
||||
}
|
||||
|
||||
export async function listScheduledEventsDiscord(
|
||||
@@ -88,9 +74,7 @@ export async function listScheduledEventsDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIGuildScheduledEvent[]> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(
|
||||
Routes.guildScheduledEvents(guildId),
|
||||
)) as APIGuildScheduledEvent[];
|
||||
return (await rest.get(Routes.guildScheduledEvents(guildId))) as APIGuildScheduledEvent[];
|
||||
}
|
||||
|
||||
export async function createScheduledEventDiscord(
|
||||
@@ -114,15 +98,12 @@ export async function timeoutMemberDiscord(
|
||||
const ms = payload.durationMinutes * 60 * 1000;
|
||||
until = new Date(Date.now() + ms).toISOString();
|
||||
}
|
||||
return (await rest.patch(
|
||||
Routes.guildMember(payload.guildId, payload.userId),
|
||||
{
|
||||
body: { communication_disabled_until: until ?? null },
|
||||
headers: payload.reason
|
||||
? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
|
||||
: undefined,
|
||||
},
|
||||
)) as APIGuildMember;
|
||||
return (await rest.patch(Routes.guildMember(payload.guildId, payload.userId), {
|
||||
body: { communication_disabled_until: until ?? null },
|
||||
headers: payload.reason
|
||||
? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
|
||||
: undefined,
|
||||
})) as APIGuildMember;
|
||||
}
|
||||
|
||||
export async function kickMemberDiscord(
|
||||
@@ -144,15 +125,11 @@ export async function banMemberDiscord(
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const deleteMessageDays =
|
||||
typeof payload.deleteMessageDays === "number" &&
|
||||
Number.isFinite(payload.deleteMessageDays)
|
||||
typeof payload.deleteMessageDays === "number" && Number.isFinite(payload.deleteMessageDays)
|
||||
? Math.min(Math.max(Math.floor(payload.deleteMessageDays), 0), 7)
|
||||
: undefined;
|
||||
await rest.put(Routes.guildBan(payload.guildId, payload.userId), {
|
||||
body:
|
||||
deleteMessageDays !== undefined
|
||||
? { delete_message_days: deleteMessageDays }
|
||||
: undefined,
|
||||
body: deleteMessageDays !== undefined ? { delete_message_days: deleteMessageDays } : undefined,
|
||||
headers: payload.reason
|
||||
? { "X-Audit-Log-Reason": encodeURIComponent(payload.reason) }
|
||||
: undefined,
|
||||
|
||||
@@ -25,10 +25,7 @@ export async function readMessagesDiscord(
|
||||
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[];
|
||||
return (await rest.get(Routes.channelMessages(channelId), params)) as APIMessage[];
|
||||
}
|
||||
|
||||
export async function fetchMessageDiscord(
|
||||
@@ -37,9 +34,7 @@ export async function fetchMessageDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<APIMessage> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
return (await rest.get(
|
||||
Routes.channelMessage(channelId, messageId),
|
||||
)) as APIMessage;
|
||||
return (await rest.get(Routes.channelMessage(channelId, messageId))) as APIMessage;
|
||||
}
|
||||
|
||||
export async function editMessageDiscord(
|
||||
@@ -106,10 +101,7 @@ export async function createThreadDiscord(
|
||||
return await rest.post(route, { body });
|
||||
}
|
||||
|
||||
export async function listThreadsDiscord(
|
||||
payload: DiscordThreadList,
|
||||
opts: DiscordReactOpts = {},
|
||||
) {
|
||||
export async function listThreadsDiscord(payload: DiscordThreadList, opts: DiscordReactOpts = {}) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
if (payload.includeArchived) {
|
||||
if (!payload.channelId) {
|
||||
@@ -118,10 +110,7 @@ export async function listThreadsDiscord(
|
||||
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.channelThreads(payload.channelId, "public"), params);
|
||||
}
|
||||
return await rest.get(Routes.guildActiveThreads(payload.guildId));
|
||||
}
|
||||
@@ -147,7 +136,5 @@ 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?${params.toString()}`,
|
||||
);
|
||||
return await rest.get(`/guilds/${query.guildId}/messages/search?${params.toString()}`);
|
||||
}
|
||||
|
||||
@@ -40,9 +40,7 @@ export async function sendMessageDiscord(
|
||||
const { token, rest, request } = createDiscordClient(opts, cfg);
|
||||
const recipient = parseRecipient(to);
|
||||
const { channelId } = await resolveChannelId(rest, recipient, request);
|
||||
let result:
|
||||
| { id: string; channel_id: string }
|
||||
| { id: string | null; channel_id: string };
|
||||
let result: { id: string; channel_id: string } | { id: string | null; channel_id: string };
|
||||
try {
|
||||
if (opts.mediaUrl) {
|
||||
result = await sendDiscordMedia(
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
import { RequestClient } from "@buape/carbon";
|
||||
import type {
|
||||
APIChannel,
|
||||
APIGuild,
|
||||
APIGuildMember,
|
||||
APIRole,
|
||||
} from "discord-api-types/v10";
|
||||
import {
|
||||
ChannelType,
|
||||
PermissionFlagsBits,
|
||||
Routes,
|
||||
} from "discord-api-types/v10";
|
||||
import type { APIChannel, APIGuild, APIGuildMember, APIRole } from "discord-api-types/v10";
|
||||
import { ChannelType, PermissionFlagsBits, Routes } from "discord-api-types/v10";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { RetryConfig } from "../infra/retry.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import type {
|
||||
DiscordPermissionsSummary,
|
||||
DiscordReactOpts,
|
||||
} from "./send.types.js";
|
||||
import type { DiscordPermissionsSummary, DiscordReactOpts } from "./send.types.js";
|
||||
import { normalizeDiscordToken } from "./token.js";
|
||||
|
||||
const PERMISSION_ENTRIES = Object.entries(PermissionFlagsBits).filter(
|
||||
@@ -32,11 +20,7 @@ type DiscordClientOpts = {
|
||||
verbose?: boolean;
|
||||
};
|
||||
|
||||
function resolveToken(params: {
|
||||
explicit?: string;
|
||||
accountId: string;
|
||||
fallbackToken?: string;
|
||||
}) {
|
||||
function resolveToken(params: { explicit?: string; accountId: string; fallbackToken?: string }) {
|
||||
const explicit = normalizeDiscordToken(params.explicit);
|
||||
if (explicit) return explicit;
|
||||
const fallback = normalizeDiscordToken(params.fallbackToken);
|
||||
@@ -119,9 +103,7 @@ export async function fetchChannelPermissionsDiscord(
|
||||
rest.get(Routes.guildMember(guildId, botId)) as Promise<APIGuildMember>,
|
||||
]);
|
||||
|
||||
const rolesById = new Map<string, APIRole>(
|
||||
(guild.roles ?? []).map((role) => [role.id, role]),
|
||||
);
|
||||
const rolesById = new Map<string, APIRole>((guild.roles ?? []).map((role) => [role.id, role]));
|
||||
const everyoneRole = rolesById.get(guildId);
|
||||
let base = 0n;
|
||||
if (everyoneRole?.permissions) {
|
||||
@@ -136,9 +118,7 @@ export async function fetchChannelPermissionsDiscord(
|
||||
|
||||
let permissions = base;
|
||||
const overwrites =
|
||||
"permission_overwrites" in channel
|
||||
? (channel.permission_overwrites ?? [])
|
||||
: [];
|
||||
"permission_overwrites" in channel ? (channel.permission_overwrites ?? []) : [];
|
||||
for (const overwrite of overwrites) {
|
||||
if (overwrite.id === guildId) {
|
||||
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
|
||||
|
||||
@@ -20,8 +20,7 @@ export async function reactMessageDiscord(
|
||||
const { rest, request } = createDiscordClient(opts, cfg);
|
||||
const encoded = normalizeReactionEmoji(emoji);
|
||||
await request(
|
||||
() =>
|
||||
rest.put(Routes.channelMessageOwnReaction(channelId, messageId, encoded)),
|
||||
() => rest.put(Routes.channelMessageOwnReaction(channelId, messageId, encoded)),
|
||||
"react",
|
||||
);
|
||||
return { ok: true };
|
||||
@@ -35,9 +34,7 @@ export async function removeReactionDiscord(
|
||||
) {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const encoded = normalizeReactionEmoji(emoji);
|
||||
await rest.delete(
|
||||
Routes.channelMessageOwnReaction(channelId, messageId, encoded),
|
||||
);
|
||||
await rest.delete(Routes.channelMessageOwnReaction(channelId, messageId, encoded));
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -47,9 +44,7 @@ export async function removeOwnReactionsDiscord(
|
||||
opts: DiscordReactOpts = {},
|
||||
): Promise<{ ok: true; removed: string[] }> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const message = (await rest.get(
|
||||
Routes.channelMessage(channelId, messageId),
|
||||
)) as {
|
||||
const message = (await rest.get(Routes.channelMessage(channelId, messageId))) as {
|
||||
reactions?: Array<{ emoji: { id?: string | null; name?: string | null } }>;
|
||||
};
|
||||
const identifiers = new Set<string>();
|
||||
@@ -63,11 +58,7 @@ export async function removeOwnReactionsDiscord(
|
||||
Array.from(identifiers, (identifier) => {
|
||||
removed.push(identifier);
|
||||
return rest.delete(
|
||||
Routes.channelMessageOwnReaction(
|
||||
channelId,
|
||||
messageId,
|
||||
normalizeReactionEmoji(identifier),
|
||||
),
|
||||
Routes.channelMessageOwnReaction(channelId, messageId, normalizeReactionEmoji(identifier)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -80,9 +71,7 @@ export async function fetchReactionsDiscord(
|
||||
opts: DiscordReactOpts & { limit?: number } = {},
|
||||
): Promise<DiscordReactionSummary[]> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const message = (await rest.get(
|
||||
Routes.channelMessage(channelId, messageId),
|
||||
)) as {
|
||||
const message = (await rest.get(Routes.channelMessage(channelId, messageId))) as {
|
||||
reactions?: Array<{
|
||||
count: number;
|
||||
emoji: { id?: string | null; name?: string | null };
|
||||
@@ -100,10 +89,9 @@ export async function fetchReactionsDiscord(
|
||||
const identifier = buildReactionIdentifier(reaction.emoji);
|
||||
if (!identifier) continue;
|
||||
const encoded = encodeURIComponent(identifier);
|
||||
const users = (await rest.get(
|
||||
Routes.channelMessageReaction(channelId, messageId, encoded),
|
||||
{ limit },
|
||||
)) as Array<{ id: string; username?: string; discriminator?: string }>;
|
||||
const users = (await rest.get(Routes.channelMessageReaction(channelId, messageId, encoded), {
|
||||
limit,
|
||||
})) as Array<{ id: string; username?: string; discriminator?: string }>;
|
||||
summaries.push({
|
||||
emoji: {
|
||||
id: reaction.emoji.id ?? null,
|
||||
|
||||
@@ -283,9 +283,7 @@ describe("fetchReactionsDiscord", () => {
|
||||
{ count: 1, emoji: { name: "party_blob", id: "123" } },
|
||||
],
|
||||
})
|
||||
.mockResolvedValueOnce([
|
||||
{ id: "u1", username: "alpha", discriminator: "0001" },
|
||||
])
|
||||
.mockResolvedValueOnce([{ id: "u1", username: "alpha", discriminator: "0001" }])
|
||||
.mockResolvedValueOnce([{ id: "u2", username: "beta" }]);
|
||||
const res = await fetchReactionsDiscord("chan1", "msg1", {
|
||||
rest,
|
||||
@@ -313,8 +311,7 @@ describe("fetchChannelPermissionsDiscord", () => {
|
||||
|
||||
it("calculates permissions from guild roles", async () => {
|
||||
const { rest, getMock } = makeRest();
|
||||
const perms =
|
||||
PermissionFlagsBits.ViewChannel | PermissionFlagsBits.SendMessages;
|
||||
const perms = PermissionFlagsBits.ViewChannel | PermissionFlagsBits.SendMessages;
|
||||
getMock
|
||||
.mockResolvedValueOnce({
|
||||
id: "chan1",
|
||||
@@ -349,11 +346,7 @@ describe("readMessagesDiscord", () => {
|
||||
it("passes query params as an object", async () => {
|
||||
const { rest, getMock } = makeRest();
|
||||
getMock.mockResolvedValue([]);
|
||||
await readMessagesDiscord(
|
||||
"chan1",
|
||||
{ limit: 5, before: "10" },
|
||||
{ rest, token: "t" },
|
||||
);
|
||||
await readMessagesDiscord("chan1", { limit: 5, before: "10" }, { rest, token: "t" });
|
||||
const call = getMock.mock.calls[0];
|
||||
const options = call?.[1] as Record<string, unknown>;
|
||||
expect(options).toEqual({ limit: 5, before: "10" });
|
||||
@@ -368,12 +361,7 @@ describe("edit/delete message helpers", () => {
|
||||
it("edits message content", async () => {
|
||||
const { rest, patchMock } = makeRest();
|
||||
patchMock.mockResolvedValue({ id: "m1" });
|
||||
await editMessageDiscord(
|
||||
"chan1",
|
||||
"m1",
|
||||
{ content: "hello" },
|
||||
{ rest, token: "t" },
|
||||
);
|
||||
await editMessageDiscord("chan1", "m1", { content: "hello" }, { rest, token: "t" });
|
||||
expect(patchMock).toHaveBeenCalledWith(
|
||||
Routes.channelMessage("chan1", "m1"),
|
||||
expect.objectContaining({ body: { content: "hello" } }),
|
||||
@@ -384,9 +372,7 @@ describe("edit/delete message helpers", () => {
|
||||
const { rest, deleteMock } = makeRest();
|
||||
deleteMock.mockResolvedValue({});
|
||||
await deleteMessageDiscord("chan1", "m1", { rest, token: "t" });
|
||||
expect(deleteMock).toHaveBeenCalledWith(
|
||||
Routes.channelMessage("chan1", "m1"),
|
||||
);
|
||||
expect(deleteMock).toHaveBeenCalledWith(Routes.channelMessage("chan1", "m1"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,22 +5,12 @@ import { Routes } from "discord-api-types/v10";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { RetryConfig } from "../infra/retry.js";
|
||||
import {
|
||||
createDiscordRetryRunner,
|
||||
type RetryRunner,
|
||||
} from "../infra/retry-policy.js";
|
||||
import {
|
||||
normalizePollDurationHours,
|
||||
normalizePollInput,
|
||||
type PollInput,
|
||||
} from "../polls.js";
|
||||
import { createDiscordRetryRunner, type RetryRunner } from "../infra/retry-policy.js";
|
||||
import { normalizePollDurationHours, normalizePollInput, type PollInput } from "../polls.js";
|
||||
import { loadWebMedia } from "../web/media.js";
|
||||
import { resolveDiscordAccount } from "./accounts.js";
|
||||
import { chunkDiscordText } from "./chunk.js";
|
||||
import {
|
||||
fetchChannelPermissionsDiscord,
|
||||
isThreadChannelType,
|
||||
} from "./send.permissions.js";
|
||||
import { fetchChannelPermissionsDiscord, isThreadChannelType } from "./send.permissions.js";
|
||||
import { DiscordSendError } from "./send.types.js";
|
||||
import { normalizeDiscordToken } from "./token.js";
|
||||
|
||||
@@ -51,11 +41,7 @@ type DiscordClientOpts = {
|
||||
verbose?: boolean;
|
||||
};
|
||||
|
||||
function resolveToken(params: {
|
||||
explicit?: string;
|
||||
accountId: string;
|
||||
fallbackToken?: string;
|
||||
}) {
|
||||
function resolveToken(params: { explicit?: string; accountId: string; fallbackToken?: string }) {
|
||||
const explicit = normalizeDiscordToken(params.explicit);
|
||||
if (explicit) return explicit;
|
||||
const fallback = normalizeDiscordToken(params.fallbackToken);
|
||||
@@ -124,9 +110,7 @@ function parseRecipient(raw: string): DiscordRecipient {
|
||||
if (trimmed.startsWith("@")) {
|
||||
const candidate = trimmed.slice(1);
|
||||
if (!/^\d+$/.test(candidate)) {
|
||||
throw new Error(
|
||||
"Discord DMs require a user id (use user:<id> or a <@id> mention)",
|
||||
);
|
||||
throw new Error("Discord DMs require a user id (use user:<id> or a <@id> mention)");
|
||||
}
|
||||
return { kind: "user", id: candidate };
|
||||
}
|
||||
@@ -272,9 +256,7 @@ async function sendDiscordText(
|
||||
if (!text.trim()) {
|
||||
throw new Error("Message must be non-empty for Discord sends");
|
||||
}
|
||||
const messageReference = replyTo
|
||||
? { message_id: replyTo, fail_if_not_exists: false }
|
||||
: undefined;
|
||||
const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined;
|
||||
const chunks = chunkDiscordText(text, {
|
||||
maxChars: DISCORD_TEXT_LIMIT,
|
||||
maxLines: maxLinesPerMessage,
|
||||
@@ -327,9 +309,7 @@ async function sendDiscordMedia(
|
||||
})
|
||||
: [];
|
||||
const caption = chunks[0] ?? "";
|
||||
const messageReference = replyTo
|
||||
? { message_id: replyTo, fail_if_not_exists: false }
|
||||
: undefined;
|
||||
const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined;
|
||||
const res = (await request(
|
||||
() =>
|
||||
rest.post(Routes.channelMessages(channelId), {
|
||||
@@ -348,32 +328,19 @@ async function sendDiscordMedia(
|
||||
)) as { id: string; channel_id: string };
|
||||
for (const chunk of chunks.slice(1)) {
|
||||
if (!chunk.trim()) continue;
|
||||
await sendDiscordText(
|
||||
rest,
|
||||
channelId,
|
||||
chunk,
|
||||
undefined,
|
||||
request,
|
||||
maxLinesPerMessage,
|
||||
);
|
||||
await sendDiscordText(rest, channelId, chunk, undefined, request, maxLinesPerMessage);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function buildReactionIdentifier(emoji: {
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
}) {
|
||||
function buildReactionIdentifier(emoji: { id?: string | null; name?: string | null }) {
|
||||
if (emoji.id && emoji.name) {
|
||||
return `${emoji.name}:${emoji.id}`;
|
||||
}
|
||||
return emoji.name ?? "";
|
||||
}
|
||||
|
||||
function formatReactionEmoji(emoji: {
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
}) {
|
||||
function formatReactionEmoji(emoji: { id?: string | null; name?: string | null }) {
|
||||
return buildReactionIdentifier(emoji);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,11 +37,7 @@ export {
|
||||
searchMessagesDiscord,
|
||||
unpinMessageDiscord,
|
||||
} from "./send.messages.js";
|
||||
export {
|
||||
sendMessageDiscord,
|
||||
sendPollDiscord,
|
||||
sendStickerDiscord,
|
||||
} from "./send.outbound.js";
|
||||
export { sendMessageDiscord, sendPollDiscord, sendStickerDiscord } from "./send.outbound.js";
|
||||
export {
|
||||
fetchChannelPermissionsDiscord,
|
||||
fetchReactionsDiscord,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
normalizeAccountId,
|
||||
} from "../routing/session-key.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
|
||||
export type DiscordTokenSource = "env" | "config" | "none";
|
||||
|
||||
@@ -37,9 +34,7 @@ export function resolveDiscordToken(
|
||||
: undefined;
|
||||
if (envToken) return { token: envToken, source: "env" };
|
||||
|
||||
const configToken = allowEnv
|
||||
? normalizeDiscordToken(discordCfg?.token ?? undefined)
|
||||
: undefined;
|
||||
const configToken = allowEnv ? normalizeDiscordToken(discordCfg?.token ?? undefined) : undefined;
|
||||
if (configToken) return { token: configToken, source: "config" };
|
||||
|
||||
return { token: "", source: "none" };
|
||||
|
||||
Reference in New Issue
Block a user