




















@@ -32,14 +32,10 @@ import {
3232resolveDiscordGuildEntry,
3333shouldEmitDiscordReactionNotification,
3434} from "./allow-list.js";
35-import {
36-resolveDiscordChannelInfoSafe,
37-resolveDiscordChannelParentIdSafe,
38-} from "./channel-access.js";
3935import { formatDiscordReactionEmoji, formatDiscordUserTag } from "./format.js";
40-import { resolveDiscordChannelInfo } from "./message-utils.js";
4136import { setPresence } from "./presence-cache.js";
4237import { isThreadArchived } from "./thread-bindings.discord-api.js";
38+import { resolveFetchedDiscordThreadLikeChannelContext } from "./thread-channel-context.js";
4339import { closeDiscordThreadSessions } from "./thread-session-close.js";
4440import { normalizeDiscordListenerTimeoutMs, runDiscordTaskWithTimeout } from "./timeouts.js";
4541@@ -77,6 +73,11 @@ type DiscordReactionRoutingParams = {
7773guildEntries?: Record<string, import("./allow-list.js").DiscordGuildEntryResolved>;
7874};
797576+type DiscordReactionMode = "off" | "own" | "all" | "allowlist";
77+type DiscordReactionChannelConfig = ReturnType<typeof resolveDiscordChannelConfigWithFallback>;
78+type DiscordReactionIngressAccess = Awaited<ReturnType<typeof authorizeDiscordReactionIngress>>;
79+type DiscordFetchedReactionMessage = { author?: User } | null;
80+8081const DISCORD_SLOW_LISTENER_THRESHOLD_MS = 30_000;
8182const discordEventQueueLog = createSubsystemLogger("discord/event-queue");
8283@@ -409,6 +410,117 @@ async function authorizeDiscordReactionIngress(
409410return { allowed: true };
410411}
411412413+async function handleDiscordThreadReactionNotification(params: {
414+reactionMode: DiscordReactionMode;
415+message: DiscordReactionEvent["message"];
416+parentId?: string;
417+resolveThreadChannelAccess: () => Promise<{
418+access: DiscordReactionIngressAccess;
419+channelConfig: DiscordReactionChannelConfig;
420+}>;
421+shouldNotifyReaction: (options: {
422+mode: DiscordReactionMode;
423+messageAuthorId?: string;
424+channelConfig?: DiscordReactionChannelConfig;
425+}) => boolean;
426+resolveReactionBase: () => { baseText: string; contextKey: string };
427+emitReaction: (text: string, parentPeerId?: string) => void;
428+emitReactionWithAuthor: (message: DiscordFetchedReactionMessage) => void;
429+}) {
430+if (params.reactionMode === "off") {
431+return;
432+}
433+434+if (params.reactionMode === "all" || params.reactionMode === "allowlist") {
435+const { access, channelConfig } = await params.resolveThreadChannelAccess();
436+if (
437+!access.allowed ||
438+!params.shouldNotifyReaction({ mode: params.reactionMode, channelConfig })
439+) {
440+return;
441+}
442+443+const { baseText } = params.resolveReactionBase();
444+params.emitReaction(baseText, params.parentId);
445+return;
446+}
447+448+const message = await params.message.fetch().catch(() => null);
449+const { access, channelConfig } = await params.resolveThreadChannelAccess();
450+const messageAuthorId = message?.author?.id ?? undefined;
451+if (
452+!access.allowed ||
453+!params.shouldNotifyReaction({
454+mode: params.reactionMode,
455+ messageAuthorId,
456+ channelConfig,
457+})
458+) {
459+return;
460+}
461+462+params.emitReactionWithAuthor(message);
463+}
464+465+async function handleDiscordChannelReactionNotification(params: {
466+isGuildMessage: boolean;
467+reactionMode: DiscordReactionMode;
468+message: DiscordReactionEvent["message"];
469+channelConfig: DiscordReactionChannelConfig;
470+parentId?: string;
471+authorizeReactionIngressForChannel: (
472+channelConfig: DiscordReactionChannelConfig,
473+) => Promise<DiscordReactionIngressAccess>;
474+shouldNotifyReaction: (options: {
475+mode: DiscordReactionMode;
476+messageAuthorId?: string;
477+channelConfig?: DiscordReactionChannelConfig;
478+}) => boolean;
479+resolveReactionBase: () => { baseText: string; contextKey: string };
480+emitReaction: (text: string, parentPeerId?: string) => void;
481+emitReactionWithAuthor: (message: DiscordFetchedReactionMessage) => void;
482+}) {
483+if (params.isGuildMessage) {
484+const access = await params.authorizeReactionIngressForChannel(params.channelConfig);
485+if (!access.allowed) {
486+return;
487+}
488+}
489+490+if (params.reactionMode === "off") {
491+return;
492+}
493+494+if (params.reactionMode === "all" || params.reactionMode === "allowlist") {
495+if (
496+!params.shouldNotifyReaction({
497+mode: params.reactionMode,
498+channelConfig: params.channelConfig,
499+})
500+) {
501+return;
502+}
503+504+const { baseText } = params.resolveReactionBase();
505+params.emitReaction(baseText, params.parentId);
506+return;
507+}
508+509+const message = await params.message.fetch().catch(() => null);
510+const messageAuthorId = message?.author?.id ?? undefined;
511+if (
512+!params.shouldNotifyReaction({
513+mode: params.reactionMode,
514+ messageAuthorId,
515+channelConfig: params.channelConfig,
516+})
517+) {
518+return;
519+}
520+521+params.emitReactionWithAuthor(message);
522+}
523+412524async function handleDiscordReactionEvent(
413525params: {
414526data: DiscordReactionEvent;
@@ -449,16 +561,17 @@ async function handleDiscordReactionEvent(
449561if (!channel) {
450562return;
451563}
452-const channelInfo = resolveDiscordChannelInfoSafe(channel);
453-const channelName = channelInfo.name;
454-const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
455-const channelType = channelInfo.type;
564+const channelContext = await resolveFetchedDiscordThreadLikeChannelContext({
565+ client,
566+ channel,
567+channelIdFallback: data.channel_id,
568+});
569+const channelName = channelContext.channelName;
570+const channelSlug = channelContext.channelSlug;
571+const channelType = channelContext.channelType;
456572const isDirectMessage = channelType === ChannelType.DM;
457573const isGroupDm = channelType === ChannelType.GroupDM;
458-const isThreadChannel =
459-channelType === ChannelType.PublicThread ||
460-channelType === ChannelType.PrivateThread ||
461-channelType === ChannelType.AnnouncementThread;
574+const isThreadChannel = channelContext.isThreadChannel;
462575const memberRoleIds = Array.isArray(data.rawMember?.roles)
463576 ? data.rawMember.roles.map((roleId: string) => roleId)
464577 : [];
@@ -490,9 +603,9 @@ async function handleDiscordReactionEvent(
490603return;
491604}
492605}
493-let parentId = resolveDiscordChannelParentIdSafe(channel);
494-let parentName: string | undefined;
495-let parentSlug = "";
606+const parentId = isThreadChannel ? channelContext.threadParentId : channelContext.parentId;
607+const parentName = isThreadChannel ? channelContext.threadParentName : undefined;
608+const parentSlug = isThreadChannel ? channelContext.threadParentSlug : "";
496609let reactionBase: { baseText: string; contextKey: string } | null = null;
497610const resolveReactionBase = () => {
498611if (reactionBase) {
@@ -535,9 +648,9 @@ async function handleDiscordReactionEvent(
535648});
536649};
537650const shouldNotifyReaction = (options: {
538-mode: "off" | "own" | "all" | "allowlist";
651+mode: DiscordReactionMode;
539652messageAuthorId?: string;
540-channelConfig?: ReturnType<typeof resolveDiscordChannelConfigWithFallback>;
653+channelConfig?: DiscordReactionChannelConfig;
541654}) =>
542655shouldEmitDiscordReactionNotification({
543656mode: options.mode,
@@ -557,14 +670,6 @@ async function handleDiscordReactionEvent(
557670const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText;
558671emitReaction(text, parentId);
559672};
560-const loadThreadParentInfo = async () => {
561-if (!parentId) {
562-return;
563-}
564-const parentInfo = await resolveDiscordChannelInfo(client, parentId);
565-parentName = parentInfo?.name;
566-parentSlug = parentName ? normalizeDiscordSlug(parentName) : "";
567-};
568673const resolveThreadChannelConfig = () =>
569674resolveDiscordChannelConfigWithFallback({
570675 guildInfo,
@@ -577,77 +682,30 @@ async function handleDiscordReactionEvent(
577682scope: "thread",
578683});
579684const authorizeReactionIngressForChannel = async (
580-channelConfig: ReturnType<typeof resolveDiscordChannelConfigWithFallback>,
685+channelConfig: DiscordReactionChannelConfig,
581686) =>
582687await authorizeDiscordReactionIngress({
583688 ...reactionIngressBase,
584689 channelConfig,
585690});
586-const resolveThreadChannelAccess = async (channelInfo: { parentId?: string } | null) => {
587-parentId = channelInfo?.parentId;
588-await loadThreadParentInfo();
691+const resolveThreadChannelAccess = async () => {
589692const channelConfig = resolveThreadChannelConfig();
590693const access = await authorizeReactionIngressForChannel(channelConfig);
591694return { access, channelConfig };
592695};
593696594-// Parallelize async operations for thread channels
595697if (isThreadChannel) {
596698const reactionMode = guildInfo?.reactionNotifications ?? "own";
597-598-// Early exit: skip fetching message if notifications are off
599-if (reactionMode === "off") {
600-return;
601-}
602-603-const channelInfoPromise = parentId
604- ? Promise.resolve({ parentId })
605- : resolveDiscordChannelInfo(client, data.channel_id);
606-607-// Fast path: for "all" and "allowlist" modes, we don't need to fetch the message
608-if (reactionMode === "all" || reactionMode === "allowlist") {
609-const channelInfo = await channelInfoPromise;
610-const { access: threadAccess, channelConfig: threadChannelConfig } =
611-await resolveThreadChannelAccess(channelInfo);
612-if (!threadAccess.allowed) {
613-return;
614-}
615-if (
616-!shouldNotifyReaction({
617-mode: reactionMode,
618-channelConfig: threadChannelConfig,
619-})
620-) {
621-return;
622-}
623-624-const { baseText } = resolveReactionBase();
625-emitReaction(baseText, parentId);
626-return;
627-}
628-629-// For "own" mode, we need to fetch the message to check the author
630-const messagePromise = data.message.fetch().catch(() => null);
631-632-const [channelInfo, message] = await Promise.all([channelInfoPromise, messagePromise]);
633-const { access: threadAccess, channelConfig: threadChannelConfig } =
634-await resolveThreadChannelAccess(channelInfo);
635-if (!threadAccess.allowed) {
636-return;
637-}
638-639-const messageAuthorId = message?.author?.id ?? undefined;
640-if (
641-!shouldNotifyReaction({
642-mode: reactionMode,
643- messageAuthorId,
644-channelConfig: threadChannelConfig,
645-})
646-) {
647-return;
648-}
649-650-emitReactionWithAuthor(message);
699+await handleDiscordThreadReactionNotification({
700+ reactionMode,
701+message: data.message,
702+ parentId,
703+ resolveThreadChannelAccess,
704+ shouldNotifyReaction,
705+ resolveReactionBase,
706+ emitReaction,
707+ emitReactionWithAuthor,
708+});
651709return;
652710}
653711@@ -662,39 +720,19 @@ async function handleDiscordReactionEvent(
662720 parentSlug,
663721scope: "channel",
664722});
665-if (isGuildMessage) {
666-const channelAccess = await authorizeReactionIngressForChannel(channelConfig);
667-if (!channelAccess.allowed) {
668-return;
669-}
670-}
671-672723const reactionMode = guildInfo?.reactionNotifications ?? "own";
673-674-// Early exit: skip fetching message if notifications are off
675-if (reactionMode === "off") {
676-return;
677-}
678-679-// Fast path: for "all" and "allowlist" modes, we don't need to fetch the message
680-if (reactionMode === "all" || reactionMode === "allowlist") {
681-if (!shouldNotifyReaction({ mode: reactionMode, channelConfig })) {
682-return;
683-}
684-685-const { baseText } = resolveReactionBase();
686-emitReaction(baseText, parentId);
687-return;
688-}
689-690-// For "own" mode, we need to fetch the message to check the author
691-const message = await data.message.fetch().catch(() => null);
692-const messageAuthorId = message?.author?.id ?? undefined;
693-if (!shouldNotifyReaction({ mode: reactionMode, messageAuthorId, channelConfig })) {
694-return;
695-}
696-697-emitReactionWithAuthor(message);
724+await handleDiscordChannelReactionNotification({
725+ isGuildMessage,
726+ reactionMode,
727+message: data.message,
728+ channelConfig,
729+ parentId,
730+ authorizeReactionIngressForChannel,
731+ shouldNotifyReaction,
732+ resolveReactionBase,
733+ emitReaction,
734+ emitReactionWithAuthor,
735+});
698736} catch (err) {
699737params.logger.error(danger(`discord reaction handler failed: ${String(err)}`));
700738}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。