chore: migrate to oxlint and oxfmt

Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-14 14:31:43 +00:00
parent 912ebffc63
commit c379191f80
1480 changed files with 28608 additions and 43547 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
? {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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