426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
783 lines
17 KiB
TypeScript
783 lines
17 KiB
TypeScript
/**
|
|
* Message gating for the Discord connector. Resolves whether a user or channel
|
|
* is allowed under the configured DM policy and channel allowlist, matching by
|
|
* wildcard, id, name, tag, or parent channel.
|
|
*/
|
|
import type { Guild, GuildMember, User } from "discord.js";
|
|
import type {
|
|
DiscordAccountConfig,
|
|
DiscordGuildChannelConfig,
|
|
DiscordGuildEntry,
|
|
} from "./accounts";
|
|
|
|
/**
|
|
* Normalized allowlist structure for Discord entities
|
|
*/
|
|
export interface DiscordAllowList {
|
|
allowAll: boolean;
|
|
ids: Set<string>;
|
|
names: Set<string>;
|
|
}
|
|
|
|
/**
|
|
* Match source type for allowlist matching
|
|
*/
|
|
export type AllowListMatchSource = "wildcard" | "id" | "name" | "tag";
|
|
|
|
/**
|
|
* Result of an allowlist match operation
|
|
*/
|
|
export interface DiscordAllowListMatch {
|
|
allowed: boolean;
|
|
matchKey?: string;
|
|
matchSource?: AllowListMatchSource;
|
|
}
|
|
|
|
/**
|
|
* Match source for channel configuration
|
|
*/
|
|
export type ChannelMatchSource = "id" | "name" | "slug" | "wildcard" | "parent";
|
|
|
|
/**
|
|
* Resolved channel configuration with match metadata
|
|
*/
|
|
export interface DiscordChannelConfigResolved {
|
|
allowed: boolean;
|
|
requireMention?: boolean;
|
|
skills?: string[];
|
|
enabled?: boolean;
|
|
users?: Array<string | number>;
|
|
systemPrompt?: string;
|
|
autoThread?: boolean;
|
|
matchKey?: string;
|
|
matchSource?: ChannelMatchSource;
|
|
}
|
|
|
|
/**
|
|
* Normalizes a Discord slug (channel name, guild name, etc.)
|
|
* Converts to lowercase, removes special characters, replaces spaces with dashes
|
|
*/
|
|
export function normalizeDiscordSlug(value: string): string {
|
|
return value
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/^#/, "")
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
}
|
|
|
|
/**
|
|
* Formats a Discord user tag for matching
|
|
*/
|
|
export function formatDiscordUserTag(user: User | GuildMember): string {
|
|
if ("user" in user) {
|
|
return user.user.discriminator === "0"
|
|
? user.user.username
|
|
: `${user.user.username}#${user.user.discriminator}`;
|
|
}
|
|
return user.discriminator === "0"
|
|
? user.username
|
|
: `${user.username}#${user.discriminator}`;
|
|
}
|
|
|
|
/**
|
|
* Normalizes a raw allowlist array into a structured DiscordAllowList
|
|
*/
|
|
export function normalizeDiscordAllowList(
|
|
raw: Array<string | number> | undefined,
|
|
prefixes: string[] = ["discord:", "user:", "pk:"],
|
|
): DiscordAllowList | null {
|
|
if (!raw || raw.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const ids = new Set<string>();
|
|
const names = new Set<string>();
|
|
const allowAll = raw.some((entry) => String(entry).trim() === "*");
|
|
|
|
for (const entry of raw) {
|
|
const text = String(entry).trim();
|
|
if (!text || text === "*") {
|
|
continue;
|
|
}
|
|
|
|
const normalized = normalizeDiscordSlug(text);
|
|
|
|
// Check for mention format <@!123456> or <@123456>
|
|
const maybeId = text.replace(/^<@!?/, "").replace(/>$/, "");
|
|
if (/^\d+$/.test(maybeId)) {
|
|
ids.add(maybeId);
|
|
continue;
|
|
}
|
|
|
|
// Check for prefixed IDs
|
|
const prefix = prefixes.find((p) => text.startsWith(p));
|
|
if (prefix) {
|
|
const candidate = text.slice(prefix.length);
|
|
if (candidate) {
|
|
ids.add(candidate);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Otherwise treat as name/slug
|
|
if (normalized) {
|
|
names.add(normalized);
|
|
}
|
|
}
|
|
|
|
return { allowAll, ids, names };
|
|
}
|
|
|
|
/**
|
|
* Checks if a candidate matches the allowlist
|
|
*/
|
|
export function allowListMatches(
|
|
list: DiscordAllowList,
|
|
candidate: { id?: string; name?: string; tag?: string },
|
|
): boolean {
|
|
if (list.allowAll) {
|
|
return true;
|
|
}
|
|
|
|
if (candidate.id && list.ids.has(candidate.id)) {
|
|
return true;
|
|
}
|
|
|
|
const slug = candidate.name ? normalizeDiscordSlug(candidate.name) : "";
|
|
if (slug && list.names.has(slug)) {
|
|
return true;
|
|
}
|
|
|
|
if (candidate.tag && list.names.has(normalizeDiscordSlug(candidate.tag))) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Resolves an allowlist match with detailed match information
|
|
*/
|
|
export function resolveDiscordAllowListMatch(params: {
|
|
allowList: DiscordAllowList;
|
|
candidate: { id?: string; name?: string; tag?: string };
|
|
}): DiscordAllowListMatch {
|
|
const { allowList, candidate } = params;
|
|
|
|
if (allowList.allowAll) {
|
|
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
}
|
|
|
|
if (candidate.id && allowList.ids.has(candidate.id)) {
|
|
return { allowed: true, matchKey: candidate.id, matchSource: "id" };
|
|
}
|
|
|
|
const nameSlug = candidate.name ? normalizeDiscordSlug(candidate.name) : "";
|
|
if (nameSlug && allowList.names.has(nameSlug)) {
|
|
return { allowed: true, matchKey: nameSlug, matchSource: "name" };
|
|
}
|
|
|
|
const tagSlug = candidate.tag ? normalizeDiscordSlug(candidate.tag) : "";
|
|
if (tagSlug && allowList.names.has(tagSlug)) {
|
|
return { allowed: true, matchKey: tagSlug, matchSource: "tag" };
|
|
}
|
|
|
|
return { allowed: false };
|
|
}
|
|
|
|
/**
|
|
* Checks if a Discord user is allowed based on an allowlist
|
|
*/
|
|
export function resolveDiscordUserAllowed(params: {
|
|
allowList?: Array<string | number>;
|
|
userId: string;
|
|
userName?: string;
|
|
userTag?: string;
|
|
}): boolean {
|
|
const allowList = normalizeDiscordAllowList(params.allowList, [
|
|
"discord:",
|
|
"user:",
|
|
"pk:",
|
|
]);
|
|
if (!allowList) {
|
|
return true;
|
|
}
|
|
|
|
return allowListMatches(allowList, {
|
|
id: params.userId,
|
|
name: params.userName,
|
|
tag: params.userTag,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Checks if a user is authorized for commands in DMs
|
|
*/
|
|
export function resolveDiscordCommandAuthorized(params: {
|
|
isDirectMessage: boolean;
|
|
allowFrom?: Array<string | number>;
|
|
author: User;
|
|
}): boolean {
|
|
if (!params.isDirectMessage) {
|
|
return true;
|
|
}
|
|
|
|
const allowList = normalizeDiscordAllowList(params.allowFrom, [
|
|
"discord:",
|
|
"user:",
|
|
"pk:",
|
|
]);
|
|
if (!allowList) {
|
|
return true;
|
|
}
|
|
|
|
return allowListMatches(allowList, {
|
|
id: params.author.id,
|
|
name: params.author.username,
|
|
tag: formatDiscordUserTag(params.author),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Resolves guild entry configuration by ID or slug
|
|
*/
|
|
export function resolveDiscordGuildEntry(params: {
|
|
guild?: Guild | null;
|
|
guildEntries?: Record<string, DiscordGuildEntry>;
|
|
}): (DiscordGuildEntry & { id?: string }) | null {
|
|
const { guild, guildEntries } = params;
|
|
if (!guild || !guildEntries) {
|
|
return null;
|
|
}
|
|
|
|
// Check by ID first
|
|
const byId = guildEntries[guild.id];
|
|
if (byId) {
|
|
return { ...byId, id: guild.id };
|
|
}
|
|
|
|
// Check by slug
|
|
const slug = normalizeDiscordSlug(guild.name ?? "");
|
|
const bySlug = guildEntries[slug];
|
|
if (bySlug) {
|
|
return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
|
|
}
|
|
|
|
// Check for wildcard
|
|
const wildcard = guildEntries["*"];
|
|
if (wildcard) {
|
|
return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Builds channel key candidates for matching
|
|
*/
|
|
function buildChannelKeyCandidates(
|
|
id: string,
|
|
slug?: string,
|
|
name?: string,
|
|
allowNameMatch = true,
|
|
): string[] {
|
|
const keys: string[] = [id];
|
|
|
|
if (allowNameMatch) {
|
|
if (slug) {
|
|
keys.push(slug);
|
|
}
|
|
if (name) {
|
|
const nameSlug = normalizeDiscordSlug(name);
|
|
if (nameSlug && nameSlug !== slug) {
|
|
keys.push(nameSlug);
|
|
}
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
/**
|
|
* Resolves channel entry match from configuration
|
|
*/
|
|
function resolveChannelEntryMatch(
|
|
channels: Record<string, DiscordGuildChannelConfig>,
|
|
keys: string[],
|
|
parentKeys?: string[],
|
|
): {
|
|
entry: DiscordGuildChannelConfig;
|
|
matchKey: string;
|
|
matchSource: ChannelMatchSource;
|
|
} | null {
|
|
// Try direct keys first
|
|
for (const key of keys) {
|
|
const entry = channels[key];
|
|
if (entry) {
|
|
const source: ChannelMatchSource = /^\d+$/.test(key) ? "id" : "name";
|
|
return { entry, matchKey: key, matchSource: source };
|
|
}
|
|
}
|
|
|
|
// Try parent keys if provided
|
|
if (parentKeys) {
|
|
for (const parentKey of parentKeys) {
|
|
const entry = channels[parentKey];
|
|
if (entry) {
|
|
return { entry, matchKey: parentKey, matchSource: "parent" };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try wildcard
|
|
const wildcard = channels["*"];
|
|
if (wildcard) {
|
|
return { entry: wildcard, matchKey: "*", matchSource: "wildcard" };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Resolves channel configuration from guild info
|
|
*/
|
|
export function resolveDiscordChannelConfig(params: {
|
|
guildInfo?: (DiscordGuildEntry & { id?: string }) | null;
|
|
channelId: string;
|
|
channelName?: string;
|
|
channelSlug?: string;
|
|
}): DiscordChannelConfigResolved | null {
|
|
const { guildInfo, channelId, channelName } = params;
|
|
const channelSlug =
|
|
params.channelSlug ??
|
|
(channelName ? normalizeDiscordSlug(channelName) : "");
|
|
const channels = guildInfo?.channels;
|
|
|
|
if (!channels) {
|
|
return null;
|
|
}
|
|
|
|
const keys = buildChannelKeyCandidates(channelId, channelSlug, channelName);
|
|
const match = resolveChannelEntryMatch(channels, keys);
|
|
|
|
if (!match) {
|
|
return { allowed: false };
|
|
}
|
|
|
|
return {
|
|
allowed: match.entry.allow !== false,
|
|
requireMention: match.entry.requireMention,
|
|
skills: match.entry.skills,
|
|
enabled: match.entry.enabled,
|
|
users: match.entry.users,
|
|
systemPrompt: match.entry.systemPrompt,
|
|
autoThread: match.entry.autoThread,
|
|
matchKey: match.matchKey,
|
|
matchSource: match.matchSource,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolves channel configuration with thread/parent fallback
|
|
*/
|
|
export function resolveDiscordChannelConfigWithFallback(params: {
|
|
guildInfo?: (DiscordGuildEntry & { id?: string }) | null;
|
|
channelId: string;
|
|
channelName?: string;
|
|
channelSlug?: string;
|
|
parentId?: string;
|
|
parentName?: string;
|
|
parentSlug?: string;
|
|
isThread?: boolean;
|
|
}): DiscordChannelConfigResolved | null {
|
|
const {
|
|
guildInfo,
|
|
channelId,
|
|
channelName,
|
|
parentId,
|
|
parentName,
|
|
parentSlug,
|
|
isThread = false,
|
|
} = params;
|
|
const channelSlug =
|
|
params.channelSlug ??
|
|
(channelName ? normalizeDiscordSlug(channelName) : "");
|
|
const channels = guildInfo?.channels;
|
|
|
|
if (!channels) {
|
|
return null;
|
|
}
|
|
|
|
const resolvedParentSlug =
|
|
parentSlug ?? (parentName ? normalizeDiscordSlug(parentName) : "");
|
|
|
|
// For threads, don't match by name (thread names change)
|
|
const keys = buildChannelKeyCandidates(
|
|
channelId,
|
|
channelSlug,
|
|
channelName,
|
|
!isThread,
|
|
);
|
|
|
|
const parentKeys =
|
|
parentId || parentName || parentSlug
|
|
? buildChannelKeyCandidates(
|
|
parentId ?? "",
|
|
resolvedParentSlug,
|
|
parentName,
|
|
)
|
|
: undefined;
|
|
|
|
const match = resolveChannelEntryMatch(channels, keys, parentKeys);
|
|
|
|
if (!match) {
|
|
return { allowed: false };
|
|
}
|
|
|
|
return {
|
|
allowed: match.entry.allow !== false,
|
|
requireMention: match.entry.requireMention,
|
|
skills: match.entry.skills,
|
|
enabled: match.entry.enabled,
|
|
users: match.entry.users,
|
|
systemPrompt: match.entry.systemPrompt,
|
|
autoThread: match.entry.autoThread,
|
|
matchKey: match.matchKey,
|
|
matchSource: match.matchSource,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determines if a mention is required for the bot to respond
|
|
*/
|
|
export function resolveDiscordShouldRequireMention(params: {
|
|
isGuildMessage: boolean;
|
|
isThread: boolean;
|
|
botId?: string | null;
|
|
threadOwnerId?: string | null;
|
|
channelConfig?: DiscordChannelConfigResolved | null;
|
|
guildInfo?: (DiscordGuildEntry & { id?: string }) | null;
|
|
isAutoThreadOwnedByBot?: boolean;
|
|
}): boolean {
|
|
if (!params.isGuildMessage) {
|
|
return false;
|
|
}
|
|
|
|
// Don't require mention in threads created by the bot (autoThread)
|
|
const isBotThread =
|
|
params.isAutoThreadOwnedByBot ?? isDiscordAutoThreadOwnedByBot(params);
|
|
if (isBotThread) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
params.channelConfig?.requireMention ??
|
|
params.guildInfo?.requireMention ??
|
|
true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks if a thread was created by the bot via autoThread feature
|
|
*/
|
|
export function isDiscordAutoThreadOwnedByBot(params: {
|
|
isThread: boolean;
|
|
channelConfig?: DiscordChannelConfigResolved | null;
|
|
botId?: string | null;
|
|
threadOwnerId?: string | null;
|
|
}): boolean {
|
|
if (!params.isThread) {
|
|
return false;
|
|
}
|
|
|
|
if (!params.channelConfig?.autoThread) {
|
|
return false;
|
|
}
|
|
|
|
const botId = params.botId?.trim();
|
|
const threadOwnerId = params.threadOwnerId?.trim();
|
|
|
|
return Boolean(botId && threadOwnerId && botId === threadOwnerId);
|
|
}
|
|
|
|
/**
|
|
* Checks if a group (guild channel) is allowed by the policy
|
|
*/
|
|
export function isDiscordGroupAllowedByPolicy(params: {
|
|
groupPolicy: "open" | "disabled" | "allowlist";
|
|
guildAllowlisted: boolean;
|
|
channelAllowlistConfigured: boolean;
|
|
channelAllowed: boolean;
|
|
}): boolean {
|
|
const {
|
|
groupPolicy,
|
|
guildAllowlisted,
|
|
channelAllowlistConfigured,
|
|
channelAllowed,
|
|
} = params;
|
|
|
|
if (groupPolicy === "disabled") {
|
|
return false;
|
|
}
|
|
|
|
if (groupPolicy === "open") {
|
|
return true;
|
|
}
|
|
|
|
// allowlist mode
|
|
if (!guildAllowlisted) {
|
|
return false;
|
|
}
|
|
|
|
if (!channelAllowlistConfigured) {
|
|
return true;
|
|
}
|
|
|
|
return channelAllowed;
|
|
}
|
|
|
|
/**
|
|
* Resolves if a group DM is allowed
|
|
*/
|
|
export function resolveGroupDmAllow(params: {
|
|
channels?: Array<string | number>;
|
|
channelId: string;
|
|
channelName?: string;
|
|
channelSlug?: string;
|
|
}): boolean {
|
|
const { channels, channelId, channelName } = params;
|
|
const channelSlug =
|
|
params.channelSlug ??
|
|
(channelName ? normalizeDiscordSlug(channelName) : "");
|
|
|
|
if (!channels || channels.length === 0) {
|
|
return true;
|
|
}
|
|
|
|
const allowList = new Set(
|
|
channels.map((entry) => normalizeDiscordSlug(String(entry))),
|
|
);
|
|
const candidates = [
|
|
normalizeDiscordSlug(channelId),
|
|
channelSlug,
|
|
channelName ? normalizeDiscordSlug(channelName) : "",
|
|
].filter(Boolean);
|
|
|
|
return (
|
|
allowList.has("*") ||
|
|
candidates.some((candidate) => allowList.has(candidate))
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Determines if a reaction notification should be emitted
|
|
*/
|
|
export function shouldEmitDiscordReactionNotification(params: {
|
|
mode?: "off" | "own" | "all" | "allowlist";
|
|
botId?: string;
|
|
messageAuthorId?: string;
|
|
userId: string;
|
|
userName?: string;
|
|
userTag?: string;
|
|
allowlist?: Array<string | number>;
|
|
}): boolean {
|
|
const mode = params.mode ?? "own";
|
|
|
|
if (mode === "off") {
|
|
return false;
|
|
}
|
|
|
|
if (mode === "all") {
|
|
return true;
|
|
}
|
|
|
|
if (mode === "own") {
|
|
return Boolean(params.botId && params.messageAuthorId === params.botId);
|
|
}
|
|
|
|
if (mode === "allowlist") {
|
|
const list = normalizeDiscordAllowList(params.allowlist, [
|
|
"discord:",
|
|
"user:",
|
|
"pk:",
|
|
]);
|
|
if (!list) {
|
|
return false;
|
|
}
|
|
|
|
return allowListMatches(list, {
|
|
id: params.userId,
|
|
name: params.userName,
|
|
tag: params.userTag,
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Validates a message against all allowlist policies
|
|
*/
|
|
export function validateMessageAllowed(params: {
|
|
accountConfig: DiscordAccountConfig;
|
|
isDirectMessage: boolean;
|
|
isGroupDm: boolean;
|
|
guild?: Guild | null;
|
|
channelId: string;
|
|
channelName?: string;
|
|
author: User;
|
|
botId?: string;
|
|
}): {
|
|
allowed: boolean;
|
|
reason?: string;
|
|
channelConfig?: DiscordChannelConfigResolved | null;
|
|
guildInfo?: (DiscordGuildEntry & { id?: string }) | null;
|
|
} {
|
|
const {
|
|
accountConfig,
|
|
isDirectMessage,
|
|
isGroupDm,
|
|
guild,
|
|
channelId,
|
|
channelName,
|
|
author,
|
|
} = params;
|
|
|
|
// Handle DMs
|
|
if (isDirectMessage && !isGroupDm) {
|
|
const dmConfig = accountConfig.dm;
|
|
|
|
if (dmConfig?.enabled === false) {
|
|
return { allowed: false, reason: "DMs disabled" };
|
|
}
|
|
|
|
const dmPolicy = dmConfig?.policy ?? "pairing";
|
|
if (dmPolicy === "disabled") {
|
|
return { allowed: false, reason: "DM policy disabled" };
|
|
}
|
|
|
|
if (dmPolicy === "open") {
|
|
return { allowed: true };
|
|
}
|
|
|
|
if (dmConfig?.allowFrom) {
|
|
const isAllowed = resolveDiscordUserAllowed({
|
|
allowList: dmConfig.allowFrom,
|
|
userId: author.id,
|
|
userName: author.username,
|
|
userTag: formatDiscordUserTag(author),
|
|
});
|
|
|
|
if (isAllowed) {
|
|
return { allowed: true };
|
|
}
|
|
}
|
|
|
|
return {
|
|
allowed: false,
|
|
reason:
|
|
dmPolicy === "pairing"
|
|
? "DM pairing required"
|
|
: "User not in DM allowlist",
|
|
};
|
|
}
|
|
|
|
// Handle group DMs
|
|
if (isGroupDm) {
|
|
const dmConfig = accountConfig.dm;
|
|
|
|
if (!dmConfig?.groupEnabled) {
|
|
return { allowed: false, reason: "Group DMs disabled" };
|
|
}
|
|
|
|
const isAllowed = resolveGroupDmAllow({
|
|
channels: dmConfig.groupChannels,
|
|
channelId,
|
|
channelName,
|
|
});
|
|
|
|
if (!isAllowed) {
|
|
return { allowed: false, reason: "Group DM channel not allowed" };
|
|
}
|
|
|
|
return { allowed: true };
|
|
}
|
|
|
|
// Handle guild messages
|
|
const groupPolicy = accountConfig.groupPolicy ?? "open";
|
|
const guildInfo = resolveDiscordGuildEntry({
|
|
guild,
|
|
guildEntries: accountConfig.guilds,
|
|
});
|
|
|
|
const guildAllowlisted = guildInfo !== null;
|
|
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
|
|
|
const channelConfig = resolveDiscordChannelConfig({
|
|
guildInfo,
|
|
channelId,
|
|
channelName,
|
|
channelSlug,
|
|
});
|
|
|
|
const channelAllowlistConfigured = Boolean(guildInfo?.channels);
|
|
const channelAllowed = channelConfig?.allowed ?? false;
|
|
|
|
const isAllowed = isDiscordGroupAllowedByPolicy({
|
|
groupPolicy,
|
|
guildAllowlisted,
|
|
channelAllowlistConfigured,
|
|
channelAllowed,
|
|
});
|
|
|
|
if (!isAllowed) {
|
|
return {
|
|
allowed: false,
|
|
reason: "Channel not allowed by policy",
|
|
channelConfig,
|
|
guildInfo,
|
|
};
|
|
}
|
|
|
|
// Check user allowlist for channel
|
|
if (channelConfig?.users) {
|
|
const userAllowed = resolveDiscordUserAllowed({
|
|
allowList: channelConfig.users,
|
|
userId: author.id,
|
|
userName: author.username,
|
|
userTag: formatDiscordUserTag(author),
|
|
});
|
|
|
|
if (!userAllowed) {
|
|
return {
|
|
allowed: false,
|
|
reason: "User not in channel allowlist",
|
|
channelConfig,
|
|
guildInfo,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check user allowlist for guild
|
|
if (guildInfo?.users) {
|
|
const userAllowed = resolveDiscordUserAllowed({
|
|
allowList: guildInfo.users,
|
|
userId: author.id,
|
|
userName: author.username,
|
|
userTag: formatDiscordUserTag(author),
|
|
});
|
|
|
|
if (!userAllowed) {
|
|
return {
|
|
allowed: false,
|
|
reason: "User not in guild allowlist",
|
|
channelConfig,
|
|
guildInfo,
|
|
};
|
|
}
|
|
}
|
|
|
|
return { allowed: true, channelConfig, guildInfo };
|
|
}
|