refactor: dedupe channel and gateway surfaces

This commit is contained in:
Peter Steinberger
2026-03-02 19:48:12 +00:00
parent 9617ac9dd5
commit 9d30159fcd
44 changed files with 1072 additions and 1479 deletions

View File

@@ -12,16 +12,44 @@ function buildDmAccess(overrides: Partial<DiscordDmCommandAccess>): DiscordDmCom
};
}
const TEST_ACCOUNT_ID = "default";
const TEST_SENDER = { id: "123", tag: "alice#0001", name: "alice" };
function createDmDecisionHarness(params?: { pairingCreated?: boolean }) {
const onPairingCreated = vi.fn(async () => {});
const onUnauthorized = vi.fn(async () => {});
const upsertPairingRequest = vi.fn(async () => ({
code: "PAIR-1",
created: params?.pairingCreated ?? true,
}));
return { onPairingCreated, onUnauthorized, upsertPairingRequest };
}
async function runPairingDecision(params?: { pairingCreated?: boolean }) {
const harness = createDmDecisionHarness({ pairingCreated: params?.pairingCreated });
const allowed = await handleDiscordDmCommandDecision({
dmAccess: buildDmAccess({
decision: "pairing",
commandAuthorized: false,
allowMatch: { allowed: false },
}),
accountId: TEST_ACCOUNT_ID,
sender: TEST_SENDER,
onPairingCreated: harness.onPairingCreated,
onUnauthorized: harness.onUnauthorized,
upsertPairingRequest: harness.upsertPairingRequest,
});
return { allowed, ...harness };
}
describe("handleDiscordDmCommandDecision", () => {
it("returns true for allowed DM access", async () => {
const onPairingCreated = vi.fn(async () => {});
const onUnauthorized = vi.fn(async () => {});
const upsertPairingRequest = vi.fn(async () => ({ code: "PAIR-1", created: true }));
const { onPairingCreated, onUnauthorized, upsertPairingRequest } = createDmDecisionHarness();
const allowed = await handleDiscordDmCommandDecision({
dmAccess: buildDmAccess({ decision: "allow" }),
accountId: "default",
sender: { id: "123", tag: "alice#0001", name: "alice" },
accountId: TEST_ACCOUNT_ID,
sender: TEST_SENDER,
onPairingCreated,
onUnauthorized,
upsertPairingRequest,
@@ -34,31 +62,17 @@ describe("handleDiscordDmCommandDecision", () => {
});
it("creates pairing reply for new pairing requests", async () => {
const onPairingCreated = vi.fn(async () => {});
const onUnauthorized = vi.fn(async () => {});
const upsertPairingRequest = vi.fn(async () => ({ code: "PAIR-1", created: true }));
const allowed = await handleDiscordDmCommandDecision({
dmAccess: buildDmAccess({
decision: "pairing",
commandAuthorized: false,
allowMatch: { allowed: false },
}),
accountId: "default",
sender: { id: "123", tag: "alice#0001", name: "alice" },
onPairingCreated,
onUnauthorized,
upsertPairingRequest,
});
const { allowed, onPairingCreated, onUnauthorized, upsertPairingRequest } =
await runPairingDecision();
expect(allowed).toBe(false);
expect(upsertPairingRequest).toHaveBeenCalledWith({
channel: "discord",
id: "123",
accountId: "default",
accountId: TEST_ACCOUNT_ID,
meta: {
tag: "alice#0001",
name: "alice",
tag: TEST_SENDER.tag,
name: TEST_SENDER.name,
},
});
expect(onPairingCreated).toHaveBeenCalledWith("PAIR-1");
@@ -66,21 +80,8 @@ describe("handleDiscordDmCommandDecision", () => {
});
it("skips pairing reply when pairing request already exists", async () => {
const onPairingCreated = vi.fn(async () => {});
const onUnauthorized = vi.fn(async () => {});
const upsertPairingRequest = vi.fn(async () => ({ code: "PAIR-1", created: false }));
const allowed = await handleDiscordDmCommandDecision({
dmAccess: buildDmAccess({
decision: "pairing",
commandAuthorized: false,
allowMatch: { allowed: false },
}),
accountId: "default",
sender: { id: "123", tag: "alice#0001", name: "alice" },
onPairingCreated,
onUnauthorized,
upsertPairingRequest,
const { allowed, onPairingCreated, onUnauthorized } = await runPairingDecision({
pairingCreated: false,
});
expect(allowed).toBe(false);
@@ -89,9 +90,7 @@ describe("handleDiscordDmCommandDecision", () => {
});
it("runs unauthorized handler for blocked DM access", async () => {
const onPairingCreated = vi.fn(async () => {});
const onUnauthorized = vi.fn(async () => {});
const upsertPairingRequest = vi.fn(async () => ({ code: "PAIR-1", created: true }));
const { onPairingCreated, onUnauthorized, upsertPairingRequest } = createDmDecisionHarness();
const allowed = await handleDiscordDmCommandDecision({
dmAccess: buildDmAccess({
@@ -99,8 +98,8 @@ describe("handleDiscordDmCommandDecision", () => {
commandAuthorized: false,
allowMatch: { allowed: false },
}),
accountId: "default",
sender: { id: "123", tag: "alice#0001", name: "alice" },
accountId: TEST_ACCOUNT_ID,
sender: TEST_SENDER,
onPairingCreated,
onUnauthorized,
upsertPairingRequest,

View File

@@ -374,7 +374,7 @@ async function handleDiscordReactionEvent(params: {
channelType === ChannelType.PublicThread ||
channelType === ChannelType.PrivateThread ||
channelType === ChannelType.AnnouncementThread;
const ingressAccess = await authorizeDiscordReactionIngress({
const reactionIngressBase: Omit<DiscordReactionIngressAuthorizationParams, "channelConfig"> = {
accountId: params.accountId,
user,
isDirectMessage,
@@ -391,7 +391,8 @@ async function handleDiscordReactionEvent(params: {
groupPolicy: params.groupPolicy,
allowNameMatching: params.allowNameMatching,
guildInfo,
});
};
const ingressAccess = await authorizeDiscordReactionIngress(reactionIngressBase);
if (!ingressAccess.allowed) {
logVerbose(`discord reaction blocked sender=${user.id} (reason=${ingressAccess.reason})`);
return;
@@ -486,22 +487,7 @@ async function handleDiscordReactionEvent(params: {
channelConfig: ReturnType<typeof resolveDiscordChannelConfigWithFallback>,
) =>
await authorizeDiscordReactionIngress({
accountId: params.accountId,
user,
isDirectMessage,
isGroupDm,
isGuildMessage,
channelId: data.channel_id,
channelName,
channelSlug,
dmEnabled: params.dmEnabled,
groupDmEnabled: params.groupDmEnabled,
groupDmChannels: params.groupDmChannels,
dmPolicy: params.dmPolicy,
allowFrom: params.allowFrom,
groupPolicy: params.groupPolicy,
allowNameMatching: params.allowNameMatching,
guildInfo,
...reactionIngressBase,
channelConfig,
});
const authorizeThreadChannelAccess = async (channelInfo: { parentId?: string } | null) => {

View File

@@ -3,7 +3,10 @@ import { inboundCtxCapture as capture } from "../../../test/helpers/inbound-cont
import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js";
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
import { processDiscordMessage } from "./message-handler.process.js";
import { createBaseDiscordMessageContext } from "./message-handler.test-harness.js";
import {
createBaseDiscordMessageContext,
createDiscordDirectMessageContextOverrides,
} from "./message-handler.test-harness.js";
describe("discord processDiscordMessage inbound contract", () => {
it("passes a finalized MsgContext to dispatchInboundMessage", async () => {
@@ -11,26 +14,7 @@ describe("discord processDiscordMessage inbound contract", () => {
const messageCtx = await createBaseDiscordMessageContext({
cfg: { messages: {} },
ackReactionScope: "direct",
data: { guild: null },
channelInfo: null,
channelName: undefined,
isGuildMessage: false,
isDirectMessage: true,
isGroupDm: false,
shouldRequireMention: false,
canDetectMention: false,
effectiveWasMentioned: false,
displayChannelSlug: "",
guildInfo: null,
guildSlug: "",
baseSessionKey: "agent:main:discord:direct:u1",
route: {
agentId: "main",
channel: "discord",
accountId: "default",
sessionKey: "agent:main:discord:direct:u1",
mainSessionKey: "agent:main:main",
},
...createDiscordDirectMessageContextOverrides(),
});
await processDiscordMessage(messageCtx);

View File

@@ -1,6 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { DEFAULT_EMOJIS } from "../../channels/status-reactions.js";
import { createBaseDiscordMessageContext } from "./message-handler.test-harness.js";
import {
createBaseDiscordMessageContext,
createDiscordDirectMessageContextOverrides,
} from "./message-handler.test-harness.js";
import {
__testing as threadBindingTesting,
createThreadBindingManager,
@@ -295,18 +298,7 @@ describe("processDiscordMessage ack reactions", () => {
describe("processDiscordMessage session routing", () => {
it("stores DM lastRoute with user target for direct-session continuity", async () => {
const ctx = await createBaseContext({
data: { guild: null },
channelInfo: null,
channelName: undefined,
isGuildMessage: false,
isDirectMessage: true,
isGroupDm: false,
shouldRequireMention: false,
canDetectMention: false,
effectiveWasMentioned: false,
displayChannelSlug: "",
guildInfo: null,
guildSlug: "",
...createDiscordDirectMessageContextOverrides(),
message: {
id: "m1",
channelId: "dm1",
@@ -314,14 +306,6 @@ describe("processDiscordMessage session routing", () => {
attachments: [],
},
messageChannelId: "dm1",
baseSessionKey: "agent:main:discord:direct:u1",
route: {
agentId: "main",
channel: "discord",
accountId: "default",
sessionKey: "agent:main:discord:direct:u1",
mainSessionKey: "agent:main:main",
},
});
// oxlint-disable-next-line typescript/no-explicit-any

View File

@@ -72,3 +72,28 @@ export async function createBaseDiscordMessageContext(
...overrides,
} as unknown as DiscordMessagePreflightContext;
}
export function createDiscordDirectMessageContextOverrides(): Record<string, unknown> {
return {
data: { guild: null },
channelInfo: null,
channelName: undefined,
isGuildMessage: false,
isDirectMessage: true,
isGroupDm: false,
shouldRequireMention: false,
canDetectMention: false,
effectiveWasMentioned: false,
displayChannelSlug: "",
guildInfo: null,
guildSlug: "",
baseSessionKey: "agent:main:discord:direct:u1",
route: {
agentId: "main",
channel: "discord",
accountId: "default",
sessionKey: "agent:main:discord:direct:u1",
mainSessionKey: "agent:main:main",
},
};
}

View File

@@ -30,6 +30,68 @@ function asMessage(payload: Record<string, unknown>): Message {
return payload as unknown as Message;
}
function expectSinglePngDownload(params: {
result: unknown;
expectedUrl: string;
filePathHint: string;
expectedPath: string;
placeholder: "<media:image>" | "<media:sticker>";
}) {
expect(fetchRemoteMedia).toHaveBeenCalledTimes(1);
expect(fetchRemoteMedia).toHaveBeenCalledWith({
url: params.expectedUrl,
filePathHint: params.filePathHint,
maxBytes: 512,
fetchImpl: undefined,
ssrfPolicy: expect.objectContaining({ allowRfc2544BenchmarkRange: true }),
});
expect(saveMediaBuffer).toHaveBeenCalledTimes(1);
expect(saveMediaBuffer).toHaveBeenCalledWith(expect.any(Buffer), "image/png", "inbound", 512);
expect(params.result).toEqual([
{
path: params.expectedPath,
contentType: "image/png",
placeholder: params.placeholder,
},
]);
}
function expectAttachmentImageFallback(params: { result: unknown; attachment: { url: string } }) {
expect(saveMediaBuffer).not.toHaveBeenCalled();
expect(params.result).toEqual([
{
path: params.attachment.url,
contentType: "image/png",
placeholder: "<media:image>",
},
]);
}
function asForwardedSnapshotMessage(params: {
content: string;
embeds: Array<{ title?: string; description?: string }>;
}) {
return asMessage({
content: "",
rawData: {
message_snapshots: [
{
message: {
content: params.content,
embeds: params.embeds,
attachments: [],
author: {
id: "u2",
username: "Bob",
discriminator: "0",
},
},
},
],
},
});
}
describe("resolveDiscordMessageChannelId", () => {
it.each([
{
@@ -157,14 +219,7 @@ describe("resolveForwardedMediaList", () => {
512,
);
expect(saveMediaBuffer).not.toHaveBeenCalled();
expect(result).toEqual([
{
path: attachment.url,
contentType: "image/png",
placeholder: "<media:image>",
},
]);
expectAttachmentImageFallback({ result, attachment });
});
it("downloads forwarded stickers", async () => {
@@ -191,23 +246,13 @@ describe("resolveForwardedMediaList", () => {
512,
);
expect(fetchRemoteMedia).toHaveBeenCalledTimes(1);
expect(fetchRemoteMedia).toHaveBeenCalledWith({
url: "https://media.discordapp.net/stickers/sticker-1.png",
expectSinglePngDownload({
result,
expectedUrl: "https://media.discordapp.net/stickers/sticker-1.png",
filePathHint: "wave.png",
maxBytes: 512,
fetchImpl: undefined,
ssrfPolicy: expect.objectContaining({ allowRfc2544BenchmarkRange: true }),
expectedPath: "/tmp/sticker.png",
placeholder: "<media:sticker>",
});
expect(saveMediaBuffer).toHaveBeenCalledTimes(1);
expect(saveMediaBuffer).toHaveBeenCalledWith(expect.any(Buffer), "image/png", "inbound", 512);
expect(result).toEqual([
{
path: "/tmp/sticker.png",
contentType: "image/png",
placeholder: "<media:sticker>",
},
]);
});
it("returns empty when no snapshots are present", async () => {
@@ -260,23 +305,13 @@ describe("resolveMediaList", () => {
512,
);
expect(fetchRemoteMedia).toHaveBeenCalledTimes(1);
expect(fetchRemoteMedia).toHaveBeenCalledWith({
url: "https://media.discordapp.net/stickers/sticker-2.png",
expectSinglePngDownload({
result,
expectedUrl: "https://media.discordapp.net/stickers/sticker-2.png",
filePathHint: "hello.png",
maxBytes: 512,
fetchImpl: undefined,
ssrfPolicy: expect.objectContaining({ allowRfc2544BenchmarkRange: true }),
expectedPath: "/tmp/sticker-2.png",
placeholder: "<media:sticker>",
});
expect(saveMediaBuffer).toHaveBeenCalledTimes(1);
expect(saveMediaBuffer).toHaveBeenCalledWith(expect.any(Buffer), "image/png", "inbound", 512);
expect(result).toEqual([
{
path: "/tmp/sticker-2.png",
contentType: "image/png",
placeholder: "<media:sticker>",
},
]);
});
it("forwards fetchImpl to sticker downloads", async () => {
@@ -324,14 +359,7 @@ describe("resolveMediaList", () => {
512,
);
expect(saveMediaBuffer).not.toHaveBeenCalled();
expect(result).toEqual([
{
path: attachment.url,
contentType: "image/png",
placeholder: "<media:image>",
},
]);
expectAttachmentImageFallback({ result, attachment });
});
it("falls back to URL when saveMediaBuffer fails", async () => {
@@ -471,24 +499,9 @@ describe("Discord media SSRF policy", () => {
describe("resolveDiscordMessageText", () => {
it("includes forwarded message snapshots in body text", () => {
const text = resolveDiscordMessageText(
asMessage({
content: "",
rawData: {
message_snapshots: [
{
message: {
content: "forwarded hello",
embeds: [],
attachments: [],
author: {
id: "u2",
username: "Bob",
discriminator: "0",
},
},
},
],
},
asForwardedSnapshotMessage({
content: "forwarded hello",
embeds: [],
}),
{ includeForwarded: true },
);
@@ -560,24 +573,9 @@ describe("resolveDiscordMessageText", () => {
it("joins forwarded snapshot embed title and description when content is empty", () => {
const text = resolveDiscordMessageText(
asMessage({
asForwardedSnapshotMessage({
content: "",
rawData: {
message_snapshots: [
{
message: {
content: "",
embeds: [{ title: "Forwarded title", description: "Forwarded details" }],
attachments: [],
author: {
id: "u2",
username: "Bob",
discriminator: "0",
},
},
},
],
},
embeds: [{ title: "Forwarded title", description: "Forwarded details" }],
}),
{ includeForwarded: true },
);

View File

@@ -122,6 +122,27 @@ describe("runDiscordGatewayLifecycle", () => {
expect(params.releaseEarlyGatewayErrorGuard).toHaveBeenCalledTimes(1);
}
function createGatewayHarness(params?: {
state?: {
sessionId?: string | null;
resumeGatewayUrl?: string | null;
sequence?: number | null;
};
sequence?: number | null;
}) {
const emitter = new EventEmitter();
const gateway = {
isConnected: false,
options: {},
disconnect: vi.fn(),
connect: vi.fn(),
...(params?.state ? { state: params.state } : {}),
...(params?.sequence !== undefined ? { sequence: params.sequence } : {}),
emitter,
};
return { emitter, gateway };
}
it("cleans up thread bindings when exec approvals startup fails", async () => {
const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js");
const { lifecycleParams, start, stop, threadStop, releaseEarlyGatewayErrorGuard } =
@@ -229,20 +250,14 @@ describe("runDiscordGatewayLifecycle", () => {
vi.useFakeTimers();
try {
const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js");
const emitter = new EventEmitter();
const gateway = {
isConnected: false,
options: {},
disconnect: vi.fn(),
connect: vi.fn(),
const { emitter, gateway } = createGatewayHarness({
state: {
sessionId: "session-1",
resumeGatewayUrl: "wss://gateway.discord.gg",
sequence: 123,
},
sequence: 123,
emitter,
};
});
getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter);
waitForDiscordGatewayStopMock.mockImplementationOnce(async () => {
emitter.emit("debug", "WebSocket connection opened");
@@ -260,9 +275,10 @@ describe("runDiscordGatewayLifecycle", () => {
expect(gateway.connect).toHaveBeenNthCalledWith(1, true);
expect(gateway.connect).toHaveBeenNthCalledWith(2, true);
expect(gateway.connect).toHaveBeenNthCalledWith(3, false);
expect(gateway.state.sessionId).toBeNull();
expect(gateway.state.resumeGatewayUrl).toBeNull();
expect(gateway.state.sequence).toBeNull();
expect(gateway.state).toBeDefined();
expect(gateway.state?.sessionId).toBeNull();
expect(gateway.state?.resumeGatewayUrl).toBeNull();
expect(gateway.state?.sequence).toBeNull();
expect(gateway.sequence).toBeNull();
} finally {
vi.useRealTimers();
@@ -273,20 +289,14 @@ describe("runDiscordGatewayLifecycle", () => {
vi.useFakeTimers();
try {
const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js");
const emitter = new EventEmitter();
const gateway = {
isConnected: false,
options: {},
disconnect: vi.fn(),
connect: vi.fn(),
const { emitter, gateway } = createGatewayHarness({
state: {
sessionId: "session-2",
resumeGatewayUrl: "wss://gateway.discord.gg",
sequence: 456,
},
sequence: 456,
emitter,
};
});
getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter);
waitForDiscordGatewayStopMock.mockImplementationOnce(async () => {
emitter.emit("debug", "WebSocket connection opened");
@@ -324,14 +334,7 @@ describe("runDiscordGatewayLifecycle", () => {
vi.useFakeTimers();
try {
const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js");
const emitter = new EventEmitter();
const gateway = {
isConnected: false,
options: {},
disconnect: vi.fn(),
connect: vi.fn(),
emitter,
};
const { emitter, gateway } = createGatewayHarness();
getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter);
waitForDiscordGatewayStopMock.mockImplementationOnce(
(waitParams: WaitForDiscordGatewayStopParams) =>
@@ -356,14 +359,7 @@ describe("runDiscordGatewayLifecycle", () => {
vi.useFakeTimers();
try {
const { runDiscordGatewayLifecycle } = await import("./provider.lifecycle.js");
const emitter = new EventEmitter();
const gateway = {
isConnected: false,
options: {},
disconnect: vi.fn(),
connect: vi.fn(),
emitter,
};
const { emitter, gateway } = createGatewayHarness();
getDiscordGatewayEmitterMock.mockReturnValueOnce(emitter);
let resolveWait: (() => void) | undefined;
waitForDiscordGatewayStopMock.mockImplementationOnce(

View File

@@ -14,6 +14,11 @@ import { resolveTextChunkLimit } from "../../auto-reply/chunk.js";
import { listNativeCommandSpecsForConfig } from "../../auto-reply/commands-registry.js";
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js";
import {
resolveThreadBindingIdleTimeoutMs,
resolveThreadBindingMaxAgeMs,
resolveThreadBindingsEnabled,
} from "../../channels/thread-bindings-policy.js";
import {
isNativeCommandsExplicitlyDisabled,
resolveNativeCommandsEnabled,
@@ -110,59 +115,6 @@ function summarizeGuilds(entries?: Record<string, unknown>) {
return `${sample.join(", ")}${suffix}`;
}
const DEFAULT_THREAD_BINDING_IDLE_HOURS = 24;
const DEFAULT_THREAD_BINDING_MAX_AGE_HOURS = 0;
function normalizeThreadBindingHours(raw: unknown): number | undefined {
if (typeof raw !== "number" || !Number.isFinite(raw)) {
return undefined;
}
if (raw < 0) {
return undefined;
}
return raw;
}
function resolveThreadBindingIdleTimeoutMs(params: {
channelIdleHoursRaw: unknown;
sessionIdleHoursRaw: unknown;
}): number {
const idleHours =
normalizeThreadBindingHours(params.channelIdleHoursRaw) ??
normalizeThreadBindingHours(params.sessionIdleHoursRaw) ??
DEFAULT_THREAD_BINDING_IDLE_HOURS;
return Math.floor(idleHours * 60 * 60 * 1000);
}
function resolveThreadBindingMaxAgeMs(params: {
channelMaxAgeHoursRaw: unknown;
sessionMaxAgeHoursRaw: unknown;
}): number {
const maxAgeHours =
normalizeThreadBindingHours(params.channelMaxAgeHoursRaw) ??
normalizeThreadBindingHours(params.sessionMaxAgeHoursRaw) ??
DEFAULT_THREAD_BINDING_MAX_AGE_HOURS;
return Math.floor(maxAgeHours * 60 * 60 * 1000);
}
function normalizeThreadBindingsEnabled(raw: unknown): boolean | undefined {
if (typeof raw !== "boolean") {
return undefined;
}
return raw;
}
function resolveThreadBindingsEnabled(params: {
channelEnabledRaw: unknown;
sessionEnabledRaw: unknown;
}): boolean {
return (
normalizeThreadBindingsEnabled(params.channelEnabledRaw) ??
normalizeThreadBindingsEnabled(params.sessionEnabledRaw) ??
true
);
}
function formatThreadBindingDurationForConfigLabel(durationMs: number): string {
const label = formatThreadBindingDurationLabel(durationMs);
return label === "disabled" ? "off" : label;
@@ -612,43 +564,26 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
client.listeners,
new DiscordMessageListener(messageHandler, logger, trackInboundEvent),
);
const reactionListenerOptions = {
cfg,
accountId: account.accountId,
runtime,
botUserId,
dmEnabled,
groupDmEnabled,
groupDmChannels: groupDmChannels ?? [],
dmPolicy,
allowFrom: allowFrom ?? [],
groupPolicy,
allowNameMatching: isDangerousNameMatchingEnabled(discordCfg),
guildEntries,
logger,
onEvent: trackInboundEvent,
};
registerDiscordListener(client.listeners, new DiscordReactionListener(reactionListenerOptions));
registerDiscordListener(
client.listeners,
new DiscordReactionListener({
cfg,
accountId: account.accountId,
runtime,
botUserId,
dmEnabled,
groupDmEnabled,
groupDmChannels: groupDmChannels ?? [],
dmPolicy,
allowFrom: allowFrom ?? [],
groupPolicy,
allowNameMatching: isDangerousNameMatchingEnabled(discordCfg),
guildEntries,
logger,
onEvent: trackInboundEvent,
}),
);
registerDiscordListener(
client.listeners,
new DiscordReactionRemoveListener({
cfg,
accountId: account.accountId,
runtime,
botUserId,
dmEnabled,
groupDmEnabled,
groupDmChannels: groupDmChannels ?? [],
dmPolicy,
allowFrom: allowFrom ?? [],
groupPolicy,
allowNameMatching: isDangerousNameMatchingEnabled(discordCfg),
guildEntries,
logger,
onEvent: trackInboundEvent,
}),
new DiscordReactionRemoveListener(reactionListenerOptions),
);
if (discordCfg.intents?.presence) {