UNPKG

discord-starboard-plus

Version:

Discord Starboard Plus: A clean, maintainable starboard system for Discord.js bots. Features per-guild configuration, TypeScript support. Highlight your community's favorite messages with customizable starboards.

219 lines 9.19 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EmbedBuilderService = void 0; const discord_js_1 = require("discord.js"); /** * Service for building starboard embeds. * Supports both classic embeds and Discord Components V2. */ class EmbedBuilderService { /** * Create a starboard embed for a message. * * - EmbedBuilder class instead of plain objects * - displayAvatarURL with size option (not 'dynamic') * - Proper GIF handling * - Message ID in footer for reliable search * - Optional Components V2 support */ createStarboardEmbed(message, reactionCount, options) { // Use Components V2 if enabled if (options.useComponentsV2) { return this.createComponentsV2Message(message, reactionCount, options); } return this.createClassicEmbed(message, reactionCount, options); } /** * Create classic embed format (default) */ createClassicEmbed(message, reactionCount, options) { // Build content string const channelMention = `<#${message.channel.id}>`; const timestamp = options.showMessageDate ? ` | ${new Date(message.createdTimestamp).toLocaleDateString()}` : ''; const content = `\ud83d\udcab **${reactionCount}** ${channelMention}${timestamp}`; // Build embed using modern EmbedBuilder const embed = new discord_js_1.EmbedBuilder() .setColor(options.embedColor) .setAuthor({ name: message.author.tag, iconURL: message.author.displayAvatarURL({ size: 64 }) }) .setFooter({ text: `ID: ${message.id}` }) // For reliable search .setTimestamp(message.createdAt); // Set message content as description if (message.content) { embed.setDescription(message.content); } // Add jump to message field if (options.jumpToMessage) { embed.addFields({ name: 'Source', value: `[Jump to message](${message.url})` }); } // Process attachments this.processAttachments(embed, message, options); return { content, embeds: [embed] }; } /** * Create Components V2 format (modern Discord UI) * Uses containers, sections, and media galleries for a richer display. * * Components V2 patterns: * - ContainerBuilder with accent color * - TextDisplayBuilder for markdown content * - SectionBuilder with thumbnail accessory * - MediaGalleryBuilder for images * - SeparatorBuilder for visual separation * - MessageFlags.IsComponentsV2 required */ createComponentsV2Message(message, reactionCount, options) { const channelMention = `<#${message.channel.id}>`; const timestamp = options.showMessageDate ? ` | ${new Date(message.createdTimestamp).toLocaleDateString()}` : ''; // Build container with accent color const container = new discord_js_1.ContainerBuilder() .setAccentColor(options.embedColor); // Header with star count and channel container.addTextDisplayComponents(new discord_js_1.TextDisplayBuilder() .setContent(`💫 **${reactionCount}** ${channelMention}${timestamp}`)); // Separator after header container.addSeparatorComponents(new discord_js_1.SeparatorBuilder() .setSpacing(discord_js_1.SeparatorSpacingSize.Small) .setDivider(true)); // Author section with thumbnail (avatar) // Using constructor syntax as per discord.js guide const authorSection = new discord_js_1.SectionBuilder() .addTextDisplayComponents(new discord_js_1.TextDisplayBuilder() .setContent(`**${message.author.tag}**`)) .setThumbnailAccessory(new discord_js_1.ThumbnailBuilder({ media: { url: message.author.displayAvatarURL({ size: 64 }) }, description: message.author.username })); container.addSectionComponents(authorSection); // Add message content if present if (message.content) { container.addTextDisplayComponents(new discord_js_1.TextDisplayBuilder().setContent(message.content)); } // Add media gallery for image/gif attachments const attachments = Array.from(message.attachments.values()) .slice(0, options.maxAttachments); const mediaAttachments = attachments.filter(a => { const type = this.getAttachmentType(a); return type === 'image' || type === 'gif'; }); if (mediaAttachments.length > 0) { const gallery = new discord_js_1.MediaGalleryBuilder(); for (const attachment of mediaAttachments) { // Using .setURL() as per discord.js guide gallery.addItems(new discord_js_1.MediaGalleryItemBuilder() .setURL(attachment.url) .setDescription(attachment.name ?? 'Image')); } container.addMediaGalleryComponents(gallery); } // Add jump to message link and ID in footer if (options.jumpToMessage) { container.addSeparatorComponents(new discord_js_1.SeparatorBuilder() .setSpacing(discord_js_1.SeparatorSpacingSize.Small) .setDivider(false)); container.addTextDisplayComponents(new discord_js_1.TextDisplayBuilder() .setContent(`[Jump to message](${message.url}) • ID: ${message.id}`)); } else { // Always include ID for search functionality container.addTextDisplayComponents(new discord_js_1.TextDisplayBuilder() .setContent(`ID: ${message.id}`)); } return { content: '', embeds: [], components: [container], flags: discord_js_1.MessageFlags.IsComponentsV2 }; } /** * Update the reaction count in existing starboard message content. */ updateReactionCount(currentContent, newCount) { return currentContent.replace(/\ud83d\udcab \*\*\d+\*\*/, `\ud83d\udcab **${newCount}**`); } /** * Process message attachments and add them to the embed. * Handles images, GIFs, and videos appropriately. */ processAttachments(embed, message, options) { const attachments = Array.from(message.attachments.values()) .slice(0, options.maxAttachments); if (attachments.length === 0) return; const categorized = this.categorizeAttachments(attachments); // Set primary image in embed if (categorized.primaryImage) { embed.setImage(categorized.primaryImage.url); } // Add additional media as links in description if (categorized.additionalMedia.length > 0) { const currentDescription = embed.data.description ?? ''; const mediaLinks = categorized.additionalMedia .map(a => a.url) .join('\n'); const newDescription = currentDescription ? `${currentDescription}\n\n${mediaLinks}` : mediaLinks; embed.setDescription(newDescription); } } /** * Categorize attachments into primary image and additional media. */ categorizeAttachments(attachments) { let primaryImage = null; const additionalMedia = []; for (const attachment of attachments) { const type = this.getAttachmentType(attachment); if ((type === 'image' || type === 'gif') && !primaryImage) { primaryImage = attachment; } else if (type === 'image' || type === 'gif' || type === 'video') { additionalMedia.push(attachment); } } return { primaryImage, additionalMedia }; } /** * Determine the type of an attachment. */ getAttachmentType(attachment) { const contentType = attachment.contentType?.toLowerCase() ?? ''; const url = attachment.url.toLowerCase(); // Check for GIF first (can be both in contentType and URL) if (contentType === 'image/gif' || url.endsWith('.gif')) { return 'gif'; } // Check for images if (contentType.startsWith('image/')) { return 'image'; } // Check for videos if (contentType.startsWith('video/')) { return 'video'; } // Fallback: check URL extension for images const imageExtensions = ['.png', '.jpg', '.jpeg', '.webp']; if (imageExtensions.some(ext => url.endsWith(ext))) { return 'image'; } // Fallback: check URL extension for videos const videoExtensions = ['.mp4', '.webm', '.mov']; if (videoExtensions.some(ext => url.endsWith(ext))) { return 'video'; } return 'other'; } } exports.EmbedBuilderService = EmbedBuilderService; //# sourceMappingURL=EmbedBuilderService.js.map