mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 22:18:27 +00:00
chore: Enable "curly" rule to avoid single-statement if confusion/errors.
This commit is contained in:
@@ -14,19 +14,25 @@ export type ResolvedDiscordAccount = {
|
||||
|
||||
function listConfiguredAccountIds(cfg: OpenClawConfig): string[] {
|
||||
const accounts = cfg.channels?.discord?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return [];
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(accounts).filter(Boolean);
|
||||
}
|
||||
|
||||
export function listDiscordAccountIds(cfg: OpenClawConfig): string[] {
|
||||
const ids = listConfiguredAccountIds(cfg);
|
||||
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
|
||||
if (ids.length === 0) {
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
return ids.toSorted((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
export function resolveDefaultDiscordAccountId(cfg: OpenClawConfig): string {
|
||||
const ids = listDiscordAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
@@ -35,7 +41,9 @@ function resolveAccountConfig(
|
||||
accountId: string,
|
||||
): DiscordAccountConfig | undefined {
|
||||
const accounts = cfg.channels?.discord?.accounts;
|
||||
if (!accounts || typeof accounts !== "object") return undefined;
|
||||
if (!accounts || typeof accounts !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return accounts[accountId] as DiscordAccountConfig | undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,14 @@ type DiscordApiErrorPayload = {
|
||||
|
||||
function parseDiscordApiErrorPayload(text: string): DiscordApiErrorPayload | null {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.parse(trimmed);
|
||||
if (payload && typeof payload === "object") return payload as DiscordApiErrorPayload;
|
||||
if (payload && typeof payload === "object") {
|
||||
return payload as DiscordApiErrorPayload;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -34,22 +38,30 @@ function parseRetryAfterSeconds(text: string, response: Response): number | unde
|
||||
payload && typeof payload.retry_after === "number" && Number.isFinite(payload.retry_after)
|
||||
? payload.retry_after
|
||||
: undefined;
|
||||
if (retryAfter !== undefined) return retryAfter;
|
||||
if (retryAfter !== undefined) {
|
||||
return retryAfter;
|
||||
}
|
||||
const header = response.headers.get("Retry-After");
|
||||
if (!header) return undefined;
|
||||
if (!header) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(header);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function formatRetryAfterSeconds(value: number | undefined): string | undefined {
|
||||
if (value === undefined || !Number.isFinite(value) || value < 0) return undefined;
|
||||
if (value === undefined || !Number.isFinite(value) || value < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const rounded = value < 10 ? value.toFixed(1) : Math.round(value).toString();
|
||||
return `${rounded}s`;
|
||||
}
|
||||
|
||||
function formatDiscordApiErrorText(text: string): string | undefined {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const payload = parseDiscordApiErrorPayload(trimmed);
|
||||
if (!payload) {
|
||||
const looksJson = trimmed.startsWith("{") && trimmed.endsWith("}");
|
||||
|
||||
@@ -27,25 +27,41 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
}
|
||||
|
||||
function shouldAuditChannelConfig(config: DiscordGuildChannelConfig | undefined) {
|
||||
if (!config) return true;
|
||||
if (config.allow === false) return false;
|
||||
if (config.enabled === false) return false;
|
||||
if (!config) {
|
||||
return true;
|
||||
}
|
||||
if (config.allow === false) {
|
||||
return false;
|
||||
}
|
||||
if (config.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function listConfiguredGuildChannelKeys(
|
||||
guilds: Record<string, DiscordGuildEntry> | undefined,
|
||||
): string[] {
|
||||
if (!guilds) return [];
|
||||
if (!guilds) {
|
||||
return [];
|
||||
}
|
||||
const ids = new Set<string>();
|
||||
for (const entry of Object.values(guilds)) {
|
||||
if (!entry || typeof entry !== "object") continue;
|
||||
if (!entry || typeof entry !== "object") {
|
||||
continue;
|
||||
}
|
||||
const channelsRaw = (entry as { channels?: unknown }).channels;
|
||||
if (!isRecord(channelsRaw)) continue;
|
||||
if (!isRecord(channelsRaw)) {
|
||||
continue;
|
||||
}
|
||||
for (const [key, value] of Object.entries(channelsRaw)) {
|
||||
const channelId = String(key).trim();
|
||||
if (!channelId) continue;
|
||||
if (!shouldAuditChannelConfig(value as DiscordGuildChannelConfig | undefined)) continue;
|
||||
if (!channelId) {
|
||||
continue;
|
||||
}
|
||||
if (!shouldAuditChannelConfig(value as DiscordGuildChannelConfig | undefined)) {
|
||||
continue;
|
||||
}
|
||||
ids.add(channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ function hasBalancedFences(chunk: string) {
|
||||
let open: { markerChar: string; markerLen: number } | null = null;
|
||||
for (const line of chunk.split("\n")) {
|
||||
const match = line.match(/^( {0,3})(`{3,}|~{3,})(.*)$/);
|
||||
if (!match) continue;
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const marker = match[2];
|
||||
if (!open) {
|
||||
open = { markerChar: marker[0], markerLen: marker.length };
|
||||
|
||||
@@ -24,13 +24,17 @@ const DEFAULT_MAX_LINES = 17;
|
||||
const FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
||||
|
||||
function countLines(text: string) {
|
||||
if (!text) return 0;
|
||||
if (!text) {
|
||||
return 0;
|
||||
}
|
||||
return text.split("\n").length;
|
||||
}
|
||||
|
||||
function parseFenceLine(line: string): OpenFence | null {
|
||||
const match = line.match(FENCE_RE);
|
||||
if (!match) return null;
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const indent = match[1] ?? "";
|
||||
const marker = match[2] ?? "";
|
||||
return {
|
||||
@@ -46,10 +50,16 @@ function closeFenceLine(openFence: OpenFence) {
|
||||
}
|
||||
|
||||
function closeFenceIfNeeded(text: string, openFence: OpenFence | null) {
|
||||
if (!openFence) return text;
|
||||
if (!openFence) {
|
||||
return text;
|
||||
}
|
||||
const closeLine = closeFenceLine(openFence);
|
||||
if (!text) return closeLine;
|
||||
if (!text.endsWith("\n")) return `${text}\n${closeLine}`;
|
||||
if (!text) {
|
||||
return closeLine;
|
||||
}
|
||||
if (!text.endsWith("\n")) {
|
||||
return `${text}\n${closeLine}`;
|
||||
}
|
||||
return `${text}${closeLine}`;
|
||||
}
|
||||
|
||||
@@ -59,7 +69,9 @@ function splitLongLine(
|
||||
opts: { preserveWhitespace: boolean },
|
||||
): string[] {
|
||||
const limit = Math.max(1, Math.floor(maxChars));
|
||||
if (line.length <= limit) return [line];
|
||||
if (line.length <= limit) {
|
||||
return [line];
|
||||
}
|
||||
const out: string[] = [];
|
||||
let remaining = line;
|
||||
while (remaining.length > limit) {
|
||||
@@ -76,12 +88,16 @@ function splitLongLine(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (breakIdx <= 0) breakIdx = limit;
|
||||
if (breakIdx <= 0) {
|
||||
breakIdx = limit;
|
||||
}
|
||||
out.push(remaining.slice(0, breakIdx));
|
||||
// Keep the separator for the next segment so words don't get glued together.
|
||||
remaining = remaining.slice(breakIdx);
|
||||
}
|
||||
if (remaining.length) out.push(remaining);
|
||||
if (remaining.length) {
|
||||
out.push(remaining);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -94,10 +110,14 @@ export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}):
|
||||
const maxLines = Math.max(1, Math.floor(opts.maxLines ?? DEFAULT_MAX_LINES));
|
||||
|
||||
const body = text ?? "";
|
||||
if (!body) return [];
|
||||
if (!body) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const alreadyOk = body.length <= maxChars && countLines(body) <= maxLines;
|
||||
if (alreadyOk) return [body];
|
||||
if (alreadyOk) {
|
||||
return [body];
|
||||
}
|
||||
|
||||
const lines = body.split("\n");
|
||||
const chunks: string[] = [];
|
||||
@@ -107,9 +127,13 @@ export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}):
|
||||
let openFence: OpenFence | null = null;
|
||||
|
||||
const flush = () => {
|
||||
if (!current) return;
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
const payload = closeFenceIfNeeded(current, openFence);
|
||||
if (payload.trim().length) chunks.push(payload);
|
||||
if (payload.trim().length) {
|
||||
chunks.push(payload);
|
||||
}
|
||||
current = "";
|
||||
currentLines = 0;
|
||||
if (openFence) {
|
||||
@@ -162,7 +186,9 @@ export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}):
|
||||
|
||||
if (current.length > 0) {
|
||||
current += addition;
|
||||
if (!isLineContinuation) currentLines += 1;
|
||||
if (!isLineContinuation) {
|
||||
currentLines += 1;
|
||||
}
|
||||
} else {
|
||||
current = segment;
|
||||
currentLines = 1;
|
||||
@@ -174,7 +200,9 @@ export function chunkDiscordText(text: string, opts: ChunkDiscordTextOpts = {}):
|
||||
|
||||
if (current.length) {
|
||||
const payload = closeFenceIfNeeded(current, openFence);
|
||||
if (payload.trim().length) chunks.push(payload);
|
||||
if (payload.trim().length) {
|
||||
chunks.push(payload);
|
||||
}
|
||||
}
|
||||
|
||||
return rebalanceReasoningItalics(text, chunks);
|
||||
@@ -210,11 +238,15 @@ export function chunkDiscordTextWithMode(
|
||||
// each chunk and reopen at the start of the next so every chunk renders
|
||||
// consistently.
|
||||
function rebalanceReasoningItalics(source: string, chunks: string[]): string[] {
|
||||
if (chunks.length <= 1) return chunks;
|
||||
if (chunks.length <= 1) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
const opensWithReasoningItalics =
|
||||
source.startsWith("Reasoning:\n_") && source.trimEnd().endsWith("_");
|
||||
if (!opensWithReasoningItalics) return chunks;
|
||||
if (!opensWithReasoningItalics) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
const adjusted = [...chunks];
|
||||
for (let i = 0; i < adjusted.length; i++) {
|
||||
@@ -227,7 +259,9 @@ function rebalanceReasoningItalics(source: string, chunks: string[]): string[] {
|
||||
adjusted[i] = `${current}_`;
|
||||
}
|
||||
|
||||
if (isLast) break;
|
||||
if (isLast) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Re-open italics on the next chunk if needed.
|
||||
const next = adjusted[i + 1];
|
||||
|
||||
@@ -23,7 +23,9 @@ export async function listDiscordDirectoryGroupsLive(
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const token = normalizeDiscordToken(account.token);
|
||||
if (!token) return [];
|
||||
if (!token) {
|
||||
return [];
|
||||
}
|
||||
const query = normalizeQuery(params.query);
|
||||
const guilds = await fetchDiscord<DiscordGuild[]>("/users/@me/guilds", token);
|
||||
const rows: ChannelDirectoryEntry[] = [];
|
||||
@@ -32,8 +34,12 @@ export async function listDiscordDirectoryGroupsLive(
|
||||
const channels = await fetchDiscord<DiscordChannel[]>(`/guilds/${guild.id}/channels`, token);
|
||||
for (const channel of channels) {
|
||||
const name = channel.name?.trim();
|
||||
if (!name) continue;
|
||||
if (query && !normalizeDiscordSlug(name).includes(normalizeDiscordSlug(query))) continue;
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
if (query && !normalizeDiscordSlug(name).includes(normalizeDiscordSlug(query))) {
|
||||
continue;
|
||||
}
|
||||
rows.push({
|
||||
kind: "group",
|
||||
id: `channel:${channel.id}`,
|
||||
@@ -55,9 +61,13 @@ export async function listDiscordDirectoryPeersLive(
|
||||
): Promise<ChannelDirectoryEntry[]> {
|
||||
const account = resolveDiscordAccount({ cfg: params.cfg, accountId: params.accountId });
|
||||
const token = normalizeDiscordToken(account.token);
|
||||
if (!token) return [];
|
||||
if (!token) {
|
||||
return [];
|
||||
}
|
||||
const query = normalizeQuery(params.query);
|
||||
if (!query) return [];
|
||||
if (!query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const guilds = await fetchDiscord<DiscordGuild[]>("/users/@me/guilds", token);
|
||||
const rows: ChannelDirectoryEntry[] = [];
|
||||
@@ -74,7 +84,9 @@ export async function listDiscordDirectoryPeersLive(
|
||||
);
|
||||
for (const member of members) {
|
||||
const user = member.user;
|
||||
if (!user?.id) continue;
|
||||
if (!user?.id) {
|
||||
continue;
|
||||
}
|
||||
const name = member.nick?.trim() || user.global_name?.trim() || user.username?.trim();
|
||||
rows.push({
|
||||
kind: "user",
|
||||
@@ -84,7 +96,9 @@ export async function listDiscordDirectoryPeersLive(
|
||||
rank: buildUserRank(user),
|
||||
raw: member,
|
||||
});
|
||||
if (rows.length >= limit) return rows;
|
||||
if (rows.length >= limit) {
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,12 @@ const shouldPromoteGatewayDebug = (message: string) =>
|
||||
INFO_DEBUG_MARKERS.some((marker) => message.includes(marker));
|
||||
|
||||
const formatGatewayMetrics = (metrics: unknown) => {
|
||||
if (metrics === null || metrics === undefined) return String(metrics);
|
||||
if (typeof metrics === "string") return metrics;
|
||||
if (metrics === null || metrics === undefined) {
|
||||
return String(metrics);
|
||||
}
|
||||
if (typeof metrics === "string") {
|
||||
return metrics;
|
||||
}
|
||||
if (typeof metrics === "number" || typeof metrics === "boolean" || typeof metrics === "bigint") {
|
||||
return String(metrics);
|
||||
}
|
||||
@@ -32,7 +36,9 @@ export function attachDiscordGatewayLogging(params: {
|
||||
runtime: RuntimeEnv;
|
||||
}) {
|
||||
const { emitter, runtime } = params;
|
||||
if (!emitter) return () => {};
|
||||
if (!emitter) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const onGatewayDebug = (msg: unknown) => {
|
||||
const message = String(msg);
|
||||
|
||||
@@ -24,7 +24,9 @@ export async function waitForDiscordGatewayStop(params: {
|
||||
emitter?.removeListener("error", onGatewayErrorEvent);
|
||||
};
|
||||
const finishResolve = () => {
|
||||
if (settled) return;
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
try {
|
||||
@@ -34,7 +36,9 @@ export async function waitForDiscordGatewayStop(params: {
|
||||
}
|
||||
};
|
||||
const finishReject = (err: unknown) => {
|
||||
if (settled) return;
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
cleanup();
|
||||
try {
|
||||
|
||||
@@ -53,13 +53,17 @@ export function normalizeDiscordAllowList(
|
||||
raw: Array<string | number> | undefined,
|
||||
prefixes: string[],
|
||||
) {
|
||||
if (!raw || raw.length === 0) return 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;
|
||||
if (!text || text === "*") {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeDiscordSlug(text);
|
||||
const maybeId = text.replace(/^<@!?/, "").replace(/>$/, "");
|
||||
if (/^\d+$/.test(maybeId)) {
|
||||
@@ -69,7 +73,9 @@ export function normalizeDiscordAllowList(
|
||||
const prefix = prefixes.find((entry) => text.startsWith(entry));
|
||||
if (prefix) {
|
||||
const candidate = text.slice(prefix.length);
|
||||
if (candidate) ids.add(candidate);
|
||||
if (candidate) {
|
||||
ids.add(candidate);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (normalized) {
|
||||
@@ -92,11 +98,19 @@ export function allowListMatches(
|
||||
list: DiscordAllowList,
|
||||
candidate: { id?: string; name?: string; tag?: string },
|
||||
) {
|
||||
if (list.allowAll) return true;
|
||||
if (candidate.id && list.ids.has(candidate.id)) return true;
|
||||
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;
|
||||
if (slug && list.names.has(slug)) {
|
||||
return true;
|
||||
}
|
||||
if (candidate.tag && list.names.has(normalizeDiscordSlug(candidate.tag))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -129,7 +143,9 @@ export function resolveDiscordUserAllowed(params: {
|
||||
userTag?: string;
|
||||
}) {
|
||||
const allowList = normalizeDiscordAllowList(params.allowList, ["discord:", "user:"]);
|
||||
if (!allowList) return true;
|
||||
if (!allowList) {
|
||||
return true;
|
||||
}
|
||||
return allowListMatches(allowList, {
|
||||
id: params.userId,
|
||||
name: params.userName,
|
||||
@@ -143,9 +159,13 @@ export function resolveDiscordCommandAuthorized(params: {
|
||||
guildInfo?: DiscordGuildEntryResolved | null;
|
||||
author: User;
|
||||
}) {
|
||||
if (!params.isDirectMessage) return true;
|
||||
if (!params.isDirectMessage) {
|
||||
return true;
|
||||
}
|
||||
const allowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:"]);
|
||||
if (!allowList) return true;
|
||||
if (!allowList) {
|
||||
return true;
|
||||
}
|
||||
return allowListMatches(allowList, {
|
||||
id: params.author.id,
|
||||
name: params.author.username,
|
||||
@@ -159,14 +179,22 @@ export function resolveDiscordGuildEntry(params: {
|
||||
}): DiscordGuildEntryResolved | null {
|
||||
const guild = params.guild;
|
||||
const entries = params.guildEntries;
|
||||
if (!guild || !entries) return null;
|
||||
if (!guild || !entries) {
|
||||
return null;
|
||||
}
|
||||
const byId = entries[guild.id];
|
||||
if (byId) return { ...byId, id: guild.id };
|
||||
if (byId) {
|
||||
return { ...byId, id: guild.id };
|
||||
}
|
||||
const slug = normalizeDiscordSlug(guild.name ?? "");
|
||||
const bySlug = entries[slug];
|
||||
if (bySlug) return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
|
||||
if (bySlug) {
|
||||
return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
|
||||
}
|
||||
const wildcard = entries["*"];
|
||||
if (wildcard) return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
|
||||
if (wildcard) {
|
||||
return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -227,7 +255,9 @@ export function resolveDiscordChannelConfig(params: {
|
||||
}): DiscordChannelConfigResolved | null {
|
||||
const { guildInfo, channelId, channelName, channelSlug } = params;
|
||||
const channels = guildInfo?.channels;
|
||||
if (!channels) return null;
|
||||
if (!channels) {
|
||||
return null;
|
||||
}
|
||||
const match = resolveDiscordChannelEntryMatch(channels, {
|
||||
id: channelId,
|
||||
name: channelName,
|
||||
@@ -258,7 +288,9 @@ export function resolveDiscordChannelConfigWithFallback(params: {
|
||||
scope,
|
||||
} = params;
|
||||
const channels = guildInfo?.channels;
|
||||
if (!channels) return null;
|
||||
if (!channels) {
|
||||
return null;
|
||||
}
|
||||
const resolvedParentSlug = parentSlug ?? (parentName ? normalizeDiscordSlug(parentName) : "");
|
||||
const match = resolveDiscordChannelEntryMatch(
|
||||
channels,
|
||||
@@ -289,10 +321,14 @@ export function resolveDiscordShouldRequireMention(params: {
|
||||
/** Pass pre-computed value to avoid redundant checks. */
|
||||
isAutoThreadOwnedByBot?: boolean;
|
||||
}): boolean {
|
||||
if (!params.isGuildMessage) return false;
|
||||
if (!params.isGuildMessage) {
|
||||
return false;
|
||||
}
|
||||
// Only skip mention requirement in threads created by the bot (when autoThread is enabled).
|
||||
const isBotThread = params.isAutoThreadOwnedByBot ?? isDiscordAutoThreadOwnedByBot(params);
|
||||
if (isBotThread) return false;
|
||||
if (isBotThread) {
|
||||
return false;
|
||||
}
|
||||
return params.channelConfig?.requireMention ?? params.guildInfo?.requireMention ?? true;
|
||||
}
|
||||
|
||||
@@ -302,8 +338,12 @@ export function isDiscordAutoThreadOwnedByBot(params: {
|
||||
botId?: string | null;
|
||||
threadOwnerId?: string | null;
|
||||
}): boolean {
|
||||
if (!params.isThread) return false;
|
||||
if (!params.channelConfig?.autoThread) return false;
|
||||
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);
|
||||
@@ -316,10 +356,18 @@ export function isDiscordGroupAllowedByPolicy(params: {
|
||||
channelAllowed: boolean;
|
||||
}): boolean {
|
||||
const { groupPolicy, guildAllowlisted, channelAllowlistConfigured, channelAllowed } = params;
|
||||
if (groupPolicy === "disabled") return false;
|
||||
if (groupPolicy === "open") return true;
|
||||
if (!guildAllowlisted) return false;
|
||||
if (!channelAllowlistConfigured) return true;
|
||||
if (groupPolicy === "disabled") {
|
||||
return false;
|
||||
}
|
||||
if (groupPolicy === "open") {
|
||||
return true;
|
||||
}
|
||||
if (!guildAllowlisted) {
|
||||
return false;
|
||||
}
|
||||
if (!channelAllowlistConfigured) {
|
||||
return true;
|
||||
}
|
||||
return channelAllowed;
|
||||
}
|
||||
|
||||
@@ -330,7 +378,9 @@ export function resolveGroupDmAllow(params: {
|
||||
channelSlug: string;
|
||||
}) {
|
||||
const { channels, channelId, channelName, channelSlug } = params;
|
||||
if (!channels || channels.length === 0) return true;
|
||||
if (!channels || channels.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const allowList = new Set(channels.map((entry) => normalizeDiscordSlug(String(entry))));
|
||||
const candidates = [
|
||||
normalizeDiscordSlug(channelId),
|
||||
@@ -350,14 +400,20 @@ export function shouldEmitDiscordReactionNotification(params: {
|
||||
allowlist?: Array<string | number>;
|
||||
}) {
|
||||
const mode = params.mode ?? "own";
|
||||
if (mode === "off") return false;
|
||||
if (mode === "all") return true;
|
||||
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:"]);
|
||||
if (!list) return false;
|
||||
if (!list) {
|
||||
return false;
|
||||
}
|
||||
return allowListMatches(list, {
|
||||
id: params.userId,
|
||||
name: params.userName,
|
||||
|
||||
@@ -65,12 +65,16 @@ export function buildExecApprovalCustomId(
|
||||
export function parseExecApprovalData(
|
||||
data: ComponentData,
|
||||
): { approvalId: string; action: ExecApprovalDecision } | null {
|
||||
if (!data || typeof data !== "object") return null;
|
||||
if (!data || typeof data !== "object") {
|
||||
return null;
|
||||
}
|
||||
const coerce = (value: unknown) =>
|
||||
typeof value === "string" || typeof value === "number" ? String(value) : "";
|
||||
const rawId = coerce(data.id);
|
||||
const rawAction = coerce(data.action);
|
||||
if (!rawId || !rawAction) return null;
|
||||
if (!rawId || !rawAction) {
|
||||
return null;
|
||||
}
|
||||
const action = rawAction as ExecApprovalDecision;
|
||||
if (action !== "allow-once" && action !== "allow-always" && action !== "deny") {
|
||||
return null;
|
||||
@@ -205,19 +209,29 @@ export class DiscordExecApprovalHandler {
|
||||
|
||||
shouldHandle(request: ExecApprovalRequest): boolean {
|
||||
const config = this.opts.config;
|
||||
if (!config.enabled) return false;
|
||||
if (!config.approvers || config.approvers.length === 0) return false;
|
||||
if (!config.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (!config.approvers || config.approvers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check agent filter
|
||||
if (config.agentFilter?.length) {
|
||||
if (!request.request.agentId) return false;
|
||||
if (!config.agentFilter.includes(request.request.agentId)) return false;
|
||||
if (!request.request.agentId) {
|
||||
return false;
|
||||
}
|
||||
if (!config.agentFilter.includes(request.request.agentId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check session filter (substring match)
|
||||
if (config.sessionFilter?.length) {
|
||||
const session = request.request.sessionKey;
|
||||
if (!session) return false;
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
const matches = config.sessionFilter.some((p) => {
|
||||
try {
|
||||
return session.includes(p) || new RegExp(p).test(session);
|
||||
@@ -225,14 +239,18 @@ export class DiscordExecApprovalHandler {
|
||||
return session.includes(p);
|
||||
}
|
||||
});
|
||||
if (!matches) return false;
|
||||
if (!matches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.started) return;
|
||||
if (this.started) {
|
||||
return;
|
||||
}
|
||||
this.started = true;
|
||||
|
||||
const config = this.opts.config;
|
||||
@@ -270,7 +288,9 @@ export class DiscordExecApprovalHandler {
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this.started) return;
|
||||
if (!this.started) {
|
||||
return;
|
||||
}
|
||||
this.started = false;
|
||||
|
||||
// Clear all pending timeouts
|
||||
@@ -297,7 +317,9 @@ export class DiscordExecApprovalHandler {
|
||||
}
|
||||
|
||||
private async handleApprovalRequested(request: ExecApprovalRequest): Promise<void> {
|
||||
if (!this.shouldHandle(request)) return;
|
||||
if (!this.shouldHandle(request)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug(`discord exec approvals: received request ${request.id}`);
|
||||
|
||||
@@ -394,7 +416,9 @@ export class DiscordExecApprovalHandler {
|
||||
|
||||
private async handleApprovalResolved(resolved: ExecApprovalResolved): Promise<void> {
|
||||
const pending = this.pending.get(resolved.id);
|
||||
if (!pending) return;
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(pending.timeoutId);
|
||||
this.pending.delete(resolved.id);
|
||||
@@ -402,7 +426,9 @@ export class DiscordExecApprovalHandler {
|
||||
const request = this.requestCache.get(resolved.id);
|
||||
this.requestCache.delete(resolved.id);
|
||||
|
||||
if (!request) return;
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug(`discord exec approvals: resolved ${resolved.id} with ${resolved.decision}`);
|
||||
|
||||
@@ -415,14 +441,18 @@ export class DiscordExecApprovalHandler {
|
||||
|
||||
private async handleApprovalTimeout(approvalId: string): Promise<void> {
|
||||
const pending = this.pending.get(approvalId);
|
||||
if (!pending) return;
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.pending.delete(approvalId);
|
||||
|
||||
const request = this.requestCache.get(approvalId);
|
||||
this.requestCache.delete(approvalId);
|
||||
|
||||
if (!request) return;
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
|
||||
logDebug(`discord exec approvals: timeout for ${approvalId}`);
|
||||
|
||||
|
||||
@@ -7,8 +7,12 @@ export function resolveDiscordSystemLocation(params: {
|
||||
channelName: string;
|
||||
}) {
|
||||
const { isDirectMessage, isGroupDm, guild, channelName } = params;
|
||||
if (isDirectMessage) return "DM";
|
||||
if (isGroupDm) return `Group DM #${channelName}`;
|
||||
if (isDirectMessage) {
|
||||
return "DM";
|
||||
}
|
||||
if (isGroupDm) {
|
||||
return `Group DM #${channelName}`;
|
||||
}
|
||||
return guild?.name ? `${guild.name} #${channelName}` : `#${channelName}`;
|
||||
}
|
||||
|
||||
@@ -28,7 +32,9 @@ export function formatDiscordUserTag(user: User) {
|
||||
}
|
||||
|
||||
export function resolveTimestampMs(timestamp?: string | null) {
|
||||
if (!timestamp) return undefined;
|
||||
if (!timestamp) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Date.parse(timestamp);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ function logSlowDiscordListener(params: {
|
||||
event: string;
|
||||
durationMs: number;
|
||||
}) {
|
||||
if (params.durationMs < DISCORD_SLOW_LISTENER_THRESHOLD_MS) return;
|
||||
if (params.durationMs < DISCORD_SLOW_LISTENER_THRESHOLD_MS) {
|
||||
return;
|
||||
}
|
||||
const duration = formatDurationSeconds(params.durationMs, {
|
||||
decimals: 1,
|
||||
unit: "seconds",
|
||||
@@ -180,10 +182,16 @@ async function handleDiscordReactionEvent(params: {
|
||||
}) {
|
||||
try {
|
||||
const { data, client, action, botUserId, guildEntries } = params;
|
||||
if (!("user" in data)) return;
|
||||
if (!("user" in data)) {
|
||||
return;
|
||||
}
|
||||
const user = data.user;
|
||||
if (!user || user.bot) return;
|
||||
if (!data.guild_id) return;
|
||||
if (!user || user.bot) {
|
||||
return;
|
||||
}
|
||||
if (!data.guild_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const guildInfo = resolveDiscordGuildEntry({
|
||||
guild: data.guild ?? undefined,
|
||||
@@ -194,7 +202,9 @@ async function handleDiscordReactionEvent(params: {
|
||||
}
|
||||
|
||||
const channel = await client.fetchChannel(data.channel_id);
|
||||
if (!channel) return;
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
const channelName = "name" in channel ? (channel.name ?? undefined) : undefined;
|
||||
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
|
||||
const channelType = "type" in channel ? channel.type : undefined;
|
||||
@@ -226,9 +236,13 @@ async function handleDiscordReactionEvent(params: {
|
||||
parentSlug,
|
||||
scope: isThreadChannel ? "thread" : "channel",
|
||||
});
|
||||
if (channelConfig?.allowed === false) return;
|
||||
if (channelConfig?.allowed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (botUserId && user.id === botUserId) return;
|
||||
if (botUserId && user.id === botUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reactionMode = guildInfo?.reactionNotifications ?? "own";
|
||||
const message = await data.message.fetch().catch(() => null);
|
||||
@@ -242,7 +256,9 @@ async function handleDiscordReactionEvent(params: {
|
||||
userTag: formatDiscordUserTag(user),
|
||||
allowlist: guildInfo?.users,
|
||||
});
|
||||
if (!shouldNotify) return;
|
||||
if (!shouldNotify) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojiLabel = formatDiscordReactionEmoji(data.emoji);
|
||||
const actorLabel = formatDiscordUserTag(user);
|
||||
@@ -290,7 +306,9 @@ export class DiscordPresenceListener extends PresenceUpdateListener {
|
||||
"user" in data && data.user && typeof data.user === "object" && "id" in data.user
|
||||
? String(data.user.id)
|
||||
: undefined;
|
||||
if (!userId) return;
|
||||
if (!userId) {
|
||||
return;
|
||||
}
|
||||
setPresence(
|
||||
this.accountId,
|
||||
userId,
|
||||
|
||||
@@ -61,12 +61,16 @@ export async function preflightDiscordMessage(
|
||||
const logger = getChildLogger({ module: "discord-auto-reply" });
|
||||
const message = params.data.message;
|
||||
const author = params.data.author;
|
||||
if (!author) return null;
|
||||
if (!author) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allowBots = params.discordConfig?.allowBots ?? false;
|
||||
if (author.bot) {
|
||||
// Always ignore own messages to prevent self-reply loops
|
||||
if (params.botUserId && author.id === params.botUserId) return null;
|
||||
if (params.botUserId && author.id === params.botUserId) {
|
||||
return null;
|
||||
}
|
||||
if (!allowBots) {
|
||||
logVerbose("discord: drop bot message (allowBots=false)");
|
||||
return null;
|
||||
@@ -300,7 +304,9 @@ export async function preflightDiscordMessage(
|
||||
channelName: displayChannelName,
|
||||
channelSlug: displayChannelSlug,
|
||||
});
|
||||
if (isGroupDm && !groupDmAllowed) return null;
|
||||
if (isGroupDm && !groupDmAllowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const channelAllowlistConfigured =
|
||||
Boolean(guildInfo?.channels) && Object.keys(guildInfo?.channels ?? {}).length > 0;
|
||||
|
||||
@@ -47,22 +47,34 @@ export function createDiscordMessageHandler(params: {
|
||||
buildKey: (entry) => {
|
||||
const message = entry.data.message;
|
||||
const authorId = entry.data.author?.id;
|
||||
if (!message || !authorId) return null;
|
||||
if (!message || !authorId) {
|
||||
return null;
|
||||
}
|
||||
const channelId = message.channelId;
|
||||
if (!channelId) return null;
|
||||
if (!channelId) {
|
||||
return null;
|
||||
}
|
||||
return `discord:${params.accountId}:${channelId}:${authorId}`;
|
||||
},
|
||||
shouldDebounce: (entry) => {
|
||||
const message = entry.data.message;
|
||||
if (!message) return false;
|
||||
if (message.attachments && message.attachments.length > 0) return false;
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
return false;
|
||||
}
|
||||
const baseText = resolveDiscordMessageText(message, { includeForwarded: false });
|
||||
if (!baseText.trim()) return false;
|
||||
if (!baseText.trim()) {
|
||||
return false;
|
||||
}
|
||||
return !hasControlCommand(baseText, params.cfg);
|
||||
},
|
||||
onFlush: async (entries) => {
|
||||
const last = entries.at(-1);
|
||||
if (!last) return;
|
||||
if (!last) {
|
||||
return;
|
||||
}
|
||||
if (entries.length === 1) {
|
||||
const ctx = await preflightDiscordMessage({
|
||||
...params,
|
||||
@@ -71,7 +83,9 @@ export function createDiscordMessageHandler(params: {
|
||||
data: last.data,
|
||||
client: last.client,
|
||||
});
|
||||
if (!ctx) return;
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
await processDiscordMessage(ctx);
|
||||
return;
|
||||
}
|
||||
@@ -100,7 +114,9 @@ export function createDiscordMessageHandler(params: {
|
||||
data: syntheticData,
|
||||
client: last.client,
|
||||
});
|
||||
if (!ctx) return;
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
if (entries.length > 1) {
|
||||
const ids = entries.map((entry) => entry.data.message?.id).filter(Boolean) as string[];
|
||||
if (ids.length > 0) {
|
||||
|
||||
@@ -55,7 +55,9 @@ export async function resolveDiscordChannelInfo(
|
||||
): Promise<DiscordChannelInfo | null> {
|
||||
const cached = DISCORD_CHANNEL_INFO_CACHE.get(channelId);
|
||||
if (cached) {
|
||||
if (cached.expiresAt > Date.now()) return cached.value;
|
||||
if (cached.expiresAt > Date.now()) {
|
||||
return cached.value;
|
||||
}
|
||||
DISCORD_CHANNEL_INFO_CACHE.delete(channelId);
|
||||
}
|
||||
try {
|
||||
@@ -98,7 +100,9 @@ export async function resolveMediaList(
|
||||
maxBytes: number,
|
||||
): Promise<DiscordMediaInfo[]> {
|
||||
const attachments = message.attachments ?? [];
|
||||
if (attachments.length === 0) return [];
|
||||
if (attachments.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const out: DiscordMediaInfo[] = [];
|
||||
for (const attachment of attachments) {
|
||||
try {
|
||||
@@ -127,22 +131,34 @@ export async function resolveMediaList(
|
||||
|
||||
function inferPlaceholder(attachment: APIAttachment): string {
|
||||
const mime = attachment.content_type ?? "";
|
||||
if (mime.startsWith("image/")) return "<media:image>";
|
||||
if (mime.startsWith("video/")) return "<media:video>";
|
||||
if (mime.startsWith("audio/")) return "<media:audio>";
|
||||
if (mime.startsWith("image/")) {
|
||||
return "<media:image>";
|
||||
}
|
||||
if (mime.startsWith("video/")) {
|
||||
return "<media:video>";
|
||||
}
|
||||
if (mime.startsWith("audio/")) {
|
||||
return "<media:audio>";
|
||||
}
|
||||
return "<media:document>";
|
||||
}
|
||||
|
||||
function isImageAttachment(attachment: APIAttachment): boolean {
|
||||
const mime = attachment.content_type ?? "";
|
||||
if (mime.startsWith("image/")) return true;
|
||||
if (mime.startsWith("image/")) {
|
||||
return true;
|
||||
}
|
||||
const name = attachment.filename?.toLowerCase() ?? "";
|
||||
if (!name) return false;
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
return /\.(avif|bmp|gif|heic|heif|jpe?g|png|tiff?|webp)$/.test(name);
|
||||
}
|
||||
|
||||
function buildDiscordAttachmentPlaceholder(attachments?: APIAttachment[]): string {
|
||||
if (!attachments || attachments.length === 0) return "";
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return "";
|
||||
}
|
||||
const count = attachments.length;
|
||||
const allImages = attachments.every(isImageAttachment);
|
||||
const label = allImages ? "image" : "file";
|
||||
@@ -161,22 +177,34 @@ export function resolveDiscordMessageText(
|
||||
message.embeds?.[0]?.description ||
|
||||
options?.fallbackText?.trim() ||
|
||||
"";
|
||||
if (!options?.includeForwarded) return baseText;
|
||||
if (!options?.includeForwarded) {
|
||||
return baseText;
|
||||
}
|
||||
const forwardedText = resolveDiscordForwardedMessagesText(message);
|
||||
if (!forwardedText) return baseText;
|
||||
if (!baseText) return forwardedText;
|
||||
if (!forwardedText) {
|
||||
return baseText;
|
||||
}
|
||||
if (!baseText) {
|
||||
return forwardedText;
|
||||
}
|
||||
return `${baseText}\n${forwardedText}`;
|
||||
}
|
||||
|
||||
function resolveDiscordForwardedMessagesText(message: Message): string {
|
||||
const snapshots = resolveDiscordMessageSnapshots(message);
|
||||
if (snapshots.length === 0) return "";
|
||||
if (snapshots.length === 0) {
|
||||
return "";
|
||||
}
|
||||
const forwardedBlocks = snapshots
|
||||
.map((snapshot) => {
|
||||
const snapshotMessage = snapshot.message;
|
||||
if (!snapshotMessage) return null;
|
||||
if (!snapshotMessage) {
|
||||
return null;
|
||||
}
|
||||
const text = resolveDiscordSnapshotMessageText(snapshotMessage);
|
||||
if (!text) return null;
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const authorLabel = formatDiscordSnapshotAuthor(snapshotMessage.author);
|
||||
const heading = authorLabel
|
||||
? `[Forwarded message from ${authorLabel}]`
|
||||
@@ -184,7 +212,9 @@ function resolveDiscordForwardedMessagesText(message: Message): string {
|
||||
return `${heading}\n${text}`;
|
||||
})
|
||||
.filter((entry): entry is string => Boolean(entry));
|
||||
if (forwardedBlocks.length === 0) return "";
|
||||
if (forwardedBlocks.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return forwardedBlocks.join("\n\n");
|
||||
}
|
||||
|
||||
@@ -194,7 +224,9 @@ function resolveDiscordMessageSnapshots(message: Message): DiscordMessageSnapsho
|
||||
rawData?.message_snapshots ??
|
||||
(message as { message_snapshots?: unknown }).message_snapshots ??
|
||||
(message as { messageSnapshots?: unknown }).messageSnapshots;
|
||||
if (!Array.isArray(snapshots)) return [];
|
||||
if (!Array.isArray(snapshots)) {
|
||||
return [];
|
||||
}
|
||||
return snapshots.filter(
|
||||
(entry): entry is DiscordMessageSnapshot => Boolean(entry) && typeof entry === "object",
|
||||
);
|
||||
@@ -211,7 +243,9 @@ function resolveDiscordSnapshotMessageText(snapshot: DiscordSnapshotMessage): st
|
||||
function formatDiscordSnapshotAuthor(
|
||||
author: DiscordSnapshotAuthor | null | undefined,
|
||||
): string | undefined {
|
||||
if (!author) return undefined;
|
||||
if (!author) {
|
||||
return undefined;
|
||||
}
|
||||
const globalName = author.global_name ?? undefined;
|
||||
const username = author.username ?? undefined;
|
||||
const name = author.name ?? undefined;
|
||||
@@ -220,8 +254,12 @@ function formatDiscordSnapshotAuthor(
|
||||
if (username && discriminator && discriminator !== "0") {
|
||||
return `@${username}#${discriminator}`;
|
||||
}
|
||||
if (base) return `@${base}`;
|
||||
if (author.id) return `@${author.id}`;
|
||||
if (base) {
|
||||
return `@${base}`;
|
||||
}
|
||||
if (author.id) {
|
||||
return `@${author.id}`;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ function buildDiscordCommandOptions(params: {
|
||||
}): CommandOptions | undefined {
|
||||
const { command, cfg } = params;
|
||||
const args = command.args;
|
||||
if (!args || args.length === 0) return undefined;
|
||||
if (!args || args.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return args.map((arg) => {
|
||||
const required = arg.required ?? false;
|
||||
if (arg.type === "number") {
|
||||
@@ -121,7 +123,9 @@ function readDiscordCommandArgs(
|
||||
interaction: CommandInteraction,
|
||||
definitions?: CommandArgDefinition[],
|
||||
): CommandArgs | undefined {
|
||||
if (!definitions || definitions.length === 0) return undefined;
|
||||
if (!definitions || definitions.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const values: CommandArgValues = {};
|
||||
for (const definition of definitions) {
|
||||
let value: string | number | boolean | null | undefined;
|
||||
@@ -140,7 +144,9 @@ function readDiscordCommandArgs(
|
||||
}
|
||||
|
||||
function chunkItems<T>(items: T[], size: number): T[][] {
|
||||
if (size <= 0) return [items];
|
||||
if (size <= 0) {
|
||||
return [items];
|
||||
}
|
||||
const rows: T[][] = [];
|
||||
for (let i = 0; i < items.length; i += size) {
|
||||
rows.push(items.slice(i, i + size));
|
||||
@@ -168,16 +174,24 @@ function decodeDiscordCommandArgValue(value: string): string {
|
||||
}
|
||||
|
||||
function isDiscordUnknownInteraction(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") return false;
|
||||
if (!error || typeof error !== "object") {
|
||||
return false;
|
||||
}
|
||||
const err = error as {
|
||||
discordCode?: number;
|
||||
status?: number;
|
||||
message?: string;
|
||||
rawBody?: { code?: number; message?: string };
|
||||
};
|
||||
if (err.discordCode === 10062 || err.rawBody?.code === 10062) return true;
|
||||
if (err.status === 404 && /Unknown interaction/i.test(err.message ?? "")) return true;
|
||||
if (/Unknown interaction/i.test(err.rawBody?.message ?? "")) return true;
|
||||
if (err.discordCode === 10062 || err.rawBody?.code === 10062) {
|
||||
return true;
|
||||
}
|
||||
if (err.status === 404 && /Unknown interaction/i.test(err.message ?? "")) {
|
||||
return true;
|
||||
}
|
||||
if (/Unknown interaction/i.test(err.rawBody?.message ?? "")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -213,14 +227,18 @@ function buildDiscordCommandArgCustomId(params: {
|
||||
function parseDiscordCommandArgData(
|
||||
data: ComponentData,
|
||||
): { command: string; arg: string; value: string; userId: string } | null {
|
||||
if (!data || typeof data !== "object") return null;
|
||||
if (!data || typeof data !== "object") {
|
||||
return null;
|
||||
}
|
||||
const coerce = (value: unknown) =>
|
||||
typeof value === "string" || typeof value === "number" ? String(value) : "";
|
||||
const rawCommand = coerce(data.command);
|
||||
const rawArg = coerce(data.arg);
|
||||
const rawValue = coerce(data.value);
|
||||
const rawUser = coerce(data.user);
|
||||
if (!rawCommand || !rawArg || !rawValue || !rawUser) return null;
|
||||
if (!rawCommand || !rawArg || !rawValue || !rawUser) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
command: decodeDiscordCommandArgValue(rawCommand),
|
||||
arg: decodeDiscordCommandArgValue(rawArg),
|
||||
@@ -273,7 +291,9 @@ async function handleDiscordCommandArgInteraction(
|
||||
components: [],
|
||||
}),
|
||||
);
|
||||
if (!updated) return;
|
||||
if (!updated) {
|
||||
return;
|
||||
}
|
||||
const commandArgs = createCommandArgsWithValue({
|
||||
argName: parsed.arg,
|
||||
value: parsed.value,
|
||||
@@ -502,7 +522,9 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
|
||||
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
||||
const user = interaction.user;
|
||||
if (!user) return;
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
const channel = interaction.channel;
|
||||
const channelType = channel?.type;
|
||||
const isDirectMessage = channelType === ChannelType.DM;
|
||||
@@ -851,25 +873,35 @@ async function deliverDiscordInteractionReply(params: {
|
||||
maxLines: maxLinesPerMessage,
|
||||
chunkMode,
|
||||
});
|
||||
if (!chunks.length && text) chunks.push(text);
|
||||
if (!chunks.length && text) {
|
||||
chunks.push(text);
|
||||
}
|
||||
const caption = chunks[0] ?? "";
|
||||
await sendMessage(caption, media);
|
||||
for (const chunk of chunks.slice(1)) {
|
||||
if (!chunk.trim()) continue;
|
||||
if (!chunk.trim()) {
|
||||
continue;
|
||||
}
|
||||
await interaction.followUp({ content: chunk });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text.trim()) return;
|
||||
if (!text.trim()) {
|
||||
return;
|
||||
}
|
||||
const chunks = chunkDiscordTextWithMode(text, {
|
||||
maxChars: textLimit,
|
||||
maxLines: maxLinesPerMessage,
|
||||
chunkMode,
|
||||
});
|
||||
if (!chunks.length && text) chunks.push(text);
|
||||
if (!chunks.length && text) {
|
||||
chunks.push(text);
|
||||
}
|
||||
for (const chunk of chunks) {
|
||||
if (!chunk.trim()) continue;
|
||||
if (!chunk.trim()) {
|
||||
continue;
|
||||
}
|
||||
await sendMessage(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +52,18 @@ export type MonitorDiscordOpts = {
|
||||
};
|
||||
|
||||
function summarizeAllowList(list?: Array<string | number>) {
|
||||
if (!list || list.length === 0) return "any";
|
||||
if (!list || list.length === 0) {
|
||||
return "any";
|
||||
}
|
||||
const sample = list.slice(0, 4).map((entry) => String(entry));
|
||||
const suffix = list.length > sample.length ? ` (+${list.length - sample.length})` : "";
|
||||
return `${sample.join(", ")}${suffix}`;
|
||||
}
|
||||
|
||||
function summarizeGuilds(entries?: Record<string, unknown>) {
|
||||
if (!entries || Object.keys(entries).length === 0) return "any";
|
||||
if (!entries || Object.keys(entries).length === 0) {
|
||||
return "any";
|
||||
}
|
||||
const keys = Object.keys(entries);
|
||||
const sample = keys.slice(0, 4);
|
||||
const suffix = keys.length > sample.length ? ` (+${keys.length - sample.length})` : "";
|
||||
@@ -71,7 +75,9 @@ async function deployDiscordCommands(params: {
|
||||
runtime: RuntimeEnv;
|
||||
enabled: boolean;
|
||||
}) {
|
||||
if (!params.enabled) return;
|
||||
if (!params.enabled) {
|
||||
return;
|
||||
}
|
||||
const runWithRetry = createDiscordRetryRunner({ verbose: shouldLogVerbose() });
|
||||
try {
|
||||
await runWithRetry(() => params.client.handleDeployRequest(), "command deploy");
|
||||
@@ -84,12 +90,16 @@ async function deployDiscordCommands(params: {
|
||||
}
|
||||
|
||||
function formatDiscordDeployErrorDetails(err: unknown): string {
|
||||
if (!err || typeof err !== "object") return "";
|
||||
if (!err || typeof err !== "object") {
|
||||
return "";
|
||||
}
|
||||
const status = (err as { status?: unknown }).status;
|
||||
const discordCode = (err as { discordCode?: unknown }).discordCode;
|
||||
const rawBody = (err as { rawBody?: unknown }).rawBody;
|
||||
const details: string[] = [];
|
||||
if (typeof status === "number") details.push(`status=${status}`);
|
||||
if (typeof status === "number") {
|
||||
details.push(`status=${status}`);
|
||||
}
|
||||
if (typeof discordCode === "number" || typeof discordCode === "string") {
|
||||
details.push(`code=${discordCode}`);
|
||||
}
|
||||
@@ -204,7 +214,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
try {
|
||||
const entries: Array<{ input: string; guildKey: string; channelKey?: string }> = [];
|
||||
for (const [guildKey, guildCfg] of Object.entries(guildEntries)) {
|
||||
if (guildKey === "*") continue;
|
||||
if (guildKey === "*") {
|
||||
continue;
|
||||
}
|
||||
const channels = guildCfg?.channels ?? {};
|
||||
const channelKeys = Object.keys(channels).filter((key) => key !== "*");
|
||||
if (channelKeys.length === 0) {
|
||||
@@ -229,7 +241,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
const unresolved: string[] = [];
|
||||
for (const entry of resolved) {
|
||||
const source = entries.find((item) => item.input === entry.input);
|
||||
if (!source) continue;
|
||||
if (!source) {
|
||||
continue;
|
||||
}
|
||||
const sourceGuild = guildEntries?.[source.guildKey] ?? {};
|
||||
if (!entry.resolved || !entry.guildId) {
|
||||
unresolved.push(entry.input);
|
||||
@@ -301,22 +315,32 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
if (guildEntries && Object.keys(guildEntries).length > 0) {
|
||||
const userEntries = new Set<string>();
|
||||
for (const guild of Object.values(guildEntries)) {
|
||||
if (!guild || typeof guild !== "object") continue;
|
||||
if (!guild || typeof guild !== "object") {
|
||||
continue;
|
||||
}
|
||||
const users = (guild as { users?: Array<string | number> }).users;
|
||||
if (Array.isArray(users)) {
|
||||
for (const entry of users) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed && trimmed !== "*") userEntries.add(trimmed);
|
||||
if (trimmed && trimmed !== "*") {
|
||||
userEntries.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
const channels = (guild as { channels?: Record<string, unknown> }).channels ?? {};
|
||||
for (const channel of Object.values(channels)) {
|
||||
if (!channel || typeof channel !== "object") continue;
|
||||
if (!channel || typeof channel !== "object") {
|
||||
continue;
|
||||
}
|
||||
const channelUsers = (channel as { users?: Array<string | number> }).users;
|
||||
if (!Array.isArray(channelUsers)) continue;
|
||||
if (!Array.isArray(channelUsers)) {
|
||||
continue;
|
||||
}
|
||||
for (const entry of channelUsers) {
|
||||
const trimmed = String(entry).trim();
|
||||
if (trimmed && trimmed !== "*") userEntries.add(trimmed);
|
||||
if (trimmed && trimmed !== "*") {
|
||||
userEntries.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,7 +361,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
|
||||
const nextGuilds = { ...guildEntries };
|
||||
for (const [guildKey, guildConfig] of Object.entries(guildEntries ?? {})) {
|
||||
if (!guildConfig || typeof guildConfig !== "object") continue;
|
||||
if (!guildConfig || typeof guildConfig !== "object") {
|
||||
continue;
|
||||
}
|
||||
const nextGuild = { ...guildConfig } as Record<string, unknown>;
|
||||
const users = (guildConfig as { users?: Array<string | number> }).users;
|
||||
if (Array.isArray(users) && users.length > 0) {
|
||||
@@ -345,7 +371,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
for (const entry of users) {
|
||||
const trimmed = String(entry).trim();
|
||||
const resolved = resolvedMap.get(trimmed);
|
||||
if (resolved?.resolved && resolved.id) additions.push(resolved.id);
|
||||
if (resolved?.resolved && resolved.id) {
|
||||
additions.push(resolved.id);
|
||||
}
|
||||
}
|
||||
nextGuild.users = mergeAllowlist({ existing: users, additions });
|
||||
}
|
||||
@@ -353,14 +381,20 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
if (channels && typeof channels === "object") {
|
||||
const nextChannels: Record<string, unknown> = { ...channels };
|
||||
for (const [channelKey, channelConfig] of Object.entries(channels)) {
|
||||
if (!channelConfig || typeof channelConfig !== "object") continue;
|
||||
if (!channelConfig || typeof channelConfig !== "object") {
|
||||
continue;
|
||||
}
|
||||
const channelUsers = (channelConfig as { users?: Array<string | number> }).users;
|
||||
if (!Array.isArray(channelUsers) || channelUsers.length === 0) continue;
|
||||
if (!Array.isArray(channelUsers) || channelUsers.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const additions: string[] = [];
|
||||
for (const entry of channelUsers) {
|
||||
const trimmed = String(entry).trim();
|
||||
const resolved = resolvedMap.get(trimmed);
|
||||
if (resolved?.resolved && resolved.id) additions.push(resolved.id);
|
||||
if (resolved?.resolved && resolved.id) {
|
||||
additions.push(resolved.id);
|
||||
}
|
||||
}
|
||||
nextChannels[channelKey] = {
|
||||
...channelConfig,
|
||||
@@ -564,7 +598,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
});
|
||||
const abortSignal = opts.abortSignal;
|
||||
const onAbort = () => {
|
||||
if (!gateway) return;
|
||||
if (!gateway) {
|
||||
return;
|
||||
}
|
||||
// Carbon emits an error when maxAttempts is 0; keep a one-shot listener to avoid
|
||||
// an unhandled error after we tear down listeners during abort.
|
||||
gatewayEmitter?.once("error", () => {});
|
||||
@@ -581,8 +617,12 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
let helloTimeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
const onGatewayDebug = (msg: unknown) => {
|
||||
const message = String(msg);
|
||||
if (!message.includes("WebSocket connection opened")) return;
|
||||
if (helloTimeoutId) clearTimeout(helloTimeoutId);
|
||||
if (!message.includes("WebSocket connection opened")) {
|
||||
return;
|
||||
}
|
||||
if (helloTimeoutId) {
|
||||
clearTimeout(helloTimeoutId);
|
||||
}
|
||||
helloTimeoutId = setTimeout(() => {
|
||||
if (!gateway?.isConnected) {
|
||||
runtime.log?.(
|
||||
@@ -618,7 +658,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
|
||||
});
|
||||
} finally {
|
||||
stopGatewayLogging();
|
||||
if (helloTimeoutId) clearTimeout(helloTimeoutId);
|
||||
if (helloTimeoutId) {
|
||||
clearTimeout(helloTimeoutId);
|
||||
}
|
||||
gatewayEmitter?.removeListener("debug", onGatewayDebug);
|
||||
abortSignal?.removeEventListener("abort", onAbort);
|
||||
if (execApprovalsHandler) {
|
||||
|
||||
@@ -9,11 +9,15 @@ export function resolveReplyContext(
|
||||
options?: { envelope?: EnvelopeFormatOptions },
|
||||
): string | null {
|
||||
const referenced = message.referencedMessage;
|
||||
if (!referenced?.author) return null;
|
||||
if (!referenced?.author) {
|
||||
return null;
|
||||
}
|
||||
const referencedText = resolveDiscordMessageText(referenced, {
|
||||
includeForwarded: true,
|
||||
});
|
||||
if (!referencedText) return null;
|
||||
if (!referencedText) {
|
||||
return null;
|
||||
}
|
||||
const fromLabel = referenced.author ? buildDirectLabel(referenced.author) : "Unknown";
|
||||
const body = `${referencedText}\n[discord message id: ${referenced.id} channel: ${referenced.channelId} from: ${formatDiscordUserTag(referenced.author)} user id:${referenced.author?.id ?? "unknown"}]`;
|
||||
return formatAgentEnvelope({
|
||||
|
||||
@@ -27,7 +27,9 @@ export async function deliverDiscordReply(params: {
|
||||
const rawText = payload.text ?? "";
|
||||
const tableMode = params.tableMode ?? "code";
|
||||
const text = convertMarkdownTables(rawText, tableMode);
|
||||
if (!text && mediaList.length === 0) continue;
|
||||
if (!text && mediaList.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const replyTo = params.replyToId?.trim() || undefined;
|
||||
|
||||
if (mediaList.length === 0) {
|
||||
@@ -38,10 +40,14 @@ export async function deliverDiscordReply(params: {
|
||||
maxLines: params.maxLinesPerMessage,
|
||||
chunkMode: mode,
|
||||
});
|
||||
if (!chunks.length && text) chunks.push(text);
|
||||
if (!chunks.length && text) {
|
||||
chunks.push(text);
|
||||
}
|
||||
for (const chunk of chunks) {
|
||||
const trimmed = chunk.trim();
|
||||
if (!trimmed) continue;
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
await sendMessageDiscord(params.target, trimmed, {
|
||||
token: params.token,
|
||||
rest: params.rest,
|
||||
@@ -54,7 +60,9 @@ export async function deliverDiscordReply(params: {
|
||||
}
|
||||
|
||||
const firstMedia = mediaList[0];
|
||||
if (!firstMedia) continue;
|
||||
if (!firstMedia) {
|
||||
continue;
|
||||
}
|
||||
await sendMessageDiscord(params.target, text, {
|
||||
token: params.token,
|
||||
rest: params.rest,
|
||||
|
||||
@@ -48,7 +48,9 @@ export function resolveDiscordThreadChannel(params: {
|
||||
message: DiscordMessageEvent["message"];
|
||||
channelInfo: import("./message-utils.js").DiscordChannelInfo | null;
|
||||
}): DiscordThreadChannel | null {
|
||||
if (!params.isGuildMessage) return null;
|
||||
if (!params.isGuildMessage) {
|
||||
return null;
|
||||
}
|
||||
const { message, channelInfo } = params;
|
||||
const channel = "channel" in message ? (message as { channel?: unknown }).channel : undefined;
|
||||
const isThreadChannel =
|
||||
@@ -57,8 +59,12 @@ export function resolveDiscordThreadChannel(params: {
|
||||
"isThread" in channel &&
|
||||
typeof (channel as { isThread?: unknown }).isThread === "function" &&
|
||||
(channel as { isThread: () => boolean }).isThread();
|
||||
if (isThreadChannel) return channel as unknown as DiscordThreadChannel;
|
||||
if (!isDiscordThreadType(channelInfo?.type)) return null;
|
||||
if (isThreadChannel) {
|
||||
return channel as unknown as DiscordThreadChannel;
|
||||
}
|
||||
if (!isDiscordThreadType(channelInfo?.type)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: message.channelId,
|
||||
name: channelInfo?.name ?? undefined,
|
||||
@@ -76,7 +82,9 @@ export async function resolveDiscordThreadParentInfo(params: {
|
||||
const { threadChannel, channelInfo, client } = params;
|
||||
const parentId =
|
||||
threadChannel.parentId ?? threadChannel.parent?.id ?? channelInfo?.parentId ?? undefined;
|
||||
if (!parentId) return {};
|
||||
if (!parentId) {
|
||||
return {};
|
||||
}
|
||||
let parentName = threadChannel.parent?.name;
|
||||
const parentInfo = await resolveDiscordChannelInfo(client, parentId);
|
||||
parentName = parentName ?? parentInfo?.name;
|
||||
@@ -93,13 +101,17 @@ export async function resolveDiscordThreadStarter(params: {
|
||||
}): Promise<DiscordThreadStarter | null> {
|
||||
const cacheKey = params.channel.id;
|
||||
const cached = DISCORD_THREAD_STARTER_CACHE.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
try {
|
||||
const parentType = params.parentType;
|
||||
const isForumParent =
|
||||
parentType === ChannelType.GuildForum || parentType === ChannelType.GuildMedia;
|
||||
const messageChannelId = isForumParent ? params.channel.id : params.parentId;
|
||||
if (!messageChannelId) return null;
|
||||
if (!messageChannelId) {
|
||||
return null;
|
||||
}
|
||||
const starter = (await params.client.rest.get(
|
||||
Routes.channelMessage(messageChannelId, params.channel.id),
|
||||
)) as {
|
||||
@@ -113,9 +125,13 @@ export async function resolveDiscordThreadStarter(params: {
|
||||
};
|
||||
timestamp?: string | null;
|
||||
};
|
||||
if (!starter) return null;
|
||||
if (!starter) {
|
||||
return null;
|
||||
}
|
||||
const text = starter.content?.trim() ?? starter.embeds?.[0]?.description?.trim() ?? "";
|
||||
if (!text) return null;
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
const author =
|
||||
starter.member?.nick ??
|
||||
starter.member?.displayName ??
|
||||
@@ -142,10 +158,16 @@ export function resolveDiscordReplyTarget(opts: {
|
||||
replyToId?: string;
|
||||
hasReplied: boolean;
|
||||
}): string | undefined {
|
||||
if (opts.replyToMode === "off") return undefined;
|
||||
if (opts.replyToMode === "off") {
|
||||
return undefined;
|
||||
}
|
||||
const replyToId = opts.replyToId?.trim();
|
||||
if (!replyToId) return undefined;
|
||||
if (opts.replyToMode === "all") return replyToId;
|
||||
if (!replyToId) {
|
||||
return undefined;
|
||||
}
|
||||
if (opts.replyToMode === "all") {
|
||||
return replyToId;
|
||||
}
|
||||
return opts.hasReplied ? undefined : replyToId;
|
||||
}
|
||||
|
||||
@@ -183,9 +205,13 @@ export function resolveDiscordAutoThreadContext(params: {
|
||||
createdThreadId?: string | null;
|
||||
}): DiscordAutoThreadContext | null {
|
||||
const createdThreadId = String(params.createdThreadId ?? "").trim();
|
||||
if (!createdThreadId) return null;
|
||||
if (!createdThreadId) {
|
||||
return null;
|
||||
}
|
||||
const messageChannelId = params.messageChannelId.trim();
|
||||
if (!messageChannelId) return null;
|
||||
if (!messageChannelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const threadSessionKey = buildAgentSessionKey({
|
||||
agentId: params.agentId,
|
||||
@@ -262,9 +288,15 @@ export async function maybeCreateDiscordAutoThread(params: {
|
||||
baseText: string;
|
||||
combinedBody: string;
|
||||
}): Promise<string | undefined> {
|
||||
if (!params.isGuildMessage) return undefined;
|
||||
if (!params.channelConfig?.autoThread) return undefined;
|
||||
if (params.threadChannel) return undefined;
|
||||
if (!params.isGuildMessage) {
|
||||
return undefined;
|
||||
}
|
||||
if (!params.channelConfig?.autoThread) {
|
||||
return undefined;
|
||||
}
|
||||
if (params.threadChannel) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const threadName = sanitizeDiscordThreadName(
|
||||
params.baseText || params.combinedBody || "Thread",
|
||||
|
||||
@@ -2,7 +2,9 @@ import type { Client } from "@buape/carbon";
|
||||
|
||||
export async function sendTyping(params: { client: Client; channelId: string }) {
|
||||
const channel = await params.client.fetchChannel(params.channelId);
|
||||
if (!channel) return;
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
if ("triggerTyping" in channel && typeof channel.triggerTyping === "function") {
|
||||
await channel.triggerTyping();
|
||||
}
|
||||
|
||||
@@ -37,8 +37,12 @@ export function resolveDiscordPrivilegedIntentsFromFlags(
|
||||
flags: number,
|
||||
): DiscordPrivilegedIntentsSummary {
|
||||
const resolve = (enabledBit: number, limitedBit: number) => {
|
||||
if ((flags & enabledBit) !== 0) return "enabled";
|
||||
if ((flags & limitedBit) !== 0) return "limited";
|
||||
if ((flags & enabledBit) !== 0) {
|
||||
return "enabled";
|
||||
}
|
||||
if ((flags & limitedBit) !== 0) {
|
||||
return "limited";
|
||||
}
|
||||
return "disabled";
|
||||
};
|
||||
return {
|
||||
@@ -60,7 +64,9 @@ export async function fetchDiscordApplicationSummary(
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<DiscordApplicationSummary | undefined> {
|
||||
const normalized = normalizeDiscordToken(token);
|
||||
if (!normalized) return undefined;
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const res = await fetchWithTimeout(
|
||||
`${DISCORD_API_BASE}/oauth2/applications/@me`,
|
||||
@@ -70,7 +76,9 @@ export async function fetchDiscordApplicationSummary(
|
||||
Authorization: `Bot ${normalized}`,
|
||||
},
|
||||
);
|
||||
if (!res.ok) return undefined;
|
||||
if (!res.ok) {
|
||||
return undefined;
|
||||
}
|
||||
const json = (await res.json()) as { id?: string; flags?: number };
|
||||
const flags =
|
||||
typeof json.flags === "number" && Number.isFinite(json.flags) ? json.flags : undefined;
|
||||
@@ -162,7 +170,9 @@ export async function fetchDiscordApplicationId(
|
||||
fetcher: typeof fetch = fetch,
|
||||
): Promise<string | undefined> {
|
||||
const normalized = normalizeDiscordToken(token);
|
||||
if (!normalized) return undefined;
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const res = await fetchWithTimeout(
|
||||
`${DISCORD_API_BASE}/oauth2/applications/@me`,
|
||||
@@ -172,7 +182,9 @@ export async function fetchDiscordApplicationId(
|
||||
Authorization: `Bot ${normalized}`,
|
||||
},
|
||||
);
|
||||
if (!res.ok) return undefined;
|
||||
if (!res.ok) {
|
||||
return undefined;
|
||||
}
|
||||
const json = (await res.json()) as { id?: string };
|
||||
return json.id ?? undefined;
|
||||
} catch {
|
||||
|
||||
@@ -35,11 +35,17 @@ function parseDiscordChannelInput(raw: string): {
|
||||
guildOnly?: boolean;
|
||||
} {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return {};
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
const mention = trimmed.match(/^<#(\d+)>$/);
|
||||
if (mention) return { channelId: mention[1] };
|
||||
if (mention) {
|
||||
return { channelId: mention[1] };
|
||||
}
|
||||
const channelPrefix = trimmed.match(/^(?:channel:|discord:)?(\d+)$/i);
|
||||
if (channelPrefix) return { channelId: channelPrefix[1] };
|
||||
if (channelPrefix) {
|
||||
return { channelId: channelPrefix[1] };
|
||||
}
|
||||
const guildPrefix = trimmed.match(/^(?:guild:|server:)?(\d+)$/i);
|
||||
if (guildPrefix && !trimmed.includes("/") && !trimmed.includes("#")) {
|
||||
return { guildId: guildPrefix[1], guildOnly: true };
|
||||
@@ -51,7 +57,9 @@ function parseDiscordChannelInput(raw: string): {
|
||||
if (!channel) {
|
||||
return guild ? { guild: guild.trim(), guildOnly: true } : {};
|
||||
}
|
||||
if (guild && /^\d+$/.test(guild)) return { guildId: guild, channel };
|
||||
if (guild && /^\d+$/.test(guild)) {
|
||||
return { guildId: guild, channel };
|
||||
}
|
||||
return { guild, channel };
|
||||
}
|
||||
return { guild: trimmed, guildOnly: true };
|
||||
@@ -100,7 +108,9 @@ async function fetchChannel(
|
||||
channelId: string,
|
||||
): Promise<DiscordChannelSummary | null> {
|
||||
const raw = await fetchDiscord(`/channels/${channelId}`, token, fetcher);
|
||||
if (!raw || !("guild_id" in raw)) return null;
|
||||
if (!raw || !("guild_id" in raw)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: raw.id,
|
||||
name: "name" in raw ? (raw.name ?? "") : "",
|
||||
@@ -110,7 +120,9 @@ async function fetchChannel(
|
||||
}
|
||||
|
||||
function preferActiveMatch(candidates: DiscordChannelSummary[]): DiscordChannelSummary | undefined {
|
||||
if (candidates.length === 0) return undefined;
|
||||
if (candidates.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const scored = candidates.map((channel) => {
|
||||
const isThread = channel.type === 11 || channel.type === 12;
|
||||
const archived = Boolean(channel.archived);
|
||||
@@ -126,7 +138,9 @@ function resolveGuildByName(
|
||||
input: string,
|
||||
): DiscordGuildSummary | undefined {
|
||||
const slug = normalizeDiscordSlug(input);
|
||||
if (!slug) return undefined;
|
||||
if (!slug) {
|
||||
return undefined;
|
||||
}
|
||||
return guilds.find((guild) => guild.slug === slug);
|
||||
}
|
||||
|
||||
@@ -136,17 +150,20 @@ export async function resolveDiscordChannelAllowlist(params: {
|
||||
fetcher?: typeof fetch;
|
||||
}): Promise<DiscordChannelResolution[]> {
|
||||
const token = normalizeDiscordToken(params.token);
|
||||
if (!token)
|
||||
if (!token) {
|
||||
return params.entries.map((input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
}));
|
||||
}
|
||||
const fetcher = params.fetcher ?? fetch;
|
||||
const guilds = await listGuilds(token, fetcher);
|
||||
const channelsByGuild = new Map<string, Promise<DiscordChannelSummary[]>>();
|
||||
const getChannels = (guildId: string) => {
|
||||
const existing = channelsByGuild.get(guildId);
|
||||
if (existing) return existing;
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const promise = listGuildChannels(token, fetcher, guildId);
|
||||
channelsByGuild.set(guildId, promise);
|
||||
return promise;
|
||||
|
||||
@@ -38,16 +38,24 @@ function parseDiscordUserInput(raw: string): {
|
||||
userName?: string;
|
||||
} {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return {};
|
||||
if (!trimmed) {
|
||||
return {};
|
||||
}
|
||||
const mention = trimmed.match(/^<@!?(\d+)>$/);
|
||||
if (mention) return { userId: mention[1] };
|
||||
if (mention) {
|
||||
return { userId: mention[1] };
|
||||
}
|
||||
const prefixed = trimmed.match(/^(?:user:|discord:)?(\d+)$/i);
|
||||
if (prefixed) return { userId: prefixed[1] };
|
||||
if (prefixed) {
|
||||
return { userId: prefixed[1] };
|
||||
}
|
||||
const split = trimmed.includes("/") ? trimmed.split("/") : trimmed.split("#");
|
||||
if (split.length >= 2) {
|
||||
const guild = split[0]?.trim();
|
||||
const user = split.slice(1).join("#").trim();
|
||||
if (guild && /^\d+$/.test(guild)) return { guildId: guild, userName: user };
|
||||
if (guild && /^\d+$/.test(guild)) {
|
||||
return { guildId: guild, userName: user };
|
||||
}
|
||||
return { guildName: guild, userName: user };
|
||||
}
|
||||
return { userName: trimmed.replace(/^@/, "") };
|
||||
@@ -73,9 +81,15 @@ function scoreDiscordMember(member: DiscordMember, query: string): number {
|
||||
.map((value) => value?.toLowerCase())
|
||||
.filter(Boolean) as string[];
|
||||
let score = 0;
|
||||
if (candidates.some((value) => value === q)) score += 3;
|
||||
if (candidates.some((value) => value?.includes(q))) score += 1;
|
||||
if (!user.bot) score += 1;
|
||||
if (candidates.some((value) => value === q)) {
|
||||
score += 3;
|
||||
}
|
||||
if (candidates.some((value) => value?.includes(q))) {
|
||||
score += 1;
|
||||
}
|
||||
if (!user.bot) {
|
||||
score += 1;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
@@ -85,11 +99,12 @@ export async function resolveDiscordUserAllowlist(params: {
|
||||
fetcher?: typeof fetch;
|
||||
}): Promise<DiscordUserResolution[]> {
|
||||
const token = normalizeDiscordToken(params.token);
|
||||
if (!token)
|
||||
if (!token) {
|
||||
return params.entries.map((input) => ({
|
||||
input,
|
||||
resolved: false,
|
||||
}));
|
||||
}
|
||||
const fetcher = params.fetcher ?? fetch;
|
||||
const guilds = await listGuilds(token, fetcher);
|
||||
const results: DiscordUserResolution[] = [];
|
||||
@@ -133,7 +148,9 @@ export async function resolveDiscordUserAllowlist(params: {
|
||||
);
|
||||
for (const member of members) {
|
||||
const score = scoreDiscordMember(member, query);
|
||||
if (score === 0) continue;
|
||||
if (score === 0) {
|
||||
continue;
|
||||
}
|
||||
matches += 1;
|
||||
if (!best || score > best.score) {
|
||||
best = { member, guild, score };
|
||||
|
||||
@@ -17,11 +17,21 @@ export async function createChannelDiscord(
|
||||
const body: Record<string, unknown> = {
|
||||
name: payload.name,
|
||||
};
|
||||
if (payload.type !== undefined) body.type = payload.type;
|
||||
if (payload.parentId) body.parent_id = payload.parentId;
|
||||
if (payload.topic) body.topic = payload.topic;
|
||||
if (payload.position !== undefined) body.position = payload.position;
|
||||
if (payload.nsfw !== undefined) body.nsfw = payload.nsfw;
|
||||
if (payload.type !== undefined) {
|
||||
body.type = payload.type;
|
||||
}
|
||||
if (payload.parentId) {
|
||||
body.parent_id = payload.parentId;
|
||||
}
|
||||
if (payload.topic) {
|
||||
body.topic = payload.topic;
|
||||
}
|
||||
if (payload.position !== undefined) {
|
||||
body.position = payload.position;
|
||||
}
|
||||
if (payload.nsfw !== undefined) {
|
||||
body.nsfw = payload.nsfw;
|
||||
}
|
||||
return (await rest.post(Routes.guildChannels(payload.guildId), {
|
||||
body,
|
||||
})) as APIChannel;
|
||||
@@ -33,12 +43,24 @@ export async function editChannelDiscord(
|
||||
): Promise<APIChannel> {
|
||||
const rest = resolveDiscordRest(opts);
|
||||
const body: Record<string, unknown> = {};
|
||||
if (payload.name !== undefined) body.name = payload.name;
|
||||
if (payload.topic !== undefined) body.topic = payload.topic;
|
||||
if (payload.position !== undefined) body.position = payload.position;
|
||||
if (payload.parentId !== undefined) body.parent_id = payload.parentId;
|
||||
if (payload.nsfw !== undefined) body.nsfw = payload.nsfw;
|
||||
if (payload.rateLimitPerUser !== undefined) body.rate_limit_per_user = payload.rateLimitPerUser;
|
||||
if (payload.name !== undefined) {
|
||||
body.name = payload.name;
|
||||
}
|
||||
if (payload.topic !== undefined) {
|
||||
body.topic = payload.topic;
|
||||
}
|
||||
if (payload.position !== undefined) {
|
||||
body.position = payload.position;
|
||||
}
|
||||
if (payload.parentId !== undefined) {
|
||||
body.parent_id = payload.parentId;
|
||||
}
|
||||
if (payload.nsfw !== undefined) {
|
||||
body.nsfw = payload.nsfw;
|
||||
}
|
||||
if (payload.rateLimitPerUser !== undefined) {
|
||||
body.rate_limit_per_user = payload.rateLimitPerUser;
|
||||
}
|
||||
return (await rest.patch(Routes.channel(payload.channelId), {
|
||||
body,
|
||||
})) as APIChannel;
|
||||
@@ -71,8 +93,12 @@ export async function setChannelPermissionDiscord(
|
||||
const body: Record<string, unknown> = {
|
||||
type: payload.targetType,
|
||||
};
|
||||
if (payload.allow !== undefined) body.allow = payload.allow;
|
||||
if (payload.deny !== undefined) body.deny = payload.deny;
|
||||
if (payload.allow !== undefined) {
|
||||
body.allow = payload.allow;
|
||||
}
|
||||
if (payload.deny !== undefined) {
|
||||
body.deny = payload.deny;
|
||||
}
|
||||
await rest.put(`/channels/${payload.channelId}/permissions/${payload.targetId}`, { body });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -21,10 +21,18 @@ export async function readMessagesDiscord(
|
||||
? Math.min(Math.max(Math.floor(query.limit), 1), 100)
|
||||
: undefined;
|
||||
const params: Record<string, string | number> = {};
|
||||
if (limit) params.limit = limit;
|
||||
if (query.before) params.before = query.before;
|
||||
if (query.after) params.after = query.after;
|
||||
if (query.around) params.around = query.around;
|
||||
if (limit) {
|
||||
params.limit = limit;
|
||||
}
|
||||
if (query.before) {
|
||||
params.before = query.before;
|
||||
}
|
||||
if (query.after) {
|
||||
params.after = query.after;
|
||||
}
|
||||
if (query.around) {
|
||||
params.around = query.around;
|
||||
}
|
||||
return (await rest.get(Routes.channelMessages(channelId), params)) as APIMessage[];
|
||||
}
|
||||
|
||||
@@ -108,8 +116,12 @@ export async function listThreadsDiscord(payload: DiscordThreadList, opts: Disco
|
||||
throw new Error("channelId required to list archived threads");
|
||||
}
|
||||
const params: Record<string, string | number> = {};
|
||||
if (payload.before) params.before = payload.before;
|
||||
if (payload.limit) params.limit = payload.limit;
|
||||
if (payload.before) {
|
||||
params.before = payload.before;
|
||||
}
|
||||
if (payload.limit) {
|
||||
params.limit = payload.limit;
|
||||
}
|
||||
return await rest.get(Routes.channelThreads(payload.channelId, "public"), params);
|
||||
}
|
||||
return await rest.get(Routes.guildActiveThreads(payload.guildId));
|
||||
|
||||
@@ -22,7 +22,9 @@ type DiscordClientOpts = {
|
||||
|
||||
function resolveToken(params: { explicit?: string; accountId: string; fallbackToken?: string }) {
|
||||
const explicit = normalizeDiscordToken(params.explicit);
|
||||
if (explicit) return explicit;
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
const fallback = normalizeDiscordToken(params.fallbackToken);
|
||||
if (!fallback) {
|
||||
throw new Error(
|
||||
@@ -48,12 +50,16 @@ function resolveDiscordRest(opts: DiscordClientOpts) {
|
||||
}
|
||||
|
||||
function addPermissionBits(base: bigint, add?: string) {
|
||||
if (!add) return base;
|
||||
if (!add) {
|
||||
return base;
|
||||
}
|
||||
return base | BigInt(add);
|
||||
}
|
||||
|
||||
function removePermissionBits(base: bigint, deny?: string) {
|
||||
if (!deny) return base;
|
||||
if (!deny) {
|
||||
return base;
|
||||
}
|
||||
return base & ~BigInt(deny);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,13 @@ export async function removeOwnReactionsDiscord(
|
||||
const identifiers = new Set<string>();
|
||||
for (const reaction of message.reactions ?? []) {
|
||||
const identifier = buildReactionIdentifier(reaction.emoji);
|
||||
if (identifier) identifiers.add(identifier);
|
||||
if (identifier) {
|
||||
identifiers.add(identifier);
|
||||
}
|
||||
}
|
||||
if (identifiers.size === 0) {
|
||||
return { ok: true, removed: [] };
|
||||
}
|
||||
if (identifiers.size === 0) return { ok: true, removed: [] };
|
||||
const removed: string[] = [];
|
||||
await Promise.allSettled(
|
||||
Array.from(identifiers, (identifier) => {
|
||||
@@ -78,7 +82,9 @@ export async function fetchReactionsDiscord(
|
||||
}>;
|
||||
};
|
||||
const reactions = message.reactions ?? [];
|
||||
if (reactions.length === 0) return [];
|
||||
if (reactions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const limit =
|
||||
typeof opts.limit === "number" && Number.isFinite(opts.limit)
|
||||
? Math.min(Math.max(Math.floor(opts.limit), 1), 100)
|
||||
@@ -87,7 +93,9 @@ export async function fetchReactionsDiscord(
|
||||
const summaries: DiscordReactionSummary[] = [];
|
||||
for (const reaction of reactions) {
|
||||
const identifier = buildReactionIdentifier(reaction.emoji);
|
||||
if (!identifier) continue;
|
||||
if (!identifier) {
|
||||
continue;
|
||||
}
|
||||
const encoded = encodeURIComponent(identifier);
|
||||
const users = (await rest.get(Routes.channelMessageReaction(channelId, messageId, encoded), {
|
||||
limit,
|
||||
|
||||
@@ -45,7 +45,9 @@ type DiscordClientOpts = {
|
||||
|
||||
function resolveToken(params: { explicit?: string; accountId: string; fallbackToken?: string }) {
|
||||
const explicit = normalizeDiscordToken(params.explicit);
|
||||
if (explicit) return explicit;
|
||||
if (explicit) {
|
||||
return explicit;
|
||||
}
|
||||
const fallback = normalizeDiscordToken(params.fallbackToken);
|
||||
if (!fallback) {
|
||||
throw new Error(
|
||||
@@ -183,14 +185,18 @@ function normalizeDiscordPollInput(input: PollInput): RESTAPIPoll {
|
||||
}
|
||||
|
||||
function getDiscordErrorCode(err: unknown) {
|
||||
if (!err || typeof err !== "object") return undefined;
|
||||
if (!err || typeof err !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
const candidate =
|
||||
"code" in err && err.code !== undefined
|
||||
? err.code
|
||||
: "rawError" in err && err.rawError && typeof err.rawError === "object"
|
||||
? (err.rawError as { code?: unknown }).code
|
||||
: undefined;
|
||||
if (typeof candidate === "number") return candidate;
|
||||
if (typeof candidate === "number") {
|
||||
return candidate;
|
||||
}
|
||||
if (typeof candidate === "string" && /^\d+$/.test(candidate)) {
|
||||
return Number(candidate);
|
||||
}
|
||||
@@ -206,7 +212,9 @@ async function buildDiscordSendError(
|
||||
hasMedia: boolean;
|
||||
},
|
||||
) {
|
||||
if (err instanceof DiscordSendError) return err;
|
||||
if (err instanceof DiscordSendError) {
|
||||
return err;
|
||||
}
|
||||
const code = getDiscordErrorCode(err);
|
||||
if (code === DISCORD_CANNOT_DM) {
|
||||
return new DiscordSendError(
|
||||
@@ -214,7 +222,9 @@ async function buildDiscordSendError(
|
||||
{ kind: "dm-blocked" },
|
||||
);
|
||||
}
|
||||
if (code !== DISCORD_MISSING_PERMISSIONS) return err;
|
||||
if (code !== DISCORD_MISSING_PERMISSIONS) {
|
||||
return err;
|
||||
}
|
||||
|
||||
let missing: string[] = [];
|
||||
try {
|
||||
@@ -288,7 +298,9 @@ async function sendDiscordText(
|
||||
maxLines: maxLinesPerMessage,
|
||||
chunkMode,
|
||||
});
|
||||
if (!chunks.length && text) chunks.push(text);
|
||||
if (!chunks.length && text) {
|
||||
chunks.push(text);
|
||||
}
|
||||
if (chunks.length === 1) {
|
||||
const res = (await request(
|
||||
() =>
|
||||
@@ -344,7 +356,9 @@ async function sendDiscordMedia(
|
||||
chunkMode,
|
||||
})
|
||||
: [];
|
||||
if (!chunks.length && text) chunks.push(text);
|
||||
if (!chunks.length && text) {
|
||||
chunks.push(text);
|
||||
}
|
||||
const caption = chunks[0] ?? "";
|
||||
const messageReference = replyTo ? { message_id: replyTo, fail_if_not_exists: false } : undefined;
|
||||
const res = (await request(
|
||||
@@ -365,7 +379,9 @@ async function sendDiscordMedia(
|
||||
"media",
|
||||
)) as { id: string; channel_id: string };
|
||||
for (const chunk of chunks.slice(1)) {
|
||||
if (!chunk.trim()) continue;
|
||||
if (!chunk.trim()) {
|
||||
continue;
|
||||
}
|
||||
await sendDiscordText(
|
||||
rest,
|
||||
channelId,
|
||||
|
||||
@@ -10,7 +10,9 @@ export class DiscordSendError extends Error {
|
||||
constructor(message: string, opts?: Partial<DiscordSendError>) {
|
||||
super(message);
|
||||
this.name = "DiscordSendError";
|
||||
if (opts) Object.assign(this, opts);
|
||||
if (opts) {
|
||||
Object.assign(this, opts);
|
||||
}
|
||||
}
|
||||
|
||||
override toString() {
|
||||
|
||||
@@ -22,7 +22,9 @@ export function parseDiscordTarget(
|
||||
options: DiscordTargetParseOptions = {},
|
||||
): DiscordTarget | undefined {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
|
||||
if (mentionMatch) {
|
||||
return buildMessagingTarget("user", mentionMatch[1], trimmed);
|
||||
@@ -80,7 +82,9 @@ export async function resolveDiscordTarget(
|
||||
parseOptions: DiscordTargetParseOptions = {},
|
||||
): Promise<MessagingTarget | undefined> {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const likelyUsername = isLikelyUsername(trimmed);
|
||||
const shouldLookup = isExplicitUserLookup(trimmed, parseOptions) || likelyUsername;
|
||||
|
||||
@@ -9,9 +9,13 @@ export type DiscordTokenResolution = {
|
||||
};
|
||||
|
||||
export function normalizeDiscordToken(raw?: string | null): string | undefined {
|
||||
if (!raw) return undefined;
|
||||
if (!raw) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return undefined;
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
return trimmed.replace(/^Bot\s+/i, "");
|
||||
}
|
||||
|
||||
@@ -26,16 +30,22 @@ export function resolveDiscordToken(
|
||||
? discordCfg?.accounts?.[accountId]
|
||||
: discordCfg?.accounts?.[DEFAULT_ACCOUNT_ID];
|
||||
const accountToken = normalizeDiscordToken(accountCfg?.token ?? undefined);
|
||||
if (accountToken) return { token: accountToken, source: "config" };
|
||||
if (accountToken) {
|
||||
return { token: accountToken, source: "config" };
|
||||
}
|
||||
|
||||
const allowEnv = accountId === DEFAULT_ACCOUNT_ID;
|
||||
const configToken = allowEnv ? normalizeDiscordToken(discordCfg?.token ?? undefined) : undefined;
|
||||
if (configToken) return { token: configToken, source: "config" };
|
||||
if (configToken) {
|
||||
return { token: configToken, source: "config" };
|
||||
}
|
||||
|
||||
const envToken = allowEnv
|
||||
? normalizeDiscordToken(opts.envToken ?? process.env.DISCORD_BOT_TOKEN)
|
||||
: undefined;
|
||||
if (envToken) return { token: envToken, source: "env" };
|
||||
if (envToken) {
|
||||
return { token: envToken, source: "env" };
|
||||
}
|
||||
|
||||
return { token: "", source: "none" };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user