fix: resolve Discord usernames to user IDs for outbound messages

When sending Discord messages via cron jobs or the message tool,
usernames like "john.doe" were incorrectly treated as channel names,
causing silent delivery failures.

This fix adds a resolveDiscordTarget() function that:
- Queries Discord directory to resolve usernames to user IDs
- Falls back to standard parsing for known formats
- Enables sending DMs by username without requiring explicit user:ID format

Changes:
- Added resolveDiscordTarget() in targets.ts with directory lookup
- Added parseAndResolveRecipient() in send.shared.ts
- Updated all outbound send functions to use username resolution

Fixes #2627
This commit is contained in:
nonggia.liang
2026-01-27 15:55:53 +08:00
committed by Shadow
parent 57d9c09f6e
commit 7958ead91a
3 changed files with 105 additions and 5 deletions

View File

@@ -5,8 +5,13 @@ import {
type MessagingTarget,
type MessagingTargetKind,
type MessagingTargetParseOptions,
type DirectoryConfigParams,
type ChannelDirectoryEntry,
} from "../channels/targets.js";
import { listDiscordDirectoryPeersLive } from "./directory-live.js";
import { resolveDiscordAccount } from "./accounts.js";
export type DiscordTargetKind = MessagingTargetKind;
export type DiscordTarget = MessagingTarget;
@@ -60,3 +65,60 @@ 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<MessagingTarget | undefined> {
const trimmed = raw.trim();
if (!trimmed) return undefined;
// If already a known format, parse directly
const directParse = parseDiscordTarget(trimmed, options);
if (directParse && directParse.kind !== "channel" && !isLikelyUsername(trimmed)) {
return directParse;
}
// 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:<id>")
const userId = match.id.replace(/^user:/, "");
return buildMessagingTarget("user", userId, trimmed);
}
} catch (error) {
// 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);
}
/**
* Check if a string looks like a Discord username (not a mention, prefix, or ID).
* Usernames typically don't start with special characters except underscore.
*/
function isLikelyUsername(input: string): boolean {
// Skip if it's already a known format
if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) {
return false;
}
// Likely a username if it doesn't match known patterns
return true;
}