Discord: add PluralKit sender identity resolver (#5838)

* Discord: add PluralKit sender identity resolver

* fix: resolve PluralKit sender identities (#5838) (thanks @thewilloftheshadow)
This commit is contained in:
Shadow
2026-01-31 19:50:06 -06:00
committed by GitHub
parent 66e33abd7b
commit 8e2b17e0c5
15 changed files with 354 additions and 55 deletions

View File

@@ -1,8 +1,5 @@
import { ChannelType, MessageType, type User } from "@buape/carbon";
import type {
DiscordMessagePreflightContext,
DiscordMessagePreflightParams,
} from "./message-handler.preflight.types.js";
import { hasControlCommand } from "../../auto-reply/command-detection.js";
import { shouldHandleTextCommands } from "../../auto-reply/commands-registry.js";
import {
@@ -27,6 +24,7 @@ import {
upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { fetchPluralKitMessageInfo } from "../pluralkit.js";
import { sendMessageDiscord } from "../send.js";
import {
allowListMatches,
@@ -45,13 +43,19 @@ import {
resolveDiscordSystemLocation,
resolveTimestampMs,
} from "./format.js";
import type {
DiscordMessagePreflightContext,
DiscordMessagePreflightParams,
} from "./message-handler.preflight.types.js";
import { resolveDiscordChannelInfo, resolveDiscordMessageText } from "./message-utils.js";
import { resolveDiscordSenderIdentity, resolveDiscordWebhookId } from "./sender-identity.js";
import { resolveDiscordSystemEvent } from "./system-events.js";
import { resolveDiscordThreadChannel, resolveDiscordThreadParentInfo } from "./threading.js";
export type {
DiscordMessagePreflightContext,
DiscordMessagePreflightParams,
DiscordSenderIdentity,
} from "./message-handler.preflight.types.js";
export async function preflightDiscordMessage(
@@ -65,12 +69,33 @@ export async function preflightDiscordMessage(
}
const allowBots = params.discordConfig?.allowBots ?? false;
if (author.bot) {
if (author.bot && params.botUserId && author.id === params.botUserId) {
// Always ignore own messages to prevent self-reply loops
if (params.botUserId && author.id === params.botUserId) {
return null;
return null;
}
const pluralkitConfig = params.discordConfig?.pluralkit;
const webhookId = resolveDiscordWebhookId(message);
const shouldCheckPluralKit = Boolean(pluralkitConfig?.enabled) && !webhookId;
let pluralkitInfo: Awaited<ReturnType<typeof fetchPluralKitMessageInfo>> = null;
if (shouldCheckPluralKit) {
try {
pluralkitInfo = await fetchPluralKitMessageInfo({
messageId: message.id,
config: pluralkitConfig,
});
} catch (err) {
logVerbose(`discord: pluralkit lookup failed for ${message.id}: ${String(err)}`);
}
if (!allowBots) {
}
const sender = resolveDiscordSenderIdentity({
author,
member: params.data.member,
pluralkitInfo,
});
if (author.bot) {
if (!allowBots && !sender.isPluralKit) {
logVerbose("discord: drop bot message (allowBots=false)");
return null;
}
@@ -100,14 +125,14 @@ export async function preflightDiscordMessage(
if (dmPolicy !== "open") {
const storeAllowFrom = await readChannelAllowFromStore("discord").catch(() => []);
const effectiveAllowFrom = [...(params.allowFrom ?? []), ...storeAllowFrom];
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:"]);
const allowList = normalizeDiscordAllowList(effectiveAllowFrom, ["discord:", "user:", "pk:"]);
const allowMatch = allowList
? resolveDiscordAllowListMatch({
allowList,
candidate: {
id: author.id,
name: author.username,
tag: formatDiscordUserTag(author),
id: sender.id,
name: sender.name,
tag: sender.tag,
},
})
: { allowed: false };
@@ -148,7 +173,7 @@ export async function preflightDiscordMessage(
}
} else {
logVerbose(
`Blocked unauthorized discord sender ${author.id} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
`Blocked unauthorized discord sender ${sender.id} (dmPolicy=${dmPolicy}, ${allowMatchMeta})`,
);
}
return null;
@@ -349,7 +374,7 @@ export async function preflightDiscordMessage(
const historyEntry =
isGuildMessage && params.historyLimit > 0 && textForHistory
? ({
sender: params.data.member?.nickname ?? author.globalName ?? author.username ?? author.id,
sender: sender.label,
body: textForHistory,
timestamp: resolveTimestampMs(message.timestamp),
messageId: message.id,
@@ -372,12 +397,16 @@ export async function preflightDiscordMessage(
const hasControlCommandInMessage = hasControlCommand(baseText, params.cfg);
if (!isDirectMessage) {
const ownerAllowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:"]);
const ownerAllowList = normalizeDiscordAllowList(params.allowFrom, [
"discord:",
"user:",
"pk:",
]);
const ownerOk = ownerAllowList
? allowListMatches(ownerAllowList, {
id: author.id,
name: author.username,
tag: formatDiscordUserTag(author),
id: sender.id,
name: sender.name,
tag: sender.tag,
})
: false;
const channelUsers = channelConfig?.users ?? guildInfo?.users;
@@ -385,9 +414,9 @@ export async function preflightDiscordMessage(
Array.isArray(channelUsers) && channelUsers.length > 0
? resolveDiscordUserAllowed({
allowList: channelUsers,
userId: author.id,
userName: author.username,
userTag: formatDiscordUserTag(author),
userId: sender.id,
userName: sender.name,
userTag: sender.tag,
})
: false;
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
@@ -408,7 +437,7 @@ export async function preflightDiscordMessage(
log: logVerbose,
channel: "discord",
reason: "control command (unauthorized)",
target: author.id,
target: sender.id,
});
return null;
}
@@ -452,12 +481,12 @@ export async function preflightDiscordMessage(
if (Array.isArray(channelUsers) && channelUsers.length > 0) {
const userOk = resolveDiscordUserAllowed({
allowList: channelUsers,
userId: author.id,
userName: author.username,
userTag: formatDiscordUserTag(author),
userId: sender.id,
userName: sender.name,
userTag: sender.tag,
});
if (!userOk) {
logVerbose(`Blocked discord guild sender ${author.id} (not in channel users allowlist)`);
logVerbose(`Blocked discord guild sender ${sender.id} (not in channel users allowlist)`);
return null;
}
}
@@ -501,6 +530,7 @@ export async function preflightDiscordMessage(
client: params.client,
message,
author,
sender,
channelInfo,
channelName,
isGuildMessage,