import type { DiscordEmbed, DiscordEmbedField } from './types.js';
import type { EmbedParams } from '../tools/types.js';
import { ValidationError } from '../utils/errors.js';
export class EmbedBuilder {
private embed: DiscordEmbed = {};
setTitle(title: string): this {
if (title.length > 256) {
throw new ValidationError('Embed title cannot exceed 256 characters');
}
this.embed.title = title;
return this;
}
setDescription(description: string): this {
if (description.length > 4096) {
throw new ValidationError('Embed description cannot exceed 4096 characters');
}
this.embed.description = description;
return this;
}
setUrl(url: string): this {
this.embed.url = url;
return this;
}
setColor(color: string): this {
const colorValue = this.parseColor(color);
this.embed.color = colorValue;
return this;
}
setTimestamp(useTimestamp: boolean): this {
if (useTimestamp) {
this.embed.timestamp = new Date().toISOString();
}
return this;
}
setFooter(text: string, iconUrl?: string): this {
if (text.length > 2048) {
throw new ValidationError('Footer text cannot exceed 2048 characters');
}
this.embed.footer = { text, icon_url: iconUrl };
return this;
}
setImage(url: string): this {
this.embed.image = { url };
return this;
}
setThumbnail(url: string): this {
this.embed.thumbnail = { url };
return this;
}
setAuthor(name: string, url?: string, iconUrl?: string): this {
if (name.length > 256) {
throw new ValidationError('Author name cannot exceed 256 characters');
}
this.embed.author = { name, url, icon_url: iconUrl };
return this;
}
addField(name: string, value: string, inline = false): this {
this.embed.fields ??= [];
if (this.embed.fields.length >= 25) {
throw new ValidationError('Embeds cannot have more than 25 fields');
}
if (name.length > 256) {
throw new ValidationError('Field name cannot exceed 256 characters');
}
if (value.length > 1024) {
throw new ValidationError('Field value cannot exceed 1024 characters');
}
this.embed.fields.push({ name, value, inline });
return this;
}
addFields(fields: DiscordEmbedField[]): this {
for (const field of fields) {
this.addField(field.name, field.value, field.inline);
}
return this;
}
build(): DiscordEmbed {
// Validate total character count
const totalChars = this.getTotalCharacters();
if (totalChars > 6000) {
throw new ValidationError(
`Embed total characters (${String(totalChars)}) cannot exceed 6000`
);
}
return this.embed;
}
fromParams(params: EmbedParams): this {
if (params.title) this.setTitle(params.title);
if (params.description) this.setDescription(params.description);
if (params.url) this.setUrl(params.url);
if (params.color) this.setColor(params.color);
if (params.timestamp) this.setTimestamp(params.timestamp);
if (params.footer_text) this.setFooter(params.footer_text, params.footer_icon);
if (params.image_url) this.setImage(params.image_url);
if (params.thumbnail_url) this.setThumbnail(params.thumbnail_url);
if (params.author_name) {
this.setAuthor(params.author_name, params.author_url, params.author_icon);
}
return this;
}
private parseColor(color: string): number {
// Handle hex colors
if (color.startsWith('#')) {
const hex = color.slice(1);
return parseInt(hex, 16);
}
// Handle named colors
const namedColors: Record<string, number> = {
red: 0xff0000,
green: 0x00ff00,
blue: 0x0000ff,
yellow: 0xffff00,
orange: 0xffa500,
purple: 0x800080,
pink: 0xffc0cb,
white: 0xffffff,
black: 0x000000,
gray: 0x808080,
blurple: 0x5865f2, // Discord's brand color
};
const lowerColor = color.toLowerCase();
if (lowerColor in namedColors) {
return namedColors[lowerColor];
}
// Try parsing as decimal
const decimal = parseInt(color, 10);
if (!isNaN(decimal) && decimal >= 0 && decimal <= 0xffffff) {
return decimal;
}
throw new ValidationError(
`Invalid color: ${color}. Use hex (#FF0000), named (red), or decimal (16711680)`
);
}
private getTotalCharacters(): number {
let total = 0;
if (this.embed.title) total += this.embed.title.length;
if (this.embed.description) total += this.embed.description.length;
if (this.embed.footer) total += this.embed.footer.text.length;
if (this.embed.author) total += this.embed.author.name.length;
if (this.embed.fields) {
for (const field of this.embed.fields) {
total += field.name.length + field.value.length;
}
}
return total;
}
}