discord-html-transcripts-fix
Version:
A nicely formatted html transcript generator for discord.js. Bugfix fork with support for the latest discord.js and Components v2.
514 lines (480 loc) • 29.6 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = DiscordMessage;
const jsx_runtime_1 = require("react/jsx-runtime");
const discord_js_1 = require("discord.js");
const utils_1 = require("../../utils/utils");
const attachment_1 = require("./attachment");
const components_1 = __importDefault(require("./components"));
const content_1 = __importStar(require("./content"));
const embed_1 = require("./embed");
const reply_1 = __importDefault(require("./reply"));
const systemMessage_1 = __importDefault(require("./systemMessage"));
const FLAG_CROSSPOSTED = 1 << 0;
const FLAG_IS_CROSSPOST = 1 << 1;
const FLAG_SUPPRESS_EMBEDS = 1 << 2;
const FLAG_SUPPRESS_NOTIFICATIONS = 1 << 12;
const FLAG_IS_VOICE_MESSAGE = 1 << 13;
const FLAG_HAS_SNAPSHOT = 1 << 14;
const ACTIVITY_TYPES = { 1: 'Join', 2: 'Spectate', 3: 'Listen', 5: 'Join Request' };
function abbrCount(n) {
if (n < 1000) return String(n);
if (n < 1000000) return (n / 1000).toFixed(n < 10000 ? 1 : 0).replace(/\.0$/, '') + 'K';
return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
}
function t(context, key, fallback) {
const dict = context?.i18n?.[context?.lang] || context?.i18n?.en || {};
return dict[key] || fallback || key;
}
async function DiscordMessage({ message, context }) {
try {
if (message.system)
return (0, jsx_runtime_1.jsx)(systemMessage_1.default, { message: message, context: context });
const isCrossGuildReply = message.reference && message.reference.guildId !== message.guild?.id;
// Data-attrs for client-side filtering (keyword/author/role/date/has-*).
const memberRoleIds = message.member?.roles?.cache
? Array.from(message.member.roles.cache.keys()).join(',')
: '';
const authorName = (message.member?.nickname || message.author?.displayName || message.author?.username || '').toLowerCase();
const ts = message.createdAt instanceof Date ? message.createdAt.getTime() : 0;
const lowerText = (message.content || '').toLowerCase().slice(0, 4096);
const mediaFlags = detectMediaFlags(message);
const hasImage = mediaFlags.hasImage;
const hasEmbed = mediaFlags.hasEmbed;
const hasAttachment = mediaFlags.hasAttachment;
const hasComponentV2 = mediaFlags.hasComponentV2;
const stickers = message.stickers && message.stickers.size > 0
? Array.from(message.stickers.values()).map((s) => ({
id: s.id,
name: s.name,
url: s.url || `https://media.discordapp.net/stickers/${s.id}.png`,
format: s.format,
}))
: [];
const poll = (message.poll && (message.poll.question || (message.poll.answers && message.poll.answers.size))) ? message.poll : null;
const flags = (typeof message.flags?.bitfield === 'number') ? message.flags.bitfield : 0;
const suppressEmbeds = !!(flags & FLAG_SUPPRESS_EMBEDS);
const isVoice = !!(flags & FLAG_IS_VOICE_MESSAGE);
const isCrosspost = !!(flags & FLAG_IS_CROSSPOST);
const isCrossposted = !!(flags & FLAG_CROSSPOSTED);
const isSilent = !!(flags & FLAG_SUPPRESS_NOTIFICATIONS);
const activity = message.activity ? {
type: ACTIVITY_TYPES[message.activity.type] || 'Activity',
partyId: message.activity.partyId,
} : null;
// Forwarded message snapshots (messageSnapshots) — render before the empty body
const snapshots = Array.isArray(message.messageSnapshots) ? message.messageSnapshots : [];
const editedAtIso = message.editedAt instanceof Date ? message.editedAt.toISOString() : null;
const pinned = !!message.pinned;
// Forum applied tags — only on the first message of a forum thread (the post itself)
let appliedTagPills = null;
if (message.channel?.isThread?.() && message.channel.parent?.availableTags && Array.isArray(message.channel.appliedTags) && message.channel.appliedTags.length > 0) {
const isFirstInThread = message.id === message.channel.id; // Forum post: thread id === starter message id
if (isFirstInThread) {
const available = message.channel.parent.availableTags;
appliedTagPills = (0, jsx_runtime_1.jsx)("div", {
className: "dht-applied-tags",
children: message.channel.appliedTags.map((tagId) => {
const tag = available.find((t) => t.id === tagId);
if (!tag) return null;
return (0, jsx_runtime_1.jsxs)("span", { className: "dht-tag-pill", children: [
tag.emoji?.name && (0, jsx_runtime_1.jsx)("span", { className: "dht-tag-emoji", children: tag.emoji.name }),
' ',
tag.name
] }, tagId);
})
});
}
}
// Slash command interaction data is attached to <discord-command> as
// data attributes so the popup can show parameters Discord-style. We
// no longer render a separate "/cmd opt:val by user" line.
const slashData = buildSlashCommandData(message);
// FIX: pass timestamp as ISO string. React would otherwise stringify a
// Date via toString() (locale-dependent like "Tue May 26 2026 …"),
// which skyra's <discord-message> often fails to parse — falling back
// to new Date() == transcript creation time. ISO is round-trip safe.
const createdAtIso = message.createdAt instanceof Date
? message.createdAt.toISOString()
: (typeof message.createdAt === 'string' ? message.createdAt : undefined);
return ((0, jsx_runtime_1.jsxs)("discord-message", {
id: `m-${message.id}`,
timestamp: createdAtIso,
edited: message.editedAt !== null,
server: (isCrossGuildReply || isCrosspost || isCrossposted) ? true : undefined,
highlight: message.mentions.everyone || pinned,
profile: message.author.id,
className: pinned ? 'dht-msg dht-msg-pinned' : 'dht-msg',
"data-author-id": message.author.id,
"data-author-name": authorName,
"data-roles": memberRoleIds,
"data-timestamp": ts ? String(ts) : undefined,
"data-text": lowerText,
"data-pinned": pinned ? 'true' : undefined,
"data-has-image": hasImage ? 'true' : undefined,
"data-has-embed": hasEmbed ? 'true' : undefined,
"data-has-attachment": hasAttachment ? 'true' : undefined,
"data-has-component-v2": hasComponentV2 ? 'true' : undefined,
children: [
appliedTagPills,
(0, jsx_runtime_1.jsx)(reply_1.default, { message: message, context: context }),
message.interaction && ((0, jsx_runtime_1.jsx)("discord-command", {
slot: "reply",
profile: message.interaction.user.id,
command: (slashData && slashData.cmd) || ('/' + message.interaction.commandName),
className: "dht-slash-clickable",
"data-slash-cmd": slashData ? slashData.cmd : ('/' + message.interaction.commandName),
"data-slash-by": slashData ? slashData.by : '',
"data-slash-by-id": slashData ? slashData.byId : message.interaction.user.id,
"data-slash-options": JSON.stringify((slashData && slashData.opts) || []),
})),
isCrossposted && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-crosspost", title: "Published from announcement channel", children: '📣 Published' }),
isCrosspost && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-crosspost", title: "Follows another channel", children: '📡 Follow' }),
isSilent && (0, jsx_runtime_1.jsx)("span", { className: "dht-badge dht-badge-silent", title: "Silent message (no notification)", children: '🔕 Silent' }),
activity && (0, jsx_runtime_1.jsxs)("div", { className: "dht-activity", children: [
(0, jsx_runtime_1.jsx)("span", { className: "dht-activity-icon", children: '🎮' }),
(0, jsx_runtime_1.jsxs)("span", { children: [activity.type, ' invite', activity.partyId ? ` · Party ${activity.partyId.slice(-6)}` : ''] })
] }),
editedAtIso && (0, jsx_runtime_1.jsx)("span", { className: "dht-edit-marker", title: editedAtIso, "data-edit-iso": editedAtIso, "data-i18n": "edited", "data-i18n-params": JSON.stringify({ time: editedAtIso }), children: '(' + t(context, 'edited', 'edited') + ')' }),
Array.isArray(message.editHistory) && message.editHistory.length > 0 && renderEditHistory(message.editHistory, context),
snapshots.length > 0 && renderSnapshots(snapshots, context),
message.content && ((0, jsx_runtime_1.jsx)(content_1.default, { content: message.content, context: Object.assign({}, context, { type: message.webhookId ? content_1.RenderType.WEBHOOK : content_1.RenderType.NORMAL }) })),
(0, jsx_runtime_1.jsx)(attachment_1.Attachments, { message: message, context: context }),
isVoice && renderVoiceIndicator(message, context),
stickers.length > 0 && renderStickers(stickers),
poll && renderPoll(poll, context),
!suppressEmbeds && message.embeds.map((embed, id) => ((0, jsx_runtime_1.jsx)(embed_1.DiscordEmbed, { embed: embed, context: Object.assign({}, context, { index: id, message }) }, id))),
suppressEmbeds && message.embeds.length > 0 && (0, jsx_runtime_1.jsx)("span", { className: "dht-suppressed", "data-i18n": "suppressedEmbeds", children: t(context, 'suppressedEmbeds', '(embeds hidden)') }),
message.components.length > 0 && ((0, jsx_runtime_1.jsx)("discord-attachments", { slot: "components", children: message.components.map((component, id) => ((0, jsx_runtime_1.jsx)(components_1.default, { id: id, component: component, context: context }, id))) })),
message.reactions.cache.size > 0 && ((0, jsx_runtime_1.jsx)("discord-reactions", {
slot: "reactions",
children: Array.from(message.reactions.cache.values()).map((reaction, id) => {
const total = reaction.count ?? 0;
const burstCount = reaction.count_details?.burst || reaction.burst_count || 0;
const burst = burstCount > 0;
const burstColors = Array.isArray(reaction.burst_colors) && reaction.burst_colors.length
? reaction.burst_colors.filter((c) => typeof c === 'string' && /^#[0-9a-fA-F]{3,8}$/.test(c))
: [];
const title = burst
? `${total} reactions (${burstCount} super)`
: `${total} reaction${total !== 1 ? 's' : ''}`;
const style = burst && burstColors.length
? { boxShadow: `inset 0 0 0 2px ${burstColors[0]}` }
: undefined;
return ((0, jsx_runtime_1.jsx)("discord-reaction", {
name: reaction.emoji?.name || ':unknown:',
emoji: (0, utils_1.parseDiscordEmoji)(reaction.emoji) || undefined,
count: abbrCount(total),
title,
"data-burst": burst ? 'true' : undefined,
className: burst ? 'dht-reaction--burst' : undefined,
style,
}, `${message.id}r${id}`));
})
})),
message.hasThread && message.thread && renderThread(message.thread, context),
]
}, message.id));
} catch (err) {
console.warn('[discord-html-transcripts-fix] Render failed', message?.id, err?.message || err);
const safeAuthor = (message?.author?.displayName || message?.author?.username) || 'Unknown';
return ((0, jsx_runtime_1.jsx)("discord-message", { id: `m-${message?.id}`, children: (0, jsx_runtime_1.jsx)("span", { style: { opacity: 0.6 }, children: `[message from ${safeAuthor} failed to render]` }) }, message?.id));
}
}
function renderThread(thread, context) {
const cta = thread.messageCount
? `${thread.messageCount} Message${thread.messageCount > 1 ? 's' : ''}`
: 'View Thread';
const badges = [];
if (thread.archived) badges.push((0, jsx_runtime_1.jsx)("span", { className: "dht-thread-badge dht-thread-badge--archived", "data-i18n": "threadArchived", children: t(context, 'threadArchived', 'Archived') }, 'a'));
if (thread.locked) badges.push((0, jsx_runtime_1.jsx)("span", { className: "dht-thread-badge dht-thread-badge--locked", "data-i18n": "threadLocked", children: t(context, 'threadLocked', 'Locked') }, 'l'));
return (0, jsx_runtime_1.jsxs)("discord-thread", { slot: "thread", name: thread.name, cta, children: [
badges,
thread.lastMessage
? (0, jsx_runtime_1.jsx)("discord-thread-message", { profile: thread.lastMessage.author.id, children: (0, jsx_runtime_1.jsx)(content_1.default, { content: thread.lastMessage.content.length > 128 ? thread.lastMessage.content.substring(0, 125) + '...' : thread.lastMessage.content, context: Object.assign({}, context, { type: content_1.RenderType.REPLY }) }) })
: 'Thread messages not saved.'
] });
}
function renderStickers(stickers) {
return (0, jsx_runtime_1.jsx)("div", {
className: "dht-stickers",
children: stickers.map((s) => {
// Discord sticker formats: 1=PNG, 2=APNG (animated png), 3=Lottie (JSON), 4=GIF
const format = s.format;
const baseUrl = `https://media.discordapp.net/stickers/${s.id}`;
if (format === 3) {
// Lottie — render as iframe to discord's lottie player CDN page, or fallback link
return (0, jsx_runtime_1.jsxs)("div", { className: "dht-sticker dht-sticker--lottie", title: s.name || '', children: [
(0, jsx_runtime_1.jsx)("div", { className: "dht-sticker-placeholder", children: '🎞️' }),
(0, jsx_runtime_1.jsxs)("span", { className: "dht-sticker-name", children: [s.name || 'sticker', ' (Lottie)'] })
] }, s.id);
}
if (format === 4) {
return (0, jsx_runtime_1.jsx)("img", { src: s.url || `${baseUrl}.gif`, alt: s.name || 'sticker', title: s.name || '', className: "dht-sticker" }, s.id);
}
if (format === 2) {
return (0, jsx_runtime_1.jsx)("img", { src: s.url || `${baseUrl}.png`, alt: s.name || 'sticker', title: s.name || '', className: "dht-sticker dht-sticker--apng" }, s.id);
}
return (0, jsx_runtime_1.jsx)("img", { src: s.url || `${baseUrl}.png`, alt: s.name || 'sticker', title: s.name || '', className: "dht-sticker" }, s.id);
})
});
}
function renderPoll(poll, context) {
let totalVotes = 0;
const answers = poll.answers ? Array.from(poll.answers.values()) : [];
for (const a of answers) totalVotes += a.voteCount || 0;
const locale = context?.lang === 'de' ? 'de-DE' : 'en-US';
const expires = poll.expiresAt instanceof Date ? poll.expiresAt.toLocaleString(locale) : '';
return (0, jsx_runtime_1.jsxs)("div", {
className: "dht-poll",
children: [
(0, jsx_runtime_1.jsxs)("div", { className: "dht-poll-q", children: ['📊 ', poll.question?.text || 'Poll'] }),
(0, jsx_runtime_1.jsx)("div", {
className: "dht-poll-answers",
children: answers.map((a, i) => {
const text = a.text ?? a.media?.text ?? `Option ${i + 1}`;
const votes = a.voteCount || 0;
const pct = totalVotes > 0 ? Math.round((votes / totalVotes) * 100) : 0;
return (0, jsx_runtime_1.jsxs)("div", { className: "dht-poll-a", children: [
(0, jsx_runtime_1.jsx)("div", { className: "dht-poll-bar", style: { width: pct + '%' } }),
(0, jsx_runtime_1.jsxs)("div", { className: "dht-poll-line", children: [
(0, jsx_runtime_1.jsx)("span", { children: String(text) }),
(0, jsx_runtime_1.jsxs)("span", { className: "dht-poll-pct", children: [votes, ' (', pct, '%)'] })
] })
] }, i);
})
}),
(0, jsx_runtime_1.jsxs)("div", { className: "dht-poll-foot", children: [totalVotes, ' votes', expires ? ' · ends ' + expires : ''] })
]
});
}
function renderVoiceIndicator(message, context) {
// FIX: discord.js Collection.find takes a predicate but on Map it conflicts; iterate values explicitly.
let audio = null;
if (message.attachments && typeof message.attachments.values === 'function') {
for (const a of message.attachments.values()) {
if (typeof a.contentType === 'string' && a.contentType.startsWith('audio/')) { audio = a; break; }
}
}
const duration = audio?.duration_secs || audio?.durationSecs;
const waveformB64 = audio?.waveform;
let waveformRects = null;
let waveformWidth = 0;
if (typeof waveformB64 === 'string' && waveformB64.length > 0 && waveformB64.length < 16384) {
try {
const buf = Buffer.from(waveformB64, 'base64');
const bars = Array.from(buf).slice(0, 100);
const max = bars.length ? Math.max(1, ...bars) : 1;
waveformWidth = bars.length * 3;
waveformRects = bars.map((b, i) => {
const h = Math.max(2, (b / max) * 20);
return (0, jsx_runtime_1.jsx)("rect", { x: i * 3, y: (22 - h) / 2, width: 2, height: h, rx: 1, fill: "#5865F2" }, i);
});
} catch (_e) { waveformRects = null; }
}
return (0, jsx_runtime_1.jsxs)("div", { className: "dht-voice", children: [
(0, jsx_runtime_1.jsx)("span", { className: "dht-voice-icon", children: '🎤' }),
(0, jsx_runtime_1.jsx)("span", { className: "dht-voice-label", "data-i18n": "voiceMessage", children: t(context, 'voiceMessage', 'Voice message') }),
waveformRects && (0, jsx_runtime_1.jsx)("svg", {
xmlns: "http://www.w3.org/2000/svg",
viewBox: `0 0 ${waveformWidth} 22`,
width: waveformWidth,
height: 22,
className: "dht-voice-wave",
children: waveformRects,
}),
duration ? (0, jsx_runtime_1.jsxs)("span", { className: "dht-voice-dur", children: [Math.round(duration), 's'] }) : null,
] });
}
function toArray(maybeColl) {
if (!maybeColl) return [];
if (Array.isArray(maybeColl)) return maybeColl;
if (typeof maybeColl.values === 'function') return Array.from(maybeColl.values());
if (typeof maybeColl === 'object') return Object.values(maybeColl);
return [];
}
function renderSnapshots(snapshots, context, depth = 0) {
const arr = toArray(snapshots);
if (arr.length === 0 || depth >= 5) return null; // hard-cap at 5 levels
return (0, jsx_runtime_1.jsx)("div", {
className: "dht-forwarded-wrap",
children: arr.map((snap, i) => {
const m = snap.message || snap;
const author = m?.author?.displayName || m?.author?.username || 'Unknown';
const text = m?.content || '';
const attachments = toArray(m?.attachments);
const nested = toArray(m?.messageSnapshots);
const embeds = toArray(m?.embeds);
const stickers = toArray(m?.stickers).map((s) => ({
id: s.id,
name: s.name,
url: s.url || `https://media.discordapp.net/stickers/${s.id}.png`,
format: s.format,
}));
return (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded", "data-depth": depth, children: [
(0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-head", children: [
(0, jsx_runtime_1.jsx)("span", { className: "dht-forwarded-icon", children: '↪' }),
(0, jsx_runtime_1.jsx)("span", { "data-i18n": "forwardedFrom", children: t(context, 'forwardedFrom', 'Forwarded from') }),
(0, jsx_runtime_1.jsx)("strong", { children: ' ' + author }),
] }),
text && (0, jsx_runtime_1.jsx)("div", { className: "dht-forwarded-body", children: (0, jsx_runtime_1.jsx)(content_1.default, { content: text, context: Object.assign({}, context, { type: content_1.RenderType.NORMAL }) }) }),
attachments.length > 0 && (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-attachments", children: [
(0, jsx_runtime_1.jsxs)("strong", { children: ['📎 ', attachments.length, ' attachment', attachments.length !== 1 ? 's' : '', ':'] }),
(0, jsx_runtime_1.jsx)("ul", { children: attachments.map((a, ai) => {
const safeUrl = (0, utils_1.safeHref)(a.url);
return (0, jsx_runtime_1.jsxs)("li", { children: [
a.contentType?.startsWith?.('image/') && a.url
? (0, jsx_runtime_1.jsx)("img", { src: safeUrl !== '#' ? safeUrl : '', alt: a.name || '', style: { maxWidth: '200px', borderRadius: '4px', display: 'block', margin: '4px 0' } })
: (0, jsx_runtime_1.jsxs)("a", { href: safeUrl, target: "_blank", rel: "noreferrer", children: [a.name || 'attachment', a.size ? ` (${Math.round(a.size / 1024)} KB)` : ''] })
] }, ai);
}) })
] }),
stickers.length > 0 && renderStickers(stickers),
embeds.length > 0 && (0, jsx_runtime_1.jsxs)("div", { className: "dht-forwarded-embeds", children: ['📑 ', embeds.length, ' embed', embeds.length !== 1 ? 's' : ''] }),
nested.length > 0 && renderSnapshots(nested, context, depth + 1),
] }, i);
})
});
}
const COMPONENT_TYPE_ACTION_ROW = 1;
const COMPONENT_TYPE_SECTION = 9;
const COMPONENT_TYPE_THUMBNAIL = 11;
const COMPONENT_TYPE_MEDIA_GALLERY = 12;
const COMPONENT_TYPE_FILE = 13;
const COMPONENT_TYPE_CONTAINER = 17;
const FLAG_IS_COMPONENTS_V2 = 1 << 15;
// Walks message.embeds and message.components to determine whether the message
// carries media or V2 containers — used to drive the filter checkboxes
// (data-has-image, data-has-attachment, data-has-embed, data-has-component-v2).
function detectMediaFlags(message) {
const out = { hasImage: false, hasAttachment: false, hasEmbed: false, hasComponentV2: false };
// Direct attachments
if (message.attachments && typeof message.attachments.values === 'function') {
for (const a of message.attachments.values()) {
out.hasAttachment = true;
const ct = typeof a.contentType === 'string' ? a.contentType : '';
if (ct.startsWith('image/') || ct.startsWith('video/')) out.hasImage = true;
}
}
// Classic embeds
if (Array.isArray(message.embeds)) {
for (const e of message.embeds) {
if (!e) continue;
out.hasEmbed = true;
if (e.image || e.thumbnail) out.hasImage = true;
if (e.video && (e.video.url || e.video.proxyURL || e.video.proxy_url)) out.hasImage = true;
}
}
// Components V2 — message-level flag and recursive walk
const flags = (typeof message.flags?.bitfield === 'number') ? message.flags.bitfield : 0;
if (flags & FLAG_IS_COMPONENTS_V2) out.hasComponentV2 = true;
const walk = (comp) => {
if (!comp || typeof comp.type !== 'number') return;
switch (comp.type) {
case COMPONENT_TYPE_CONTAINER:
out.hasComponentV2 = true;
if (Array.isArray(comp.components)) for (const c of comp.components) walk(c);
break;
case COMPONENT_TYPE_SECTION:
if (Array.isArray(comp.components)) for (const c of comp.components) walk(c);
if (comp.accessory) walk(comp.accessory);
break;
case COMPONENT_TYPE_ACTION_ROW:
if (Array.isArray(comp.components)) for (const c of comp.components) walk(c);
break;
case COMPONENT_TYPE_MEDIA_GALLERY:
if (Array.isArray(comp.items) && comp.items.length > 0) out.hasImage = true;
break;
case COMPONENT_TYPE_THUMBNAIL:
if (comp.media && (comp.media.url || comp.media.proxy_url || comp.media.proxyURL)) out.hasImage = true;
break;
case COMPONENT_TYPE_FILE:
out.hasAttachment = true;
if (comp.file && typeof comp.file.url === 'string' && /\.(png|jpe?g|gif|webp|bmp|svg|mp4|webm|mov)(\?|$)/i.test(comp.file.url)) out.hasImage = true;
break;
default:
if (Array.isArray(comp.components)) for (const c of comp.components) walk(c);
break;
}
};
if (Array.isArray(message.components)) for (const c of message.components) walk(c);
return out;
}
function buildSlashCommandData(message) {
const interaction = message?.interaction;
const metadata = message?.interactionMetadata;
const optsRaw = message?.interactionOptions || interaction?.options;
if (!interaction && !metadata) return null;
let cmdParts = [interaction?.commandName || metadata?.name || '?'];
let valueOpts = Array.isArray(optsRaw) ? optsRaw : [];
// Walk subcommand-group → subcommand nesting.
for (let i = 0; i < 2; i++) {
if (valueOpts.length === 1 && valueOpts[0] && (valueOpts[0].type === 1 || valueOpts[0].type === 2)) {
cmdParts.push(valueOpts[0].name);
valueOpts = Array.isArray(valueOpts[0].options) ? valueOpts[0].options : [];
}
}
valueOpts = valueOpts.filter((o) => o && !o.focused);
const triggeredBy = metadata?.user || interaction?.user || null;
const opts = valueOpts.map((o) => ({
name: String(o.name ?? ''),
value: o.value == null ? '' : String(o.value),
type: typeof o.type === 'number' ? o.type : null,
}));
return {
cmd: '/' + cmdParts.filter(Boolean).join(' '),
opts,
by: triggeredBy ? (triggeredBy.displayName || triggeredBy.username || triggeredBy.id || '') : '',
byId: triggeredBy ? (triggeredBy.id || '') : '',
};
}
function renderEditHistory(history, context) {
// Optional feature — host bot may attach `message.editHistory: [{content, editedAt}, …]`
return (0, jsx_runtime_1.jsxs)("details", { className: "dht-edit-history", children: [
(0, jsx_runtime_1.jsx)("summary", { "data-i18n": "editHistoryTitle", children: t(context, 'editHistoryTitle', 'Edit history') }),
(0, jsx_runtime_1.jsx)("ol", { children: history.map((entry, i) => {
const when = entry.editedAt instanceof Date ? entry.editedAt.toISOString() : (typeof entry.editedAt === 'string' ? entry.editedAt : '');
return (0, jsx_runtime_1.jsxs)("li", { children: [
when && (0, jsx_runtime_1.jsxs)("time", { dateTime: when, children: [t(context, 'editHistoryAt', 'at'), ' ', when] }),
(0, jsx_runtime_1.jsx)("div", { children: String(entry.content || '') })
] }, i);
}) })
] });
}
//# sourceMappingURL=message.js.map