refactor(src): split oversized modules
This commit is contained in:
234
src/discord/monitor/threading.ts
Normal file
234
src/discord/monitor/threading.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
import { ChannelType, type Client } from "@buape/carbon";
|
||||
import { Routes } from "discord-api-types/v10";
|
||||
import { createReplyReferencePlanner } from "../../auto-reply/reply/reply-reference.js";
|
||||
import type { ReplyToMode } from "../../config/config.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { truncateUtf16Safe } from "../../utils.js";
|
||||
import type { DiscordChannelConfigResolved } from "./allow-list.js";
|
||||
import type { DiscordMessageEvent } from "./listeners.js";
|
||||
import { resolveDiscordChannelInfo } from "./message-utils.js";
|
||||
|
||||
export type DiscordThreadChannel = {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
parentId?: string | null;
|
||||
parent?: { id?: string; name?: string };
|
||||
};
|
||||
|
||||
export type DiscordThreadStarter = {
|
||||
text: string;
|
||||
author: string;
|
||||
timestamp?: number;
|
||||
};
|
||||
|
||||
type DiscordThreadParentInfo = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
type?: ChannelType;
|
||||
};
|
||||
|
||||
const DISCORD_THREAD_STARTER_CACHE = new Map<string, DiscordThreadStarter>();
|
||||
|
||||
function isDiscordThreadType(type: ChannelType | undefined): boolean {
|
||||
return (
|
||||
type === ChannelType.PublicThread ||
|
||||
type === ChannelType.PrivateThread ||
|
||||
type === ChannelType.AnnouncementThread
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDiscordThreadChannel(params: {
|
||||
isGuildMessage: boolean;
|
||||
message: DiscordMessageEvent["message"];
|
||||
channelInfo: import("./message-utils.js").DiscordChannelInfo | null;
|
||||
}): DiscordThreadChannel | null {
|
||||
if (!params.isGuildMessage) return null;
|
||||
const { message, channelInfo } = params;
|
||||
const channel =
|
||||
"channel" in message
|
||||
? (message as { channel?: unknown }).channel
|
||||
: undefined;
|
||||
const isThreadChannel =
|
||||
channel &&
|
||||
typeof channel === "object" &&
|
||||
"isThread" in channel &&
|
||||
typeof (channel as { isThread?: unknown }).isThread === "function" &&
|
||||
(channel as { isThread: () => boolean }).isThread();
|
||||
if (isThreadChannel) return channel as unknown as DiscordThreadChannel;
|
||||
if (!isDiscordThreadType(channelInfo?.type)) return null;
|
||||
return {
|
||||
id: message.channelId,
|
||||
name: channelInfo?.name ?? undefined,
|
||||
parentId: channelInfo?.parentId ?? undefined,
|
||||
parent: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveDiscordThreadParentInfo(params: {
|
||||
client: Client;
|
||||
threadChannel: DiscordThreadChannel;
|
||||
channelInfo: import("./message-utils.js").DiscordChannelInfo | null;
|
||||
}): Promise<DiscordThreadParentInfo> {
|
||||
const { threadChannel, channelInfo, client } = params;
|
||||
const parentId =
|
||||
threadChannel.parentId ??
|
||||
threadChannel.parent?.id ??
|
||||
channelInfo?.parentId ??
|
||||
undefined;
|
||||
if (!parentId) return {};
|
||||
let parentName = threadChannel.parent?.name;
|
||||
const parentInfo = await resolveDiscordChannelInfo(client, parentId);
|
||||
parentName = parentName ?? parentInfo?.name;
|
||||
const parentType = parentInfo?.type;
|
||||
return { id: parentId, name: parentName, type: parentType };
|
||||
}
|
||||
|
||||
export async function resolveDiscordThreadStarter(params: {
|
||||
channel: DiscordThreadChannel;
|
||||
client: Client;
|
||||
parentId?: string;
|
||||
parentType?: ChannelType;
|
||||
resolveTimestampMs: (value?: string | null) => number | undefined;
|
||||
}): Promise<DiscordThreadStarter | null> {
|
||||
const cacheKey = params.channel.id;
|
||||
const cached = DISCORD_THREAD_STARTER_CACHE.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
try {
|
||||
const parentType = params.parentType;
|
||||
const isForumParent =
|
||||
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),
|
||||
)) as {
|
||||
content?: string | null;
|
||||
embeds?: Array<{ description?: string | null }>;
|
||||
member?: { nick?: string | null; displayName?: string | null };
|
||||
author?: {
|
||||
id?: string | null;
|
||||
username?: string | null;
|
||||
discriminator?: string | null;
|
||||
};
|
||||
timestamp?: string | null;
|
||||
};
|
||||
if (!starter) return null;
|
||||
const text =
|
||||
starter.content?.trim() ?? starter.embeds?.[0]?.description?.trim() ?? "";
|
||||
if (!text) return null;
|
||||
const author =
|
||||
starter.member?.nick ??
|
||||
starter.member?.displayName ??
|
||||
(starter.author
|
||||
? starter.author.discriminator && starter.author.discriminator !== "0"
|
||||
? `${starter.author.username ?? "Unknown"}#${starter.author.discriminator}`
|
||||
: (starter.author.username ?? starter.author.id ?? "Unknown")
|
||||
: "Unknown");
|
||||
const timestamp = params.resolveTimestampMs(starter.timestamp);
|
||||
const payload: DiscordThreadStarter = {
|
||||
text,
|
||||
author,
|
||||
timestamp: timestamp ?? undefined,
|
||||
};
|
||||
DISCORD_THREAD_STARTER_CACHE.set(cacheKey, payload);
|
||||
return payload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDiscordReplyTarget(opts: {
|
||||
replyToMode: ReplyToMode;
|
||||
replyToId?: string;
|
||||
hasReplied: boolean;
|
||||
}): string | undefined {
|
||||
if (opts.replyToMode === "off") return undefined;
|
||||
const replyToId = opts.replyToId?.trim();
|
||||
if (!replyToId) return undefined;
|
||||
if (opts.replyToMode === "all") return replyToId;
|
||||
return opts.hasReplied ? undefined : replyToId;
|
||||
}
|
||||
|
||||
export function sanitizeDiscordThreadName(
|
||||
rawName: string,
|
||||
fallbackId: string,
|
||||
): string {
|
||||
const cleanedName = rawName
|
||||
.replace(/<@!?\d+>/g, "") // user mentions
|
||||
.replace(/<@&\d+>/g, "") // role mentions
|
||||
.replace(/<#\d+>/g, "") // channel mentions
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const baseSource = cleanedName || `Thread ${fallbackId}`;
|
||||
const base = truncateUtf16Safe(baseSource, 80);
|
||||
return truncateUtf16Safe(base, 100) || `Thread ${fallbackId}`;
|
||||
}
|
||||
|
||||
type DiscordReplyDeliveryPlan = {
|
||||
deliverTarget: string;
|
||||
replyTarget: string;
|
||||
replyReference: ReturnType<typeof createReplyReferencePlanner>;
|
||||
};
|
||||
|
||||
export async function maybeCreateDiscordAutoThread(params: {
|
||||
client: Client;
|
||||
message: DiscordMessageEvent["message"];
|
||||
isGuildMessage: boolean;
|
||||
channelConfig?: DiscordChannelConfigResolved | null;
|
||||
threadChannel?: DiscordThreadChannel | null;
|
||||
baseText: string;
|
||||
combinedBody: string;
|
||||
}): Promise<string | undefined> {
|
||||
if (!params.isGuildMessage) return undefined;
|
||||
if (!params.channelConfig?.autoThread) return undefined;
|
||||
if (params.threadChannel) return undefined;
|
||||
try {
|
||||
const threadName = sanitizeDiscordThreadName(
|
||||
params.baseText || params.combinedBody || "Thread",
|
||||
params.message.id,
|
||||
);
|
||||
const created = (await params.client.rest.post(
|
||||
`${Routes.channelMessage(params.message.channelId, params.message.id)}/threads`,
|
||||
{
|
||||
body: {
|
||||
name: threadName,
|
||||
auto_archive_duration: 60,
|
||||
},
|
||||
},
|
||||
)) as { id?: string };
|
||||
const createdId = created?.id ? String(created.id) : "";
|
||||
return createdId || undefined;
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`discord: autoThread failed for ${params.message.channelId}/${params.message.id}: ${String(err)}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveDiscordReplyDeliveryPlan(params: {
|
||||
replyTarget: string;
|
||||
replyToMode: ReplyToMode;
|
||||
messageId: string;
|
||||
threadChannel?: DiscordThreadChannel | null;
|
||||
createdThreadId?: string | null;
|
||||
}): DiscordReplyDeliveryPlan {
|
||||
const originalReplyTarget = params.replyTarget;
|
||||
let deliverTarget = originalReplyTarget;
|
||||
let replyTarget = originalReplyTarget;
|
||||
if (params.createdThreadId) {
|
||||
deliverTarget = `channel:${params.createdThreadId}`;
|
||||
replyTarget = deliverTarget;
|
||||
}
|
||||
const allowReference = deliverTarget === originalReplyTarget;
|
||||
const replyReference = createReplyReferencePlanner({
|
||||
replyToMode: allowReference ? params.replyToMode : "off",
|
||||
existingId: params.threadChannel ? params.messageId : undefined,
|
||||
startId: params.messageId,
|
||||
allowReference,
|
||||
});
|
||||
return { deliverTarget, replyTarget, replyReference };
|
||||
}
|
||||
Reference in New Issue
Block a user