@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,349 lines (1,335 loc) • 104 kB
JavaScript
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js";
import { Nt as resolvePreferredOpenClawTmpDir } from "./entry.js";
import { i as loadConfig } from "./config-B2kL1ciP.js";
import { h as normalizeDiscordToken, m as resolveDiscordAccount } from "./normalize-Db7Xtx2v.js";
import { a as requireTargetKind, i as parseTargetPrefixes, n as ensureTargetId, r as parseTargetMention, t as buildMessagingTarget } from "./targets-DJmA1jvy.js";
import { _ as maxBytesForKind, l as extensionForMime } from "./image-ops-lDlFpoR2.js";
import { n as retryAsync, t as resolveRetryConfig } from "./retry-BcjnIuo-.js";
import { c as resolveChunkMode, i as chunkMarkdownTextWithMode } from "./chunk-Z2NYDchs.js";
import { n as resolveMarkdownTableMode } from "./markdown-tables-GCHx-nGT.js";
import { a as loadWebMedia, o as loadWebMediaRaw } from "./ir-DAwVi0a7.js";
import { t as resolveFetch } from "./fetch-vg2oFVIH.js";
import { n as normalizePollInput, t as normalizePollDurationHours } from "./polls-BmLZB1uH.js";
import { n as recordChannelActivity, r as createDiscordRetryRunner } from "./channel-activity-BYGpAtHP.js";
import { t as convertMarkdownTables } from "./tables-BrqD0SUa.js";
import { execFile } from "node:child_process";
import path from "node:path";
import { promisify } from "node:util";
import fs from "node:fs/promises";
import crypto from "node:crypto";
import { Button, ChannelSelectMenu, CheckboxGroup, Container, Embed, File, Label, LinkButton, MediaGallery, MentionableSelectMenu, Modal, RadioGroup, RequestClient, RoleSelectMenu, Row, Section, Separator, StringSelectMenu, TextDisplay, TextInput, Thumbnail, UserSelectMenu, parseCustomId, serializePayload } from "@buape/carbon";
import { ButtonStyle, ChannelType as ChannelType$1, MessageFlags, PermissionFlagsBits, Routes, TextInputStyle } from "discord-api-types/v10";
import { PollLayoutType } from "discord-api-types/payloads/v10";
//#region src/channels/channel-config.ts
function applyChannelMatchMeta(result, match) {
if (match.matchKey && match.matchSource) {
result.matchKey = match.matchKey;
result.matchSource = match.matchSource;
}
return result;
}
function resolveChannelMatchConfig(match, resolveEntry) {
if (!match.entry) return null;
return applyChannelMatchMeta(resolveEntry(match.entry), match);
}
function normalizeChannelSlug(value) {
return value.trim().toLowerCase().replace(/^#/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
function buildChannelKeyCandidates(...keys) {
const seen = /* @__PURE__ */ new Set();
const candidates = [];
for (const key of keys) {
if (typeof key !== "string") continue;
const trimmed = key.trim();
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
candidates.push(trimmed);
}
return candidates;
}
function resolveChannelEntryMatch(params) {
const entries = params.entries ?? {};
const match = {};
for (const key of params.keys) {
if (!Object.prototype.hasOwnProperty.call(entries, key)) continue;
match.entry = entries[key];
match.key = key;
break;
}
if (params.wildcardKey && Object.prototype.hasOwnProperty.call(entries, params.wildcardKey)) {
match.wildcardEntry = entries[params.wildcardKey];
match.wildcardKey = params.wildcardKey;
}
return match;
}
function resolveChannelEntryMatchWithFallback(params) {
const direct = resolveChannelEntryMatch({
entries: params.entries,
keys: params.keys,
wildcardKey: params.wildcardKey
});
if (direct.entry && direct.key) return {
...direct,
matchKey: direct.key,
matchSource: "direct"
};
const normalizeKey = params.normalizeKey;
if (normalizeKey) {
const normalizedKeys = params.keys.map((key) => normalizeKey(key)).filter(Boolean);
if (normalizedKeys.length > 0) for (const [entryKey, entry] of Object.entries(params.entries ?? {})) {
const normalizedEntry = normalizeKey(entryKey);
if (normalizedEntry && normalizedKeys.includes(normalizedEntry)) return {
...direct,
entry,
key: entryKey,
matchKey: entryKey,
matchSource: "direct"
};
}
}
const parentKeys = params.parentKeys ?? [];
if (parentKeys.length > 0) {
const parent = resolveChannelEntryMatch({
entries: params.entries,
keys: parentKeys
});
if (parent.entry && parent.key) return {
...direct,
entry: parent.entry,
key: parent.key,
parentEntry: parent.entry,
parentKey: parent.key,
matchKey: parent.key,
matchSource: "parent"
};
if (normalizeKey) {
const normalizedParentKeys = parentKeys.map((key) => normalizeKey(key)).filter(Boolean);
if (normalizedParentKeys.length > 0) for (const [entryKey, entry] of Object.entries(params.entries ?? {})) {
const normalizedEntry = normalizeKey(entryKey);
if (normalizedEntry && normalizedParentKeys.includes(normalizedEntry)) return {
...direct,
entry,
key: entryKey,
parentEntry: entry,
parentKey: entryKey,
matchKey: entryKey,
matchSource: "parent"
};
}
}
}
if (direct.wildcardEntry && direct.wildcardKey) return {
...direct,
entry: direct.wildcardEntry,
key: direct.wildcardKey,
matchKey: direct.wildcardKey,
matchSource: "wildcard"
};
return direct;
}
//#endregion
//#region src/discord/api.ts
const DISCORD_API_BASE = "https://discord.com/api/v10";
const DISCORD_API_RETRY_DEFAULTS = {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 3e4,
jitter: .1
};
function parseDiscordApiErrorPayload(text) {
const trimmed = text.trim();
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
try {
const payload = JSON.parse(trimmed);
if (payload && typeof payload === "object") return payload;
} catch {
return null;
}
return null;
}
function parseRetryAfterSeconds(text, response) {
const payload = parseDiscordApiErrorPayload(text);
const retryAfter = payload && typeof payload.retry_after === "number" && Number.isFinite(payload.retry_after) ? payload.retry_after : void 0;
if (retryAfter !== void 0) return retryAfter;
const header = response.headers.get("Retry-After");
if (!header) return;
const parsed = Number(header);
return Number.isFinite(parsed) ? parsed : void 0;
}
function formatRetryAfterSeconds(value) {
if (value === void 0 || !Number.isFinite(value) || value < 0) return;
return `${value < 10 ? value.toFixed(1) : Math.round(value).toString()}s`;
}
function formatDiscordApiErrorText(text) {
const trimmed = text.trim();
if (!trimmed) return;
const payload = parseDiscordApiErrorPayload(trimmed);
if (!payload) return trimmed.startsWith("{") && trimmed.endsWith("}") ? "unknown error" : trimmed;
const message = typeof payload.message === "string" && payload.message.trim() ? payload.message.trim() : "unknown error";
const retryAfter = formatRetryAfterSeconds(typeof payload.retry_after === "number" ? payload.retry_after : void 0);
return retryAfter ? `${message} (retry after ${retryAfter})` : message;
}
var DiscordApiError = class extends Error {
constructor(message, status, retryAfter) {
super(message);
this.status = status;
this.retryAfter = retryAfter;
}
};
async function fetchDiscord(path, token, fetcher = fetch, options) {
const fetchImpl = resolveFetch(fetcher);
if (!fetchImpl) throw new Error("fetch is not available");
return retryAsync(async () => {
const res = await fetchImpl(`${DISCORD_API_BASE}${path}`, { headers: { Authorization: `Bot ${token}` } });
if (!res.ok) {
const text = await res.text().catch(() => "");
const detail = formatDiscordApiErrorText(text);
const suffix = detail ? `: ${detail}` : "";
const retryAfter = res.status === 429 ? parseRetryAfterSeconds(text, res) : void 0;
throw new DiscordApiError(`Discord API ${path} failed (${res.status})${suffix}`, res.status, retryAfter);
}
return await res.json();
}, {
...resolveRetryConfig(DISCORD_API_RETRY_DEFAULTS, options?.retry),
label: options?.label ?? path,
shouldRetry: (err) => err instanceof DiscordApiError && err.status === 429,
retryAfterMs: (err) => err instanceof DiscordApiError && typeof err.retryAfter === "number" ? err.retryAfter * 1e3 : void 0
});
}
//#endregion
//#region src/discord/monitor/format.ts
function resolveDiscordSystemLocation(params) {
const { isDirectMessage, isGroupDm, guild, channelName } = params;
if (isDirectMessage) return "DM";
if (isGroupDm) return `Group DM #${channelName}`;
return guild?.name ? `${guild.name} #${channelName}` : `#${channelName}`;
}
function formatDiscordReactionEmoji(emoji) {
if (emoji.id && emoji.name) return `<:${emoji.name}:${emoji.id}>`;
if (emoji.id) return `emoji:${emoji.id}`;
return emoji.name ?? "emoji";
}
function formatDiscordUserTag(user) {
const discriminator = (user.discriminator ?? "").trim();
if (discriminator && discriminator !== "0") return `${user.username}#${discriminator}`;
return user.username ?? user.id;
}
function resolveTimestampMs(timestamp) {
if (!timestamp) return;
const parsed = Date.parse(timestamp);
return Number.isNaN(parsed) ? void 0 : parsed;
}
//#endregion
//#region src/discord/monitor/allow-list.ts
function normalizeDiscordAllowList(raw, prefixes) {
if (!raw || raw.length === 0) return null;
const ids = /* @__PURE__ */ new Set();
const names = /* @__PURE__ */ new Set();
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);
const maybeId = text.replace(/^<@!?/, "").replace(/>$/, "");
if (/^\d+$/.test(maybeId)) {
ids.add(maybeId);
continue;
}
const prefix = prefixes.find((entry) => text.startsWith(entry));
if (prefix) {
const candidate = text.slice(prefix.length);
if (candidate) ids.add(candidate);
continue;
}
if (normalized) names.add(normalized);
}
return {
allowAll,
ids,
names
};
}
function normalizeDiscordSlug(value) {
return value.trim().toLowerCase().replace(/^#/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
function allowListMatches(list, candidate) {
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;
}
function resolveDiscordAllowListMatch(params) {
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 };
}
function resolveDiscordUserAllowed(params) {
const allowList = normalizeDiscordAllowList(params.allowList, [
"discord:",
"user:",
"pk:"
]);
if (!allowList) return true;
return allowListMatches(allowList, {
id: params.userId,
name: params.userName,
tag: params.userTag
});
}
function resolveDiscordRoleAllowed(params) {
const allowList = normalizeDiscordAllowList(params.allowList, ["role:"]);
if (!allowList) return true;
if (allowList.allowAll) return true;
return params.memberRoleIds.some((roleId) => allowList.ids.has(roleId));
}
function resolveDiscordMemberAllowed(params) {
const hasUserRestriction = Array.isArray(params.userAllowList) && params.userAllowList.length > 0;
const hasRoleRestriction = Array.isArray(params.roleAllowList) && params.roleAllowList.length > 0;
if (!hasUserRestriction && !hasRoleRestriction) return true;
const userOk = hasUserRestriction ? resolveDiscordUserAllowed({
allowList: params.userAllowList,
userId: params.userId,
userName: params.userName,
userTag: params.userTag
}) : false;
const roleOk = hasRoleRestriction ? resolveDiscordRoleAllowed({
allowList: params.roleAllowList,
memberRoleIds: params.memberRoleIds
}) : false;
return userOk || roleOk;
}
function resolveDiscordMemberAccessState(params) {
const channelUsers = params.channelConfig?.users ?? params.guildInfo?.users;
const channelRoles = params.channelConfig?.roles ?? params.guildInfo?.roles;
return {
channelUsers,
channelRoles,
hasAccessRestrictions: Array.isArray(channelUsers) && channelUsers.length > 0 || Array.isArray(channelRoles) && channelRoles.length > 0,
memberAllowed: resolveDiscordMemberAllowed({
userAllowList: channelUsers,
roleAllowList: channelRoles,
memberRoleIds: params.memberRoleIds,
userId: params.sender.id,
userName: params.sender.name,
userTag: params.sender.tag
})
};
}
function resolveDiscordOwnerAllowFrom(params) {
const rawAllowList = params.channelConfig?.users ?? params.guildInfo?.users;
if (!Array.isArray(rawAllowList) || rawAllowList.length === 0) return;
const allowList = normalizeDiscordAllowList(rawAllowList, [
"discord:",
"user:",
"pk:"
]);
if (!allowList) return;
const match = resolveDiscordAllowListMatch({
allowList,
candidate: {
id: params.sender.id,
name: params.sender.name,
tag: params.sender.tag
}
});
if (!match.allowed || !match.matchKey || match.matchKey === "*") return;
return [match.matchKey];
}
function resolveDiscordGuildEntry(params) {
const guild = params.guild;
const entries = params.guildEntries;
if (!guild || !entries) return null;
const byId = entries[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
};
const wildcard = entries["*"];
if (wildcard) return {
...wildcard,
id: guild.id,
slug: slug || wildcard.slug
};
return null;
}
function buildDiscordChannelKeys(params) {
const allowNameMatch = params.allowNameMatch !== false;
return buildChannelKeyCandidates(params.id, allowNameMatch ? params.slug : void 0, allowNameMatch ? params.name : void 0);
}
function resolveDiscordChannelEntryMatch(channels, params, parentParams) {
return resolveChannelEntryMatchWithFallback({
entries: channels,
keys: buildDiscordChannelKeys(params),
parentKeys: parentParams ? buildDiscordChannelKeys(parentParams) : void 0,
wildcardKey: "*"
});
}
function hasConfiguredDiscordChannels(channels) {
return Boolean(channels && Object.keys(channels).length > 0);
}
function resolveDiscordChannelConfigEntry(entry) {
return {
allowed: entry.allow !== false,
requireMention: entry.requireMention,
skills: entry.skills,
enabled: entry.enabled,
users: entry.users,
roles: entry.roles,
systemPrompt: entry.systemPrompt,
includeThreadStarter: entry.includeThreadStarter,
autoThread: entry.autoThread
};
}
function resolveDiscordChannelConfigWithFallback(params) {
const { guildInfo, channelId, channelName, channelSlug, parentId, parentName, parentSlug, scope } = params;
const channels = guildInfo?.channels;
if (!hasConfiguredDiscordChannels(channels)) return null;
const resolvedParentSlug = parentSlug ?? (parentName ? normalizeDiscordSlug(parentName) : "");
return resolveChannelMatchConfig(resolveDiscordChannelEntryMatch(channels, {
id: channelId,
name: channelName,
slug: channelSlug,
allowNameMatch: scope !== "thread"
}, parentId || parentName || parentSlug ? {
id: parentId ?? "",
name: parentName,
slug: resolvedParentSlug
} : void 0), resolveDiscordChannelConfigEntry) ?? { allowed: false };
}
function resolveDiscordShouldRequireMention(params) {
if (!params.isGuildMessage) return false;
if (params.isAutoThreadOwnedByBot ?? isDiscordAutoThreadOwnedByBot(params)) return false;
return params.channelConfig?.requireMention ?? params.guildInfo?.requireMention ?? true;
}
function isDiscordAutoThreadOwnedByBot(params) {
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);
}
function isDiscordGroupAllowedByPolicy(params) {
const { groupPolicy, guildAllowlisted, channelAllowlistConfigured, channelAllowed } = params;
if (groupPolicy === "disabled") return false;
if (groupPolicy === "open") return true;
if (!guildAllowlisted) return false;
if (!channelAllowlistConfigured) return true;
return channelAllowed;
}
function resolveGroupDmAllow(params) {
const { channels, channelId, channelName, channelSlug } = params;
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));
}
function shouldEmitDiscordReactionNotification(params) {
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;
}
//#endregion
//#region src/discord/directory-live.ts
function normalizeQuery(value) {
return value?.trim().toLowerCase() ?? "";
}
function buildUserRank(user) {
return user.bot ? 0 : 1;
}
async function listDiscordDirectoryGroupsLive(params) {
const token = normalizeDiscordToken(resolveDiscordAccount({
cfg: params.cfg,
accountId: params.accountId
}).token);
if (!token) return [];
const query = normalizeQuery(params.query);
const guilds = (await fetchDiscord("/users/@me/guilds", token)).filter((g) => g.id && g.name);
const rows = [];
for (const guild of guilds) {
const channels = await fetchDiscord(`/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;
rows.push({
kind: "group",
id: `channel:${channel.id}`,
name,
handle: `#${name}`,
raw: channel
});
if (typeof params.limit === "number" && params.limit > 0 && rows.length >= params.limit) return rows;
}
}
return rows;
}
async function listDiscordDirectoryPeersLive(params) {
const token = normalizeDiscordToken(resolveDiscordAccount({
cfg: params.cfg,
accountId: params.accountId
}).token);
if (!token) return [];
const query = normalizeQuery(params.query);
if (!query) return [];
const guilds = (await fetchDiscord("/users/@me/guilds", token)).filter((g) => g.id && g.name);
const rows = [];
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 25;
for (const guild of guilds) {
const paramsObj = new URLSearchParams({
query,
limit: String(Math.min(limit, 100))
});
const members = await fetchDiscord(`/guilds/${guild.id}/members/search?${paramsObj.toString()}`, token);
for (const member of members) {
const user = member.user;
if (!user?.id) continue;
const name = member.nick?.trim() || user.global_name?.trim() || user.username?.trim();
rows.push({
kind: "user",
id: `user:${user.id}`,
name: name || void 0,
handle: user.username ? `@${user.username}` : void 0,
rank: buildUserRank(user),
raw: member
});
if (rows.length >= limit) return rows;
}
}
return rows;
}
//#endregion
//#region src/discord/targets.ts
function parseDiscordTarget(raw, options = {}) {
const trimmed = raw.trim();
if (!trimmed) return;
const mentionTarget = parseTargetMention({
raw: trimmed,
mentionPattern: /^<@!?(\d+)>$/,
kind: "user"
});
if (mentionTarget) return mentionTarget;
const prefixedTarget = parseTargetPrefixes({
raw: trimmed,
prefixes: [
{
prefix: "user:",
kind: "user"
},
{
prefix: "channel:",
kind: "channel"
},
{
prefix: "discord:",
kind: "user"
}
]
});
if (prefixedTarget) return prefixedTarget;
if (trimmed.startsWith("@")) return buildMessagingTarget("user", ensureTargetId({
candidate: trimmed.slice(1).trim(),
pattern: /^\d+$/,
errorMessage: "Discord DMs require a user id (use user:<id> or a <@id> mention)"
}), trimmed);
if (/^\d+$/.test(trimmed)) {
if (options.defaultKind) return buildMessagingTarget(options.defaultKind, trimmed, trimmed);
throw new Error(options.ambiguousMessage ?? `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.`);
}
return buildMessagingTarget("channel", trimmed, trimmed);
}
function resolveDiscordChannelId(raw) {
return requireTargetKind({
platform: "Discord",
target: parseDiscordTarget(raw, { defaultKind: "channel" }),
kind: "channel"
});
}
/**
* Resolve a Discord username to user ID using the directory lookup.
* This enables sending DMs by username instead of requiring explicit user IDs.
*
* @param raw - The username or raw target string (e.g., "john.doe")
* @param options - Directory configuration params (cfg, accountId, limit)
* @param parseOptions - Messaging target parsing options (defaults, ambiguity message)
* @returns Parsed MessagingTarget with user ID, or undefined if not found
*/
async function resolveDiscordTarget(raw, options, parseOptions = {}) {
const trimmed = raw.trim();
if (!trimmed) return;
const likelyUsername = isLikelyUsername(trimmed);
const shouldLookup = isExplicitUserLookup(trimmed, parseOptions) || likelyUsername;
const directParse = safeParseDiscordTarget(trimmed, parseOptions);
if (directParse && directParse.kind !== "channel" && !likelyUsername) return directParse;
if (!shouldLookup) return directParse ?? parseDiscordTarget(trimmed, parseOptions);
try {
const match = (await listDiscordDirectoryPeersLive({
...options,
query: trimmed,
limit: 1
}))[0];
if (match && match.kind === "user") return buildMessagingTarget("user", match.id.replace(/^user:/, ""), trimmed);
} catch {}
return parseDiscordTarget(trimmed, parseOptions);
}
function safeParseDiscordTarget(input, options) {
try {
return parseDiscordTarget(input, options);
} catch {
return;
}
}
function isExplicitUserLookup(input, options) {
if (/^<@!?(\d+)>$/.test(input)) return true;
if (/^(user:|discord:)/.test(input)) return true;
if (input.startsWith("@")) return true;
if (/^\d+$/.test(input)) return options.defaultKind === "user";
return false;
}
/**
* Check if a string looks like a Discord username (not a mention, prefix, or ID).
* Usernames typically don't start with special characters except underscore.
*/
function isLikelyUsername(input) {
if (/^(user:|channel:|discord:|@|<@!?)|[\d]+$/.test(input)) return false;
return true;
}
//#endregion
//#region src/discord/chunk.ts
const DEFAULT_MAX_CHARS = 2e3;
const DEFAULT_MAX_LINES = 17;
const FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
function countLines(text) {
if (!text) return 0;
return text.split("\n").length;
}
function parseFenceLine(line) {
const match = line.match(FENCE_RE);
if (!match) return null;
const indent = match[1] ?? "";
const marker = match[2] ?? "";
return {
indent,
markerChar: marker[0] ?? "`",
markerLen: marker.length,
openLine: line
};
}
function closeFenceLine(openFence) {
return `${openFence.indent}${openFence.markerChar.repeat(openFence.markerLen)}`;
}
function closeFenceIfNeeded(text, openFence) {
if (!openFence) return text;
const closeLine = closeFenceLine(openFence);
if (!text) return closeLine;
if (!text.endsWith("\n")) return `${text}\n${closeLine}`;
return `${text}${closeLine}`;
}
function splitLongLine(line, maxChars, opts) {
const limit = Math.max(1, Math.floor(maxChars));
if (line.length <= limit) return [line];
const out = [];
let remaining = line;
while (remaining.length > limit) {
if (opts.preserveWhitespace) {
out.push(remaining.slice(0, limit));
remaining = remaining.slice(limit);
continue;
}
const window = remaining.slice(0, limit);
let breakIdx = -1;
for (let i = window.length - 1; i >= 0; i--) if (/\s/.test(window[i])) {
breakIdx = i;
break;
}
if (breakIdx <= 0) breakIdx = limit;
out.push(remaining.slice(0, breakIdx));
remaining = remaining.slice(breakIdx);
}
if (remaining.length) out.push(remaining);
return out;
}
/**
* Chunks outbound Discord text by both character count and (soft) line count,
* while keeping fenced code blocks balanced across chunks.
*/
function chunkDiscordText(text, opts = {}) {
const maxChars = Math.max(1, Math.floor(opts.maxChars ?? DEFAULT_MAX_CHARS));
const maxLines = Math.max(1, Math.floor(opts.maxLines ?? DEFAULT_MAX_LINES));
const body = text ?? "";
if (!body) return [];
if (body.length <= maxChars && countLines(body) <= maxLines) return [body];
const lines = body.split("\n");
const chunks = [];
let current = "";
let currentLines = 0;
let openFence = null;
const flush = () => {
if (!current) return;
const payload = closeFenceIfNeeded(current, openFence);
if (payload.trim().length) chunks.push(payload);
current = "";
currentLines = 0;
if (openFence) {
current = openFence.openLine;
currentLines = 1;
}
};
for (const originalLine of lines) {
const fenceInfo = parseFenceLine(originalLine);
const wasInsideFence = openFence !== null;
let nextOpenFence = openFence;
if (fenceInfo) {
if (!openFence) nextOpenFence = fenceInfo;
else if (openFence.markerChar === fenceInfo.markerChar && fenceInfo.markerLen >= openFence.markerLen) nextOpenFence = null;
}
const reserveChars = nextOpenFence ? closeFenceLine(nextOpenFence).length + 1 : 0;
const reserveLines = nextOpenFence ? 1 : 0;
const effectiveMaxChars = maxChars - reserveChars;
const effectiveMaxLines = maxLines - reserveLines;
const charLimit = effectiveMaxChars > 0 ? effectiveMaxChars : maxChars;
const lineLimit = effectiveMaxLines > 0 ? effectiveMaxLines : maxLines;
const prefixLen = current.length > 0 ? current.length + 1 : 0;
const segments = splitLongLine(originalLine, Math.max(1, charLimit - prefixLen), { preserveWhitespace: wasInsideFence });
for (let segIndex = 0; segIndex < segments.length; segIndex++) {
const segment = segments[segIndex];
const isLineContinuation = segIndex > 0;
const addition = `${isLineContinuation ? "" : current.length > 0 ? "\n" : ""}${segment}`;
const nextLen = current.length + addition.length;
const nextLines = currentLines + (isLineContinuation ? 0 : 1);
if ((nextLen > charLimit || nextLines > lineLimit) && current.length > 0) flush();
if (current.length > 0) {
current += addition;
if (!isLineContinuation) currentLines += 1;
} else {
current = segment;
currentLines = 1;
}
}
openFence = nextOpenFence;
}
if (current.length) {
const payload = closeFenceIfNeeded(current, openFence);
if (payload.trim().length) chunks.push(payload);
}
return rebalanceReasoningItalics(text, chunks);
}
function chunkDiscordTextWithMode(text, opts) {
if ((opts.chunkMode ?? "length") !== "newline") return chunkDiscordText(text, opts);
const lineChunks = chunkMarkdownTextWithMode(text, Math.max(1, Math.floor(opts.maxChars ?? DEFAULT_MAX_CHARS)), "newline");
const chunks = [];
for (const line of lineChunks) {
const nested = chunkDiscordText(line, opts);
if (!nested.length && line) {
chunks.push(line);
continue;
}
chunks.push(...nested);
}
return chunks;
}
function rebalanceReasoningItalics(source, chunks) {
if (chunks.length <= 1) return chunks;
if (!(source.startsWith("Reasoning:\n_") && source.trimEnd().endsWith("_"))) return chunks;
const adjusted = [...chunks];
for (let i = 0; i < adjusted.length; i++) {
const isLast = i === adjusted.length - 1;
const current = adjusted[i];
if (!current.trimEnd().endsWith("_")) adjusted[i] = `${current}_`;
if (isLast) break;
const next = adjusted[i + 1];
const leadingWhitespaceLen = next.length - next.trimStart().length;
const leadingWhitespace = next.slice(0, leadingWhitespaceLen);
const nextBody = next.slice(leadingWhitespaceLen);
if (!nextBody.startsWith("_")) adjusted[i + 1] = `${leadingWhitespace}_${nextBody}`;
}
return adjusted;
}
//#endregion
//#region src/discord/client.ts
function resolveToken(params) {
const explicit = normalizeDiscordToken(params.explicit);
if (explicit) return explicit;
const fallback = normalizeDiscordToken(params.fallbackToken);
if (!fallback) throw new Error(`Discord bot token missing for account "${params.accountId}" (set discord.accounts.${params.accountId}.token or DISCORD_BOT_TOKEN for default).`);
return fallback;
}
function resolveRest(token, rest) {
return rest ?? new RequestClient(token);
}
function createDiscordRestClient(opts, cfg = loadConfig()) {
const account = resolveDiscordAccount({
cfg,
accountId: opts.accountId
});
const token = resolveToken({
explicit: opts.token,
accountId: account.accountId,
fallbackToken: account.token
});
return {
token,
rest: resolveRest(token, opts.rest),
account
};
}
function createDiscordClient(opts, cfg = loadConfig()) {
const { token, rest, account } = createDiscordRestClient(opts, cfg);
return {
token,
rest,
request: createDiscordRetryRunner({
retry: opts.retry,
configRetry: account.config.retry,
verbose: opts.verbose
})
};
}
function resolveDiscordRest(opts) {
return createDiscordRestClient(opts).rest;
}
//#endregion
//#region src/discord/send.permissions.ts
const PERMISSION_ENTRIES = Object.entries(PermissionFlagsBits).filter(([, value]) => typeof value === "bigint");
const ALL_PERMISSIONS = PERMISSION_ENTRIES.reduce((acc, [, value]) => acc | value, 0n);
const ADMINISTRATOR_BIT = PermissionFlagsBits.Administrator;
function addPermissionBits(base, add) {
if (!add) return base;
return base | BigInt(add);
}
function removePermissionBits(base, deny) {
if (!deny) return base;
return base & ~BigInt(deny);
}
function bitfieldToPermissions(bitfield) {
return PERMISSION_ENTRIES.filter(([, value]) => (bitfield & value) === value).map(([name]) => name).toSorted();
}
function hasAdministrator(bitfield) {
return (bitfield & ADMINISTRATOR_BIT) === ADMINISTRATOR_BIT;
}
function hasPermissionBit(bitfield, permission) {
return (bitfield & permission) === permission;
}
function isThreadChannelType(channelType) {
return channelType === ChannelType$1.GuildNewsThread || channelType === ChannelType$1.GuildPublicThread || channelType === ChannelType$1.GuildPrivateThread;
}
async function fetchBotUserId(rest) {
const me = await rest.get(Routes.user("@me"));
if (!me?.id) throw new Error("Failed to resolve bot user id");
return me.id;
}
/**
* Fetch guild-level permissions for a user. This does not include channel-specific overwrites.
*/
async function fetchMemberGuildPermissionsDiscord(guildId, userId, opts = {}) {
const rest = resolveDiscordRest(opts);
try {
const [guild, member] = await Promise.all([rest.get(Routes.guild(guildId)), rest.get(Routes.guildMember(guildId, userId))]);
const rolesById = new Map((guild.roles ?? []).map((role) => [role.id, role]));
const everyoneRole = rolesById.get(guildId);
let permissions = 0n;
if (everyoneRole?.permissions) permissions = addPermissionBits(permissions, everyoneRole.permissions);
for (const roleId of member.roles ?? []) {
const role = rolesById.get(roleId);
if (role?.permissions) permissions = addPermissionBits(permissions, role.permissions);
}
return permissions;
} catch {
return null;
}
}
/**
* Returns true when the user has ADMINISTRATOR or any required permission bit.
*/
async function hasAnyGuildPermissionDiscord(guildId, userId, requiredPermissions, opts = {}) {
const permissions = await fetchMemberGuildPermissionsDiscord(guildId, userId, opts);
if (permissions === null) return false;
if (hasAdministrator(permissions)) return true;
return requiredPermissions.some((permission) => hasPermissionBit(permissions, permission));
}
async function fetchChannelPermissionsDiscord(channelId, opts = {}) {
const rest = resolveDiscordRest(opts);
const channel = await rest.get(Routes.channel(channelId));
const channelType = "type" in channel ? channel.type : void 0;
const guildId = "guild_id" in channel ? channel.guild_id : void 0;
if (!guildId) return {
channelId,
permissions: [],
raw: "0",
isDm: true,
channelType
};
const botId = await fetchBotUserId(rest);
const [guild, member] = await Promise.all([rest.get(Routes.guild(guildId)), rest.get(Routes.guildMember(guildId, botId))]);
const rolesById = new Map((guild.roles ?? []).map((role) => [role.id, role]));
const everyoneRole = rolesById.get(guildId);
let base = 0n;
if (everyoneRole?.permissions) base = addPermissionBits(base, everyoneRole.permissions);
for (const roleId of member.roles ?? []) {
const role = rolesById.get(roleId);
if (role?.permissions) base = addPermissionBits(base, role.permissions);
}
if (hasAdministrator(base)) return {
channelId,
guildId,
permissions: bitfieldToPermissions(ALL_PERMISSIONS),
raw: ALL_PERMISSIONS.toString(),
isDm: false,
channelType
};
let permissions = base;
const overwrites = "permission_overwrites" in channel ? channel.permission_overwrites ?? [] : [];
for (const overwrite of overwrites) if (overwrite.id === guildId) {
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
}
for (const overwrite of overwrites) if (member.roles?.includes(overwrite.id)) {
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
}
for (const overwrite of overwrites) if (overwrite.id === botId) {
permissions = removePermissionBits(permissions, overwrite.deny ?? "0");
permissions = addPermissionBits(permissions, overwrite.allow ?? "0");
}
return {
channelId,
guildId,
permissions: bitfieldToPermissions(permissions),
raw: permissions.toString(),
isDm: false,
channelType
};
}
//#endregion
//#region src/discord/send.types.ts
var DiscordSendError = class extends Error {
constructor(message, opts) {
super(message);
this.name = "DiscordSendError";
if (opts) Object.assign(this, opts);
}
toString() {
return this.message;
}
};
const DISCORD_MAX_EMOJI_BYTES = 256 * 1024;
const DISCORD_MAX_STICKER_BYTES = 512 * 1024;
//#endregion
//#region src/discord/send.shared.ts
const DISCORD_TEXT_LIMIT = 2e3;
const DISCORD_MAX_STICKERS = 3;
const DISCORD_POLL_MAX_ANSWERS = 10;
const DISCORD_POLL_MAX_DURATION_HOURS = 768;
const DISCORD_MISSING_PERMISSIONS = 50013;
const DISCORD_CANNOT_DM = 50007;
function normalizeReactionEmoji(raw) {
const trimmed = raw.trim();
if (!trimmed) throw new Error("emoji required");
const customMatch = trimmed.match(/^<a?:([^:>]+):(\d+)>$/);
const identifier = customMatch ? `${customMatch[1]}:${customMatch[2]}` : trimmed.replace(/[\uFE0E\uFE0F]/g, "");
return encodeURIComponent(identifier);
}
/**
* Parse and resolve Discord recipient, including username lookup.
* This enables sending DMs by username (e.g., "john.doe") by querying
* the Discord directory to resolve usernames to user IDs.
*
* @param raw - The recipient string (username, ID, or known format)
* @param accountId - Discord account ID to use for directory lookup
* @returns Parsed DiscordRecipient with resolved user ID if applicable
*/
async function parseAndResolveRecipient(raw, accountId) {
const cfg = loadConfig();
const accountInfo = resolveDiscordAccount({
cfg,
accountId
});
const trimmed = raw.trim();
const parseOptions = { ambiguousMessage: `Ambiguous Discord recipient "${trimmed}". Use "user:${trimmed}" for DMs or "channel:${trimmed}" for channel messages.` };
const resolved = await resolveDiscordTarget(raw, {
cfg,
accountId: accountInfo.accountId
}, parseOptions);
if (resolved) return {
kind: resolved.kind,
id: resolved.id
};
const parsed = parseDiscordTarget(raw, parseOptions);
if (!parsed) throw new Error("Recipient is required for Discord sends");
return {
kind: parsed.kind,
id: parsed.id
};
}
function normalizeStickerIds(raw) {
const ids = raw.map((entry) => entry.trim()).filter(Boolean);
if (ids.length === 0) throw new Error("At least one sticker id is required");
if (ids.length > DISCORD_MAX_STICKERS) throw new Error("Discord supports up to 3 stickers per message");
return ids;
}
function normalizeEmojiName(raw, label) {
const name = raw.trim();
if (!name) throw new Error(`${label} is required`);
return name;
}
function normalizeDiscordPollInput(input) {
const poll = normalizePollInput(input, { maxOptions: DISCORD_POLL_MAX_ANSWERS });
const duration = normalizePollDurationHours(poll.durationHours, {
defaultHours: 24,
maxHours: DISCORD_POLL_MAX_DURATION_HOURS
});
return {
question: { text: poll.question },
answers: poll.options.map((answer) => ({ poll_media: { text: answer } })),
duration,
allow_multiselect: poll.maxSelections > 1,
layout_type: PollLayoutType.Default
};
}
function getDiscordErrorCode(err) {
if (!err || typeof err !== "object") return;
const candidate = "code" in err && err.code !== void 0 ? err.code : "rawError" in err && err.rawError && typeof err.rawError === "object" ? err.rawError.code : void 0;
if (typeof candidate === "number") return candidate;
if (typeof candidate === "string" && /^\d+$/.test(candidate)) return Number(candidate);
}
async function buildDiscordSendError(err, ctx) {
if (err instanceof DiscordSendError) return err;
const code = getDiscordErrorCode(err);
if (code === DISCORD_CANNOT_DM) return new DiscordSendError("discord dm failed: user blocks dms or privacy settings disallow it", { kind: "dm-blocked" });
if (code !== DISCORD_MISSING_PERMISSIONS) return err;
let missing = [];
try {
const permissions = await fetchChannelPermissionsDiscord(ctx.channelId, {
rest: ctx.rest,
token: ctx.token
});
const current = new Set(permissions.permissions);
const required = ["ViewChannel", "SendMessages"];
if (isThreadChannelType(permissions.channelType)) required.push("SendMessagesInThreads");
if (ctx.hasMedia) required.push("AttachFiles");
missing = required.filter((permission) => !current.has(permission));
} catch {}
return new DiscordSendError(`${missing.length ? `missing permissions in channel ${ctx.channelId}: ${missing.join(", ")}` : `missing permissions in channel ${ctx.channelId}`}. bot might be muted or blocked by role/channel overrides`, {
kind: "missing-permissions",
channelId: ctx.channelId,
missingPermissions: missing
});
}
async function resolveChannelId(rest, recipient, request) {
if (recipient.kind === "channel") return { channelId: recipient.id };
const dmChannel = await request(() => rest.post(Routes.userChannels(), { body: { recipient_id: recipient.id } }), "dm-channel");
if (!dmChannel?.id) throw new Error("Failed to create Discord DM channel");
return {
channelId: dmChannel.id,
dm: true
};
}
const SUPPRESS_NOTIFICATIONS_FLAG$1 = 4096;
function buildDiscordTextChunks(text, opts = {}) {
if (!text) return [];
const chunks = chunkDiscordTextWithMode(text, {
maxChars: opts.maxChars ?? DISCORD_TEXT_LIMIT,
maxLines: opts.maxLinesPerMessage,
chunkMode: opts.chunkMode
});
if (!chunks.length && text) chunks.push(text);
return chunks;
}
function hasV2Components(components) {
return Boolean(components?.some((component) => "isV2" in component && component.isV2));
}
function resolveDiscordSendComponents(params) {
if (!params.components || !params.isFirst) return;
return typeof params.components === "function" ? params.components(params.text) : params.components;
}
function normalizeDiscordEmbeds(embeds) {
if (!embeds?.length) return;
return embeds.map((embed) => embed instanceof Embed ? embed : new Embed(embed));
}
function resolveDiscordSendEmbeds(params) {
if (!params.embeds || !params.isFirst) return;
return normalizeDiscordEmbeds(params.embeds);
}
function buildDiscordMessagePayload(params) {
const payload = {};
const hasV2 = hasV2Components(params.components);
const trimmed = params.text.trim();
if (!hasV2 && trimmed) payload.content = params.text;
if (params.components?.length) payload.components = params.components;
if (!hasV2 && params.embeds?.length) payload.embeds = params.embeds;
if (params.flags !== void 0) payload.flags = params.flags;
if (params.files?.length) payload.files = params.files;
return payload;
}
function stripUndefinedFields(value) {
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
}
async function sendDiscordText(rest, channelId, text, replyTo, request, maxLinesPerMessage, components, embeds, chunkMode, silent) {
if (!text.trim()) throw new Error("Message must be non-empty for Discord sends");
const messageReference = replyTo ? {
message_id: replyTo,
fail_if_not_exists: false
} : void 0;
const flags = silent ? SUPPRESS_NOTIFICATIONS_FLAG$1 : void 0;
const chunks = buildDiscordTextChunks(text, {
maxLinesPerMessage,
chunkMode
});
const sendChunk = async (chunk, isFirst) => {
const body = stripUndefinedFields({
...serializePayload(buildDiscordMessagePayload({
text: chunk,
components: resolveDiscordSendComponents({
components,
text: chunk,
isFirst
}),
embeds: resolveDiscordSendEmbeds({
embeds,
isFirst
}),
flags
})),
...messageReference ? { message_reference: messageReference } : {}
});
return await request(() => rest.post(Routes.channelMessages(channelId), { body }), "text");
};
if (chunks.length === 1) return await sendChunk(chunks[0], true);
let last = null;
for (const [index, chunk] of chunks.entries()) last = await sendChunk(chunk, index === 0);
if (!last) throw new Error("Discord send failed (empty chunk result)");
return last;
}
async function sendDiscordMedia(rest, channelId, text, mediaUrl, mediaLocalRoots, replyTo, request, maxLinesPerMessage, components, embeds, chunkMode, silent) {
const media = await loadWebMedia(mediaUrl, { localRoots: mediaLocalRoots });
const chunks = text ? buildDiscordTextChunks(text, {
maxLinesPerMessage,
chunkMode
}) : [];
const caption = chunks[0] ?? "";
const messageReference = replyTo ? {
message_id: replyTo,
fail_if_not_exists: false
} : void 0;
const flags = silent ? SUPPRESS_NOTIFICATIONS_FLAG$1 : void 0;
let fileData;
if (media.buffer instanceof Blob) fileData = media.buffer;
else {
const arrayBuffer = new ArrayBuffer(media.buffer.byteLength);
new Uint8Array(arrayBuffer).set(media.buffer);
fileData = new Blob([arrayBuffer]);
}
const payload = buildDiscordMessagePayload({
text: caption,
components: resolveDiscordSendComponents({
components,
text: caption,
isFirst: true
}),
embeds: resolveDiscordSendEmbeds({
embeds,
isFirst: true
}),
flags,
files: [{
data: fileData,
name: media.fileName ?? "upload"
}]
});
const res = await request(() => rest.post(Routes.channelMessages(channelId), { body: stripUndefinedFields({
...serializePayload(payload),
...messageReference ? { message_reference: messageReference } : {}
}) }), "media");
for (const chunk of chunks.slice(1)) {
if (!chunk.trim()) continue;
await sendDiscordText(rest, channelId, chunk, replyTo, request, maxLinesPerMessage, void 0, void 0, chunkMode, silent);
}
return res;
}
function buildReactionIdentifier(emoji) {
if (emoji.id && emoji.name) return `${emoji.name}:${emoji.id}`;
return emoji.name ?? "";
}
function formatReactionEmoji(emoji) {
return buildReactionIdentifier(emoji);
}
//#endregion
//#region src/discord/send.channels.ts
async function createChannelDiscord(payload, opts = {}) {
const rest = resolveDiscordRest(opts);
const body = { name: payload.name };
if (payload.type !== void 0) body.type = payload.type;
if (payload.parentId) body.parent_id = payload.parentId;
if (payload.topic) body.topic = payload.topic;
if (payload.position !== void 0) body.position = payload.position;
if (payload.nsfw !== void 0) body.nsfw = payload.nsfw;
return await rest.post(Routes.guildChannels(payload.guildId), { body });
}
async function editChannelDiscord(payload, opts = {}) {
const rest = resolveDiscordRest(opts);
const body = {};
if (payload.name !== void 0) body.name = payload.name;
if (payload.topic !== void 0) body.topic = payload.topic;
if (payload.position !== void 0) body.position = payload.position;
if (payload.parentId !== void 0) body.parent_id = payload.parentId;
if (payload.nsfw !== void 0) body.nsfw = payload.nsfw;
if (payload.rateLimitPerUser !== void 0) body.rate_limit_per_user = payload.rateLimitPerUser;
if (payload.archived !== void 0) body.archived = payload.archived;
if (payload.locked !== void 0) body.locked = payload.locked;
if (payload.autoArchiveDuration !== void 0) body.auto_archive_duration = payload.autoArchiveDuration;
if (payload.availableTags !== void 0) body.available_tags = payload.availableTags.map((t) => ({
...t.id !== void 0 && { id: t.id },
name: t.name,
...t.moderated !== void 0 && { moderated: t.moderated },
...t.emoji_id !== void 0 && { emoji_id: t.emoji_id },
...t.emoji_name !== void 0 && { emoji_name: t.emoji_name }
}));
return await rest.patch(Routes.channel(payload.channelId), { body });
}
async function deleteChannelDiscord(channelId, opts = {}) {
await resolveDiscordRest(opts).delete(Routes.channel(channelId));
return {
ok: true,
channelId
};
}
async function moveChannelDiscord(payload, opts = {}) {
const rest = resolveDiscordRest(opts);
const body = [{
id: payload.channelId,
...payload.parentId !== void 0 && { parent_id: payload.parentId },
...payload.position !== void 0 && { position: payload.position }
}];
await rest.patch(Routes.guildChannels(payload.guildId), { body });
return { ok: true };
}
async function setChannelPermissionDiscord(payload, opts = {}) {
const rest = resolveDiscordRest(opts);
const body = { type: payload.targetType };
if (payload.allow !== void 0) body.allow = payload.allow;
if (payload.deny !== void 0) body.deny = payload.deny;
await rest.put(`/channels/${payload.channelId}/permissions/${payload.targetId}`, { body });
return { ok: true };
}
async function removeChannelPermissionDiscord(channelId, targetId, opts = {}) {
await resolveDiscordRest(opts).delete(`/channels/${channelId}/permissions/${targetId}`);
return { ok: true };
}
//#endregion
//#region src/discord/send.emojis-stickers.ts
async function listGuildEmojisDiscord(guildId, opts = {}) {
return await resolveDiscordRest(opts).get(Routes.guildEmojis(guildId));
}
async function uploadEmojiDiscord(payload, opts = {}) {
const rest = resolveDiscordRest(opts);
const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_EMOJI_BYTES);
const contentType = media.contentType?.toLowerCase();
if (!contentType || ![
"image/png",
"image/jpeg",
"image/jpg",
"image/gif"
].includes(contentType)) throw new Error("Discord emoji uploads require a PNG, JPG, or GIF image");
const image = `data:${contentType};base64,${media.buffer.toString("base64")}`;
const roleIds = (payload.roleIds ?? []).map((id) => id.trim()).filter(Boolean);
return await rest.post(Routes.guildEmojis(payload.guildId), { body: {
name: normalizeEmojiName(payload.name, "Emoji name"),
image,
roles: roleIds.length ? roleIds : void 0
} });
}
async function uploadStickerDiscord(payload, opts = {}) {
const rest = resolveDiscordRest(opts);
const media = await loadWebMediaRaw(payload.mediaUrl, DISCORD_MAX_STICKER_BYTES);
const contentType = media.contentType?.toLowerCase();
if (!contentType || ![
"image/png",
"image/apng",
"application/json"
].includes(contentType)) throw new Error("Discord sticker uploads require a PNG, APNG, or Lottie JSON file");
return await rest.post(Routes.guildStickers(payload.guildId), { body: {
name: normalizeEmojiName(payload.name, "Sticker name"),
description: normalizeEmojiName(payload.description, "Sticker description"),
tags: normalizeEmojiName(payload.tags, "Sticker tags"),
files: [{
data: media.buffer,
name: media.fileName ?? "sticker",
contentType
}]
} });
}
//#endregion
//#region src/discord/send.guild.ts
async function fetchMemberInfoDiscord(guildId, userId, opts = {}) {
return await resolveDiscordRest(opts).get(Routes.guildMember(guildId, userId));
}
async function fetchRoleInfoDiscord(guildId, opts = {}) {
return await resolveDiscordRest(opts).get(Routes.guildRoles(guildId));
}
async function addRoleDiscord(payload, opts = {}) {
await resolveDiscordRest(opts).put(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId));
return { ok: true };
}
async function removeRoleDiscord(payload, opts = {}) {
await resolveDiscordRest(opts).delete(Routes.guildMemberRole(payload.guildId, payload.userId, payload.roleId));
return { ok: true };
}
async function fetchChannelInfoDiscord(channelId, opts = {}) {
return await resolveDiscordRest(opts).get(Routes.channel(channelId));
}
async function listGuildChannelsDiscord(guildId, opts = {}) {
return await resolveDiscordRest(opts).get(Routes.guildChannels(guildId));
}
async function fetchVoiceStatusDiscord(guildId, userId, opts = {}) {
return await resolveDiscordRest(opts).get(Routes.guildVoiceState(guildId, userId));
}
async function listScheduledEventsDiscord(guildId, opts = {}) {
return await resolveDiscordRest(opts).get(Routes.guildSchedul