import { buildMessagingTarget, ensureTargetId, requireTargetKind, type MessagingTarget, type MessagingTargetKind, type MessagingTargetParseOptions, } from "../channels/targets.js"; import type { DirectoryConfigParams } from "../channels/plugins/directory-config.js"; import { listDiscordDirectoryPeersLive } from "./directory-live.js"; export type DiscordTargetKind = MessagingTargetKind; export type DiscordTarget = MessagingTarget; type DiscordTargetParseOptions = MessagingTargetParseOptions; export function parseDiscordTarget( raw: string, options: DiscordTargetParseOptions = {}, ): DiscordTarget | undefined { const trimmed = raw.trim(); if (!trimmed) return undefined; const mentionMatch = trimmed.match(/^<@!?(\d+)>$/); if (mentionMatch) { return buildMessagingTarget("user", mentionMatch[1], trimmed); } if (trimmed.startsWith("user:")) { const id = trimmed.slice("user:".length).trim(); return id ? buildMessagingTarget("user", id, trimmed) : undefined; } if (trimmed.startsWith("channel:")) { const id = trimmed.slice("channel:".length).trim(); return id ? buildMessagingTarget("channel", id, trimmed) : undefined; } if (trimmed.startsWith("discord:")) { const id = trimmed.slice("discord:".length).trim(); return id ? buildMessagingTarget("user", id, trimmed) : undefined; } if (trimmed.startsWith("@")) { const candidate = trimmed.slice(1).trim(); const id = ensureTargetId({ candidate, pattern: /^\d+$/, errorMessage: "Discord DMs require a user id (use user: or a <@id> mention)", }); return buildMessagingTarget("user", id, trimmed); } if (/^\d+$/.test(trimmed)) { if (options.defaultKind) { return buildMessagingTarget(options.defaultKind, trimmed, trimmed); } throw new Error( options.ambiguousMessage ?? `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`, ); } return buildMessagingTarget("channel", trimmed, trimmed); } export function resolveDiscordChannelId(raw: string): string { const target = parseDiscordTarget(raw, { defaultKind: "channel" }); return requireTargetKind({ platform: "Discord", target, kind: "channel" }); } /** * Resolve a Discord username to user ID using the directory lookup. * This enables sending DMs by username instead of requiring explicit user IDs. * * @param raw - The username or raw target string (e.g., "john.doe") * @param options - Directory configuration params (cfg, accountId, limit) * @returns Parsed MessagingTarget with user ID, or undefined if not found */ export async function resolveDiscordTarget( raw: string, options: DirectoryConfigParams, ): Promise { const trimmed = raw.trim(); if (!trimmed) return undefined; const shouldLookup = isExplicitUserLookup(trimmed, options); const directParse = safeParseDiscordTarget(trimmed, options); if (directParse && directParse.kind !== "channel") { return directParse; } if (!shouldLookup) { return directParse ?? parseDiscordTarget(trimmed, options); } // Try to resolve as a username via directory lookup try { const directoryEntries = await listDiscordDirectoryPeersLive({ ...options, query: trimmed, limit: 1, }); const match = directoryEntries[0]; if (match && match.kind === "user") { // Extract user ID from the directory entry (format: "user:") const userId = match.id.replace(/^user:/, ""); return buildMessagingTarget("user", userId, trimmed); } } catch { // Directory lookup failed - fall through to parse as-is // This preserves existing behavior for channel names } // Fallback to original parsing (for channels, etc.) return parseDiscordTarget(trimmed, options); } function safeParseDiscordTarget( input: string, options: DiscordTargetParseOptions, ): MessagingTarget | undefined { try { return parseDiscordTarget(input, options); } catch { return undefined; } } function isExplicitUserLookup(input: string, options: DiscordTargetParseOptions): boolean { if (/^<@!?(\d+)>$/.test(input)) { return true; } if (/^(user:|discord:)/.test(input)) { return true; } if (input.startsWith("@")) { return true; } if (/^\d+$/.test(input)) { return options.defaultKind === "user"; } return false; }