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
JavaScript
;
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